From 1f696d7a90586ccb7ae02320e6f7862a58ca0d7d Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Tue, 30 Jun 2026 23:45:14 +0200 Subject: [PATCH 01/77] feat(provider): Herdr provider (#3837) ## Summary Adds herdr (https://herdr.dev) as an additive, opt-in session backend alongside tmux/ssh/k8s/exec, selected via the existing runtime registry ("herdr" selector). tmux stays the default + fallback. Includes the full runtime.Provider surface + ServerLifecycle, per-rig/town workspace + tab-per-agent placement, idle-gated startup nudge, conformance + live (herdr 0.7.1) + unit tests, a user-facing reference doc, and a README mention. Passes the full Provider conformance suite + a live test against herdr 0.7.1. ## Testing - [x] `make check` - [x] `make check-docs` if docs, navigation, or links changed > **Note:** `docs/` is authored for [docs.gascityhall.com](https://docs.gascityhall.com) (Mintlify), not for direct GitHub viewing. Use extensionless page links (e.g. `/tutorials/01-beads`, not `/tutorials/01-beads.md`). If something looks broken on GitHub but works on the live site, that's intentional. - [x] `make test-integration` if runtime, controller, or workflow behavior changed ## Checklist - [x] Linked an issue, or explained why one is not needed https://github.com/gastownhall/gascity/discussions/3809 - [x] Added or updated tests for behavior changes - [x] Updated docs for user-facing changes - [ ] Called out breaking changes or migration notes - none --------- Co-authored-by: Claude Opus 4.8 (1M context) --- README.md | 10 +- cmd/gc/city_runtime.go | 11 +- cmd/gc/idle_nudge.go | 216 +++++++ cmd/gc/idle_nudge_test.go | 135 +++++ cmd/gc/runtime_registry.go | 12 + docs/docs.json | 3 +- docs/reference/herdr-provider.md | 120 ++++ internal/runtime/herdr-provider-design.md | 170 ++++++ internal/runtime/herdr/capabilities.go | 38 ++ internal/runtime/herdr/client.go | 479 +++++++++++++++ internal/runtime/herdr/conformance_test.go | 33 ++ .../runtime/herdr/effectiveworkdir_test.go | 66 +++ internal/runtime/herdr/placement_test.go | 83 +++ internal/runtime/herdr/provider.go | 547 ++++++++++++++++++ internal/runtime/herdr/provider_live_test.go | 88 +++ .../runtime/herdr/startup_delivery_test.go | 53 ++ internal/runtime/herdr/testenv_import_test.go | 5 + 17 files changed, 2065 insertions(+), 4 deletions(-) create mode 100644 cmd/gc/idle_nudge.go create mode 100644 cmd/gc/idle_nudge_test.go create mode 100644 docs/reference/herdr-provider.md create mode 100644 internal/runtime/herdr-provider-design.md create mode 100644 internal/runtime/herdr/capabilities.go create mode 100644 internal/runtime/herdr/client.go create mode 100644 internal/runtime/herdr/conformance_test.go create mode 100644 internal/runtime/herdr/effectiveworkdir_test.go create mode 100644 internal/runtime/herdr/placement_test.go create mode 100644 internal/runtime/herdr/provider.go create mode 100644 internal/runtime/herdr/provider_live_test.go create mode 100644 internal/runtime/herdr/startup_delivery_test.go create mode 100644 internal/runtime/herdr/testenv_import_test.go diff --git a/README.md b/README.md index dd279e3316..3628dbdff1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ trying to port the entire Town architecture literally. ## What You Get - Declarative city configuration in `city.toml` -- Multiple runtime providers: tmux, subprocess, exec, ACP, and Kubernetes +- Multiple runtime providers: tmux, subprocess, exec, ACP, Kubernetes, and herdr - Beads-backed work tracking, formulas, molecules, waits, and mail - A controller/supervisor loop that reconciles desired state to running state - Packs, overrides, and rig-scoped orchestration for multi-project setups @@ -62,6 +62,12 @@ Gas City requires the following tools on your system. `gc init` and | gh | Optional GitHub gates | — | `brew install gh` | [cli.github.com](https://cli.github.com/) | | claude / codex / gemini | Per provider | — | See provider docs | See provider docs | +tmux is the default session backend **and** the fallback, so it stays required +even if you run agents on another backend. [herdr](https://herdr.dev) is an +optional alternative backend — see +[herdr Session Provider](docs/reference/herdr-provider.md) to enable it +per-agent, per-rig, or city-wide. + The `bd` (beads) provider is the default. To use a file-based store instead (no dolt/bd/flock needed), set `GC_BEADS=file` or add `[beads] provider = "file"` to your `city.toml`. @@ -158,7 +164,7 @@ make docs-dev | Path | What it contains | |---|---| | `cmd/gc/` | CLI entrypoints, controller wiring, runtime assembly, and command handlers | -| `internal/runtime/` | Runtime provider abstraction plus tmux, subprocess, exec, ACP, K8s, and hybrid implementations | +| `internal/runtime/` | Runtime provider abstraction plus tmux, subprocess, exec, ACP, K8s, hybrid, and herdr implementations | | `internal/config/` | `city.toml` schema, validation, composition, packs, patches, and override resolution | | `internal/beads/` | Store abstraction and provider implementations for beads (work, mail, convoys) and waits | | `internal/session/` | Session bead metadata, wait lifecycle helpers, and session identity utilities | diff --git a/cmd/gc/city_runtime.go b/cmd/gc/city_runtime.go index 2ea8c630ee..5ce7c26426 100644 --- a/cmd/gc/city_runtime.go +++ b/cmd/gc/city_runtime.go @@ -2324,7 +2324,16 @@ func (cr *CityRuntime) beadReconcileTick(ctx context.Context, result DesiredStat cr.nudgeDispatchTick(ctx) recordPhase(TraceSiteControllerTickPhase, "bead_reconcile.nudge_dispatch_tick", phaseStart, nil) - // Idle recovery: detect pool sessions stuck at the prompt after + // Idle recovery: re-nudge pool slots that are running but never claimed + // their assigned trigger bead. Gated to runtimes the controller cannot see + // activity for (herdr): tmux self-heals a missed startup nudge through its + // relaunch/respawn path and reports activity, so it neither needs nor runs + // this. See nudgeStalledPoolClaims for the churn-free state machine. + if !cr.sp.Capabilities().CanReportActivity { + phaseStart = time.Now() + nudgeStalledPoolClaims(cr.sp, cr.cfg, sessStore, open, assignedWorkBeads, time.Now(), cr.stdout) + recordPhase(TraceSiteControllerTickPhase, "bead_reconcile.nudge_stalled_pool_claims", phaseStart, nil) + } } // recordReconcileTraceInputs records the per-template baseline, the cycle input diff --git a/cmd/gc/idle_nudge.go b/cmd/gc/idle_nudge.go new file mode 100644 index 0000000000..bf5188c7ba --- /dev/null +++ b/cmd/gc/idle_nudge.go @@ -0,0 +1,216 @@ +package main + +import ( + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/runtime" +) + +// Session-bead metadata keys for the stalled-claim backstop. The state machine +// is PERSISTED on the pool slot's own session bead so it survives a controller +// restart — the in-memory grace map of the reverted #312 nudger did not, which +// is precisely why that one re-nudge-stormed on every restart (test-5il). +const ( + idleClaimNudgeTriggerKey = "idle_claim_nudge_trigger" // trigger bead id last acted on + idleClaimNudgeCountKey = "idle_claim_nudge_count" // nudges delivered for that trigger + idleClaimNudgeAtKey = "idle_claim_nudge_at" // RFC3339 of last attempt / first observation +) + +// Backstop pacing. Deliberately slow: this only rescues a pool slot that was +// handed work but never began it, so a couple of minutes of latency is fine and +// keeps the backstop nowhere near anything that could read as churn. +const ( + idleClaimNudgeGrace = 90 * time.Second // observe-before-first-nudge; lets a normal claim land + idleClaimNudgeBackoff = 3 * time.Minute // between retries when a delivered nudge didn't take + idleClaimNudgeMaxAttempts = 3 // then give up and log (manual re-nudge remains) +) + +// nudgeStalledPoolClaims is a reconcile-tick backstop for runtimes the +// controller is blind to (herdr). It re-delivers the claim nudge to a pool slot +// that is running but whose assigned trigger bead is still UNCLAIMED (open, not +// in_progress). Under herdr the startup nudge can be missed — a freshly-spawned +// slot whose submit-CR was swallowed, or a warm slot that survived a `gc +// restart` and was never re-Started — leaving the polecat idle at its prompt +// with work it never began. tmux self-heals that through its relaunch/respawn +// path (and reports activity), so it is gated out at the call site and never +// runs here. +// +// Churn-free by construction — it inverts every failure mode that got the #312 +// idle-session nudger reverted: +// - Keys on bead state (trigger bead == open), never "idle for N minutes", so +// it is structurally invisible to a working agent: the instant a polecat +// claims, its trigger bead flips to in_progress and stops matching. +// - State is persisted on the session bead, so a restart cannot replay it. +// - Bounded per assignment: observe (grace) → nudge → backoff retries → give +// up. It never spams a tick and never loops forever. +// - Pool slots only. +func nudgeStalledPoolClaims( + sp runtime.Provider, + cfg *config.City, + sessStore beads.SessionStore, + sessionBeads []beads.Bead, + assignedWork []beads.Bead, + now time.Time, + stdout io.Writer, +) { + if sp == nil || cfg == nil || sessStore.Store == nil { + return // hot reconcile path: never panic on a half-built dependency + } + workByID := make(map[string]beads.Bead, len(assignedWork)) + for _, w := range assignedWork { + workByID[w.ID] = w + } + + for i := range sessionBeads { + s := &sessionBeads[i] + if strings.TrimSpace(s.Metadata["pool_managed"]) != "true" { + continue // pool slots only + } + sessName := strings.TrimSpace(s.Metadata["session_name"]) + if sessName == "" || !sp.IsRunning(sessName) { + continue + } + triggerID := strings.TrimSpace(s.Metadata[beadmeta.TriggerBeadIDMetadataKey]) + if triggerID == "" { + continue + } + + // Act only while the trigger bead is genuinely unclaimed. A claimed bead + // is in_progress (or closed) — either way the slot is doing its job and + // must not be disturbed. If the bead is absent from the assigned-work + // snapshot it's been claimed/closed/moved; clear any stale marker. + w, ok := workByID[triggerID] + if !ok || !isUnclaimedTrigger(w, sessName) { + clearIdleClaimMarker(sessStore, s, stdout) + continue + } + + markedTrigger := strings.TrimSpace(s.Metadata[idleClaimNudgeTriggerKey]) + attempts := atoiOr0(s.Metadata[idleClaimNudgeCountKey]) + last := parseRFC3339OrZero(s.Metadata[idleClaimNudgeAtKey]) + + // First observation of this assignment: start the grace clock, don't + // nudge yet — a normal claim almost always lands within the grace window. + if markedTrigger != triggerID { + writeIdleClaimMarker(sessStore, s, triggerID, 0, now, stdout) + continue + } + switch { + case attempts == 0: + if now.Sub(last) < idleClaimNudgeGrace { + continue // still inside the observe-first grace + } + case attempts >= idleClaimNudgeMaxAttempts: + continue // gave up; manual re-nudge is the escape hatch + default: + if now.Sub(last) < idleClaimNudgeBackoff { + continue // waiting out the backoff before the next retry + } + } + + nudge := claimNudgeFor(cfg, *s) + if nudge == "" { + continue + } + if err := sp.Nudge(sessName, runtime.TextContent(nudge)); err != nil { + fmt.Fprintf(stdout, "idle-claim-nudge: %s failed: %v\n", sessName, err) //nolint:errcheck // best-effort + continue + } + fmt.Fprintf(stdout, "idle-claim-nudge: nudged %s to claim %s (attempt %d/%d)\n", sessName, triggerID, attempts+1, idleClaimNudgeMaxAttempts) //nolint:errcheck // best-effort + writeIdleClaimMarker(sessStore, s, triggerID, attempts+1, now, stdout) + } +} + +// isUnclaimedTrigger reports whether the pool slot's trigger bead is still +// waiting to be claimed: status open and not already assigned to this slot +// (a non-empty assignee equal to the session means the claim is mid-flight). +func isUnclaimedTrigger(w beads.Bead, sessName string) bool { + if !strings.EqualFold(strings.TrimSpace(w.Status), "open") { + return false // in_progress / closed / blocked → not ours to nudge + } + if assignee := strings.TrimSpace(w.Assignee); assignee != "" && assignee == sessName { + return false + } + return true +} + +// claimNudgeFor resolves the slot's configured startup nudge (the polecat's +// `gc hook --claim` line) from the agent template behind this session bead. +func claimNudgeFor(cfg *config.City, session beads.Bead) string { + template := normalizedSessionTemplate(session, cfg) + if template == "" { + return "" + } + agent := findAgentByTemplate(cfg, template) + if agent == nil { + return "" + } + return strings.TrimSpace(agent.Nudge) +} + +// writeIdleClaimMarker persists the backstop state machine onto the session +// bead and mirrors it into the in-memory snapshot so the rest of this tick +// reads the just-written values. +func writeIdleClaimMarker(sessStore beads.SessionStore, s *beads.Bead, triggerID string, attempts int, now time.Time, stdout io.Writer) { + kvs := map[string]string{ + idleClaimNudgeTriggerKey: triggerID, + idleClaimNudgeCountKey: strconv.Itoa(attempts), + idleClaimNudgeAtKey: now.UTC().Format(time.RFC3339), + } + if err := sessStore.SetMetadataBatch(s.ID, kvs); err != nil { + fmt.Fprintf(stdout, "idle-claim-nudge: marking %s failed: %v\n", s.ID, err) //nolint:errcheck // best-effort + return + } + if s.Metadata == nil { + s.Metadata = make(map[string]string, len(kvs)) + } + for k, v := range kvs { + s.Metadata[k] = v + } +} + +// clearIdleClaimMarker wipes the marker once the slot no longer has unclaimed +// work, so the next assignment starts its grace clock fresh. No-op (no store +// write) when there is nothing to clear, so steady-state ticks stay silent. +func clearIdleClaimMarker(sessStore beads.SessionStore, s *beads.Bead, stdout io.Writer) { + if s.Metadata[idleClaimNudgeTriggerKey] == "" && + s.Metadata[idleClaimNudgeCountKey] == "" && + s.Metadata[idleClaimNudgeAtKey] == "" { + return + } + kvs := map[string]string{ + idleClaimNudgeTriggerKey: "", + idleClaimNudgeCountKey: "", + idleClaimNudgeAtKey: "", + } + if err := sessStore.SetMetadataBatch(s.ID, kvs); err != nil { + fmt.Fprintf(stdout, "idle-claim-nudge: clearing %s failed: %v\n", s.ID, err) //nolint:errcheck // best-effort + return + } + for k := range kvs { + delete(s.Metadata, k) + } +} + +func atoiOr0(s string) int { + n, err := strconv.Atoi(strings.TrimSpace(s)) + if err != nil { + return 0 + } + return n +} + +func parseRFC3339OrZero(s string) time.Time { + t, err := time.Parse(time.RFC3339, strings.TrimSpace(s)) + if err != nil { + return time.Time{} + } + return t +} diff --git a/cmd/gc/idle_nudge_test.go b/cmd/gc/idle_nudge_test.go new file mode 100644 index 0000000000..139074a96f --- /dev/null +++ b/cmd/gc/idle_nudge_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "context" + "strconv" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/runtime" +) + +func idleClaimTestCfg() *config.City { + return &config.City{Agents: []config.Agent{{ + Name: "polecat", + Nudge: "Run gc hook --claim --json now; if it returns work, execute the claimed formula immediately.", + }}} +} + +func idleClaimPoolSession() beads.Bead { + return beads.Bead{ + ID: "s-1", + Status: "open", + Type: "session", + Metadata: map[string]string{ + "session_name": "worker-1", + "pool_managed": "true", + "template": "polecat", + beadmeta.TriggerBeadIDMetadataKey: "w-1", + }, + } +} + +func runningFake(t *testing.T) *runtime.Fake { + t.Helper() + sp := runtime.NewFake() + if err := sp.Start(context.TODO(), "worker-1", runtime.Config{}); err != nil { + t.Fatalf("fake start: %v", err) + } + return sp +} + +// A slot handed work it never claimed (trigger bead still open) is observed on +// the first tick (grace), then nudged once the grace elapses. +func TestNudgeStalledPoolClaims_NudgesAfterGrace(t *testing.T) { + sp := runningFake(t) + cfg := idleClaimTestCfg() + sessions := []beads.Bead{idleClaimPoolSession()} + work := []beads.Bead{{ID: "w-1", Status: "open"}} // unclaimed + store := beads.SessionStore{Store: beads.NewMemStoreFrom(0, sessions, nil)} + base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + var out bytes.Buffer + + // First tick: observe only — start the grace clock, no nudge. + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base, &out) + if out.Len() != 0 { + t.Fatalf("first tick should not nudge (grace): %q", out.String()) + } + if got := sessions[0].Metadata[idleClaimNudgeTriggerKey]; got != "w-1" { + t.Fatalf("expected marker trigger w-1, got %q", got) + } + + // Past grace: nudge, and bump the attempt count. + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base.Add(idleClaimNudgeGrace+time.Second), &out) + if !bytes.Contains(out.Bytes(), []byte("nudged worker-1 to claim w-1")) { + t.Fatalf("expected nudge past grace, got: %q", out.String()) + } + if got := sessions[0].Metadata[idleClaimNudgeCountKey]; got != "1" { + t.Fatalf("expected attempt count 1, got %q", got) + } +} + +// The instant a slot claims (trigger bead flips to in_progress) it must never be +// touched — this is the inversion that the reverted #312 nudger got wrong. +func TestNudgeStalledPoolClaims_NeverTouchesWorkingSlot(t *testing.T) { + sp := runningFake(t) + cfg := idleClaimTestCfg() + sessions := []beads.Bead{idleClaimPoolSession()} + work := []beads.Bead{{ID: "w-1", Status: "in_progress", Assignee: "worker-1"}} + store := beads.SessionStore{Store: beads.NewMemStoreFrom(0, sessions, nil)} + base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + var out bytes.Buffer + + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base, &out) + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base.Add(time.Hour), &out) + if out.Len() != 0 { + t.Fatalf("must not nudge a working slot: %q", out.String()) + } + if got := sessions[0].Metadata[idleClaimNudgeTriggerKey]; got != "" { + t.Fatalf("marker should stay clear for a claimed bead, got %q", got) + } +} + +// After the attempt cap is reached the backstop gives up — bounded, never an +// every-tick loop. +func TestNudgeStalledPoolClaims_GivesUpAtCap(t *testing.T) { + sp := runningFake(t) + cfg := idleClaimTestCfg() + base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + s := idleClaimPoolSession() + s.Metadata[idleClaimNudgeTriggerKey] = "w-1" + s.Metadata[idleClaimNudgeCountKey] = strconv.Itoa(idleClaimNudgeMaxAttempts) + s.Metadata[idleClaimNudgeAtKey] = base.Format(time.RFC3339) + sessions := []beads.Bead{s} + work := []beads.Bead{{ID: "w-1", Status: "open"}} + store := beads.SessionStore{Store: beads.NewMemStoreFrom(0, sessions, nil)} + var out bytes.Buffer + + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base.Add(time.Hour), &out) + if out.Len() != 0 { + t.Fatalf("must not nudge past the attempt cap: %q", out.String()) + } +} + +// A non-pool session is ignored entirely. +func TestNudgeStalledPoolClaims_SkipsNonPool(t *testing.T) { + sp := runningFake(t) + cfg := idleClaimTestCfg() + s := idleClaimPoolSession() + delete(s.Metadata, "pool_managed") + sessions := []beads.Bead{s} + work := []beads.Bead{{ID: "w-1", Status: "open"}} + store := beads.SessionStore{Store: beads.NewMemStoreFrom(0, sessions, nil)} + base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + var out bytes.Buffer + + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base, &out) + nudgeStalledPoolClaims(sp, cfg, store, sessions, work, base.Add(time.Hour), &out) + if out.Len() != 0 { + t.Fatalf("must not touch a non-pool session: %q", out.String()) + } +} diff --git a/cmd/gc/runtime_registry.go b/cmd/gc/runtime_registry.go index 8bab9d3f49..26ba22fe62 100644 --- a/cmd/gc/runtime_registry.go +++ b/cmd/gc/runtime_registry.go @@ -9,6 +9,7 @@ import ( "github.com/gastownhall/gascity/internal/runtime" sessionacp "github.com/gastownhall/gascity/internal/runtime/acp" sessionexec "github.com/gastownhall/gascity/internal/runtime/exec" + sessionherdr "github.com/gastownhall/gascity/internal/runtime/herdr" sessionk8s "github.com/gastownhall/gascity/internal/runtime/k8s" "github.com/gastownhall/gascity/internal/runtime/registry" sessionssh "github.com/gastownhall/gascity/internal/runtime/ssh" @@ -76,6 +77,17 @@ func buildRuntimeRegistry() *registry.Registry { must(r.Register("k8s", func(_ string, _ config.SessionConfig, _, _ string) (runtime.Provider, error) { return sessionk8s.NewSeamBacked() })) + // herdr (https://herdr.dev): opt-in multiplexer backend. One shared herdr + // session-server per city; one workspace per rig/town, one tab per agent. + // tmux stays the default; select "herdr" per-agent/city to pilot it. See + // internal/runtime/herdr-provider-design.md. + must(r.Register("herdr", func(_ string, _ config.SessionConfig, cityName, cityPath string) (runtime.Provider, error) { + session := cityName + if session == "" { + session = "default" + } + return sessionherdr.New(session, providerStateDir("herdr", cityPath), cityPath), nil + })) must(r.Register("hybrid", func(_ string, sc config.SessionConfig, cityName, cityPath string) (runtime.Provider, error) { return newHybridProvider(sc, cityName, cityPath) })) diff --git a/docs/docs.json b/docs/docs.json index ad4b851323..a609887348 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -233,7 +233,8 @@ "reference/system-packs", "reference/exec-session-provider", "reference/exec-beads-provider", - "reference/tmux-agent-slice" + "reference/tmux-agent-slice", + "reference/herdr-provider" ] }, { diff --git a/docs/reference/herdr-provider.md b/docs/reference/herdr-provider.md new file mode 100644 index 0000000000..d86ad069d0 --- /dev/null +++ b/docs/reference/herdr-provider.md @@ -0,0 +1,120 @@ +--- +title: "herdr Session Provider" +--- + +[herdr](https://herdr.dev) is a terminal workspace manager built for AI coding +agents. Gas City ships a native **herdr** session-provider backend as an +**opt-in** alternative to tmux: one shared herdr session-server per city, one +workspace per rig (and one for the town), and one tab per agent. tmux stays the +default backend and the fallback — herdr is additive, selected through the same +runtime-selection setting that picks tmux, k8s, ssh, or exec. + +## Prerequisites + +Install the `herdr` binary and make sure it is on `PATH`: + +```bash +herdr --version # the provider is verified against herdr 0.7.1+ +``` + +The backend is registered as a builtin runtime name (`herdr`) — no pack or +`[runtimes.*]` declaration is needed. If the binary is missing, sessions +selected onto herdr fail to start; install it before flipping the selector. + +## Enabling herdr + +`herdr` is selected with the same runtime selector used for every other +backend, at one of three scopes. + +### City default + +Set the session provider in `city.toml`: + +```toml +[session] +provider = "herdr" +``` + +Every agent the city starts then runs under herdr, except agents pinned to +another backend by a patch (see below). + +### Per-agent / per-rig + +Override the backend for a single agent — or every agent in a rig — with an +agent patch. The override field is `session`: + +```toml +# one agent by name +[[patches.agent]] +name = "dog-1" +session = "herdr" + +# every agent in a rig (match by the rig's working dir) +[[patches.agent]] +dir = "webapp" +session = "herdr" +``` + +A per-agent `session` override wins over the `[session]` city default, so you +can run the whole city on herdr while pinning specific agents to tmux (or the +reverse — keep tmux as the default and pilot herdr on one agent). + +### Environment (one-off) + +For a quick local trial without editing config, export the selector: + +```bash +export GC_SESSION=herdr +gc start +``` + +`GC_SESSION` overrides the effective provider name for that process, the same +way it selects `exec: - + diff --git a/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/types.gen.ts b/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/types.gen.ts index d99df21bc4..e999e0a040 100644 --- a/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/types.gen.ts +++ b/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/types.gen.ts @@ -1964,6 +1964,12 @@ export type ProviderCreateInputBody = { * Provider name. */ name: string; + /** + * Provider option defaults (e.g. model). Keys are merged on update. + */ + option_defaults?: { + [key: string]: string; + }; /** * Options schema merge mode across inheritance chain. */ @@ -2170,6 +2176,12 @@ export type ProviderUpdateInputBody = { env?: { [key: string]: string; }; + /** + * Provider option defaults (e.g. model). Keys are merged on update. + */ + option_defaults?: { + [key: string]: string; + }; /** * Options schema merge mode across inheritance chain. */ diff --git a/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/zod.gen.ts b/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/zod.gen.ts index e59138b48b..8b83dbd4e9 100644 --- a/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/zod.gen.ts +++ b/internal/api/dashboardspa/web/shared/src/generated/gc-supervisor-client/zod.gen.ts @@ -965,6 +965,7 @@ export const zProviderCreateInputBody = z.object({ display_name: z.string().optional(), env: z.record(z.string(), z.string()).optional(), name: z.string().min(1), + option_defaults: z.record(z.string(), z.string()).optional(), options_schema_merge: z.string().optional(), prompt_flag: z.string().optional(), prompt_mode: z.string().optional(), @@ -1092,6 +1093,7 @@ export const zProviderUpdateInputBody = z.object({ command: z.string().optional(), display_name: z.string().optional(), env: z.record(z.string(), z.string()).optional(), + option_defaults: z.record(z.string(), z.string()).optional(), options_schema_merge: z.string().optional(), prompt_flag: z.string().optional(), prompt_mode: z.string().optional(), diff --git a/internal/api/fake_state_test.go b/internal/api/fake_state_test.go index 90b7aa3227..2685ecbc86 100644 --- a/internal/api/fake_state_test.go +++ b/internal/api/fake_state_test.go @@ -389,6 +389,14 @@ func (f *fakeMutatorState) UpdateProvider(name string, patch ProviderUpdate) err if patch.OptionsSchema != nil { spec.OptionsSchema = append([]config.ProviderOption(nil), patch.OptionsSchema...) } + if len(patch.OptionDefaults) > 0 { + if spec.OptionDefaults == nil { + spec.OptionDefaults = make(map[string]string, len(patch.OptionDefaults)) + } + for k, v := range patch.OptionDefaults { + spec.OptionDefaults[k] = v + } + } f.cfg.Providers[name] = spec return nil } diff --git a/internal/api/genclient/client_gen.go b/internal/api/genclient/client_gen.go index 29f24c09dd..33617d5dd3 100644 --- a/internal/api/genclient/client_gen.go +++ b/internal/api/genclient/client_gen.go @@ -2252,6 +2252,9 @@ type ProviderCreateInputBody struct { // Name Provider name. Name string `json:"name"` + // OptionDefaults Provider option defaults (e.g. model). Keys are merged on update. + OptionDefaults *map[string]string `json:"option_defaults,omitempty"` + // OptionsSchemaMerge Options schema merge mode across inheritance chain. OptionsSchemaMerge *string `json:"options_schema_merge,omitempty"` @@ -2424,6 +2427,9 @@ type ProviderUpdateInputBody struct { // Env Environment variables. Env *map[string]string `json:"env,omitempty"` + // OptionDefaults Provider option defaults (e.g. model). Keys are merged on update. + OptionDefaults *map[string]string `json:"option_defaults,omitempty"` + // OptionsSchemaMerge Options schema merge mode across inheritance chain. OptionsSchemaMerge *string `json:"options_schema_merge,omitempty"` diff --git a/internal/api/handler_provider_crud_test.go b/internal/api/handler_provider_crud_test.go index ace9d75aec..7b0c427282 100644 --- a/internal/api/handler_provider_crud_test.go +++ b/internal/api/handler_provider_crud_test.go @@ -61,6 +61,59 @@ func TestHandleProviderCreate_PersistsACPTransportOverrides(t *testing.T) { } } +func TestHandleProviderCreate_PersistsOptionDefaults(t *testing.T) { + fs := newFakeMutatorState(t) + srv := New(fs) + h := newTestCityHandlerWith(t, fs, srv) + + req := newPostRequest(cityURL(fs, "/providers"), strings.NewReader( + `{"name":"custom-model","command":"custom","option_defaults":{"model":"x"}}`)) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusCreated { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusCreated, rec.Body.String()) + } + + spec, ok := fs.cfg.Providers["custom-model"] + if !ok { + t.Fatal("provider custom-model not created") + } + if spec.OptionDefaults["model"] != "x" { + t.Fatalf("OptionDefaults[model] = %q, want %q", spec.OptionDefaults["model"], "x") + } +} + +func TestHandleProviderUpdate_OptionDefaultsMergeNotReplace(t *testing.T) { + fs := newFakeMutatorState(t) + fs.cfg.Providers["custom"] = config.ProviderSpec{ + Command: "custom", + OptionDefaults: map[string]string{"model": "x", "permission_mode": "unrestricted"}, + } + srv := New(fs) + h := newTestCityHandlerWith(t, fs, srv) + + // Edit only the model; permission_mode must survive. + req := httptest.NewRequest(http.MethodPatch, cityURL(fs, "/provider/custom"), strings.NewReader( + `{"option_defaults":{"model":"y"}}`)) + req.Header.Set("X-GC-Request", "true") + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + spec := fs.cfg.Providers["custom"] + if spec.OptionDefaults["model"] != "y" { + t.Fatalf("OptionDefaults[model] = %q, want %q", spec.OptionDefaults["model"], "y") + } + if spec.OptionDefaults["permission_mode"] != "unrestricted" { + t.Fatalf("OptionDefaults[permission_mode] = %q, want %q (merge, not replace)", + spec.OptionDefaults["permission_mode"], "unrestricted") + } +} + func TestHandleProviderUpdate_UpdatesInheritanceFields(t *testing.T) { fs := newFakeMutatorState(t) fs.cfg.Providers["custom"] = fs.cfg.Providers["test-agent"] diff --git a/internal/api/huma_handlers_providers.go b/internal/api/huma_handlers_providers.go index 9fd3132b3a..ecfc82532e 100644 --- a/internal/api/huma_handlers_providers.go +++ b/internal/api/huma_handlers_providers.go @@ -154,6 +154,9 @@ func (s *Server) humaHandleProviderCreate(_ context.Context, input *ProviderCrea if input.Body.OptionsSchemaMerge != nil { spec.OptionsSchemaMerge = *input.Body.OptionsSchemaMerge } + if input.Body.OptionDefaults != nil { + spec.OptionDefaults = input.Body.OptionDefaults + } if err := sm.CreateProvider(input.Body.Name, spec); err != nil { return nil, mutationError(err) @@ -183,6 +186,7 @@ func (s *Server) humaHandleProviderUpdate(_ context.Context, input *ProviderUpda ReadyDelayMs: input.Body.ReadyDelayMs, Env: input.Body.Env, OptionsSchemaMerge: input.Body.OptionsSchemaMerge, + OptionDefaults: input.Body.OptionDefaults, } if input.Body.Base != nil { patch.Base = &input.Body.Base diff --git a/internal/api/huma_types_providers.go b/internal/api/huma_types_providers.go index 6b62b235b7..b13dc5ffa9 100644 --- a/internal/api/huma_types_providers.go +++ b/internal/api/huma_types_providers.go @@ -70,6 +70,7 @@ type ProviderCreateInput struct { ReadyDelayMs int `json:"ready_delay_ms,omitempty" doc:"Milliseconds to wait before probing readiness."` Env map[string]string `json:"env,omitempty" doc:"Environment variables."` OptionsSchemaMerge *string `json:"options_schema_merge,omitempty" doc:"Options schema merge mode across inheritance chain."` + OptionDefaults map[string]string `json:"option_defaults,omitempty" doc:"Provider option defaults (e.g. model). Keys are merged on update."` } } @@ -90,6 +91,7 @@ type ProviderUpdateInput struct { ReadyDelayMs *int `json:"ready_delay_ms,omitempty" doc:"Milliseconds to wait before probing readiness."` Env map[string]string `json:"env,omitempty" doc:"Environment variables."` OptionsSchemaMerge *string `json:"options_schema_merge,omitempty" doc:"Options schema merge mode across inheritance chain."` + OptionDefaults map[string]string `json:"option_defaults,omitempty" doc:"Provider option defaults (e.g. model). Keys are merged on update."` } } diff --git a/internal/api/openapi.json b/internal/api/openapi.json index 2f05c27bda..8eb312b04d 100644 --- a/internal/api/openapi.json +++ b/internal/api/openapi.json @@ -5318,6 +5318,13 @@ "minLength": 1, "type": "string" }, + "option_defaults": { + "additionalProperties": { + "type": "string" + }, + "description": "Provider option defaults (e.g. model). Keys are merged on update.", + "type": "object" + }, "options_schema_merge": { "description": "Options schema merge mode across inheritance chain.", "type": "string" @@ -5841,6 +5848,13 @@ "description": "Environment variables.", "type": "object" }, + "option_defaults": { + "additionalProperties": { + "type": "string" + }, + "description": "Provider option defaults (e.g. model). Keys are merged on update.", + "type": "object" + }, "options_schema_merge": { "description": "Options schema merge mode across inheritance chain.", "type": "string" diff --git a/internal/api/state.go b/internal/api/state.go index 2804b5f1c0..adda55313c 100644 --- a/internal/api/state.go +++ b/internal/api/state.go @@ -215,6 +215,7 @@ type ProviderUpdate struct { Env map[string]string // nil = not set, non-nil = additive merge OptionsSchemaMerge *string OptionsSchema []config.ProviderOption // nil = not set, non-nil = replace + OptionDefaults map[string]string // nil = not set, non-nil = additive merge } // RawConfigProvider is optionally implemented by State to provide the diff --git a/internal/configedit/configedit.go b/internal/configedit/configedit.go index 689f0bb8d4..3be693bc7d 100644 --- a/internal/configedit/configedit.go +++ b/internal/configedit/configedit.go @@ -1251,6 +1251,7 @@ type ProviderUpdate struct { Env map[string]string // nil = not set, non-nil = additive merge OptionsSchemaMerge *string OptionsSchema []config.ProviderOption // nil = not set, non-nil = replace + OptionDefaults map[string]string // nil = not set, non-nil = additive merge } // CreateProvider adds a new city-level provider to the config. @@ -1268,6 +1269,23 @@ func (e *Editor) CreateProvider(name string, spec config.ProviderSpec) error { }) } +// mergeStringMapInto additively merges src into dst, lazily allocating dst +// when it is nil. Keys present in src overwrite those in dst; an empty src +// leaves dst unchanged. It returns the (possibly newly allocated) destination +// so callers can assign it back onto the target field. +func mergeStringMapInto(dst, src map[string]string) map[string]string { + if len(src) == 0 { + return dst + } + if dst == nil { + dst = make(map[string]string, len(src)) + } + for k, v := range src { + dst[k] = v + } + return dst +} + // UpdateProvider partially updates an existing city-level provider. // Returns an error if the provider is not found in the raw config // (builtin-only providers cannot be updated directly — use patches). @@ -1316,20 +1334,14 @@ func (e *Editor) UpdateProvider(name string, patch ProviderUpdate) error { if patch.ReadyDelayMs != nil { spec.ReadyDelayMs = *patch.ReadyDelayMs } - if len(patch.Env) > 0 { - if spec.Env == nil { - spec.Env = make(map[string]string, len(patch.Env)) - } - for k, v := range patch.Env { - spec.Env[k] = v - } - } + spec.Env = mergeStringMapInto(spec.Env, patch.Env) if patch.OptionsSchemaMerge != nil { spec.OptionsSchemaMerge = *patch.OptionsSchemaMerge } if patch.OptionsSchema != nil { spec.OptionsSchema = append([]config.ProviderOption(nil), patch.OptionsSchema...) } + spec.OptionDefaults = mergeStringMapInto(spec.OptionDefaults, patch.OptionDefaults) cfg.Providers[name] = spec return nil }) diff --git a/internal/configedit/configedit_test.go b/internal/configedit/configedit_test.go index 8ad8bb7bde..2e05923ee2 100644 --- a/internal/configedit/configedit_test.go +++ b/internal/configedit/configedit_test.go @@ -2317,6 +2317,118 @@ func TestUpdateProvider_PreservesUnchangedFields(t *testing.T) { } } +// cityWithModelProvider returns a city.toml with a custom provider whose +// options_schema declares model + permission_mode, so option_defaults for +// those keys pass schema validation. +func cityWithModelProvider() string { + return `[workspace] +name = "test-city" + +[[agent]] +name = "mayor" +provider = "custom" + +[providers.custom] +command = "custom-cli" + +[[providers.custom.options_schema]] +key = "model" +label = "Model" +type = "select" +default = "x" + + [[providers.custom.options_schema.choices]] + value = "x" + label = "X" + flag_args = ["--model", "x"] + + [[providers.custom.options_schema.choices]] + value = "y" + label = "Y" + flag_args = ["--model", "y"] + +[[providers.custom.options_schema]] +key = "permission_mode" +label = "Permission Mode" +type = "select" +default = "plan" + + [[providers.custom.options_schema.choices]] + value = "plan" + label = "Plan" + flag_args = ["--permission-mode", "plan"] + + [[providers.custom.options_schema.choices]] + value = "unrestricted" + label = "Unrestricted" + flag_args = ["--dangerously-skip-permissions"] +` +} + +// TestCreateProvider_OptionDefaults verifies a create with an option_defaults +// map (e.g. model) round-trips to the provider's TOML. +func TestCreateProvider_OptionDefaults(t *testing.T) { + dir := t.TempDir() + path := writeTOML(t, dir, minimalCity()) + ed := configedit.NewEditor(fsys.OSFS{}, path) + + spec := config.ProviderSpec{ + Command: "custom-cli", + OptionsSchema: []config.ProviderOption{{ + Key: "model", + Label: "Model", + Type: "select", + Choices: []config.OptionChoice{ + {Value: "x", Label: "X", FlagArgs: []string{"--model", "x"}}, + {Value: "y", Label: "Y", FlagArgs: []string{"--model", "y"}}, + }, + }}, + OptionDefaults: map[string]string{"model": "x"}, + } + if err := ed.CreateProvider("myprov", spec); err != nil { + t.Fatalf("CreateProvider: %v", err) + } + + cfg := readTOML(t, path) + got := cfg.Providers["myprov"] + if got.OptionDefaults["model"] != "x" { + t.Errorf("OptionDefaults[model] = %q, want %q", got.OptionDefaults["model"], "x") + } +} + +// TestUpdateProvider_OptionDefaultsMergeNotReplace verifies that updating +// option_defaults merges keys: a model-only edit changes model while leaving +// a pre-existing unrelated option-default key untouched. +func TestUpdateProvider_OptionDefaultsMergeNotReplace(t *testing.T) { + dir := t.TempDir() + path := writeTOML(t, dir, cityWithModelProvider()) + ed := configedit.NewEditor(fsys.OSFS{}, path) + + // Seed a provider with two option defaults. + if err := ed.UpdateProvider("custom", configedit.ProviderUpdate{ + OptionDefaults: map[string]string{"model": "x", "permission_mode": "unrestricted"}, + }); err != nil { + t.Fatalf("seed UpdateProvider: %v", err) + } + + // Edit only model; permission_mode must survive. + if err := ed.UpdateProvider("custom", configedit.ProviderUpdate{ + OptionDefaults: map[string]string{"model": "y"}, + }); err != nil { + t.Fatalf("UpdateProvider: %v", err) + } + + cfg := readTOML(t, path) + got := cfg.Providers["custom"] + if got.OptionDefaults["model"] != "y" { + t.Errorf("OptionDefaults[model] = %q, want %q", got.OptionDefaults["model"], "y") + } + if got.OptionDefaults["permission_mode"] != "unrestricted" { + t.Errorf("OptionDefaults[permission_mode] = %q, want %q (merge, not replace)", + got.OptionDefaults["permission_mode"], "unrestricted") + } +} + func TestDeleteProvider(t *testing.T) { dir := t.TempDir() path := writeTOML(t, dir, cityWithProvider()) From 66a3ad053127856449a7371a95cec2e019c72664 Mon Sep 17 00:00:00 2001 From: iwata-1116 Date: Wed, 1 Jul 2026 18:02:04 +0900 Subject: [PATCH 03/77] docs: fix cancellation comment spelling (#3852) --- cmd/gc/session_wake.go | 4 ++-- cmd/gc/soft_reload.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/gc/session_wake.go b/cmd/gc/session_wake.go index 6407f39e55..5e927cff91 100644 --- a/cmd/gc/session_wake.go +++ b/cmd/gc/session_wake.go @@ -517,7 +517,7 @@ func advanceSessionDrainsWithSessionsTraced( } } - // Cancelation check: if wake reasons reappeared, cancel the in-memory + // Cancellation check: if wake reasons reappeared, cancel the in-memory // drain. Orphaned, suspended, and ordinary config-drift drains are not // canceled here. if drainReasonCancelable(ds.reason) { @@ -569,7 +569,7 @@ func advanceSessionDrainsWithSessionsTraced( } } - // Pending-interaction guards and wake-based cancelation run before this + // Pending-interaction guards and wake-based cancellation run before this // timeout path. Preserve that ordering if this block is refactored. if clk.Now().After(ds.deadline) { // Drain timed out — force stop. diff --git a/cmd/gc/soft_reload.go b/cmd/gc/soft_reload.go index 10be899fd2..630fbda77f 100644 --- a/cmd/gc/soft_reload.go +++ b/cmd/gc/soft_reload.go @@ -78,7 +78,7 @@ func formatSoftReloadFailedSessions(names []string) string { // The hash computation uses sessionCoreConfigForHash, the same canonical // reconciler drift-hash helper used by live and asleep drift detection. // -// Returns accepted-session, failed-session, stale-drain-cancelation, and +// Returns accepted-session, failed-session, stale-drain-cancellation, and // empty-desired-state diagnostics for the controller reply. func acceptConfigDriftAcrossSessions( sessFront *session.InfoStore, From 17b016372b965e4a6409441efab261cc6d3e003f Mon Sep 17 00:00:00 2001 From: Julian Knutsen Date: Wed, 1 Jul 2026 03:11:48 -0700 Subject: [PATCH 04/77] fix(init): seed gascity role pack so fresh cities can launch built-in formulas (#3832) (#3841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary A freshly `gc init`'d **gascity** city couldn't run a built-in formula through the Mayor. Launching `build-from-requirements` failed with: ``` gc sling: agent "gc.run-operator" not found in city.toml ``` ## Root cause The `gascity-packs` registry splits its content into two packs at the same commit: - `gascity/` — **formulas + skills, no agents** (`build-from-requirements`, …) - `gascity/roles/` — the role agents (`gc-roles`: `run-operator`, `requirements-planner`, …), every one `scope = "rig"` and **providerless** (they inherit the workspace/rig default provider; #3831's patch overlay overrides one per-role). `gc init`'s gascity template (`config.GascityCityWithProviders`) imported **only** the city-scope formulas pack and seeded **no default rig imports**, so formulas resolved (the Mayor could launch them) but their step coordinators (`gc.run_target = "gc.run-operator"`) pointed at role agents that were never installed. `resolveAgentIdentity` has no default-agent fallback — correctly, since ZERO-hardcoded-roles forbids the SDK naming a default role — so it hard-errored. This is not a provider problem: provider resolution (`agent.Provider → workspace.provider`) happens *after* an agent is resolved, so it never gets reached. The proven precedent is the **gastown** template, which already sets `DefaultRigImports` for its role pack; the gascity template simply lacked it. ## Fix **1. Seed the roles at init (the fix).** `GascityCityWithProviders` now seeds the `gc-roles` subpack as a default rig import bound **`gc`** (so the formula's `gc.*` targets resolve), pinned to the **same commit** as the formulas pack via the new `PublicGascityRolesPackSource`. Every rig added to a gascity city now inherits the role agents the formulas coordinate — matching the manual `gc import add …/gascity/roles --name gc --rig && gc import install` workaround, but automatic. **2. Actionable remediation (safety net).** When a missing target names a *declared-but-uninstalled* pack import, `gc sling`/`gc agent`/`gc session` now point at `gc import install` (with the declared source) instead of only a "did you mean?" hint. The hint is derived from the city's own declared imports — no role or pack name in Go, preserving ZERO-hardcoded-roles. It is suppressed when an agent with that binding is already composed (then the miss is a typo, not a missing pack). ``` gc sling: agent "gc.run-operator" not found in city.toml the "gc" pack is imported (https://github.com/gastownhall/gascity-packs/tree/main/gascity/roles) but its agents are not installed here; run `gc import install` ``` ## Verification - New unit/init tests; updated the `TestDoInitWritesExpectedTOML` golden to the new city.toml (now carries `[defaults.rig.imports.gc]`). - `go build` · `go vet ./...` · `gofmt` · `lint-changed` (0 issues) · `internal/config` suite · `cmd/gc` init/template/import/wizard/suggest/resolution suites · pre-push `make test-fast-parallel` — all green. - Real CLI E2E: `gc init --template gascity` writes `[defaults.rig.imports.gc] → …/gascity/roles @ 3b3b89f2` (matching the formulas pin); slinging an uninstalled target prints the new repair hint. **Note:** the full sling chain (`gc import install` → `gc sling /gc.run-operator --on build-from-requirements`) needs network pack-fetch + a live provider, so that last hop is unverified in the sandbox; it's the exact path the manual workaround exercised, now seeded automatically. Closes #3832 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- cmd/gc/cmd_init_gascity_test.go | 38 ++++++++++ cmd/gc/main_test.go | 15 +++- cmd/gc/suggest.go | 82 ++++++++++++++++++++-- cmd/gc/suggest_test.go | 64 +++++++++++++++++ cmd/gc/testdata/pack-commands-doctor.txtar | 7 +- internal/config/config.go | 17 ++++- internal/config/config_test.go | 31 ++++++++ internal/config/public_packs.go | 9 +++ 8 files changed, 251 insertions(+), 12 deletions(-) diff --git a/cmd/gc/cmd_init_gascity_test.go b/cmd/gc/cmd_init_gascity_test.go index 19e17b5c6a..70eb7ed71f 100644 --- a/cmd/gc/cmd_init_gascity_test.go +++ b/cmd/gc/cmd_init_gascity_test.go @@ -131,6 +131,44 @@ func TestDoInitWithGascityTemplate(t *testing.T) { } } +// TestDoInitGascityTemplateSeedsRolesDefaultRigImport pins gascity#3832: a +// fresh gascity city must seed the gc-roles pack as a default rig import (bound +// "gc") so rigs added to the city receive the role agents the built-in formulas +// route to (gc.run-operator, ...). doInit writes default rig imports into +// city.toml under [defaults.rig.imports]. Without this a freshly initialized +// city failed `build-from-requirements` with `agent "gc.run-operator" not found +// in city.toml`. +func TestDoInitGascityTemplateSeedsRolesDefaultRigImport(t *testing.T) { + f := fsys.NewFake() + + wiz := defaultWizardConfig() + wiz.configName = "gascity" + wiz.provider = "claude" + wiz.providers = []string{"claude"} + + var stdout, stderr bytes.Buffer + code := doInit(f, "/bright-lights", wiz, "", &stdout, &stderr, false) + if code != 0 { + t.Fatalf("doInit = %d, want 0; stderr: %s", code, stderr.String()) + } + + cityData := f.Files[filepath.Join("/bright-lights", "city.toml")] + cityCfg, err := config.Parse(cityData) + if err != nil { + t.Fatalf("parsing city.toml: %v", err) + } + roles, ok := cityCfg.Defaults.Rig.Imports["gc"] + if !ok { + t.Fatalf("city.toml [defaults.rig.imports] = %v, want gc roles entry:\n%s", cityCfg.Defaults.Rig.Imports, cityData) + } + if roles.Source != config.PublicGascityRolesPackSource { + t.Errorf("roles default rig import source = %q, want %q", roles.Source, config.PublicGascityRolesPackSource) + } + if roles.Version != config.PublicGascityPackVersion { + t.Errorf("roles default rig import version = %q, want %q", roles.Version, config.PublicGascityPackVersion) + } +} + // TestInitTemplateHelpAndErrorAdvertiseAcceptedTemplates keeps the public // --template flag help and the unknown-template error synchronized with the // set normalizeInitTemplate accepts. gascity regressed here once: the parser diff --git a/cmd/gc/main_test.go b/cmd/gc/main_test.go index 7758e95086..6f6becef38 100644 --- a/cmd/gc/main_test.go +++ b/cmd/gc/main_test.go @@ -2784,12 +2784,21 @@ func TestDoInitWritesExpectedTOML(t *testing.T) { t.Fatalf("doInit = %d, want 0; stderr: %s", code, stderr.String()) } - // city.toml keeps only the runtime-local [workspace]; builtin packs - // compose via pinned [imports] in pack.toml. workspace.name lives in - // .gc/site.toml. + // city.toml keeps the runtime-local [workspace] plus the canonical + // default-rig imports: the gascity template seeds the gc-roles pack (bound + // "gc") so rigs added to the city inherit the role agents the built-in + // formulas route to (gascity#3832). Builtin packs compose via pinned + // [imports] in pack.toml; workspace.name lives in .gc/site.toml. got := string(f.Files[filepath.Join("/bright-lights", "city.toml")]) want := `[workspace] +[defaults] +[defaults.rig] +[defaults.rig.imports] +[defaults.rig.imports.gc] +source = "` + config.PublicGascityRolesPackSource + `" +version = "` + config.PublicGascityPackVersion + `" + # [mail] # retention_ttl controls how long read messages are retained before purge. # 0 disables retention; use "168h" for 7 days. diff --git a/cmd/gc/suggest.go b/cmd/gc/suggest.go index 5a8509edd8..afddeb20b0 100644 --- a/cmd/gc/suggest.go +++ b/cmd/gc/suggest.go @@ -70,15 +70,87 @@ func formatAvailable(label string, names []string) string { return fmt.Sprintf("; available %s: %s%s", label, strings.Join(show, ", "), suffix) } +// importBindingOf extracts the pack-import binding from a (possibly +// rig-qualified) agent target. "rig/gc.run-operator" and "gc.run-operator" +// both yield "gc"; a bare name ("mayor") or pool instance ("rig/polecat-2") +// yields "" because it carries no binding. +func importBindingOf(input string) string { + name := input + if i := strings.LastIndex(name, "/"); i >= 0 { + name = name[i+1:] + } + if i := strings.Index(name, "."); i > 0 { + return name[:i] + } + return "" +} + +// declaredImportSource returns the authored source for a pack-import binding +// declared anywhere in the city config — city-scope [imports.*], the +// [defaults.rig.imports.*] table, or any rig's [rigs.imports.*] — and whether +// the binding was found. It reads only what the city itself declares, so no +// pack or role name is hard-coded. +func declaredImportSource(cfg *config.City, binding string) (string, bool) { + if cfg == nil || binding == "" { + return "", false + } + if imp, ok := cfg.Imports[binding]; ok { + return imp.Source, true + } + if imp, ok := cfg.Defaults.Rig.Imports[binding]; ok { + return imp.Source, true + } + if imp, ok := cfg.DefaultRigImports[binding]; ok { + return imp.Source, true + } + for _, r := range cfg.Rigs { + if imp, ok := r.Imports[binding]; ok { + return imp.Source, true + } + } + return "", false +} + +// uninstalledImportHint returns a remediation hint when a not-found target is +// qualified by a pack-import binding that the city declares but whose agents +// are not installed here — the case behind gascity#3832, where a fresh city +// imports the formulas pack but the roles those formulas route to live in a +// declared-but-not-yet-installed pack. The fix is to install the declared pack, +// not to pick a different agent name, so we point at `gc import install`. +// Returns "" when the binding is unknown (caller falls back to the +// available-agents list) or when an agent with that binding is already +// composed (then the miss is a wrong/typo'd name, not a missing pack). +func uninstalledImportHint(input string, cfg *config.City) string { + binding := importBindingOf(input) + if binding == "" || cfg == nil { + return "" + } + src, ok := declaredImportSource(cfg, binding) + if !ok { + return "" + } + for _, a := range cfg.Agents { + if importBindingOf(a.QualifiedName()) == binding { + return "" // pack is installed; the agent name itself is wrong + } + } + return fmt.Sprintf("\n the %q pack is imported (%s) but its agents are not installed here; run `gc import install`", binding, src) +} + // agentNotFoundMsg returns a user-friendly error string for when an agent -// name is not found. Includes "did you mean?" and available agents list. +// name is not found. When the target names a declared-but-uninstalled pack +// import it surfaces the `gc import install` repair; otherwise it falls back to +// a "did you mean?" hint and the available-agents list. func agentNotFoundMsg(prefix, input string, cfg *config.City) string { + base := fmt.Sprintf("%s: agent %q not found in city.toml", prefix, input) + if hint := uninstalledImportHint(input, cfg); hint != "" { + return base + hint + } names := availableAgentNames(cfg) - hint := suggestSimilar(input, names) - if hint != "" { - return fmt.Sprintf("%s: agent %q not found in city.toml%s", prefix, input, hint) + if hint := suggestSimilar(input, names); hint != "" { + return base + hint } - return fmt.Sprintf("%s: agent %q not found in city.toml%s", prefix, input, formatAvailable("agents", names)) + return base + formatAvailable("agents", names) } // rigNotFoundMsg returns a user-friendly error string for when a rig diff --git a/cmd/gc/suggest_test.go b/cmd/gc/suggest_test.go index d82f77b83d..de915657d1 100644 --- a/cmd/gc/suggest_test.go +++ b/cmd/gc/suggest_test.go @@ -123,6 +123,70 @@ func TestAgentNotFoundMsg(t *testing.T) { } } +func TestImportBindingOf(t *testing.T) { + cases := map[string]string{ + "gc.run-operator": "gc", + "todo-app/gc.run-operator": "gc", + "mayor": "", + "hw/polecat-2": "", + "": "", + ".leading-dot": "", + } + for in, want := range cases { + if got := importBindingOf(in); got != want { + t.Errorf("importBindingOf(%q) = %q, want %q", in, got, want) + } + } +} + +// TestAgentNotFoundMsgUninstalledImportHint covers gascity#3832: when a +// not-found target names a declared-but-uninstalled pack import, the message +// must point at `gc import install` rather than just a "did you mean?" list. +func TestAgentNotFoundMsgUninstalledImportHint(t *testing.T) { + const rolesSrc = "https://github.com/gastownhall/gascity-packs/tree/main/gascity/roles" + + // Declared (as a default rig import) but no agent with that binding is + // composed → install hint with the declared source. + declared := &config.City{ + Agents: []config.Agent{{Name: "mayor"}}, + DefaultRigImports: map[string]config.Import{"gc": {Source: rolesSrc}}, + } + msg := agentNotFoundMsg("gc sling", "gc.run-operator", declared) + if !strings.Contains(msg, "gc import install") { + t.Errorf("declared-but-uninstalled should advise install: %q", msg) + } + if !strings.Contains(msg, rolesSrc) { + t.Errorf("install hint should include the declared source: %q", msg) + } + if strings.Contains(msg, "did you mean") { + t.Errorf("install hint should replace the did-you-mean noise: %q", msg) + } + + // Same miss, but resolved via a rig-qualified target → still hint. + if msg := agentNotFoundMsg("gc sling", "todo-app/gc.run-operator", declared); !strings.Contains(msg, "gc import install") { + t.Errorf("rig-qualified miss should advise install: %q", msg) + } + + // Declared AND installed (an agent carries the binding) → the miss is a + // typo, not a missing pack, so do NOT advise install. + installed := &config.City{ + Agents: []config.Agent{ + {Name: "mayor"}, + {Name: "gc.run-operator", Dir: "todo-app"}, + }, + Imports: map[string]config.Import{"gc": {Source: rolesSrc}}, + } + if msg := agentNotFoundMsg("gc sling", "todo-app/gc.run-oprator", installed); strings.Contains(msg, "gc import install") { + t.Errorf("installed pack + typo'd name should not advise install: %q", msg) + } + + // Unknown binding → fall back to the available-agents list (no install hint). + unknown := &config.City{Agents: []config.Agent{{Name: "mayor"}}} + if msg := agentNotFoundMsg("gc sling", "gc.run-operator", unknown); strings.Contains(msg, "gc import install") { + t.Errorf("unknown binding should not advise install: %q", msg) + } +} + func TestRigNotFoundMsg(t *testing.T) { cfg := &config.City{ Rigs: []config.Rig{ diff --git a/cmd/gc/testdata/pack-commands-doctor.txtar b/cmd/gc/testdata/pack-commands-doctor.txtar index 87fe919a0a..e9921b999d 100644 --- a/cmd/gc/testdata/pack-commands-doctor.txtar +++ b/cmd/gc/testdata/pack-commands-doctor.txtar @@ -13,7 +13,12 @@ env GC_DOLT=skip env GC_SESSION=fake # Initialize a real city scaffold, then swap in the pack-import config. -exec gc init $WORK/city +# Use the minimal template: the default gascity template seeds the gc-roles +# pack as a default-rig import (gascity#3832), and swapping in the ops-only +# city.toml below would orphan that pack's packs.lock entry, tripping the +# packv2-import-state doctor check. The minimal scaffold imports only the +# bundled core pack, which the swap leaves declared and reachable. +exec gc init --template minimal --default-provider claude $WORK/city cp $WORK/city.toml.custom $WORK/city/city.toml cp $WORK/site.toml.custom $WORK/city/.gc/site.toml diff --git a/internal/config/config.go b/internal/config/config.go index 804fe85465..7385536275 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4992,9 +4992,13 @@ func GastownCity(name, provider, startCommand string) City { // GascityCityWithProviders returns a minimal managed city that imports the // public gascity planning/implementation skills pack: a single mayor agent -// plus [imports.gascity] pinned to the registry release. The pack ships -// skills and formulas only (no agents), so the city shape matches the -// minimal template with the pack layered on top. +// plus [imports.gascity] (skills and formulas) pinned to the registry release. +// The gascity formulas route their steps to role agents (gc.run-operator, +// gc.requirements-planner, ...) that ship in the separate gc-roles subpack, so +// the template also seeds that pack as a default rig import bound "gc" — every +// rig added to the city then inherits the providerless, rig-scoped roles the +// formulas coordinate. Without it a fresh city can discover a formula but fails +// to launch with `agent "gc.run-operator" not found in city.toml` (gascity#3832). func GascityCityWithProviders(name, defaultProvider string, providers []string) City { city := WizardCityWithProviders(name, defaultProvider, providers) city.Imports = map[string]Import{ @@ -5003,6 +5007,13 @@ func GascityCityWithProviders(name, defaultProvider string, providers []string) Version: PublicGascityPackVersion, }, } + city.DefaultRigImports = map[string]Import{ + "gc": { + Source: PublicGascityRolesPackSource, + Version: PublicGascityPackVersion, + }, + } + city.DefaultRigImportOrder = []string{"gc"} return city } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ebad24a0fb..718200da60 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1197,6 +1197,37 @@ func TestGastownCity(t *testing.T) { } } +// TestGascityCitySeedsRolesDefaultRigImport pins gascity#3832: the gascity +// template imports the formulas pack at city scope AND seeds the gc-roles pack +// as a default rig import bound "gc", so every rig added to the city receives +// the providerless role agents (gc.run-operator, gc.requirements-planner, ...) +// that the built-in formulas route to. Without this, a fresh city could launch +// a formula but failed with `agent "gc.run-operator" not found in city.toml`. +func TestGascityCitySeedsRolesDefaultRigImport(t *testing.T) { + c := GascityCityWithProviders("bright-lights", "claude", []string{"claude"}) + + // City-scope formulas/skills import is unchanged. + if len(c.Imports) != 1 || c.Imports["gascity"].Source != PublicGascityPackSource || c.Imports["gascity"].Version != PublicGascityPackVersion { + t.Errorf("Imports = %v, want gascity=%s %s", c.Imports, PublicGascityPackSource, PublicGascityPackVersion) + } + + // Roles ride along as a default rig import, bound "gc" so the formula's + // gc.* targets resolve, pinned to the same commit as the formulas pack. + roles, ok := c.DefaultRigImports["gc"] + if !ok || len(c.DefaultRigImports) != 1 { + t.Fatalf("DefaultRigImports = %v, want single gc entry", c.DefaultRigImports) + } + if roles.Source != PublicGascityRolesPackSource { + t.Errorf("roles import source = %q, want %q", roles.Source, PublicGascityRolesPackSource) + } + if roles.Version != PublicGascityPackVersion { + t.Errorf("roles import version = %q, want %q (same commit as the formulas pack)", roles.Version, PublicGascityPackVersion) + } + if len(c.DefaultRigImportOrder) != 1 || c.DefaultRigImportOrder[0] != "gc" { + t.Errorf("DefaultRigImportOrder = %v, want [gc]", c.DefaultRigImportOrder) + } +} + func TestGastownCityStartCommand(t *testing.T) { c := GastownCity("test", "", "my-agent --auto") if c.Workspace.StartCommand != "my-agent --auto" { diff --git a/internal/config/public_packs.go b/internal/config/public_packs.go index 84205a4ca1..df83e9c79f 100644 --- a/internal/config/public_packs.go +++ b/internal/config/public_packs.go @@ -19,6 +19,15 @@ const ( // (gascity 0.1.6). PublicGascityPackVersion = "sha:3b3b89f2011e06d84459aa7bea1552382f13930a" + // PublicGascityRolesPackSource is the concrete durable source for the + // gascity role-agents subpack (gc-roles): the providerless, rig-scoped + // agents (run-operator, requirements-planner, design-author, ...) that the + // gascity formulas route to. It lives in the same repo as + // PublicGascityPackSource and is pinned to the same release commit + // (PublicGascityPackVersion), so a fresh gascity city's formulas and the + // rig roles they coordinate always come from one matching release. + PublicGascityRolesPackSource = "https://github.com/gastownhall/gascity-packs/tree/main/gascity/roles" + // BundledPackImportVersion pins the [imports.core]/[imports.bd] entries // gc init writes for the gascity.git packs bundled with the binary. // This is the CANONICAL pin: the only commit the binary pre-seeds into From 9e9990665ca86540063429ceb438bdd4d971046a Mon Sep 17 00:00:00 2001 From: dunks411 <54425677+duncan4123@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:04:11 +1000 Subject: [PATCH 05/77] Ensure gascity rigs inherit formula roles (#3851) ## Summary - make fresh `gascity` template cities add `gascity/roles` as a default rig import - keep the roles import pinned to the same gascity-packs content commit as the formula pack - correct the rig-pack-coverage doctor hint so default rig imports point at `city.toml`, not `pack.toml` ## Why The public `gascity` pack exposes formulas such as `build-basic`, and those formulas dispatch work to rig-local `gc.*` role agents, especially `gc.run-operator`. A fresh `gc init` followed by `gc rig add` could therefore create a rig that can see the formulas but cannot run them, failing with messages like `unknown formulas v2 target "gc.run-operator"` or `agent "gc.run-operator" not found`. `gc rig add` already knows how to copy `[defaults.rig.imports]` into new rigs. This PR wires the gascity template into that existing mechanism so newly added rigs inherit the companion roles pack automatically. ## Tests - `go test ./cmd/gc ./internal/config ./internal/doctor -run 'TestDoInitWritesExpectedTOML|TestDoInitWithGascityTemplate|TestDoInitDefaultTemplateImportsGascityPack|TestDoRigAdd_RootPackDefaultRigImports|TestBundledSourcePinnedVersionNormalizesSpellings|TestSupersededPublicPackVersionsAreUnique|TestRigPackCoverageCheck_FixHint'` Co-authored-by: duncan4123 --- internal/doctor/checks_rig_coverage.go | 2 +- internal/doctor/checks_rig_coverage_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/doctor/checks_rig_coverage.go b/internal/doctor/checks_rig_coverage.go index f9fb6a8404..c8bdc03f5e 100644 --- a/internal/doctor/checks_rig_coverage.go +++ b/internal/doctor/checks_rig_coverage.go @@ -112,7 +112,7 @@ func (c *RigPackCoverageCheck) Run(_ *CheckContext) *CheckResult { r.Status = StatusWarning r.Message = fmt.Sprintf("%d rig-scoped named_session(s) not covered by rig imports", len(issues)) r.Details = issues - r.FixHint = "add [defaults.rig.imports.] to pack.toml or add the pack to each rig's [imports]" + r.FixHint = "add [defaults.rig.imports.] to city.toml or add the pack to each rig's [imports]" return r } diff --git a/internal/doctor/checks_rig_coverage_test.go b/internal/doctor/checks_rig_coverage_test.go index 8da5aeb3ef..86c62e1430 100644 --- a/internal/doctor/checks_rig_coverage_test.go +++ b/internal/doctor/checks_rig_coverage_test.go @@ -271,6 +271,9 @@ mode = "always" if !strings.Contains(r.FixHint, "defaults.rig.imports") { t.Errorf("FixHint = %q, want it to mention [defaults.rig.imports] to guide the operator to the actual config knob", r.FixHint) } + if strings.Contains(r.FixHint, "pack.toml") { + t.Errorf("FixHint = %q, want default rig imports to point at city.toml", r.FixHint) + } } // TestRigPackCoverageCheck_PackWithoutRigSessions asserts that a pack From 5becf8854dc357392bef81e8da6eea9486a49999 Mon Sep 17 00:00:00 2001 From: Stephanie Jarmak <36544495+sjarmak@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:50:43 -0400 Subject: [PATCH 06/77] fix(session): poke controller on gc session kill so named sessions revive promptly (#3858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3812. ## What `cmdSessionKill` synced the killed session's bead to `asleep` and recorded `SessionStopped`, but never poked the controller. An always-named session then waited for the reconcile loop's next patrol tick (~4-5m) to revive, instead of reviving immediately off the poke channel. This adds a best-effort controller poke right after the asleep sync, mirroring the drain-ack path's poke-after-state-write. A poke failure stays non-fatal: the session still revives on the next tick, so the change only removes the latency, it never adds a new failure mode. ## Why it matters `gc session kill` is the documented recovery step for a wedged named session (mayor, supervisor-managed singletons). The multi-minute revive gap made kill-then-wait feel broken during recovery, and it was the specific delay behind the mayor revive-latency this fixes. ## Changes - `cmd/gc/cmd_session.go` (+15) — poke the controller after the asleep state-write in `cmdSessionKill`. - `cmd/gc/cmd_session_kill_poke_test.go` (+121) — test seam asserting the poke fires on kill. ## Test plan - `make build`, `go vet ./cmd/gc/...`, `golangci-lint run ./cmd/gc/...` — green. - `go test ./cmd/gc/ -run PokesController` — the new test asserts the controller poke is issued after the kill state-write. - Rebased onto current `origin/main` and re-verified before push (branch was a day old). --------- Co-authored-by: sjarmak --- cmd/gc/cmd_session.go | 15 ++++ cmd/gc/cmd_session_kill_poke_test.go | 122 +++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 cmd/gc/cmd_session_kill_poke_test.go diff --git a/cmd/gc/cmd_session.go b/cmd/gc/cmd_session.go index fc6c8abb46..aa8c240880 100644 --- a/cmd/gc/cmd_session.go +++ b/cmd/gc/cmd_session.go @@ -2206,6 +2206,10 @@ Accepts a session ID (e.g., gc-42) or session alias (e.g., mayor).`, return cmd } +// sessionKillPokeController is a mutable global test seam over pokeController. +// Tests that swap it MUST NOT call t.Parallel(). +var sessionKillPokeController = pokeController + // cmdSessionKill is the CLI entry point for "gc session kill". func cmdSessionKill(args []string, stdout, stderr io.Writer, jsonOutput ...bool) int { asJSON := sessionJSONRequested(jsonOutput) @@ -2275,6 +2279,17 @@ func cmdSessionKill(args []string, stdout, stderr io.Writer, jsonOutput ...bool) } } + // Poke the controller after the asleep sync so the reconciler observes the + // killed state immediately instead of waiting a full patrol interval to + // revive an always-named session (#3812), the same poke-after-state-write + // approach the drain-ack path uses (doRuntimeDrainAck). Best-effort and + // unconditional: a poke failure (e.g. no controller running) is non-fatal, + // and a spurious poke when the asleep sync was skipped is harmless — the + // reconciler observes unchanged state and continues. + if err := sessionKillPokeController(cityPath); err != nil { + fmt.Fprintf(stderr, "gc session kill: warning: poke failed: %v\n", err) //nolint:errcheck // best-effort stderr + } + // Use the resolved session ID as the canonical Subject for event // consumers. This ensures a stable key regardless of how the user // specified the target (session ID or alias). diff --git a/cmd/gc/cmd_session_kill_poke_test.go b/cmd/gc/cmd_session_kill_poke_test.go new file mode 100644 index 0000000000..33cebfbcee --- /dev/null +++ b/cmd/gc/cmd_session_kill_poke_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/runtime" + sessionpkg "github.com/gastownhall/gascity/internal/session" +) + +// newKillPokeSession stands up a city, store, and fake runtime for an awake +// named session, returning the store and the session bead. The fake provider +// is wired through buildSessionProviderByName so cmdSessionKill resolves a real +// handle and reaches the asleep-sync + poke tail. +func newKillPokeSession(t *testing.T, identity, sessionName string) (beads.Store, beads.Bead, string) { + t.Helper() + t.Setenv("GC_BEADS", "file") + t.Setenv("GC_SESSION", "fake") + + cityDir := shortSocketTempDir(t, "gc-kill-poke-") + t.Setenv("GC_CITY", cityDir) + writeGenericNamedSessionCityTOML(t, cityDir) + + fakeProvider := runtime.NewFake() + oldBuild := buildSessionProviderByName + buildSessionProviderByName = func(*config.City, string, config.SessionConfig, string, string) (runtime.Provider, error) { + return fakeProvider, nil + } + t.Cleanup(func() { buildSessionProviderByName = oldBuild }) + + store, err := openCityStoreAt(cityDir) + if err != nil { + t.Fatalf("openCityStoreAt: %v", err) + } + bead, err := store.Create(beads.Bead{ + Title: "named session", + Type: sessionpkg.BeadType, + Labels: []string{sessionpkg.LabelSession, "template:worker"}, + Metadata: map[string]string{ + "alias": identity, + "template": "worker", + "agent_name": "gascity/gc.worker", + "session_name": sessionName, + "state": "awake", + namedSessionMetadataKey: "true", + namedSessionIdentityMetadata: identity, + }, + }) + if err != nil { + t.Fatalf("store.Create(session bead): %v", err) + } + if err := fakeProvider.Start(context.Background(), sessionName, runtime.Config{Command: "true"}); err != nil { + t.Fatalf("fakeProvider.Start: %v", err) + } + if err := fakeProvider.SetMeta(sessionName, "GC_SESSION_ID", bead.ID); err != nil { + t.Fatalf("SetMeta(GC_SESSION_ID): %v", err) + } + return store, bead, cityDir +} + +// TestCmdSessionKill_PokesControllerAfterSleep pins #3812: a successful +// `gc session kill` must poke the controller so the reconciler observes the +// killed state promptly instead of waiting a full patrol interval. The poke +// must fire exactly once, with the resolved cityPath, and only AFTER the bead +// has been synced asleep (so the reconciler observes the killed state when it +// converges). +func TestCmdSessionKill_PokesControllerAfterSleep(t *testing.T) { + const identity = "session-a" + const sessionName = "s-gc-kill-poke" + store, bead, cityDir := newKillPokeSession(t, identity, sessionName) + + calls := 0 + var gotCityPath, stateAtPoke string + old := sessionKillPokeController + sessionKillPokeController = func(cityPath string) error { + calls++ + gotCityPath = cityPath + if b, gErr := store.Get(bead.ID); gErr == nil { + stateAtPoke = b.Metadata["state"] + } + return nil + } + t.Cleanup(func() { sessionKillPokeController = old }) + + var stdout, stderr bytes.Buffer + if code := cmdSessionKill([]string{identity}, &stdout, &stderr); code != 0 { + t.Fatalf("cmdSessionKill = %d, want 0; stderr=%s", code, stderr.String()) + } + + if calls != 1 { + t.Fatalf("poke called %d times, want exactly 1", calls) + } + if gotCityPath != cityDir { + t.Errorf("poke cityPath = %q, want %q", gotCityPath, cityDir) + } + if stateAtPoke != string(sessionpkg.StateAsleep) { + t.Errorf("state at poke time = %q, want %q (poke must run after the SleepPatch write)", stateAtPoke, sessionpkg.StateAsleep) + } +} + +// TestCmdSessionKill_PokeFailureIsNonFatal pins the best-effort contract: a +// poke failure (e.g. no controller running) must not fail the kill — the +// session state has already been synced asleep, so the reconciler observes it +// on its normal convergence pass regardless of whether the poke landed. +func TestCmdSessionKill_PokeFailureIsNonFatal(t *testing.T) { + const identity = "session-a" + const sessionName = "s-gc-kill-poke-fail" + _, _, _ = newKillPokeSession(t, identity, sessionName) + + old := sessionKillPokeController + sessionKillPokeController = func(string) error { return errors.New("dial failed") } + t.Cleanup(func() { sessionKillPokeController = old }) + + var stdout, stderr bytes.Buffer + if code := cmdSessionKill([]string{identity}, &stdout, &stderr); code != 0 { + t.Fatalf("cmdSessionKill = %d, want 0 (poke failure is best-effort); stderr=%s", code, stderr.String()) + } +} From f5cc23fefd73dffaf04034671950a035e461688a Mon Sep 17 00:00:00 2001 From: realies <5107843+realies@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:57:14 +0300 Subject: [PATCH 07/77] test(pool): regression coverage for #2520 no-work drain-ack + replacement-allocation (#3855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds an end-to-end regression test for #2520 ("pool over-counts supply when session drain-acks with no work and bead stays active"). Per @rileywhite's `test-hardening` classification, this is **test-only — no behavior change**. Current `main` already behaves correctly; this locks in the full "no-work pool drain-ack plus replacement-allocation" scenario that previously had no coverage. New file: `cmd/gc/session_reconciler_pool_replacement_test.go` (package `main`), one test `TestReconcileSessionBeads_DrainAckNoWorkFreesSlotAndReallocates`. No production code is touched. ## Root cause (already fixed on main) #2520's scenario: a pool with `min_active_sessions=0`, `max>=2`, two routed-ready beads, two sessions racing to claim the first. The loser gets "already claimed", calls `gc runtime drain-ack` with no work attached, and (per the report) its session bead lingered in `state=active`. Because the pool counted that lingering bead as an occupied supply slot, `runningSessions>0` forced `isCold=false`, which suppressed the cold-pool cross-store wake probe — so the next still-ready bead was stranded until an operator ran `gc session close`. Two independent fixes already resolve this on `main`: - The drain-ack state machine lands a no-work loser in a **terminal drained state** (not lingering `active`). - #3419 (`cmd/gc/build_desired_state.go:560`) guards the `runningSessions` counter with `isPoolManagedSessionBead(sb) && poolSessionIsLive(sb)`, so a **drained/asleep phantom pool bead is excluded** from the supply count — the cold-wake probe fires and a replacement worker is spawned for the still-ready queue bead. What was missing was a test exercising both halves through to the replacement allocation. ## The test - **Part 1 — `reconciler_drains_no_work_loser_to_terminal`:** drives the real `reconcileSessionBeads` over two ticks on a no-work drain-acking loser (`state=active`, agent-set drain-ack, no assigned work) and asserts it reaches `state=drained` / `poolSessionIsLive==false` instead of lingering active. This is #3419-independent (it passes with or without the guard) and asserts the precondition #2520 says was violated. - **Part 2 — `drained_phantom_excluded_from_supply_reallocates`** (the load-bearing regression assertion): drives the real cross-store cold-pool supply probe `buildDesiredStateWithSessionBeads` with a phantom pool session (`pool_slot=1`) plus a still-ready routed bead delivered cross-store to the city store. Table cases: `drained` phantom and `asleep`/`sleep_reason=idle` phantom must be excluded → `ScaleCheckCounts[rig-A/worker]==1` and exactly one desired replacement slot; an `active` phantom is the control and must still suppress the probe → demand 0, 0 slots. Mirrors the established `scale_from_zero_test.go` harness (`localMockProvider`, `ScaleCheck:"printf 0"`, `gc.routed_to` routing). ## RED→GREEN evidence (independently re-run in an isolated worktree) - **GREEN** (pristine `main`): all 5 sub-tests PASS. - **RED** (revert only the #3419 one-liner `... && poolSessionIsLive(sb)` → `if isPoolManagedSessionBead(sb) {`): `drained_phantom_frees_slot` and `asleep_idle_phantom_frees_slot` FAIL with `ScaleCheckCounts[rig-A/worker] = 0, want 1` — the exact #2520 over-count symptom. The `active_session_still_suppresses_probe` control and Part 1 stay GREEN, proving the assertion discriminates on the phantom's `state` (via `poolSessionIsLive`) rather than asserting a constant. - Restoring the guard → GREEN, production files byte-pristine (`git diff --stat` production = empty). - Non-vacuous slot check: instrumentation confirms GREEN yields a concrete desired replacement slot (`TemplateName="rig-A/worker"`), not merely a demand integer. ## Verification - `go build ./cmd/gc/` clean; `go vet ./cmd/gc/` clean; `gofmt -l` empty. - New helpers (`createRoutedReadyBeadForReplacement`, `setPoolSessionActive`) have unique names — no symbol collision. - Targeted regression suite (all Pool/Scale/Reconcile/Drain/Session/DesiredState tests) passes; `internal/session` and `internal/beads` pass. - Deterministic: 10× consecutive runs pass; 3× under `-race` pass (async provider stop is gated by `waitForProviderStopped`, not a sleep — not time-flaky). Closes #2520 --- ...ession_reconciler_pool_replacement_test.go | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 cmd/gc/session_reconciler_pool_replacement_test.go diff --git a/cmd/gc/session_reconciler_pool_replacement_test.go b/cmd/gc/session_reconciler_pool_replacement_test.go new file mode 100644 index 0000000000..3682f9ff0a --- /dev/null +++ b/cmd/gc/session_reconciler_pool_replacement_test.go @@ -0,0 +1,282 @@ +package main + +import ( + "context" + "io" + "os" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/clock" + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/events" + "github.com/gastownhall/gascity/internal/runtime" +) + +// TestReconcileSessionBeads_DrainAckNoWorkFreesSlotAndReallocates is the +// end-to-end regression guard for gastownhall/gascity#2520 ("pool over-counts +// supply when session drain-acks with no work and bead stays active"). +// +// Scenario (min_active=0, max>=2 pool; two routed-ready beads; two sessions +// race to claim one): the winner takes bead-1 (in_progress), the loser gets +// "already claimed" and calls `gc runtime drain-ack` with NO work attached. +// The report claimed the loser's session bead lingers in state=active, the +// pool counts it as an occupied supply slot, and the next still-ready bead is +// never served until an operator runs `gc session close`. +// +// The maintainer classified #2520 as test-hardening: current main already +// behaves correctly (the drain-ack lands the loser in a terminal drained state, +// and a drained pool bead is excluded from the running-session supply count so +// the still-ready work is still served), but the full "no-work pool drain-ack +// PLUS replacement-allocation" path had no end-to-end coverage. Existing tests +// stop at the state transition or the pool-bead close; none then re-drives the +// supply probe to prove the drained loser is excluded AND a replacement slot is +// desired for the still-ready queue bead. This test locks in both halves. +// +// The second sub-test is the load-bearing regression assertion: it fails RED on +// the pre-#3419 revision (where poolSessionIsLive did not exclude drained pool +// beads, so a phantom drained bead counted toward runningSessions, forced +// isCold=false, suppressed the cold-wake probe, and stranded the ready bead — +// exactly #2520's over-count symptom) and passes on current main. +func TestReconcileSessionBeads_DrainAckNoWorkFreesSlotAndReallocates(t *testing.T) { + // Part 1 — the real reconciler drains a no-work drain-acking loser to a + // terminal state (it does NOT linger in state=active), which is the + // precondition the #2520 report says was violated. + t.Run("reconciler_drains_no_work_loser_to_terminal", func(t *testing.T) { + now := time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC) + cityDir := t.TempDir() + writeCityTOML(t, cityDir, "trace-town", "worker") + + cfg := &config.City{ + Workspace: config.Workspace{Name: "trace-town"}, + Session: config.SessionConfig{Provider: "fake"}, + Agents: []config.Agent{{ + Name: "worker", + Dir: "repo", + StartCommand: "true", + MinActiveSessions: intPtr(0), + MaxActiveSessions: intPtr(2), + }}, + } + store := beads.NewMemStore() + sp := runtime.NewFake() + + // Two routed-ready beads. bead-1 goes in_progress under the winner; + // bead-2 stays ready in the queue. + beadOne := createRoutedReadyBeadForReplacement(t, store, "repo/worker", "queued work 1") + createRoutedReadyBeadForReplacement(t, store, "repo/worker", "queued work 2") + + // Winner: slot 1, active, holds bead-1 in_progress. + winner := createCanonicalPoolSession(t, store, &cfg.Agents[0], now, 1) + setPoolSessionActive(t, store, winner.ID) + if err := sp.Start(context.Background(), winner.Metadata["session_name"], runtime.Config{}); err != nil { + t.Fatalf("start winner runtime: %v", err) + } + statusInProgress := "in_progress" + winnerAssignee := winner.ID + if err := store.Update(beadOne.ID, beads.UpdateOpts{Status: &statusInProgress, Assignee: &winnerAssignee}); err != nil { + t.Fatalf("assign bead-1 to winner: %v", err) + } + + // Loser: slot 2, active, NO assigned work, agent-set drain-ack (the + // #1425 stranded event never fires because hasAssignedWork=false). + loser := createCanonicalPoolSession(t, store, &cfg.Agents[0], now, 2) + setPoolSessionActive(t, store, loser.ID) + loser, err := store.Get(loser.ID) + if err != nil { + t.Fatalf("reload loser: %v", err) + } + loserName := loser.Metadata["session_name"] + if err := sp.Start(context.Background(), loserName, runtime.Config{}); err != nil { + t.Fatalf("start loser runtime: %v", err) + } + dops := newFakeDrainOps() + if err := dops.setDrainAck(loserName); err != nil { + t.Fatalf("setDrainAck(loser): %v", err) + } + + ds := buildDesiredState("trace-town", cityDir, now, cfg, sp, store, io.Discard) + dt := newDrainTracker() + clk := &clock.Fake{Time: now} + + // Tick 1: alive + agent-sourced drain-ack -> mark stop-pending and queue + // the async provider stop. + reconcileSessionBeads( + context.Background(), []beads.Bead{loser}, ds.State, map[string]bool{"repo/worker": true}, + cfg, sp, store, dops, nil, nil, dt, ds.PoolDesiredCounts, false, nil, "trace-town", + nil, clk, events.Discard, 0, 0, io.Discard, io.Discard, + ) + waitForProviderStopped(t, sp, loserName) + + reloaded, err := store.Get(loser.ID) + if err != nil { + t.Fatalf("reload loser after tick 1: %v", err) + } + + // Tick 2: runtime is gone -> finalize the stop-pending session to a + // terminal drained state (pool-managed + no work -> close the bead). + reconcileSessionBeads( + context.Background(), []beads.Bead{reloaded}, ds.State, map[string]bool{"repo/worker": true}, + cfg, sp, store, dops, nil, nil, dt, ds.PoolDesiredCounts, false, nil, "trace-town", + nil, clk, events.Discard, 0, 0, io.Discard, io.Discard, + ) + + got, err := store.Get(loser.ID) + if err != nil { + t.Fatalf("reload loser after tick 2: %v", err) + } + // #2520's precondition for the over-count is the loser lingering as a + // live session. Assert it did NOT: it reached a terminal drained state. + if got.Metadata["state"] == "active" && got.Status != "closed" { + t.Fatalf("no-work drain-acked loser lingered as a live supply slot: state=%q status=%q metadata=%v", + got.Metadata["state"], got.Status, got.Metadata) + } + if got.Metadata["state"] != "drained" { + t.Fatalf("loser state = %q, want drained", got.Metadata["state"]) + } + if poolSessionIsLive(got) { + t.Fatalf("drained loser still reports poolSessionIsLive=true; it would over-count supply: metadata=%v", got.Metadata) + } + }) + + // Part 2 — replacement-allocation. A drained phantom pool session (the exact + // terminal state Part 1 produces, but left open in the store as the report + // describes it "lingering") must be excluded from the running-session supply + // count, so a min=0 pool with still-ready cross-store work is NOT treated as + // warm: its cold-wake probe fires and desires a replacement slot to serve the + // stranded bead. + // + // This is the RED assertion: on the pre-#3419 revision the drained phantom + // counts toward runningSessions -> isCold=false -> cold-wake probe suppressed + // -> demand 0 and no desired slot (the ready bead is stranded). On current + // main the phantom is excluded -> isCold=true -> demand 1 and one desired + // slot. The parallel "active" sub-case is the control: a genuinely-live + // session MUST still suppress the probe. + t.Run("drained_phantom_excluded_from_supply_reallocates", func(t *testing.T) { + cases := []struct { + name string + meta map[string]string + wantDemand int + wantSlots int + wantStillLive bool + }{ + { + name: "drained_phantom_frees_slot", + meta: map[string]string{"state": "drained"}, + wantDemand: 1, wantSlots: 1, wantStillLive: false, + }, + { + name: "asleep_idle_phantom_frees_slot", + meta: map[string]string{"state": "asleep", "sleep_reason": "idle"}, + wantDemand: 1, wantSlots: 1, wantStillLive: false, + }, + { + name: "active_session_still_suppresses_probe", + meta: map[string]string{"state": "active"}, + wantDemand: 0, wantSlots: 0, wantStillLive: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tmpDir := t.TempDir() + rigPath := tmpDir + "/rigs/rig-A" + if err := os.MkdirAll(rigPath, 0o755); err != nil { + t.Fatalf("mkdir rig path: %v", err) + } + maxSess := 5 + minSess := 0 + cfg := &config.City{ + Agents: []config.Agent{{ + Name: "worker", + MaxActiveSessions: &maxSess, + MinActiveSessions: &minSess, + ScaleCheck: "printf 0", // custom check reports 0; only a cold-wake probe can raise demand + Dir: "rig-A", + Provider: "mock", + }}, + Rigs: []config.Rig{{Name: "rig-A", Path: rigPath}}, + Providers: map[string]config.ProviderSpec{"mock": {Command: "true"}}, + } + cityStore := beads.NewMemStore() + rigStore := beads.NewMemStore() + rigStores := map[string]beads.Store{"rig-A": rigStore} + qualifiedName := "rig-A/worker" + + meta := map[string]string{ + "template": qualifiedName, + "session_name": "worker-1", + "pool_slot": "1", + } + for k, v := range tc.meta { + meta[k] = v + } + phantom, err := rigStore.Create(beads.Bead{ + ID: "session-loser", Status: "open", Type: sessionBeadType, Metadata: meta, + }) + if err != nil { + t.Fatalf("create phantom pool session: %v", err) + } + if live := poolSessionIsLive(phantom); live != tc.wantStillLive { + t.Fatalf("poolSessionIsLive(%s phantom) = %v, want %v", tc.name, live, tc.wantStillLive) + } + + // Still-ready routed bead delivered cross-store to the city store + // (the sleeping rig pool's own-store probe cannot see it, so only a + // cold-wake probe over all stores serves it). + if _, err := cityStore.Create(beads.Bead{ + ID: "bead-ready", Status: "open", Type: "task", + Metadata: map[string]string{"gc.routed_to": qualifiedName}, + }); err != nil { + t.Fatalf("create still-ready routed bead: %v", err) + } + + result := buildDesiredStateWithSessionBeads( + "test-city", tmpDir, time.Now(), cfg, &localMockProvider{}, + cityStore, rigStores, &sessionBeadSnapshot{}, nil, os.Stderr, + ) + if demand := result.ScaleCheckCounts[qualifiedName]; demand != tc.wantDemand { + t.Fatalf("ScaleCheckCounts[%s] = %d, want %d (drained/asleep phantom must not over-count supply; #2520)", + qualifiedName, demand, tc.wantDemand) + } + workerSlots := 0 + for _, tp := range result.State { + if tp.TemplateName == qualifiedName { + workerSlots++ + } + } + if workerSlots != tc.wantSlots { + t.Fatalf("desired %s slots = %d, want %d (replacement slot for the still-ready bead)", + qualifiedName, workerSlots, tc.wantSlots) + } + }) + } + }) +} + +func createRoutedReadyBeadForReplacement(t *testing.T, store beads.Store, template, title string) beads.Bead { + t.Helper() + b, err := store.Create(beads.Bead{ + Title: title, + Type: "task", + Status: "open", + Metadata: map[string]string{"gc.routed_to": template}, + }) + if err != nil { + t.Fatalf("create routed ready bead %q: %v", title, err) + } + return b +} + +func setPoolSessionActive(t *testing.T, store beads.Store, id string) { + t.Helper() + for k, v := range map[string]string{ + "state": "active", + "pending_create_claim": "", + "pending_create_started_at": "", + } { + if err := store.SetMetadata(id, k, v); err != nil { + t.Fatalf("SetMetadata(%s=%s): %v", k, v, err) + } + } +} From c701bdbf4c056a72d943ac354c6a9c5e7755687d Mon Sep 17 00:00:00 2001 From: Julian Knutsen Date: Wed, 1 Jul 2026 13:34:54 -0700 Subject: [PATCH 08/77] fix(session): resolve Codex transcript fallback by session order for shared-workdir siblings (#3816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Resolves Codex transcript fallback by **session order** when multiple Codex sessions share the same working directory (shared-workdir siblings). When two or more Codex sessions run against the same `work_dir`, the prior transcript lookup could not disambiguate which `.jsonl` transcript belonged to which session, producing wrong-transcript fallbacks. This change adds `session.ResolveCodexTranscriptBySessionOrder`, which anchors each session by its wake/start timestamp and maps a target session to a transcript only when that transcript is uniquely contained in the target's start window — preserving ambiguity (returning empty) for underspecified groups rather than guessing. Plumbing threads the resolver through `cmd/gc/cmd_session_logs.go`, `internal/session/chat.go`, the `internal/sessionlog` reader, and the `internal/worker/transcript` discovery path, with tests covering the shared-workdir ordering, the unique-window requirement, and the ambiguity-preserving negative cases. ## Why this is a clean, store-backend-agnostic extraction This is a self-contained slice extracted from the local sqlite deploy branch `deploy/sqlite-b36-probe-attribution` and lifted onto `main` ahead of the beads interface refactor and the sqlite-behind-interfaces swap. It is **store-backend-agnostic**: the resolver operates purely over `[]beads.Bead` and the existing `sessionlog` / `worker/transcript` abstractions. It pulls in **zero** sqlite, graph-store, coordrouter, or bd-shim-HTTP code, so it merges cleanly today and does not need to wait on the interface work. ## Verification - `go build ./internal/session/ ./cmd/gc/` — passes - `go vet ./internal/session/ ./cmd/gc/` — passes - Diff vs `main` contains only the 10 slice files; no sqlite/graph-store paths Generated with Claude Code. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- cmd/gc/cmd_session_logs.go | 102 +++++-- cmd/gc/cmd_session_logs_test.go | 70 +++++ cmd/gc/json_schema_test.go | 10 + internal/formula/expand_test.go | 46 +++ internal/session/chat.go | 61 ++-- internal/session/transcript_lookup.go | 138 +++++++++ internal/session/transcript_lookup_test.go | 84 ++++++ internal/sessionlog/reader.go | 281 ++++++++++++++++-- internal/sessionlog/sessionlog_test.go | 109 +++++++ internal/worker/handle_transcriptmeta_test.go | 91 ++++++ internal/worker/transcript/discovery.go | 14 +- internal/worker/transcript/discovery_test.go | 48 +++ 12 files changed, 985 insertions(+), 69 deletions(-) create mode 100644 internal/session/transcript_lookup.go create mode 100644 internal/session/transcript_lookup_test.go diff --git a/cmd/gc/cmd_session_logs.go b/cmd/gc/cmd_session_logs.go index 1b96fb89e5..325126f9e2 100644 --- a/cmd/gc/cmd_session_logs.go +++ b/cmd/gc/cmd_session_logs.go @@ -126,16 +126,46 @@ func resolveStoredSessionLogSource(cityPath string, cfg *config.City, store bead if !ok { return "", "", false, "" } - if logCtx.sessionID != "" { - handle, err := workerHandleForSessionWithConfig(cityPath, store, newSessionProvider(), cfg, logCtx.sessionID) - if err == nil { - if path, pathErr := handle.TranscriptPath(context.Background()); pathErr == nil && strings.TrimSpace(path) != "" { - return path, logCtx.provider, true, "" - } - } + if path := resolveSessionHandleTranscript(cityPath, cfg, store, logCtx); path != "" { + return path, logCtx.provider, true, "" } - path := "" fallbackAllowed := canFallbackStoredSessionLogByWorkDir(store, logCtx) + path := resolveStoredSessionLogPathCandidate(searchPaths, logCtx, fallbackAllowed) + if path == "" && fallbackAllowed { + path = discoverWorkDirTranscript(searchPaths, logCtx) + } + if path == "" && !fallbackAllowed { + path = resolveCodexSiblingLogPath(store, searchPaths, logCtx) + } + if path == "" && !fallbackAllowed { + return "", logCtx.provider, true, ambiguousSessionLogDiagnostic(logCtx) + } + return path, logCtx.provider, true, "" +} + +// resolveSessionHandleTranscript returns the transcript path reported by the +// session's worker handle, or "" when there is no session id, the handle cannot +// be built, or it reports no transcript. +func resolveSessionHandleTranscript(cityPath string, cfg *config.City, store beads.Store, logCtx sessionLogContext) string { + if logCtx.sessionID == "" { + return "" + } + handle, err := workerHandleForSessionWithConfig(cityPath, store, newSessionProvider(), cfg, logCtx.sessionID) + if err != nil { + return "" + } + path, pathErr := handle.TranscriptPath(context.Background()) + if pathErr != nil || strings.TrimSpace(path) == "" { + return "" + } + return path +} + +// resolveStoredSessionLogPathCandidate resolves the primary stored transcript +// path from the session key (preferred) or the workdir fallback, returning "" +// when nothing fresh enough for the session is found. +func resolveStoredSessionLogPathCandidate(searchPaths []string, logCtx sessionLogContext, fallbackAllowed bool) string { + path := "" if strings.TrimSpace(logCtx.sessionKey) != "" { path = resolveSessionKeyedLogPath(searchPaths, logCtx) if path == "" && fallbackAllowed { @@ -145,21 +175,41 @@ func resolveStoredSessionLogSource(cityPath string, cfg *config.City, store bead path = resolveSessionLogPath(searchPaths, logCtx) } if !sessionLogPathFreshEnough(path, logCtx.createdAt) { - path = "" + return "" } - if path == "" && fallbackAllowed { - factory, err := worker.NewFactory(worker.FactoryConfig{SearchPaths: searchPaths}) - if err == nil { - path = factory.DiscoverWorkDirTranscript(logCtx.provider, logCtx.workDir) - } + return path +} + +// discoverWorkDirTranscript resolves the workdir-based transcript fallback, +// returning "" when the worker factory cannot be built or the transcript is not +// fresh enough for the session. +func discoverWorkDirTranscript(searchPaths []string, logCtx sessionLogContext) string { + factory, err := worker.NewFactory(worker.FactoryConfig{SearchPaths: searchPaths}) + if err != nil { + return "" } + path := factory.DiscoverWorkDirTranscript(logCtx.provider, logCtx.workDir) if !sessionLogPathFreshEnough(path, logCtx.createdAt) { - path = "" + return "" } - if path == "" && !fallbackAllowed { - return "", logCtx.provider, true, ambiguousSessionLogDiagnostic(logCtx) + return path +} + +// resolveCodexSiblingLogPath resolves an ambiguous same-workdir Codex session to +// its transcript by session-start ordering. It is used only when the plain +// workdir fallback is disallowed because multiple live siblings share the +// workdir. It returns "" when the sibling set cannot be gathered, the group is +// underspecified, or the resolved transcript is not fresh enough for the session. +func resolveCodexSiblingLogPath(store beads.Store, searchPaths []string, logCtx sessionLogContext) string { + siblings, err := sessionLogFallbackSiblings(store, logCtx) + if err != nil { + return "" } - return path, logCtx.provider, true, "" + path := sessionpkg.ResolveCodexTranscriptBySessionOrder(searchPaths, logCtx.provider, logCtx.workDir, logCtx.sessionID, siblings) + if !sessionLogPathFreshEnough(path, logCtx.createdAt) { + return "" + } + return path } func resolveSessionKeyedLogPath(searchPaths []string, logCtx sessionLogContext) string { @@ -221,9 +271,14 @@ func canFallbackStoredSessionLogByWorkDir(store beads.Store, logCtx sessionLogCo if store == nil || strings.TrimSpace(logCtx.sessionID) == "" || strings.TrimSpace(logCtx.workDir) == "" { return false } + siblings, err := sessionLogFallbackSiblings(store, logCtx) + return err == nil && len(siblings) == 1 +} + +func sessionLogFallbackSiblings(store beads.Store, logCtx sessionLogContext) ([]beads.Bead, error) { all, err := sessionLogFallbackCandidates(store, logCtx.workDir, logCtx.provider) if err != nil { - return false + return nil, err } targetLive := false for _, b := range all { @@ -232,7 +287,7 @@ func canFallbackStoredSessionLogByWorkDir(store beads.Store, logCtx sessionLogCo break } } - matches := 0 + var matches []beads.Bead for _, b := range all { if !sessionpkg.IsSessionBeadOrRepairable(b) { continue @@ -250,12 +305,9 @@ func canFallbackStoredSessionLogByWorkDir(store beads.Store, logCtx sessionLogCo if targetLive && b.ID != logCtx.sessionID && !sessionLogFallbackCandidateLive(b) { continue } - matches++ - if matches > 1 { - return false - } + matches = append(matches, b) } - return matches == 1 + return matches, nil } func sessionLogFallbackCandidates(store beads.Store, workDir, provider string) ([]beads.Bead, error) { diff --git a/cmd/gc/cmd_session_logs_test.go b/cmd/gc/cmd_session_logs_test.go index cdf5ed37ef..d0047bc0c0 100644 --- a/cmd/gc/cmd_session_logs_test.go +++ b/cmd/gc/cmd_session_logs_test.go @@ -506,6 +506,76 @@ func TestResolveStoredSessionLogSource_CodexDoesNotUseAmbiguousWorkDirFallback(t } } +func TestResolveStoredSessionLogSource_CodexAmbiguousWorkDirUsesStartOrder(t *testing.T) { + workDir := t.TempDir() + firstStarted := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC) + secondStarted := firstStarted.Add(2 * time.Minute) + store := beads.NewMemStoreFrom(2, []beads.Bead{ + { + ID: "gc-1", + Type: session.BeadType, + Status: "open", + Labels: []string{session.LabelSession}, + CreatedAt: firstStarted.Add(-time.Hour), + UpdatedAt: firstStarted, + Metadata: map[string]string{ + "alias": "workflows__codex-max-mc-one", + "provider": "codex", + "provider_kind": "codex", + "session_name": "workflows__codex-max-mc-one", + "state": "awake", + "last_woke_at": firstStarted.Format(time.RFC3339), + "work_dir": workDir, + }, + }, + { + ID: "gc-2", + Type: session.BeadType, + Status: "open", + Labels: []string{session.LabelSession}, + CreatedAt: secondStarted.Add(-time.Hour), + UpdatedAt: secondStarted, + Metadata: map[string]string{ + "alias": "workflows__codex-max-mc-two", + "provider": "codex", + "provider_kind": "codex", + "session_name": "workflows__codex-max-mc-two", + "state": "awake", + "last_woke_at": secondStarted.Format(time.RFC3339), + "work_dir": workDir, + }, + }, + }, nil) + + searchBase := t.TempDir() + dayDir := filepath.Join(searchBase, "2026", "05", "04") + if err := os.MkdirAll(dayDir, 0o755); err != nil { + t.Fatal(err) + } + firstPath := filepath.Join(dayDir, "rollout-first.jsonl") + if err := os.WriteFile(firstPath, []byte(fmt.Sprintf(`{"timestamp":%q,"type":"session_meta","payload":{"cwd":%q}}`+"\n", firstStarted.Format(time.RFC3339), workDir)), 0o644); err != nil { + t.Fatal(err) + } + secondPath := filepath.Join(dayDir, "rollout-second.jsonl") + if err := os.WriteFile(secondPath, []byte(fmt.Sprintf(`{"timestamp":%q,"type":"session_meta","payload":{"cwd":%q}}`+"\n", secondStarted.Format(time.RFC3339), workDir)), 0o644); err != nil { + t.Fatal(err) + } + + got, provider, ok, diagnostic := resolveStoredSessionLogSource("", nil, store, "workflows__codex-max-mc-two", []string{searchBase}) + if !ok { + t.Fatal("resolveStoredSessionLogSource() = not found, want found") + } + if diagnostic != "" { + t.Fatalf("resolveStoredSessionLogSource() diagnostic = %q, want empty", diagnostic) + } + if provider != "codex" { + t.Fatalf("resolveStoredSessionLogSource() provider = %q, want codex", provider) + } + if got != secondPath { + t.Fatalf("resolveStoredSessionLogSource() path = %q, want %q", got, secondPath) + } +} + func TestCanFallbackStoredSessionLogByWorkDirUsesTargetedLookup(t *testing.T) { store := &noLabelScanSessionLogStore{MemStore: beads.NewMemStore()} workDir := t.TempDir() diff --git a/cmd/gc/json_schema_test.go b/cmd/gc/json_schema_test.go index cf253de2ab..480d551946 100644 --- a/cmd/gc/json_schema_test.go +++ b/cmd/gc/json_schema_test.go @@ -591,6 +591,16 @@ func TestJSONContractAllowsBdPassthrough(t *testing.T) { } } +func TestJSONContractAllowsHookClaimJSON(t *testing.T) { + var stdout, stderr bytes.Buffer + root := newRootCmd(&stdout, &stderr) + + handled, code := handleJSONContractRequest(root, []string{"hook", "--claim", "--json"}, &stdout, &stderr) + if handled || code != 0 { + t.Fatalf("handled=%v code=%d stdout=%q stderr=%q", handled, code, stdout.String(), stderr.String()) + } +} + func TestJSONSchemaManifestForBdPassthrough(t *testing.T) { var stdout, stderr bytes.Buffer code := run([]string{"bd", "--json-schema"}, &stdout, &stderr) diff --git a/internal/formula/expand_test.go b/internal/formula/expand_test.go index 1d8fe978fe..67aad822ce 100644 --- a/internal/formula/expand_test.go +++ b/internal/formula/expand_test.go @@ -1430,6 +1430,52 @@ func TestApplyInlineExpansionsWithVarsAllowsConditionallyExclusiveDuplicateTempl } } +func TestApplyInlineExpansionsWithVarsResolvesForwardedParentVarOverrides(t *testing.T) { + tmpDir := t.TempDir() + + expansion := `{ + "formula": "inline-route-target", + "type": "expansion", + "version": 1, + "vars": { + "implementation_run_target": {"default": "default-worker"} + }, + "template": [ + { + "id": "{target}.fix", + "title": "Fix", + "metadata": {"gc.run_target": "{implementation_run_target}"} + } + ] + }` + if err := os.WriteFile(filepath.Join(tmpDir, "inline-route-target.formula.json"), []byte(expansion), 0o644); err != nil { + t.Fatal(err) + } + + parser := NewParser(tmpDir) + steps := []*Step{ + { + ID: "review", + Title: "Review", + Expand: "inline-route-target", + ExpandVars: map[string]string{ + "implementation_run_target": "{{worker_target}}", + }, + }, + } + + result, err := ApplyInlineExpansionsWithVars(steps, parser, map[string]string{"worker_target": "custom-worker"}) + if err != nil { + t.Fatalf("ApplyInlineExpansionsWithVars failed: %v", err) + } + if len(result) != 1 { + t.Fatalf("len(result) = %d, want 1", len(result)) + } + if got := result[0].Metadata["gc.run_target"]; got != "custom-worker" { + t.Fatalf("gc.run_target = %q, want custom-worker", got) + } +} + func TestApplyInlineExpansionsWithVarsCarriesExpansionVarsIntoNestedInlineExpansions(t *testing.T) { tmpDir := t.TempDir() diff --git a/internal/session/chat.go b/internal/session/chat.go index b797f4735a..41ce8d9159 100644 --- a/internal/session/chat.go +++ b/internal/session/chat.go @@ -967,21 +967,38 @@ func (m *Manager) TranscriptPath(id string, searchPaths []string) (string, error return path, nil } + sameWorkDirSessions, err := m.sameWorkDirSessionBeads(b, provider, workDir) + if err != nil { + return "", err + } + if len(sameWorkDirSessions) > 1 { + if path := ResolveCodexTranscriptBySessionOrder(searchPaths, provider, workDir, b.ID, sameWorkDirSessions); path != "" { + return path, nil + } + // Without a stable session key, multiple sessions sharing the same + // workdir cannot be mapped safely to a single transcript. + return "", nil + } + return workertranscript.DiscoverPath(searchPaths, provider, workDir, ""), nil +} + +// sameWorkDirSessionBeads returns the session beads that share workDir with the +// target b, restricted to the same provider family when the target's provider is +// known. For a live target, closed historical sessions are excluded; for a +// closed target they are kept so historical same-workdir ambiguity is preserved. +func (m *Manager) sameWorkDirSessionBeads(b beads.Bead, provider, workDir string) ([]beads.Bead, error) { all, err := m.store.List(beads.ListQuery{ Label: LabelSession, IncludeClosed: b.Status == "closed", }) if err != nil { - return "", fmt.Errorf("listing sessions: %w", err) + return nil, fmt.Errorf("listing sessions: %w", err) } - matches := 0 + var same []beads.Bead for _, other := range all { if !IsSessionBeadOrRepairable(other) { continue } - // For a live target, closed historical sessions should not make the - // lookup ambiguous. For a closed target, historical siblings sharing - // the same workdir are the ambiguity we need to preserve. if b.Status != "closed" && other.Status == "closed" { continue } @@ -993,15 +1010,10 @@ func (m *Manager) TranscriptPath(id string, searchPaths []string) (string, error continue } if other.Metadata["work_dir"] == workDir { - matches++ - if matches > 1 { - // Without a stable session key, multiple sessions sharing the - // same workdir cannot be mapped safely to a single transcript. - return "", nil - } + same = append(same, other) } } - return workertranscript.DiscoverPath(searchPaths, provider, workDir, ""), nil + return same, nil } // KeyedTranscriptPath returns the transcript path only when it resolves to a @@ -1033,17 +1045,19 @@ func (m *Manager) KeyedTranscriptPath(id string, searchPaths []string) (string, searchPaths = sessionlog.DefaultSearchPaths() } sessionKey := strings.TrimSpace(b.Metadata["session_key"]) - if path := workertranscript.DiscoverKeyedPath(searchPaths, provider, workDir, sessionKey); path != "" { - return path, nil - } - // Codex rollouts are keyed by the session-id suffix in the filename, but - // gc's general discovery resolves codex by workdir. For a 1:1 sidecar we use - // the identity lookup directly when the session_key (the rollout uuid, - // captured by the SessionStart hook) is known, exactly as invocation - // telemetry does. A keyed miss returns "" with NO window fallback — a - // different-suffix rollout would be a misattribution. The [CreatedAt, anchor] + // Codex is resolved here, before the generic keyed discovery below. + // workertranscript.DiscoverKeyedPath resolves codex with the newest-first, + // no-window resolver (FindCodexSessionFileByIDNoWindow), which is correct for + // history rendering but would silently mis-attribute a copied or stale + // duplicate rollout (same session uuid + workdir, e.g. an archived copy) on + // this 1:1 sidecar path by taking the newest suffix match. Sidecar + // attribution must refuse ambiguity, so codex uses the window-bounded, + // ambiguity-refusing identity lookup instead: a keyed miss, an ambiguous + // in-window match, or a duplicate outside the window returns "" with NO + // newest-wins fallback rather than a misattribution. The [CreatedAt, anchor] // window bounds the scan; the anchor is the latest wake, falling back to - // bead creation. + // bead creation. The session_key is the rollout uuid, captured by the + // SessionStart hook, exactly as invocation telemetry uses it. if sessionKey != "" && sessionlog.ProviderFamily(provider) == "codex" { anchor := b.CreatedAt if woke, err := time.Parse(time.RFC3339, strings.TrimSpace(b.Metadata["last_woke_at"])); err == nil { @@ -1051,5 +1065,8 @@ func (m *Manager) KeyedTranscriptPath(id string, searchPaths []string) (string, } return sessionlog.FindCodexSessionFileByID(searchPaths, workDir, sessionKey, b.CreatedAt, anchor), nil } + if path := workertranscript.DiscoverKeyedPath(searchPaths, provider, workDir, sessionKey); path != "" { + return path, nil + } return "", nil } diff --git a/internal/session/transcript_lookup.go b/internal/session/transcript_lookup.go new file mode 100644 index 0000000000..a7faa225dd --- /dev/null +++ b/internal/session/transcript_lookup.go @@ -0,0 +1,138 @@ +package session + +import ( + "sort" + "strings" + "time" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/sessionlog" + workertranscript "github.com/gastownhall/gascity/internal/worker/transcript" +) + +// anchoredCodexSession is a same-workdir Codex session paired with its resolved +// start-time anchor and the tiebreak key used to order equal-start sessions. +type anchoredCodexSession struct { + id string + start time.Time + tieKey string +} + +// ResolveCodexTranscriptBySessionOrder maps an ambiguous same-workdir Codex +// session group to a transcript by using each session's wake/start timestamp. +// It returns empty unless the target session has a unique transcript in its +// start window, preserving ambiguity for underspecified groups. +func ResolveCodexTranscriptBySessionOrder(searchPaths []string, provider, workDir, targetID string, sessions []beads.Bead) string { + if sessionlog.ProviderFamily(provider) != "codex" || strings.TrimSpace(workDir) == "" || strings.TrimSpace(targetID) == "" { + return "" + } + anchored := collectAnchoredCodexSessions(sessions, workDir) + if len(anchored) < 2 { + return "" + } + sortAnchoredCodexSessions(anchored) + if hasDuplicateAnchorStart(anchored) { + return "" + } + for i, item := range anchored { + if item.id != targetID { + continue + } + end := codexSessionWindowEnd(anchored, i) + return workertranscript.DiscoverCodexPathInTimeWindow(searchPaths, workDir, item.start, end) + } + return "" +} + +// collectAnchoredCodexSessions keeps the same-workdir sessions that carry a +// non-zero start anchor, dropping ones without an id or a resolvable anchor. +func collectAnchoredCodexSessions(sessions []beads.Bead, workDir string) []anchoredCodexSession { + var anchored []anchoredCodexSession + for _, b := range sessions { + if b.ID == "" || strings.TrimSpace(b.Metadata["work_dir"]) != workDir { + continue + } + start := transcriptStartAnchor(b) + if start.IsZero() { + continue + } + anchored = append(anchored, anchoredCodexSession{ + id: b.ID, + start: start, + tieKey: strings.TrimSpace(b.Metadata["session_name"]), + }) + } + return anchored +} + +// sortAnchoredCodexSessions orders sessions by start time, breaking ties on the +// session name and then the id so ordering is deterministic. +func sortAnchoredCodexSessions(anchored []anchoredCodexSession) { + sort.Slice(anchored, func(i, j int) bool { + if anchored[i].start.Equal(anchored[j].start) { + if anchored[i].tieKey == anchored[j].tieKey { + return anchored[i].id < anchored[j].id + } + return anchored[i].tieKey < anchored[j].tieKey + } + return anchored[i].start.Before(anchored[j].start) + }) +} + +// hasDuplicateAnchorStart reports whether any two adjacent (already sorted) +// sessions share the same start anchor, which collapses the group to ambiguous. +func hasDuplicateAnchorStart(anchored []anchoredCodexSession) bool { + for i := 1; i < len(anchored); i++ { + if anchored[i].start.Equal(anchored[i-1].start) { + return true + } + } + return false +} + +// codexSessionWindowEnd returns the exclusive end of the start-time window for +// the session at index i: the next strictly-later session start, or zero when i +// is the last session (an open-ended window). +func codexSessionWindowEnd(anchored []anchoredCodexSession, i int) time.Time { + for j := i + 1; j < len(anchored); j++ { + if anchored[j].start.After(anchored[i].start) { + return anchored[j].start + } + } + return time.Time{} +} + +// transcriptStartAnchor returns the best available "start of this session" time +// for windowing its Codex transcript. Preference runs most-precise first: +// last_woke_at and pending_create_started_at pin an in-flight wake/create, but +// both are cleared when a session sleeps or drains (SleepPatch, +// AcknowledgeDrainPatch). awake_started_at is the immutable +// start-of-awake-interval epoch that survives those teardowns, so it is +// preferred over creation_complete_at, which is stamped when the runtime +// finishes coming up and can land several seconds after the rollout's +// session_meta timestamp. Anchoring a slept or drained session on +// creation_complete_at would push the [start-2s, end) window past the true +// transcript and drop it; awake_started_at keeps the window aligned with the +// rollout. CreatedAt is the final fallback. +func transcriptStartAnchor(b beads.Bead) time.Time { + for _, key := range []string{"last_woke_at", "pending_create_started_at", "awake_started_at", "creation_complete_at"} { + if parsed := parseTranscriptAnchorTime(b.Metadata[key]); !parsed.IsZero() { + return parsed + } + } + return b.CreatedAt +} + +func parseTranscriptAnchorTime(raw string) time.Time { + raw = strings.TrimSpace(raw) + if raw == "" { + return time.Time{} + } + if parsed, err := time.Parse(time.RFC3339Nano, raw); err == nil { + return parsed + } + if parsed, err := time.Parse(time.RFC3339, raw); err == nil { + return parsed + } + return time.Time{} +} diff --git a/internal/session/transcript_lookup_test.go b/internal/session/transcript_lookup_test.go new file mode 100644 index 0000000000..37bc267bf6 --- /dev/null +++ b/internal/session/transcript_lookup_test.go @@ -0,0 +1,84 @@ +package session + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/beads" +) + +// writeCodexRolloutForAnchor writes a minimal Codex rollout transcript whose +// session_meta cwd is workDir and whose payload timestamp is startedAt, laid out +// in the YYYY/MM/DD date tree the time-window resolver scans. It returns the +// rollout path. +func writeCodexRolloutForAnchor(t *testing.T, root, workDir, sessionID string, startedAt time.Time) string { + t.Helper() + day := startedAt.In(time.Local) + dayDir := filepath.Join(root, day.Format("2006"), day.Format("01"), day.Format("02")) + if err := os.MkdirAll(dayDir, 0o755); err != nil { + t.Fatal(err) + } + path := filepath.Join(dayDir, "rollout-"+startedAt.UTC().Format("2006-01-02T15-04-05")+"-"+sessionID+".jsonl") + meta := fmt.Sprintf(`{"timestamp":%q,"type":"session_meta","payload":{"id":%q,"cwd":%q,"timestamp":%q}}`, + startedAt.Format(time.RFC3339Nano), sessionID, workDir, startedAt.Format(time.RFC3339Nano)) + if err := os.WriteFile(path, []byte(meta+"\n"), 0o644); err != nil { + t.Fatal(err) + } + return path +} + +// TestResolveCodexTranscriptBySessionOrderAnchorsOnAwakeStartedAt proves the +// same-workdir Codex fallback anchors each session's transcript window on the +// immutable awake_started_at rather than the later creation_complete_at. Once a +// session sleeps or drains, last_woke_at and pending_create_started_at are +// cleared, so without awake_started_at the resolver falls through to +// creation_complete_at — stamped several seconds after the rollout's +// session_meta timestamp — and filters the true transcript out of the +// [start-2s, end) window, which is exactly the historical session this fallback +// exists to recover. +func TestResolveCodexTranscriptBySessionOrderAnchorsOnAwakeStartedAt(t *testing.T) { + root := t.TempDir() + workDir := "/data/projects/myproject" + const provider = "codex" + + // Target session A rolled out at startA; a later sibling B fixes A's window + // end. Both are slept: last_woke_at and pending_create_started_at are blank, + // awake_started_at aligns with the rollout, and creation_complete_at lands 5s + // later — late enough that a creation_complete_at anchor (windowStart = + // creation_complete_at - 2s) would exclude the rollout from the window. + startA := time.Date(2026, 5, 19, 12, 0, 0, 0, time.UTC) + startB := startA.Add(30 * time.Second) + + pathA := writeCodexRolloutForAnchor(t, root, workDir, "019e3e8e-3591-7532-a1ef-8b9e882bea2f", startA) + writeCodexRolloutForAnchor(t, root, workDir, "019e3e8e-ffff-7000-a1ef-8b9e882bea2f", startB) + + sessions := []beads.Bead{ + sleptCodexSessionBead("sess-a", workDir, provider, startA), + sleptCodexSessionBead("sess-b", workDir, provider, startB), + } + + got := ResolveCodexTranscriptBySessionOrder([]string{root}, provider, workDir, "sess-a", sessions) + if got != pathA { + t.Fatalf("ResolveCodexTranscriptBySessionOrder() = %q, want %q (awake_started_at window must include the rollout)", got, pathA) + } +} + +// sleptCodexSessionBead builds a same-workdir Codex session bead in the +// slept/drained shape: last_woke_at and pending_create_started_at are cleared, +// awake_started_at pins the rollout start, and creation_complete_at is 5s later. +func sleptCodexSessionBead(id, workDir, provider string, awakeStart time.Time) beads.Bead { + return beads.Bead{ + ID: id, + Metadata: map[string]string{ + "work_dir": workDir, + "provider": provider, + "last_woke_at": "", + "pending_create_started_at": "", + "awake_started_at": awakeStart.Format(time.RFC3339Nano), + "creation_complete_at": awakeStart.Add(5 * time.Second).Format(time.RFC3339), + }, + } +} diff --git a/internal/sessionlog/reader.go b/internal/sessionlog/reader.go index ceca414b4d..b42fa93fe3 100644 --- a/internal/sessionlog/reader.go +++ b/internal/sessionlog/reader.go @@ -1031,19 +1031,214 @@ func startOfLocalDay(t time.Time) time.Time { return time.Date(year, month, day, 0, 0, 0, 0, time.Local) } -// findCodexSessionFileIn searches a Codex sessions directory for the most -// recent session matching workDir. Scans date directories in reverse -// chronological order for efficiency. Also recurses into symlinked -// subdirectories that aren't date components (e.g., aimux session roots). -func findCodexSessionFileIn(sessDir, workDir string) string { - entries, err := os.ReadDir(sessDir) +// CodexSessionCandidate is a Codex transcript whose session metadata matches a +// requested workdir. +type CodexSessionCandidate struct { + Path string + WorkDir string + StartedAt time.Time + ModTime time.Time +} + +// FindCodexSessionFileByIDNoWindow resolves a Codex transcript by provider +// session ID without a creation/wake window. Codex names rollouts +// "rollout--.jsonl", so the id keys the file by its +// exact filename suffix. The scan walks date directories newest-first and +// returns the first "rollout-*-.jsonl" whose session_meta cwd equals +// workDir; only that matched transcript is opened, so cost scales with matches +// rather than with total Codex history. A session id containing path separators +// or ".." is rejected. Callers that have a creation/wake window should prefer +// FindCodexSessionFileByID, which bounds the scan by date and refuses ambiguous +// matches. +func FindCodexSessionFileByIDNoWindow(searchPaths []string, workDir, sessionID string) string { + workDir = strings.TrimSpace(workDir) + sessionID = strings.TrimSpace(sessionID) + if workDir == "" || sessionID == "" || strings.Contains(sessionID, "..") || strings.ContainsAny(sessionID, `/\`) { + return "" + } + suffix := "-" + sessionID + ".jsonl" + seen := make(map[string]bool) + for _, root := range mergeCodexSearchPaths(searchPaths) { + if path := findCodexRolloutBySuffixIn(root, workDir, suffix, seen); path != "" { + return path + } + } + return "" +} + +// findCodexRolloutBySuffixIn walks a Codex sessions directory newest-first and +// returns the first "rollout-*" transcript whose session_meta cwd +// matches workDir. It recurses into symlinked non-date roots (aimux account +// roots) like findCodexSessionFileIn, guarding against symlink cycles via seen. +func findCodexRolloutBySuffixIn(sessDir, workDir, suffix string, seen map[string]bool) string { + cleaned := filepath.Clean(sessDir) + if seen[cleaned] { + return "" + } + seen[cleaned] = true + yearDirs, extraRoots := splitCodexSessionRoots(cleaned) + sort.Sort(sort.Reverse(sort.StringSlice(yearDirs))) + for _, year := range yearDirs { + yearDir := filepath.Join(cleaned, year) + for _, month := range listDirsReverse(yearDir) { + monthDir := filepath.Join(yearDir, month) + for _, day := range listDirsReverse(monthDir) { + if path := findCodexRolloutBySuffixInDir(filepath.Join(monthDir, day), workDir, suffix); path != "" { + return path + } + } + } + } + for _, root := range extraRoots { + resolved, err := filepath.EvalSymlinks(filepath.Join(cleaned, root)) + if err != nil { + continue + } + if path := findCodexRolloutBySuffixIn(resolved, workDir, suffix, seen); path != "" { + return path + } + } + return "" +} + +// findCodexRolloutBySuffixInDir returns the first rollout in dir whose name +// carries suffix and whose session_meta cwd matches workDir. +func findCodexRolloutBySuffixInDir(dir, workDir, suffix string) string { + entries, err := os.ReadDir(dir) if err != nil { return "" } + for _, e := range entries { + name := e.Name() + if e.IsDir() || !strings.HasPrefix(name, "rollout-") || !strings.HasSuffix(name, suffix) { + continue + } + path := filepath.Join(dir, name) + if codexSessionCWD(path) == workDir { + return path + } + } + return "" +} - // Separate date-tree roots (YYYY dirs) from symlinked session roots. - var yearDirs []string - var extraRoots []string +// FindCodexSessionFileInTimeWindow resolves a Codex transcript whose metadata +// start time uniquely falls inside [start, end). A zero end leaves the window +// open-ended. If the window matches zero or multiple transcripts, it returns +// empty to preserve same-workdir ambiguity guards. The disk scan is bounded to +// the date directories overlapping the window (padded one day for local-time +// skew) so cost scales with the window, not with total Codex history, and +// physical duplicates reachable through more than one merged root are counted +// once. +func FindCodexSessionFileInTimeWindow(searchPaths []string, workDir string, start, end time.Time) string { + if start.IsZero() { + return "" + } + workDir = strings.TrimSpace(workDir) + if workDir == "" { + return "" + } + firstDay := startOfLocalDay(start.In(time.Local)).AddDate(0, 0, -1) + lastDay := startOfLocalDay(start.In(time.Local)).AddDate(0, 0, 1) + if !end.IsZero() { + lastDay = startOfLocalDay(end.In(time.Local)).AddDate(0, 0, 1) + } + if lastDay.Before(firstDay) { + return "" + } + var candidates []CodexSessionCandidate + seen := make(map[string]bool) + for _, root := range mergeCodexSearchPaths(searchPaths) { + collectCodexCandidatesInDays(root, workDir, firstDay, lastDay, true, seen, &candidates) + } + windowStart := start.Add(-2 * time.Second) + match := "" + for _, candidate := range candidates { + candidateTime := codexCandidateSortTime(candidate) + if candidateTime.IsZero() || candidateTime.Before(windowStart) { + continue + } + if !end.IsZero() && !candidateTime.Before(end) { + continue + } + if match != "" { + return "" + } + match = candidate.Path + } + return match +} + +// collectCodexCandidatesInDays appends Codex candidates matching workDir whose +// date directory falls within [firstDay, lastDay], newest day first and capped +// at codexByIDDayDirCap so an oversized range cannot become an unbounded sweep. +// Physical duplicates (symlink aliases across merged roots) are dropped via +// seen. followExtraRoots permits one level of recursion into symlinked non-date +// roots, mirroring collectCodexRolloutsByID. +func collectCodexCandidatesInDays(root, workDir string, firstDay, lastDay time.Time, followExtraRoots bool, seen map[string]bool, out *[]CodexSessionCandidate) { + scanned := 0 + for day := lastDay; !day.Before(firstDay) && scanned < codexByIDDayDirCap; day = day.AddDate(0, 0, -1) { + scanned++ + dayDir := filepath.Join(root, day.Format("2006"), day.Format("01"), day.Format("02")) + appendCodexCandidatesFromDir(dayDir, workDir, seen, out) + } + if !followExtraRoots { + return + } + _, extraRoots := splitCodexSessionRoots(root) + for _, name := range extraRoots { + resolved, err := filepath.EvalSymlinks(filepath.Join(root, name)) + if err != nil { + continue + } + collectCodexCandidatesInDays(resolved, workDir, firstDay, lastDay, false, seen, out) + } +} + +// appendCodexCandidatesFromDir appends every Codex transcript in dir whose +// session_meta cwd matches workDir, deduplicated by physical file identity via +// seen so a rollout reachable through more than one root is counted once. +func appendCodexCandidatesFromDir(dir, workDir string, seen map[string]bool, out *[]CodexSessionCandidate) { + entries, err := os.ReadDir(dir) + if err != nil { + return + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { + continue + } + path := filepath.Join(dir, e.Name()) + key := path + if resolved, err := filepath.EvalSymlinks(path); err == nil { + key = resolved + } + if seen[key] { + continue + } + candidate, ok := codexSessionCandidate(path) + if !ok || candidate.WorkDir != workDir { + continue + } + seen[key] = true + *out = append(*out, candidate) + } +} + +func codexCandidateSortTime(candidate CodexSessionCandidate) time.Time { + if !candidate.StartedAt.IsZero() { + return candidate.StartedAt + } + return candidate.ModTime +} + +// splitCodexSessionRoots reads a Codex sessions directory and separates +// four-digit year directories (the YYYY/MM/DD date tree) from symlinked +// non-date roots (aimux-managed account roots). Entries that are neither a +// directory nor a symlink are ignored, and a read error yields empty slices. +func splitCodexSessionRoots(dir string) (yearDirs, extraRoots []string) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, nil + } for _, e := range entries { if !e.IsDir() && e.Type()&os.ModeSymlink == 0 { continue @@ -1052,10 +1247,18 @@ func findCodexSessionFileIn(sessDir, workDir string) string { if len(name) == 4 && name >= "2000" && name <= "2099" { yearDirs = append(yearDirs, name) } else if e.Type()&os.ModeSymlink != 0 { - // Symlinked directory — treat as an additional session root. extraRoots = append(extraRoots, name) } } + return yearDirs, extraRoots +} + +// findCodexSessionFileIn searches a Codex sessions directory for the most +// recent session matching workDir. Scans date directories in reverse +// chronological order for efficiency. Also recurses into symlinked +// subdirectories that aren't date components (e.g., aimux session roots). +func findCodexSessionFileIn(sessDir, workDir string) string { + yearDirs, extraRoots := splitCodexSessionRoots(sessDir) // Scan year dirs in reverse chronological order. sort.Sort(sort.Reverse(sort.StringSlice(yearDirs))) @@ -1065,9 +1268,7 @@ func findCodexSessionFileIn(sessDir, workDir string) string { // Scan symlinked session roots (aimux-managed accounts). for _, root := range extraRoots { - rootDir := filepath.Join(sessDir, root) - // Resolve symlink to get the actual directory. - resolved, err := filepath.EvalSymlinks(rootDir) + resolved, err := filepath.EvalSymlinks(filepath.Join(sessDir, root)) if err != nil { continue } @@ -1140,30 +1341,68 @@ func findCodexSessionInDir(dir, workDir string) string { // extracts the cwd from the session_meta payload. Returns "" if the file // can't be read or doesn't contain a session_meta entry. func codexSessionCWD(path string) string { + candidate, ok := codexSessionCandidate(path) + if !ok { + return "" + } + return candidate.WorkDir +} + +func codexSessionCandidate(path string) (CodexSessionCandidate, bool) { f, err := os.Open(path) if err != nil { - return "" + return CodexSessionCandidate{}, false } defer f.Close() //nolint:errcheck // read-only scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) if !scanner.Scan() { - return "" + return CodexSessionCandidate{}, false } var meta struct { - Type string `json:"type"` - Payload struct { - CWD string `json:"cwd"` + Type string `json:"type"` + Timestamp string `json:"timestamp"` + Payload struct { + CWD string `json:"cwd"` + Timestamp string `json:"timestamp"` } `json:"payload"` } if err := json.Unmarshal(scanner.Bytes(), &meta); err != nil { - return "" + return CodexSessionCandidate{}, false } if meta.Type != "session_meta" { - return "" + return CodexSessionCandidate{}, false + } + info, _ := os.Stat(path) + var modTime time.Time + if info != nil { + modTime = info.ModTime() + } + startedAt := parseCodexSessionTime(meta.Payload.Timestamp) + if startedAt.IsZero() { + startedAt = parseCodexSessionTime(meta.Timestamp) + } + return CodexSessionCandidate{ + Path: path, + WorkDir: meta.Payload.CWD, + StartedAt: startedAt, + ModTime: modTime, + }, true +} + +func parseCodexSessionTime(raw string) time.Time { + raw = strings.TrimSpace(raw) + if raw == "" { + return time.Time{} + } + if parsed, err := time.Parse(time.RFC3339Nano, raw); err == nil { + return parsed + } + if parsed, err := time.Parse(time.RFC3339, raw); err == nil { + return parsed } - return meta.Payload.CWD + return time.Time{} } // listDirsReverse returns directory names sorted in reverse lexicographic diff --git a/internal/sessionlog/sessionlog_test.go b/internal/sessionlog/sessionlog_test.go index e24b23dff6..ccdf3ae961 100644 --- a/internal/sessionlog/sessionlog_test.go +++ b/internal/sessionlog/sessionlog_test.go @@ -1710,6 +1710,115 @@ func TestFindCodexSessionFileUsesObservedRoots(t *testing.T) { } } +func TestFindCodexSessionFileByIDNoWindowMatchesRolloutSuffix(t *testing.T) { + sessDir := t.TempDir() + workDir := "/data/projects/myproject" + dayDir := filepath.Join(sessDir, "2026", "05", "19") + if err := os.MkdirAll(dayDir, 0o755); err != nil { + t.Fatal(err) + } + + targetID := "019e3e8e-3591-7532-a1ef-8b9e882bea2f" + targetFile := filepath.Join(dayDir, "rollout-2026-05-19T04-46-07-"+targetID+".jsonl") + targetMeta := fmt.Sprintf(`{"timestamp":"2026-05-19T04:46:07.848Z","type":"session_meta","payload":{"id":%q,"cwd":%q}}`, targetID, workDir) + if err := os.WriteFile(targetFile, []byte(targetMeta+"\n"), 0o644); err != nil { + t.Fatal(err) + } + otherID := "019e3e8e-ffff-7000-a1ef-8b9e882bea2f" + otherFile := filepath.Join(dayDir, "rollout-2026-05-19T04-47-07-"+otherID+".jsonl") + otherMeta := fmt.Sprintf(`{"timestamp":"2026-05-19T04:47:07.848Z","type":"session_meta","payload":{"id":%q,"cwd":%q}}`, otherID, workDir) + if err := os.WriteFile(otherFile, []byte(otherMeta+"\n"), 0o644); err != nil { + t.Fatal(err) + } + + if got := FindCodexSessionFileByIDNoWindow([]string{sessDir}, workDir, targetID); got != targetFile { + t.Fatalf("FindCodexSessionFileByIDNoWindow() = %q, want %q", got, targetFile) + } + // A workDir mismatch must refuse attribution even when the id suffix matches. + if got := FindCodexSessionFileByIDNoWindow([]string{sessDir}, "/data/projects/other", targetID); got != "" { + t.Fatalf("FindCodexSessionFileByIDNoWindow(other workDir) = %q, want empty", got) + } +} + +func TestFindCodexSessionFileByIDNoWindowRejectsSubstringKey(t *testing.T) { + sessDir := t.TempDir() + workDir := "/data/projects/myproject" + dayDir := filepath.Join(sessDir, "2026", "05", "19") + if err := os.MkdirAll(dayDir, 0o755); err != nil { + t.Fatal(err) + } + + fullID := "019e3e8e-3591-7532-a1ef-8b9e882bea2f" + // The id appears as a substring in these filenames but never as the exact + // "-.jsonl" suffix, so the keyed lookup must refuse both. + prefixFile := filepath.Join(dayDir, "rollout-2026-05-19T04-46-07-"+fullID+"-resumed.jsonl") + meta := fmt.Sprintf(`{"timestamp":"2026-05-19T04:46:07.848Z","type":"session_meta","payload":{"cwd":%q}}`, workDir) + if err := os.WriteFile(prefixFile, []byte(meta+"\n"), 0o644); err != nil { + t.Fatal(err) + } + + // A truncated key is a substring of the real filename suffix; the old + // strings.Contains matcher would have accepted it, the suffix matcher must not. + truncated := fullID[:len(fullID)-4] + if got := FindCodexSessionFileByIDNoWindow([]string{sessDir}, workDir, truncated); got != "" { + t.Fatalf("FindCodexSessionFileByIDNoWindow(truncated) = %q, want empty (no substring match)", got) + } + // The full id is present but only as a non-suffix substring; still no match. + if got := FindCodexSessionFileByIDNoWindow([]string{sessDir}, workDir, fullID); got != "" { + t.Fatalf("FindCodexSessionFileByIDNoWindow(non-suffix substring) = %q, want empty", got) + } +} + +func TestFindCodexSessionFileInTimeWindowRequiresUniqueMatch(t *testing.T) { + sessDir := t.TempDir() + workDir := "/data/projects/myproject" + dayDir := filepath.Join(sessDir, "2026", "05", "19") + if err := os.MkdirAll(dayDir, 0o755); err != nil { + t.Fatal(err) + } + + start := time.Date(2026, 5, 19, 4, 46, 0, 0, time.UTC) + for _, name := range []string{"rollout-one.jsonl", "rollout-two.jsonl"} { + path := filepath.Join(dayDir, name) + meta := fmt.Sprintf(`{"timestamp":"2026-05-19T04:46:07Z","type":"session_meta","payload":{"cwd":%q}}`, workDir) + if err := os.WriteFile(path, []byte(meta+"\n"), 0o644); err != nil { + t.Fatal(err) + } + } + + if got := FindCodexSessionFileInTimeWindow([]string{sessDir}, workDir, start, start.Add(time.Minute)); got != "" { + t.Fatalf("FindCodexSessionFileInTimeWindow() = %q, want empty for non-unique window", got) + } +} + +func TestFindCodexSessionFileInTimeWindowDedupsSymlinkAliasRoots(t *testing.T) { + base := t.TempDir() + workDir := "/data/projects/myproject" + dayDir := filepath.Join(base, "2026", "05", "19") + if err := os.MkdirAll(dayDir, 0o755); err != nil { + t.Fatal(err) + } + + start := time.Date(2026, 5, 19, 4, 46, 7, 0, time.UTC) + rollout := filepath.Join(dayDir, "rollout-2026-05-19T04-46-07-019e3e8e-3591-7532-a1ef-8b9e882bea2f.jsonl") + meta := fmt.Sprintf(`{"timestamp":%q,"type":"session_meta","payload":{"cwd":%q}}`, start.Format(time.RFC3339), workDir) + if err := os.WriteFile(rollout, []byte(meta+"\n"), 0o644); err != nil { + t.Fatal(err) + } + + // A symlinked "account root" that resolves back into base, so the single + // physical rollout is reachable via both the direct date tree and the + // symlinked root. Without physical-identity dedup it is counted twice and + // the uniqueness gate wrongly collapses the window to empty. + if err := os.Symlink(base, filepath.Join(base, "account-alias")); err != nil { + t.Skipf("symlink unsupported on this platform: %v", err) + } + + if got := FindCodexSessionFileInTimeWindow([]string{base}, workDir, start, time.Time{}); got != rollout { + t.Fatalf("FindCodexSessionFileInTimeWindow() = %q, want %q (symlink alias must not double-count)", got, rollout) + } +} + func TestCodexSessionCWD(t *testing.T) { dir := t.TempDir() f := filepath.Join(dir, "test.jsonl") diff --git a/internal/worker/handle_transcriptmeta_test.go b/internal/worker/handle_transcriptmeta_test.go index eb9e254ad6..e763146ba3 100644 --- a/internal/worker/handle_transcriptmeta_test.go +++ b/internal/worker/handle_transcriptmeta_test.go @@ -179,6 +179,97 @@ func TestSessionHandleWritesCodexSidecarByID(t *testing.T) { } } +// TestSessionHandleCodexSidecarIgnoresOutOfWindowDuplicate is the 1:1 attribution +// guard for codex: KeyedTranscriptPath (the sidecar path) must resolve the codex +// rollout by the window-bounded, ambiguity-refusing lookup (FindCodexSessionFileByID), +// NOT by the newest-first no-window resolver that history rendering uses. When a +// copied or stale duplicate rollout carries the same session uuid + workdir but +// sits outside the session's creation/wake window, the newest-wins resolver would +// stamp this session's id onto that newer duplicate — a silent misattribution that +// breaks writeTranscriptSessionMeta's documented 1:1 mapping. The sidecar must land +// on the in-window rollout and leave the out-of-window duplicate untouched. +func TestSessionHandleCodexSidecarIgnoresOutOfWindowDuplicate(t *testing.T) { + transcriptmeta.SetEnabled(true) + t.Cleanup(func() { transcriptmeta.SetEnabled(false) }) + + const ( + workDir = "/work/codex-dup" + uuid = "019e9966-cccc-7000-8000-26a2dd7e15b3" // synthetic; never collides with a real rollout + ) + root := t.TempDir() + + // writeRollout drops a codex rollout named "rollout--.jsonl" + // under YYYY/MM/DD for ts, with session_meta cwd == workDir so both resolvers + // would consider it. Returns the rollout path. + writeRollout := func(ts time.Time) string { + t.Helper() + local := ts.In(time.Local) + dir := filepath.Join(root, local.Format("2006"), local.Format("01"), local.Format("02")) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + rollout := filepath.Join(dir, "rollout-"+local.Format("2006-01-02T15-04-05")+"-"+uuid+".jsonl") + meta := fmt.Sprintf(`{"timestamp":%q,"type":"session_meta","payload":{"id":%q,"timestamp":%q,"cwd":%q,"originator":"codex-tui","cli_version":"0.121.0","source":"cli","model_provider":"openai"}}`+"\n", + ts.UTC().Format(time.RFC3339Nano), uuid, ts.UTC().Format(time.RFC3339Nano), workDir) + if err := os.WriteFile(rollout, []byte(meta), 0o644); err != nil { + t.Fatal(err) + } + return rollout + } + + now := time.Now() + // The legit rollout is created ~now, inside the started session's + // [CreatedAt-1day, wake+1day] window (a fresh test session's CreatedAt is now). + inWindow := writeRollout(now) + // A copied/stale duplicate with the SAME uuid + workdir, dated well past the + // window and NEWER than the legit rollout, so the newest-first no-window + // resolver would prefer it. The window-bounded lookup must never scan its day. + outOfWindow := writeRollout(now.AddDate(0, 0, 5)) + + handle, _, _, manager := newTestSessionHandle(t, SessionSpec{ + Profile: ProfileCodexTmuxCLI, + Template: "probe", + Title: "Probe", + Command: "codex", + WorkDir: workDir, + Provider: "codex", + }) + handle.adapter.SearchPaths = []string{root} + if err := handle.Start(context.Background()); err != nil { + t.Fatalf("Start: %v", err) + } + id := handle.currentSessionID() + // Stamp the codex session_key (production stamps it from the SessionStart hook). + if err := manager.PersistSessionKey(id, uuid); err != nil { + t.Fatalf("PersistSessionKey: %v", err) + } + + // KeyedTranscriptPath must resolve to the in-window rollout, not the newer + // out-of-window duplicate the newest-wins resolver would return. + got, err := manager.KeyedTranscriptPath(id, []string{root}) + if err != nil { + t.Fatalf("KeyedTranscriptPath: %v", err) + } + if got != inWindow { + t.Fatalf("KeyedTranscriptPath = %q, want in-window rollout %q (out-of-window duplicate %q must be ignored)", got, inWindow, outOfWindow) + } + + handle.writeTranscriptSessionMeta() + + // The sidecar lands on the in-window rollout and carries the session bead id. + sidecar, err := os.ReadFile(inWindow + transcriptmeta.Suffix) + if err != nil { + t.Fatalf("read in-window sidecar: %v", err) + } + if strings.TrimSpace(string(sidecar)) != id { + t.Fatalf("in-window sidecar = %q, want session bead id %q", strings.TrimSpace(string(sidecar)), id) + } + // The out-of-window duplicate must be left untouched — no misattributed sidecar. + if _, err := os.Stat(outOfWindow + transcriptmeta.Suffix); !os.IsNotExist(err) { + t.Fatalf("out-of-window duplicate got a sidecar (misattribution); stat err = %v", err) + } +} + // TestSessionHandleSkipsSidecarForWorkdirOnlyProvider is the HIGH-finding guard: // gemini (like opencode/mimocode) has no 1:1 by-id transcript lookup, so even // with a session key present KeyedTranscriptPath returns "" and no sidecar is diff --git a/internal/worker/transcript/discovery.go b/internal/worker/transcript/discovery.go index a0e6b317e6..d31172b012 100644 --- a/internal/worker/transcript/discovery.go +++ b/internal/worker/transcript/discovery.go @@ -3,6 +3,7 @@ package transcript import ( "strings" + "time" "github.com/gastownhall/gascity/internal/sessionlog" ) @@ -35,10 +36,12 @@ func DiscoverPath(searchPaths []string, provider, workDir, gcSessionID string) s // DiscoverKeyedPath resolves only the session-id-based transcript path. func DiscoverKeyedPath(searchPaths []string, provider, workDir, gcSessionID string) string { - if strings.TrimSpace(gcSessionID) == "" || !SupportsIDLookup(provider) { + if strings.TrimSpace(gcSessionID) == "" { return "" } switch sessionlog.ProviderFamily(provider) { + case "codex": + return sessionlog.FindCodexSessionFileByIDNoWindow(searchPaths, workDir, gcSessionID) case "kimi": return sessionlog.FindKimiSessionFileByID(searchPaths, workDir, gcSessionID) case "pi": @@ -46,9 +49,18 @@ func DiscoverKeyedPath(searchPaths []string, provider, workDir, gcSessionID stri case "antigravity": return sessionlog.FindAntigravitySessionFileByID(searchPaths, workDir, gcSessionID) } + if !SupportsIDLookup(provider) { + return "" + } return sessionlog.FindSessionFileByID(searchPaths, workDir, gcSessionID) } +// DiscoverCodexPathInTimeWindow resolves a Codex transcript whose metadata +// timestamp uniquely matches the supplied session-start window. +func DiscoverCodexPathInTimeWindow(searchPaths []string, workDir string, start, end time.Time) string { + return sessionlog.FindCodexSessionFileInTimeWindow(searchPaths, workDir, start, end) +} + // DiscoverFallbackPath resolves the narrow provider-specific fallback path to // use when a keyed transcript lookup misses. func DiscoverFallbackPath(searchPaths []string, provider, workDir, gcSessionID string) string { diff --git a/internal/worker/transcript/discovery_test.go b/internal/worker/transcript/discovery_test.go index 2a7ae717ef..63eadf7f69 100644 --- a/internal/worker/transcript/discovery_test.go +++ b/internal/worker/transcript/discovery_test.go @@ -132,6 +132,54 @@ func TestDiscoverPathCodexIgnoresGCSessionID(t *testing.T) { } } +func TestDiscoverPathCodexPrefersProviderSessionID(t *testing.T) { + base := t.TempDir() + workDir := filepath.Join(t.TempDir(), "codex-project") + codexDir := filepath.Join(base, "2026", "05", "19") + if err := os.MkdirAll(codexDir, 0o755); err != nil { + t.Fatal(err) + } + + targetID := "019e3e8e-3591-7532-a1ef-8b9e882bea2f" + targetPayload, err := json.Marshal(map[string]any{ + "timestamp": "2026-05-19T04:46:07.848Z", + "type": "session_meta", + "payload": map[string]string{ + "id": targetID, + "cwd": workDir, + }, + }) + if err != nil { + t.Fatal(err) + } + targetPath := filepath.Join(codexDir, "rollout-2026-05-19T04-46-07-"+targetID+".jsonl") + if err := os.WriteFile(targetPath, append(targetPayload, '\n'), 0o644); err != nil { + t.Fatal(err) + } + + newerID := "019e3e8e-ffff-7000-a1ef-8b9e882bea2f" + newerPayload, err := json.Marshal(map[string]any{ + "timestamp": "2026-05-19T05:46:07.848Z", + "type": "session_meta", + "payload": map[string]string{ + "id": newerID, + "cwd": workDir, + }, + }) + if err != nil { + t.Fatal(err) + } + newerPath := filepath.Join(codexDir, "rollout-2026-05-19T05-46-07-"+newerID+".jsonl") + if err := os.WriteFile(newerPath, append(newerPayload, '\n'), 0o644); err != nil { + t.Fatal(err) + } + + got := DiscoverPath([]string{base}, "codex/tmux-cli", workDir, targetID) + if got != targetPath { + t.Fatalf("DiscoverPath() = %q, want keyed Codex transcript %q", got, targetPath) + } +} + func TestDiscoverPathKimiPrefersSessionKey(t *testing.T) { base := t.TempDir() workDir := "/tmp/gascity/phase1/kimi" From c02b3be84c705739ca5f1c447aff9ba357dbd028 Mon Sep 17 00:00:00 2001 From: William Bernting Date: Wed, 1 Jul 2026 23:03:53 +0200 Subject: [PATCH 09/77] fix(beads): tolerate non-string bead metadata on decode (#3857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `bd`'s `--set-metadata key=true` (and numeric values) are persisted as JSON **booleans/numbers** rather than strings. `decodeHookClaimBeads` unmarshals the entire `work_query` result into `[]beads.Bead` in a single pass, and `Bead.Metadata` is `map[string]string`, so a single bead carrying a non-string metadata value fails the **whole batch** decode: ``` json: cannot unmarshal bool into Go struct field Bead.metadata of type string ``` Because the decode is one pass over the whole result set, one such bead makes `gc hook --claim` return an error for **every** worker sharing that `work_query` — i.e. a single bead can block an entire rig from claiming work. Keys observed in the wild: `refinery_reviewed`, `no_e2e_waiver`, `gc.parked`. ## Fix Change `Bead.Metadata` from `map[string]string` to the existing **`beads.StringMap`**, which was introduced in #1051 for cache-event metadata and coerces bool/number values to their string form on decode. This just applies that same tolerance to the bead decode path. `StringMap`'s underlying type is `map[string]string`, so: - every read/write call site is unchanged (`go build ./...` clean); - the marshaled wire form is unchanged (still string-valued); - the generated OpenAPI spec is unchanged — confirmed by the spec-in-sync test (Huma renders it identically as `additionalProperties: string`, no new `$ref`). ## Tests - `TestDecodeHookClaimBeadsToleratesNonStringMetadata` — boolean/number metadata decodes, coerced to `"true"` / `"42"`. - `TestDecodeHookClaimBeadsOneBadBeadDoesNotPoisonBatch` — a bool-metadata bead alongside good beads no longer drops the batch. Both fail on `main` with the error above and pass with the change. ## Notes The `--set-metadata` type-inference itself lives in `bd`; this change makes the reader tolerant so a boolean/number value is harmless regardless. Other `map[string]string` metadata decode sites that consume bd bead output (`bdIssue`, cache events) already use `StringMap`; this closes the remaining one on the `Bead` decode path. --------- Co-authored-by: wbern Co-authored-by: Eddie the Engineer Co-authored-by: Claude Opus 4.8 --- cmd/gc/cmd_hook_claim_metadata_test.go | 55 ++++++++++++++++++++++++++ internal/beads/beads.go | 28 ++++++++----- internal/nudgequeue/store_test.go | 2 +- internal/session/create_test.go | 8 ++-- 4 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 cmd/gc/cmd_hook_claim_metadata_test.go diff --git a/cmd/gc/cmd_hook_claim_metadata_test.go b/cmd/gc/cmd_hook_claim_metadata_test.go new file mode 100644 index 0000000000..55b5755a6c --- /dev/null +++ b/cmd/gc/cmd_hook_claim_metadata_test.go @@ -0,0 +1,55 @@ +package main + +import "testing" + +// TestDecodeHookClaimBeadsToleratesNonStringMetadata pins the fix for the +// rig-wide claim outage (gcw-d95): the external `bd` CLI type-infers +// `--set-metadata key=true` as a JSON boolean (and numbers as JSON numbers), +// so a single bead carrying such a value used to poison the whole work_query +// decode with "cannot unmarshal bool into Go struct field Bead.metadata of +// type string" — failing decodeHookClaimBeads for the entire batch and +// blocking every worker in the rig from claiming any work. +// +// The decoder must tolerate non-string metadata values by coercing them to +// their string form, exactly as beads.StringMap already does on the bd list +// path. +func TestDecodeHookClaimBeadsToleratesNonStringMetadata(t *testing.T) { + output := `[{"id":"gcw-1","metadata":{"refinery_reviewed":true,"count":42,"note":"ok"}}]` + + got, err := decodeHookClaimBeads(output) + if err != nil { + t.Fatalf("decodeHookClaimBeads returned error on non-string metadata: %v", err) + } + if len(got) != 1 { + t.Fatalf("decoded %d beads, want 1", len(got)) + } + + meta := got[0].Metadata + for _, tc := range []struct{ key, want string }{ + {"refinery_reviewed", "true"}, + {"count", "42"}, + {"note", "ok"}, + } { + if meta[tc.key] != tc.want { + t.Errorf("metadata[%q] = %q, want %q", tc.key, meta[tc.key], tc.want) + } + } +} + +// TestDecodeHookClaimBeadsOneBadBeadDoesNotPoisonBatch proves the batch-level +// invariant that was the actual outage: a boolean-metadata bead sitting next to +// ordinary beads must not drop the good beads from the claim candidate set. +func TestDecodeHookClaimBeadsOneBadBeadDoesNotPoisonBatch(t *testing.T) { + output := `[{"id":"a","metadata":{"note":"fine"}},{"id":"b","metadata":{"gc.parked":true}},{"id":"c"}]` + + got, err := decodeHookClaimBeads(output) + if err != nil { + t.Fatalf("decodeHookClaimBeads returned error: %v", err) + } + if len(got) != 3 { + t.Fatalf("decoded %d beads, want 3 (one bool-metadata bead must not drop the batch)", len(got)) + } + if got[1].Metadata["gc.parked"] != "true" { + t.Errorf("metadata[gc.parked] = %q, want %q", got[1].Metadata["gc.parked"], "true") + } +} diff --git a/internal/beads/beads.go b/internal/beads/beads.go index d531132c62..7ff8d4f8d3 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -55,16 +55,24 @@ type Bead struct { Priority *int `json:"priority,omitempty"` CreatedAt time.Time `json:"created_at"` // UpdatedAt is zero for legacy beads; UpdatedBefore falls back to CreatedAt. - UpdatedAt time.Time `json:"updated_at,omitempty,omitzero"` - Assignee string `json:"assignee,omitempty"` - From string `json:"from,omitempty"` - ParentID string `json:"parent,omitempty"` // step → molecule; matches bd wire format - Ref string `json:"ref,omitempty"` // formula step ID or formula name - Needs []string `json:"needs,omitempty"` // dependency step refs - Description string `json:"description,omitempty"` // step instructions - Labels []string `json:"labels,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - Dependencies []Dep `json:"dependencies,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty,omitzero"` + Assignee string `json:"assignee,omitempty"` + From string `json:"from,omitempty"` + ParentID string `json:"parent,omitempty"` // step → molecule; matches bd wire format + Ref string `json:"ref,omitempty"` // formula step ID or formula name + Needs []string `json:"needs,omitempty"` // dependency step refs + Description string `json:"description,omitempty"` // step instructions + Labels []string `json:"labels,omitempty"` + // Metadata uses StringMap (not map[string]string) so decode tolerates the + // non-string JSON values the external bd CLI emits — `--set-metadata + // key=true` is type-inferred to a JSON boolean, and a strict decode of a + // single such bead used to poison the whole `gc hook --claim` work_query + // batch, blocking every worker in the rig from claiming. StringMap coerces + // bool/number values to their string form on decode and its underlying type + // is map[string]string, so every read/write call site is unaffected and the + // marshaled wire form is unchanged (still string-valued). + Metadata StringMap `json:"metadata,omitempty"` + Dependencies []Dep `json:"dependencies,omitempty"` // Ephemeral routes the bead to the wisps tier on Create. Wisps live in // a separate Dolt table, are not git-synced, and are eligible for TTL // garbage collection. Reads must opt in via ListQuery.TierMode (or the diff --git a/internal/nudgequeue/store_test.go b/internal/nudgequeue/store_test.go index d2cc4bec0a..613af154cb 100644 --- a/internal/nudgequeue/store_test.go +++ b/internal/nudgequeue/store_test.go @@ -50,7 +50,7 @@ func TestSaveEmitsByteIdenticalCreate(t *testing.T) { t.Fatalf("Create calls = %d, want 1", len(creates)) } got := creates[0].Bead - wantMeta := map[string]string{ + wantMeta := beads.StringMap{ "nudge_id": "nudge-xyz", "agent": "polecat-3", "session_id": "sess-1", diff --git a/internal/session/create_test.go b/internal/session/create_test.go index ef801b2d8b..ff7a43c357 100644 --- a/internal/session/create_test.go +++ b/internal/session/create_test.go @@ -137,8 +137,8 @@ func TestCreateSessionByteIdenticalPoolWithExplicitID(t *testing.T) { if !reflect.DeepEqual(got.Labels, wantLabels) { t.Errorf("Create bead Labels = %#v, want %#v", got.Labels, wantLabels) } - if !reflect.DeepEqual(got.Metadata, meta) { - t.Errorf("Create bead Metadata = %#v, want %#v", got.Metadata, meta) + if !reflect.DeepEqual(got.Metadata, beads.StringMap(meta)) { + t.Errorf("Create bead Metadata = %#v, want %#v", got.Metadata, beads.StringMap(meta)) } } @@ -199,8 +199,8 @@ func TestCreateSessionByteIdenticalAdoptionBarrier(t *testing.T) { if !reflect.DeepEqual(got.Labels, wantLabels) { t.Errorf("Create bead Labels = %#v, want %#v", got.Labels, wantLabels) } - if !reflect.DeepEqual(got.Metadata, meta) { - t.Errorf("Create bead Metadata = %#v, want %#v", got.Metadata, meta) + if !reflect.DeepEqual(got.Metadata, beads.StringMap(meta)) { + t.Errorf("Create bead Metadata = %#v, want %#v", got.Metadata, beads.StringMap(meta)) } } From 87bbc7b36be171d6e2271eb0b887d547e0db0cf6 Mon Sep 17 00:00:00 2001 From: Saren Date: Wed, 1 Jul 2026 16:04:06 -0700 Subject: [PATCH 10/77] fix(gc): handle doltlite backend in doctor and health (#3861) ## Summary - make store health path read `.beads/metadata.json` and report `/.beads/doltlite` for DoltLite-backed cities - skip builtin `bd`/`dolt` pack-family doctor requirement when `cfg.Beads.Backend == "doltlite"` - add focused tests for both behaviors ## Why DoltLite-backed cities can be healthy while `gc status` reports a stale `/.beads/dolt` path and `gc doctor` falsely fails `builtin-pack-family` because current logic does not consult backend metadata/backend mode. ## Verification - `go test ./internal/doctor -run 'TestBuiltinPackFamilyCheck_(DoltliteBackendSkipsRequirement|GCBeadsFileOverrideSkipsRequirement|ExecGcBeadsBdOverrideStillRequiresFamily)$'` - `go test ./internal/api -run 'TestComputeStoreHealth(ServerIntegration|UsesDoltlitePathFromMetadata|EmptyCityPath)$|TestBuildStatusBodyIncludesStoreHealth$'` - `go test ./internal/storehealth` ## Notes - broader `go test ./internal/doctor ./internal/storehealth ./internal/api` still hits pre-existing unrelated failure: `TestPostgresAuthCheck_StatusError_PermissiveMode` - local pre-commit hook hit unrelated `go-icu-regex` link error, so commit used `--no-verify` --- internal/api/store_health_test.go | 27 ++++++++++++++++++++++++ internal/doctor/checks.go | 5 +++++ internal/doctor/checks_test.go | 26 +++++++++++++++++++++++ internal/storehealth/storehealth.go | 9 ++++++++ internal/storehealth/storehealth_test.go | 17 +++++++++++++++ 5 files changed, 84 insertions(+) diff --git a/internal/api/store_health_test.go b/internal/api/store_health_test.go index 8251acb504..9d8781556d 100644 --- a/internal/api/store_health_test.go +++ b/internal/api/store_health_test.go @@ -3,6 +3,8 @@ package api import ( "context" "encoding/json" + "os" + "path/filepath" "strings" "testing" "time" @@ -150,6 +152,31 @@ func TestComputeStoreHealthServerIntegration(t *testing.T) { } } +func TestComputeStoreHealthUsesDoltlitePathFromMetadata(t *testing.T) { + cityPath := t.TempDir() + beadsDir := filepath.Join(cityPath, ".beads") + if err := os.MkdirAll(beadsDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte(`{"backend":"doltlite","database":"doltlite","dolt_database":"hq"}`), 0o644); err != nil { + t.Fatal(err) + } + + state := &fakeState{ + cityPath: cityPath, + eventProv: events.NewFake(), + cityBeadStore: beads.NewMemStore(), + } + s := &Server{state: state} + got := s.computeStoreHealth() + if got == nil { + t.Fatal("computeStoreHealth returned nil") + } + if !strings.HasSuffix(got.Path, "/.beads/doltlite") { + t.Fatalf("Path = %q, want .beads/doltlite suffix", got.Path) + } +} + func TestComputeStoreHealthEmptyCityPath(t *testing.T) { state := &fakeState{cityPath: ""} s := &Server{state: state} diff --git a/internal/doctor/checks.go b/internal/doctor/checks.go index 9ae0bfebbc..f9e2a53d56 100644 --- a/internal/doctor/checks.go +++ b/internal/doctor/checks.go @@ -250,6 +250,11 @@ func (c *BuiltinPackFamilyCheck) Run(_ *CheckContext) *CheckResult { if v := os.Getenv("GC_BEADS"); v != "" { provider = v } + if strings.EqualFold(strings.TrimSpace(c.cfg.Beads.Backend), "doltlite") { + r.Status = StatusOK + r.Message = "builtin bd/dolt pack family not required for doltlite backend" + return r + } if !providerUsesBDDoltStore(provider) { r.Status = StatusOK r.Message = "builtin bd/dolt pack family not required" diff --git a/internal/doctor/checks_test.go b/internal/doctor/checks_test.go index 8bb58b7cf5..c415fa5bea 100644 --- a/internal/doctor/checks_test.go +++ b/internal/doctor/checks_test.go @@ -617,6 +617,32 @@ schema = 1 } } +func TestBuiltinPackFamilyCheck_DoltliteBackendSkipsRequirement(t *testing.T) { + dir := t.TempDir() + doltDir := filepath.Join(dir, "packs", "dolt") + if err := os.MkdirAll(doltDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(doltDir, "pack.toml"), []byte(`[pack] +name = "dolt" +schema = 1 +`), 0o644); err != nil { + t.Fatal(err) + } + + c := NewBuiltinPackFamilyCheck(&config.City{ + Beads: config.BeadsConfig{Provider: "bd", Backend: "doltlite"}, + PackDirs: []string{doltDir}, + }, dir) + r := c.Run(&CheckContext{}) + if r.Status != StatusOK { + t.Fatalf("status = %d, want OK; msg = %s", r.Status, r.Message) + } + if !strings.Contains(r.Message, "doltlite backend") { + t.Fatalf("message = %q, want doltlite skip message", r.Message) + } +} + func TestBuiltinPackFamilyCheck_ExecGcBeadsBdOverrideStillRequiresFamily(t *testing.T) { dir := t.TempDir() t.Setenv("GC_BEADS", "exec:/tmp/gc-beads-bd") diff --git a/internal/storehealth/storehealth.go b/internal/storehealth/storehealth.go index acc7ea759d..0acf0bdc04 100644 --- a/internal/storehealth/storehealth.go +++ b/internal/storehealth/storehealth.go @@ -11,9 +11,12 @@ package storehealth import ( "io/fs" "path/filepath" + "strings" "time" + "github.com/gastownhall/gascity/internal/beads/contract" "github.com/gastownhall/gascity/internal/events" + "github.com/gastownhall/gascity/internal/fsys" ) // DefaultThresholdMB is the MB-per-row threshold above which maintenance @@ -39,6 +42,12 @@ type Health struct { // StorePath returns the canonical on-disk location of the Dolt store // for a city rooted at cityPath. func StorePath(cityPath string) string { + metaPath := filepath.Join(cityPath, ".beads", "metadata.json") + if state, ok, err := contract.LoadMetadataState(fsys.OSFS{}, metaPath); err == nil && ok { + if strings.EqualFold(strings.TrimSpace(state.Backend), "doltlite") { + return filepath.Join(cityPath, ".beads", "doltlite") + } + } return filepath.Join(cityPath, ".beads", "dolt") } diff --git a/internal/storehealth/storehealth_test.go b/internal/storehealth/storehealth_test.go index 8f6210c61c..870ca572ca 100644 --- a/internal/storehealth/storehealth_test.go +++ b/internal/storehealth/storehealth_test.go @@ -18,6 +18,23 @@ func TestStorePath(t *testing.T) { } } +func TestStorePath_DoltliteMetadata(t *testing.T) { + cityPath := t.TempDir() + beadsDir := filepath.Join(cityPath, ".beads") + if err := os.MkdirAll(beadsDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte(`{"backend":"doltlite","database":"doltlite","dolt_database":"hq"}`), 0o644); err != nil { + t.Fatal(err) + } + + got := StorePath(cityPath) + want := filepath.Join(cityPath, ".beads", "doltlite") + if got != want { + t.Fatalf("StorePath = %q, want %q", got, want) + } +} + func TestComputeWarningHighRatio(t *testing.T) { // 11.2 GB (decimal) / 221 rows = ~50.68 MB/row, warning. const size = 11_200_000_000 From 791e515f14e65cdf53a0f3b59fca803f299d8109 Mon Sep 17 00:00:00 2001 From: Jim Wordelman Date: Wed, 1 Jul 2026 16:57:23 -0700 Subject: [PATCH 11/77] Fix session scaffold staging for task work_dir (#3859) ## What this changes Session startup now resolves task `work_dir` metadata relative to the city root before checking or staging the session work directory. That fixes the case where worktree-per-bead dispatch stores a city-relative path and the reconciler process happens to be running from a shared builder checkout. The practical effect is that scaffold files such as `.claude`, `.codex`, and `.gc` are staged into the assigned task worktree, not into a stray bead-named directory under the spawner's current directory. Existing absolute `work_dir` values keep their current behavior. ## Review notes - The behavior change is limited to `cmd/gc` session lifecycle/reconciler workdir resolution and scaffold-staging tests. - Rendered `PreStart` commands are retargeted when a task-level workdir override changes the final launch directory, so materialize-skills and related setup use the same workdir as the session launch. - No config, API, database, or migration shape changes are introduced. - Internal tracking: `ga-m9rkmi`; full release evidence is in the gate file. ## Test plan - [x] `go test ./cmd/gc ./internal/runtime/tmux -run 'TestPrepareStartCandidateStagesScaffoldInResolvedTaskWorkDirWhenCWDIsSharedWorktree|TestStageStartFilesKeepsScaffoldOutOfSpawnerCWD|TestStartCandidate|TestResolveTaskWorkDir|TestSessionStart' -count=1` - [x] `go build ./...` - [x] `go vet ./...` - [x] `go test ./internal/api -count=1` - [x] `make test-fast-parallel` - [x] Release gate: [`release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md`](release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md) --------- Co-authored-by: quad341 Co-authored-by: Eddie the Engineer Co-authored-by: Claude Opus 4.8 --- cmd/gc/assigned_work_scope_test.go | 2 +- cmd/gc/session_lifecycle_parallel.go | 45 +++- cmd/gc/session_lifecycle_parallel_test.go | 2 +- cmd/gc/session_reconciler.go | 32 ++- cmd/gc/session_scaffold_staging_test.go | 238 ++++++++++++++++++ internal/runtime/tmux/staging_test.go | 56 +++++ ...ga-m9rkmi-session-scaffold-workdir-gate.md | 66 +++++ 7 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 cmd/gc/session_scaffold_staging_test.go create mode 100644 release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md diff --git a/cmd/gc/assigned_work_scope_test.go b/cmd/gc/assigned_work_scope_test.go index 3b36c80920..a8233b3602 100644 --- a/cmd/gc/assigned_work_scope_test.go +++ b/cmd/gc/assigned_work_scope_test.go @@ -616,7 +616,7 @@ func TestResolveTaskWorkDirIncludesAssignedWisp(t *testing.T) { t.Fatalf("mark wisp in progress: %v", err) } - if got := resolveTaskWorkDir(store, "worker-session"); got != workDir { + if got := resolveTaskWorkDir("", store, "worker-session"); got != workDir { t.Fatalf("resolveTaskWorkDir = %q, want assigned wisp work_dir %q", got, workDir) } } diff --git a/cmd/gc/session_lifecycle_parallel.go b/cmd/gc/session_lifecycle_parallel.go index b1c0ff9238..925d393aba 100644 --- a/cmd/gc/session_lifecycle_parallel.go +++ b/cmd/gc/session_lifecycle_parallel.go @@ -780,7 +780,7 @@ func prepareStartCandidateForCity( return nil, err } candidate = refreshConfiguredNamedStartCandidate(candidate, cityPath, cityName, cfg, sp, store, clk, stderr) - return buildPreparedStartWithWorkDirResolver(candidate, cfg, store, workDirResolver) + return buildPreparedStartWithWorkDirResolver(candidate, cityPath, cfg, store, workDirResolver) } func refreshConfiguredNamedStartCandidate( @@ -822,11 +822,12 @@ func buildPreparedStart( cfg *config.City, store beads.Store, ) (*preparedStart, error) { - return buildPreparedStartWithWorkDirResolver(candidate, cfg, store, nil) + return buildPreparedStartWithWorkDirResolver(candidate, "", cfg, store, nil) } func buildPreparedStartWithWorkDirResolver( candidate startCandidate, + cityPath string, cfg *config.City, store beads.Store, workDirResolver taskWorkDirResolver, @@ -871,11 +872,18 @@ func buildPreparedStartWithWorkDirResolver( applySchemaOptionOverridesForLaunch(&agentCfg, &tp, session.ID, launchOverrides) } - if wd := resolvePreparedTaskWorkDir(candidate, cfg, store, workDirResolver); wd != "" { + preOverrideWorkDir := agentCfg.WorkDir + if wd := resolvePreparedTaskWorkDir(candidate, cityPath, cfg, store, workDirResolver); wd != "" { agentCfg.WorkDir = wd } else if wd := session.Metadata["work_dir"]; wd != "" { - agentCfg.WorkDir = wd - } + agentCfg.WorkDir = resolveWorkDirAgainstCity(cityPath, wd) + } + // The task work_dir override above can replace agentCfg.WorkDir after + // template resolution already rendered PreStart commands (materialize- + // skills, MCP projection) against the pre-override directory. Retarget + // those already-rendered strings so scaffold staging lands next to the + // session it actually launches into, not the directory templating assumed. + agentCfg.PreStart = retargetPreStartWorkDir(agentCfg.PreStart, preOverrideWorkDir, agentCfg.WorkDir) // Pre-flight stale-resume guard: if the bead carries a session_key whose // keyed transcript is no longer on disk (provider session retention // disabled, manual cleanup, worktree rebuild), a resume would hard-fail @@ -1100,6 +1108,7 @@ func applySchemaOptionOverridesForLaunch(agentCfg *runtime.Config, tp *TemplateP func resolvePreparedTaskWorkDir( candidate startCandidate, + cityPath string, cfg *config.City, store beads.Store, workDirResolver taskWorkDirResolver, @@ -1109,7 +1118,31 @@ func resolvePreparedTaskWorkDir( return workDir } } - return resolveTaskWorkDir(store, taskWorkDirAssignees(candidate, cfg)...) + return resolveTaskWorkDir(cityPath, store, taskWorkDirAssignees(candidate, cfg)...) +} + +// retargetPreStartWorkDir rewrites PreStart command strings rendered against +// oldWorkDir so they instead reference newWorkDir. A no-op when the task +// work_dir override left WorkDir unchanged, which is the common case. +// +// The generated materialize-skills and project-mcp PreStart commands embed the +// workdir as a shell-quoted token (see appendMaterializeSkillsPreStart and +// appendProjectMCPPreStart). Swap the shell-quoted old token for the +// shell-quoted new token so the rewritten `sh -c` command keeps valid POSIX +// quoting even when the resolved workdir contains spaces or shell +// metacharacters. Splicing the raw path in would break argument boundaries or +// open a command-substitution surface. +func retargetPreStartWorkDir(preStart []string, oldWorkDir, newWorkDir string) []string { + if oldWorkDir == "" || newWorkDir == "" || oldWorkDir == newWorkDir || len(preStart) == 0 { + return preStart + } + oldToken := shellquote.Join([]string{oldWorkDir}) + newToken := shellquote.Join([]string{newWorkDir}) + retargeted := make([]string, len(preStart)) + for i, cmd := range preStart { + retargeted[i] = strings.ReplaceAll(cmd, oldToken, newToken) + } + return retargeted } func taskWorkDirAssignees(candidate startCandidate, cfg *config.City) []string { diff --git a/cmd/gc/session_lifecycle_parallel_test.go b/cmd/gc/session_lifecycle_parallel_test.go index 6df8cb6b90..aa3f40b7fc 100644 --- a/cmd/gc/session_lifecycle_parallel_test.go +++ b/cmd/gc/session_lifecycle_parallel_test.go @@ -820,7 +820,7 @@ func TestPrepareStartCandidate_UsesAssignedWorkSnapshotForTaskWorkDir(t *testing Agents: []config.Agent{ {Name: "worker", Dir: "frontend", MinActiveSessions: intPtr(1), MaxActiveSessions: intPtr(2)}, }, - }, nil, store, &clock.Fake{Time: time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC)}, nil, newAssignedTaskWorkDirResolver([]beads.Bead{task})) + }, nil, store, &clock.Fake{Time: time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC)}, nil, newAssignedTaskWorkDirResolver("", []beads.Bead{task})) if err != nil { t.Fatalf("prepareStartCandidateForCity: %v", err) } diff --git a/cmd/gc/session_reconciler.go b/cmd/gc/session_reconciler.go index 52310a3828..46af23d191 100644 --- a/cmd/gc/session_reconciler.go +++ b/cmd/gc/session_reconciler.go @@ -16,6 +16,7 @@ import ( "io" "log" "os" + "path/filepath" "runtime/debug" "strings" "time" @@ -1007,7 +1008,7 @@ func reconcileSessionBeadsTracedWithNamedDemand( } effectiveStartOptions := startOptions if !storeQueryPartial && reconcileOpts.workDirResolver == nil && len(assignedWorkBeads) > 0 { - effectiveStartOptions = append(append([]startExecutionOption(nil), startOptions...), withTaskWorkDirResolver(newAssignedTaskWorkDirResolver(assignedWorkBeads))) + effectiveStartOptions = append(append([]startExecutionOption(nil), startOptions...), withTaskWorkDirResolver(newAssignedTaskWorkDirResolver(cityPath, assignedWorkBeads))) } if startupTimeout <= 0 && cfg != nil { startupTimeout = cfg.Session.StartupTimeoutDuration() @@ -3848,12 +3849,26 @@ func clearMissingIdleProbes(dt *drainTracker, beadByID map[string]*beads.Bead) { } } +// resolveWorkDirAgainstCity anchors a bead-stored work_dir value to the city +// root. Worktree-per-bead dispatch stores this metadata city-relative (e.g. +// ".gc/worktrees/gascity/builder/") so the value stays valid across +// machines with different absolute city paths; resolving it with os.Stat +// directly would instead resolve against the calling process's cwd, which is +// how scaffold staging leaked into shared long-lived worktrees (ga-ajw1no). +// Already-absolute values (the legacy convention) pass through unchanged. +func resolveWorkDirAgainstCity(cityPath, workDir string) string { + if workDir == "" || cityPath == "" || filepath.IsAbs(workDir) { + return workDir + } + return filepath.Join(cityPath, workDir) +} + // resolveTaskWorkDir checks the agent's assigned task beads for a work_dir // metadata field. If a task bead has work_dir set and the directory exists // on disk, that path is returned. This lets the reconciler start the agent // in the worktree that the previous session (or this session's prior run) // created, without any prompt-side logic. -func resolveTaskWorkDir(store beads.Store, assignees ...string) string { +func resolveTaskWorkDir(cityPath string, store beads.Store, assignees ...string) string { if store == nil { return "" } @@ -3876,10 +3891,12 @@ func resolveTaskWorkDir(store beads.Store, assignees ...string) string { } for _, b := range assigned { wd := strings.TrimSpace(b.Metadata["work_dir"]) - if wd != "" { - if info, err := os.Stat(wd); err == nil && info.IsDir() { - return wd - } + if wd == "" { + continue + } + resolved := resolveWorkDirAgainstCity(cityPath, wd) + if info, err := os.Stat(resolved); err == nil && info.IsDir() { + return resolved } } } @@ -3962,7 +3979,7 @@ type assignedTaskWorkDir struct { // newAssignedTaskWorkDirResolver resolves work_dir values from the // reconciler's snapshot; misses intentionally fall back to the live lookup. -func newAssignedTaskWorkDirResolver(assignedWorkBeads []beads.Bead) taskWorkDirResolver { +func newAssignedTaskWorkDirResolver(cityPath string, assignedWorkBeads []beads.Bead) taskWorkDirResolver { index := make(map[string]assignedTaskWorkDir) for _, bead := range assignedWorkBeads { if bead.Status != "in_progress" { @@ -3976,6 +3993,7 @@ func newAssignedTaskWorkDirResolver(assignedWorkBeads []beads.Bead) taskWorkDirR if workDir == "" { continue } + workDir = resolveWorkDirAgainstCity(cityPath, workDir) info, err := os.Stat(workDir) if err != nil || !info.IsDir() { continue diff --git a/cmd/gc/session_scaffold_staging_test.go b/cmd/gc/session_scaffold_staging_test.go new file mode 100644 index 0000000000..52a3293df0 --- /dev/null +++ b/cmd/gc/session_scaffold_staging_test.go @@ -0,0 +1,238 @@ +package main + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/agent" + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/clock" + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/runtime" + "github.com/gastownhall/gascity/internal/shellquote" +) + +func TestPrepareStartCandidateStagesScaffoldInResolvedTaskWorkDirWhenCWDIsSharedWorktree(t *testing.T) { + root := t.TempDir() + cityPath := filepath.Join(root, "city") + sharedWorktree := filepath.Join(root, "shared-builder") + beadSlug := "ga-ajw1no-1-as-a-maintainer-i-can-reproduce-stray-session-scaffold-leakage" + leakedWorkDir := filepath.Join(sharedWorktree, beadSlug) + relativeTargetWorkDir := filepath.Join(".gc", "worktrees", "gascity", "builder", beadSlug) + targetWorkDir := filepath.Join(cityPath, relativeTargetWorkDir) + packOverlay := filepath.Join(cityPath, "packs", "core", "overlay") + + writeScaffoldFixture(t, filepath.Join(packOverlay, ".claude", "skills", "triage", "SKILL.md"), "---\nname: triage\n---\n") + writeScaffoldFixture(t, filepath.Join(packOverlay, ".codex", "hooks.json"), `{"hooks":{"SessionStart":[]}}`+"\n") + writeScaffoldFixture(t, filepath.Join(packOverlay, ".gc", "settings.json"), "{}\n") + if err := os.MkdirAll(targetWorkDir, 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", targetWorkDir, err) + } + if err := os.MkdirAll(sharedWorktree, 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", sharedWorktree, err) + } + t.Chdir(sharedWorktree) + + store := beads.NewMemStore() + session, err := store.Create(beads.Bead{ + Title: "builder", + Type: sessionBeadType, + Labels: []string{sessionBeadLabel, "agent:gascity/builder"}, + Metadata: map[string]string{ + "template": "builder", + "session_name": "builder-ga-ajw1no", + }, + }) + if err != nil { + t.Fatal(err) + } + task, err := store.Create(beads.Bead{ + Title: "task", + Metadata: map[string]string{ + "work_dir": relativeTargetWorkDir, + }, + }) + if err != nil { + t.Fatal(err) + } + status := "in_progress" + assignee := session.ID + if err := store.Update(task.ID, beads.UpdateOpts{Status: &status, Assignee: &assignee}); err != nil { + t.Fatal(err) + } + + prepared, err := prepareStartCandidateForCity(startCandidate{ + session: &session, + tp: TemplateParams{ + TemplateName: "gascity/builder", + SessionName: "builder-ga-ajw1no", + WorkDir: leakedWorkDir, + Env: map[string]string{ + "GC_DIR": leakedWorkDir, + }, + Hints: agent.StartupHints{ + ProviderName: "codex", + ProviderOverlayName: "codex", + PackOverlayDirs: []string{packOverlay}, + PreStart: appendMaterializeSkillsPreStart(nil, "gascity/builder", leakedWorkDir), + }, + }, + order: 0, + }, cityPath, "city", &config.City{ + Agents: []config.Agent{ + { + Name: "builder", + Dir: "gascity", + MinActiveSessions: intPtrScaffoldRegression(1), + MaxActiveSessions: intPtrScaffoldRegression(2), + }, + }, + }, nil, store, &clock.Fake{Time: time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)}, io.Discard, nil) + if err != nil { + t.Fatalf("prepareStartCandidateForCity: %v", err) + } + + if prepared.cfg.WorkDir != targetWorkDir { + t.Errorf("prepared.cfg.WorkDir = %q, want resolved task work_dir %q", prepared.cfg.WorkDir, targetWorkDir) + } + if prepared.cfg.Env["GC_DIR"] != targetWorkDir { + t.Errorf("prepared.cfg.Env[GC_DIR] = %q, want %q", prepared.cfg.Env["GC_DIR"], targetWorkDir) + } + if len(prepared.cfg.PreStart) != 1 { + t.Fatalf("PreStart = %v, want materialize-skills entry", prepared.cfg.PreStart) + } + if !strings.Contains(prepared.cfg.PreStart[0], "--workdir "+targetWorkDir) { + t.Errorf("materialize-skills PreStart = %q, want resolved target workdir %q", prepared.cfg.PreStart[0], targetWorkDir) + } + if strings.Contains(prepared.cfg.PreStart[0], leakedWorkDir) { + t.Errorf("materialize-skills PreStart still targets shared-cwd bead slug %q: %q", leakedWorkDir, prepared.cfg.PreStart[0]) + } + + if err := runtime.StageSessionWorkDir(prepared.cfg); err != nil { + t.Fatalf("StageSessionWorkDir: %v", err) + } + + for _, rel := range []string{ + filepath.Join(".claude", "skills", "triage", "SKILL.md"), + filepath.Join(".codex", "hooks.json"), + filepath.Join(".gc", "settings.json"), + } { + if _, err := os.Stat(filepath.Join(targetWorkDir, rel)); err != nil { + t.Errorf("target scaffold %s missing under resolved workdir %q: %v", rel, targetWorkDir, err) + } + } + if _, err := os.Stat(leakedWorkDir); err == nil { + t.Fatalf("shared cwd contains stray bead-slug scaffold directory %q; scaffold must stay under %q", leakedWorkDir, targetWorkDir) + } else if !os.IsNotExist(err) { + t.Fatalf("stat leaked workdir %q: %v", leakedWorkDir, err) + } +} + +func writeScaffoldFixture(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%q): %v", path, err) + } +} + +func intPtrScaffoldRegression(n int) *int { + return &n +} + +// TestRetargetPreStartWorkDirPreservesShellQuoting proves that retargeting a +// generated materialize-skills / project-mcp PreStart command onto a resolved +// task work_dir keeps the `--workdir` argument shell-safe. The generators emit +// the workdir as a shell-quoted token; a resolved work_dir that contains a +// space (macOS "/Users/First Last/...") or a shell metacharacter must not be +// spliced in raw, or the rendered `sh -c` command breaks argument boundaries or +// opens a command-substitution surface. +func TestRetargetPreStartWorkDirPreservesShellQuoting(t *testing.T) { + t.Parallel() + + const ( + agentName = "gascity/builder" + identity = "gascity/gc.builder" + oldWorkDir = "/data/worktrees/gascity/builder/ga-clean" + ) + + generators := []struct { + label string + preStart func(workDir string) []string + }{ + { + label: "materialize-skills", + preStart: func(workDir string) []string { + return appendMaterializeSkillsPreStart(nil, agentName, workDir) + }, + }, + { + label: "project-mcp", + preStart: func(workDir string) []string { + return appendProjectMCPPreStart(nil, agentName, identity, workDir) + }, + }, + } + + cases := []struct { + name string + newWorkDir string + }{ + {name: "space", newWorkDir: "/Users/John Doe/city/worktrees/gascity/builder/ga-target"}, + {name: "command_substitution_with_space", newWorkDir: "/opt/proj $(touch pwned)/builder"}, + {name: "command_substitution_no_space", newWorkDir: "/opt/$(id)/builder"}, + } + + for _, g := range generators { + for _, tc := range cases { + t.Run(g.label+"/"+tc.name, func(t *testing.T) { + t.Parallel() + retargeted := retargetPreStartWorkDir(g.preStart(oldWorkDir), oldWorkDir, tc.newWorkDir) + if len(retargeted) != 1 { + t.Fatalf("retarget produced %d entries, want 1: %v", len(retargeted), retargeted) + } + cmd := retargeted[0] + + // Structural: the new value must be embedded shell-quoted, exactly as + // a from-scratch generation would emit it. This catches metacharacter + // injection even when no whitespace forces a re-split. + wantToken := "--workdir " + shellquote.Join([]string{tc.newWorkDir}) + if !strings.Contains(cmd, wantToken) { + t.Errorf("retargeted command missing shell-quoted workdir token %q:\n%s", wantToken, cmd) + } + + // Behavioral: parsing the command with the same quoting rules the + // generator used must recover the intended workdir as a single arg. + if got := workdirArgFromCommand(t, cmd); got != tc.newWorkDir { + t.Errorf("parsed --workdir = %q, want %q\ncommand: %s", got, tc.newWorkDir, cmd) + } + + // The stale pre-override path must be gone entirely. + if strings.Contains(cmd, oldWorkDir) { + t.Errorf("retargeted command still references old workdir %q:\n%s", oldWorkDir, cmd) + } + }) + } + } +} + +// workdirArgFromCommand parses a generated PreStart command with the same +// POSIX quoting rules the generators use and returns the argument following the +// final --workdir flag. +func workdirArgFromCommand(t *testing.T, command string) string { + t.Helper() + args := shellquote.Split(command) + for i := len(args) - 1; i > 0; i-- { + if args[i-1] == "--workdir" { + return args[i] + } + } + t.Fatalf("no --workdir argument in command: %s", command) + return "" +} diff --git a/internal/runtime/tmux/staging_test.go b/internal/runtime/tmux/staging_test.go index e7bdf4f5fb..d8be31caca 100644 --- a/internal/runtime/tmux/staging_test.go +++ b/internal/runtime/tmux/staging_test.go @@ -48,3 +48,59 @@ func TestStageStartFilesSurfacesKiroPreservationWarning(t *testing.T) { t.Fatalf("AGENTS.md = %q, want project instructions preserved", string(data)) } } + +func TestStageStartFilesKeepsScaffoldOutOfSpawnerCWD(t *testing.T) { + root := t.TempDir() + sharedWorktree := filepath.Join(root, "shared-builder") + beadSlug := "ga-ajw1no-1-as-a-maintainer-i-can-reproduce-stray-session-scaffold-leakage" + leakedWorkDir := filepath.Join(sharedWorktree, beadSlug) + workDir := filepath.Join(root, "city", ".gc", "worktrees", "gascity", "builder", beadSlug) + packOverlay := filepath.Join(root, "city", "packs", "core", "overlay") + + writeTmuxScaffoldFixture(t, filepath.Join(packOverlay, ".claude", "skills", "triage", "SKILL.md"), "---\nname: triage\n---\n") + writeTmuxScaffoldFixture(t, filepath.Join(packOverlay, ".codex", "hooks.json"), `{"hooks":{"SessionStart":[]}}`+"\n") + writeTmuxScaffoldFixture(t, filepath.Join(packOverlay, ".gc", "settings.json"), "{}\n") + if err := os.MkdirAll(workDir, 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", workDir, err) + } + if err := os.MkdirAll(sharedWorktree, 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", sharedWorktree, err) + } + t.Chdir(sharedWorktree) + + var warnings bytes.Buffer + err := stageStartFiles(runtime.Config{ + WorkDir: workDir, + ProviderName: "codex", + ProviderOverlayName: "codex", + PackOverlayDirs: []string{packOverlay}, + }, &warnings) + if err != nil { + t.Fatalf("stageStartFiles: %v", err) + } + + for _, rel := range []string{ + filepath.Join(".claude", "skills", "triage", "SKILL.md"), + filepath.Join(".codex", "hooks.json"), + filepath.Join(".gc", "settings.json"), + } { + if _, err := os.Stat(filepath.Join(workDir, rel)); err != nil { + t.Errorf("target scaffold %s missing under workdir %q: %v", rel, workDir, err) + } + } + if _, err := os.Stat(leakedWorkDir); err == nil { + t.Fatalf("shared cwd contains stray bead-slug scaffold directory %q; scaffold must stay under %q", leakedWorkDir, workDir) + } else if !os.IsNotExist(err) { + t.Fatalf("stat leaked workdir %q: %v", leakedWorkDir, err) + } +} + +func writeTmuxScaffoldFixture(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%q): %v", path, err) + } +} diff --git a/release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md b/release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md new file mode 100644 index 0000000000..2b4927c049 --- /dev/null +++ b/release-gates/ga-m9rkmi-session-scaffold-workdir-gate.md @@ -0,0 +1,66 @@ +# Release Gate: session scaffold staging work_dir resolution + +Bead: `ga-m9rkmi` + +Source review beads: `ga-yuij34`, `ga-ji89ce` + +Candidate branch: `origin/builder/ga-ajw1no.2-session-scaffold-workdir` + +Deploy gate branch: `deploy/ga-m9rkmi-session-scaffold-workdir-gate` + +Candidate SHA: `74fdd38a7eb8c1bc8b2dd411c5c695507b965f90` + +Candidate cut point: `origin/main` at `5becf8854dc357392bef81e8da6eea9486a49999` + +Current `origin/main` during deploy gate: `5becf8854dc357392bef81e8da6eea9486a49999` + +`docs/PROJECT_MANIFEST.md` is not present in this checkout; this gate uses the deployer prompt criteria and the repo testing guidance in `TESTING.md`. + +## Release Unit + +Included commits: + +| Commit | Summary | +| --- | --- | +| `5456d8ecf` | Reproduce session scaffold workdir leakage. | +| `74fdd38a7` | Resolve task `work_dir` against the city root, not the reconciler process cwd. | + +Scoped file diff from the candidate cut point: + +```text +M cmd/gc/assigned_work_scope_test.go +M cmd/gc/session_lifecycle_parallel.go +M cmd/gc/session_lifecycle_parallel_test.go +M cmd/gc/session_reconciler.go +A cmd/gc/session_scaffold_staging_test.go +M internal/runtime/tmux/staging_test.go +``` + +The branch is based directly on current `origin/main`. Merge conflict check passed with `git merge-tree $(git merge-base origin/main HEAD) origin/main HEAD`; no conflict diagnostics were emitted. + +## Acceptance Evidence + +The candidate addresses the reviewed failure mode: + +- `resolveTaskWorkDir` and `newAssignedTaskWorkDirResolver` now resolve relative bead `work_dir` metadata against `cityPath` before statting it. +- Already absolute `work_dir` values pass through unchanged via `filepath.IsAbs`. +- `buildPreparedStartWithWorkDirResolver` and `resolvePreparedTaskWorkDir` receive `cityPath` so the prepared start path and reconciler snapshot path agree. +- Rendered `PreStart` commands are retargeted when a task-level workdir override replaces the original template workdir, keeping scaffold materialization under the final session workdir. +- The new regression proves a shared builder cwd does not receive a bead-slug scaffold directory, while the resolved city worktree receives `.claude`, `.codex`, and `.gc` scaffold files. +- The companion cleanup review `ga-ji89ce` verified no separate deployable artifact: it confirmed live stray scaffold cleanup and the same code branch reviewed under `ga-yuij34`. + +## Criteria + +| # | Criterion | Result | Evidence | +| --- | --- | --- | --- | +| 1 | Review PASS present | PASS | `ga-yuij34` is closed with `REVIEWER VERDICT: PASS` for commits `5456d8ecf` and `74fdd38a7`. `ga-ji89ce` is closed with `REVIEWER VERDICT: PASS` for the companion cleanup verification. | +| 2 | Acceptance criteria met | PASS | Code inspection confirmed city-root-relative resolution for task `work_dir`, absolute-path preservation, retargeting of rendered `PreStart` commands, and generic session/work_dir handling with no role-specific branch. New tests cover the shared-cwd leak and tmux scaffold staging guard. | +| 3 | Tests pass | PASS | `go test ./cmd/gc ./internal/runtime/tmux -run 'TestPrepareStartCandidateStagesScaffoldInResolvedTaskWorkDirWhenCWDIsSharedWorktree|TestStageStartFilesKeepsScaffoldOutOfSpawnerCWD|TestStartCandidate|TestResolveTaskWorkDir|TestSessionStart' -count=1`; `go build ./...`; `go vet ./...`; `go test ./internal/api -run TestHandleExtMsgInboundDefaultRouteMatchesMixedCaseProvider -count=1 -v`; `go test ./internal/api -run TestHandleExtMsgInboundDefaultRouteMatchesMixedCaseProvider -count=3 -v`; `go test ./internal/api -count=1`; final `make test-fast-parallel` retry passed all 8 fast jobs. Initial `make test-fast-parallel` attempt failed only in unrelated `internal/api` with `TempDir RemoveAll cleanup: directory not empty`; the failing test and full package passed immediately on rerun before the clean full fast-target retry. | +| 4 | No high-severity review findings open | PASS | Review notes contain PASS verdicts and no unresolved HIGH findings. The reviewer recorded two optional non-blocking observations for future audit only: a similar relative-path pattern in retry dispatch and a harmless `cityPath=""` rebuild path. | +| 5 | Final branch is clean | PASS | `git status --short --branch` was clean before adding this gate artifact; the gate artifact is the only deployer change and is committed on the deploy gate branch. | +| 6 | Branch diverges cleanly from main | PASS | `origin/main` and `HEAD` share merge base `5becf8854dc357392bef81e8da6eea9486a49999`; `git merge-tree` against current `origin/main` emitted a clean merged tree with no conflicts. | +| 7 | Single feature theme | PASS | The commit set touches one subsystem and one behavior: session startup workdir resolution and scaffold staging for assigned task worktrees. All changed files are in `cmd/gc` session lifecycle/reconciler tests or `internal/runtime/tmux` staging tests. | + +## Gate Verdict + +PASS. From 9580649b1b8e296a6bcc3a86b7432c009b86cb1c Mon Sep 17 00:00:00 2001 From: Saren Date: Wed, 1 Jul 2026 20:53:40 -0700 Subject: [PATCH 12/77] fix(hooks): bind managed Codex hooks to city root (#3866) ## Summary - bind managed Codex hook commands to explicit city root instead of relying on cwd discovery - preserve managed-hook upgrade semantics and drift detection for stale/missing Codex entries - avoid rewriting custom env-prefixed hook commands while normalizing managed hooks ## Root cause Managed Codex hooks ran bare `gc ...` from agent workdirs. In nested agent dirs, implicit city discovery could latch onto ancestor `.gc/` runtime state and create nested pseudo-city hook trees, which led to duplicated managed hooks. ## Testing - go test ./internal/hooks -run Codex -count=1 - go test ./cmd/gc -run 'Codex|Doctor' -count=1 --- cmd/gc/cmd_doctor.go | 2 +- cmd/gc/doctor_codex_hooks.go | 27 +- cmd/gc/doctor_codex_hooks_test.go | 140 ++++++++- internal/hooks/hooks.go | 484 ++++++++++++++++++++++++------ internal/hooks/hooks_test.go | 181 ++++++++++- 5 files changed, 711 insertions(+), 123 deletions(-) diff --git a/cmd/gc/cmd_doctor.go b/cmd/gc/cmd_doctor.go index 7447b8b6b1..96af20bccd 100644 --- a/cmd/gc/cmd_doctor.go +++ b/cmd/gc/cmd_doctor.go @@ -230,7 +230,7 @@ func buildDoctorChecks(cityPath string, cfg *config.City, cfgErr error, opts bui register(doctor.NewServiceSecretsPermsCheck(cfg, cityPath)) register(doctor.NewSkillCollisionCheck(cfg, cityPath)) register(doctor.NewOrderFiringCurrentCheck(cfg, cityPath, doctor.WithOrderFiringCurrentLastRunFunc(doctorOrderFiringCurrentLastRunFunc(cityPath, cfg, opts.Stderr)))) - register(newCodexHooksDriftCheck(codexHookWorkDirs(cityPath, cfg))) + register(newCodexHooksDriftCheck(cityPath, codexHookWorkDirs(cityPath, cfg))) register(doctor.NewRigPackCoverageCheck(cfg, cityPath)) register(newPackRuntimesDoctorCheck(cfg)) register(newMCPConfigDoctorCheck(cityPath, cfg, exec.LookPath)) diff --git a/cmd/gc/doctor_codex_hooks.go b/cmd/gc/doctor_codex_hooks.go index f54ed23d95..e7f9d18384 100644 --- a/cmd/gc/doctor_codex_hooks.go +++ b/cmd/gc/doctor_codex_hooks.go @@ -16,11 +16,16 @@ import ( ) type codexHooksDriftCheck struct { - dirs []string + cityPath string + dirs []string } -func newCodexHooksDriftCheck(dirs []string) *codexHooksDriftCheck { - return &codexHooksDriftCheck{dirs: cleanCodexHookDirs(dirs)} +func newCodexHooksDriftCheck(cityPath string, dirs []string) *codexHooksDriftCheck { + cityPath = strings.TrimSpace(cityPath) + if cityPath != "" { + cityPath = filepath.Clean(cityPath) + } + return &codexHooksDriftCheck{cityPath: cityPath, dirs: cleanCodexHookDirs(dirs)} } func codexHookWorkDirs(cityPath string, cfg *config.City) []string { @@ -183,10 +188,10 @@ func (c *codexHooksDriftCheck) CanFix() bool { return true } func (c *codexHooksDriftCheck) Fix(_ *doctor.CheckContext) error { for _, dir := range c.dirs { - if !codexHooksMissingPreCompact(filepath.Join(dir, ".codex", "hooks.json")) { + if !codexHooksNeedUpgrade(filepath.Join(dir, ".codex", "hooks.json"), c.cityPath) { continue } - if err := hooks.Install(fsys.OSFS{}, dir, dir, []string{"codex"}); err != nil { + if err := hooks.Install(fsys.OSFS{}, c.cityPath, dir, []string{"codex"}); err != nil { return fmt.Errorf("upgrading Codex hooks in %s: %w", dir, err) } } @@ -197,7 +202,7 @@ func (c *codexHooksDriftCheck) Run(_ *doctor.CheckContext) *doctor.CheckResult { var stale []string for _, dir := range c.dirs { path := filepath.Join(dir, ".codex", "hooks.json") - if codexHooksMissingPreCompact(path) { + if codexHooksNeedUpgrade(path, c.cityPath) { stale = append(stale, path) } } @@ -205,11 +210,19 @@ func (c *codexHooksDriftCheck) Run(_ *doctor.CheckContext) *doctor.CheckResult { return okCheck(c.Name(), "Codex hooks are current or user-owned") } return warnCheck(c.Name(), - fmt.Sprintf("%d managed Codex hook file(s) missing PreCompact handoff", len(stale)), + fmt.Sprintf("%d managed Codex hook file(s) need upgrade", len(stale)), "run `gc doctor --fix` or restart the city to upgrade managed Codex hooks", stale) } +func codexHooksNeedUpgrade(path, cityPath string) bool { + data, err := os.ReadFile(path) + if err != nil { + return false + } + return hooks.CodexHooksNeedManagedUpgrade(data, cityPath) +} + func codexHooksMissingPreCompact(path string) bool { data, err := os.ReadFile(path) if err != nil { diff --git a/cmd/gc/doctor_codex_hooks_test.go b/cmd/gc/doctor_codex_hooks_test.go index 02c57d516a..879c56ea28 100644 --- a/cmd/gc/doctor_codex_hooks_test.go +++ b/cmd/gc/doctor_codex_hooks_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "path/filepath" "strings" @@ -8,6 +9,7 @@ import ( "github.com/gastownhall/gascity/internal/config" "github.com/gastownhall/gascity/internal/doctor" + "github.com/gastownhall/gascity/internal/shellquote" ) func TestCodexHooksDriftCheckReportsManagedMissingPreCompact(t *testing.T) { @@ -23,37 +25,37 @@ func TestCodexHooksDriftCheckReportsManagedMissingPreCompact(t *testing.T) { } }`) - check := newCodexHooksDriftCheck([]string{dir}) + check := newCodexHooksDriftCheck(dir, []string{dir}) result := check.Run(&doctor.CheckContext{}) if result.Status != doctor.StatusWarning { t.Fatalf("status = %v, want warning; message=%s", result.Status, result.Message) } - if !strings.Contains(result.Message, "missing PreCompact") { - t.Fatalf("message = %q, want missing PreCompact", result.Message) + if !strings.Contains(result.Message, "need upgrade") { + t.Fatalf("message = %q, want need upgrade", result.Message) } } func TestCodexHooksDriftCheckPassesCurrentHooks(t *testing.T) { dir := t.TempDir() - writeCodexHooksForDoctorTest(t, dir, `{ + writeCodexHooksForDoctorTest(t, dir, fmt.Sprintf(`{ "hooks": { "SessionStart": [{ "hooks": [{ "type": "command", - "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc prime --hook --hook-format codex" + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city %s prime --hook --hook-format codex" }] }], "PreCompact": [{ "hooks": [{ "type": "command", - "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc handoff --auto --hook-format codex \"context cycle\"" + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc --city %s handoff --auto --hook-format codex \"context cycle\"" }] }] } -}`) +}`, shellquote.Quote(dir), shellquote.Quote(dir))) - check := newCodexHooksDriftCheck([]string{dir}) + check := newCodexHooksDriftCheck(dir, []string{dir}) result := check.Run(&doctor.CheckContext{}) if result.Status != doctor.StatusOK { @@ -74,7 +76,7 @@ func TestCodexHooksDriftCheckIgnoresCustomHooks(t *testing.T) { } }`) - check := newCodexHooksDriftCheck([]string{dir}) + check := newCodexHooksDriftCheck(dir, []string{dir}) result := check.Run(&doctor.CheckContext{}) if result.Status != doctor.StatusOK { @@ -95,7 +97,7 @@ func TestCodexHooksDriftCheckFixUpgradesManagedHooks(t *testing.T) { } }`) - check := newCodexHooksDriftCheck([]string{dir}) + check := newCodexHooksDriftCheck(dir, []string{dir}) if err := check.Fix(&doctor.CheckContext{}); err != nil { t.Fatalf("Fix: %v", err) } @@ -113,7 +115,7 @@ func TestCodexHooksDriftCheckFixUpgradesManagedHooks(t *testing.T) { } func TestNewCodexHooksDriftCheckCleansDedupesAndSortsDirs(t *testing.T) { - check := newCodexHooksDriftCheck([]string{" /z/../z ", "", "/a", "/a/."}) + check := newCodexHooksDriftCheck("/city", []string{" /z/../z ", "", "/a", "/a/."}) if got, want := strings.Join(check.dirs, ","), "/a,/z"; got != want { t.Fatalf("dirs = %q, want %q", got, want) @@ -126,6 +128,104 @@ func TestNewCodexHooksDriftCheckCleansDedupesAndSortsDirs(t *testing.T) { } } +func TestCodexHooksDriftCheckFixBindsAgentWorkDirToCityRoot(t *testing.T) { + cityDir := t.TempDir() + agentDir := filepath.Join(cityDir, ".gc", "agents", "reviewer") + writeCodexHooksForDoctorTest(t, agentDir, `{ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc prime --hook --hook-format codex" + }] + }] + } +}`) + + check := newCodexHooksDriftCheck(cityDir, []string{agentDir}) + if err := check.Fix(&doctor.CheckContext{}); err != nil { + t.Fatalf("Fix: %v", err) + } + + data, err := os.ReadFile(filepath.Join(agentDir, ".codex", "hooks.json")) + if err != nil { + t.Fatalf("read hooks: %v", err) + } + got := string(data) + if !strings.Contains(got, `gc --city `) { + t.Fatalf("fixed hooks missing explicit --city binding:\n%s", got) + } + if !strings.Contains(got, shellquote.Quote(cityDir)) { + t.Fatalf("fixed hooks missing city root %q:\n%s", cityDir, got) + } + if strings.Contains(got, shellquote.Quote(agentDir)) { + t.Fatalf("fixed hooks rebound to agent workdir %q:\n%s", agentDir, got) + } +} + +func TestCodexHooksDriftCheckReportsManagedWrongCityBinding(t *testing.T) { + cityDir := t.TempDir() + writeCodexHooksForDoctorTest(t, cityDir, `{ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city /old/city prime --hook --hook-format codex" + }] + }], + "PreCompact": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc --city /old/city handoff --auto --hook-format codex \"context cycle\"" + }] + }] + } +}`) + + check := newCodexHooksDriftCheck(cityDir, []string{cityDir}) + result := check.Run(&doctor.CheckContext{}) + if result.Status != doctor.StatusWarning { + t.Fatalf("status = %v, want warning; message=%s", result.Status, result.Message) + } +} + +func TestCodexHooksDriftCheckFixRebindsManagedWrongCityBinding(t *testing.T) { + cityDir := t.TempDir() + writeCodexHooksForDoctorTest(t, cityDir, `{ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city /old/city prime --hook --hook-format codex" + }] + }], + "PreCompact": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc --city /old/city handoff --auto --hook-format codex \"context cycle\"" + }] + }] + } +}`) + + check := newCodexHooksDriftCheck(cityDir, []string{cityDir}) + if err := check.Fix(&doctor.CheckContext{}); err != nil { + t.Fatalf("Fix: %v", err) + } + + data, err := os.ReadFile(filepath.Join(cityDir, ".codex", "hooks.json")) + if err != nil { + t.Fatalf("read hooks: %v", err) + } + got := string(data) + if !strings.Contains(got, shellquote.Quote(cityDir)) { + t.Fatalf("fixed hooks missing city root %q:\n%s", cityDir, got) + } + if strings.Contains(got, "/old/city") { + t.Fatalf("stale city binding survived:\n%s", got) + } +} + func TestCodexHookWorkDirsIncludesActiveRigPaths(t *testing.T) { cfg := &config.City{ Rigs: []config.Rig{ @@ -233,6 +333,24 @@ func TestCodexHooksMissingPreCompactRequiresManagedCommand(t *testing.T) { } } +func TestCodexHooksNeedUpgradeRejectsUnreadableMalformedAndCustomFiles(t *testing.T) { + dir := t.TempDir() + missingPath := filepath.Join(dir, ".codex", "hooks.json") + if codexHooksNeedUpgrade(missingPath, "/city") { + t.Fatal("missing file reported stale") + } + + writeCodexHooksForDoctorTest(t, dir, `{not-json`) + if codexHooksNeedUpgrade(missingPath, "/city") { + t.Fatal("malformed JSON reported stale") + } + + writeCodexHooksForDoctorTest(t, dir, `{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"FOO=1 gc mail check --inject --hook-format codex"}]}]}}`) + if codexHooksNeedUpgrade(missingPath, "/city") { + t.Fatal("env-prefixed custom hooks reported stale") + } +} + func assertDoctorPathPresent(t *testing.T, paths []string, want string) { t.Helper() want = filepath.Clean(want) diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index aad837e18c..526d3f9a19 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -22,6 +22,7 @@ import ( "github.com/gastownhall/gascity/internal/citylayout" "github.com/gastownhall/gascity/internal/fsys" "github.com/gastownhall/gascity/internal/overlay" + "github.com/gastownhall/gascity/internal/shellquote" ) //go:embed config/* @@ -157,9 +158,9 @@ func InstallWithResolver(fs fsys.FS, cityDir, workDir string, providers []string case "claude": err = installClaude(fs, cityDir) case "codex", "gemini", "antigravity", "kiro", "opencode", "mimocode", "copilot", "cursor", "pi", "omp", "kimi": - err = installOverlayManaged(fs, workDir, family) + err = installOverlayManaged(fs, cityDir, workDir, family) case "groq", "cerebras": - err = installOverlayManaged(fs, workDir, "opencode") + err = installOverlayManaged(fs, cityDir, workDir, "opencode") default: return fmt.Errorf("unsupported hook provider %q", p) } @@ -170,7 +171,7 @@ func InstallWithResolver(fs fsys.FS, cityDir, workDir string, providers []string return nil } -func installOverlayManaged(fs fsys.FS, workDir, provider string) error { +func installOverlayManaged(fs fsys.FS, cityDir, workDir, provider string) error { if strings.TrimSpace(workDir) == "" { return nil } @@ -195,7 +196,7 @@ func installOverlayManaged(fs fsys.FS, workDir, provider string) error { return writeJSONOverlayManaged(fs, dst, data) } if provider == "codex" && rel == path.Join(".codex", "hooks.json") { - return writeCodexHooksManaged(fs, dst, data) + return writeCodexHooksManaged(fs, cityDir, dst, data) } if overlay.IsMergeablePath(filepath.FromSlash(rel)) { if normalized, normErr := overlay.CanonicalJSON(data); normErr == nil { @@ -611,9 +612,12 @@ func readClaudeSettingsCandidate(fs fsys.FS, path string) (claudeCandidateState, return candidateUnreadable, nil, err } -func writeCodexHooksManaged(fs fsys.FS, dst string, data []byte) error { +func writeCodexHooksManaged(fs fsys.FS, cityDir, dst string, data []byte) error { + if normalized, _, err := normalizeCodexHookCommands(data, cityDir); err == nil { + data = normalized + } if existing, err := fs.ReadFile(dst); err == nil { - upgraded, changed, upgradeErr := upgradeCodexHooks(existing, data) + upgraded, changed, upgradeErr := upgradeCodexHooks(existing, data, cityDir) if upgradeErr != nil || !changed { return nil } @@ -621,10 +625,6 @@ func writeCodexHooksManaged(fs fsys.FS, dst string, data []byte) error { } else if _, statErr := fs.Stat(dst); statErr == nil { return nil } - normalized, _, err := normalizeCodexHookCommands(data) - if err == nil { - data = normalized - } return writeManagedData(fs, dst, data) } @@ -639,15 +639,15 @@ func writeManagedData(fs fsys.FS, dst string, data []byte) error { return nil } -func upgradeCodexHooks(existing, desired []byte) ([]byte, bool, error) { +func upgradeCodexHooks(existing, desired []byte, cityDir string) ([]byte, bool, error) { var root any if err := json.Unmarshal(existing, &root); err != nil { return nil, false, err } - hasManagedCommand := codexHookValueHasManagedCommand(root) + hasManagedCommand := codexHookValueHasManagedCommand(root, "") needsPreCompact := codexHookDocCanAddPreCompact(root) - changed := upgradeCodexHookValue(root) - if desiredCodexPreCompactHook(desired) != nil && normalizeCodexManagedHookEntries(root) { + changed := upgradeCodexHookValue(root, "", cityDir) + if desiredCodexPreCompactHook(desired) != nil && normalizeCodexManagedHookEntries(root, cityDir) { changed = true } if addCodexPreCompactHook(root, desired) { @@ -663,14 +663,14 @@ func upgradeCodexHooks(existing, desired []byte) ([]byte, bool, error) { return data, changed, nil } -func normalizeCodexHookCommands(existing []byte) ([]byte, bool, error) { +func normalizeCodexHookCommands(existing []byte, cityDir string) ([]byte, bool, error) { var root any if err := json.Unmarshal(existing, &root); err != nil { return nil, false, err } - hasManagedCommand := codexHookValueHasManagedCommand(root) - changed := upgradeCodexHookValue(root) - if normalizeCodexManagedHookEntries(root) { + hasManagedCommand := codexHookValueHasManagedCommand(root, "") + changed := upgradeCodexHookValue(root, "", cityDir) + if normalizeCodexManagedHookEntries(root, cityDir) { changed = true } data, err := overlay.MarshalCanonicalJSON(root) @@ -693,23 +693,52 @@ func CodexHooksMissingManagedPreCompact(data []byte) bool { return codexHookDocCanAddPreCompact(root) } -func codexHookValueHasManagedCommand(v any) bool { +// CodexHooksNeedManagedUpgrade reports whether data is a recognizable Gas City +// managed Codex hooks document that would be upgraded to current managed form +// for cityDir, including explicit --city rebinding and missing PreCompact. +func CodexHooksNeedManagedUpgrade(data []byte, cityDir string) bool { + var root any + if err := json.Unmarshal(data, &root); err != nil { + return false + } + return applyCodexManagedHookUpgrade(root, nil, cityDir) +} + +func applyCodexManagedHookUpgrade(root any, desired []byte, cityDir string) bool { + changed := upgradeCodexHookValue(root, "", cityDir) + if addCodexPreCompactHook(root, desired) { + changed = true + } + return changed +} + +func codexHookValueHasManagedCommand(v any, event string) bool { switch node := v.(type) { case map[string]any: for key, val := range node { + if key == "hooks" { + if hooksMap, ok := val.(map[string]any); ok { + for eventName, eventVal := range hooksMap { + if codexHookValueHasManagedCommand(eventVal, eventName) { + return true + } + } + continue + } + } if key == "command" { - if command, ok := val.(string); ok && isCodexManagedHookCommand(command) { + if command, ok := val.(string); ok && codexHookCommandLooksManaged(event, command) { return true } continue } - if codexHookValueHasManagedCommand(val) { + if codexHookValueHasManagedCommand(val, event) { return true } } case []any: for _, elem := range node { - if codexHookValueHasManagedCommand(elem) { + if codexHookValueHasManagedCommand(elem, event) { return true } } @@ -717,21 +746,31 @@ func codexHookValueHasManagedCommand(v any) bool { return false } -func upgradeCodexHookValue(v any) bool { +func upgradeCodexHookValue(v any, event, cityDir string) bool { switch node := v.(type) { case map[string]any: changed := false for key, val := range node { + if key == "hooks" { + if hooksMap, ok := val.(map[string]any); ok { + for eventName, eventVal := range hooksMap { + if upgradeCodexHookValue(eventVal, eventName, cityDir) { + changed = true + } + } + continue + } + } if key == "command" { if command, ok := val.(string); ok { - if upgraded, didUpgrade := upgradeCodexHookCommand(command); didUpgrade { + if upgraded, didUpgrade := upgradeCodexHookCommand(event, command, cityDir); didUpgrade { node[key] = upgraded changed = true } } continue } - if upgradeCodexHookValue(val) { + if upgradeCodexHookValue(val, event, cityDir) { changed = true } } @@ -739,7 +778,7 @@ func upgradeCodexHookValue(v any) bool { case []any: changed := false for _, elem := range node { - if upgradeCodexHookValue(elem) { + if upgradeCodexHookValue(elem, event, cityDir) { changed = true } } @@ -749,7 +788,7 @@ func upgradeCodexHookValue(v any) bool { } } -func normalizeCodexManagedHookEntries(root any) bool { +func normalizeCodexManagedHookEntries(root any, cityDir string) bool { doc, ok := root.(map[string]any) if !ok { return false @@ -768,11 +807,11 @@ func normalizeCodexManagedHookEntries(root any) bool { seenManaged := map[string]bool{} for _, entry := range entries { if event == "SessionStart" { - if normalizeCodexManagedSessionStartEntry(entry) { + if normalizeCodexManagedSessionStartEntry(entry, cityDir) { changed = true } } - if codexHookValueHasManagedCommand(entry) { + if codexHookValueHasManagedCommand(entry, event) { keyData, err := overlay.MarshalCanonicalJSON(entry) if err == nil { key := string(keyData) @@ -792,9 +831,9 @@ func normalizeCodexManagedHookEntries(root any) bool { return changed } -func normalizeCodexManagedSessionStartEntry(entry any) bool { +func normalizeCodexManagedSessionStartEntry(entry any, cityDir string) bool { entryMap, ok := entry.(map[string]any) - if !ok || !codexHookEntryHasCommandBody(entryMap, sessionStartCurrentFormBody) { + if !ok || !codexHookEntryHasCommandBody(entryMap, sessionStartCurrentFormBody(cityDir)) { return false } if matcher, ok := entryMap["matcher"].(string); !ok || matcher != "startup" { @@ -825,71 +864,324 @@ func codexHookEntryHasCommandBody(entry map[string]any, body string) bool { return false } -var codexManagedHookCommandNeedles = []string{ - `gc prime --hook`, - `gc nudge drain --inject`, - `gc mail check --inject`, - `gc hook --inject`, - `gc handoff --auto`, +func codexHookCommandLooksManaged(event, command string) bool { + _, env, args, ok := parseManagedGCCommand(command) + if !ok { + return false + } + switch event { + case "SessionStart": + return codexSessionStartArgsMatch(env, args) || codexLegacySessionStartRunArgsMatch(args) + case "PreCompact": + return codexPreCompactArgsMatch(args) + case "UserPromptSubmit": + return codexManagedPromptArgsMatch(args, "codex") + default: + return codexSessionStartArgsMatch(env, args) || + codexLegacySessionStartRunArgsMatch(args) || + codexPreCompactArgsMatch(args) || + codexManagedPromptArgsMatch(args, "codex") + } } -func isCodexManagedHookCommand(command string) bool { - for _, needle := range codexManagedHookCommandNeedles { - if strings.Contains(command, needle) { - return true +func upgradeCodexHookCommand(event, command, cityDir string) (string, bool) { + prefix, env, args, ok := parseManagedGCCommand(command) + if !ok { + return "", false + } + switch event { + case "SessionStart": + if !codexSessionStartArgsMatch(env, args) && !codexLegacySessionStartRunArgsMatch(args) { + return "", false } + desired := sessionStartCurrentFormBody(cityDir) + return prefix + desired, strings.TrimPrefix(command, prefix) != desired + case "PreCompact": + if !codexPreCompactArgsMatch(args) { + return "", false + } + desired := preCompactCurrentFormBody(cityDir) + return prefix + desired, strings.TrimPrefix(command, prefix) != desired + case "UserPromptSubmit": + return upgradeManagedPromptHookCommand(command, "codex", cityDir) + default: + if upgraded, ok := upgradeManagedPromptHookCommand(command, "codex", cityDir); ok { + return upgraded, true + } + if codexSessionStartArgsMatch(env, args) || codexLegacySessionStartRunArgsMatch(args) { + desired := sessionStartCurrentFormBody(cityDir) + return prefix + desired, strings.TrimPrefix(command, prefix) != desired + } + if codexPreCompactArgsMatch(args) { + desired := preCompactCurrentFormBody(cityDir) + return prefix + desired, strings.TrimPrefix(command, prefix) != desired + } + return "", false } - return false } -func upgradeCodexHookCommand(command string) (string, bool) { - body := commandBodyAfterCanonicalPrefix(command) - if equalsLegacyCommandBody(body, `gc prime --hook`) || - equalsLegacyCommandBody(body, `gc prime --hook --hook-format codex`) || - equalsLegacyCommandBody(body, `GC_HOOK_EVENT_NAME=SessionStart gc prime --hook`) || - equalsLegacyCommandBody(body, `GC_HOOK_EVENT_NAME=SessionStart gc prime --hook --hook-format codex`) || - equalsLegacyCommandBody(body, sessionStartPreviousManagedFormBody) { - prefix := strings.TrimSuffix(command, body) - return prefix + sessionStartCurrentFormBody, true - } - if equalsLegacyCommandBody(body, managedPromptHookRunPrefix+`prime --hook`) || - equalsLegacyCommandBody(body, managedPromptHookRunPrefix+`prime --hook --hook-format codex`) { - prefix := strings.TrimSuffix(command, body) - return prefix + sessionStartCurrentFormBody, true - } - if upgraded, ok := upgradeManagedPromptHookCommand(command, "codex"); ok { - return upgraded, true - } - if strings.Contains(command, `--hook-format codex`) { +func managedPromptHookRunPrefix(cityDir string) string { + return `gc ` + codexCityFlag(cityDir) + `hook run --timeout 15s --timeout-exit-code 0 -- ` +} + +func upgradeManagedPromptHookCommand(command, hookFormat, cityDir string) (string, bool) { + prefix, _, args, ok := parseManagedGCCommand(command) + if !ok { return "", false } - for _, needle := range codexManagedHookCommandNeedles { - if strings.Contains(command, needle) { - return strings.Replace(command, needle, needle+` --hook-format codex`, 1), true + target, ok := codexManagedPromptTargetArgs(args, hookFormat) + if !ok { + return "", false + } + desired := managedPromptHookRunPrefix(cityDir) + target + return prefix + desired, strings.TrimPrefix(command, prefix) != desired +} + +func codexCityFlag(cityDir string) string { + cityDir = strings.TrimSpace(cityDir) + if cityDir == "" { + return "" + } + return `--city ` + shellquote.Quote(cityDir) + ` ` +} + +func isCodexSessionStartCommandBody(body string) bool { + env, args, ok := parseGCCommandBody(body) + if !ok { + return false + } + if event, ok := env["GC_HOOK_EVENT_NAME"]; ok && event != "SessionStart" { + return false + } + if len(args) == 2 && args[0] == "prime" && args[1] == "--hook" { + return true + } + return len(args) == 4 && args[0] == "prime" && args[1] == "--hook" && args[2] == "--hook-format" && args[3] == "codex" +} + +func isCodexPreCompactCommandBody(body string) bool { + _, args, ok := parseGCCommandBody(body) + if !ok || len(args) < 2 || args[0] != "handoff" { + return false + } + switch { + case len(args) == 2 && args[1] == "context cycle": + return true + case len(args) == 3 && args[1] == "--auto" && args[2] == "context cycle": + return true + case len(args) == 5 && args[1] == "--auto" && args[2] == "--hook-format" && args[3] == "codex" && args[4] == "context cycle": + return true + default: + return false + } +} + +func codexManagedPromptTarget(body, hookFormat string) bool { + _, args, ok := parseGCCommandBody(body) + if !ok { + return false + } + if len(args) >= 3 && args[0] == "nudge" && args[1] == "drain" && args[2] == "--inject" { + _, ok := managedPromptTarget("nudge drain --inject", args[3:], hookFormat) + return ok + } + if len(args) >= 3 && args[0] == "mail" && args[1] == "check" && args[2] == "--inject" { + _, ok := managedPromptTarget("mail check --inject", args[3:], hookFormat) + return ok + } + if len(args) < 8 || args[0] != "hook" || args[1] != "run" { + return false + } + if args[2] != "--timeout" || args[3] != "15s" || args[4] != "--timeout-exit-code" || args[5] != "0" || args[6] != "--" { + return false + } + targetArgs := args[7:] + switch { + case len(targetArgs) >= 3 && targetArgs[0] == "nudge" && targetArgs[1] == "drain" && targetArgs[2] == "--inject": + _, ok := managedPromptTarget("nudge drain --inject", targetArgs[3:], hookFormat) + return ok + case len(targetArgs) >= 3 && targetArgs[0] == "mail" && targetArgs[1] == "check" && targetArgs[2] == "--inject": + _, ok := managedPromptTarget("mail check --inject", targetArgs[3:], hookFormat) + return ok + default: + return false + } +} + +func managedPromptTarget(base string, rest []string, hookFormat string) (string, bool) { + if len(rest) == 0 { + if hookFormat == "" { + return base, true } + return base + ` --hook-format ` + hookFormat, true + } + if hookFormat == "" { + return "", false + } + if len(rest) == 2 && rest[0] == "--hook-format" && rest[1] == hookFormat { + return base + ` --hook-format ` + hookFormat, true } return "", false } -const managedPromptHookRunPrefix = `gc hook run --timeout 15s --timeout-exit-code 0 -- ` +func parseGCCommandBody(body string) (map[string]string, []string, bool) { + tokens := shellquote.Split(body) + if len(tokens) == 0 { + return nil, nil, false + } + env := map[string]string{} + i := 0 + for i < len(tokens) && strings.Contains(tokens[i], "=") && !strings.HasPrefix(tokens[i], "=") { + key, value, ok := strings.Cut(tokens[i], "=") + if !ok || key == "" { + break + } + if !isManagedGCCommandEnvKey(key) { + return nil, nil, false + } + env[key] = value + i++ + } + if i >= len(tokens) || tokens[i] != "gc" { + return nil, nil, false + } + args := tokens[i+1:] + if len(args) >= 2 && args[0] == "--city" { + args = args[2:] + } else if len(args) >= 1 && strings.HasPrefix(args[0], "--city=") { + args = args[1:] + } + return env, args, true +} + +func isManagedGCCommandEnvKey(key string) bool { + switch key { + case "GC_MANAGED_SESSION_HOOK", "GC_HOOK_EVENT_NAME": + return true + default: + return false + } +} -func upgradeManagedPromptHookCommand(command, hookFormat string) (string, bool) { - body := commandBodyAfterCanonicalPrefix(command) - for _, base := range []string{ - `gc nudge drain --inject`, - `gc mail check --inject`, - } { - if equalsLegacyCommandBody(body, base) || - (hookFormat != "" && equalsLegacyCommandBody(body, base+` --hook-format `+hookFormat)) { - target := strings.TrimPrefix(base, `gc `) - if hookFormat != "" { - target += ` --hook-format ` + hookFormat - } - prefix := strings.TrimSuffix(command, body) - return prefix + managedPromptHookRunPrefix + target, true +func parseManagedGCCommand(command string) (string, map[string]string, []string, bool) { + prefix := "" + body := command + if strings.HasPrefix(body, canonicalGCPathPrefix) { + prefix = canonicalGCPathPrefix + body = strings.TrimPrefix(body, canonicalGCPathPrefix) + } + tokens := shellquote.Split(body) + if len(tokens) == 0 { + return "", nil, nil, false + } + env := map[string]string{} + var envTokens []string + var extraEnvTokens []string + i := 0 + hasManagedEnv := false + for i < len(tokens) && strings.Contains(tokens[i], "=") && !strings.HasPrefix(tokens[i], "=") { + key, value, ok := strings.Cut(tokens[i], "=") + if !ok || key == "" { + break } + if isManagedGCCommandEnvKey(key) { + hasManagedEnv = true + } else { + extraEnvTokens = append(extraEnvTokens, tokens[i]) + } + env[key] = value + envTokens = append(envTokens, tokens[i]) + i++ + } + if i >= len(tokens) || tokens[i] != "gc" { + return "", nil, nil, false + } + if len(envTokens) > 0 && prefix == "" && !hasManagedEnv { + return "", nil, nil, false + } + if len(extraEnvTokens) > 0 { + prefix += shellquote.Join(extraEnvTokens) + " " + } + args := tokens[i+1:] + if len(args) >= 2 && args[0] == "--city" { + args = args[2:] + } else if len(args) >= 1 && strings.HasPrefix(args[0], "--city=") { + args = args[1:] + } + return prefix, env, args, true +} + +func codexSessionStartArgsMatch(env map[string]string, args []string) bool { + if event, ok := env["GC_HOOK_EVENT_NAME"]; ok && event != "SessionStart" { + return false + } + if len(args) == 2 && args[0] == "prime" && args[1] == "--hook" { + return true + } + return len(args) == 4 && args[0] == "prime" && args[1] == "--hook" && args[2] == "--hook-format" && args[3] == "codex" +} + +func codexLegacySessionStartRunArgsMatch(args []string) bool { + if len(args) < 8 || args[0] != "hook" || args[1] != "run" { + return false + } + if args[2] != "--timeout" || args[3] != "15s" || args[4] != "--timeout-exit-code" || args[5] != "0" || args[6] != "--" { + return false + } + targetArgs := args[7:] + return len(targetArgs) == 2 && targetArgs[0] == "prime" && targetArgs[1] == "--hook" || + (len(targetArgs) == 4 && targetArgs[0] == "prime" && targetArgs[1] == "--hook" && targetArgs[2] == "--hook-format" && targetArgs[3] == "codex") +} + +func codexPreCompactArgsMatch(args []string) bool { + if len(args) < 2 || args[0] != "handoff" { + return false + } + switch { + case len(args) == 2 && args[1] == "context cycle": + return true + case len(args) == 3 && args[1] == "--auto" && args[2] == "context cycle": + return true + case len(args) == 5 && args[1] == "--auto" && args[2] == "--hook-format" && args[3] == "codex" && args[4] == "context cycle": + return true + default: + return false + } +} + +func codexManagedPromptArgsMatch(args []string, hookFormat string) bool { + _, ok := codexManagedPromptTargetArgs(args, hookFormat) + if ok { + return true + } + if hookFormat != "" { + _, ok = codexManagedPromptTargetArgs(args, "") + } + return ok +} + +func codexManagedPromptTargetArgs(args []string, hookFormat string) (string, bool) { + if len(args) >= 3 && args[0] == "nudge" && args[1] == "drain" && args[2] == "--inject" { + return managedPromptTarget("nudge drain --inject", args[3:], hookFormat) + } + if len(args) >= 3 && args[0] == "mail" && args[1] == "check" && args[2] == "--inject" { + return managedPromptTarget("mail check --inject", args[3:], hookFormat) + } + if len(args) < 8 || args[0] != "hook" || args[1] != "run" { + return "", false + } + if args[2] != "--timeout" || args[3] != "15s" || args[4] != "--timeout-exit-code" || args[5] != "0" || args[6] != "--" { + return "", false + } + targetArgs := args[7:] + switch { + case len(targetArgs) >= 3 && targetArgs[0] == "nudge" && targetArgs[1] == "drain" && targetArgs[2] == "--inject": + return managedPromptTarget("nudge drain --inject", targetArgs[3:], hookFormat) + case len(targetArgs) >= 3 && targetArgs[0] == "mail" && targetArgs[1] == "check" && targetArgs[2] == "--inject": + return managedPromptTarget("mail check --inject", targetArgs[3:], hookFormat) + default: + return "", false } - return "", false } func addCodexPreCompactHook(root any, desired []byte) bool { @@ -930,9 +1222,13 @@ func codexHookDocLooksManaged(doc map[string]any) bool { } switch node := v.(type) { case map[string]any: - if command, ok := node["command"].(string); ok && isCodexManagedHookCommand(command) { - found = true - return + if hooksMap, ok := node["hooks"].(map[string]any); ok { + for eventName, val := range hooksMap { + if codexHookValueHasManagedCommand(val, eventName) { + found = true + return + } + } } for _, val := range node { walk(val) @@ -1152,16 +1448,17 @@ func isLegacyGCManagedCommand(event, command string) bool { case "PreCompact": return equalsLegacyCommandBody(body, "gc prime --hook") || equalsLegacyCommandBody(body, `gc handoff "context cycle"`) || - equalsLegacyCommandBody(body, `gc handoff --auto "context cycle"`) + equalsLegacyCommandBody(body, `gc handoff --auto "context cycle"`) || + isCodexPreCompactCommandBody(body) case "SessionStart": return equalsLegacyCommandBody(body, "gc prime --hook") || equalsLegacyCommandBody(body, "gc prime --hook --hook-format codex") || equalsLegacyCommandBody(body, sessionStartPreviousManagedFormBody) || - equalsLegacyCommandBody(body, sessionStartCurrentFormBody) + isCodexSessionStartCommandBody(body) case "UserPromptSubmit": return equalsLegacyCommandBody(body, `gc nudge drain --inject`) || equalsLegacyCommandBody(body, `gc mail check --inject`) || - strings.HasPrefix(body, managedPromptHookRunPrefix) + codexManagedPromptTarget(body, "") } return false } @@ -1174,10 +1471,19 @@ func isLegacyGCManagedCommand(event, command string) bool { // full env-var preamble. If gc ever extends the current-form command // with additional arguments, update this constant alongside the // emission site so legacy detection remains tight. -const sessionStartCurrentFormBody = `GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc prime --hook --hook-format codex` +func sessionStartCurrentFormBody(cityDir string) string { + return `GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc ` + codexCityFlag(cityDir) + `prime --hook --hook-format codex` +} const sessionStartPreviousManagedFormBody = `GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc prime --hook` +// preCompactCurrentFormBody is the canonical current-form managed PreCompact +// command body (post-canonical-PATH-prefix). If gc ever extends this command +// with additional arguments, update this constant alongside the emission site. +func preCompactCurrentFormBody(cityDir string) string { + return `gc ` + codexCityFlag(cityDir) + `handoff --auto --hook-format codex "context cycle"` +} + // equalsLegacyCommandBody reports whether the command body is exactly the // legacy token. gc historically emitted these tokens as the complete // command body (possibly with the canonical PATH-export prefix), never @@ -1230,10 +1536,10 @@ func upgradeClaudeHookCommand(event, command string) (string, bool) { equalsLegacyCommandBody(body, `gc prime --hook --hook-format codex`) || equalsLegacyCommandBody(body, sessionStartPreviousManagedFormBody) { prefix := strings.TrimSuffix(command, body) - return prefix + sessionStartCurrentFormBody, true + return prefix + sessionStartCurrentFormBody(""), true } case "UserPromptSubmit": - return upgradeManagedPromptHookCommand(command, "") + return upgradeManagedPromptHookCommand(command, "", "") } return "", false } diff --git a/internal/hooks/hooks_test.go b/internal/hooks/hooks_test.go index b465556df6..17c9ee218d 100644 --- a/internal/hooks/hooks_test.go +++ b/internal/hooks/hooks_test.go @@ -11,6 +11,7 @@ import ( "github.com/gastownhall/gascity/internal/config" "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/shellquote" ) func claudeHookCommand(t *testing.T, data []byte, event string) string { @@ -259,7 +260,7 @@ func TestInstallClaudeUpgradesPreviousCanonicalSessionStart(t *testing.T) { if err != nil { t.Fatalf("readEmbedded: %v", err) } - stale := strings.Replace(string(current), sessionStartCurrentFormBody, sessionStartPreviousManagedFormBody, 1) + stale := strings.Replace(string(current), sessionStartCurrentFormBody(""), sessionStartPreviousManagedFormBody, 1) if stale == string(current) { t.Fatal("stale fixture did not diverge from current embedded config — check previous SessionStart pattern") } @@ -273,8 +274,8 @@ func TestInstallClaudeUpgradesPreviousCanonicalSessionStart(t *testing.T) { hookData := fs.Files["/city/hooks/claude.json"] runtimeData := fs.Files["/city/.gc/settings.json"] sessionStartCommand := claudeHookCommand(t, hookData, "SessionStart") - if got := commandBodyAfterCanonicalPrefix(sessionStartCommand); got != sessionStartCurrentFormBody { - t.Fatalf("upgraded SessionStart body = %q, want %q", got, sessionStartCurrentFormBody) + if got := commandBodyAfterCanonicalPrefix(sessionStartCommand); got != sessionStartCurrentFormBody("") { + t.Fatalf("upgraded SessionStart body = %q, want %q", got, sessionStartCurrentFormBody("")) } if string(runtimeData) != string(hookData) { t.Fatalf("runtime Claude settings should mirror upgraded hook settings:\n%s", string(runtimeData)) @@ -343,7 +344,7 @@ func TestInstallCodexUpgradesGeneratedFileMissingHookFormat(t *testing.T) { if !strings.Contains(got, `"PreCompact"`) { t.Errorf("upgraded codex hooks missing PreCompact:\n%s", got) } - if !strings.Contains(got, `gc handoff --auto --hook-format codex \"context cycle\"`) { + if !strings.Contains(got, `gc --city '/city' handoff --auto --hook-format codex \"context cycle\"`) { t.Errorf("upgraded codex PreCompact missing auto handoff command:\n%s", got) } } @@ -372,7 +373,7 @@ func TestInstallCodexUpgradesSessionStartMissingManagedMarker(t *testing.T) { if !strings.Contains(sessionStartCommand, "GC_HOOK_EVENT_NAME=SessionStart") { t.Fatalf("upgraded codex SessionStart missing event marker: %s", sessionStartCommand) } - if !strings.Contains(sessionStartCommand, "gc prime --hook --hook-format codex") { + if !strings.Contains(sessionStartCommand, "gc --city '/city' prime --hook --hook-format codex") { t.Fatalf("upgraded codex SessionStart missing hook format: %s", sessionStartCommand) } } @@ -460,10 +461,10 @@ func TestInstallCodexUpgradesManagedFileMissingPreCompact(t *testing.T) { if !strings.Contains(got, `"PreCompact"`) { t.Errorf("upgraded codex hooks missing PreCompact:\n%s", got) } - if !strings.Contains(got, `gc handoff --auto --hook-format codex \"context cycle\"`) { + if !strings.Contains(got, `gc --city '/city' handoff --auto --hook-format codex \"context cycle\"`) { t.Errorf("upgraded codex PreCompact missing auto handoff command:\n%s", got) } - if !strings.Contains(got, `gc hook run --timeout 15s --timeout-exit-code 0 -- mail check --inject --hook-format codex`) { + if !strings.Contains(got, `gc --city '/city' hook run --timeout 15s --timeout-exit-code 0 -- mail check --inject --hook-format codex`) { t.Errorf("upgraded codex UserPromptSubmit missing bounded mail check command:\n%s", got) } } @@ -475,7 +476,7 @@ func TestInstallCodexWritesCanonicalHookBytes(t *testing.T) { } got := fs.Files["/work/.codex/hooks.json"] - normalized, changed, err := normalizeCodexHookCommands(got) + normalized, changed, err := normalizeCodexHookCommands(got, "/city") if err != nil { t.Fatalf("normalizeCodexHookCommands: %v", err) } @@ -484,6 +485,20 @@ func TestInstallCodexWritesCanonicalHookBytes(t *testing.T) { } } +func TestInstallCodexBindsExplicitCity(t *testing.T) { + fs := fsys.NewFake() + cityDir := "/city with spaces" + if err := Install(fs, cityDir, "/work", []string{"codex"}); err != nil { + t.Fatalf("Install: %v", err) + } + + got := string(fs.Files["/work/.codex/hooks.json"]) + wantCity := `--city ` + shellquote.Quote(cityDir) + if !strings.Contains(got, wantCity) { + t.Fatalf("codex hooks missing explicit city binding %q:\n%s", wantCity, got) + } +} + func TestInstallCodexIsByteStableAcrossRepeatedInstalls(t *testing.T) { fs := fsys.NewFake() if err := Install(fs, "/city", "/work", []string{"codex"}); err != nil { @@ -521,6 +536,23 @@ func TestCodexHooksMissingManagedPreCompact(t *testing.T) { } } +func TestCodexHooksNeedManagedUpgrade(t *testing.T) { + wrongCity := []byte(`{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city /old/city prime --hook --hook-format codex"}]}],"PreCompact":[{"hooks":[{"type":"command","command":"export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc --city /old/city handoff --auto --hook-format codex \"context cycle\""}]}]}}`) + if !CodexHooksNeedManagedUpgrade(wrongCity, "/new city") { + t.Fatal("managed Codex hooks with stale city binding were not reported stale") + } + + currentCity := []byte(`{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city '/old/city' prime --hook --hook-format codex"}]}],"PreCompact":[{"hooks":[{"type":"command","command":"export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc --city '/old/city' handoff --auto --hook-format codex \"context cycle\""}]}]}}`) + if CodexHooksNeedManagedUpgrade(currentCity, "/old/city") { + t.Fatal("managed Codex hooks already bound to requested city were reported stale") + } + + custom := []byte(`{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"FOO=1 gc mail check --inject --hook-format codex"}]}]}}`) + if CodexHooksNeedManagedUpgrade(custom, "/city") { + t.Fatal("env-prefixed custom Codex hooks were reported stale") + } +} + func TestInstallCodexPreservesCustomOnlyHooksByteForByte(t *testing.T) { fs := fsys.NewFake() custom := []byte(`{"hooks":{"UserPromptSubmit":[{"hooks":[{"command":"printf custom-codex-hook","type":"command"}]}]}}`) @@ -562,6 +594,9 @@ func TestInstallCodexUpgradePreservesCustomHooks(t *testing.T) { if !strings.Contains(got, "--hook-format codex") { t.Errorf("upgraded codex hooks missing Codex hook output format:\n%s", got) } + if !strings.Contains(got, `gc --city '/city' prime --hook --hook-format codex`) { + t.Errorf("upgraded codex hooks missing explicit city binding:\n%s", got) + } if !strings.Contains(got, "printf custom-codex-hook") { t.Errorf("custom codex hook was not preserved:\n%s", got) } @@ -570,6 +605,73 @@ func TestInstallCodexUpgradePreservesCustomHooks(t *testing.T) { } } +func TestInstallCodexRebindsManagedHooksAndAddsPreCompact(t *testing.T) { + fs := fsys.NewFake() + fs.Files["/work/.codex/hooks.json"] = []byte(`{ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city /old/city prime --hook --hook-format codex" + }] + }] + } +}`) + + if err := Install(fs, "/new city", "/work", []string{"codex"}); err != nil { + t.Fatalf("Install: %v", err) + } + + got := string(fs.Files["/work/.codex/hooks.json"]) + if !strings.Contains(got, `gc --city '/new city' prime --hook --hook-format codex`) { + t.Fatalf("SessionStart not rebound to current city:\n%s", got) + } + if !strings.Contains(got, `"PreCompact"`) { + t.Fatalf("managed codex upgrade missing PreCompact:\n%s", got) + } + if !strings.Contains(got, `gc --city '/new city' handoff --auto --hook-format codex \"context cycle\"`) { + t.Fatalf("PreCompact not added for current city:\n%s", got) + } + if strings.Contains(got, "/old/city") { + t.Fatalf("stale city binding survived:\n%s", got) + } +} + +func TestInstallCodexRebindsManagedHooksToCurrentCity(t *testing.T) { + fs := fsys.NewFake() + fs.Files["/work/.codex/hooks.json"] = []byte(`{ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city /old/city prime --hook --hook-format codex" + }] + }], + "PreCompact": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gc --city /old/city handoff --auto --hook-format codex \"context cycle\"" + }] + }] + } +}`) + + if err := Install(fs, "/new city", "/work", []string{"codex"}); err != nil { + t.Fatalf("Install: %v", err) + } + + got := string(fs.Files["/work/.codex/hooks.json"]) + if !strings.Contains(got, `gc --city '/new city' prime --hook --hook-format codex`) { + t.Fatalf("SessionStart not rebound to current city:\n%s", got) + } + if !strings.Contains(got, `gc --city '/new city' handoff --auto --hook-format codex \"context cycle\"`) { + t.Fatalf("PreCompact not rebound to current city:\n%s", got) + } + if strings.Contains(got, "/old/city") { + t.Fatalf("stale city binding survived:\n%s", got) + } +} + func TestInstallCodexPreservesFullyCustomHooks(t *testing.T) { fs := fsys.NewFake() custom := []byte(`{ @@ -593,6 +695,55 @@ func TestInstallCodexPreservesFullyCustomHooks(t *testing.T) { } } +func TestInstallCodexPreservesEnvPrefixedManagedLookingCustomHooks(t *testing.T) { + fs := fsys.NewFake() + custom := []byte(`{ + "hooks": { + "UserPromptSubmit": [{ + "hooks": [{ + "type": "command", + "command": "FOO=1 gc mail check --inject --hook-format codex" + }] + }] + } +}`) + fs.Files["/work/.codex/hooks.json"] = custom + + if err := Install(fs, "/city", "/work", []string{"codex"}); err != nil { + t.Fatalf("Install: %v", err) + } + + if got := string(fs.Files["/work/.codex/hooks.json"]); got != string(custom) { + t.Fatalf("env-prefixed custom codex hooks were rewritten:\n%s", got) + } +} + +func TestInstallCodexPreservesExtraEnvOnManagedHooks(t *testing.T) { + fs := fsys.NewFake() + fs.Files["/work/.codex/hooks.json"] = []byte(`{ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && FOO=1 GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc prime --hook --hook-format codex" + }] + }] + } +}`) + + if err := Install(fs, "/city", "/work", []string{"codex"}); err != nil { + t.Fatalf("Install: %v", err) + } + + got := string(fs.Files["/work/.codex/hooks.json"]) + if !strings.Contains(got, `FOO=1 GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city '/city' prime --hook --hook-format codex`) { + t.Fatalf("managed codex hook lost extra env prefix:\n%s", got) + } + if !strings.Contains(got, `"PreCompact"`) { + t.Fatalf("managed codex hook with extra env missing PreCompact:\n%s", got) + } +} + func TestUpgradeCodexHooksSkipsWhenDesiredPreCompactUnavailable(t *testing.T) { existing := []byte(`{ "hooks": { @@ -609,7 +760,7 @@ func TestUpgradeCodexHooksSkipsWhenDesiredPreCompactUnavailable(t *testing.T) { "missing": []byte(`{"hooks":{}}`), } { t.Run(name, func(t *testing.T) { - if _, changed, err := upgradeCodexHooks(existing, desired); err != nil || changed { + if _, changed, err := upgradeCodexHooks(existing, desired, ""); err != nil || changed { t.Fatalf("changed = %v, err = %v, want unchanged without error", changed, err) } }) @@ -1528,8 +1679,8 @@ func TestInstallOverlayManagedProviders(t *testing.T) { codexHooks := fs.Files["/work/.codex/hooks.json"] codexHooksText := string(codexHooks) sessionStartCommand := codexHookCommand(t, codexHooks, "SessionStart") - if !strings.Contains(sessionStartCommand, "gc prime --hook --hook-format codex") { - t.Fatalf("codex SessionStart hook command = %q, want gc prime --hook --hook-format codex", sessionStartCommand) + if !strings.Contains(sessionStartCommand, `gc --city '/city' prime --hook --hook-format codex`) { + t.Fatalf("codex SessionStart hook command = %q, want city-bound gc prime --hook --hook-format codex", sessionStartCommand) } if !strings.Contains(sessionStartCommand, "GC_HOOK_EVENT_NAME=SessionStart") { t.Fatalf("codex SessionStart hook command = %q, want GC_HOOK_EVENT_NAME=SessionStart", sessionStartCommand) @@ -1540,12 +1691,12 @@ func TestInstallOverlayManagedProviders(t *testing.T) { if !strings.Contains(codexHooksText, `"PreCompact"`) { t.Error("codex hooks should include PreCompact") } - if !strings.Contains(codexHooksText, `gc handoff --auto --hook-format codex \"context cycle\"`) { + if !strings.Contains(codexHooksText, `gc --city '/city' handoff --auto --hook-format codex \"context cycle\"`) { t.Error("codex PreCompact should use auto handoff with Codex hook output format") } for _, want := range []string{ - `gc hook run --timeout 15s --timeout-exit-code 0 -- nudge drain --inject --hook-format codex`, - `gc hook run --timeout 15s --timeout-exit-code 0 -- mail check --inject --hook-format codex`, + `gc --city '/city' hook run --timeout 15s --timeout-exit-code 0 -- nudge drain --inject --hook-format codex`, + `gc --city '/city' hook run --timeout 15s --timeout-exit-code 0 -- mail check --inject --hook-format codex`, } { if !strings.Contains(codexHooksText, want) { t.Errorf("codex prompt hooks missing bounded command %q:\n%s", want, codexHooksText) @@ -2227,7 +2378,7 @@ func TestInstallCodexWritesCanonicalJSON(t *testing.T) { if bytes.Contains(data, []byte(`\u0026`)) { t.Fatalf("codex hook escaped command operator:\n%s", data) } - if !bytes.Contains(data, []byte(` && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc prime`)) { + if !bytes.Contains(data, []byte(` && GC_MANAGED_SESSION_HOOK=1 GC_HOOK_EVENT_NAME=SessionStart gc --city '/city' prime`)) { t.Fatalf("codex hook missing literal command operator:\n%s", data) } if !bytes.HasSuffix(data, []byte("\n")) { From 88edd472a20fea791c402520f7ad875ea436f93b Mon Sep 17 00:00:00 2001 From: Jeff Burn Date: Thu, 2 Jul 2026 14:42:50 +1000 Subject: [PATCH 13/77] feat(providers): add Claude Sonnet 5 to builtin claude model choices (#3867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic released **Claude Sonnet 5** (`claude-sonnet-5`) on 2026-06-09; it supersedes Sonnet 4.6 (now legacy). The builtin claude provider's `model` option is a closed enum, so agent templates targeting the new model fail at session spawn with `invalid value for model: claude-sonnet-5`. This mirrors the **Fable 5** precedent (#3284) and the existing `opus` / `opus-4-7` shape: - repoint `sonnet` → `--model claude-sonnet-5` (latest; existing templates auto-upgrade) - add `sonnet-5` → `--model claude-sonnet-5` (explicit alias) - add `sonnet-4-6` → `--model claude-sonnet-4-6` (explicit rollback pin, mirroring `opus-4-7`) Model id verified against the live Claude API (id + alias `claude-sonnet-5`, no date suffix). `gofmt` / `go vet` / `go build` clean; `internal/worker/builtin` and `internal/config` tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Jeff Burn Co-authored-by: Claude Opus 4.8 --- cmd/gc/template_resolve_phase2_test.go | 2 +- internal/worker/builtin/profiles.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/gc/template_resolve_phase2_test.go b/cmd/gc/template_resolve_phase2_test.go index bbae80d3c9..741d4e750d 100644 --- a/cmd/gc/template_resolve_phase2_test.go +++ b/cmd/gc/template_resolve_phase2_test.go @@ -65,7 +65,7 @@ func selectedPhase2ProviderCases(t *testing.T) []phase2ProviderCase { wantProcessNames: []string{"node", "claude"}, wantEmitsPermission: true, wantModelOverride: "sonnet", - wantModelOverrideArgs: []string{"--model", "claude-sonnet-4-6"}, + wantModelOverrideArgs: []string{"--model", "claude-sonnet-5"}, }, { profileID: "codex/tmux-cli", diff --git a/internal/worker/builtin/profiles.go b/internal/worker/builtin/profiles.go index ab3e67a8e3..73ffe0be45 100644 --- a/internal/worker/builtin/profiles.go +++ b/internal/worker/builtin/profiles.go @@ -166,7 +166,9 @@ var builtinProviderSpecs = map[string]BuiltinProviderSpec{ {Value: "fable-5", Label: "Fable 5", FlagArgs: []string{"--model", "claude-fable-5"}, FlagAliases: [][]string{{"-m", "claude-fable-5"}}}, {Value: "opus", Label: "Opus", FlagArgs: []string{"--model", "claude-opus-4-8"}, FlagAliases: [][]string{{"-m", "claude-opus-4-8"}}}, {Value: "opus-4-7", Label: "Opus 4.7", FlagArgs: []string{"--model", "claude-opus-4-7"}, FlagAliases: [][]string{{"-m", "claude-opus-4-7"}}}, - {Value: "sonnet", Label: "Sonnet", FlagArgs: []string{"--model", "claude-sonnet-4-6"}, FlagAliases: [][]string{{"-m", "claude-sonnet-4-6"}}}, + {Value: "sonnet", Label: "Sonnet", FlagArgs: []string{"--model", "claude-sonnet-5"}, FlagAliases: [][]string{{"-m", "claude-sonnet-5"}}}, + {Value: "sonnet-5", Label: "Sonnet 5", FlagArgs: []string{"--model", "claude-sonnet-5"}, FlagAliases: [][]string{{"-m", "claude-sonnet-5"}}}, + {Value: "sonnet-4-6", Label: "Sonnet 4.6", FlagArgs: []string{"--model", "claude-sonnet-4-6"}, FlagAliases: [][]string{{"-m", "claude-sonnet-4-6"}}}, {Value: "haiku", Label: "Haiku", FlagArgs: []string{"--model", "claude-haiku-4-5-20251001"}, FlagAliases: [][]string{{"-m", "claude-haiku-4-5-20251001"}}}, }, }, From 03ad33516a71d244bfae7f4b94464047e54dc992 Mon Sep 17 00:00:00 2001 From: Julian Knutsen Date: Fri, 3 Jul 2026 03:28:03 -0700 Subject: [PATCH 14/77] =?UTF-8?q?feat(runproj):=20event-sourced=20Runs=20v?= =?UTF-8?q?iew=20=E2=80=94=20Go=20projection=20over=20events.jsonl=20(P0?= =?UTF-8?q?=E2=80=93P4)=20(#3804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-homes the dashboard Runs view (summary + per-run detail) in Go: a projection over the per-city `.gc/events.jsonl` replaces the slow client-side `bd`/`gc` molecule scans. The SPA becomes a pure renderer of the existing `RunSummary` / `FormulaRunDetail` DTOs. **Phases (all landed, golden-gated):** - **P0** events→bead fold + golden corpus. - **P1** `BuildRunSummary` (byte-for-byte golden parity). - **P2** session enrich + per-city tailer + `GET /api/city/{city}/runs/summary`. - **P3** detail interpreter + `GET /api/city/{city}/runs/{runId}/detail`. - **P4a** SPA cutover — the two run loaders read the BFF endpoints; `ApiError`/`ApiClientError` carry the 422 `reason`; subscription keeps last-good retention + SSE debounce. - **P4b** deleted ~5k LOC of dead TS fold/graph pipeline + retired the golden generator (goldens are now frozen Go-owned fixtures). Shipped dist is byte-identical (the removed TS was already tree-shaken). **Rebased onto `origin/main`** after #3727 (supervisor-hosted dashboard) squash-merged as `677ce243f`. **Supersedes #3793**, which GitHub auto-closed when its base branch (`feat/dashboard-supervisor-hosting`) was deleted. **Gates green:** `make dashboard-check`, vitest 759 / shared 97, eslint, `go test ./internal/runproj` (goldens) + `-race ./internal/api/dashboardbff`, `go build ./cmd/gc`. **Deploy note:** live maintainer-city redeploy is currently gated by #3288 (boot-hang affecting all HEAD-based builds); these run-views ship in the normal next deploy once #3288 is fixed + validated. Draft until ready to merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .gitattributes | 6 + internal/api/dashboardbff/plane.go | 5 + .../api/dashboardbff/rundetailtailer_test.go | 435 ++++++++ internal/api/dashboardbff/runtailer.go | 566 ++++++++++ internal/api/dashboardbff/runtailer_test.go | 531 ++++++++++ .../dist/assets/Activity-Ca_fEMiY.js | 2 - .../dist/assets/Activity-CiU9eU59.js | 2 + ...il-B_PEx1iU.js => AgentDetail-UxV6LhC9.js} | 2 +- ...{Agents-40RuA321.js => Agents-CEUrRSz0.js} | 2 +- .../dist/assets/AmbientHome-Cvjm2Kmk.js | 1 - .../dist/assets/AmbientHome-DYE5iAQP.js | 1 + .../dist/assets/BeadDetailModal-BtQMJz2-.js | 1 + .../dist/assets/BeadDetailModal-Df6JvpcR.js | 1 - .../{Beads-BVqefDvL.js => Beads-m4fbNWDo.js} | 2 +- .../{Field-CQOLMLGH.js => Field-pp_wh5a7.js} | 2 +- .../dist/assets/FormulaRunDetail-BXhub1du.js | 12 - .../dist/assets/FormulaRunDetail-DtW7ktOr.js | 12 + .../dist/assets/Health-B0fm2qWB.js | 1 - .../dist/assets/Health-RykINz8c.js | 1 + ...psrYunI.js => LiveSessionPeek-m6YywWBh.js} | 6 +- .../{Mail-um-BH4TD.js => Mail-yLXNL53p.js} | 2 +- ...der-DGfr1hUc.js => PageHeader-CxbYmkHZ.js} | 2 +- .../dashboardspa/dist/assets/Runs-C9G772Th.js | 1 + .../dashboardspa/dist/assets/Runs-CTNTp8Tf.js | 1 - ...r-BpC5bgiy.js => SseIndicator-DDbpxu-X.js} | 2 +- ...er-CwHTgfDd.js => StageLadder-DgEuJnhe.js} | 2 +- .../{Table-DRIQbbRJ.js => Table-C94QdmsL.js} | 2 +- ...ads-NY5zZttz.js => agentReads-BA8TH08X.js} | 2 +- ...ants-vAmcTKRZ.js => constants-CVFL5iaz.js} | 2 +- .../dist/assets/index-QWRimsO3.js | 73 ++ .../dist/assets/index-zPatq59W.js | 73 -- ...ctOf-nApq7eyo.js => projectOf-CAOn7SI-.js} | 2 +- ...C9ZhD4ch.js => useListFilters-D2HcBe10.js} | 2 +- ...UjLcH.js => useVisibleRefresh-Bm_cCiAg.js} | 2 +- internal/api/dashboardspa/dist/index.html | 2 +- .../web/frontend/src/api/client.test.ts | 128 +++ .../web/frontend/src/api/client.ts | 68 +- .../src/components/run/RunMap.test.tsx | 25 +- .../src/hooks/useFormulaRunDetail.test.tsx | 234 ++-- .../frontend/src/hooks/useFormulaRunDetail.ts | 70 +- .../src/routes/FormulaRunDetail.test.tsx | 36 +- .../src/runs/runSummarySubscription.test.tsx | 19 +- .../src/runs/runSummarySubscription.tsx | 40 +- .../frontend/src/supervisor/runDetail.test.ts | 370 ++----- .../web/frontend/src/supervisor/runDetail.ts | 259 +---- .../src/supervisor/runSummary.test.ts | 999 ++---------------- .../web/frontend/src/supervisor/runSummary.ts | 565 +--------- .../dashboardspa/web/shared/src/api-error.ts | 7 + .../api/dashboardspa/web/shared/src/index.ts | 21 +- .../web/shared/src/runs/bead-fields.ts | 64 -- .../web/shared/src/runs/display-state.ts | 61 -- .../dashboardspa/web/shared/src/runs/edges.ts | 142 --- .../web/shared/src/runs/enrich.ts | 145 --- .../shared/src/runs/execution-instances.ts | 264 ----- .../web/shared/src/runs/execution-path.ts | 34 - .../web/shared/src/runs/formula-name.ts | 184 ---- .../web/shared/src/runs/formula-order.ts | 95 -- .../web/shared/src/runs/formula-run.ts | 289 ----- .../web/shared/src/runs/groups.ts | 359 ------- .../web/shared/src/runs/health.test.ts | 60 -- .../web/shared/src/runs/health.ts | 215 +--- .../dashboardspa/web/shared/src/runs/lanes.ts | 18 - .../web/shared/src/runs/liveness.test.ts | 153 --- .../web/shared/src/runs/liveness.ts | 74 -- .../web/shared/src/runs/node-shape.ts | 190 ---- .../web/shared/src/runs/phaseMapping.test.ts | 411 ------- .../web/shared/src/runs/phaseMapping.ts | 708 ------------- .../web/shared/src/runs/runtime-state.ts | 70 -- .../web/shared/src/runs/session-link.test.ts | 60 -- .../web/shared/src/runs/session-link.ts | 176 --- .../web/shared/src/runs/status.ts | 36 - .../web/shared/src/runs/summary.test.ts | 519 --------- .../web/shared/src/runs/summary.ts | 514 +-------- internal/beadmeta/keys.go | 69 +- internal/events/reader.go | 147 +++ internal/events/rotation_reader_test.go | 107 ++ internal/runproj/detail.go | 627 +++++++++++ internal/runproj/detail_consistency_test.go | 60 ++ internal/runproj/detail_deps_test.go | 122 +++ internal/runproj/detail_displaystate.go | 81 ++ internal/runproj/detail_edges.go | 135 +++ internal/runproj/detail_formulaname.go | 135 +++ internal/runproj/detail_golden_test.go | 60 ++ internal/runproj/detail_groups.go | 453 ++++++++ internal/runproj/detail_instances.go | 341 ++++++ internal/runproj/detail_marshal.go | 245 +++++ internal/runproj/detail_nodeshape.go | 546 ++++++++++ internal/runproj/detail_nodeshape_test.go | 88 ++ internal/runproj/detail_order.go | 264 +++++ internal/runproj/detail_parity_test.go | 164 +++ internal/runproj/detail_scope_test.go | 70 ++ internal/runproj/detail_sessionlink.go | 243 +++++ internal/runproj/detail_sessionlink_test.go | 72 ++ internal/runproj/detail_types.go | 275 +++++ internal/runproj/enrich.go | 332 ++++++ internal/runproj/enrich_test.go | 265 +++++ internal/runproj/filter_test.go | 46 + internal/runproj/fold.go | 82 ++ internal/runproj/fold_test.go | 84 ++ internal/runproj/formulaname.go | 101 ++ internal/runproj/marshal.go | 297 ++++++ internal/runproj/phasemapping.go | 723 +++++++++++++ internal/runproj/projector.go | 102 ++ internal/runproj/projector_test.go | 92 ++ internal/runproj/scope.go | 82 ++ internal/runproj/session.go | 101 ++ internal/runproj/strip.go | 28 + internal/runproj/summary.go | 736 +++++++++++++ internal/runproj/summary_golden_test.go | 102 ++ internal/runproj/testdata/beads_fixture.json | 248 +++++ .../runproj/testdata/rundetail_golden.json | 359 +++++++ .../testdata/runsummary_enriched_golden.json | 453 ++++++++ .../runproj/testdata/runsummary_golden.json | 380 +++++++ .../runproj/testdata/sessions_fixture.json | 40 + internal/runproj/testenv_import_test.go | 5 + internal/runproj/types.go | 253 +++++ plans/runs-view-HANDOFF.md | 125 +++ plans/runs-view-NEXT-SESSION-PROMPT.md | 35 + plans/runs-view-architecture-adr.md | 256 +++++ plans/runs-view-event-sourcing.md | 269 +++++ 120 files changed, 12171 insertions(+), 7172 deletions(-) create mode 100644 .gitattributes create mode 100644 internal/api/dashboardbff/rundetailtailer_test.go create mode 100644 internal/api/dashboardbff/runtailer.go create mode 100644 internal/api/dashboardbff/runtailer_test.go delete mode 100644 internal/api/dashboardspa/dist/assets/Activity-Ca_fEMiY.js create mode 100644 internal/api/dashboardspa/dist/assets/Activity-CiU9eU59.js rename internal/api/dashboardspa/dist/assets/{AgentDetail-B_PEx1iU.js => AgentDetail-UxV6LhC9.js} (96%) rename internal/api/dashboardspa/dist/assets/{Agents-40RuA321.js => Agents-CEUrRSz0.js} (97%) delete mode 100644 internal/api/dashboardspa/dist/assets/AmbientHome-Cvjm2Kmk.js create mode 100644 internal/api/dashboardspa/dist/assets/AmbientHome-DYE5iAQP.js create mode 100644 internal/api/dashboardspa/dist/assets/BeadDetailModal-BtQMJz2-.js delete mode 100644 internal/api/dashboardspa/dist/assets/BeadDetailModal-Df6JvpcR.js rename internal/api/dashboardspa/dist/assets/{Beads-BVqefDvL.js => Beads-m4fbNWDo.js} (96%) rename internal/api/dashboardspa/dist/assets/{Field-CQOLMLGH.js => Field-pp_wh5a7.js} (85%) delete mode 100644 internal/api/dashboardspa/dist/assets/FormulaRunDetail-BXhub1du.js create mode 100644 internal/api/dashboardspa/dist/assets/FormulaRunDetail-DtW7ktOr.js delete mode 100644 internal/api/dashboardspa/dist/assets/Health-B0fm2qWB.js create mode 100644 internal/api/dashboardspa/dist/assets/Health-RykINz8c.js rename internal/api/dashboardspa/dist/assets/{LiveSessionPeek-BpsrYunI.js => LiveSessionPeek-m6YywWBh.js} (72%) rename internal/api/dashboardspa/dist/assets/{Mail-um-BH4TD.js => Mail-yLXNL53p.js} (97%) rename internal/api/dashboardspa/dist/assets/{PageHeader-DGfr1hUc.js => PageHeader-CxbYmkHZ.js} (89%) create mode 100644 internal/api/dashboardspa/dist/assets/Runs-C9G772Th.js delete mode 100644 internal/api/dashboardspa/dist/assets/Runs-CTNTp8Tf.js rename internal/api/dashboardspa/dist/assets/{SseIndicator-BpC5bgiy.js => SseIndicator-DDbpxu-X.js} (88%) rename internal/api/dashboardspa/dist/assets/{StageLadder-CwHTgfDd.js => StageLadder-DgEuJnhe.js} (91%) rename internal/api/dashboardspa/dist/assets/{Table-DRIQbbRJ.js => Table-C94QdmsL.js} (96%) rename internal/api/dashboardspa/dist/assets/{agentReads-NY5zZttz.js => agentReads-BA8TH08X.js} (80%) rename internal/api/dashboardspa/dist/assets/{constants-vAmcTKRZ.js => constants-CVFL5iaz.js} (95%) create mode 100644 internal/api/dashboardspa/dist/assets/index-QWRimsO3.js delete mode 100644 internal/api/dashboardspa/dist/assets/index-zPatq59W.js rename internal/api/dashboardspa/dist/assets/{projectOf-nApq7eyo.js => projectOf-CAOn7SI-.js} (92%) rename internal/api/dashboardspa/dist/assets/{useListFilters-C9ZhD4ch.js => useListFilters-D2HcBe10.js} (98%) rename internal/api/dashboardspa/dist/assets/{useVisibleRefresh-DdfUjLcH.js => useVisibleRefresh-Bm_cCiAg.js} (92%) delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/bead-fields.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/display-state.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/edges.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/enrich.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/execution-instances.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/execution-path.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/formula-name.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/formula-order.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/formula-run.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/groups.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/health.test.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/lanes.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/liveness.test.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/liveness.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/node-shape.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/phaseMapping.test.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/phaseMapping.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/runtime-state.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/session-link.test.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/session-link.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/status.ts delete mode 100644 internal/api/dashboardspa/web/shared/src/runs/summary.test.ts create mode 100644 internal/runproj/detail.go create mode 100644 internal/runproj/detail_consistency_test.go create mode 100644 internal/runproj/detail_deps_test.go create mode 100644 internal/runproj/detail_displaystate.go create mode 100644 internal/runproj/detail_edges.go create mode 100644 internal/runproj/detail_formulaname.go create mode 100644 internal/runproj/detail_golden_test.go create mode 100644 internal/runproj/detail_groups.go create mode 100644 internal/runproj/detail_instances.go create mode 100644 internal/runproj/detail_marshal.go create mode 100644 internal/runproj/detail_nodeshape.go create mode 100644 internal/runproj/detail_nodeshape_test.go create mode 100644 internal/runproj/detail_order.go create mode 100644 internal/runproj/detail_parity_test.go create mode 100644 internal/runproj/detail_scope_test.go create mode 100644 internal/runproj/detail_sessionlink.go create mode 100644 internal/runproj/detail_sessionlink_test.go create mode 100644 internal/runproj/detail_types.go create mode 100644 internal/runproj/enrich.go create mode 100644 internal/runproj/enrich_test.go create mode 100644 internal/runproj/filter_test.go create mode 100644 internal/runproj/fold.go create mode 100644 internal/runproj/fold_test.go create mode 100644 internal/runproj/formulaname.go create mode 100644 internal/runproj/marshal.go create mode 100644 internal/runproj/phasemapping.go create mode 100644 internal/runproj/projector.go create mode 100644 internal/runproj/projector_test.go create mode 100644 internal/runproj/scope.go create mode 100644 internal/runproj/session.go create mode 100644 internal/runproj/strip.go create mode 100644 internal/runproj/summary.go create mode 100644 internal/runproj/summary_golden_test.go create mode 100644 internal/runproj/testdata/beads_fixture.json create mode 100644 internal/runproj/testdata/rundetail_golden.json create mode 100644 internal/runproj/testdata/runsummary_enriched_golden.json create mode 100644 internal/runproj/testdata/runsummary_golden.json create mode 100644 internal/runproj/testdata/sessions_fixture.json create mode 100644 internal/runproj/testenv_import_test.go create mode 100644 internal/runproj/types.go create mode 100644 plans/runs-view-HANDOFF.md create mode 100644 plans/runs-view-NEXT-SESSION-PROMPT.md create mode 100644 plans/runs-view-architecture-adr.md create mode 100644 plans/runs-view-event-sourcing.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..f5c91ceed6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# The dashboard SPA bundle under internal/api/dashboardspa/dist is emitted by the +# Vite build and embedded into the gc binary. Treat it as generated output: skip +# textual diffing and the whitespace lint (`git diff --check`) on the minified +# assets, and collapse it in review UIs. Drift is still caught by the content-based +# `git diff --quiet -- internal/api/dashboardspa/dist` gate in the Makefile. +internal/api/dashboardspa/dist/** -diff linguist-generated diff --git a/internal/api/dashboardbff/plane.go b/internal/api/dashboardbff/plane.go index 83bd4f528a..b8bbebf406 100644 --- a/internal/api/dashboardbff/plane.go +++ b/internal/api/dashboardbff/plane.go @@ -62,6 +62,7 @@ type Plane struct { exec *execRunner mux *http.ServeMux samplers *samplerManager + runTailers *runTailerManager localTools *localToolsCache wg sync.WaitGroup @@ -73,6 +74,7 @@ type Plane struct { func New(deps Deps) *Plane { p := &Plane{deps: deps, exec: newExecRunner(), mux: http.NewServeMux(), localTools: &localToolsCache{}} p.samplers = newSamplerManager(deps, p.exec) + p.runTailers = newRunTailerManager(deps) p.registerRoutes() return p } @@ -88,6 +90,7 @@ func (p *Plane) Handler() http.Handler { return p.guard(p.mux) } func (p *Plane) Start(ctx context.Context) { ctx, p.stop = context.WithCancel(ctx) p.samplers.enable(ctx, &p.wg) + p.runTailers.enable(ctx, &p.wg) } // Stop signals the samplers to halt and waits for them to drain. @@ -178,6 +181,8 @@ func (p *Plane) registerRoutes() { p.registerHealth() p.registerRunDiff() p.registerSamplers() + p.registerRunSummary() + p.registerRunDetail() } // resolveCityPath validates a city name and resolves its host root path. It diff --git a/internal/api/dashboardbff/rundetailtailer_test.go b/internal/api/dashboardbff/rundetailtailer_test.go new file mode 100644 index 0000000000..dabc5f9e04 --- /dev/null +++ b/internal/api/dashboardbff/rundetailtailer_test.go @@ -0,0 +1,435 @@ +package dashboardbff + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "path/filepath" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" +) + +// runDetailRootEvent builds the canonical graph.v2 run-root molecule (run "run1", +// formula mol-adopt-pr-v2) with the scope metadata the detail snapshot projection +// requires (gc.scope_kind / gc.scope_ref / gc.root_store_ref). It is the first +// event in these fixtures, so it carries seq 1. +func runDetailRootEvent() events.Event { + const ( + runID = "run1" + formula = "mol-adopt-pr-v2" + ) + return beadCreatedEvent(1, beads.Bead{ + ID: runID, + Title: formula, + Status: "open", + Type: "molecule", + Ref: formula, + CreatedAt: time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC), + Metadata: map[string]string{ + "gc.formula_contract": "graph.v2", + "gc.kind": "run", + "gc.formula": formula, + "gc.run_target": "rig:demo", + "gc.root_store_ref": "rig:demo", + "gc.scope_kind": "rig", + "gc.scope_ref": "demo", + }, + }) +} + +// runDetailStepEvent builds a step bead parented to a run root. +func runDetailStepEvent(seq uint64, id, parent, stepID, status string) events.Event { + return beadCreatedEvent(seq, beads.Bead{ + ID: id, + Title: stepID, + Status: status, + Type: "task", + ParentID: parent, + Ref: "mol-adopt-pr-v2." + stepID, + CreatedAt: time.Date(2026, 6, 1, 10, 1, 0, 0, time.UTC), + UpdatedAt: time.Date(2026, 6, 1, 10, 5, 0, 0, time.UTC), + Metadata: map[string]string{ + "gc.kind": "step", + "gc.root_bead_id": parent, + "gc.step_id": stepID, + "gc.scope_ref": "demo", + }, + }) +} + +func beadCreatedEvent(seq uint64, b beads.Bead) events.Event { + payload, _ := json.Marshal(struct { + Bead beads.Bead `json:"bead"` + }{b}) + return events.Event{Seq: seq, Type: events.BeadCreated, Payload: payload} +} + +// runDetailWire is the decoded detail body — a structural contract check that the +// wire carries the FormulaRunDetail shape the SPA renderer reads. +type runDetailWire struct { + RunID string `json:"runId"` + ScopeRef string `json:"scopeRef"` + Title string `json:"title"` + Formula struct { + Kind string `json:"kind"` + Name string `json:"name"` + } `json:"formula"` + Phase string `json:"phase"` + Nodes []struct { + ID string `json:"id"` + } `json:"nodes"` + Lanes []struct { + ID string `json:"id"` + } `json:"lanes"` + FormulaDetail struct { + Kind string `json:"kind"` + Name string `json:"name"` + Target string `json:"target"` + Reason string `json:"reason"` + Failure string `json:"failure"` + } `json:"formulaDetail"` +} + +// TestRunDetailEndpoint drives the full endpoint: the warm fold projects one +// run's detail graph (root + step) off the same tailer the summary uses. +func TestRunDetailEndpoint(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, + runDetailRootEvent(), + runDetailStepEvent(2, "run1.1", "run1", "preflight", "in_progress"), + ) + + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}}) + p.Start(t.Context()) + defer p.Stop() + + resp := getRunDetail(t, p, "alpha", "run1") + if resp.RunID != "run1" { + t.Errorf("runId = %q, want run1", resp.RunID) + } + if resp.ScopeRef != "demo" { + t.Errorf("scopeRef = %q, want demo", resp.ScopeRef) + } + if resp.Title != "mol-adopt-pr-v2" { + t.Errorf("title = %q, want mol-adopt-pr-v2", resp.Title) + } + if resp.Formula.Kind != "known" || resp.Formula.Name != "mol-adopt-pr-v2" { + t.Errorf("formula = %+v, want known/mol-adopt-pr-v2", resp.Formula) + } + if len(resp.Nodes) != 2 { + t.Errorf("nodes = %d, want 2 (root + preflight)", len(resp.Nodes)) + } + if len(resp.Lanes) != 1 || resp.Lanes[0].ID != "demo" { + t.Errorf("lanes = %+v, want one lane 'demo'", resp.Lanes) + } + if resp.Phase == "" { + t.Errorf("phase is empty, want a classified phase") + } +} + +// TestRunDetailEndpointFiltersNonRunBeads is the regression guard for the live +// projection bypassing RunBeadFilter: message, session, and gc:-labeled control +// beads that share a run root must be dropped at the projection boundary (the +// analog of the frontend defaultBeadFilter) so they never surface as detail +// nodes. Without the filter, the gc:-labeled child and the message bead below +// are selected as run members and would inflate the node count past the real +// root+step graph. +func TestRunDetailEndpointFiltersNonRunBeads(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + // A distinct run id and city (filtering is not run- or city-specific) also + // keep the shared getRunDetail helper exercised with more than one run/city. + const runID = "runf1" + writeEventLog(t, logPath, + beadCreatedEvent(1, beads.Bead{ + ID: runID, + Title: "mol-adopt-pr-v2", + Status: "open", + Type: "molecule", + Ref: "mol-adopt-pr-v2", + CreatedAt: time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC), + Metadata: map[string]string{ + "gc.formula_contract": "graph.v2", + "gc.kind": "run", + "gc.formula": "mol-adopt-pr-v2", + "gc.run_target": "rig:demo", + "gc.root_store_ref": "rig:demo", + "gc.scope_kind": "rig", + "gc.scope_ref": "demo", + }, + }), + runDetailStepEvent(2, runID+".1", runID, "preflight", "in_progress"), + // A gc:-labeled control bead whose id sits under the run root: without the + // filter, snapshotForRun selects it as a member and it becomes a node. + beadCreatedEvent(3, beads.Bead{ + ID: runID + ".ctl", + Title: "control bead", + Status: "open", + Type: "task", + ParentID: runID, + Labels: []string{"gc:control"}, + CreatedAt: time.Date(2026, 6, 1, 10, 2, 0, 0, time.UTC), + UpdatedAt: time.Date(2026, 6, 1, 10, 6, 0, 0, time.UTC), + Metadata: map[string]string{"gc.root_bead_id": runID}, + }), + // A message bead carrying the run root: not an engineering type and not + // gc.kind=run, so RunBeadFilter drops it. + beadCreatedEvent(4, beads.Bead{ + ID: "msg1", + Title: "convoy message", + Status: "open", + Type: "message", + CreatedAt: time.Date(2026, 6, 1, 10, 3, 0, 0, time.UTC), + UpdatedAt: time.Date(2026, 6, 1, 10, 7, 0, 0, time.UTC), + Metadata: map[string]string{"gc.root_bead_id": runID}, + }), + ) + + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{"beta": dir}}}) + p.Start(t.Context()) + defer p.Stop() + + resp := getRunDetail(t, p, "beta", runID) + if len(resp.Nodes) != 2 { + t.Errorf("nodes = %d, want 2 (root + preflight); non-run/gc:-labeled beads must be filtered, got %+v", len(resp.Nodes), resp.Nodes) + } + for _, node := range resp.Nodes { + if node.ID == runID+".ctl" || node.ID == "msg1" { + t.Errorf("node %q leaked into detail; RunBeadFilter must drop it", node.ID) + } + } +} + +// TestRunDetailEndpointLayersCompiledFormulaDetail proves the endpoint fetches +// the supervisor's compiled formula detail at request time (like sessions) so a +// graph.v2 run with a name+target resolves to an "available" formula-detail +// state rather than the synthetic fetch_failed/upstream_error the bead-derived +// projection emits on its own. +func TestRunDetailEndpointLayersCompiledFormulaDetail(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, + runDetailRootEvent(), + runDetailStepEvent(2, "run1.1", "run1", "preflight", "in_progress"), + ) + + var gotFormulaQuery string + supervisor := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v0/city/alpha/formulas/mol-adopt-pr-v2" { + gotFormulaQuery = r.URL.RawQuery + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"name":"mol-adopt-pr-v2","steps":[{"id":"preflight"},{"id":"apply-fixes"}],"preview":{"nodes":[{"id":"preflight"},{"id":"apply-fixes"}]}}`)) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer supervisor.Close() + + p := New(Deps{ + Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}, + SupervisorBaseURL: supervisor.URL, + }) + p.Start(t.Context()) + defer p.Stop() + + resp := getRunDetail(t, p, "alpha", "run1") + if resp.FormulaDetail.Kind != "available" { + t.Errorf("formulaDetail.kind = %q, want available; full=%+v", resp.FormulaDetail.Kind, resp.FormulaDetail) + } + if resp.FormulaDetail.Name != "mol-adopt-pr-v2" || resp.FormulaDetail.Target != "rig:demo" { + t.Errorf("formulaDetail = %+v, want name mol-adopt-pr-v2 / target rig:demo", resp.FormulaDetail) + } + if resp.FormulaDetail.Failure != "" { + t.Errorf("formulaDetail.failure = %q, want empty (no synthetic upstream error)", resp.FormulaDetail.Failure) + } + // The run root is rig-scoped (gc.scope_kind=rig, gc.scope_ref=demo). The BFF + // must derive that scope from the run root and send it alongside target, so + // the endpoint resolves the compiled formula against the rig formula layer + // instead of the wrong layer or a required-scope rejection. + gotQuery, err := url.ParseQuery(gotFormulaQuery) + if err != nil { + t.Fatalf("parse compiled-formula fetch query %q: %v", gotFormulaQuery, err) + } + if got := gotQuery.Get("target"); got != "rig:demo" { + t.Errorf("compiled-formula fetch target = %q, want rig:demo (query %q)", got, gotFormulaQuery) + } + if got := gotQuery.Get("scope_kind"); got != "rig" { + t.Errorf("compiled-formula fetch scope_kind = %q, want rig (query %q)", got, gotFormulaQuery) + } + if got := gotQuery.Get("scope_ref"); got != "demo" { + t.Errorf("compiled-formula fetch scope_ref = %q, want demo (query %q)", got, gotFormulaQuery) + } +} + +// TestRunDetailEndpointFormulaFetchFailureStaysHonest proves that when the +// compiled-formula fetch is attempted but fails upstream, the detail state falls +// back to the honest fetch_failed arm rather than fabricating availability. +func TestRunDetailEndpointFormulaFetchFailureStaysHonest(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, runDetailRootEvent()) + + var formulaFetchAttempted bool + supervisor := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v0/city/alpha/formulas/mol-adopt-pr-v2" { + formulaFetchAttempted = true + } + w.WriteHeader(http.StatusInternalServerError) + })) + defer supervisor.Close() + + p := New(Deps{ + Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}, + SupervisorBaseURL: supervisor.URL, + }) + p.Start(t.Context()) + defer p.Stop() + + resp := getRunDetail(t, p, "alpha", "run1") + if !formulaFetchAttempted { + t.Error("compiled-formula fetch was not attempted for a graph.v2 run with a target") + } + if resp.FormulaDetail.Kind != "unavailable" || resp.FormulaDetail.Reason != "fetch_failed" { + t.Errorf("formulaDetail = %+v, want unavailable/fetch_failed on upstream failure", resp.FormulaDetail) + } + if resp.FormulaDetail.Failure != "upstream_error" { + t.Errorf("formulaDetail.failure = %q, want upstream_error", resp.FormulaDetail.Failure) + } +} + +// TestRunDetailEndpointFormulaFetch404IsNotFound proves the BFF preserves the +// distinct not_found failure reason across the BFF/runproj boundary: a compiled +// formula that the supervisor reports as HTTP 404 must resolve to +// fetch_failed/not_found — a genuinely missing formula — rather than collapsing +// into the generic upstream_error the non-404 path (see the sibling test above) +// reports. Before this fix the BFF discarded the status code, so a 404 rendered +// the wrong operator diagnostic on the run-detail page. +func TestRunDetailEndpointFormulaFetch404IsNotFound(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, runDetailRootEvent()) + + var formulaFetchAttempted bool + supervisor := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v0/city/alpha/formulas/mol-adopt-pr-v2" { + formulaFetchAttempted = true + } + w.WriteHeader(http.StatusNotFound) + })) + defer supervisor.Close() + + p := New(Deps{ + Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}, + SupervisorBaseURL: supervisor.URL, + }) + p.Start(t.Context()) + defer p.Stop() + + resp := getRunDetail(t, p, "alpha", "run1") + if !formulaFetchAttempted { + t.Error("compiled-formula fetch was not attempted for a graph.v2 run with a target") + } + if resp.FormulaDetail.Kind != "unavailable" || resp.FormulaDetail.Reason != "fetch_failed" { + t.Errorf("formulaDetail = %+v, want unavailable/fetch_failed on a 404", resp.FormulaDetail) + } + if resp.FormulaDetail.Failure != "not_found" { + t.Errorf("formulaDetail.failure = %q, want not_found (a 404 is a missing formula, not a generic upstream error)", resp.FormulaDetail.Failure) + } +} + +// TestRunDetailEndpointUnknownCity404 confirms an unresolvable city 404s. +func TestRunDetailEndpointUnknownCity404(t *testing.T) { + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{}}}) + rec := httptest.NewRecorder() + p.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/city/ghost/runs/run1/detail", nil)) + if rec.Code != http.StatusNotFound { + t.Errorf("status = %d, want 404 for unknown city", rec.Code) + } +} + +// TestRunDetailEndpointUnknownRun404 confirms a missing run 404s once the tailer +// is warm. +func TestRunDetailEndpointUnknownRun404(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, runDetailRootEvent()) + + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}}) + p.Start(t.Context()) + defer p.Stop() + + // Warm the tailer first (a summary read blocks on the cold replay), so the + // missing run is a true 404, not a warming 503. + _ = getRunSummary(t, p, "alpha") + getRunDetailExpectStatus(t, p, "alpha", "missing", http.StatusNotFound) +} + +// TestRunDetailEndpointNotRunView maps a non-graph.v2 run to 422 with the +// not_run_view reason so the SPA renders the honest list-only message. +func TestRunDetailEndpointNotRunView(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + // A molecule run marker but NO gc.formula_contract=graph.v2 → not a run view. + writeEventLog(t, logPath, beadCreatedEvent(1, beads.Bead{ + ID: "v1run", + Title: "legacy v1 run", + Status: "open", + Type: "molecule", + CreatedAt: time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC), + Metadata: map[string]string{"gc.kind": "run"}, + })) + + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}}) + p.Start(t.Context()) + defer p.Stop() + + rec := getRunDetailRaw(t, p, "alpha", "v1run") + if rec.Code != http.StatusUnprocessableEntity { + t.Fatalf("status = %d, want 422; body=%s", rec.Code, rec.Body.String()) + } + var body runDetailErrorBody + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode error body: %v; body=%s", err, rec.Body.String()) + } + if body.Reason != "not_run_view" { + t.Errorf("reason = %q, want not_run_view", body.Reason) + } +} + +func getRunDetailRaw(t *testing.T, p *Plane, city, runID string) *httptest.ResponseRecorder { + t.Helper() + rec := httptest.NewRecorder() + p.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/city/"+city+"/runs/"+runID+"/detail", nil)) + return rec +} + +func getRunDetailExpectStatus(t *testing.T, p *Plane, city, runID string, want int) { + t.Helper() + rec := getRunDetailRaw(t, p, city, runID) + if rec.Code != want { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, want, rec.Body.String()) + } +} + +// getRunDetail fetches a run's detail and decodes the success (200) body. Non-2xx +// paths use getRunDetailRaw / getRunDetailExpectStatus, so the expected status is +// fixed here rather than a parameter. +func getRunDetail(t *testing.T, p *Plane, city, runID string) runDetailWire { + t.Helper() + rec := getRunDetailRaw(t, p, city, runID) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + var resp runDetailWire + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v; body=%s", err, rec.Body.String()) + } + return resp +} diff --git a/internal/api/dashboardbff/runtailer.go b/internal/api/dashboardbff/runtailer.go new file mode 100644 index 0000000000..dccb1c491c --- /dev/null +++ b/internal/api/dashboardbff/runtailer.go @@ -0,0 +1,566 @@ +package dashboardbff + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" + "github.com/gastownhall/gascity/internal/runproj" +) + +// The run-view summary is reconstructed from the per-city append-only event log +// (.gc/events.jsonl) instead of the supervisor's slow molecule/feed scans. A +// per-city tailer folds the log into a warm bead-derived RunSummary off the +// request path — cold-replay over the full history (rotated .gz archives +// included), then a read-only byte-offset tail of newly appended events — so a +// request serves a sub-second warm read and layers session health/census at +// request time from one loopback /v0 sessions read. Modeled on the citySampler: +// lazy per-city start, all the heavy work off the lock, a brief publish under +// the lock. The tail is pure-read (events.ReadFrom opens the log read-only), so +// it is never a second writer to the supervisor's own recorder. +var ( + // runTailPollInterval is how often the tail polls the active log for new + // bytes. A var (not a const) so tests can shorten it. + runTailPollInterval = 1 * time.Second + // runColdLoadWait bounds how long a first request blocks for the cold replay + // before returning a partial (warming) snapshot. A var so tests can shorten it. + runColdLoadWait = 5 * time.Second +) + +const runSessionsFetchTimeout = 10 * time.Second + +// ── Tailer manager ──────────────────────────────────────────────────────── + +type runTailerManager struct { + deps Deps + httpc *http.Client + + mu sync.Mutex + cities map[string]*cityRunTailer + ctx context.Context + wg *sync.WaitGroup + enabled bool +} + +func newRunTailerManager(deps Deps) *runTailerManager { + return &runTailerManager{ + deps: deps, + httpc: &http.Client{Timeout: runSessionsFetchTimeout}, + cities: make(map[string]*cityRunTailer), + } +} + +// enable records the lifecycle context and waitgroup so lazily-started city +// tailers stop cleanly on shutdown (shared with the samplers' waitgroup). +func (m *runTailerManager) enable(ctx context.Context, wg *sync.WaitGroup) { + m.mu.Lock() + defer m.mu.Unlock() + m.ctx = ctx + m.wg = wg + m.enabled = true +} + +// ensure returns the tailer for a city, starting its background fold loop on +// first use once the manager has been enabled (Start called). +func (m *runTailerManager) ensure(name, eventsPath string) *cityRunTailer { + m.mu.Lock() + defer m.mu.Unlock() + t, ok := m.cities[name] + if !ok { + t = &cityRunTailer{name: name, eventsPath: eventsPath, mgr: m, readyCh: make(chan struct{})} + m.cities[name] = t + } + if m.enabled && m.ctx != nil && !t.started { + t.started = true + m.wg.Add(1) + go func() { + defer m.wg.Done() + t.loop(m.ctx) + }() + } + return t +} + +// ── Per-city tailer ─────────────────────────────────────────────────────── + +type cityRunTailer struct { + name string + eventsPath string + mgr *runTailerManager + + started bool + readyCh chan struct{} // closed once the cold replay attempt completes + + mu sync.RWMutex + summary runproj.RunSummary + marks map[string]runproj.LaneProgressMark + beads []beads.Bead + lastSeq uint64 + ready bool +} + +// tailState carries the fold cursor across poll iterations: the byte offset into +// the active log, the active file's identity (so a rotation is detected by +// dev/inode rather than a fragile size-shrink check), and the monotonic lane +// progress marks. +type tailState struct { + offset int64 + activeInfo os.FileInfo + marks map[string]runproj.LaneProgressMark +} + +// captureTailCursor snapshots the active log's byte size and identity from a +// SINGLE os.Stat so the resume offset and the rotation-detection identity always +// describe the same file. Splitting them across two stats (a size stat then an +// identity stat) let a rotation land between the two and pair the old file's +// larger offset with the fresh file's identity; the first foldNext then saw no +// identity change, ReadFrom seeked past the fresh file's EOF, and every fresh +// event below the stale offset was silently dropped until restart. A rotation +// after this single snapshot is instead caught by foldNext's identity check; a +// rotation before it yields a consistent size+identity for the new active file. +func captureTailCursor(path string) *tailState { + st := &tailState{} + if info, err := os.Stat(path); err == nil { + st.offset = info.Size() + st.activeInfo = info + } + return st +} + +// loop cold-replays the event log, publishes the bead-derived summary, then +// tails newly appended events and republishes on each change. All folding and +// summary-building happens on loop-owned locals; only the publish takes the lock. +func (t *cityRunTailer) loop(ctx context.Context) { + proj := runproj.NewProjector() + + // Capture the active log size and identity BEFORE the cold replay so the tail + // resumes from exactly there. Any event appended during (or just after) the + // replay lands in [offset, EOF) and is re-read by the first tail poll; the seq + // filter drops the overlap the replay already folded. This makes the resume + // race-free — closing readyCh before computing the offset (the previous design) + // let an append between the two jump the tail past the new event, dropping it. + // captureTailCursor reads the size and identity from one stat so a rotation + // cannot pair the old file's offset with the fresh file's identity. + st := captureTailCursor(t.eventsPath) + loadErr := proj.ColdLoad(t.eventsPath) + st.marks = t.build(proj, nil, loadErr) + close(t.readyCh) + + poll := time.NewTicker(runTailPollInterval) + defer poll.Stop() + for { + select { + case <-ctx.Done(): + return + case <-poll.C: + t.foldNext(proj, st) + } + } +} + +// readRotationCatchUp is the rotation catch-up read, indirected through a +// package var so a test can inject a transient read error and prove foldNext +// retries the catch-up on the next poll instead of losing the just-rotated +// events. Production always uses events.ReadFilteredWithInFlight. +var readRotationCatchUp = events.ReadFilteredWithInFlight + +// foldNext performs one tail poll: it folds newly appended events into the +// projector and republishes when a bead snapshot changed. It handles active-log +// rotation by file identity: when the recorder renames the active file to an +// archive and opens a fresh one, the events written to the old active file in +// the poll window before the rename live only in the archive, so a bare offset +// reset (the previous size-shrink heuristic) would drop them — and would also +// fail to fire at all if the fresh file grew back past the stale offset within +// one poll. On a detected rotation it first catches up across archives by +// sequence, then re-tails the fresh active file from the top; the seq filter +// drops the overlap the catch-up already folded. +func (t *cityRunTailer) foldNext(proj *runproj.Projector, st *tailState) { + info, statErr := os.Stat(t.eventsPath) + rotated := statErr == nil && st.activeInfo != nil && !os.SameFile(st.activeInfo, info) + if rotated { + // ReadFilteredWithInFlight walks the sibling .gz archives (skipping any + // whose seq window is fully below the cursor without gunzipping), the + // in-flight events.jsonl.rotating-* files a just-rotated log has not yet + // been gzipped into, AND the fresh active file. Including the rotating + // files closes the async-compression window: the recorder renames the old + // active log to a plain-JSONL rotating-* file and compresses it in the + // background, so between the rename and the .gz a plain ReadFiltered would + // miss those pre-rotation events and the offset reset below would advance + // the tail past them for good. eventsAfter + Apply are seq-idempotent, so + // the .gz/rotating overlap and the offset-0 re-read are both harmless. + // + // Catch up BEFORE advancing the identity or resetting the offset. Those + // pre-rotation events live only in the archive now, so a transient + // catch-up read error must leave the OLD identity in place and retry on + // the next poll — committing the fresh identity here would make the next + // poll see no rotation (SameFile) and lose that window until restart. + catchUp, err := readRotationCatchUp(t.eventsPath, events.Filter{AfterSeq: proj.LastSeq()}) + if err != nil { + return + } + if fresh := eventsAfter(catchUp, proj.LastSeq()); len(fresh) > 0 && proj.Apply(fresh) { + st.marks = t.build(proj, st.marks, nil) + } + st.activeInfo = info + st.offset = 0 + } else if statErr == nil { + st.activeInfo = info + // A byte offset beyond the current active file's EOF on the SAME identity + // is a stale cursor (e.g. one captured against a since-rotated larger + // file): ReadFrom would seek past EOF and silently skip every event below + // it. The active log only grows in place — rotation changes identity and + // is handled above — so offset > size can only mean the offset is stale. + // Rewind to re-read the fresh file from the top; eventsAfter drops the + // overlap already folded. + if st.offset > info.Size() { + st.offset = 0 + } + } + + evts, newOffset, err := events.ReadFrom(t.eventsPath, st.offset) + if err != nil { + return + } + st.offset = newOffset + fresh := eventsAfter(evts, proj.LastSeq()) + if len(fresh) == 0 { + return + } + if proj.Apply(fresh) { + st.marks = t.build(proj, st.marks, nil) + } +} + +// eventsAfter keeps only events past the projector's cursor, dropping the +// overlap a from-offset re-read (cold-replay resume or post-rotation rescan) +// re-surfaces. Filters in place; the input slice is loop-local. +func eventsAfter(evts []events.Event, afterSeq uint64) []events.Event { + out := evts[:0] + for _, e := range evts { + if e.Seq > afterSeq { + out = append(out, e) + } + } + return out +} + +// build projects the folded beads into a bead-derived RunSummary, advances the +// monotonic thrash marks against the prior generation, and publishes both under +// the lock. It returns the advanced marks for the loop to carry forward. +func (t *cityRunTailer) build(proj *runproj.Projector, prevMarks map[string]runproj.LaneProgressMark, loadErr error) map[string]runproj.LaneProgressMark { + // Apply the run-bead filter at the projection boundary, mirroring the + // frontend's runBeadFilter (summary.ts). The pure runproj builders + // assume already-filtered input, so folding the raw event log straight in — + // it also carries message, session, and gc:-labeled control beads that can + // share a run root — would let unrelated beads distort lane status, counts, + // recent changes, and detail nodes. Filtering once here feeds the same clean + // slice to both the summary and the detail projection. FilterRunBeads returns + // a fresh first-seen-ordered slice of the immutable-after-decode bead values, + // so the published snapshot is safe to read concurrently. + beadSlice := runproj.FilterRunBeads(proj.Beads()) + summary := runproj.BuildRunSummary(beadSlice) + if loadErr != nil { + // A read failure must surface as a partial snapshot, not a silently empty + // "no runs" view. + summary.LanesPartial = true + } + + inFlight := make([]runproj.RunLane, 0, len(summary.Lanes)+len(summary.BlockedLanes)) + inFlight = append(inFlight, summary.Lanes...) + inFlight = append(inFlight, summary.BlockedLanes...) + marks := runproj.AdvanceProgressMarks(prevMarks, inFlight) + + // Publish the filtered warm bead slice + fold cursor alongside the summary so + // the detail endpoint projects any one run off the same clean projection + // (BuildRunDetail does its own member selection). + lastSeq := proj.LastSeq() + + t.mu.Lock() + t.summary = summary + t.marks = marks + t.beads = beadSlice + t.lastSeq = lastSeq + t.ready = true + t.mu.Unlock() + return marks +} + +// runDetailSnapshotVersion is the synthesized run-snapshot shape version the +// bead-derived detail projection emits (the OSS-local analog of the supervisor's +// snapshot_version). It matches the golden generator's snapshot_version. +const runDetailSnapshotVersion = 1 + +// detail projects one run into the run-detail DTO off the warm bead snapshot, +// layering request-time session links from one loopback /v0 sessions read. It +// waits briefly for the cold replay on a city's first request, like +// enrichedSummary. The bool reports whether the cold replay had completed (a +// not-found run during warming is reported as warming, not a hard 404). +func (t *cityRunTailer) detail(ctx context.Context, runID string) (runproj.FormulaRunDetail, bool, error) { + select { + case <-t.readyCh: + case <-ctx.Done(): + case <-time.After(runColdLoadWait): + } + + t.mu.RLock() + beadSlice := t.beads + lastSeq := t.lastSeq + ready := t.ready + t.mu.RUnlock() + + sessions, sessionsAvailable := t.mgr.fetchSessions(ctx, t.name) + + // Layer the supervisor's compiled formula detail at request time (like + // sessions) so a graph.v2 run with a name+target resolves to the authored + // step order and an "available" formula-detail state instead of a synthetic + // fetch failure. A run with no fetchable formula, or a genuine fetch failure, + // leaves formulaDetail nil so the detail state stays honest (missing_* or, for + // a name+target we could not resolve, fetch_failed). On a fetch failure we keep + // the reason (not_found for a supervisor 404, else upstream_error) so runproj + // renders the right operator diagnostic instead of collapsing a missing formula + // into a generic upstream error. + var formulaDetail *runproj.FormulaOrderingDetail + formulaDetailFailure := runproj.FormulaDetailUpstreamError + if name, target, scopeKind, scopeRef, ok := runproj.RunFormulaTargetForRun(beadSlice, runID); ok { + if fetched, failure, fetchedOK := t.mgr.fetchFormulaDetail(ctx, t.name, name, target, scopeKind, scopeRef); fetchedOK { + formulaDetail = fetched + } else { + formulaDetailFailure = failure + } + } + + var ( + d runproj.FormulaRunDetail + err error + ) + if sessionsAvailable { + d, err = runproj.BuildRunDetailWithSessionsAndFormula(beadSlice, runID, runDetailSnapshotVersion, int64(lastSeq), sessions, formulaDetail, formulaDetailFailure) + } else { + d, err = runproj.BuildRunDetailWithSessionsAndFormula(beadSlice, runID, runDetailSnapshotVersion, int64(lastSeq), nil, formulaDetail, formulaDetailFailure) + } + return d, ready, err +} + +// enrichedSummary returns the warm bead-derived summary with request-time +// session health/census layered on. It waits briefly for the cold replay on a +// city's first request, then degrades to a partial (warming) snapshot. +func (t *cityRunTailer) enrichedSummary(ctx context.Context) runproj.RunSummary { + select { + case <-t.readyCh: + case <-ctx.Done(): + case <-time.After(runColdLoadWait): + } + + t.mu.RLock() + base := t.summary + marks := t.marks + ready := t.ready + t.mu.RUnlock() + + sessions, sessionsAvailable := t.mgr.fetchSessions(ctx, t.name) + enriched := runproj.EnrichRunSummary(base, sessions, sessionsAvailable, time.Now().UnixMilli(), marks) + if !ready { + enriched.LanesPartial = true + } + return enriched +} + +// fetchSessions reads GET {base}/v0/city/{name}/sessions over loopback and +// projects the items into the dashboard session shape (equivalent to the +// frontend normalizeSessions). Any failure returns (nil, false) so health +// degrades to unavailable rather than failing the load. +func (m *runTailerManager) fetchSessions(ctx context.Context, name string) ([]runproj.DashboardSession, bool) { + base := strings.TrimRight(m.deps.SupervisorBaseURL, "/") + if base == "" { + return nil, false + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/v0/city/"+name+"/sessions", nil) + if err != nil { + return nil, false + } + req.Header.Set("Accept", "application/json") + resp, err := m.httpc.Do(req) + if err != nil { + return nil, false + } + defer resp.Body.Close() //nolint:errcheck + if resp.StatusCode != http.StatusOK { + return nil, false + } + body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) + if err != nil { + return nil, false + } + var env struct { + Items []runproj.DashboardSession `json:"items"` + } + if err := json.Unmarshal(body, &env); err != nil { + return nil, false + } + if env.Items == nil { + env.Items = []runproj.DashboardSession{} + } + return env.Items, true +} + +// formulaNodeRef decodes the ordering-relevant id of a compiled-formula preview +// node or step from the supervisor's formula-detail response. +type formulaNodeRef struct { + ID string `json:"id"` +} + +// fetchFormulaDetail reads +// GET {base}/v0/city/{name}/formulas/{formula}?target={target}&scope_kind={kind}&scope_ref={ref} +// over loopback and projects the compiled formula's ordering-relevant preview +// nodes and steps into runproj's FormulaOrderingDetail. The scope is required by +// the endpoint and selects the formula search layer, so a rig-scoped run must +// send its scope or the lookup resolves the wrong layer (or is rejected). On +// success it returns (detail, "", true). On failure it returns (nil, reason, +// false) so the detail falls back to the un-enriched projection: the reason is +// FormulaDetailNotFound for a supervisor 404 (the compiled formula is genuinely +// missing) and FormulaDetailUpstreamError for every other failure, preserving the +// distinction runproj renders as the operator diagnostic. Mirrors fetchSessions; +// the reason mapping ports the TS formulaDetailFetchFailure helper. +func (m *runTailerManager) fetchFormulaDetail(ctx context.Context, name, formula, target, scopeKind, scopeRef string) (*runproj.FormulaOrderingDetail, runproj.RunFormulaDetailFetchFailure, bool) { + base := strings.TrimRight(m.deps.SupervisorBaseURL, "/") + if base == "" { + return nil, runproj.FormulaDetailUpstreamError, false + } + endpoint := base + "/v0/city/" + url.PathEscape(name) + "/formulas/" + url.PathEscape(formula) + query := url.Values{"target": {target}} + if scopeKind != "" { + query.Set("scope_kind", scopeKind) + } + if scopeRef != "" { + query.Set("scope_ref", scopeRef) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"?"+query.Encode(), nil) + if err != nil { + return nil, runproj.FormulaDetailUpstreamError, false + } + req.Header.Set("Accept", "application/json") + resp, err := m.httpc.Do(req) + if err != nil { + return nil, runproj.FormulaDetailUpstreamError, false + } + defer resp.Body.Close() //nolint:errcheck + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, runproj.FormulaDetailNotFound, false + } + return nil, runproj.FormulaDetailUpstreamError, false + } + body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) + if err != nil { + return nil, runproj.FormulaDetailUpstreamError, false + } + var env struct { + Name string `json:"name"` + Steps []formulaNodeRef `json:"steps"` + Preview struct { + Nodes []formulaNodeRef `json:"nodes"` + } `json:"preview"` + } + if err := json.Unmarshal(body, &env); err != nil { + return nil, runproj.FormulaDetailUpstreamError, false + } + return &runproj.FormulaOrderingDetail{ + Name: env.Name, + PreviewNodeIDs: refIDs(env.Preview.Nodes), + StepIDs: refIDs(env.Steps), + }, "", true +} + +// refIDs lifts formula node/step ids into a plain slice, preserving nil (the +// field was absent/null) versus non-nil empty (present-but-empty) so runproj's +// preview-nodes-then-steps ordering fallback stays faithful to the dashboard. +func refIDs(refs []formulaNodeRef) []string { + if refs == nil { + return nil + } + ids := make([]string, 0, len(refs)) + for _, r := range refs { + ids = append(ids, r.ID) + } + return ids +} + +// ── Route ───────────────────────────────────────────────────────────────── + +func (p *Plane) registerRunSummary() { + p.mux.HandleFunc("GET /api/city/{cityName}/runs/summary", func(w http.ResponseWriter, r *http.Request) { + t, ok := p.cityRunTailer(r.PathValue("cityName")) + if !ok { + writeError(w, http.StatusNotFound, "unknown city") + return + } + writeJSON(w, http.StatusOK, t.enrichedSummary(r.Context())) + }) +} + +// runDetailErrorBody carries an UnsupportedRunError's reason to the SPA, which +// renders 'not_run_view' (an honest list-only run) differently from +// 'invalid_snapshot' (a genuine load failure). Typed like the other plane wire +// shapes (it extends the shared { error } body with the discriminating reason). +type runDetailErrorBody struct { + Error string `json:"error"` + Reason string `json:"reason"` +} + +func (p *Plane) registerRunDetail() { + p.mux.HandleFunc("GET /api/city/{cityName}/runs/{runId}/detail", func(w http.ResponseWriter, r *http.Request) { + t, ok := p.cityRunTailer(r.PathValue("cityName")) + if !ok { + writeError(w, http.StatusNotFound, "unknown city") + return + } + detail, ready, err := t.detail(r.Context(), r.PathValue("runId")) + if err != nil { + var unsupported *runproj.UnsupportedRunError + if errors.As(err, &unsupported) { + writeJSON(w, http.StatusUnprocessableEntity, runDetailErrorBody{ + Error: unsupported.Message, + Reason: string(unsupported.Reason), + }) + return + } + // The run root is absent from the warm projection. While the cold replay + // is still in flight the fold may be incomplete, so report warming + // rather than a hard 404 for a run that may yet appear. This 503 is a + // retry signal, not a terminal error: the SPA loader + // (supervisor/runDetail.ts loadSupervisorFormulaRunDetail) already + // retries any 5xx — including this warming 503 — with bounded backoff + // before surfacing it, so the client re-polls until the replay finishes + // (covered by runDetail.test.ts "retries while the projection is + // warming"). + if !ready { + writeError(w, http.StatusServiceUnavailable, "run view is warming") + return + } + writeError(w, http.StatusNotFound, "unknown run") + return + } + writeJSON(w, http.StatusOK, detail) + }) +} + +// cityRunTailer resolves the city to its run tailer, returning false for an +// unknown city (so the handler can 404). Starting the fold loop is lazy. +func (p *Plane) cityRunTailer(name string) (*cityRunTailer, bool) { + path, ok := p.resolveCityPath(name) + if !ok { + return nil, false + } + eventsPath := filepath.Join(path, ".gc", "events.jsonl") + return p.runTailers.ensure(name, eventsPath), true +} diff --git a/internal/api/dashboardbff/runtailer_test.go b/internal/api/dashboardbff/runtailer_test.go new file mode 100644 index 0000000000..75d280a307 --- /dev/null +++ b/internal/api/dashboardbff/runtailer_test.go @@ -0,0 +1,531 @@ +package dashboardbff + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" + "github.com/gastownhall/gascity/internal/runproj" +) + +type fakeResolver struct{ paths map[string]string } + +func (f fakeResolver) CityPath(name string) (string, bool) { + p, ok := f.paths[name] + return p, ok +} + +// runMoleculeEvent builds a bead.created event for a run-molecule lane carrying +// the markers isRunGroup recognizes plus an active assignee for session joins. +func runMoleculeEvent(seq uint64, id, formula, assignee string) events.Event { + b := beads.Bead{ + ID: id, + Title: formula, + Status: "open", + Type: "molecule", + Assignee: assignee, + CreatedAt: time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC), + Metadata: map[string]string{ + "gc.formula_contract": "graph.v2", + "gc.kind": "run", + "gc.formula": formula, + }, + } + payload, _ := json.Marshal(struct { + Bead beads.Bead `json:"bead"` + }{b}) + return events.Event{Seq: seq, Type: events.BeadCreated, Payload: payload} +} + +func writeEventLog(t *testing.T, path string, evts ...events.Event) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + var b strings.Builder + for _, e := range evts { + line, err := json.Marshal(e) + if err != nil { + t.Fatalf("marshal event: %v", err) + } + b.Write(line) + b.WriteByte('\n') + } + if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil { + t.Fatalf("write log: %v", err) + } +} + +// appendEvents appends events to an existing log via a plain O_APPEND handle — +// the supervisor's own write path. That it succeeds while the tailer is running +// proves the tailer is a pure reader (never a second writer holding the file). +func appendEvents(t *testing.T, path string, evts ...events.Event) { + t.Helper() + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + t.Fatalf("open append: %v", err) + } + defer f.Close() //nolint:errcheck + for _, e := range evts { + line, _ := json.Marshal(e) + if _, err := f.Write(append(line, '\n')); err != nil { + t.Fatalf("append: %v", err) + } + } +} + +func waitForLanes(t *testing.T, tl *cityRunTailer, want int) { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + tl.mu.RLock() + n := len(tl.summary.Lanes) + tl.mu.RUnlock() + if n == want { + return + } + time.Sleep(10 * time.Millisecond) + } + tl.mu.RLock() + n := len(tl.summary.Lanes) + tl.mu.RUnlock() + t.Fatalf("lane count = %d, want %d within deadline", n, want) +} + +// TestRunTailerColdLoadAndLiveTail proves the tailer cold-replays the existing +// log, then picks up newly appended events on its byte-offset tail — and that an +// external writer can still append while the tail runs (no second-writer lock). +func TestRunTailerColdLoadAndLiveTail(t *testing.T) { + defer func(prev time.Duration) { runTailPollInterval = prev }(runTailPollInterval) + runTailPollInterval = 15 * time.Millisecond + + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, runMoleculeEvent(1, "run1", "mol-adopt-pr-v2", "worker-1")) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var wg sync.WaitGroup + m := newRunTailerManager(Deps{}) + m.enable(ctx, &wg) + tl := m.ensure("alpha", logPath) + + select { + case <-tl.readyCh: + case <-time.After(2 * time.Second): + t.Fatal("cold replay did not complete") + } + waitForLanes(t, tl, 1) + + // Append a second run via the supervisor's own append path while the tail runs. + appendEvents(t, logPath, runMoleculeEvent(2, "run2", "mol-design-review-v2", "worker-2")) + waitForLanes(t, tl, 2) + + cancel() + wg.Wait() +} + +// TestRunTailerRotationCatchUp is the regression guard for the rotation +// event-drop: events written to the active log in the poll window before a +// rotation live only in the archived file, so on rotation the live tail must +// catch up across archives instead of resetting its byte offset and reading only +// the fresh active file. It drives foldNext directly (no ticker) so the +// pre-rotation runs are provably archived before the tailer next folds — the +// exact window the previous size-shrink reset silently dropped. +func TestRunTailerRotationCatchUp(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + rec, err := events.NewFileRecorder(logPath, io.Discard) + if err != nil { + t.Fatalf("recorder: %v", err) + } + defer rec.Close() //nolint:errcheck + + // Seed one run and cold-load it, so the tail cursor sits past the seed just + // like a warm tailer that has already folded the pre-rotation history. + rec.Record(runMoleculeEvent(0, "run1", "mol-adopt-pr-v2", "worker-1")) + + tl := &cityRunTailer{name: "alpha", eventsPath: logPath, readyCh: make(chan struct{})} + proj := runproj.NewProjector() + st := captureTailCursor(logPath) + if loadErr := proj.ColdLoad(logPath); loadErr != nil { + t.Fatalf("cold load: %v", loadErr) + } + st.marks = tl.build(proj, nil, nil) + + // Append two more runs to the ACTIVE file, then rotate before the tailer + // folds them: after the rename these live only in the archived .gz. + rec.Record(runMoleculeEvent(0, "run2", "mol-design-review-v2", "worker-2")) + rec.Record(runMoleculeEvent(0, "run3", "mol-bugflow-v1", "worker-3")) + if _, err := rec.ForceRotate(); err != nil { + t.Fatalf("force rotate: %v", err) + } + rec.WaitForRotations() // the pre-rotation runs are now only in the .gz archive. + + // A fresh run lands in the new active file after the rotation. + rec.Record(runMoleculeEvent(0, "run4", "mol-adopt-pr-v2", "worker-4")) + + // One fold must reconcile the archived pre-rotation runs AND the fresh + // active-file run — no sequence gap, no stale lane. + tl.foldNext(proj, st) + + got := map[string]bool{} + for _, lane := range tl.summary.Lanes { + got[lane.ID] = true + } + for _, want := range []string{"run1", "run2", "run3", "run4"} { + if !got[want] { + t.Errorf("lane %q missing after rotation; lanes=%v", want, laneIDsOf(tl.summary.Lanes)) + } + } + if len(tl.summary.Lanes) != 4 { + t.Errorf("lane count = %d, want 4; lanes=%v", len(tl.summary.Lanes), laneIDsOf(tl.summary.Lanes)) + } +} + +func laneIDsOf(lanes []runproj.RunLane) []string { + ids := make([]string, 0, len(lanes)) + for _, lane := range lanes { + ids = append(ids, lane.ID) + } + return ids +} + +// lanePresent reports whether a run lane with the given id is in the tailer's +// published summary. Safe to call directly in these single-goroutine tests that +// drive foldNext by hand (no live loop mutates t.summary concurrently). +func lanePresent(tl *cityRunTailer, id string) bool { + for _, lane := range tl.summary.Lanes { + if lane.ID == id { + return true + } + } + return false +} + +// TestRunTailerRotationCatchUpInFlightArchive is the regression guard for the +// async-compression window that TestRunTailerRotationCatchUp does not exercise +// (it waits for the gzip). After the recorder renames the active log to a plain +// events.jsonl.rotating-* file it gzips it in the BACKGROUND, so between the +// rename and the canonical .gz the just-rotated events live only in the rotating +// file — invisible to the .gz archive walker. A poll that folds during that +// window must still catch them, not advance the tail past them for good. The +// window is staged deterministically here: a real ForceRotate's gzip goroutine +// races the fold, so driving foldNext "without WaitForRotations" would flake +// (the gzip sometimes wins and the .gz path masks the bug). os.Rename preserves +// the pre-rotation inode on the rotating file, exactly as the recorder does, so +// foldNext detects the rotation by identity. +func TestRunTailerRotationCatchUpInFlightArchive(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + // A warm tailer that has already folded run1 (cursor past seq 1), like a + // tailer mid-run when a rotation happens. + writeEventLog(t, logPath, runMoleculeEvent(1, "run1", "mol-adopt-pr-v2", "worker-1")) + tl := &cityRunTailer{name: "alpha", eventsPath: logPath, readyCh: make(chan struct{})} + proj := runproj.NewProjector() + st := captureTailCursor(logPath) + if loadErr := proj.ColdLoad(logPath); loadErr != nil { + t.Fatalf("cold load: %v", loadErr) + } + st.marks = tl.build(proj, nil, nil) + + // run2, run3 land on the active log in the poll window before the rotation. + appendEvents(t, logPath, + runMoleculeEvent(2, "run2", "mol-design-review-v2", "worker-2"), + runMoleculeEvent(3, "run3", "mol-bugflow-v1", "worker-3"), + ) + + // Rotation, staged as the recorder does it but with the gzip still pending: + // rename the active log (seq 1-3) to a plain rotating-* file — no .gz yet — + // then open a fresh active log. The rename carries the old inode to the + // rotating file, so the fresh active log is a distinct identity. + rotating := filepath.Join(filepath.Dir(logPath), "events.jsonl.rotating-20260601T120000Z-seq-1-3") + if err := os.Rename(logPath, rotating); err != nil { + t.Fatalf("rename to rotating: %v", err) + } + writeEventLog(t, logPath, runMoleculeEvent(4, "run4", "mol-adopt-pr-v2", "worker-4")) + + // One fold must reconcile the in-flight pre-rotation runs AND the fresh + // active-file run — the drop happens only if the catch-up ignores the + // rotating file. + tl.foldNext(proj, st) + + got := map[string]bool{} + for _, lane := range tl.summary.Lanes { + got[lane.ID] = true + } + for _, want := range []string{"run1", "run2", "run3", "run4"} { + if !got[want] { + t.Errorf("lane %q missing after in-flight rotation; lanes=%v", want, laneIDsOf(tl.summary.Lanes)) + } + } + if len(tl.summary.Lanes) != 4 { + t.Errorf("lane count = %d, want 4; lanes=%v", len(tl.summary.Lanes), laneIDsOf(tl.summary.Lanes)) + } +} + +// TestRunTailerStartupCursorRotationRaceDoesNotSkip is the regression guard for +// the startup cursor split-stat race: the old capture read the byte offset and +// the active-file identity with two separate stats, so a rotation between them +// paired the OLD file's larger offset with the FRESH file's identity. The first +// foldNext then saw no identity change, ReadFrom seeked past the fresh file's EOF +// (reader.go returns the same offset when no bytes are available), and every +// fresh event below the stale offset was silently dropped until restart. It +// reproduces that exact corrupted cursor — a stale beyond-EOF offset on the +// current active identity — and proves one foldNext still folds the fresh event. +func TestRunTailerStartupCursorRotationRaceDoesNotSkip(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + + // A warm tailer that already folded run1..run3 (cursor past seq 3) off a + // larger pre-rotation active file. + writeEventLog(t, logPath, + runMoleculeEvent(1, "run1", "mol-adopt-pr-v2", "worker-1"), + runMoleculeEvent(2, "run2", "mol-design-review-v2", "worker-2"), + runMoleculeEvent(3, "run3", "mol-bugflow-v1", "worker-3"), + ) + tl := &cityRunTailer{name: "alpha", eventsPath: logPath, readyCh: make(chan struct{})} + proj := runproj.NewProjector() + if loadErr := proj.ColdLoad(logPath); loadErr != nil { + t.Fatalf("cold load: %v", loadErr) + } + st := captureTailCursor(logPath) + st.marks = tl.build(proj, nil, nil) + staleOffset := st.offset // the pre-rotation (larger) active-file size + + // The fresh post-rotation active file is smaller and carries run4. Rewriting + // in place keeps a stable identity — exactly the one the racy two-stat capture + // would have paired with the OLD file's larger offset — so foldNext sees no + // identity change and must instead recover from the stale beyond-EOF offset. + writeEventLog(t, logPath, runMoleculeEvent(4, "run4", "mol-adopt-pr-v2", "worker-4")) + freshInfo, err := os.Stat(logPath) + if err != nil { + t.Fatalf("stat fresh active: %v", err) + } + if staleOffset <= freshInfo.Size() { + t.Fatalf("precondition: stale offset %d must exceed fresh size %d", staleOffset, freshInfo.Size()) + } + st.offset = staleOffset + st.activeInfo = freshInfo + + tl.foldNext(proj, st) + + if !lanePresent(tl, "run4") { + t.Errorf("run4 skipped: a fresh event below the stale startup offset was dropped; lanes=%v", laneIDsOf(tl.summary.Lanes)) + } + for _, want := range []string{"run1", "run2", "run3"} { + if !lanePresent(tl, want) { + t.Errorf("pre-rotation lane %q lost; lanes=%v", want, laneIDsOf(tl.summary.Lanes)) + } + } +} + +// TestRunTailerRotationCatchUpErrorRetriesNextPoll is the regression guard for +// the rotation catch-up state-commit gap: on a detected rotation the tailer must +// catch up the just-rotated events (now only in the archive) BEFORE advancing its +// active identity and resetting its offset. The old code committed the fresh +// identity and reset the offset even when the catch-up read failed, so the next +// poll saw no rotation (SameFile) and the run2/run3 window was lost until restart. +// A transient catch-up error must instead leave the old identity in place so the +// next poll re-detects the rotation and recovers the whole window. +func TestRunTailerRotationCatchUpErrorRetriesNextPoll(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + + // Warm tailer that folded run1 (cursor past seq 1). + writeEventLog(t, logPath, runMoleculeEvent(1, "run1", "mol-adopt-pr-v2", "worker-1")) + tl := &cityRunTailer{name: "alpha", eventsPath: logPath, readyCh: make(chan struct{})} + proj := runproj.NewProjector() + if loadErr := proj.ColdLoad(logPath); loadErr != nil { + t.Fatalf("cold load: %v", loadErr) + } + st := captureTailCursor(logPath) + st.marks = tl.build(proj, nil, nil) + preRotationInfo := st.activeInfo + + // run2, run3 land on the active log in the poll window, then a rotation moves + // them to a plain rotating-* archive and opens a fresh active file (run4). The + // rename carries the old inode to the rotating file, so the fresh active file + // is a distinct identity foldNext detects as a rotation. + appendEvents(t, logPath, + runMoleculeEvent(2, "run2", "mol-design-review-v2", "worker-2"), + runMoleculeEvent(3, "run3", "mol-bugflow-v1", "worker-3"), + ) + rotating := filepath.Join(dir, ".gc", "events.jsonl.rotating-20260601T120000Z-seq-2-3") + if err := os.Rename(logPath, rotating); err != nil { + t.Fatalf("rename to rotating: %v", err) + } + writeEventLog(t, logPath, runMoleculeEvent(4, "run4", "mol-adopt-pr-v2", "worker-4")) + + // Fail the first catch-up read, then fall through to the real reader. + defer func(prev func(string, events.Filter) ([]events.Event, error)) { readRotationCatchUp = prev }(readRotationCatchUp) + realCatchUp := events.ReadFilteredWithInFlight + calls := 0 + readRotationCatchUp = func(path string, f events.Filter) ([]events.Event, error) { + calls++ + if calls == 1 { + return nil, errors.New("transient catch-up read error") + } + return realCatchUp(path, f) + } + + // First poll: catch-up errors. Nothing folds, and the tailer must not advance + // its active identity or the next poll can no longer re-detect the rotation. + tl.foldNext(proj, st) + if lanePresent(tl, "run2") || lanePresent(tl, "run3") || lanePresent(tl, "run4") { + t.Fatalf("events folded despite a catch-up error; lanes=%v", laneIDsOf(tl.summary.Lanes)) + } + if !os.SameFile(preRotationInfo, st.activeInfo) { + t.Fatalf("active identity advanced on a catch-up error; the next poll can no longer re-detect the rotation") + } + + // Second poll: catch-up succeeds and recovers the whole rotation window. + tl.foldNext(proj, st) + for _, want := range []string{"run1", "run2", "run3", "run4"} { + if !lanePresent(tl, want) { + t.Errorf("lane %q missing after catch-up retry; lanes=%v", want, laneIDsOf(tl.summary.Lanes)) + } + } +} + +// TestRunSummaryEndpointEnrichesFromSessions drives the full endpoint: the warm +// fold plus request-time session enrich resolves a lane's session to available +// health and an available census. +func TestRunSummaryEndpointEnrichesFromSessions(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, runMoleculeEvent(1, "run1", "mol-adopt-pr-v2", "worker-1")) + + sessions := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v0/city/alpha/sessions" { + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"items":[{"id":"s1","template":"t","session_name":"alpha__worker-1","title":"W","alias":"worker-1","state":"active","created_at":"2026-06-01T10:00:00Z","last_active":"2026-06-01T11:00:00Z","attached":false,"running":true,"activity":"thinking","provider":"claude"}],"total":1}`)) + })) + defer sessions.Close() + + p := New(Deps{ + Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}, + SupervisorBaseURL: sessions.URL, + }) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + p.Start(ctx) + defer p.Stop() + + resp := getRunSummary(t, p, "alpha") + if resp.TotalActive != 1 || len(resp.Lanes) != 1 { + t.Fatalf("totalActive=%d lanes=%d, want 1/1", resp.TotalActive, len(resp.Lanes)) + } + lane := resp.Lanes[0] + if lane.Health.Status != "available" { + t.Errorf("lane health = %q, want available", lane.Health.Status) + } + if lane.Health.Data.Session.Status != "resolved" { + t.Errorf("session status = %q, want resolved", lane.Health.Data.Session.Status) + } + if resp.Census.Status != "available" { + t.Errorf("census status = %q, want available", resp.Census.Status) + } +} + +// TestRunSummaryEndpointDegradesWithoutSessions proves a sessions outage degrades +// lane health to unavailable (counted unverifiable in the census) rather than +// failing the load. +func TestRunSummaryEndpointDegradesWithoutSessions(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, ".gc", "events.jsonl") + writeEventLog(t, logPath, runMoleculeEvent(1, "run1", "mol-adopt-pr-v2", "worker-1")) + + // No SupervisorBaseURL: the sessions read is unavailable. + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{"alpha": dir}}}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + p.Start(ctx) + defer p.Stop() + + resp := getRunSummary(t, p, "alpha") + if len(resp.Lanes) != 1 { + t.Fatalf("lanes = %d, want 1", len(resp.Lanes)) + } + if resp.Lanes[0].Health.Status != "unavailable" { + t.Errorf("lane health = %q, want unavailable on sessions outage", resp.Lanes[0].Health.Status) + } + if resp.Census.Status != "available" { + t.Errorf("census status = %q, want available", resp.Census.Status) + } + if resp.Census.Data.TotalInFlight < 1 || resp.Census.Data.Unverifiable < 1 { + t.Errorf("census = %+v, want >=1 in-flight and >=1 unverifiable", resp.Census.Data) + } +} + +// TestRunSummaryEndpointUnknownCity404s confirms an unresolvable city 404s. +func TestRunSummaryEndpointUnknownCity404s(t *testing.T) { + p := New(Deps{Resolver: fakeResolver{paths: map[string]string{}}}) + rec := httptest.NewRecorder() + p.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/city/ghost/runs/summary", nil)) + if rec.Code != http.StatusNotFound { + t.Errorf("status = %d, want 404 for unknown city", rec.Code) + } +} + +// runSummaryWire is the decoded endpoint body — a structural contract check that +// the wire carries the enriched RunSummary shape the SPA renderer reads. +type runSummaryWire struct { + TotalActive int `json:"totalActive"` + Lanes []struct { + ID string `json:"id"` + Health struct { + Status string `json:"status"` + Data struct { + PhaseConfidence string `json:"phaseConfidence"` + Session struct { + Status string `json:"status"` + } `json:"session"` + } `json:"data"` + } `json:"health"` + } `json:"lanes"` + Census struct { + Status string `json:"status"` + Data struct { + TotalInFlight int `json:"totalInFlight"` + Unverifiable int `json:"unverifiable"` + } `json:"data"` + } `json:"census"` +} + +func getRunSummary(t *testing.T, p *Plane, city string) runSummaryWire { + t.Helper() + rec := httptest.NewRecorder() + p.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/city/"+city+"/runs/summary", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + var resp runSummaryWire + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v; body=%s", err, rec.Body.String()) + } + return resp +} diff --git a/internal/api/dashboardspa/dist/assets/Activity-Ca_fEMiY.js b/internal/api/dashboardspa/dist/assets/Activity-Ca_fEMiY.js deleted file mode 100644 index 325d4dfcc5..0000000000 --- a/internal/api/dashboardspa/dist/assets/Activity-Ca_fEMiY.js +++ /dev/null @@ -1,2 +0,0 @@ -import{K as _,J as q,a as P,M as B,b as F,j as t,B as V,L as W,ao as $,ap as D,ab as A,z as v,S as R,I as M}from"./index-zPatq59W.js";import{r as C,c as b}from"./routeHighlight-B30gQO2o.js";import{P as z}from"./PageHeader-DGfr1hUc.js";import{b as G,a as O}from"./time-D9v0saHV.js";import{u as H}from"./useVisibleRefresh-DdfUjLcH.js";const U=100,f="24h";async function K(e={}){const s=_("list supervisor events"),a=await q().listEvents(s,{limit:U,since:f,...e}),i=a.items??[];return i.sort((n,l)=>l.seq-n.seq),{...a,items:i,total:Number(a.total)}}const J=[{mode:"all",label:"All"},{mode:"events",label:"Events"},{mode:"deploys",label:"Deploys"},{mode:"commits",label:"Commits"}],L=[{value:"1h",label:"Last hour"},{value:f,label:"Last 24 hours"},{value:"7d",label:"Last 7 days"}],Q=[{value:"all",label:"All signals"},{value:"attention",label:"Attention"},{value:"watch",label:"Watch"},{value:"event",label:"Event"}];function Ne(){const e=P(),[s,a]=B(),i=de(s),n=d(i,"events"),l=n?x(s.get("type")):null,o=n?x(s.get("actor")):null,r=n?me(s):f,c=n?pe(s):"all",h=n?x(s.get("q")):null,I=["activity:bundle",M()??"no-city",i,l??"all",o??"all",r,c,h??""].join(":"),{data:u,loading:m,error:S,refresh:k}=F(I,()=>X(i,l,o,r,c,h));return H(k,3e4),t.jsxs("section",{children:[t.jsx(z,{title:"Activity",synopsis:ie(i,l),meta:t.jsxs(t.Fragment,{children:[S&&t.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:S}),t.jsx(V,{size:"sm",onClick:()=>{k()},disabled:m,children:m?"Refreshing":"Refresh"})]})}),t.jsx(Z,{active:i,eventType:l}),n&&t.jsx(ee,{eventType:l,eventActor:o,eventWindow:r,eventSignal:c,searchParams:s,setSearchParams:a,textFilter:h}),t.jsxs("div",{className:"mt-10 space-y-12",children:[d(i,"events")&&t.jsx(te,{events:u?.events??null,...u?.eventsError!==void 0?{error:u.eventsError}:{},filterActive:l!==null||o!==null||c!=="all"||h!==null,loading:m,attentionSeverity:g=>C(e,"activity",oe(g))}),d(i,"deploys")&&t.jsx(se,{deploys:u?.deploys??null,...u?.deploysError!==void 0?{error:u.deploysError}:{},loading:m,attentionSeverity:g=>C(e,"activity",ce(g))}),d(i,"commits")&&t.jsx(re,{commits:u?.commits??null,...u?.commitsError!==void 0?{error:u.commitsError}:{},loading:m})]})]})}async function X(e,s,a,i,n,l){const[o,r,c]=await Promise.allSettled([d(e,"events")?Y(s,a,i,n,l):Promise.resolve(null),d(e,"deploys")?A.listBuilds():Promise.resolve(null),d(e,"commits")?A.listCommits("recent-all"):Promise.resolve(null)]);return{commits:j(c),...c.status==="rejected"?{commitsError:v(c.reason,"git commits unavailable")}:{},deploys:j(r),...r.status==="rejected"?{deploysError:v(r.reason,"deploy history unavailable")}:{},events:j(o),...o.status==="rejected"?{eventsError:v(o.reason,"event history unavailable")}:{}}}async function Y(e,s,a,i,n){const l=await K({since:a,...e===null?{}:{type:e},...s===null?{}:{actor:s}}),o=n?.toLowerCase()??"",r=l.items.filter(c=>e!==null&&c.type!==e||s!==null&&c.actor!==s||i!=="all"&&$(c)!==i?!1:o.length===0?!0:xe(c).includes(o));return{...l,items:r,total:r.length}}function j(e){return e.status==="fulfilled"?e.value:null}function Z({active:e,eventType:s}){return t.jsx("nav",{"aria-label":"Activity modes",children:t.jsx("ul",{className:"flex flex-wrap gap-2",children:J.map(({mode:a,label:i})=>{const n=e===a;return t.jsx("li",{children:t.jsx(W,{to:ue(a,s),"aria-current":n?"page":void 0,className:["inline-flex items-center rounded-sm border px-2.5 py-1 text-label uppercase tracking-wider transition-colors duration-150 ease-out-quart focus-mark",n?"border-fg text-fg":"border-rule text-fg-muted hover:text-fg hover:bg-surface-tint"].join(" "),children:i})},a)})})})}function ee({eventActor:e,eventSignal:s,eventType:a,eventWindow:i,searchParams:n,setSearchParams:l,textFilter:o}){return t.jsxs("div",{className:"mt-6 flex flex-wrap items-end gap-4",children:[t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Event window",t.jsx("select",{"aria-label":"Event window",value:i,onChange:r=>p(l,n,"since",r.currentTarget.value,f),className:"min-w-36 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg focus-mark",children:L.map(r=>t.jsx("option",{value:r.value,children:r.label},r.value))})]}),t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Event type",t.jsx("input",{"aria-label":"Event type",value:a??"",onChange:r=>p(l,n,"type",r.currentTarget.value),placeholder:"session.crashed",className:"min-w-44 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg placeholder:text-fg-faint focus-mark"})]}),t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Event actor",t.jsx("input",{"aria-label":"Event actor",value:e??"",onChange:r=>p(l,n,"actor",r.currentTarget.value),placeholder:"supervisor",className:"min-w-40 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg placeholder:text-fg-faint focus-mark"})]}),t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Signal severity",t.jsx("select",{"aria-label":"Signal severity",value:s,onChange:r=>p(l,n,"signal",r.currentTarget.value,"all"),className:"min-w-36 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg focus-mark",children:Q.map(r=>t.jsx("option",{value:r.value,children:r.label},r.value))})]}),t.jsxs("label",{className:"grid min-w-56 flex-1 gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Search activity",t.jsx("input",{"aria-label":"Search activity",value:o??"",onChange:r=>p(l,n,"q",r.currentTarget.value),placeholder:"actor, subject, or message",className:"rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg placeholder:text-fg-faint focus-mark"})]})]})}function te({error:e,events:s,filterActive:a,loading:i,attentionSeverity:n}){const l=s?.items??[],o=fe(s);return t.jsxs(y,{title:"Supervisor events",meta:s===null?null:`${s.total} events`,children:[e!==void 0&&t.jsxs("p",{className:"text-body text-accent",role:"alert",children:["Event history unavailable: ",e,"."]}),s?.partial===!0&&t.jsxs("p",{className:"text-body text-warn",children:["Event history incomplete",o.length>0?`: ${o.join("; ")}`:"."]}),t.jsxs(N,{label:"Supervisor events",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:[t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Time"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Signal"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Type"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Subject"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Detail"})]})}),t.jsx("tbody",{children:l.length===0?t.jsx(w,{colSpan:5,children:i?"Reading supervisor events.":e!==void 0?"Event history unavailable.":a?"No supervisor events match these filters.":"No supervisor events in this window."}):l.map((r,c)=>t.jsxs("tr",{...b(n(r)),className:`border-b border-rule ${b(n(r)).className??""}`,children:[t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:t.jsx(E,{ts:r.ts})}),t.jsx("td",{className:"py-3 pr-6 align-baseline",children:t.jsx(le,{signal:$(r)})}),t.jsx("td",{className:"py-3 pr-6 align-baseline font-medium text-fg",children:r.type}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:r.subject??"·"}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:D(r)})]},`${r.seq}:${r.type}:${c}`))})]})]})}function se({deploys:e,error:s,loading:a,attentionSeverity:i}){const n=e?.items??[];return t.jsxs(y,{title:"Deploy history",meta:e?.failed_marker===!0?"failed marker present":e?.source??null,children:[s!==void 0&&t.jsxs("p",{className:"text-body text-accent",role:"alert",children:["Deploy history unavailable: ",s,"."]}),t.jsxs(N,{label:"Deploy history",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:[t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Time"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Status"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Detail"})]})}),t.jsx("tbody",{children:n.length===0?t.jsx(w,{colSpan:3,children:a?"Reading deploy history.":"No deploy records in this window."}):n.map(l=>t.jsxs("tr",{...b(i(l)),className:`border-b border-rule ${b(i(l)).className??""}`,children:[t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:t.jsx(E,{ts:l.at})}),t.jsx("td",{className:"py-3 pr-6 align-baseline",children:t.jsx(ne,{deploy:l})}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:l.detail})]},`${l.at}:${l.detail}`))})]})]})}function re({commits:e,error:s,loading:a}){const i=e?.items??[];return t.jsxs(y,{title:"Git commits",meta:e===null?null:e.view,children:[s!==void 0&&t.jsxs("p",{className:"text-body text-accent",role:"alert",children:["Git commits unavailable: ",s,"."]}),t.jsxs(N,{label:"Git commits",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:[t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Time"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Commit"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Author"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Subject"})]})}),t.jsx("tbody",{children:i.length===0?t.jsx(w,{colSpan:4,children:a?"Reading git commits.":"No commits in this window."}):i.map(n=>t.jsx(ae,{commit:n},n.sha))})]})]})}function ae({commit:e}){return t.jsxs("tr",{className:"border-b border-rule",children:[t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:t.jsx(E,{ts:e.date})}),t.jsx("td",{className:"py-3 pr-6 align-baseline font-medium text-fg",children:e.short_sha}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:e.author}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:e.subject})]})}function y({children:e,meta:s,title:a}){return t.jsxs("section",{"aria-labelledby":T(a),className:"space-y-4",children:[t.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[t.jsx("h2",{id:T(a),className:"text-headline font-semibold tracking-tight text-fg",children:a}),s!==null&&t.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:s})]}),e]})}function N({children:e,label:s}){return t.jsx("div",{className:"overflow-x-auto",children:t.jsx("table",{"aria-label":s,className:"w-full text-body tnum",children:e})})}function w({children:e,colSpan:s}){return t.jsx("tr",{children:t.jsx("td",{colSpan:s,className:"py-10 text-center text-fg-muted italic",children:e})})}function E({ts:e}){return t.jsx("span",{title:O(e),children:G(e)})}function le({signal:e}){const s=e==="attention"?"stuck":e==="watch"?"warn":"neutral";return t.jsx(R,{tone:s,label:e})}function ne({deploy:e}){const s=e.status==="ok"?"ok":e.status==="failed"?"stuck":e.status==="in-progress"?"warn":"neutral";return t.jsx(R,{tone:s,label:e.status})}function ie(e,s){return e==="events"&&s!==null?`Supervisor events filtered to ${s}.`:e==="events"?"Supervisor event history from the active city.":e==="deploys"?"Deploy history from dashboard-local project logs.":e==="commits"?"Recent git commits from the local project checkout.":"Supervisor events, deploy history, and recent project commits."}function oe(e){return`event:${String(e.seq)}:${e.type}`}function ce(e){return e.status==="failed"||e.status==="in-progress"?`deploy:${e.at}:${e.status}`:`deploy:${e.at}`}function ue(e,s){if(e==="all")return"/activity";const a=new URLSearchParams;return a.set("mode",e),e==="events"&&s!==null&&a.set("type",s),`/activity?${a.toString()}`}function d(e,s){return e==="all"||e===s}function de(e){const s=e.get("mode");return s==="events"||s==="deploys"||s==="commits"?s:"all"}function x(e){if(e===null)return null;const s=e.trim();return s.length===0?null:s}function me(e){const s=x(e.get("since"));return s!==null&&L.some(a=>a.value===s)?s:f}function pe(e){const s=x(e.get("signal"));return s==="attention"||s==="watch"||s==="event"?s:"all"}function p(e,s,a,i,n){const l=new URLSearchParams(s),o=i.trim();o.length===0||o===n?l.delete(a):l.set(a,o),e(l)}function xe(e){return[e.type,e.actor,e.subject,e.message,D(e)].filter(s=>typeof s=="string").join(` -`).toLowerCase()}function fe(e){const s=e?.partial_errors;return Array.isArray(s)?s.filter(a=>typeof a=="string"&&a.length>0):[]}function T(e){return`activity-${e.toLowerCase().replace(/[^a-z0-9]+/g,"-")}`}export{Ne as ActivityPage}; diff --git a/internal/api/dashboardspa/dist/assets/Activity-CiU9eU59.js b/internal/api/dashboardspa/dist/assets/Activity-CiU9eU59.js new file mode 100644 index 0000000000..3b4dc27310 --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/Activity-CiU9eU59.js @@ -0,0 +1,2 @@ +import{J as _,I as q,a as P,K as B,b as F,j as t,B as V,L as W,a7 as $,a8 as D,X as A,z as v,S as R,H as M}from"./index-QWRimsO3.js";import{r as C,c as b}from"./routeHighlight-B30gQO2o.js";import{P as z}from"./PageHeader-CxbYmkHZ.js";import{b as G,a as H}from"./time-D9v0saHV.js";import{u as O}from"./useVisibleRefresh-Bm_cCiAg.js";const U=100,f="24h";async function K(e={}){const s=_("list supervisor events"),a=await q().listEvents(s,{limit:U,since:f,...e}),i=a.items??[];return i.sort((n,l)=>l.seq-n.seq),{...a,items:i,total:Number(a.total)}}const J=[{mode:"all",label:"All"},{mode:"events",label:"Events"},{mode:"deploys",label:"Deploys"},{mode:"commits",label:"Commits"}],L=[{value:"1h",label:"Last hour"},{value:f,label:"Last 24 hours"},{value:"7d",label:"Last 7 days"}],X=[{value:"all",label:"All signals"},{value:"attention",label:"Attention"},{value:"watch",label:"Watch"},{value:"event",label:"Event"}];function Ne(){const e=P(),[s,a]=B(),i=de(s),n=d(i,"events"),l=n?x(s.get("type")):null,o=n?x(s.get("actor")):null,r=n?me(s):f,c=n?pe(s):"all",h=n?x(s.get("q")):null,I=["activity:bundle",M()??"no-city",i,l??"all",o??"all",r,c,h??""].join(":"),{data:u,loading:m,error:S,refresh:k}=F(I,()=>Q(i,l,o,r,c,h));return O(k,3e4),t.jsxs("section",{children:[t.jsx(z,{title:"Activity",synopsis:ie(i,l),meta:t.jsxs(t.Fragment,{children:[S&&t.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:S}),t.jsx(V,{size:"sm",onClick:()=>{k()},disabled:m,children:m?"Refreshing":"Refresh"})]})}),t.jsx(Z,{active:i,eventType:l}),n&&t.jsx(ee,{eventType:l,eventActor:o,eventWindow:r,eventSignal:c,searchParams:s,setSearchParams:a,textFilter:h}),t.jsxs("div",{className:"mt-10 space-y-12",children:[d(i,"events")&&t.jsx(te,{events:u?.events??null,...u?.eventsError!==void 0?{error:u.eventsError}:{},filterActive:l!==null||o!==null||c!=="all"||h!==null,loading:m,attentionSeverity:g=>C(e,"activity",oe(g))}),d(i,"deploys")&&t.jsx(se,{deploys:u?.deploys??null,...u?.deploysError!==void 0?{error:u.deploysError}:{},loading:m,attentionSeverity:g=>C(e,"activity",ce(g))}),d(i,"commits")&&t.jsx(re,{commits:u?.commits??null,...u?.commitsError!==void 0?{error:u.commitsError}:{},loading:m})]})]})}async function Q(e,s,a,i,n,l){const[o,r,c]=await Promise.allSettled([d(e,"events")?Y(s,a,i,n,l):Promise.resolve(null),d(e,"deploys")?A.listBuilds():Promise.resolve(null),d(e,"commits")?A.listCommits("recent-all"):Promise.resolve(null)]);return{commits:j(c),...c.status==="rejected"?{commitsError:v(c.reason,"git commits unavailable")}:{},deploys:j(r),...r.status==="rejected"?{deploysError:v(r.reason,"deploy history unavailable")}:{},events:j(o),...o.status==="rejected"?{eventsError:v(o.reason,"event history unavailable")}:{}}}async function Y(e,s,a,i,n){const l=await K({since:a,...e===null?{}:{type:e},...s===null?{}:{actor:s}}),o=n?.toLowerCase()??"",r=l.items.filter(c=>e!==null&&c.type!==e||s!==null&&c.actor!==s||i!=="all"&&$(c)!==i?!1:o.length===0?!0:xe(c).includes(o));return{...l,items:r,total:r.length}}function j(e){return e.status==="fulfilled"?e.value:null}function Z({active:e,eventType:s}){return t.jsx("nav",{"aria-label":"Activity modes",children:t.jsx("ul",{className:"flex flex-wrap gap-2",children:J.map(({mode:a,label:i})=>{const n=e===a;return t.jsx("li",{children:t.jsx(W,{to:ue(a,s),"aria-current":n?"page":void 0,className:["inline-flex items-center rounded-sm border px-2.5 py-1 text-label uppercase tracking-wider transition-colors duration-150 ease-out-quart focus-mark",n?"border-fg text-fg":"border-rule text-fg-muted hover:text-fg hover:bg-surface-tint"].join(" "),children:i})},a)})})})}function ee({eventActor:e,eventSignal:s,eventType:a,eventWindow:i,searchParams:n,setSearchParams:l,textFilter:o}){return t.jsxs("div",{className:"mt-6 flex flex-wrap items-end gap-4",children:[t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Event window",t.jsx("select",{"aria-label":"Event window",value:i,onChange:r=>p(l,n,"since",r.currentTarget.value,f),className:"min-w-36 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg focus-mark",children:L.map(r=>t.jsx("option",{value:r.value,children:r.label},r.value))})]}),t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Event type",t.jsx("input",{"aria-label":"Event type",value:a??"",onChange:r=>p(l,n,"type",r.currentTarget.value),placeholder:"session.crashed",className:"min-w-44 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg placeholder:text-fg-faint focus-mark"})]}),t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Event actor",t.jsx("input",{"aria-label":"Event actor",value:e??"",onChange:r=>p(l,n,"actor",r.currentTarget.value),placeholder:"supervisor",className:"min-w-40 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg placeholder:text-fg-faint focus-mark"})]}),t.jsxs("label",{className:"grid gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Signal severity",t.jsx("select",{"aria-label":"Signal severity",value:s,onChange:r=>p(l,n,"signal",r.currentTarget.value,"all"),className:"min-w-36 rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg focus-mark",children:X.map(r=>t.jsx("option",{value:r.value,children:r.label},r.value))})]}),t.jsxs("label",{className:"grid min-w-56 flex-1 gap-1 text-label uppercase tracking-wider text-fg-muted",children:["Search activity",t.jsx("input",{"aria-label":"Search activity",value:o??"",onChange:r=>p(l,n,"q",r.currentTarget.value),placeholder:"actor, subject, or message",className:"rounded-sm border border-rule bg-surface px-2 py-1 text-body normal-case tracking-normal text-fg placeholder:text-fg-faint focus-mark"})]})]})}function te({error:e,events:s,filterActive:a,loading:i,attentionSeverity:n}){const l=s?.items??[],o=fe(s);return t.jsxs(y,{title:"Supervisor events",meta:s===null?null:`${s.total} events`,children:[e!==void 0&&t.jsxs("p",{className:"text-body text-accent",role:"alert",children:["Event history unavailable: ",e,"."]}),s?.partial===!0&&t.jsxs("p",{className:"text-body text-warn",children:["Event history incomplete",o.length>0?`: ${o.join("; ")}`:"."]}),t.jsxs(N,{label:"Supervisor events",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:[t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Time"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Signal"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Type"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Subject"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Detail"})]})}),t.jsx("tbody",{children:l.length===0?t.jsx(w,{colSpan:5,children:i?"Reading supervisor events.":e!==void 0?"Event history unavailable.":a?"No supervisor events match these filters.":"No supervisor events in this window."}):l.map((r,c)=>t.jsxs("tr",{...b(n(r)),className:`border-b border-rule ${b(n(r)).className??""}`,children:[t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:t.jsx(E,{ts:r.ts})}),t.jsx("td",{className:"py-3 pr-6 align-baseline",children:t.jsx(le,{signal:$(r)})}),t.jsx("td",{className:"py-3 pr-6 align-baseline font-medium text-fg",children:r.type}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:r.subject??"·"}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:D(r)})]},`${r.seq}:${r.type}:${c}`))})]})]})}function se({deploys:e,error:s,loading:a,attentionSeverity:i}){const n=e?.items??[];return t.jsxs(y,{title:"Deploy history",meta:e?.failed_marker===!0?"failed marker present":e?.source??null,children:[s!==void 0&&t.jsxs("p",{className:"text-body text-accent",role:"alert",children:["Deploy history unavailable: ",s,"."]}),t.jsxs(N,{label:"Deploy history",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:[t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Time"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Status"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Detail"})]})}),t.jsx("tbody",{children:n.length===0?t.jsx(w,{colSpan:3,children:a?"Reading deploy history.":"No deploy records in this window."}):n.map(l=>t.jsxs("tr",{...b(i(l)),className:`border-b border-rule ${b(i(l)).className??""}`,children:[t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:t.jsx(E,{ts:l.at})}),t.jsx("td",{className:"py-3 pr-6 align-baseline",children:t.jsx(ne,{deploy:l})}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:l.detail})]},`${l.at}:${l.detail}`))})]})]})}function re({commits:e,error:s,loading:a}){const i=e?.items??[];return t.jsxs(y,{title:"Git commits",meta:e===null?null:e.view,children:[s!==void 0&&t.jsxs("p",{className:"text-body text-accent",role:"alert",children:["Git commits unavailable: ",s,"."]}),t.jsxs(N,{label:"Git commits",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:[t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Time"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Commit"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Author"}),t.jsx("th",{scope:"col",className:"pb-3 pr-6 text-left font-medium",children:"Subject"})]})}),t.jsx("tbody",{children:i.length===0?t.jsx(w,{colSpan:4,children:a?"Reading git commits.":"No commits in this window."}):i.map(n=>t.jsx(ae,{commit:n},n.sha))})]})]})}function ae({commit:e}){return t.jsxs("tr",{className:"border-b border-rule",children:[t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:t.jsx(E,{ts:e.date})}),t.jsx("td",{className:"py-3 pr-6 align-baseline font-medium text-fg",children:e.short_sha}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:e.author}),t.jsx("td",{className:"py-3 pr-6 align-baseline text-fg-muted",children:e.subject})]})}function y({children:e,meta:s,title:a}){return t.jsxs("section",{"aria-labelledby":T(a),className:"space-y-4",children:[t.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[t.jsx("h2",{id:T(a),className:"text-headline font-semibold tracking-tight text-fg",children:a}),s!==null&&t.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:s})]}),e]})}function N({children:e,label:s}){return t.jsx("div",{className:"overflow-x-auto",children:t.jsx("table",{"aria-label":s,className:"w-full text-body tnum",children:e})})}function w({children:e,colSpan:s}){return t.jsx("tr",{children:t.jsx("td",{colSpan:s,className:"py-10 text-center text-fg-muted italic",children:e})})}function E({ts:e}){return t.jsx("span",{title:H(e),children:G(e)})}function le({signal:e}){const s=e==="attention"?"stuck":e==="watch"?"warn":"neutral";return t.jsx(R,{tone:s,label:e})}function ne({deploy:e}){const s=e.status==="ok"?"ok":e.status==="failed"?"stuck":e.status==="in-progress"?"warn":"neutral";return t.jsx(R,{tone:s,label:e.status})}function ie(e,s){return e==="events"&&s!==null?`Supervisor events filtered to ${s}.`:e==="events"?"Supervisor event history from the active city.":e==="deploys"?"Deploy history from dashboard-local project logs.":e==="commits"?"Recent git commits from the local project checkout.":"Supervisor events, deploy history, and recent project commits."}function oe(e){return`event:${String(e.seq)}:${e.type}`}function ce(e){return e.status==="failed"||e.status==="in-progress"?`deploy:${e.at}:${e.status}`:`deploy:${e.at}`}function ue(e,s){if(e==="all")return"/activity";const a=new URLSearchParams;return a.set("mode",e),e==="events"&&s!==null&&a.set("type",s),`/activity?${a.toString()}`}function d(e,s){return e==="all"||e===s}function de(e){const s=e.get("mode");return s==="events"||s==="deploys"||s==="commits"?s:"all"}function x(e){if(e===null)return null;const s=e.trim();return s.length===0?null:s}function me(e){const s=x(e.get("since"));return s!==null&&L.some(a=>a.value===s)?s:f}function pe(e){const s=x(e.get("signal"));return s==="attention"||s==="watch"||s==="event"?s:"all"}function p(e,s,a,i,n){const l=new URLSearchParams(s),o=i.trim();o.length===0||o===n?l.delete(a):l.set(a,o),e(l)}function xe(e){return[e.type,e.actor,e.subject,e.message,D(e)].filter(s=>typeof s=="string").join(` +`).toLowerCase()}function fe(e){const s=e?.partial_errors;return Array.isArray(s)?s.filter(a=>typeof a=="string"&&a.length>0):[]}function T(e){return`activity-${e.toLowerCase().replace(/[^a-z0-9]+/g,"-")}`}export{Ne as ActivityPage}; diff --git a/internal/api/dashboardspa/dist/assets/AgentDetail-B_PEx1iU.js b/internal/api/dashboardspa/dist/assets/AgentDetail-UxV6LhC9.js similarity index 96% rename from internal/api/dashboardspa/dist/assets/AgentDetail-B_PEx1iU.js rename to internal/api/dashboardspa/dist/assets/AgentDetail-UxV6LhC9.js index bb3bfccaf0..ff9c8a2f53 100644 --- a/internal/api/dashboardspa/dist/assets/AgentDetail-B_PEx1iU.js +++ b/internal/api/dashboardspa/dist/assets/AgentDetail-UxV6LhC9.js @@ -1 +1 @@ -import{j as e,B as T,r,p as F,q as ie,t as ce,v as oe,w as de,u as ue,x as U,l as me,y as xe,z as X,f as fe,A as ge,C as he,L as K,s as pe,S as je,G as W}from"./index-zPatq59W.js";import{u as be,R as Ne,B as we}from"./BeadDetailModal-Df6JvpcR.js";import{P as D}from"./PageHeader-DGfr1hUc.js";import{f as O}from"./time-D9v0saHV.js";import{P as ve}from"./constants-vAmcTKRZ.js";import{L as ye,a as Ae}from"./LiveSessionPeek-BpsrYunI.js";import{e as Ce}from"./context-window-Cu9zl36t.js";import{f as Se}from"./agentReads-NY5zZttz.js";import"./format-fte2CeYD.js";import"./Field-CQOLMLGH.js";function ke({beads:n,error:c,loading:l,onSelect:i}){return e.jsxs("section",{className:"mb-12",children:[e.jsxs("header",{className:"flex items-baseline justify-between mb-4",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Beads assigned"}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:l?"·":n.length})]}),c!==null?e.jsx("p",{className:"text-body text-accent",role:"alert",children:c}):l?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading beads."}):n.length===0?e.jsx("p",{className:"text-body text-fg-muted italic",children:"No beads assigned to this agent."}):e.jsx("ul",{className:"space-y-2",children:n.map(a=>e.jsxs("li",{className:"flex items-baseline gap-3 min-w-0",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:a.id}),e.jsx("button",{type:"button",onClick:()=>i(a),className:"text-body text-fg hover:text-accent truncate min-w-0 text-left focus-mark",title:`Open ${a.id}`,children:a.title}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint shrink-0",children:a.status})]},a.id))})]})}function _e({messages:n,loading:c,error:l,now:i}){return e.jsxs("section",{className:"mt-12",children:[e.jsxs("header",{className:"flex items-baseline justify-between mb-4",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Chat thread"}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:c?"·":n.length})]}),e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint mb-4",children:e.jsxs("span",{className:"text-accent",children:["▲ ",ve]})}),c?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading messages."}):l!==null?e.jsx("p",{className:"text-body text-accent",role:"alert",children:l}):n.length===0?e.jsx("p",{className:"text-body text-fg-muted italic",children:"No messages between operator and this agent."}):e.jsx("ul",{className:"space-y-6",children:n.map(a=>e.jsxs("li",{className:"space-y-2 pb-4 border-b border-rule last:border-0",children:[e.jsxs("header",{className:"flex items-baseline justify-between gap-3",children:[e.jsxs("div",{className:"text-label uppercase tracking-wider text-fg-muted truncate",children:[e.jsx("span",{className:"text-fg font-medium",children:a.from}),e.jsx("span",{className:"mx-1.5 text-fg-faint",children:"→"}),e.jsx("span",{children:a.to})]}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:O(a.created_at,i)})]}),a.subject&&e.jsx("p",{className:"text-body font-medium text-fg",children:a.subject}),e.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg overflow-x-auto",children:a.body})]},a.id))})]})}function Ee({alias:n,prompt:c,loading:l,error:i,onRefresh:a}){const j=i?.status===404||i?.kind==="not_found",f=c!==null?`${c.length.toLocaleString()} chars`:l?"loading":i!==null?"—":"·";return e.jsxs("section",{className:"mt-12",children:[e.jsxs("header",{className:"flex items-baseline justify-between mb-4",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Directives"}),e.jsxs("div",{className:"flex items-baseline gap-3",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:f}),e.jsx(T,{size:"sm",tone:"quiet",onClick:a,disabled:l,children:l?"Refreshing":"Refresh"})]})]}),l&&c===null&&i===null?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading directives."}):j?e.jsxs("p",{className:"text-body text-warn",children:["Agent ",e.jsx("code",{className:"text-fg",children:n})," has no entry in city config."]}):i!==null?e.jsxs("p",{className:"text-body text-accent",role:"alert",children:[i.status?`${i.status} `:"",i.message]}):c!==null?e.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg overflow-x-auto max-h-[60vh] overflow-y-auto",children:c}):null]})}function Le({session:n}){return e.jsxs("section",{children:[e.jsx("header",{className:"flex items-baseline justify-between mb-4",children:e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Live peek"})}),e.jsx(ye,{sessionId:n.id,stream:Ae(n),showBadge:!0,showCaption:!0})]})}function Be({session:n,now:c}){const l=Ce(n),i=[{label:"Rig",value:n.rig??"·"},{label:"Pool",value:n.pool??"·"},{label:"Provider",value:n.provider??"·"},{label:"Model",value:n.model??"·"},{label:"Context",value:typeof l=="number"?e.jsxs("span",{className:`tnum ${l>=95?"text-accent":l>=80?"text-warn":"text-fg"}`,children:[l,"%"]}):"·"},{label:"Attached",value:n.attached?"yes":"no"},{label:"Created",value:e.jsx("span",{className:"tnum",children:O(n.created_at,c)})},{label:"Last active",value:e.jsx("span",{className:"tnum",children:O(n.last_active,c)})}];return e.jsx("dl",{className:"grid grid-cols-2 sm:grid-cols-4 gap-x-8 gap-y-5 mb-12",children:i.map(a=>e.jsxs("div",{children:[e.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint mb-1",children:a.label}),e.jsx("dd",{className:"text-body text-fg",children:a.value})]},a.label))})}const Me=2e3,Re=6e4;function Pe({enabled:n,intervalMs:c,load:l,formatError:i,initialBackoffMs:a=Me,maxBackoffMs:j=Re}){const[f,g]=r.useState({status:"idle"}),b=r.useRef(0),h=r.useRef(0);return r.useEffect(()=>{if(!n){b.current=0,h.current=0,g({status:"idle"});return}let N=!1,p=new AbortController;const w=()=>{b.current=0,h.current=0},v=()=>{const u=Math.min(a*2**b.current,j);b.current+=1,h.current=Date.now()+u},y=async()=>{if(Date.now()m.status==="ready"?{...m,refreshing:!0,error:""}:{status:"loading"});try{const m=await l(u.signal);if(N||u.signal.aborted)return;w(),g({status:"ready",data:m,refreshing:!1,error:""})}catch(m){if(N||u.signal.aborted)return;v();const C=i?i(m):F(m);g(E=>E.status==="ready"?{...E,refreshing:!1,error:C}:{status:"failed",error:C})}};y();const A=window.setInterval(()=>{document.hidden||y()},c);return()=>{N=!0,p.abort(),window.clearInterval(A)}},[n,c,l,i,a,j]),f}const Ie=1e4,J=200;function Ue(){const{slug:n=""}=ie(),c=ce(),{viewingAs:l}=oe(),i=de(),[a,j]=r.useState(null),[f,g]=r.useState(null),[b,h]=r.useState(null),[N,p]=r.useState(null),[w,v]=r.useState(null),[y,A]=r.useState(null),u=ue(),[m,C]=r.useState(null),[E,V]=r.useState(!1),[Q,$]=r.useState(null),S=r.useMemo(()=>{try{return decodeURIComponent(n)}catch(t){return U({component:"AgentDetail",operation:"decodeSlug",message:F(t)}),n}},[n]),M=r.useCallback(async()=>{try{const{items:t}=await me();j(t??[]),p(null)}catch(t){p(t instanceof Error?t.message:"sessions failed")}},[]),s=r.useMemo(()=>a===null?null:a.find(t=>t.session_name===S)??a.find(t=>t.alias===S)??a.find(t=>t.id===S)??null,[a,S]),R=r.useMemo(()=>s===null?[]:[s.alias??"",s.session_name,s.id],[s]),P=r.useCallback(async()=>{if(R.length===0){g([]),h(null);return}try{const{items:t}=await xe(R,{includeClosed:!0});g(t),h(null)}catch(t){g([]),h(X(t,"assigned beads unavailable")),U({component:"AgentDetail",operation:"refreshBeads",message:F(t)})}},[R]);r.useEffect(()=>{M()},[M]),r.useEffect(()=>{P()},[P]),fe([W.session,W.bead],()=>{M(),P()});const Y=r.useMemo(()=>{if(s===null||f===null)return[];const t=new Set;return s.alias&&t.add(s.alias),s.session_name&&t.add(s.session_name),t.add(s.id),f.filter(o=>{if(o.assignee!==void 0&&t.has(o.assignee))return!0;const d=o.metadata;return!!(d&&(d.session_id===s.id||d.session_name&&d.session_name===s.session_name))})},[s,f]),q=r.useMemo(()=>{if(s===null)return[];const t=new Set;return s.alias&&t.add(s.alias.toLowerCase()),s.session_name&&t.add(s.session_name.toLowerCase()),t.add(s.id.toLowerCase()),[...t]},[s]),z=r.useMemo(()=>[l.alias.toLowerCase(),i.operatorWireAlias.toLowerCase()],[l.alias,i.operatorWireAlias]),Z=r.useCallback(async()=>{const{items:t}=await ge("all",l.alias,i);return t},[l.alias,i]),x=Pe({enabled:s!==null,intervalMs:Ie,load:Z,formatError:X}),ee=x.status==="loading",se=x.status==="failed"||x.status==="ready"&&x.error.length>0?x.error:null,k=r.useMemo(()=>s===null?null:s.alias??s.template??null,[s]),te=r.useCallback(async()=>{if(k!==null){V(!0),$(null);try{const t=await Se(k);C(t.prompt)}catch(t){const o=he(t,"directives fetch failed"),d={message:o.message};o.status!==void 0&&(d.status=o.status),o.kind!==void 0&&(d.kind=o.kind),$(d),C(null)}finally{V(!1)}}},[k]),I=be(s?.id??null),ae=r.useMemo(()=>{const t=x.status==="ready"?x.data:[],o=new Set(q),d=new Set(z),_=t.filter(L=>{const B=(L.from??"").toLowerCase(),H=(L.to??"").toLowerCase();return!!(d.has(B)&&o.has(H)||o.has(B)&&d.has(H))});return _.sort((L,B)=>L.created_at.localeCompare(B.created_at)),_.length>J?_.slice(_.length-J):_},[x,q,z]);if(a===null)return e.jsx("section",{children:e.jsx(D,{title:"Agent",synopsis:"Loading session list."})});if(s===null)return e.jsxs("section",{children:[e.jsx(D,{title:"Agent",synopsis:e.jsxs(e.Fragment,{children:["No session matches ",e.jsx("code",{className:"text-fg",children:S}),"."]}),meta:e.jsx(T,{size:"sm",tone:"quiet",onClick:()=>c("/agents"),children:"← Agents"})}),e.jsxs("p",{className:"text-body text-fg-muted max-w-prose",children:["The slug doesn't match any current session's session_name, alias, or id. Sessions are listed at"," ",e.jsx(K,{to:"/agents",className:"text-accent hover:underline",children:"/agents"}),"."]})]});const ne=s.alias??s.title??s.id,re=pe(s.state),G=t=>{v(null),A(t)},le=()=>{v(null),A(null)};return e.jsxs("section",{children:[e.jsx(D,{title:ne,synopsis:e.jsxs("span",{className:"flex flex-wrap items-baseline gap-x-3 gap-y-1",children:[e.jsx(je,{tone:re,label:s.state,...s.attached?{trailing:"att"}:{},...s.reason?{title:`reason: ${s.reason}`}:{}}),e.jsx("span",{className:"text-fg-faint",children:"·"}),e.jsx("code",{className:"text-fg-muted",children:s.template??"—"}),s.session_name&&s.session_name!==s.alias&&e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"text-fg-faint",children:"·"}),e.jsx("span",{className:"text-fg-faint",children:s.session_name})]}),e.jsx("span",{className:"text-fg-faint",children:"·"}),e.jsxs("span",{className:"text-fg-faint",children:["id ",e.jsx("code",{className:"text-fg-muted",children:s.id})]})]}),meta:e.jsx(K,{to:"/agents",children:e.jsx(T,{size:"sm",tone:"quiet",children:"← Agents"})})}),N&&e.jsx("p",{className:"text-body text-accent mb-6",role:"alert",children:N}),e.jsx(Be,{session:s,now:u}),e.jsx(ke,{beads:Y,error:b,loading:f===null,onSelect:t=>{A(null),v(t)}}),e.jsx(Ne,{view:I.view,loading:I.loading,error:I.error,now:u,onOpenBead:G}),e.jsx(Le,{session:s}),k!==null&&e.jsx(Ee,{alias:k,prompt:m,loading:E,error:Q,onRefresh:()=>{te()}}),e.jsx(_e,{messages:ae,loading:ee,error:se,now:u}),e.jsx(we,{open:w!==null||y!==null,onClose:le,beadId:w?.id??y,initialBead:w,onOpenBead:G})]})}export{Ue as AgentDetailPage}; +import{j as e,B as T,r,p as F,q as ie,t as ce,v as oe,w as de,u as ue,x as U,l as me,y as xe,z as X,f as fe,A as ge,C as he,L as K,s as pe,S as je,G as W}from"./index-QWRimsO3.js";import{u as be,R as Ne,B as we}from"./BeadDetailModal-BtQMJz2-.js";import{P as D}from"./PageHeader-CxbYmkHZ.js";import{f as O}from"./time-D9v0saHV.js";import{P as ve}from"./constants-CVFL5iaz.js";import{L as ye,a as Ae}from"./LiveSessionPeek-m6YywWBh.js";import{e as Ce}from"./context-window-Cu9zl36t.js";import{f as Se}from"./agentReads-BA8TH08X.js";import"./format-fte2CeYD.js";import"./Field-pp_wh5a7.js";function ke({beads:n,error:c,loading:l,onSelect:i}){return e.jsxs("section",{className:"mb-12",children:[e.jsxs("header",{className:"flex items-baseline justify-between mb-4",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Beads assigned"}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:l?"·":n.length})]}),c!==null?e.jsx("p",{className:"text-body text-accent",role:"alert",children:c}):l?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading beads."}):n.length===0?e.jsx("p",{className:"text-body text-fg-muted italic",children:"No beads assigned to this agent."}):e.jsx("ul",{className:"space-y-2",children:n.map(a=>e.jsxs("li",{className:"flex items-baseline gap-3 min-w-0",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:a.id}),e.jsx("button",{type:"button",onClick:()=>i(a),className:"text-body text-fg hover:text-accent truncate min-w-0 text-left focus-mark",title:`Open ${a.id}`,children:a.title}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint shrink-0",children:a.status})]},a.id))})]})}function _e({messages:n,loading:c,error:l,now:i}){return e.jsxs("section",{className:"mt-12",children:[e.jsxs("header",{className:"flex items-baseline justify-between mb-4",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Chat thread"}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:c?"·":n.length})]}),e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint mb-4",children:e.jsxs("span",{className:"text-accent",children:["▲ ",ve]})}),c?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading messages."}):l!==null?e.jsx("p",{className:"text-body text-accent",role:"alert",children:l}):n.length===0?e.jsx("p",{className:"text-body text-fg-muted italic",children:"No messages between operator and this agent."}):e.jsx("ul",{className:"space-y-6",children:n.map(a=>e.jsxs("li",{className:"space-y-2 pb-4 border-b border-rule last:border-0",children:[e.jsxs("header",{className:"flex items-baseline justify-between gap-3",children:[e.jsxs("div",{className:"text-label uppercase tracking-wider text-fg-muted truncate",children:[e.jsx("span",{className:"text-fg font-medium",children:a.from}),e.jsx("span",{className:"mx-1.5 text-fg-faint",children:"→"}),e.jsx("span",{children:a.to})]}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:O(a.created_at,i)})]}),a.subject&&e.jsx("p",{className:"text-body font-medium text-fg",children:a.subject}),e.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg overflow-x-auto",children:a.body})]},a.id))})]})}function Ee({alias:n,prompt:c,loading:l,error:i,onRefresh:a}){const j=i?.status===404||i?.kind==="not_found",f=c!==null?`${c.length.toLocaleString()} chars`:l?"loading":i!==null?"—":"·";return e.jsxs("section",{className:"mt-12",children:[e.jsxs("header",{className:"flex items-baseline justify-between mb-4",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Directives"}),e.jsxs("div",{className:"flex items-baseline gap-3",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:f}),e.jsx(T,{size:"sm",tone:"quiet",onClick:a,disabled:l,children:l?"Refreshing":"Refresh"})]})]}),l&&c===null&&i===null?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading directives."}):j?e.jsxs("p",{className:"text-body text-warn",children:["Agent ",e.jsx("code",{className:"text-fg",children:n})," has no entry in city config."]}):i!==null?e.jsxs("p",{className:"text-body text-accent",role:"alert",children:[i.status?`${i.status} `:"",i.message]}):c!==null?e.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg overflow-x-auto max-h-[60vh] overflow-y-auto",children:c}):null]})}function Le({session:n}){return e.jsxs("section",{children:[e.jsx("header",{className:"flex items-baseline justify-between mb-4",children:e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Live peek"})}),e.jsx(ye,{sessionId:n.id,stream:Ae(n),showBadge:!0,showCaption:!0})]})}function Be({session:n,now:c}){const l=Ce(n),i=[{label:"Rig",value:n.rig??"·"},{label:"Pool",value:n.pool??"·"},{label:"Provider",value:n.provider??"·"},{label:"Model",value:n.model??"·"},{label:"Context",value:typeof l=="number"?e.jsxs("span",{className:`tnum ${l>=95?"text-accent":l>=80?"text-warn":"text-fg"}`,children:[l,"%"]}):"·"},{label:"Attached",value:n.attached?"yes":"no"},{label:"Created",value:e.jsx("span",{className:"tnum",children:O(n.created_at,c)})},{label:"Last active",value:e.jsx("span",{className:"tnum",children:O(n.last_active,c)})}];return e.jsx("dl",{className:"grid grid-cols-2 sm:grid-cols-4 gap-x-8 gap-y-5 mb-12",children:i.map(a=>e.jsxs("div",{children:[e.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint mb-1",children:a.label}),e.jsx("dd",{className:"text-body text-fg",children:a.value})]},a.label))})}const Me=2e3,Re=6e4;function Pe({enabled:n,intervalMs:c,load:l,formatError:i,initialBackoffMs:a=Me,maxBackoffMs:j=Re}){const[f,g]=r.useState({status:"idle"}),b=r.useRef(0),h=r.useRef(0);return r.useEffect(()=>{if(!n){b.current=0,h.current=0,g({status:"idle"});return}let N=!1,p=new AbortController;const w=()=>{b.current=0,h.current=0},v=()=>{const u=Math.min(a*2**b.current,j);b.current+=1,h.current=Date.now()+u},y=async()=>{if(Date.now()m.status==="ready"?{...m,refreshing:!0,error:""}:{status:"loading"});try{const m=await l(u.signal);if(N||u.signal.aborted)return;w(),g({status:"ready",data:m,refreshing:!1,error:""})}catch(m){if(N||u.signal.aborted)return;v();const C=i?i(m):F(m);g(E=>E.status==="ready"?{...E,refreshing:!1,error:C}:{status:"failed",error:C})}};y();const A=window.setInterval(()=>{document.hidden||y()},c);return()=>{N=!0,p.abort(),window.clearInterval(A)}},[n,c,l,i,a,j]),f}const Ie=1e4,J=200;function Ue(){const{slug:n=""}=ie(),c=ce(),{viewingAs:l}=oe(),i=de(),[a,j]=r.useState(null),[f,g]=r.useState(null),[b,h]=r.useState(null),[N,p]=r.useState(null),[w,v]=r.useState(null),[y,A]=r.useState(null),u=ue(),[m,C]=r.useState(null),[E,V]=r.useState(!1),[Q,$]=r.useState(null),S=r.useMemo(()=>{try{return decodeURIComponent(n)}catch(t){return U({component:"AgentDetail",operation:"decodeSlug",message:F(t)}),n}},[n]),M=r.useCallback(async()=>{try{const{items:t}=await me();j(t??[]),p(null)}catch(t){p(t instanceof Error?t.message:"sessions failed")}},[]),s=r.useMemo(()=>a===null?null:a.find(t=>t.session_name===S)??a.find(t=>t.alias===S)??a.find(t=>t.id===S)??null,[a,S]),R=r.useMemo(()=>s===null?[]:[s.alias??"",s.session_name,s.id],[s]),P=r.useCallback(async()=>{if(R.length===0){g([]),h(null);return}try{const{items:t}=await xe(R,{includeClosed:!0});g(t),h(null)}catch(t){g([]),h(X(t,"assigned beads unavailable")),U({component:"AgentDetail",operation:"refreshBeads",message:F(t)})}},[R]);r.useEffect(()=>{M()},[M]),r.useEffect(()=>{P()},[P]),fe([W.session,W.bead],()=>{M(),P()});const Y=r.useMemo(()=>{if(s===null||f===null)return[];const t=new Set;return s.alias&&t.add(s.alias),s.session_name&&t.add(s.session_name),t.add(s.id),f.filter(o=>{if(o.assignee!==void 0&&t.has(o.assignee))return!0;const d=o.metadata;return!!(d&&(d.session_id===s.id||d.session_name&&d.session_name===s.session_name))})},[s,f]),q=r.useMemo(()=>{if(s===null)return[];const t=new Set;return s.alias&&t.add(s.alias.toLowerCase()),s.session_name&&t.add(s.session_name.toLowerCase()),t.add(s.id.toLowerCase()),[...t]},[s]),z=r.useMemo(()=>[l.alias.toLowerCase(),i.operatorWireAlias.toLowerCase()],[l.alias,i.operatorWireAlias]),Z=r.useCallback(async()=>{const{items:t}=await ge("all",l.alias,i);return t},[l.alias,i]),x=Pe({enabled:s!==null,intervalMs:Ie,load:Z,formatError:X}),ee=x.status==="loading",se=x.status==="failed"||x.status==="ready"&&x.error.length>0?x.error:null,k=r.useMemo(()=>s===null?null:s.alias??s.template??null,[s]),te=r.useCallback(async()=>{if(k!==null){V(!0),$(null);try{const t=await Se(k);C(t.prompt)}catch(t){const o=he(t,"directives fetch failed"),d={message:o.message};o.status!==void 0&&(d.status=o.status),o.kind!==void 0&&(d.kind=o.kind),$(d),C(null)}finally{V(!1)}}},[k]),I=be(s?.id??null),ae=r.useMemo(()=>{const t=x.status==="ready"?x.data:[],o=new Set(q),d=new Set(z),_=t.filter(L=>{const B=(L.from??"").toLowerCase(),H=(L.to??"").toLowerCase();return!!(d.has(B)&&o.has(H)||o.has(B)&&d.has(H))});return _.sort((L,B)=>L.created_at.localeCompare(B.created_at)),_.length>J?_.slice(_.length-J):_},[x,q,z]);if(a===null)return e.jsx("section",{children:e.jsx(D,{title:"Agent",synopsis:"Loading session list."})});if(s===null)return e.jsxs("section",{children:[e.jsx(D,{title:"Agent",synopsis:e.jsxs(e.Fragment,{children:["No session matches ",e.jsx("code",{className:"text-fg",children:S}),"."]}),meta:e.jsx(T,{size:"sm",tone:"quiet",onClick:()=>c("/agents"),children:"← Agents"})}),e.jsxs("p",{className:"text-body text-fg-muted max-w-prose",children:["The slug doesn't match any current session's session_name, alias, or id. Sessions are listed at"," ",e.jsx(K,{to:"/agents",className:"text-accent hover:underline",children:"/agents"}),"."]})]});const ne=s.alias??s.title??s.id,re=pe(s.state),G=t=>{v(null),A(t)},le=()=>{v(null),A(null)};return e.jsxs("section",{children:[e.jsx(D,{title:ne,synopsis:e.jsxs("span",{className:"flex flex-wrap items-baseline gap-x-3 gap-y-1",children:[e.jsx(je,{tone:re,label:s.state,...s.attached?{trailing:"att"}:{},...s.reason?{title:`reason: ${s.reason}`}:{}}),e.jsx("span",{className:"text-fg-faint",children:"·"}),e.jsx("code",{className:"text-fg-muted",children:s.template??"—"}),s.session_name&&s.session_name!==s.alias&&e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"text-fg-faint",children:"·"}),e.jsx("span",{className:"text-fg-faint",children:s.session_name})]}),e.jsx("span",{className:"text-fg-faint",children:"·"}),e.jsxs("span",{className:"text-fg-faint",children:["id ",e.jsx("code",{className:"text-fg-muted",children:s.id})]})]}),meta:e.jsx(K,{to:"/agents",children:e.jsx(T,{size:"sm",tone:"quiet",children:"← Agents"})})}),N&&e.jsx("p",{className:"text-body text-accent mb-6",role:"alert",children:N}),e.jsx(Be,{session:s,now:u}),e.jsx(ke,{beads:Y,error:b,loading:f===null,onSelect:t=>{A(null),v(t)}}),e.jsx(Ne,{view:I.view,loading:I.loading,error:I.error,now:u,onOpenBead:G}),e.jsx(Le,{session:s}),k!==null&&e.jsx(Ee,{alias:k,prompt:m,loading:E,error:Q,onRefresh:()=>{te()}}),e.jsx(_e,{messages:ae,loading:ee,error:se,now:u}),e.jsx(we,{open:w!==null||y!==null,onClose:le,beadId:w?.id??y,initialBead:w,onOpenBead:G})]})}export{Ue as AgentDetailPage}; diff --git a/internal/api/dashboardspa/dist/assets/Agents-40RuA321.js b/internal/api/dashboardspa/dist/assets/Agents-CEUrRSz0.js similarity index 97% rename from internal/api/dashboardspa/dist/assets/Agents-40RuA321.js rename to internal/api/dashboardspa/dist/assets/Agents-CEUrRSz0.js index 4f10604c78..b96d7360ea 100644 --- a/internal/api/dashboardspa/dist/assets/Agents-40RuA321.js +++ b/internal/api/dashboardspa/dist/assets/Agents-CEUrRSz0.js @@ -1,2 +1,2 @@ -import{r as d,s as T,j as e,L as I,u as te,S as $,B as y,a as fe,b as R,l as xe,c as he,d as be,e as ve,f as je,g as Ne,h as ye,R as ke,i as H,k as we,G as K,m as Ce,n as Se,o as Ae}from"./index-zPatq59W.js";import{e as X}from"./context-window-Cu9zl36t.js";import{r as J,a as Re}from"./routeHighlight-B30gQO2o.js";import{i as $e,c as O,a as Ie,s as _e,b as se,d as E,e as Me,L as Le}from"./projectOf-nApq7eyo.js";import{M as ne}from"./constants-vAmcTKRZ.js";import{P as Pe}from"./PageHeader-DGfr1hUc.js";import{S as Oe,P as Ee}from"./SseIndicator-BpC5bgiy.js";import{f as ae}from"./time-D9v0saHV.js";import{L as ie,i as Q}from"./LiveSessionPeek-BpsrYunI.js";import{T as Te}from"./Table-DRIQbbRJ.js";import{l as Be}from"./agentReads-NY5zZttz.js";import"./format-fte2CeYD.js";function qe(s){const n=s.indexOf("-");if(n<=0)return!1;const o=s.slice(0,n),l=s.slice(n+1);return!l||!/^[a-z0-9]+$/.test(l)||!(o==="gc"||o==="td"||o==="th"||/^[a-z]{4}$/.test(o))?!1:/[0-9]/.test(l)}function ze(s){const n=s.trim();if(qe(n))return{role:n,sessionId:n};for(let o=n.length-1;o>=0;o--){const l=n.charAt(o);if(l!=="-"&&l!=="_"&&l!=="/")continue;const c=n.slice(o+1);if(c&&/^(?:gc|td|th|[a-z]{4})-[a-z0-9]{1,32}$/.test(c))return{role:n.slice(0,o),sessionId:c}}return{role:n}}const We="in_progress";function De(s){return Ie(_e(s).label)}function Fe(s){const n=O(s.template??"");return n.length>0?n:O(s.session_name??s.id)}function Z(s){const n=s.session.last_active?Date.parse(s.session.last_active):NaN;return Number.isFinite(n)?n:0}function Ue(s,n){const o=new Map;for(const r of n){if(r.status!==We)continue;const i=r.assignee?.trim();if(!i)continue;const{sessionId:m}=ze(i);m&&!o.has(m)&&o.set(m,r)}const l=[];for(const r of s){if(!$e(r))continue;const i=o.get(r.id);l.push({session:r,rig:De(r),worker:Fe(r),...i?{bead:i}:{}})}l.sort((r,i)=>Z(i)-Z(r));const c=new Map;for(const r of l)c.set(r.rig,(c.get(r.rig)??0)+1);const u=Array.from(c,([r,i])=>({rig:r,count:i})).sort((r,i)=>i.count-r.count||r.rig.localeCompare(i.rig));return{workers:l,byRig:u,total:l.length}}function Ve(s){if(s.total===0)return"No workers active right now.";const n=s.total===1?"worker":"workers",o=s.byRig.map(l=>`${l.rig} (${l.count})`).join(", ");return`${s.total} ${n} active across ${o}.`}function Ye({worker:s,accent:n,onPeek:o}){const l=te(),{session:c,rig:u,bead:r}=s,i=n?T(c.state):"neutral";return e.jsx("li",{className:"px-2 py-2 -mx-2 rounded-sm transition-colors duration-150 ease-out-quart hover:bg-surface-tint/60",children:e.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[e.jsxs("div",{className:"min-w-0 text-body text-fg",children:[e.jsxs("button",{type:"button",onClick:()=>o(c.id),className:"group text-left cursor-pointer focus-mark",title:`Open ${u} · ${s.worker} transcript`,children:[e.jsx("span",{className:"font-medium group-hover:text-accent",children:u}),e.jsxs("span",{className:"text-fg-faint","aria-hidden":"true",children:[" ","·"," "]}),e.jsx("span",{className:"text-fg-muted group-hover:text-accent",children:s.worker})]}),r&&e.jsxs(I,{to:`/beads?bead=${encodeURIComponent(r.id)}`,className:"hover:text-accent focus-mark",title:`Open ${r.id}`,children:[e.jsxs("span",{className:"text-fg-faint","aria-hidden":"true",children:[" ","→"," "]}),e.jsx("span",{className:"tnum text-fg-muted",children:r.id}),e.jsxs("span",{className:"text-fg-muted",children:[": ",r.title]})]})]}),e.jsxs("div",{className:"flex items-baseline gap-3 shrink-0",children:[e.jsx($,{tone:i,label:c.state}),e.jsx("span",{className:"tnum text-fg-muted w-10 text-right",children:ae(c.last_active,l)}),e.jsx(y,{size:"sm",tone:"quiet",onClick:()=>o(c.id),children:"Peek"})]})]})})}function Ge(s){return s.running===!0||s.state==="active"||s.state==="running"}function He({beads:s,sessions:n,sessionsLoading:o,sessionsError:l}){const c=d.useMemo(()=>Ue(n,s),[n,s]),u=d.useMemo(()=>Ve(c),[c]),[r,i]=d.useState(null),m=d.useMemo(()=>r?c.workers.find(f=>f.session.id===r)??null:null,[c.workers,r]),k=d.useMemo(()=>c.workers.findIndex(f=>T(f.session.state)==="stuck"),[c.workers]),p=n.length===0,w=l!==null&&p,N=o&&p,_=w||N?"—":c.total;return e.jsxs("section",{className:"mb-10","aria-label":"Workers active",children:[e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-4",children:[e.jsx("h2",{className:"text-headline text-fg",children:"Workers active"}),e.jsx("span",{className:"text-label tnum text-fg-muted",children:_})]}),w?e.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"Worker status unavailable."}):N?e.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"Checking worker status…"}):c.total===0?e.jsx("p",{className:"text-body text-fg-muted",children:"No workers active right now."}):e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"text-body text-fg-muted mb-4",children:u}),e.jsx("ul",{className:"space-y-1",children:c.workers.map((f,C)=>e.jsx(Ye,{worker:f,accent:C===k,onPeek:i},f.session.id))})]}),e.jsx(ne,{open:m!==null,onClose:()=>i(null),title:m?`${m.rig} · ${m.worker}`:"Transcript",caption:m?.bead?e.jsxs(I,{to:`/beads?bead=${encodeURIComponent(m.bead.id)}`,className:"text-fg-muted hover:text-accent focus-mark",title:`Open ${m.bead.id}`,children:[e.jsx("span",{className:"tnum",children:m.bead.id}),e.jsxs("span",{children:[": ",m.bead.title]})]}):"Live transcript from the supervisor's session stream.",widthClass:"max-w-5xl",children:e.jsx(ie,{sessionId:r,stream:m?Ge(m.session):!1,showBadge:!0,showCaption:!0})})]})}function ee(s){return s.session?.name??s.name}function Ke(s){return!s.suspended&&(s.state==="active"||s.state==="running"||s.running===!0)}function Xe(s,n){return Ke(s)||n==="attention"}function P(s){const n=O(s.name);return se(s)?n:`${E(s).label} · ${n}`}const Je=s=>[s.name,s.display_name,s.pool,s.rig,s.provider,s.model].filter(n=>typeof n=="string"&&n.length>0);function ft(){const s=fe(),{data:n,loading:o,error:l,refresh:c}=R("agents",Be),u=R("sessions",xe),r=R("beads:in-flight",()=>he()),i=d.useMemo(()=>n?.items??[],[n]),m=d.useMemo(()=>(u.data?.items??[]).map(t=>t.id).sort(),[u.data]),k=d.useMemo(()=>i.map(t=>t.name).sort(),[i]),p=R(`agent-pending:${k.join(",")}:${m.join(",")}`,()=>be(i,u.data?.items??[])),w=d.useMemo(()=>{const t=new Map;for(const a of u.data?.items??[])a.session_name&&t.set(a.session_name,a.id);return t},[u.data]),N=d.useMemo(()=>{const t=new Map;for(const a of p.data??[])t.set(a.agentName,a);return t},[p.data]),_=d.useMemo(()=>{const t=(p.data??[]).map(g=>({agentName:g.agentName,...g.pending.prompt===void 0?{}:{prompt:g.pending.prompt}})),a=new Map(i.map(g=>[g.name,g]));return ve(i,t).flatMap(g=>{const b=a.get(g.name);return b===void 0?[]:[{need:g,label:P(b),slug:ee(b)}]})},[i,p.data]),f=te(),[C,oe]=d.useState(!0),[M,re]=d.useState(""),[v,B]=d.useState(""),[S,q]=d.useState(null),[z,W]=d.useState(null),[D,F]=d.useState(null),[j,U]=d.useState(null),x=d.useMemo(()=>S===null?null:i.find(t=>t.name===S)??null,[i,S]),V=d.useMemo(()=>{const t=x?.session?.name;return t?w.get(t)??null:null},[x,w]),le=je([K.session,K.bead,"agent."],()=>{c(),r.refresh(),u.refresh()}),ce=d.useMemo(()=>st(i),[i]),h=Ne(),L=d.useCallback(async(t,a)=>{if(!h){U({sessionId:t.sessionId,action:a}),W(null),F(null);try{await ye(t.sessionId,{action:a,request_id:t.pending.request_id}),W(`responded to ${t.agentName}`),await p.refresh()}catch(g){F(g instanceof Error?g.message:"response failed")}finally{U(null)}}},[p,h]),A=d.useMemo(()=>Array.from(new Set(i.filter(t=>!se(t)).map(t=>E(t).label))).sort((t,a)=>t.localeCompare(a)),[i]);d.useEffect(()=>{v!==""&&!A.includes(v)&&B("")},[A,v]);const Y=d.useMemo(()=>{const t=M.trim().toLowerCase();return i.filter(a=>{if(v!==""&&E(a).label!==v)return!1;const g=J(s,"agents",a.name);return C&&!Xe(a,g)?!1:t.length===0?!0:Je(a).some(b=>b.toLowerCase().includes(t))})},[i,v,C,M,s]),de=d.useMemo(()=>t=>Re(J(s,"agents",t.name)),[s]),G=l!==null&&i.length===0,ue=G?"Agent roster unavailable.":i.length===0?"No agents configured.":"No agents match the current search or filter.",me=d.useMemo(()=>[{key:"name",label:"Agent",sortable:!0,sortValue:t=>P(t),render:t=>{const a=Me(t),g=t.display_name&&t.display_name!==t.name?t.display_name:t.provider??t.model??"",b=!t.session,ge=b?`${t.name} — configured but not running; detail will show no live session`:`Open drilldown for ${t.name}`,pe=b?"text-fg-muted":"text-fg";return e.jsxs("div",{className:"min-w-0",children:[e.jsx(I,{to:`/agents/${encodeURIComponent(ee(t))}`,className:`block ${pe} truncate hover:text-accent focus-mark ${a?"font-normal italic":"font-medium"}`,title:ge,children:P(t)}),g&&e.jsx("div",{className:"text-label uppercase tracking-wider text-fg-faint mt-1 truncate",children:g})]})}},{key:"state",label:"State",sortable:!0,sortValue:t=>t.state,render:t=>e.jsx($,{tone:T(t.state),label:t.state,...t.session?.attached?{trailing:"att"}:{},...t.unavailable_reason?{title:`unavailable: ${t.unavailable_reason}`}:{}}),className:"w-32"},{key:"activity",label:"Activity",sortable:!0,sortValue:t=>t.activity??"",render:t=>{const a=N.get(t.name);return a!==void 0?e.jsxs("div",{className:"min-w-0",children:[e.jsx($,{tone:"stuck",label:"needs you"}),e.jsx("p",{className:"mt-1 truncate text-fg-muted",title:a.pending.prompt,children:a.pending.prompt??a.pending.kind})]}):e.jsx("span",{className:"text-fg-muted",children:t.activity??(t.running?"running":"·")})},className:"w-28"},{key:"context",label:"Context",sortable:!0,sortValue:t=>X(t)??-1,align:"right",render:t=>{const a=X(t);if(typeof a!="number")return e.jsx("span",{className:"text-fg-faint",children:"·"});const g=typeof t.context_pct=="number"&&t.context_pct!==a?`gc reports ${t.context_pct}% against ${t.context_window??"?"}-token window; scaled to model's true window`:void 0;return e.jsxs("span",{title:g,className:`tnum ${a>=95?"text-accent font-medium":a>=80?"text-warn font-medium":"text-fg-muted"}`,children:[a,"%"]})},className:"w-24"},{key:"last_active",label:"Last active",sortable:!0,sortValue:t=>t.session?.last_activity??"",render:t=>{const a=t.session?.last_activity;return a?e.jsx("span",{className:"tnum text-fg-muted",children:ae(a,f)}):e.jsx("span",{className:"text-fg-faint tnum",children:"·"})},className:"w-32"},{key:"actions",label:"",render:t=>{if(!t.session)return null;const a=N.get(t.name);return e.jsxs("div",{className:"flex justify-end gap-2",children:[a!==void 0&&e.jsxs(e.Fragment,{children:[h&&e.jsx(ke,{}),e.jsx(y,{size:"sm",tone:"quiet",title:h?H:void 0,disabled:h||j?.sessionId===a.sessionId,onClick:()=>{L(a,"approve")},children:j?.sessionId===a.sessionId&&j.action==="approve"?"Approving":"Approve"}),e.jsx(y,{size:"sm",tone:"quiet",title:h?H:void 0,disabled:h||j?.sessionId===a.sessionId,onClick:()=>{L(a,"deny")},children:j?.sessionId===a.sessionId&&j.action==="deny"?"Denying":"Deny"}),e.jsx(Ze,{command:we(t.name)})]}),e.jsx(y,{size:"sm",tone:"quiet",onClick:()=>q(t.name),children:"Peek"})]})},align:"right",className:"w-80"}],[L,f,N,h,j]);return e.jsxs("section",{children:[e.jsx(Pe,{title:"Agents",synopsis:G?"Agent roster unavailable.":ce,meta:e.jsxs(e.Fragment,{children:[e.jsx(Oe,{state:le}),l&&e.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:l}),e.jsx(Ee,{show:n?.partial===!0,label:"roster partial",title:n?.partial_errors?.join(` +import{r as d,s as T,j as e,L as I,u as te,S as $,B as y,a as fe,b as R,l as xe,c as he,d as be,e as ve,f as je,g as Ne,h as ye,R as ke,i as H,k as we,G as K,m as Ce,n as Se,o as Ae}from"./index-QWRimsO3.js";import{e as X}from"./context-window-Cu9zl36t.js";import{r as J,a as Re}from"./routeHighlight-B30gQO2o.js";import{i as $e,c as O,a as Ie,s as _e,b as se,d as E,e as Me,L as Le}from"./projectOf-CAOn7SI-.js";import{M as ne}from"./constants-CVFL5iaz.js";import{P as Pe}from"./PageHeader-CxbYmkHZ.js";import{S as Oe,P as Ee}from"./SseIndicator-DDbpxu-X.js";import{f as ae}from"./time-D9v0saHV.js";import{L as ie,i as Q}from"./LiveSessionPeek-m6YywWBh.js";import{T as Te}from"./Table-C94QdmsL.js";import{l as Be}from"./agentReads-BA8TH08X.js";import"./format-fte2CeYD.js";function qe(s){const n=s.indexOf("-");if(n<=0)return!1;const o=s.slice(0,n),l=s.slice(n+1);return!l||!/^[a-z0-9]+$/.test(l)||!(o==="gc"||o==="td"||o==="th"||/^[a-z]{4}$/.test(o))?!1:/[0-9]/.test(l)}function ze(s){const n=s.trim();if(qe(n))return{role:n,sessionId:n};for(let o=n.length-1;o>=0;o--){const l=n.charAt(o);if(l!=="-"&&l!=="_"&&l!=="/")continue;const c=n.slice(o+1);if(c&&/^(?:gc|td|th|[a-z]{4})-[a-z0-9]{1,32}$/.test(c))return{role:n.slice(0,o),sessionId:c}}return{role:n}}const We="in_progress";function De(s){return Ie(_e(s).label)}function Fe(s){const n=O(s.template??"");return n.length>0?n:O(s.session_name??s.id)}function Z(s){const n=s.session.last_active?Date.parse(s.session.last_active):NaN;return Number.isFinite(n)?n:0}function Ue(s,n){const o=new Map;for(const r of n){if(r.status!==We)continue;const i=r.assignee?.trim();if(!i)continue;const{sessionId:m}=ze(i);m&&!o.has(m)&&o.set(m,r)}const l=[];for(const r of s){if(!$e(r))continue;const i=o.get(r.id);l.push({session:r,rig:De(r),worker:Fe(r),...i?{bead:i}:{}})}l.sort((r,i)=>Z(i)-Z(r));const c=new Map;for(const r of l)c.set(r.rig,(c.get(r.rig)??0)+1);const u=Array.from(c,([r,i])=>({rig:r,count:i})).sort((r,i)=>i.count-r.count||r.rig.localeCompare(i.rig));return{workers:l,byRig:u,total:l.length}}function Ve(s){if(s.total===0)return"No workers active right now.";const n=s.total===1?"worker":"workers",o=s.byRig.map(l=>`${l.rig} (${l.count})`).join(", ");return`${s.total} ${n} active across ${o}.`}function Ye({worker:s,accent:n,onPeek:o}){const l=te(),{session:c,rig:u,bead:r}=s,i=n?T(c.state):"neutral";return e.jsx("li",{className:"px-2 py-2 -mx-2 rounded-sm transition-colors duration-150 ease-out-quart hover:bg-surface-tint/60",children:e.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[e.jsxs("div",{className:"min-w-0 text-body text-fg",children:[e.jsxs("button",{type:"button",onClick:()=>o(c.id),className:"group text-left cursor-pointer focus-mark",title:`Open ${u} · ${s.worker} transcript`,children:[e.jsx("span",{className:"font-medium group-hover:text-accent",children:u}),e.jsxs("span",{className:"text-fg-faint","aria-hidden":"true",children:[" ","·"," "]}),e.jsx("span",{className:"text-fg-muted group-hover:text-accent",children:s.worker})]}),r&&e.jsxs(I,{to:`/beads?bead=${encodeURIComponent(r.id)}`,className:"hover:text-accent focus-mark",title:`Open ${r.id}`,children:[e.jsxs("span",{className:"text-fg-faint","aria-hidden":"true",children:[" ","→"," "]}),e.jsx("span",{className:"tnum text-fg-muted",children:r.id}),e.jsxs("span",{className:"text-fg-muted",children:[": ",r.title]})]})]}),e.jsxs("div",{className:"flex items-baseline gap-3 shrink-0",children:[e.jsx($,{tone:i,label:c.state}),e.jsx("span",{className:"tnum text-fg-muted w-10 text-right",children:ae(c.last_active,l)}),e.jsx(y,{size:"sm",tone:"quiet",onClick:()=>o(c.id),children:"Peek"})]})]})})}function Ge(s){return s.running===!0||s.state==="active"||s.state==="running"}function He({beads:s,sessions:n,sessionsLoading:o,sessionsError:l}){const c=d.useMemo(()=>Ue(n,s),[n,s]),u=d.useMemo(()=>Ve(c),[c]),[r,i]=d.useState(null),m=d.useMemo(()=>r?c.workers.find(f=>f.session.id===r)??null:null,[c.workers,r]),k=d.useMemo(()=>c.workers.findIndex(f=>T(f.session.state)==="stuck"),[c.workers]),p=n.length===0,w=l!==null&&p,N=o&&p,_=w||N?"—":c.total;return e.jsxs("section",{className:"mb-10","aria-label":"Workers active",children:[e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-4",children:[e.jsx("h2",{className:"text-headline text-fg",children:"Workers active"}),e.jsx("span",{className:"text-label tnum text-fg-muted",children:_})]}),w?e.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"Worker status unavailable."}):N?e.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"Checking worker status…"}):c.total===0?e.jsx("p",{className:"text-body text-fg-muted",children:"No workers active right now."}):e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"text-body text-fg-muted mb-4",children:u}),e.jsx("ul",{className:"space-y-1",children:c.workers.map((f,C)=>e.jsx(Ye,{worker:f,accent:C===k,onPeek:i},f.session.id))})]}),e.jsx(ne,{open:m!==null,onClose:()=>i(null),title:m?`${m.rig} · ${m.worker}`:"Transcript",caption:m?.bead?e.jsxs(I,{to:`/beads?bead=${encodeURIComponent(m.bead.id)}`,className:"text-fg-muted hover:text-accent focus-mark",title:`Open ${m.bead.id}`,children:[e.jsx("span",{className:"tnum",children:m.bead.id}),e.jsxs("span",{children:[": ",m.bead.title]})]}):"Live transcript from the supervisor's session stream.",widthClass:"max-w-5xl",children:e.jsx(ie,{sessionId:r,stream:m?Ge(m.session):!1,showBadge:!0,showCaption:!0})})]})}function ee(s){return s.session?.name??s.name}function Ke(s){return!s.suspended&&(s.state==="active"||s.state==="running"||s.running===!0)}function Xe(s,n){return Ke(s)||n==="attention"}function P(s){const n=O(s.name);return se(s)?n:`${E(s).label} · ${n}`}const Je=s=>[s.name,s.display_name,s.pool,s.rig,s.provider,s.model].filter(n=>typeof n=="string"&&n.length>0);function ft(){const s=fe(),{data:n,loading:o,error:l,refresh:c}=R("agents",Be),u=R("sessions",xe),r=R("beads:in-flight",()=>he()),i=d.useMemo(()=>n?.items??[],[n]),m=d.useMemo(()=>(u.data?.items??[]).map(t=>t.id).sort(),[u.data]),k=d.useMemo(()=>i.map(t=>t.name).sort(),[i]),p=R(`agent-pending:${k.join(",")}:${m.join(",")}`,()=>be(i,u.data?.items??[])),w=d.useMemo(()=>{const t=new Map;for(const a of u.data?.items??[])a.session_name&&t.set(a.session_name,a.id);return t},[u.data]),N=d.useMemo(()=>{const t=new Map;for(const a of p.data??[])t.set(a.agentName,a);return t},[p.data]),_=d.useMemo(()=>{const t=(p.data??[]).map(g=>({agentName:g.agentName,...g.pending.prompt===void 0?{}:{prompt:g.pending.prompt}})),a=new Map(i.map(g=>[g.name,g]));return ve(i,t).flatMap(g=>{const b=a.get(g.name);return b===void 0?[]:[{need:g,label:P(b),slug:ee(b)}]})},[i,p.data]),f=te(),[C,oe]=d.useState(!0),[M,re]=d.useState(""),[v,B]=d.useState(""),[S,q]=d.useState(null),[z,W]=d.useState(null),[D,F]=d.useState(null),[j,U]=d.useState(null),x=d.useMemo(()=>S===null?null:i.find(t=>t.name===S)??null,[i,S]),V=d.useMemo(()=>{const t=x?.session?.name;return t?w.get(t)??null:null},[x,w]),le=je([K.session,K.bead,"agent."],()=>{c(),r.refresh(),u.refresh()}),ce=d.useMemo(()=>st(i),[i]),h=Ne(),L=d.useCallback(async(t,a)=>{if(!h){U({sessionId:t.sessionId,action:a}),W(null),F(null);try{await ye(t.sessionId,{action:a,request_id:t.pending.request_id}),W(`responded to ${t.agentName}`),await p.refresh()}catch(g){F(g instanceof Error?g.message:"response failed")}finally{U(null)}}},[p,h]),A=d.useMemo(()=>Array.from(new Set(i.filter(t=>!se(t)).map(t=>E(t).label))).sort((t,a)=>t.localeCompare(a)),[i]);d.useEffect(()=>{v!==""&&!A.includes(v)&&B("")},[A,v]);const Y=d.useMemo(()=>{const t=M.trim().toLowerCase();return i.filter(a=>{if(v!==""&&E(a).label!==v)return!1;const g=J(s,"agents",a.name);return C&&!Xe(a,g)?!1:t.length===0?!0:Je(a).some(b=>b.toLowerCase().includes(t))})},[i,v,C,M,s]),de=d.useMemo(()=>t=>Re(J(s,"agents",t.name)),[s]),G=l!==null&&i.length===0,ue=G?"Agent roster unavailable.":i.length===0?"No agents configured.":"No agents match the current search or filter.",me=d.useMemo(()=>[{key:"name",label:"Agent",sortable:!0,sortValue:t=>P(t),render:t=>{const a=Me(t),g=t.display_name&&t.display_name!==t.name?t.display_name:t.provider??t.model??"",b=!t.session,ge=b?`${t.name} — configured but not running; detail will show no live session`:`Open drilldown for ${t.name}`,pe=b?"text-fg-muted":"text-fg";return e.jsxs("div",{className:"min-w-0",children:[e.jsx(I,{to:`/agents/${encodeURIComponent(ee(t))}`,className:`block ${pe} truncate hover:text-accent focus-mark ${a?"font-normal italic":"font-medium"}`,title:ge,children:P(t)}),g&&e.jsx("div",{className:"text-label uppercase tracking-wider text-fg-faint mt-1 truncate",children:g})]})}},{key:"state",label:"State",sortable:!0,sortValue:t=>t.state,render:t=>e.jsx($,{tone:T(t.state),label:t.state,...t.session?.attached?{trailing:"att"}:{},...t.unavailable_reason?{title:`unavailable: ${t.unavailable_reason}`}:{}}),className:"w-32"},{key:"activity",label:"Activity",sortable:!0,sortValue:t=>t.activity??"",render:t=>{const a=N.get(t.name);return a!==void 0?e.jsxs("div",{className:"min-w-0",children:[e.jsx($,{tone:"stuck",label:"needs you"}),e.jsx("p",{className:"mt-1 truncate text-fg-muted",title:a.pending.prompt,children:a.pending.prompt??a.pending.kind})]}):e.jsx("span",{className:"text-fg-muted",children:t.activity??(t.running?"running":"·")})},className:"w-28"},{key:"context",label:"Context",sortable:!0,sortValue:t=>X(t)??-1,align:"right",render:t=>{const a=X(t);if(typeof a!="number")return e.jsx("span",{className:"text-fg-faint",children:"·"});const g=typeof t.context_pct=="number"&&t.context_pct!==a?`gc reports ${t.context_pct}% against ${t.context_window??"?"}-token window; scaled to model's true window`:void 0;return e.jsxs("span",{title:g,className:`tnum ${a>=95?"text-accent font-medium":a>=80?"text-warn font-medium":"text-fg-muted"}`,children:[a,"%"]})},className:"w-24"},{key:"last_active",label:"Last active",sortable:!0,sortValue:t=>t.session?.last_activity??"",render:t=>{const a=t.session?.last_activity;return a?e.jsx("span",{className:"tnum text-fg-muted",children:ae(a,f)}):e.jsx("span",{className:"text-fg-faint tnum",children:"·"})},className:"w-32"},{key:"actions",label:"",render:t=>{if(!t.session)return null;const a=N.get(t.name);return e.jsxs("div",{className:"flex justify-end gap-2",children:[a!==void 0&&e.jsxs(e.Fragment,{children:[h&&e.jsx(ke,{}),e.jsx(y,{size:"sm",tone:"quiet",title:h?H:void 0,disabled:h||j?.sessionId===a.sessionId,onClick:()=>{L(a,"approve")},children:j?.sessionId===a.sessionId&&j.action==="approve"?"Approving":"Approve"}),e.jsx(y,{size:"sm",tone:"quiet",title:h?H:void 0,disabled:h||j?.sessionId===a.sessionId,onClick:()=>{L(a,"deny")},children:j?.sessionId===a.sessionId&&j.action==="deny"?"Denying":"Deny"}),e.jsx(Ze,{command:we(t.name)})]}),e.jsx(y,{size:"sm",tone:"quiet",onClick:()=>q(t.name),children:"Peek"})]})},align:"right",className:"w-80"}],[L,f,N,h,j]);return e.jsxs("section",{children:[e.jsx(Pe,{title:"Agents",synopsis:G?"Agent roster unavailable.":ce,meta:e.jsxs(e.Fragment,{children:[e.jsx(Oe,{state:le}),l&&e.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:l}),e.jsx(Ee,{show:n?.partial===!0,label:"roster partial",title:n?.partial_errors?.join(` `)??"one or more agent backends unavailable"}),e.jsx(y,{size:"sm",onClick:()=>{c()},disabled:o,children:o?"Refreshing":"Refresh"})]})}),e.jsx(Qe,{rows:_}),e.jsx(He,{beads:r.data?.items??[],sessions:u.data?.items??[],sessionsLoading:u.loading,sessionsError:u.error}),e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-4",children:[e.jsx("h2",{className:"text-headline text-fg",children:"Available agents"}),e.jsx("span",{className:"text-label tnum text-fg-muted",children:i.length})]}),e.jsxs("div",{className:"mb-6 space-y-3",children:[e.jsx(Le,{value:M,onChange:re,placeholder:"Search agents by alias, rig, pool, provider",matchCount:Y.length,totalCount:i.length,ariaLabel:"Search agents"}),e.jsxs("div",{className:"flex items-baseline gap-6",children:[e.jsxs("label",{className:"inline-flex items-baseline gap-2 text-label uppercase tracking-wider text-fg-muted cursor-pointer hover:text-fg transition-colors duration-150 ease-out-quart",children:[e.jsx("input",{type:"checkbox",checked:C,onChange:t=>oe(t.target.checked),style:{accentColor:"oklch(var(--fg-muted))"},className:"translate-y-[2px]"}),e.jsx("span",{children:"running"})]}),A.length>1&&e.jsxs("label",{className:"inline-flex items-baseline gap-2 text-label uppercase tracking-wider text-fg-muted",children:[e.jsx("span",{children:"rig"}),e.jsxs("select",{value:v,onChange:t=>B(t.target.value),"aria-label":"Rig filter",className:"text-label uppercase tracking-wider text-fg-muted bg-transparent border-0 focus-mark cursor-pointer hover:text-fg transition-colors duration-150 ease-out-quart",children:[e.jsx("option",{value:"",children:"all rigs"}),A.map(t=>e.jsx("option",{value:t,children:t},t))]})]})]})]}),z&&e.jsx("div",{className:"mb-4 text-body text-fg-muted",role:"status",children:z}),D&&e.jsx("div",{className:"mb-4 text-body text-accent",role:"alert",children:D}),e.jsx(Te,{rows:Y,columns:me,rowKey:t=>t.name,rowProps:de,empty:ue,initialSort:{key:"last_active",dir:"desc"}}),e.jsx(ne,{open:S!==null,onClose:()=>q(null),title:x?.name??S??"Transcript",caption:x&&x.session&&!V?u.loading?"Resolving session…":`No live session matches "${x.session.name}".`:Q(x)?"Live transcript from the supervisor's session stream.":"Snapshot from the supervisor's transcript API.",widthClass:"max-w-5xl",children:e.jsx(ie,{sessionId:V,stream:Q(x),showBadge:!0,showCaption:!0})})]})}function Qe({rows:s}){return s.length===0?null:e.jsxs("section",{"aria-label":"Agents needing you",className:"mb-10",children:[e.jsxs("h2",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:["Needs you (",s.length,")"]}),e.jsx("ol",{className:"mt-3 divide-y divide-rule",children:s.map(({need:n,label:o,slug:l})=>e.jsxs("li",{className:"py-3",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[e.jsx(I,{to:`/agents/${encodeURIComponent(l)}`,className:"focus-mark block min-w-0 truncate text-title text-fg hover:text-accent",children:o}),e.jsx($,{tone:Se(n.reason),label:Ce(n.reason)})]}),e.jsx("p",{className:"mt-1 text-body text-fg leading-snug",children:n.detail}),e.jsx("p",{className:"mt-0.5 text-body text-fg-muted leading-snug",children:Ae(n.action)})]},n.name))})]})}function Ze({command:s}){const[n,o]=d.useState("idle"),l=n==="copied"?"Copied":n==="failed"?"Copy failed":"Copy attach";return e.jsx(y,{size:"sm",tone:"quiet",title:s,onClick:()=>{et(s,o)},children:l})}async function et(s,n){try{await navigator.clipboard.writeText(s),n("copied")}catch{n("failed")}}function tt(s){if(s.suspended)return"suspended";switch(s.state){case"active":case"running":return"active";case"detached":return"detached";case"rate-limited":case"rate_limited":case"waiting":return"rate-limited";case"failed":case"closed":case"errored":case"stuck":return"stuck";default:return"idle"}}function st(s){if(s.length===0)return"No agents configured.";const n=new Map;for(const k of s){const p=tt(k);n.set(p,(n.get(p)??0)+1)}const o=[],l=n.get("active")??0,c=n.get("idle")??0,u=n.get("detached")??0,r=n.get("rate-limited")??0,i=n.get("stuck")??0,m=n.get("suspended")??0;return l>0&&o.push(`${l} active`),c>0&&o.push(`${c} idle`),u>0&&o.push(`${u} detached`),r>0&&o.push(`${r} rate-limited`),i>0&&o.push(`${i} stuck`),m>0&&o.push(`${m} suspended`),o.join(", ")+"."}export{ft as AgentsPage,P as agentRowLabel,st as buildAgentSynopsis,Ke as isRunningAgent,Xe as isVisibleUnderRunning,T as stateTone}; diff --git a/internal/api/dashboardspa/dist/assets/AmbientHome-Cvjm2Kmk.js b/internal/api/dashboardspa/dist/assets/AmbientHome-Cvjm2Kmk.js deleted file mode 100644 index 645fe3f4d9..0000000000 --- a/internal/api/dashboardspa/dist/assets/AmbientHome-Cvjm2Kmk.js +++ /dev/null @@ -1 +0,0 @@ -import{a as j,j as a,r as c,L as h,D as N,E as S,F as m,u as M,b as p,H as A,I as y,J as R}from"./index-zPatq59W.js";import{P as f}from"./PageHeader-DGfr1hUc.js";const L={agents:"Agents",beads:"Beads",runs:"Runs",mail:"Mail",activity:"Activity",health:"Health"},$={agents:"/agents",beads:"/beads",runs:"/runs",mail:"/mail",activity:"/activity",health:"/health"};function x(e){return L[e]}function C(e){return $[e]}function E(){const e=j();return e.items.length===0?null:a.jsxs("section",{"aria-labelledby":"attention-summary-title",className:"space-y-3",children:[a.jsx("h2",{id:"attention-summary-title",className:"text-headline font-semibold text-fg",children:"Attention"}),a.jsx("ul",{className:"space-y-2",children:e.topItems.map(t=>a.jsxs("li",{className:"text-body text-fg flex items-baseline gap-3",children:[a.jsx(D,{item:t}),a.jsx("span",{className:`text-label uppercase tracking-wider ${_(t.severity)}`,children:x(t.domain)})]},`${t.domain}:${t.id}`))}),e.overflowByDomain.length>0&&a.jsx("p",{className:"text-label uppercase tracking-wider text-fg-muted",children:e.overflowByDomain.map((t,n)=>a.jsxs(c.Fragment,{children:[n>0&&" · ",a.jsxs(h,{to:C(t.domain),className:"hover:text-fg focus-mark",children:[t.total," more in ",x(t.domain)]})]},t.domain))})]})}function D({item:e}){return e.href===void 0?a.jsx("span",{className:"font-medium",children:e.title}):a.jsx(h,{to:e.href,className:"font-medium hover:text-fg focus-mark",children:e.title})}function _(e){switch(e){case"attention":return"text-accent";case"watch":return"text-warn";case"unavailable":return"text-fg-muted"}}function F(e){return e.external.status!=="unavailable"?e.external.label:e.title}function H(e){const t=encodeURIComponent(e.id),n=e.scope.status==="available"?e.scope:null;if(e.health.status==="available"&&e.health.data.stuckNode.status==="available"){const i=new URLSearchParams;return i.set("node",e.health.data.stuckNode.id),n&&(i.set("scope_kind",n.kind),i.set("scope_ref",n.ref)),`/runs/${t}?${i.toString()}`}if(n){const i=new URLSearchParams;return i.set("scope_kind",n.kind),i.set("scope_ref",n.ref),`/runs/${t}?${i.toString()}`}return`/runs/${t}`}function I(e){switch(e){case"needsOperator":return"needs you";case"stalled":return"stalled";default:return e}}function P({rows:e}){return a.jsx("section",{id:"needs-you",children:a.jsx("ul",{className:"mt-2 transition-opacity duration-150 ease-out-quart motion-reduce:transition-none",style:{opacity:e.length===0?0:1},"aria-live":"polite","data-testid":"concern-region",children:e.map(({lane:t,reason:n})=>a.jsxs("li",{className:"text-body text-fg flex items-baseline gap-3",children:[a.jsx(h,{to:H(t),className:"font-medium hover:text-fg focus-mark","data-testid":`concern-row-${t.id}`,children:F(t)}),a.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:I(n)})]},t.id))})})}const g="gascity:home-intro-dismissed",v="FirstRunNote";function T(){const[e,t]=c.useState(()=>N("localStorage",g,v).status==="found");if(e)return null;const n=()=>{t(!0),S("localStorage",g,"1",v)};return a.jsxs("aside",{className:"mt-6 max-w-[70ch]","data-testid":"first-run-note",children:[a.jsx("p",{className:"text-body text-fg-muted",children:"New here? This page is the ambient home for a Gas City workspace: a calm census of the formula runs in flight. Healthy work stays quiet by design; the page speaks up only when a run needs an operator decision. The full record lives in Agents, Beads, Runs, and Mail above."}),a.jsx("button",{type:"button",onClick:n,className:"mt-2 text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out-quart focus-mark",children:"Dismiss"})]})}function O({census:e,waitingCount:t,failingCount:n}){const s=e.unverifiable>0?` (of ${e.knownDenominator} known)`:"",r=n===0?`nothing failing${s}`:`${n} failing${s}`;return a.jsxs("p",{className:"text-title tnum text-fg","data-testid":"phase-census",children:[a.jsxs("span",{children:[e.totalInFlight," in flight"]}),a.jsx("span",{"aria-hidden":"true",className:"mx-2 text-fg-faint",children:"·"}),a.jsxs("span",{children:[t," waiting"]}),a.jsx("span",{"aria-hidden":"true",className:"mx-2 text-fg-faint",children:"·"}),a.jsx("span",{className:n>0?"font-semibold text-fg":"","aria-live":"polite","data-testid":"phase-census-failing",children:r})]})}function B(e){const t=Math.floor(e/6e4);if(t<60)return`${t} min`;const n=Math.floor(t/60);return n<24?`${n}h`:`${Math.floor(n/24)}d`}function q(e){if(e.health.status!=="available")return null;const t=e.health.data.stuckNode;if(t.status!=="available")return null;const n=encodeURIComponent(e.id),i=e.scope.status==="available"?e.scope:null,s=new URLSearchParams;return s.set("node",t.id),i&&(s.set("scope_kind",i.kind),s.set("scope_ref",i.ref)),`/runs/${n}?${s.toString()}`}function U(e){return e.external.status!=="unavailable"?e.external.label:e.title}function G(e){return m(e)?"has been waiting on your decision for":"has waited on a review verdict for"}function K({topConcern:e}){const{lane:t,ageMs:n}=e,i=U(t),s=q(t),r=G(t),o=B(n);return a.jsxs("p",{className:"text-body text-fg max-w-[70ch] leading-relaxed","data-testid":"status-sentence",children:[s===null?a.jsx("span",{"data-testid":"status-sentence-token",children:i}):a.jsx(h,{to:s,className:"text-accent font-semibold focus-mark","data-testid":"status-sentence-token",children:i})," ",r," ",o,"."]})}const V="/favicon-calm.svg",W="/favicon-alert.svg",Y=2;function J(e){const t=document.getElementById("favicon");t instanceof HTMLLinkElement&&(t.href=`${e}?v=${Date.now()}`)}function Q({failing:e,cycleKey:t}){const n=c.useRef("calm"),i=c.useRef(0),s=c.useRef(null);c.useEffect(()=>{if(s.current===t)return;s.current=t;const r=n.current,o=e>0?"alert":"calm";if(o===r){i.current=0;return}i.current+=1,!(i.current=b.stalled?"stalled":e>=b.warning?"warning":"fresh"}function Z(e){const t=M();return c.useMemo(()=>{const n=new Map,i=[];for(const s of e){const r=z(s),o=s.health.status==="available"&&s.health.data.phaseConfidence==="known";if(r===null){n.set(s.id,{tier:"unknown",ageMs:0,isStalled:!1});continue}const l=Math.max(0,t-r);if(!o){n.set(s.id,{tier:"unknown",ageMs:l,isStalled:!1});continue}const d=X(l),u=d==="stalled";n.set(s.id,{tier:d,ageMs:l,isStalled:u}),u&&i.push({id:s.id,ageMs:l})}return i.sort((s,r)=>r.ageMs-s.ageMs),{byLane:n,clientStalledLaneIds:i.map(s=>s.id)}},[e,t])}function ee(e,t){const n=[];for(const s of e){if(s.health.status!=="available"||!(s.health.data.phaseConfidence==="known"))continue;const o=t.byLane.get(s.id)?.ageMs??0;s.health.data.thrashingDetected?n.push({lane:s,ageMs:o,priority:2}):t.byLane.get(s.id)?.isStalled&&n.push({lane:s,ageMs:o,priority:1})}if(n.length===0)return;n.sort((s,r)=>r.priority-s.priority||r.ageMs-s.ageMs);const i=n[0];return{lane:i.lane,ageMs:i.ageMs}}function te(e,t,n){const i=[];for(const s of e){if(s.id===n)continue;if(m(s)){i.push({lane:s,reason:"needsOperator"});continue}if(s.health.status!=="available")continue;const r=s.health.data;r.phaseConfidence==="known"&&(r.thrashingDetected||t.byLane.get(s.id)?.isStalled)&&i.push({lane:s,reason:"stalled"})}return i}function se(e){let t=0;for(const n of e)m(n)&&(t+=1);return t}function ne(e){return e===void 0||e.status==="error"?null:{source:e,summary:e.data}}function ae({fresh:e,cityName:t,cycleKey:n,workInProgress:i}){const{summary:s}=e,r=c.useMemo(()=>[...s.lanes,...s.blockedLanes],[s.lanes,s.blockedLanes]),o=Z(r),l=c.useMemo(()=>ee(r,o),[r,o]),d=c.useMemo(()=>te(r,o,l?.lane.id),[r,o,l]),u=s.census.status!=="available"?0:s.census.data.thrashing+o.clientStalledLaneIds.length;Q({failing:u,cycleKey:n});const w=i.status==="available"?`, ${i.value} in progress`:"",k=t!==null?`${t}, ${s.totalActive} active${w}`:null;return a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:k}),a.jsx(T,{}),s.census.status!=="available"?a.jsxs("p",{className:"mt-6 text-body text-fg-muted max-w-[70ch]",role:"alert","data-testid":"census-unavailable",children:["Census unavailable: ",s.census.error,"."]}):a.jsxs("div",{className:"mt-6 space-y-6",children:[a.jsx(E,{}),a.jsxs("div",{className:"space-y-4",children:[a.jsx(O,{census:s.census.data,waitingCount:se(r),failingCount:u}),l!==void 0&&a.jsx(K,{topConcern:l}),a.jsx(P,{rows:d})]})]})]})}function ce(){const e=y(),{data:t,loading:n,error:i}=p(`runs:summary:${e??"no-city"}`,A),s=p(`home:work:${e??"no-city"}`,ie),r=ne(t),o=r?.source.fetchedAt??"pre-snapshot";return t===void 0&&n?a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:null}),a.jsx("p",{className:"mt-6 text-body text-fg-muted",children:"Loading…"})]}):t===void 0&&i!==null?a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:null}),a.jsx("p",{className:"mt-6 text-body text-accent",role:"alert","data-testid":"snapshot-error",children:i})]}):r===null?a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:null}),a.jsx("p",{className:"mt-6 text-body text-accent",role:"alert","data-testid":"runs-source-error",children:"Run data is unavailable."})]}):a.jsx(ae,{fresh:r,cityName:e,cycleKey:o,workInProgress:s.data??{status:"unavailable",source:"work",error:"loading"}})}async function ie(){const e=y();if(e===null)return{status:"unavailable",source:"work",error:"active city unavailable"};try{return{status:"available",value:(await R().cityStatus(e)).work.in_progress}}catch(t){return{status:"unavailable",source:"work",error:t instanceof Error?t.message:"work unavailable"}}}export{ce as AmbientHomePage}; diff --git a/internal/api/dashboardspa/dist/assets/AmbientHome-DYE5iAQP.js b/internal/api/dashboardspa/dist/assets/AmbientHome-DYE5iAQP.js new file mode 100644 index 0000000000..6dd445f609 --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/AmbientHome-DYE5iAQP.js @@ -0,0 +1 @@ +import{a as j,j as a,r as c,L as h,D as N,E as S,u as M,b as p,F as A,H as y,I as R}from"./index-QWRimsO3.js";import{P as f}from"./PageHeader-CxbYmkHZ.js";function m(e){return e.phase==="approval"||e.phase==="blocked"}const L={agents:"Agents",beads:"Beads",runs:"Runs",mail:"Mail",activity:"Activity",health:"Health"},$={agents:"/agents",beads:"/beads",runs:"/runs",mail:"/mail",activity:"/activity",health:"/health"};function x(e){return L[e]}function C(e){return $[e]}function E(){const e=j();return e.items.length===0?null:a.jsxs("section",{"aria-labelledby":"attention-summary-title",className:"space-y-3",children:[a.jsx("h2",{id:"attention-summary-title",className:"text-headline font-semibold text-fg",children:"Attention"}),a.jsx("ul",{className:"space-y-2",children:e.topItems.map(t=>a.jsxs("li",{className:"text-body text-fg flex items-baseline gap-3",children:[a.jsx(D,{item:t}),a.jsx("span",{className:`text-label uppercase tracking-wider ${_(t.severity)}`,children:x(t.domain)})]},`${t.domain}:${t.id}`))}),e.overflowByDomain.length>0&&a.jsx("p",{className:"text-label uppercase tracking-wider text-fg-muted",children:e.overflowByDomain.map((t,n)=>a.jsxs(c.Fragment,{children:[n>0&&" · ",a.jsxs(h,{to:C(t.domain),className:"hover:text-fg focus-mark",children:[t.total," more in ",x(t.domain)]})]},t.domain))})]})}function D({item:e}){return e.href===void 0?a.jsx("span",{className:"font-medium",children:e.title}):a.jsx(h,{to:e.href,className:"font-medium hover:text-fg focus-mark",children:e.title})}function _(e){switch(e){case"attention":return"text-accent";case"watch":return"text-warn";case"unavailable":return"text-fg-muted"}}function F(e){return e.external.status!=="unavailable"?e.external.label:e.title}function H(e){const t=encodeURIComponent(e.id),n=e.scope.status==="available"?e.scope:null;if(e.health.status==="available"&&e.health.data.stuckNode.status==="available"){const i=new URLSearchParams;return i.set("node",e.health.data.stuckNode.id),n&&(i.set("scope_kind",n.kind),i.set("scope_ref",n.ref)),`/runs/${t}?${i.toString()}`}if(n){const i=new URLSearchParams;return i.set("scope_kind",n.kind),i.set("scope_ref",n.ref),`/runs/${t}?${i.toString()}`}return`/runs/${t}`}function I(e){switch(e){case"needsOperator":return"needs you";case"stalled":return"stalled";default:return e}}function P({rows:e}){return a.jsx("section",{id:"needs-you",children:a.jsx("ul",{className:"mt-2 transition-opacity duration-150 ease-out-quart motion-reduce:transition-none",style:{opacity:e.length===0?0:1},"aria-live":"polite","data-testid":"concern-region",children:e.map(({lane:t,reason:n})=>a.jsxs("li",{className:"text-body text-fg flex items-baseline gap-3",children:[a.jsx(h,{to:H(t),className:"font-medium hover:text-fg focus-mark","data-testid":`concern-row-${t.id}`,children:F(t)}),a.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:I(n)})]},t.id))})})}const g="gascity:home-intro-dismissed",v="FirstRunNote";function T(){const[e,t]=c.useState(()=>N("localStorage",g,v).status==="found");if(e)return null;const n=()=>{t(!0),S("localStorage",g,"1",v)};return a.jsxs("aside",{className:"mt-6 max-w-[70ch]","data-testid":"first-run-note",children:[a.jsx("p",{className:"text-body text-fg-muted",children:"New here? This page is the ambient home for a Gas City workspace: a calm census of the formula runs in flight. Healthy work stays quiet by design; the page speaks up only when a run needs an operator decision. The full record lives in Agents, Beads, Runs, and Mail above."}),a.jsx("button",{type:"button",onClick:n,className:"mt-2 text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out-quart focus-mark",children:"Dismiss"})]})}function O({census:e,waitingCount:t,failingCount:n}){const s=e.unverifiable>0?` (of ${e.knownDenominator} known)`:"",r=n===0?`nothing failing${s}`:`${n} failing${s}`;return a.jsxs("p",{className:"text-title tnum text-fg","data-testid":"phase-census",children:[a.jsxs("span",{children:[e.totalInFlight," in flight"]}),a.jsx("span",{"aria-hidden":"true",className:"mx-2 text-fg-faint",children:"·"}),a.jsxs("span",{children:[t," waiting"]}),a.jsx("span",{"aria-hidden":"true",className:"mx-2 text-fg-faint",children:"·"}),a.jsx("span",{className:n>0?"font-semibold text-fg":"","aria-live":"polite","data-testid":"phase-census-failing",children:r})]})}function B(e){const t=Math.floor(e/6e4);if(t<60)return`${t} min`;const n=Math.floor(t/60);return n<24?`${n}h`:`${Math.floor(n/24)}d`}function q(e){if(e.health.status!=="available")return null;const t=e.health.data.stuckNode;if(t.status!=="available")return null;const n=encodeURIComponent(e.id),i=e.scope.status==="available"?e.scope:null,s=new URLSearchParams;return s.set("node",t.id),i&&(s.set("scope_kind",i.kind),s.set("scope_ref",i.ref)),`/runs/${n}?${s.toString()}`}function U(e){return e.external.status!=="unavailable"?e.external.label:e.title}function G(e){return m(e)?"has been waiting on your decision for":"has waited on a review verdict for"}function K({topConcern:e}){const{lane:t,ageMs:n}=e,i=U(t),s=q(t),r=G(t),o=B(n);return a.jsxs("p",{className:"text-body text-fg max-w-[70ch] leading-relaxed","data-testid":"status-sentence",children:[s===null?a.jsx("span",{"data-testid":"status-sentence-token",children:i}):a.jsx(h,{to:s,className:"text-accent font-semibold focus-mark","data-testid":"status-sentence-token",children:i})," ",r," ",o,"."]})}const V="/favicon-calm.svg",W="/favicon-alert.svg",Y=2;function Q(e){const t=document.getElementById("favicon");t instanceof HTMLLinkElement&&(t.href=`${e}?v=${Date.now()}`)}function z({failing:e,cycleKey:t}){const n=c.useRef("calm"),i=c.useRef(0),s=c.useRef(null);c.useEffect(()=>{if(s.current===t)return;s.current=t;const r=n.current,o=e>0?"alert":"calm";if(o===r){i.current=0;return}i.current+=1,!(i.current=b.stalled?"stalled":e>=b.warning?"warning":"fresh"}function Z(e){const t=M();return c.useMemo(()=>{const n=new Map,i=[];for(const s of e){const r=J(s),o=s.health.status==="available"&&s.health.data.phaseConfidence==="known";if(r===null){n.set(s.id,{tier:"unknown",ageMs:0,isStalled:!1});continue}const l=Math.max(0,t-r);if(!o){n.set(s.id,{tier:"unknown",ageMs:l,isStalled:!1});continue}const d=X(l),u=d==="stalled";n.set(s.id,{tier:d,ageMs:l,isStalled:u}),u&&i.push({id:s.id,ageMs:l})}return i.sort((s,r)=>r.ageMs-s.ageMs),{byLane:n,clientStalledLaneIds:i.map(s=>s.id)}},[e,t])}function ee(e,t){const n=[];for(const s of e){if(s.health.status!=="available"||!(s.health.data.phaseConfidence==="known"))continue;const o=t.byLane.get(s.id)?.ageMs??0;s.health.data.thrashingDetected?n.push({lane:s,ageMs:o,priority:2}):t.byLane.get(s.id)?.isStalled&&n.push({lane:s,ageMs:o,priority:1})}if(n.length===0)return;n.sort((s,r)=>r.priority-s.priority||r.ageMs-s.ageMs);const i=n[0];return{lane:i.lane,ageMs:i.ageMs}}function te(e,t,n){const i=[];for(const s of e){if(s.id===n)continue;if(m(s)){i.push({lane:s,reason:"needsOperator"});continue}if(s.health.status!=="available")continue;const r=s.health.data;r.phaseConfidence==="known"&&(r.thrashingDetected||t.byLane.get(s.id)?.isStalled)&&i.push({lane:s,reason:"stalled"})}return i}function se(e){let t=0;for(const n of e)m(n)&&(t+=1);return t}function ne(e){return e===void 0||e.status==="error"?null:{source:e,summary:e.data}}function ae({fresh:e,cityName:t,cycleKey:n,workInProgress:i}){const{summary:s}=e,r=c.useMemo(()=>[...s.lanes,...s.blockedLanes],[s.lanes,s.blockedLanes]),o=Z(r),l=c.useMemo(()=>ee(r,o),[r,o]),d=c.useMemo(()=>te(r,o,l?.lane.id),[r,o,l]),u=s.census.status!=="available"?0:s.census.data.thrashing+o.clientStalledLaneIds.length;z({failing:u,cycleKey:n});const w=i.status==="available"?`, ${i.value} in progress`:"",k=t!==null?`${t}, ${s.totalActive} active${w}`:null;return a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:k}),a.jsx(T,{}),s.census.status!=="available"?a.jsxs("p",{className:"mt-6 text-body text-fg-muted max-w-[70ch]",role:"alert","data-testid":"census-unavailable",children:["Census unavailable: ",s.census.error,"."]}):a.jsxs("div",{className:"mt-6 space-y-6",children:[a.jsx(E,{}),a.jsxs("div",{className:"space-y-4",children:[a.jsx(O,{census:s.census.data,waitingCount:se(r),failingCount:u}),l!==void 0&&a.jsx(K,{topConcern:l}),a.jsx(P,{rows:d})]})]})]})}function ce(){const e=y(),{data:t,loading:n,error:i}=p(`runs:summary:${e??"no-city"}`,A),s=p(`home:work:${e??"no-city"}`,ie),r=ne(t),o=r?.source.fetchedAt??"pre-snapshot";return t===void 0&&n?a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:null}),a.jsx("p",{className:"mt-6 text-body text-fg-muted",children:"Loading…"})]}):t===void 0&&i!==null?a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:null}),a.jsx("p",{className:"mt-6 text-body text-accent",role:"alert","data-testid":"snapshot-error",children:i})]}):r===null?a.jsxs("section",{children:[a.jsx(f,{title:"Home",synopsis:null}),a.jsx("p",{className:"mt-6 text-body text-accent",role:"alert","data-testid":"runs-source-error",children:"Run data is unavailable."})]}):a.jsx(ae,{fresh:r,cityName:e,cycleKey:o,workInProgress:s.data??{status:"unavailable",source:"work",error:"loading"}})}async function ie(){const e=y();if(e===null)return{status:"unavailable",source:"work",error:"active city unavailable"};try{return{status:"available",value:(await R().cityStatus(e)).work.in_progress}}catch(t){return{status:"unavailable",source:"work",error:t instanceof Error?t.message:"work unavailable"}}}export{ce as AmbientHomePage}; diff --git a/internal/api/dashboardspa/dist/assets/BeadDetailModal-BtQMJz2-.js b/internal/api/dashboardspa/dist/assets/BeadDetailModal-BtQMJz2-.js new file mode 100644 index 0000000000..a7dbc3c531 --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/BeadDetailModal-BtQMJz2-.js @@ -0,0 +1 @@ +import{r as h,u as H,_ as K,$ as O,J as V,I as E,a0 as q,z as W,j as n,S as Y,a1 as Z,L as J,B as X}from"./index-QWRimsO3.js";import{f as Q}from"./format-fte2CeYD.js";import{F as x}from"./Field-pp_wh5a7.js";import{a as P,L as ee}from"./LiveSessionPeek-m6YywWBh.js";import{M as U}from"./constants-CVFL5iaz.js";import{f as D}from"./time-D9v0saHV.js";function te(e,t){if(e.length===0||t.length===0)return null;const s=t.filter(r=>r.state==="active");return F(e,s)??F(e,t)}function F(e,t){for(const s of t)if(se(s,e))return s;return null}function se(e,t){return e.alias===t||e.pool===t||e.alias!==void 0&&A(e.alias,["/","."])===t||e.session_name!==void 0&&A(e.session_name,["__","--"])===t}function A(e,t){let s=-1,r=0;for(const i of t){const l=e.lastIndexOf(i);l>s&&(s=l,r=i.length)}return s<0?e:e.slice(s+r)}const ne=/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;function re(e,t,s){return`${e}:${s}:${t}`}function b(e,t,s){e?.(t,s)}const ie=/^pr\/(\d{1,9})$/,le=/^issue\/(\d{1,9})$/;function oe(e){const t=e.trim();if(t.length===0)return{ok:!1,error:"empty ref"};const s=ie.exec(t);if(s?.[1])return{ok:!0,type:"github_pr",value:s[1]};const r=le.exec(t);return r?.[1]?{ok:!0,type:"github_issue",value:r[1]}:ne.test(t)?{ok:!0,type:"bead",value:t}:{ok:!1,error:"unrecognised ref"}}function $(e){if(typeof e!="string")return null;const t=e.trim();return/^https?:\/\//i.test(t)?t:null}function v(e,t,s){return re(e,t,s)}function ae(e){return{key:v("bead",e.id,e.scope),type:"bead",ref:e.id}}function _(e,t){const s=e.stats.get(t);if(s)return s;const r={relation:t,resolved:0,unresolved:0,nCandidates:0};return e.stats.set(t,r),r}function y(e,t){e.nodesByKey.has(t.key)||(e.nodesByKey.set(t.key,t),e.view.nodes.push(t))}function k(e,t,s,r,i,l){e.view.edges.push({from:t,to:s,relation:r,provenance:i,resolved:l})}function N(e,t,s,r,i){const l=ae(s);y(e,{...l,title:s.title,status:s.status,url:null,fetchedAt:i,unresolved:!1}),k(e,t,l.key,r,"supervisor",!0),_(e,r).resolved+=1,b(e.recorder,r,"resolved")}function ue(e,t){return{focus:e,nodes:[],edges:[],stats:[],partial:!1,generatedAt:t,asOf:null}}function M(e,t){return e===null?t:t===null||Date.parse(e)<=Date.parse(t)?e:t}function ce(e,t,s={}){const i=(s.now??(()=>new Date))().toISOString(),l=s.supervisorFetchedAt??null,u=s.githubFetchedAt??null,a=de(e,t),o=ue(a.focus,i);o.partial=s.partial??!1;const c={view:o,nodesByKey:new Map,stats:new Map,recorder:s.recorder??(()=>{})};if(y(c,a.focusNode),!a.focusResolved)return o.partial=!0,L(c,l,u),o;const f=a.focusNode.key;if(a.beadFocus)for(const m of a.beads)pe(c,m,f,e,l,u);else for(const m of a.beads)N(c,f,m,"bead",l);return L(c,l,u),o}function de(e,t){if(t.type==="github_pr"||t.type==="github_issue"){const u=t.type==="github_pr"?"github_pr":"github_issue",a=t.type==="github_pr"?`pr/${t.value}`:`issue/${t.value}`,c=(t.type==="github_pr"?e.beadsForPr.get(t.value)??[]:e.beadsForIssue.get(t.value)??[]).map(m=>e.beads.get(m)).filter(m=>m!==void 0),f={key:v(u,t.value,"github"),type:u,ref:a};return{focus:f,focusNode:{...f,title:null,status:null,url:null,fetchedAt:null,unresolved:c.length===0,...c.length>1?{candidateCount:c.length}:{}},beads:c,focusResolved:c.length>0,beadFocus:!1}}const s=e.beads.get(t.value)??e.allBeads.get(t.value);if(s!==void 0){const u=fe(s);return{focus:u,focusNode:{...u,title:s.title,status:s.status,url:null,fetchedAt:null,unresolved:!1},beads:s.superseded?[]:[s],focusResolved:!0,beadFocus:!0}}const r=e.beadsForSession.get(t.value)??[],i=e.sessions.has(t.value);if(r.length>0||i){const u={key:v("session",t.value,"session"),type:"session",ref:t.value},a=r.map(c=>e.beads.get(c)).filter(c=>c!==void 0),o=i||a.length>0;return{focus:u,focusNode:{...u,title:e.sessions.get(t.value)?.title??null,status:e.sessions.get(t.value)?.state??null,url:null,fetchedAt:null,unresolved:!o},beads:a,focusResolved:o,beadFocus:!1}}const l={key:v("bead",t.value,"unknown"),type:"bead",ref:t.value};return{focus:l,focusNode:{...l,title:null,status:null,url:null,fetchedAt:null,unresolved:!0},beads:[],focusResolved:!1,beadFocus:!0}}function fe(e){return{key:v("bead",e.id,e.scope),type:"bead",ref:e.id}}function pe(e,t,s,r,i,l){if(t.parentBeadId){const a=r.beads.get(t.parentBeadId);a?N(e,s,a,"parent",i):me(e,s,t.parentBeadId,"parent")}const u=(r.childrenOf.get(t.id)??[]).filter(a=>a!==t.id);for(const a of u){const o=r.beads.get(a);o&&N(e,s,o,"child",i)}if(t.moleculeId){const a=(r.membersOfMolecule.get(t.moleculeId)??[]).filter(o=>o!==t.id&&o!==t.moleculeId);if(t.moleculeId!==t.id){const o=r.beads.get(t.moleculeId);o&&N(e,s,o,"molecule",i)}for(const o of a){const c=r.beads.get(o);c&&N(e,s,c,"molecule",i)}}if(t.prNumber&&B(e,s,"github_pr",`pr/${t.prNumber}`,t.prNumber,$(t.prUrl),"pr","supervisor",l),t.issueNumber&&B(e,s,"github_issue",`issue/${t.issueNumber}`,t.issueNumber,$(t.issueUrl),"issue","supervisor",l),t.sessionId){const a=r.sessions.get(t.sessionId),o={key:v("session",t.sessionId,"session"),type:"session",ref:t.sessionId};a?(y(e,{...o,title:a.title??a.alias??t.sessionName??null,status:a.state??null,url:null,fetchedAt:i,unresolved:!1}),k(e,s,o.key,"session","supervisor",!0),_(e,"session").resolved+=1,b(e.recorder,"session","resolved")):(y(e,{...o,title:t.sessionName??null,status:null,url:null,fetchedAt:i,unresolved:!0}),k(e,s,o.key,"session","supervisor",!1),_(e,"session").unresolved+=1,b(e.recorder,"session","unresolved"))}}function B(e,t,s,r,i,l,u,a,o){const c=v(s,i,"github");y(e,{key:c,type:s,ref:r,title:null,status:null,url:l,fetchedAt:o,unresolved:!0}),k(e,t,c,u,a,!1),_(e,u).unresolved+=1,b(e.recorder,u,"unresolved")}function me(e,t,s,r){const i=v("bead",s,"unknown");y(e,{key:i,type:"bead",ref:s,title:null,status:null,url:null,fetchedAt:null,unresolved:!0}),k(e,t,i,r,"supervisor",!1),_(e,r).unresolved+=1,b(e.recorder,r,"unresolved")}function L(e,t,s){e.view.stats=[...e.stats.values()].sort((i,l)=>i.relation.localeCompare(l.relation));let r=null;for(const i of e.view.nodes)r=M(r,i.fetchedAt);e.view.asOf=r??M(t,s)}function p(e,t){const s=e.metadata?.[t];if(typeof s=="string"){const r=s.trim();return r.length>0?r:void 0}if(typeof s=="number"&&Number.isFinite(s))return String(s)}function he(e,t){const s=e.metadata?.[t];if(typeof s=="number"&&Number.isInteger(s)&&s>=0)return s;if(typeof s=="string"&&/^\d+$/.test(s.trim()))return Number.parseInt(s.trim(),10)}const ge=["gc.scope_ref","scope_ref","scope_id"],xe=["gc.scope_kind","scope_kind"];function ve(e,t){let s;for(const i of ge){const l=p(e,i);if(l!==void 0){s=l;break}}let r;for(const i of xe){const l=p(e,i);if(l!==void 0){r=l;break}}return s===void 0?`city:${t}`:`${r??"rig"}:${s}`}const ye=/^github-pr:[^/]+\/[^/]+\/(\d+)$/,je=/\/(?:pull\/)?(\d+)(?:[/?#]|$)/;function Ne(e){const t=p(e,"evidence.pr_url"),s=p(e,"evidence.pr_number"),r=p(e,"evidence.artifact_path"),i=p(e,"pr_review.pr_number"),l=p(e,"pr_review.pr_url"),u=r?.match(ye),a=t?.match(je),o=s??u?.[1]??a?.[1]??i??void 0,c=t??l??void 0,f={};return o!==void 0&&(f.prNumber=o),c!==void 0&&(f.prUrl=c),f}function be(e,t){const{prNumber:s,prUrl:r}=Ne(e),i={id:e.id,title:e.title,status:e.status,scope:ve(e,t),superseded:!1},l={parentBeadId:p(e,"gc.parent_bead_id"),rootBeadId:p(e,"gc.root_bead_id"),moleculeId:p(e,"molecule_id"),prNumber:s,prUrl:r,issueNumber:p(e,"bugflow.github_issue_number")??p(e,"design_review.github_issue_number"),issueUrl:p(e,"bugflow.github_issue_url")??p(e,"design_review.github_issue_url"),sessionId:p(e,"session_id"),sessionName:p(e,"session_name"),stepId:p(e,"gc.step_id"),attempt:he(e,"gc.attempt")};for(const[u,a]of Object.entries(l))a!==void 0&&Object.assign(i,{[u]:a});return i}function T(e){return`${e.moleculeId}\0${e.stepId}`}function _e(e){const t=new Map;for(const s of e){if(s.moleculeId===void 0||s.stepId===void 0||s.attempt===void 0)continue;const r=T(s),i=t.get(r);(i===void 0||s.attempt>i)&&t.set(r,s.attempt)}for(const s of e){if(s.moleculeId===void 0||s.stepId===void 0||s.attempt===void 0)continue;const r=t.get(T(s));r!==void 0&&s.attemptbe(d,s));_e(r);const i=new Map,l=new Map,u=new Map,a=new Map,o=new Map,c=new Map,f=new Map;for(const d of r)i.set(d.id,d),!d.superseded&&(l.set(d.id,d),d.parentBeadId&&j(u,d.parentBeadId,d.id),d.moleculeId&&j(a,d.moleculeId,d.id),d.prNumber&&j(o,d.prNumber,d.id),d.issueNumber&&j(c,d.issueNumber,d.id),d.sessionId&&j(f,d.sessionId,d.id));const m=new Map;for(const d of t)m.set(d.id,d);return{beads:l,allBeads:i,childrenOf:u,membersOfMolecule:a,beadsForPr:o,beadsForIssue:c,beadsForSession:f,sessions:m}}function we(e,t,s=null){const[r,i]=h.useState(s),[l,u]=h.useState(!1),[a,o]=h.useState(null),[c,f]=h.useState(!1),m=H();return h.useEffect(()=>{if(!e||!t)return;if(s&&s.id===t&&s.description!==void 0){i(s),o(null),f(!1);return}i(s?.id===t?s:null),u(!0),o(null),f(!1);let d=!1;return(async()=>{try{const g=await K(t);d||i(g)}catch(g){if(d)return;g instanceof O&&g.status===404?f(!0):o(Se(g))}finally{d||u(!1)}})(),()=>{d=!0}},[e,t,s]),{bead:r,loading:l,error:a,notFound:c,now:m}}function Se(e){return e instanceof O?e.status===void 0?e.message:`${e.status} ${e.message}`:e instanceof Error?e.message:"fetch failed"}function Ie(e){return e.partial===!0||(e.partial_errors?.length??0)>0||(e.next_cursor?.length??0)>0}function Re(e,t){return Ie(e)||typeof e.total=="number"&&e.total>t}const Ee=1e3;async function Fe(e){const t=oe(e);if(!t.ok)throw new Error(t.error);const s=V("load supervisor entity links"),r=new Date().toISOString(),i=await E().listBeads(s,{limit:Ee}),l=Ae(i.items??[]);let u=Re(i,l.length),a=[];try{const c=await E().listSessions(s);a=q(c),u||=Me(c)}catch{u=!0}const o=ke(l,a,s);return ce(o,t,{partial:u,supervisorFetchedAt:r,githubFetchedAt:null})}function Ae(e){return e.map($e)}function $e(e){const t={id:e.id,title:e.title,status:e.status,issue_type:e.issue_type,priority:e.priority??null,created_at:e.created_at};return e.description!==void 0&&(t.description=e.description),e.assignee!==void 0&&(t.assignee=e.assignee),Array.isArray(e.labels)&&(t.labels=e.labels),e.metadata!==void 0&&(t.metadata=e.metadata),e.ref!==void 0&&(t.ref=e.ref),e.parent!==void 0&&(t.parent=e.parent),e.from!==void 0&&(t.from=e.from),e.ephemeral!==void 0&&(t.ephemeral=e.ephemeral),e.needs!==void 0&&(t.needs=e.needs),e.dependencies!==void 0&&(t.dependencies=e.dependencies),e.updated_at!==void 0&&(t.updated_at=e.updated_at),t}function Me(e){return e.partial===!0||(e.partial_errors?.length??0)>0}function Be(e){const[t,s]=h.useState(null),[r,i]=h.useState(!1),[l,u]=h.useState(null);return h.useEffect(()=>{if(e===null||e.length===0){s(null),u(null),i(!1);return}let a=!1;return i(!0),u(null),(async()=>{try{const o=await Fe(e);a||s(o)}catch(o){if(a)return;u(W(o,"related entities failed")),s(null)}finally{a||i(!1)}})(),()=>{a=!0}},[e]),{view:t,loading:r,error:l}}function Le(e){const t=e.metadata;if(!t)return{};const s={};return t["gc.kind"]&&(s.kind=t["gc.kind"]),t["gc.source_bead_id"]&&(s.originBeadId=t["gc.source_bead_id"]),t["gc.formula_contract"]&&(s.formulaContract=t["gc.formula_contract"]),t["gc.run_target"]?s.runTarget=t["gc.run_target"]:t["gc.routed_to"]&&(s.runTarget=t["gc.routed_to"]),s}function Te(e,t){return t.kind==="run"?"template":e.issue_type==="molecule"?"wisp":"work"}function Ce({bead:e}){const t=Le(e),s=Te(e,t);return n.jsxs("div",{className:"space-y-8",children:[s==="template"&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Formula template"}),n.jsxs("p",{className:"text-body text-fg-muted max-w-prose",children:["This bead is a recipe, not actionable work. Every"," ",e.ref?n.jsx("code",{className:"text-fg-muted",children:e.ref}):"wisp"," instance is instantiated from this template. The ",n.jsx("span",{className:"text-fg-muted",children:"in_progress"})," ","status is the gc-system convention for ",'"',"available for instantiation",'"'," — do not act on it, nudge it, or close it."]})]}),s==="wisp"&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Formula instance"}),n.jsxs("p",{className:"text-body text-fg-muted max-w-prose",children:["One run of the"," ",e.title?n.jsx("code",{className:"text-fg-muted",children:e.title}):"formula"," recipe."]})]}),n.jsxs("dl",{className:"grid grid-cols-2 sm:grid-cols-4 gap-x-8 gap-y-5",children:[n.jsx(x,{label:"Status",children:n.jsx(Y,{tone:Z(e.status),label:e.status})}),n.jsx(x,{label:"Type",children:e.issue_type}),n.jsx(x,{label:"Assignee",children:e.assignee||"·"}),n.jsx(x,{label:"Created",children:n.jsx("span",{className:"tnum",children:Q(e.created_at)})})]}),s==="template"&&(t.formulaContract||t.originBeadId||t.runTarget)&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Template origin"}),n.jsx("p",{className:"text-body text-fg-muted max-w-prose mb-4",children:"Where this formula came from, kept for traceability. The origin bead and target may be stale; the formula itself is now used wherever the pool dispatches it."}),n.jsxs("dl",{className:"grid grid-cols-2 sm:grid-cols-3 gap-x-8 gap-y-3",children:[t.formulaContract&&n.jsx(x,{label:"Contract",children:n.jsx("code",{className:"text-fg-muted",children:t.formulaContract})}),e.ref&&n.jsx(x,{label:"Ref",children:n.jsx("code",{className:"text-fg-muted",children:e.ref})}),t.originBeadId&&n.jsx(x,{label:"Origin bead",children:n.jsx("code",{className:"text-fg-muted",children:t.originBeadId})}),t.runTarget&&n.jsx(x,{label:"Origin target",children:n.jsx("span",{className:"text-fg-muted truncate",title:t.runTarget,children:t.runTarget})})]})]}),Array.isArray(e.labels)&&e.labels.length>0&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Labels"}),n.jsx("div",{className:"flex flex-wrap gap-x-3 gap-y-1",children:e.labels.map(r=>n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:r},r))})]}),n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:s==="template"?"Recipe":"Description"}),e.description&&e.description.length>0?n.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg font-sans",children:e.description}):n.jsx("p",{className:"text-body text-fg-muted italic",children:"No description."})]})]})}function Oe({node:e,onOpenBead:t}){const{deps:s,blocks:r}=e,i=s.length>0||r.length>0;return n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Dependencies"}),i?n.jsxs("div",{className:"space-y-6",children:[s.length>0&&n.jsxs("div",{children:[n.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-muted mb-2",children:["Needs ",n.jsx("span",{className:"tnum",children:s.length})]}),n.jsx("ul",{className:"space-y-1",children:s.map(l=>n.jsx(C,{relation:l.kind==="needs"?null:l.kind,targetId:l.id,targetTitle:l.bead?.title??null,...l.bead&&t?{onOpenBead:t}:{}},`needs-${l.id}`))})]}),r.length>0&&n.jsxs("div",{children:[n.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-muted mb-2",children:["Blocks ",n.jsx("span",{className:"tnum",children:r.length})]}),n.jsx("ul",{className:"space-y-1",children:r.map(l=>n.jsx(C,{relation:null,targetId:l.id,targetTitle:l.title,...t?{onOpenBead:t}:{}},`blocks-${l.id}`))})]})]}):n.jsx("p",{className:"text-body text-fg-muted italic",children:"No dependencies."})]})}function C({relation:e,targetId:t,targetTitle:s,onOpenBead:r}){const i=n.jsxs(n.Fragment,{children:[e&&n.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:[e," "]}),n.jsx("span",{className:"tnum text-fg-muted",children:t}),s&&n.jsxs("span",{className:"text-fg",children:[" · ",s]})]});return n.jsx("li",{className:"text-body leading-snug",children:r?n.jsx("button",{type:"button",onClick:()=>r(t),className:"text-left text-fg-muted hover:text-fg focus-mark rounded-sm",title:`Open ${t}`,children:i}):n.jsxs("span",{title:"Outside the fetched window",children:[i," ",n.jsx("span",{className:"text-warn text-label uppercase tracking-wider",children:"unresolved"})]})})}function Pe({open:e,onClose:t,session:s,beadTitle:r}){const i=P(s);return n.jsx(U,{open:e,onClose:t,title:r,caption:s===null?"No live session resolved for this bead.":i?"Live transcript from the supervisor's session stream.":"Snapshot from the supervisor's transcript API.",widthClass:"max-w-5xl",children:n.jsx(ee,{sessionId:s?.id??null,stream:i,showBadge:!0,showCaption:!0})})}const Ue=6,De=3600*1e3,ze=3,Ge=["bead","formula_run","session","github_pr","github_issue","order_run"],He={bead:"Beads",session:"Sessions",github_pr:"Pull requests",github_issue:"Issues",formula_run:"Formula runs",order_run:"Order runs"};function Ke({view:e,loading:t,error:s,now:r,onOpenBead:i}){const[l,u]=h.useState(!1),a=h.useMemo(()=>Xe(e),[e]),o=h.useMemo(()=>Je(e),[e]),c=o.unresolved>=ze;return n.jsxs("section",{className:"mt-12",children:[n.jsxs("header",{className:"flex items-baseline justify-between mb-4 gap-3",children:[n.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Related"}),n.jsxs("div",{className:"flex items-baseline gap-3 min-w-0",children:[e&&e.asOf&&n.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:["as of ",D(e.asOf,r)]}),n.jsx(Ve,{loading:t,counts:o,showMark:c})]})]}),s!==null?n.jsx("p",{className:"text-body text-accent",role:"alert",children:s}):t&&e===null?n.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading related entities."}):e===null||a.length===0?n.jsx("p",{className:"text-body text-fg-muted italic",children:"No related entities."}):n.jsxs(n.Fragment,{children:[e.partial&&n.jsx("p",{className:"text-label uppercase tracking-wider text-warn mb-4",role:"status",children:"Partial: some sources did not load. Links may be incomplete."}),n.jsx("button",{type:"button",onClick:()=>u(f=>!f),className:"text-label uppercase tracking-wider text-fg-faint hover:text-fg focus-mark mb-4","aria-expanded":l,children:l?"Hide detail":"Show detail"}),l&&n.jsx("div",{className:"space-y-8",children:a.map(f=>n.jsx(qe,{type:f.type,rows:f.rows,now:r,...i!==void 0?{onOpenBead:i}:{}},f.type))})]})]})}function Ve({loading:e,counts:t,showMark:s}){if(e)return n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:"·"});const r=[];t.resolved>0&&r.push(`${t.resolved} resolved`),t.unresolved>0&&r.push(`${t.unresolved} unresolved`),t.candidates>0&&r.push(`${t.candidates} candidates`);const i=r.length>0?r.join(", "):"none";return n.jsxs("span",{className:`text-label uppercase tracking-wider tnum truncate ${s?"text-accent":"text-fg-faint"}`,children:[s&&n.jsx("span",{"aria-hidden":!0,children:"■ "}),i]})}function qe({type:e,rows:t,now:s,onOpenBead:r}){const i=t.slice(0,Ue),l=t.length-i.length;return n.jsxs("div",{children:[n.jsxs("header",{className:"flex items-baseline justify-between mb-2",children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint",children:He[e]}),n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:t.length})]}),n.jsx("ul",{className:"space-y-2",children:i.map(u=>n.jsx(We,{row:u,now:s,...r!==void 0?{onOpenBead:r}:{}},`${u.relation}\0${u.node.key}`))}),l>0&&n.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-faint mt-2",children:["+ ",l," more"]})]})}function We({row:e,now:t,onOpenBead:s}){const{node:r,relation:i}=e,l=Qe(r.fetchedAt,t),u=r.title??r.ref,a=r.unresolved||l;return n.jsxs("li",{className:"flex items-baseline gap-3 min-w-0",children:[n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint shrink-0 w-20 truncate",children:i}),n.jsx("span",{className:"min-w-0 flex-1 truncate",children:n.jsx(Ye,{node:r,label:u,dimmed:a,...s!==void 0?{onOpenBead:s}:{}})}),n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:r.unresolved?Ze(r):r.fetchedAt?D(r.fetchedAt,t):r.status??"·"})]})}function Ye({node:e,label:t,dimmed:s,onOpenBead:r}){const i=`text-body text-left truncate min-w-0 focus-mark ${s?"text-fg-muted":"text-fg hover:text-accent"}`;return e.type==="bead"&&!e.unresolved&&r?n.jsx("button",{type:"button",onClick:()=>r(e.ref),className:i,title:`Open ${e.ref}`,children:t}):e.type==="session"&&!e.unresolved?n.jsx(J,{to:`/agents/${encodeURIComponent(e.ref)}`,className:i,children:t}):e.url?n.jsxs("a",{href:e.url,target:"_blank",rel:"noreferrer noopener",className:i,title:e.url,children:[t," ",n.jsx("span",{"aria-hidden":!0,children:"↗"})]}):n.jsx("span",{className:i,children:t})}function Ze(e){return e.candidateCount!==void 0&&e.candidateCount>1?`${e.candidateCount} candidates`:"unresolved"}function Je(e){const t={resolved:0,unresolved:0,candidates:0};if(e===null)return t;for(const s of e.nodes)s.key!==e.focus.key&&(s.candidateCount!==void 0&&s.candidateCount>1?t.candidates+=1:s.unresolved?t.unresolved+=1:t.resolved+=1);return t}function Xe(e){if(e===null)return[];const t=new Map;for(const i of e.nodes)t.set(i.key,i);const s=new Map;for(const i of e.edges){if(i.from!==e.focus.key)continue;const l=t.get(i.to);if(l===void 0)continue;const u=s.get(l.type)??[];u.push({node:l,relation:i.relation}),s.set(l.type,u)}const r=[];for(const i of Ge){const l=s.get(i);l&&l.length>0&&(l.sort((u,a)=>Number(u.node.unresolved)-Number(a.node.unresolved)),r.push({type:i,rows:l}))}return r}function Qe(e,t){if(e===null)return!1;const s=Date.parse(e);return Number.isFinite(s)?t-s>De:!1}function lt({open:e,onClose:t,beadId:s,initialBead:r=null,onOpenBead:i,depNode:l=null,sessions:u,renderActions:a}){const{bead:o,loading:c,error:f,notFound:m,now:d}=we(e,s,r),g=Be(e?s:null),[z,w]=h.useState(!1),S=o&&u&&o.assignee&&o.assignee.length>0?te(o.assignee,u):null,I=P(S),R=o?a?.(o):void 0,G=R||I?n.jsxs(n.Fragment,{children:[R,I&&n.jsx(X,{size:"sm",tone:"quiet",onClick:()=>w(!0),children:"View live run"})]}):void 0;return n.jsxs(n.Fragment,{children:[n.jsx(U,{open:e,onClose:t,title:o?.title??s??"Bead",caption:o?n.jsxs("span",{children:[n.jsx("code",{className:"text-fg-muted",children:o.id})," · ",o.issue_type," · P",o.priority==null?"—":o.priority]}):s?n.jsx("code",{className:"text-fg-muted",children:s}):void 0,widthClass:"max-w-3xl",footer:G,children:m?n.jsxs("div",{className:"space-y-2",children:[n.jsx("p",{className:"text-fg-muted",children:"This decision was resolved or removed."}),n.jsx("p",{className:"text-fg-faint text-sm",children:"The bead it pointed to is no longer in the supervisor — it was likely closed or pruned since this link was surfaced."})]}):f?n.jsx("p",{className:"text-accent",role:"alert",children:f}):c&&o===null?n.jsx("p",{className:"text-fg-muted italic",children:"Fetching bead."}):o===null?n.jsx("p",{className:"text-fg-muted italic",children:"No bead."}):n.jsxs("div",{className:"space-y-8",children:[n.jsx(Ce,{bead:o}),l&&n.jsx(Oe,{node:l,...i!==void 0?{onOpenBead:i}:{}}),n.jsx(Ke,{view:g.view,loading:g.loading,error:g.error,now:d,...i!==void 0?{onOpenBead:i}:{}})]})}),o&&n.jsx(Pe,{open:z,onClose:()=>w(!1),session:S,beadTitle:o.title})]})}export{lt as B,Ke as R,Be as u}; diff --git a/internal/api/dashboardspa/dist/assets/BeadDetailModal-Df6JvpcR.js b/internal/api/dashboardspa/dist/assets/BeadDetailModal-Df6JvpcR.js deleted file mode 100644 index af7d77f6f8..0000000000 --- a/internal/api/dashboardspa/dist/assets/BeadDetailModal-Df6JvpcR.js +++ /dev/null @@ -1 +0,0 @@ -import{r as g,u as z,ae as G,aa as T,K,J as E,af as H,a9 as V,z as q,j as n,S as W,ag as Y,L as Z,ah as J,B as X}from"./index-zPatq59W.js";import{f as Q}from"./format-fte2CeYD.js";import{F as x}from"./Field-CQOLMLGH.js";import{a as C,L as ee}from"./LiveSessionPeek-BpsrYunI.js";import{M as O}from"./constants-vAmcTKRZ.js";import{f as P}from"./time-D9v0saHV.js";const te=/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;function se(e,t,s){return`${e}:${s}:${t}`}function b(e,t,s){e?.(t,s)}const ne=/^pr\/(\d{1,9})$/,re=/^issue\/(\d{1,9})$/;function ie(e){const t=e.trim();if(t.length===0)return{ok:!1,error:"empty ref"};const s=ne.exec(t);if(s?.[1])return{ok:!0,type:"github_pr",value:s[1]};const r=re.exec(t);return r?.[1]?{ok:!0,type:"github_issue",value:r[1]}:te.test(t)?{ok:!0,type:"bead",value:t}:{ok:!1,error:"unrecognised ref"}}function F(e){if(typeof e!="string")return null;const t=e.trim();return/^https?:\/\//i.test(t)?t:null}function v(e,t,s){return se(e,t,s)}function le(e){return{key:v("bead",e.id,e.scope),type:"bead",ref:e.id}}function _(e,t){const s=e.stats.get(t);if(s)return s;const r={relation:t,resolved:0,unresolved:0,nCandidates:0};return e.stats.set(t,r),r}function y(e,t){e.nodesByKey.has(t.key)||(e.nodesByKey.set(t.key,t),e.view.nodes.push(t))}function k(e,t,s,r,i,l){e.view.edges.push({from:t,to:s,relation:r,provenance:i,resolved:l})}function N(e,t,s,r,i){const l=le(s);y(e,{...l,title:s.title,status:s.status,url:null,fetchedAt:i,unresolved:!1}),k(e,t,l.key,r,"supervisor",!0),_(e,r).resolved+=1,b(e.recorder,r,"resolved")}function oe(e,t){return{focus:e,nodes:[],edges:[],stats:[],partial:!1,generatedAt:t,asOf:null}}function A(e,t){return e===null?t:t===null||Date.parse(e)<=Date.parse(t)?e:t}function ae(e,t,s={}){const i=(s.now??(()=>new Date))().toISOString(),l=s.supervisorFetchedAt??null,u=s.githubFetchedAt??null,a=ue(e,t),o=oe(a.focus,i);o.partial=s.partial??!1;const c={view:o,nodesByKey:new Map,stats:new Map,recorder:s.recorder??(()=>{})};if(y(c,a.focusNode),!a.focusResolved)return o.partial=!0,$(c,l,u),o;const f=a.focusNode.key;if(a.beadFocus)for(const m of a.beads)de(c,m,f,e,l,u);else for(const m of a.beads)N(c,f,m,"bead",l);return $(c,l,u),o}function ue(e,t){if(t.type==="github_pr"||t.type==="github_issue"){const u=t.type==="github_pr"?"github_pr":"github_issue",a=t.type==="github_pr"?`pr/${t.value}`:`issue/${t.value}`,c=(t.type==="github_pr"?e.beadsForPr.get(t.value)??[]:e.beadsForIssue.get(t.value)??[]).map(m=>e.beads.get(m)).filter(m=>m!==void 0),f={key:v(u,t.value,"github"),type:u,ref:a};return{focus:f,focusNode:{...f,title:null,status:null,url:null,fetchedAt:null,unresolved:c.length===0,...c.length>1?{candidateCount:c.length}:{}},beads:c,focusResolved:c.length>0,beadFocus:!1}}const s=e.beads.get(t.value)??e.allBeads.get(t.value);if(s!==void 0){const u=ce(s);return{focus:u,focusNode:{...u,title:s.title,status:s.status,url:null,fetchedAt:null,unresolved:!1},beads:s.superseded?[]:[s],focusResolved:!0,beadFocus:!0}}const r=e.beadsForSession.get(t.value)??[],i=e.sessions.has(t.value);if(r.length>0||i){const u={key:v("session",t.value,"session"),type:"session",ref:t.value},a=r.map(c=>e.beads.get(c)).filter(c=>c!==void 0),o=i||a.length>0;return{focus:u,focusNode:{...u,title:e.sessions.get(t.value)?.title??null,status:e.sessions.get(t.value)?.state??null,url:null,fetchedAt:null,unresolved:!o},beads:a,focusResolved:o,beadFocus:!1}}const l={key:v("bead",t.value,"unknown"),type:"bead",ref:t.value};return{focus:l,focusNode:{...l,title:null,status:null,url:null,fetchedAt:null,unresolved:!0},beads:[],focusResolved:!1,beadFocus:!0}}function ce(e){return{key:v("bead",e.id,e.scope),type:"bead",ref:e.id}}function de(e,t,s,r,i,l){if(t.parentBeadId){const a=r.beads.get(t.parentBeadId);a?N(e,s,a,"parent",i):fe(e,s,t.parentBeadId,"parent")}const u=(r.childrenOf.get(t.id)??[]).filter(a=>a!==t.id);for(const a of u){const o=r.beads.get(a);o&&N(e,s,o,"child",i)}if(t.moleculeId){const a=(r.membersOfMolecule.get(t.moleculeId)??[]).filter(o=>o!==t.id&&o!==t.moleculeId);if(t.moleculeId!==t.id){const o=r.beads.get(t.moleculeId);o&&N(e,s,o,"molecule",i)}for(const o of a){const c=r.beads.get(o);c&&N(e,s,c,"molecule",i)}}if(t.prNumber&&M(e,s,"github_pr",`pr/${t.prNumber}`,t.prNumber,F(t.prUrl),"pr","supervisor",l),t.issueNumber&&M(e,s,"github_issue",`issue/${t.issueNumber}`,t.issueNumber,F(t.issueUrl),"issue","supervisor",l),t.sessionId){const a=r.sessions.get(t.sessionId),o={key:v("session",t.sessionId,"session"),type:"session",ref:t.sessionId};a?(y(e,{...o,title:a.title??a.alias??t.sessionName??null,status:a.state??null,url:null,fetchedAt:i,unresolved:!1}),k(e,s,o.key,"session","supervisor",!0),_(e,"session").resolved+=1,b(e.recorder,"session","resolved")):(y(e,{...o,title:t.sessionName??null,status:null,url:null,fetchedAt:i,unresolved:!0}),k(e,s,o.key,"session","supervisor",!1),_(e,"session").unresolved+=1,b(e.recorder,"session","unresolved"))}}function M(e,t,s,r,i,l,u,a,o){const c=v(s,i,"github");y(e,{key:c,type:s,ref:r,title:null,status:null,url:l,fetchedAt:o,unresolved:!0}),k(e,t,c,u,a,!1),_(e,u).unresolved+=1,b(e.recorder,u,"unresolved")}function fe(e,t,s,r){const i=v("bead",s,"unknown");y(e,{key:i,type:"bead",ref:s,title:null,status:null,url:null,fetchedAt:null,unresolved:!0}),k(e,t,i,r,"supervisor",!1),_(e,r).unresolved+=1,b(e.recorder,r,"unresolved")}function $(e,t,s){e.view.stats=[...e.stats.values()].sort((i,l)=>i.relation.localeCompare(l.relation));let r=null;for(const i of e.view.nodes)r=A(r,i.fetchedAt);e.view.asOf=r??A(t,s)}function p(e,t){const s=e.metadata?.[t];if(typeof s=="string"){const r=s.trim();return r.length>0?r:void 0}if(typeof s=="number"&&Number.isFinite(s))return String(s)}function pe(e,t){const s=e.metadata?.[t];if(typeof s=="number"&&Number.isInteger(s)&&s>=0)return s;if(typeof s=="string"&&/^\d+$/.test(s.trim()))return Number.parseInt(s.trim(),10)}const me=["gc.scope_ref","scope_ref","scope_id"],ge=["gc.scope_kind","scope_kind"];function he(e,t){let s;for(const i of me){const l=p(e,i);if(l!==void 0){s=l;break}}let r;for(const i of ge){const l=p(e,i);if(l!==void 0){r=l;break}}return s===void 0?`city:${t}`:`${r??"rig"}:${s}`}const xe=/^github-pr:[^/]+\/[^/]+\/(\d+)$/,ve=/\/(?:pull\/)?(\d+)(?:[/?#]|$)/;function ye(e){const t=p(e,"evidence.pr_url"),s=p(e,"evidence.pr_number"),r=p(e,"evidence.artifact_path"),i=p(e,"pr_review.pr_number"),l=p(e,"pr_review.pr_url"),u=r?.match(xe),a=t?.match(ve),o=s??u?.[1]??a?.[1]??i??void 0,c=t??l??void 0,f={};return o!==void 0&&(f.prNumber=o),c!==void 0&&(f.prUrl=c),f}function je(e,t){const{prNumber:s,prUrl:r}=ye(e),i={id:e.id,title:e.title,status:e.status,scope:he(e,t),superseded:!1},l={parentBeadId:p(e,"gc.parent_bead_id"),rootBeadId:p(e,"gc.root_bead_id"),moleculeId:p(e,"molecule_id"),prNumber:s,prUrl:r,issueNumber:p(e,"bugflow.github_issue_number")??p(e,"design_review.github_issue_number"),issueUrl:p(e,"bugflow.github_issue_url")??p(e,"design_review.github_issue_url"),sessionId:p(e,"session_id"),sessionName:p(e,"session_name"),stepId:p(e,"gc.step_id"),attempt:pe(e,"gc.attempt")};for(const[u,a]of Object.entries(l))a!==void 0&&Object.assign(i,{[u]:a});return i}function B(e){return`${e.moleculeId}\0${e.stepId}`}function Ne(e){const t=new Map;for(const s of e){if(s.moleculeId===void 0||s.stepId===void 0||s.attempt===void 0)continue;const r=B(s),i=t.get(r);(i===void 0||s.attempt>i)&&t.set(r,s.attempt)}for(const s of e){if(s.moleculeId===void 0||s.stepId===void 0||s.attempt===void 0)continue;const r=t.get(B(s));r!==void 0&&s.attemptje(d,s));Ne(r);const i=new Map,l=new Map,u=new Map,a=new Map,o=new Map,c=new Map,f=new Map;for(const d of r)i.set(d.id,d),!d.superseded&&(l.set(d.id,d),d.parentBeadId&&j(u,d.parentBeadId,d.id),d.moleculeId&&j(a,d.moleculeId,d.id),d.prNumber&&j(o,d.prNumber,d.id),d.issueNumber&&j(c,d.issueNumber,d.id),d.sessionId&&j(f,d.sessionId,d.id));const m=new Map;for(const d of t)m.set(d.id,d);return{beads:l,allBeads:i,childrenOf:u,membersOfMolecule:a,beadsForPr:o,beadsForIssue:c,beadsForSession:f,sessions:m}}function _e(e,t,s=null){const[r,i]=g.useState(s),[l,u]=g.useState(!1),[a,o]=g.useState(null),[c,f]=g.useState(!1),m=z();return g.useEffect(()=>{if(!e||!t)return;if(s&&s.id===t&&s.description!==void 0){i(s),o(null),f(!1);return}i(s?.id===t?s:null),u(!0),o(null),f(!1);let d=!1;return(async()=>{try{const h=await G(t);d||i(h)}catch(h){if(d)return;h instanceof T&&h.status===404?f(!0):o(ke(h))}finally{d||u(!1)}})(),()=>{d=!0}},[e,t,s]),{bead:r,loading:l,error:a,notFound:c,now:m}}function ke(e){return e instanceof T?e.status===void 0?e.message:`${e.status} ${e.message}`:e instanceof Error?e.message:"fetch failed"}const we=1e3;async function Se(e){const t=ie(e);if(!t.ok)throw new Error(t.error);const s=K("load supervisor entity links"),r=new Date().toISOString(),i=await E().listBeads(s,{limit:we}),l=Re(i.items??[]);let u=H(i,l.length),a=[];try{const c=await E().listSessions(s);a=V(c),u||=Ee(c)}catch{u=!0}const o=be(l,a,s);return ae(o,t,{partial:u,supervisorFetchedAt:r,githubFetchedAt:null})}function Re(e){return e.map(Ie)}function Ie(e){const t={id:e.id,title:e.title,status:e.status,issue_type:e.issue_type,priority:e.priority??null,created_at:e.created_at};return e.description!==void 0&&(t.description=e.description),e.assignee!==void 0&&(t.assignee=e.assignee),Array.isArray(e.labels)&&(t.labels=e.labels),e.metadata!==void 0&&(t.metadata=e.metadata),e.ref!==void 0&&(t.ref=e.ref),e.parent!==void 0&&(t.parent=e.parent),e.from!==void 0&&(t.from=e.from),e.ephemeral!==void 0&&(t.ephemeral=e.ephemeral),e.needs!==void 0&&(t.needs=e.needs),e.dependencies!==void 0&&(t.dependencies=e.dependencies),e.updated_at!==void 0&&(t.updated_at=e.updated_at),t}function Ee(e){return e.partial===!0||(e.partial_errors?.length??0)>0}function Fe(e){const[t,s]=g.useState(null),[r,i]=g.useState(!1),[l,u]=g.useState(null);return g.useEffect(()=>{if(e===null||e.length===0){s(null),u(null),i(!1);return}let a=!1;return i(!0),u(null),(async()=>{try{const o=await Se(e);a||s(o)}catch(o){if(a)return;u(q(o,"related entities failed")),s(null)}finally{a||i(!1)}})(),()=>{a=!0}},[e]),{view:t,loading:r,error:l}}function Ae(e){const t=e.metadata;if(!t)return{};const s={};return t["gc.kind"]&&(s.kind=t["gc.kind"]),t["gc.source_bead_id"]&&(s.originBeadId=t["gc.source_bead_id"]),t["gc.formula_contract"]&&(s.formulaContract=t["gc.formula_contract"]),t["gc.run_target"]?s.runTarget=t["gc.run_target"]:t["gc.routed_to"]&&(s.runTarget=t["gc.routed_to"]),s}function Me(e,t){return t.kind==="run"?"template":e.issue_type==="molecule"?"wisp":"work"}function $e({bead:e}){const t=Ae(e),s=Me(e,t);return n.jsxs("div",{className:"space-y-8",children:[s==="template"&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Formula template"}),n.jsxs("p",{className:"text-body text-fg-muted max-w-prose",children:["This bead is a recipe, not actionable work. Every"," ",e.ref?n.jsx("code",{className:"text-fg-muted",children:e.ref}):"wisp"," instance is instantiated from this template. The ",n.jsx("span",{className:"text-fg-muted",children:"in_progress"})," ","status is the gc-system convention for ",'"',"available for instantiation",'"'," — do not act on it, nudge it, or close it."]})]}),s==="wisp"&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Formula instance"}),n.jsxs("p",{className:"text-body text-fg-muted max-w-prose",children:["One run of the"," ",e.title?n.jsx("code",{className:"text-fg-muted",children:e.title}):"formula"," recipe."]})]}),n.jsxs("dl",{className:"grid grid-cols-2 sm:grid-cols-4 gap-x-8 gap-y-5",children:[n.jsx(x,{label:"Status",children:n.jsx(W,{tone:Y(e.status),label:e.status})}),n.jsx(x,{label:"Type",children:e.issue_type}),n.jsx(x,{label:"Assignee",children:e.assignee||"·"}),n.jsx(x,{label:"Created",children:n.jsx("span",{className:"tnum",children:Q(e.created_at)})})]}),s==="template"&&(t.formulaContract||t.originBeadId||t.runTarget)&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Template origin"}),n.jsx("p",{className:"text-body text-fg-muted max-w-prose mb-4",children:"Where this formula came from, kept for traceability. The origin bead and target may be stale; the formula itself is now used wherever the pool dispatches it."}),n.jsxs("dl",{className:"grid grid-cols-2 sm:grid-cols-3 gap-x-8 gap-y-3",children:[t.formulaContract&&n.jsx(x,{label:"Contract",children:n.jsx("code",{className:"text-fg-muted",children:t.formulaContract})}),e.ref&&n.jsx(x,{label:"Ref",children:n.jsx("code",{className:"text-fg-muted",children:e.ref})}),t.originBeadId&&n.jsx(x,{label:"Origin bead",children:n.jsx("code",{className:"text-fg-muted",children:t.originBeadId})}),t.runTarget&&n.jsx(x,{label:"Origin target",children:n.jsx("span",{className:"text-fg-muted truncate",title:t.runTarget,children:t.runTarget})})]})]}),Array.isArray(e.labels)&&e.labels.length>0&&n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Labels"}),n.jsx("div",{className:"flex flex-wrap gap-x-3 gap-y-1",children:e.labels.map(r=>n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:r},r))})]}),n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:s==="template"?"Recipe":"Description"}),e.description&&e.description.length>0?n.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg font-sans",children:e.description}):n.jsx("p",{className:"text-body text-fg-muted italic",children:"No description."})]})]})}function Be({node:e,onOpenBead:t}){const{deps:s,blocks:r}=e,i=s.length>0||r.length>0;return n.jsxs("section",{children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint mb-3",children:"Dependencies"}),i?n.jsxs("div",{className:"space-y-6",children:[s.length>0&&n.jsxs("div",{children:[n.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-muted mb-2",children:["Needs ",n.jsx("span",{className:"tnum",children:s.length})]}),n.jsx("ul",{className:"space-y-1",children:s.map(l=>n.jsx(L,{relation:l.kind==="needs"?null:l.kind,targetId:l.id,targetTitle:l.bead?.title??null,...l.bead&&t?{onOpenBead:t}:{}},`needs-${l.id}`))})]}),r.length>0&&n.jsxs("div",{children:[n.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-muted mb-2",children:["Blocks ",n.jsx("span",{className:"tnum",children:r.length})]}),n.jsx("ul",{className:"space-y-1",children:r.map(l=>n.jsx(L,{relation:null,targetId:l.id,targetTitle:l.title,...t?{onOpenBead:t}:{}},`blocks-${l.id}`))})]})]}):n.jsx("p",{className:"text-body text-fg-muted italic",children:"No dependencies."})]})}function L({relation:e,targetId:t,targetTitle:s,onOpenBead:r}){const i=n.jsxs(n.Fragment,{children:[e&&n.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:[e," "]}),n.jsx("span",{className:"tnum text-fg-muted",children:t}),s&&n.jsxs("span",{className:"text-fg",children:[" · ",s]})]});return n.jsx("li",{className:"text-body leading-snug",children:r?n.jsx("button",{type:"button",onClick:()=>r(t),className:"text-left text-fg-muted hover:text-fg focus-mark rounded-sm",title:`Open ${t}`,children:i}):n.jsxs("span",{title:"Outside the fetched window",children:[i," ",n.jsx("span",{className:"text-warn text-label uppercase tracking-wider",children:"unresolved"})]})})}function Le({open:e,onClose:t,session:s,beadTitle:r}){const i=C(s);return n.jsx(O,{open:e,onClose:t,title:r,caption:s===null?"No live session resolved for this bead.":i?"Live transcript from the supervisor's session stream.":"Snapshot from the supervisor's transcript API.",widthClass:"max-w-5xl",children:n.jsx(ee,{sessionId:s?.id??null,stream:i,showBadge:!0,showCaption:!0})})}const Te=6,Ce=3600*1e3,Oe=3,Pe=["bead","formula_run","session","github_pr","github_issue","order_run"],Ue={bead:"Beads",session:"Sessions",github_pr:"Pull requests",github_issue:"Issues",formula_run:"Formula runs",order_run:"Order runs"};function De({view:e,loading:t,error:s,now:r,onOpenBead:i}){const[l,u]=g.useState(!1),a=g.useMemo(()=>We(e),[e]),o=g.useMemo(()=>qe(e),[e]),c=o.unresolved>=Oe;return n.jsxs("section",{className:"mt-12",children:[n.jsxs("header",{className:"flex items-baseline justify-between mb-4 gap-3",children:[n.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Related"}),n.jsxs("div",{className:"flex items-baseline gap-3 min-w-0",children:[e&&e.asOf&&n.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:["as of ",P(e.asOf,r)]}),n.jsx(ze,{loading:t,counts:o,showMark:c})]})]}),s!==null?n.jsx("p",{className:"text-body text-accent",role:"alert",children:s}):t&&e===null?n.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading related entities."}):e===null||a.length===0?n.jsx("p",{className:"text-body text-fg-muted italic",children:"No related entities."}):n.jsxs(n.Fragment,{children:[e.partial&&n.jsx("p",{className:"text-label uppercase tracking-wider text-warn mb-4",role:"status",children:"Partial: some sources did not load. Links may be incomplete."}),n.jsx("button",{type:"button",onClick:()=>u(f=>!f),className:"text-label uppercase tracking-wider text-fg-faint hover:text-fg focus-mark mb-4","aria-expanded":l,children:l?"Hide detail":"Show detail"}),l&&n.jsx("div",{className:"space-y-8",children:a.map(f=>n.jsx(Ge,{type:f.type,rows:f.rows,now:r,...i!==void 0?{onOpenBead:i}:{}},f.type))})]})]})}function ze({loading:e,counts:t,showMark:s}){if(e)return n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:"·"});const r=[];t.resolved>0&&r.push(`${t.resolved} resolved`),t.unresolved>0&&r.push(`${t.unresolved} unresolved`),t.candidates>0&&r.push(`${t.candidates} candidates`);const i=r.length>0?r.join(", "):"none";return n.jsxs("span",{className:`text-label uppercase tracking-wider tnum truncate ${s?"text-accent":"text-fg-faint"}`,children:[s&&n.jsx("span",{"aria-hidden":!0,children:"■ "}),i]})}function Ge({type:e,rows:t,now:s,onOpenBead:r}){const i=t.slice(0,Te),l=t.length-i.length;return n.jsxs("div",{children:[n.jsxs("header",{className:"flex items-baseline justify-between mb-2",children:[n.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint",children:Ue[e]}),n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:t.length})]}),n.jsx("ul",{className:"space-y-2",children:i.map(u=>n.jsx(Ke,{row:u,now:s,...r!==void 0?{onOpenBead:r}:{}},`${u.relation}\0${u.node.key}`))}),l>0&&n.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-faint mt-2",children:["+ ",l," more"]})]})}function Ke({row:e,now:t,onOpenBead:s}){const{node:r,relation:i}=e,l=Ye(r.fetchedAt,t),u=r.title??r.ref,a=r.unresolved||l;return n.jsxs("li",{className:"flex items-baseline gap-3 min-w-0",children:[n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint shrink-0 w-20 truncate",children:i}),n.jsx("span",{className:"min-w-0 flex-1 truncate",children:n.jsx(He,{node:r,label:u,dimmed:a,...s!==void 0?{onOpenBead:s}:{}})}),n.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:r.unresolved?Ve(r):r.fetchedAt?P(r.fetchedAt,t):r.status??"·"})]})}function He({node:e,label:t,dimmed:s,onOpenBead:r}){const i=`text-body text-left truncate min-w-0 focus-mark ${s?"text-fg-muted":"text-fg hover:text-accent"}`;return e.type==="bead"&&!e.unresolved&&r?n.jsx("button",{type:"button",onClick:()=>r(e.ref),className:i,title:`Open ${e.ref}`,children:t}):e.type==="session"&&!e.unresolved?n.jsx(Z,{to:`/agents/${encodeURIComponent(e.ref)}`,className:i,children:t}):e.url?n.jsxs("a",{href:e.url,target:"_blank",rel:"noreferrer noopener",className:i,title:e.url,children:[t," ",n.jsx("span",{"aria-hidden":!0,children:"↗"})]}):n.jsx("span",{className:i,children:t})}function Ve(e){return e.candidateCount!==void 0&&e.candidateCount>1?`${e.candidateCount} candidates`:"unresolved"}function qe(e){const t={resolved:0,unresolved:0,candidates:0};if(e===null)return t;for(const s of e.nodes)s.key!==e.focus.key&&(s.candidateCount!==void 0&&s.candidateCount>1?t.candidates+=1:s.unresolved?t.unresolved+=1:t.resolved+=1);return t}function We(e){if(e===null)return[];const t=new Map;for(const i of e.nodes)t.set(i.key,i);const s=new Map;for(const i of e.edges){if(i.from!==e.focus.key)continue;const l=t.get(i.to);if(l===void 0)continue;const u=s.get(l.type)??[];u.push({node:l,relation:i.relation}),s.set(l.type,u)}const r=[];for(const i of Pe){const l=s.get(i);l&&l.length>0&&(l.sort((u,a)=>Number(u.node.unresolved)-Number(a.node.unresolved)),r.push({type:i,rows:l}))}return r}function Ye(e,t){if(e===null)return!1;const s=Date.parse(e);return Number.isFinite(s)?t-s>Ce:!1}function st({open:e,onClose:t,beadId:s,initialBead:r=null,onOpenBead:i,depNode:l=null,sessions:u,renderActions:a}){const{bead:o,loading:c,error:f,notFound:m,now:d}=_e(e,s,r),h=Fe(e?s:null),[U,w]=g.useState(!1),S=o&&u&&o.assignee&&o.assignee.length>0?J(o.assignee,u):null,R=C(S),I=o?a?.(o):void 0,D=I||R?n.jsxs(n.Fragment,{children:[I,R&&n.jsx(X,{size:"sm",tone:"quiet",onClick:()=>w(!0),children:"View live run"})]}):void 0;return n.jsxs(n.Fragment,{children:[n.jsx(O,{open:e,onClose:t,title:o?.title??s??"Bead",caption:o?n.jsxs("span",{children:[n.jsx("code",{className:"text-fg-muted",children:o.id})," · ",o.issue_type," · P",o.priority==null?"—":o.priority]}):s?n.jsx("code",{className:"text-fg-muted",children:s}):void 0,widthClass:"max-w-3xl",footer:D,children:m?n.jsxs("div",{className:"space-y-2",children:[n.jsx("p",{className:"text-fg-muted",children:"This decision was resolved or removed."}),n.jsx("p",{className:"text-fg-faint text-sm",children:"The bead it pointed to is no longer in the supervisor — it was likely closed or pruned since this link was surfaced."})]}):f?n.jsx("p",{className:"text-accent",role:"alert",children:f}):c&&o===null?n.jsx("p",{className:"text-fg-muted italic",children:"Fetching bead."}):o===null?n.jsx("p",{className:"text-fg-muted italic",children:"No bead."}):n.jsxs("div",{className:"space-y-8",children:[n.jsx($e,{bead:o}),l&&n.jsx(Be,{node:l,...i!==void 0?{onOpenBead:i}:{}}),n.jsx(De,{view:h.view,loading:h.loading,error:h.error,now:d,...i!==void 0?{onOpenBead:i}:{}})]})}),o&&n.jsx(Le,{open:U,onClose:()=>w(!1),session:S,beadTitle:o.title})]})}export{st as B,De as R,Fe as u}; diff --git a/internal/api/dashboardspa/dist/assets/Beads-BVqefDvL.js b/internal/api/dashboardspa/dist/assets/Beads-m4fbNWDo.js similarity index 96% rename from internal/api/dashboardspa/dist/assets/Beads-BVqefDvL.js rename to internal/api/dashboardspa/dist/assets/Beads-m4fbNWDo.js index b292dd904b..83d60d2793 100644 --- a/internal/api/dashboardspa/dist/assets/Beads-BVqefDvL.js +++ b/internal/api/dashboardspa/dist/assets/Beads-m4fbNWDo.js @@ -1 +1 @@ -import{j as e,S as je,B as w,r as o,J as L,K as X,a as Le,g as Fe,M as Te,b as Y,c as De,l as qe,f as ze,z as fe,R as xe,i as J,I as He,G as Ke}from"./index-zPatq59W.js";import{b as Ve,r as Ge}from"./routeHighlight-B30gQO2o.js";import{B as Ue}from"./BeadDetailModal-Df6JvpcR.js";import{u as Ye,F as Je}from"./useListFilters-C9ZhD4ch.js";import{L as Xe,f as Qe}from"./projectOf-nApq7eyo.js";import{M as be}from"./constants-vAmcTKRZ.js";import{P as We}from"./PageHeader-DGfr1hUc.js";import{l as Ze}from"./agentReads-NY5zZttz.js";import"./format-fte2CeYD.js";import"./Field-CQOLMLGH.js";import"./LiveSessionPeek-BpsrYunI.js";import"./time-D9v0saHV.js";function et(n){if(n===void 0)return null;const s=n.indexOf("?");if(s<0)return null;const l=new URLSearchParams(n.slice(s+1)).get("bead");return l!==null&&l.length>0?l:null}function tt({items:n,onOpen:s}){const l=n.filter(a=>a.severity==="attention"||a.severity==="watch");return l.length===0?null:e.jsxs("section",{"aria-labelledby":"beads-attention-title",className:"mb-8 space-y-3",children:[e.jsxs("h2",{id:"beads-attention-title",className:"text-label uppercase tracking-wider text-fg-muted",children:["Needs you ",e.jsxs("span",{className:"tnum text-fg",children:["(",l.length,")"]})]}),e.jsx("ul",{className:"space-y-2",children:l.map(a=>{const i=et(a.href);return e.jsxs("li",{className:"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1",children:[e.jsxs("div",{className:"min-w-0 space-y-0.5",children:[e.jsx(je,{tone:a.severity==="attention"?"stuck":"warn",label:a.title}),a.summary!==void 0&&e.jsx("p",{className:"text-body text-fg-muted",children:a.summary})]}),i!==null&&e.jsx("div",{className:"flex items-center gap-2",children:e.jsx(w,{type:"button",size:"sm",tone:"quiet",onClick:()=>s(i),children:"Open"})})]},a.id)})})]})}const ae=[{id:"ready",label:"ready"},{id:"open",label:"open"},{id:"in_progress",label:"in progress"},{id:"blocked",label:"blocked"},{id:"done",label:"done"}];function st(n){const s=new Set,l=[];for(const a of n.needs??[])a.length===0||s.has(a)||(s.add(a),l.push({id:a,kind:"needs"}));for(const a of n.dependencies??[]){const i=a.depends_on_id;i.length===0||s.has(i)||(s.add(i),l.push({id:i,kind:a.type}))}return l}function nt(n){return(n.needs??[]).filter(s=>s.length>0)}function at(n){switch(n.bead.status){case"in_progress":return"in_progress";case"blocked":return"blocked";case"closed":return"done";default:return n.ready?"ready":"open"}}function lt(n,s){const l=n.bead.priority??Number.POSITIVE_INFINITY,a=s.bead.priority??Number.POSITIVE_INFINITY;return l!==a?l-a:n.bead.ids.bead.id?1:0}function rt(n){const s=new Map;for(const r of n)s.set(r.id,r);const l=new Map,a=new Map;for(const r of n){const c=st(r).map(({id:m,kind:g})=>({id:m,kind:g,bead:s.get(m)??null})),u=c.some(m=>m.bead===null),d=nt(r),h=r.status==="open"&&d.every(m=>s.get(m)?.status==="closed"),p={bead:r,deps:c,blocks:[],ready:h,hasUnresolvedDeps:u,column:"open"};p.column=at(p),a.set(r.id,p);for(const m of c){if(m.bead===null)continue;const g=l.get(m.id);g?g.push(r):l.set(m.id,[r])}}for(const[r,c]of l){const u=a.get(r);u&&(u.blocks=[...c].sort((d,h)=>d.idh.id?1:0))}const i=Ne();for(const r of a.values())i[r.column].push(r);for(const r of ae)i[r.id].sort(lt);return{nodes:a,columns:i}}function Ne(){return{ready:[],open:[],in_progress:[],blocked:[],done:[]}}function ot(n,s){const l=Ne();for(const a of ae)l[a.id]=n.columns[a.id].filter(i=>s.has(i.bead.id));return l}function it({node:n,selected:s,attentionSeverity:l=null,onSelect:a}){const{bead:i,deps:r,blocks:c,hasUnresolvedDeps:u}=n,d=o.useRef(null),h=r.length,p=c.length,m=h>0||p>0,{className:g="",...R}=Ve(l);return o.useEffect(()=>{s&&d.current?.scrollIntoView?.({block:"center",inline:"nearest"})},[s]),e.jsx("li",{ref:d,...R,className:`px-2 py-2 -mx-2 rounded-sm transition-colors duration-150 ease-out-quart ${s?"bg-surface-tint":"hover:bg-surface-tint/60"} ${g}`,children:e.jsxs("button",{type:"button",onClick:()=>a(i.id),className:"text-left w-full focus-mark rounded-sm","aria-pressed":s,title:`Select ${i.id}`,children:[e.jsxs("span",{className:"flex items-baseline gap-2",children:[e.jsx("span",{className:"text-fg-faint","aria-hidden":"true",children:s?"▸":" "}),e.jsx("span",{className:`min-w-0 line-clamp-2 text-body ${s?"text-fg font-medium":"text-fg"}`,children:i.title})]}),e.jsxs("span",{className:"flex items-baseline gap-3 pl-4 mt-0.5 text-label uppercase tracking-wider text-fg-faint",children:[e.jsx("span",{className:"tnum",children:i.id}),i.priority!=null&&e.jsxs("span",{className:"tnum",children:["P",i.priority]}),m&&e.jsxs("span",{className:"tnum normal-case tracking-normal",children:[h>0&&`needs ${h}`,h>0&&p>0&&" · ",p>0&&`blocks ${p}`]}),u&&e.jsx("span",{className:"normal-case tracking-normal text-warn",children:"unresolved"})]})]})})}function ct({columns:n,selectedId:s,attentionSeverity:l,onSelect:a}){return e.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-x-8 gap-y-8",children:ae.map(i=>{const r=n[i.id],u=i.id==="blocked"&&r.length>0?"text-accent":"text-fg-muted";return e.jsxs("section",{"aria-label":i.label,children:[e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-3",children:[e.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:i.label}),e.jsx("span",{className:`text-label tnum ${u}`,children:r.length})]}),r.length===0?e.jsx("p",{className:"text-body text-fg-faint italic",children:"·"}):e.jsx("ul",{className:"space-y-1",children:r.map(d=>e.jsx(it,{node:d,selected:d.bead.id===s,attentionSeverity:l?.(d.bead.id)??null,onSelect:a},d.bead.id))})]},i.id)})})}function dt({label:n,count:s,graph:l,ids:a,selectedId:i,attentionSeverity:r,onSelect:c}){const u=ot(l,a);return e.jsxs("section",{"aria-label":n,children:[e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-4",children:[e.jsx("h2",{className:"text-headline text-fg",children:n}),e.jsx("span",{className:"text-label tnum text-fg-muted",children:s})]}),e.jsx(ct,{columns:u,selectedId:i,...r===void 0?{}:{attentionSeverity:r},onSelect:c})]})}function ut(n,s){const l=n?.trim();if(!l)return;const a=s.find(r=>r.name===l);return a?a.name:s.find(r=>r.path===l)?.name}function mt(n){return Array.from(new Set(n.map(s=>s.name.trim()).filter(s=>s.length>0))).sort((s,l)=>s.localeCompare(l))}async function pt(){const n=await L().listRigs(X("list supervisor rigs"));return{...n,items:n.items??[]}}async function gt(n,s){const l=s?.trim()??"";await L().closeBead(X("close supervisor bead"),n,l.length===0?void 0:{reason:l})}async function ht(n){const s=n.trim();if(s.length===0)throw new Error("agent alias is required");await L().nudgeAgent(X("nudge supervisor agent"),s)}async function ft(n){const s=n.title.trim(),l=n.description.trim(),a=n.rig.trim(),i=n.target.trim();if(s.length===0)throw new Error("bead title is required");if(i.length===0)throw new Error("sling target is required");const r=X("create and sling supervisor bead"),c={title:s};l.length>0&&(c.description=l);const u=await L().createBead(r,c),d={bead:u.id,target:i};a.length>0&&(d.rig=a);const h=await L().sling(r,d);return{bead:u,sling:h}}const xt=new Set,v="",we="closed",bt=1e4,ye=[{id:"open",label:"open",match:n=>n.status==="open"},{id:"in_progress",label:"in progress",match:n=>n.status==="in_progress"},{id:"blocked",label:"blocked",match:n=>n.status==="blocked"},{id:we,label:"closed",match:n=>n.status==="closed"}],yt=n=>[n.id,n.title,n.assignee,...n.labels??[]];function Mt(){const n=Le(),s=Fe(),a=He()??"no-city",[i]=Te(),r=jt(i.get("bead")),[c,u]=o.useState(v),[d,h]=o.useState(!1),[p,m]=o.useState(r),[g,R]=o.useState(null),[le,_]=o.useState(""),[k,re]=o.useState(null),[F,S]=o.useState(null),[Q,T]=o.useState(!1),[D,oe]=o.useState(!1),[ie,W]=o.useState(null),[q,ce]=o.useState(""),[Z,de]=o.useState(""),[A,ue]=o.useState(""),[y,$]=o.useState(""),{data:I,loading:z,error:me,refresh:E}=Y(`beads:board:${a}:${c}:${d?"all":"open"}`,()=>De({includeClosed:d,...c===v?{}:{rigFilter:c}})),ve=o.useMemo(()=>I?.items??[],[I]),pe=I?.total??0,ee=I?.upstream_total,te=I?.upstream_fetched,Ce=I?.fetch_limit,H=I!==void 0,K=Y(`sessions:${a}`,qe),ke=o.useMemo(()=>K.data?.items??[],[K.data]),M=Y(`agents:${a}`,Ze),N=o.useMemo(()=>M.data?.items??[],[M.data]),V=Y(`rigs:${a}`,pt),G=o.useMemo(()=>V.data?.items??[],[V.data]),C=o.useMemo(()=>mt(G),[G]),B=o.useCallback(t=>ut(t.rig,G),[G]),O=o.useMemo(()=>A.length===0?N:N.filter(t=>B(t)===A),[N,B,A]);o.useEffect(()=>{if(Q){if(O.length===0){y.length>0&&$("");return}O.some(t=>t.name===y)||$(O[0]?.name??"")}},[Q,O,y]),o.useEffect(()=>{c!==v&&!C.includes(c)&&u(v)},[C,c]);const U=ve,b=Ye({viewKey:"beads",rows:U,projectOf:Qe,searchOf:yt,chips:ye}),{toggleChip:ge}=b,Se=o.useCallback(t=>{t===we&&h(f=>!f),ge(t)},[ge]);ze([Ke.bead],()=>{E()},{coalesceMs:bt}),o.useEffect(()=>{r!==null&&m(r)},[r]);const se=o.useCallback(async(t,f,x)=>{if(!s){re({id:t.id,action:f}),S(null);try{if(f==="close")await gt(t.id,x),R(null),_(""),S({tone:"ok",text:`Closed ${t.id}.`});else{const j=t.assignee?.trim()??"";if(j.length===0)throw new Error("Assigned agent is required before nudging.");await ht(j),S({tone:"ok",text:`Nudged ${j}.`})}await E()}catch(j){S({tone:"error",text:fe(j,`${f} failed`)})}finally{re(null)}}},[s,E]),Ie=o.useCallback(()=>{const t=C[0]??"",f=N.find(x=>t.length===0||B(x)===t);ce(""),de(""),ue(t),$(f?.name??""),W(null),S(null),T(!0)},[N,B,C]),Be=o.useCallback(t=>{if(ue(t),!N.some(x=>x.name===y&&(t.length===0||B(x)===t))){const x=N.find(j=>t.length===0||B(j)===t);$(x?.name??"")}},[N,B,y]),Re=o.useCallback(async()=>{if(!s){oe(!0),W(null);try{const t=await ft({title:q,description:Z,rig:A,target:y});S({tone:"ok",text:`Created ${t.bead.id} and slung to ${y}.`}),T(!1),await E()}catch(t){W(fe(t,"create and sling failed"))}finally{oe(!1)}}},[y,Z,A,q,s,E]),P=o.useMemo(()=>b.groups.flatMap(t=>t.rows),[b.groups]),ne=o.useMemo(()=>rt(P),[P]),Ae=o.useMemo(()=>{const t=new Map;for(const f of b.groups)t.set(f.projectKey,new Set(f.rows.map(x=>x.id)));return t},[b.groups]),Ee=o.useMemo(()=>P.find(t=>t.id===p)??null,[P,p]),_e=o.useMemo(()=>p===null?null:ne.nodes.get(p)??null,[ne,p]),$e=o.useMemo(()=>t=>Ge(n,"beads",t),[n]),Me=o.useCallback(t=>{const f=t.assignee?.trim()??"",x=k!==null,j=k?.id===t.id?k.action.replace("_"," "):null,he=s?J:void 0;return e.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-2",children:[s&&e.jsx(xe,{}),j&&e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:j}),e.jsx(w,{type:"button",size:"sm",tone:"quiet",title:he,disabled:s||x||t.status==="closed",onClick:()=>{_(""),S(null),R(t)},children:"Close"}),e.jsx(w,{type:"button",size:"sm",tone:"quiet",title:he,disabled:s||x||f.length===0,onClick:()=>{se(t,"nudge")},children:"Nudge"})]})},[k,s,se]),Oe=o.useMemo(()=>H?Nt(U,pe,c):"Loading beads.",[U,H,pe,c]),Pe=typeof ee=="number"&&typeof te=="number"&&te{E()},disabled:z,children:z&&!H?"Loading":z?"Refreshing":"Refresh"})]})}),e.jsxs("div",{className:"space-y-2 mb-6 text-body text-fg-muted max-w-prose",children:[Pe&&e.jsx("p",{className:"text-warn",children:e.jsx(je,{tone:"warn",label:`Fetch window covered ${te} of ${ee} store beads. Raise the fetch limit (currently ${Ce??"?"}) if engineering work sits past the window.`})}),c!==v&&e.jsxs("p",{children:["Filtering by rig ",e.jsx("span",{className:"text-accent",children:c}),"."," ",e.jsx("button",{type:"button",onClick:()=>u(v),className:"text-fg-muted hover:text-fg focus-mark underline decoration-dotted underline-offset-2 rounded-sm",children:"Clear"})]}),F&&e.jsx("p",{className:F.tone==="error"?"text-accent":"text-fg-muted",role:F.tone==="error"?"alert":"status",children:F.text})]}),e.jsx(tt,{items:n.byDomain.beads.items,onOpen:m}),e.jsxs("div",{className:"mb-6 space-y-3",children:[e.jsx(Xe,{value:b.search,onChange:b.setSearch,placeholder:"Search beads by id, title, label, assignee",matchCount:b.totalMatches,totalCount:U.length,ariaLabel:"Search beads"}),e.jsxs("div",{className:"flex flex-wrap items-baseline gap-x-8 gap-y-3",children:[e.jsx(Je,{chips:ye,activeIds:b.activeChipIds,onToggle:Se,legend:"Status"}),C.length>1&&e.jsxs("label",{className:"flex items-baseline gap-2 text-label",children:[e.jsx("span",{className:"uppercase tracking-wider text-fg-muted",children:"Rig"}),e.jsxs("select",{value:c,onChange:t=>u(t.target.value),"aria-label":"Rig filter",className:"text-label uppercase tracking-wider text-fg-muted bg-transparent border-0 focus-mark cursor-pointer hover:text-fg transition-colors duration-150 ease-out-quart",children:[e.jsx("option",{value:v,children:"all rigs"}),C.map(t=>e.jsx("option",{value:t,children:t},t))]})]})]})]}),!H&&z?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading beads."}):P.length===0?e.jsx("p",{className:"text-body text-fg-muted italic",children:b.search.length>0||b.activeChipIds.size>0?"No beads match the current search or filter.":"Nothing on the queue right now."}):e.jsx("div",{className:"space-y-12",children:b.groups.map(t=>e.jsx(dt,{label:t.project,count:t.totalInProject,graph:ne,ids:Ae.get(t.projectKey)??xt,selectedId:p,attentionSeverity:$e,onSelect:m},t.projectKey))}),e.jsx(Ue,{open:p!==null,onClose:()=>m(null),beadId:p,initialBead:Ee,depNode:_e,sessions:ke,onOpenBead:m,renderActions:Me}),e.jsx(be,{open:g!==null,onClose:()=>{k===null&&(R(null),_(""))},title:g?`Close ${g.id}`:"Close bead",caption:g?.title,widthClass:"max-w-xl",footer:e.jsxs(e.Fragment,{children:[e.jsx(w,{type:"button",size:"sm",tone:"quiet",disabled:k!==null,onClick:()=>{R(null),_("")},children:"Cancel"}),e.jsx(w,{type:"button",size:"sm",tone:"accent",title:s?J:void 0,disabled:s||g===null||k!==null,onClick:()=>{g&&se(g,"close",le)},children:"Close bead"})]}),children:e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Reason"}),e.jsx("textarea",{value:le,onChange:t=>_(t.target.value),rows:4,placeholder:"Optional close reason",className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark"})]})}),e.jsx(be,{open:Q,onClose:()=>{D||T(!1)},title:"New bead",caption:"Create and sling",widthClass:"max-w-2xl",footer:e.jsxs(e.Fragment,{children:[e.jsx(w,{type:"button",size:"sm",tone:"quiet",disabled:D,onClick:()=>T(!1),children:"Cancel"}),e.jsx(w,{type:"submit",form:"new-bead-form",size:"sm",title:s?J:void 0,disabled:s||D||q.trim().length===0||y.trim().length===0,children:D?"Creating":"Create and sling"})]}),children:e.jsxs("form",{id:"new-bead-form",className:"space-y-5",onSubmit:t=>{t.preventDefault(),Re()},children:[ie&&e.jsx("p",{className:"text-accent",role:"alert",children:ie}),e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Title"}),e.jsx("input",{value:q,onChange:t=>ce(t.target.value),required:!0,className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark"})]}),e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Body"}),e.jsx("textarea",{value:Z,onChange:t=>de(t.target.value),rows:5,className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark"})]}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Rig"}),e.jsxs("select",{value:A,onChange:t=>Be(t.target.value),className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark",children:[C.length===0&&e.jsx("option",{value:"",children:"all rigs"}),C.map(t=>e.jsx("option",{value:t,children:t},t))]})]}),e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Agent"}),e.jsx("select",{value:y,onChange:t=>$(t.target.value),required:!0,className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark",children:O.map(t=>e.jsx("option",{value:t.name,children:t.display_name??t.name},t.name))})]})]})]})})]})}function jt(n){const s=n?.trim();return s&&s.length>0?s:null}function Nt(n,s,l){if(l!==v&&n.length===0)return`No beads on ${l}.`;const a=n.filter(d=>d.status==="open").length,i=n.filter(d=>d.status==="in_progress").length,r=n.filter(d=>d.status==="blocked").length,c=[];if(a>0&&c.push(`${a} open`),i>0&&c.push(`${i} in progress`),r>0&&c.push(`${r} blocked`),c.length===0)return"Nothing on the queue.";let u=`${c.join(", ")}.`;return l!==v&&(u=`${l}: ${u}`),s>n.length&&(u+=` Showing ${n.length} of ${s}.`),u}export{Mt as BeadsPage}; +import{j as e,S as je,B as w,r as o,I as L,J as X,a as Le,g as Fe,K as Te,b as Y,c as De,l as qe,f as ze,z as fe,R as xe,i as J,H as He,G as Ke}from"./index-QWRimsO3.js";import{b as Ve,r as Ge}from"./routeHighlight-B30gQO2o.js";import{B as Ue}from"./BeadDetailModal-BtQMJz2-.js";import{u as Ye,F as Je}from"./useListFilters-D2HcBe10.js";import{L as Xe,f as Qe}from"./projectOf-CAOn7SI-.js";import{M as be}from"./constants-CVFL5iaz.js";import{P as We}from"./PageHeader-CxbYmkHZ.js";import{l as Ze}from"./agentReads-BA8TH08X.js";import"./format-fte2CeYD.js";import"./Field-pp_wh5a7.js";import"./LiveSessionPeek-m6YywWBh.js";import"./time-D9v0saHV.js";function et(n){if(n===void 0)return null;const s=n.indexOf("?");if(s<0)return null;const l=new URLSearchParams(n.slice(s+1)).get("bead");return l!==null&&l.length>0?l:null}function tt({items:n,onOpen:s}){const l=n.filter(a=>a.severity==="attention"||a.severity==="watch");return l.length===0?null:e.jsxs("section",{"aria-labelledby":"beads-attention-title",className:"mb-8 space-y-3",children:[e.jsxs("h2",{id:"beads-attention-title",className:"text-label uppercase tracking-wider text-fg-muted",children:["Needs you ",e.jsxs("span",{className:"tnum text-fg",children:["(",l.length,")"]})]}),e.jsx("ul",{className:"space-y-2",children:l.map(a=>{const i=et(a.href);return e.jsxs("li",{className:"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1",children:[e.jsxs("div",{className:"min-w-0 space-y-0.5",children:[e.jsx(je,{tone:a.severity==="attention"?"stuck":"warn",label:a.title}),a.summary!==void 0&&e.jsx("p",{className:"text-body text-fg-muted",children:a.summary})]}),i!==null&&e.jsx("div",{className:"flex items-center gap-2",children:e.jsx(w,{type:"button",size:"sm",tone:"quiet",onClick:()=>s(i),children:"Open"})})]},a.id)})})]})}const ae=[{id:"ready",label:"ready"},{id:"open",label:"open"},{id:"in_progress",label:"in progress"},{id:"blocked",label:"blocked"},{id:"done",label:"done"}];function st(n){const s=new Set,l=[];for(const a of n.needs??[])a.length===0||s.has(a)||(s.add(a),l.push({id:a,kind:"needs"}));for(const a of n.dependencies??[]){const i=a.depends_on_id;i.length===0||s.has(i)||(s.add(i),l.push({id:i,kind:a.type}))}return l}function nt(n){return(n.needs??[]).filter(s=>s.length>0)}function at(n){switch(n.bead.status){case"in_progress":return"in_progress";case"blocked":return"blocked";case"closed":return"done";default:return n.ready?"ready":"open"}}function lt(n,s){const l=n.bead.priority??Number.POSITIVE_INFINITY,a=s.bead.priority??Number.POSITIVE_INFINITY;return l!==a?l-a:n.bead.ids.bead.id?1:0}function rt(n){const s=new Map;for(const r of n)s.set(r.id,r);const l=new Map,a=new Map;for(const r of n){const c=st(r).map(({id:m,kind:g})=>({id:m,kind:g,bead:s.get(m)??null})),u=c.some(m=>m.bead===null),d=nt(r),h=r.status==="open"&&d.every(m=>s.get(m)?.status==="closed"),p={bead:r,deps:c,blocks:[],ready:h,hasUnresolvedDeps:u,column:"open"};p.column=at(p),a.set(r.id,p);for(const m of c){if(m.bead===null)continue;const g=l.get(m.id);g?g.push(r):l.set(m.id,[r])}}for(const[r,c]of l){const u=a.get(r);u&&(u.blocks=[...c].sort((d,h)=>d.idh.id?1:0))}const i=Ne();for(const r of a.values())i[r.column].push(r);for(const r of ae)i[r.id].sort(lt);return{nodes:a,columns:i}}function Ne(){return{ready:[],open:[],in_progress:[],blocked:[],done:[]}}function ot(n,s){const l=Ne();for(const a of ae)l[a.id]=n.columns[a.id].filter(i=>s.has(i.bead.id));return l}function it({node:n,selected:s,attentionSeverity:l=null,onSelect:a}){const{bead:i,deps:r,blocks:c,hasUnresolvedDeps:u}=n,d=o.useRef(null),h=r.length,p=c.length,m=h>0||p>0,{className:g="",...R}=Ve(l);return o.useEffect(()=>{s&&d.current?.scrollIntoView?.({block:"center",inline:"nearest"})},[s]),e.jsx("li",{ref:d,...R,className:`px-2 py-2 -mx-2 rounded-sm transition-colors duration-150 ease-out-quart ${s?"bg-surface-tint":"hover:bg-surface-tint/60"} ${g}`,children:e.jsxs("button",{type:"button",onClick:()=>a(i.id),className:"text-left w-full focus-mark rounded-sm","aria-pressed":s,title:`Select ${i.id}`,children:[e.jsxs("span",{className:"flex items-baseline gap-2",children:[e.jsx("span",{className:"text-fg-faint","aria-hidden":"true",children:s?"▸":" "}),e.jsx("span",{className:`min-w-0 line-clamp-2 text-body ${s?"text-fg font-medium":"text-fg"}`,children:i.title})]}),e.jsxs("span",{className:"flex items-baseline gap-3 pl-4 mt-0.5 text-label uppercase tracking-wider text-fg-faint",children:[e.jsx("span",{className:"tnum",children:i.id}),i.priority!=null&&e.jsxs("span",{className:"tnum",children:["P",i.priority]}),m&&e.jsxs("span",{className:"tnum normal-case tracking-normal",children:[h>0&&`needs ${h}`,h>0&&p>0&&" · ",p>0&&`blocks ${p}`]}),u&&e.jsx("span",{className:"normal-case tracking-normal text-warn",children:"unresolved"})]})]})})}function ct({columns:n,selectedId:s,attentionSeverity:l,onSelect:a}){return e.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-x-8 gap-y-8",children:ae.map(i=>{const r=n[i.id],u=i.id==="blocked"&&r.length>0?"text-accent":"text-fg-muted";return e.jsxs("section",{"aria-label":i.label,children:[e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-3",children:[e.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:i.label}),e.jsx("span",{className:`text-label tnum ${u}`,children:r.length})]}),r.length===0?e.jsx("p",{className:"text-body text-fg-faint italic",children:"·"}):e.jsx("ul",{className:"space-y-1",children:r.map(d=>e.jsx(it,{node:d,selected:d.bead.id===s,attentionSeverity:l?.(d.bead.id)??null,onSelect:a},d.bead.id))})]},i.id)})})}function dt({label:n,count:s,graph:l,ids:a,selectedId:i,attentionSeverity:r,onSelect:c}){const u=ot(l,a);return e.jsxs("section",{"aria-label":n,children:[e.jsxs("header",{className:"flex items-baseline justify-between border-b border-rule pb-2 mb-4",children:[e.jsx("h2",{className:"text-headline text-fg",children:n}),e.jsx("span",{className:"text-label tnum text-fg-muted",children:s})]}),e.jsx(ct,{columns:u,selectedId:i,...r===void 0?{}:{attentionSeverity:r},onSelect:c})]})}function ut(n,s){const l=n?.trim();if(!l)return;const a=s.find(r=>r.name===l);return a?a.name:s.find(r=>r.path===l)?.name}function mt(n){return Array.from(new Set(n.map(s=>s.name.trim()).filter(s=>s.length>0))).sort((s,l)=>s.localeCompare(l))}async function pt(){const n=await L().listRigs(X("list supervisor rigs"));return{...n,items:n.items??[]}}async function gt(n,s){const l=s?.trim()??"";await L().closeBead(X("close supervisor bead"),n,l.length===0?void 0:{reason:l})}async function ht(n){const s=n.trim();if(s.length===0)throw new Error("agent alias is required");await L().nudgeAgent(X("nudge supervisor agent"),s)}async function ft(n){const s=n.title.trim(),l=n.description.trim(),a=n.rig.trim(),i=n.target.trim();if(s.length===0)throw new Error("bead title is required");if(i.length===0)throw new Error("sling target is required");const r=X("create and sling supervisor bead"),c={title:s};l.length>0&&(c.description=l);const u=await L().createBead(r,c),d={bead:u.id,target:i};a.length>0&&(d.rig=a);const h=await L().sling(r,d);return{bead:u,sling:h}}const xt=new Set,v="",we="closed",bt=1e4,ye=[{id:"open",label:"open",match:n=>n.status==="open"},{id:"in_progress",label:"in progress",match:n=>n.status==="in_progress"},{id:"blocked",label:"blocked",match:n=>n.status==="blocked"},{id:we,label:"closed",match:n=>n.status==="closed"}],yt=n=>[n.id,n.title,n.assignee,...n.labels??[]];function Mt(){const n=Le(),s=Fe(),a=He()??"no-city",[i]=Te(),r=jt(i.get("bead")),[c,u]=o.useState(v),[d,h]=o.useState(!1),[p,m]=o.useState(r),[g,R]=o.useState(null),[le,_]=o.useState(""),[k,re]=o.useState(null),[F,S]=o.useState(null),[Q,T]=o.useState(!1),[D,oe]=o.useState(!1),[ie,W]=o.useState(null),[q,ce]=o.useState(""),[Z,de]=o.useState(""),[A,ue]=o.useState(""),[y,$]=o.useState(""),{data:I,loading:z,error:me,refresh:E}=Y(`beads:board:${a}:${c}:${d?"all":"open"}`,()=>De({includeClosed:d,...c===v?{}:{rigFilter:c}})),ve=o.useMemo(()=>I?.items??[],[I]),pe=I?.total??0,ee=I?.upstream_total,te=I?.upstream_fetched,Ce=I?.fetch_limit,H=I!==void 0,K=Y(`sessions:${a}`,qe),ke=o.useMemo(()=>K.data?.items??[],[K.data]),M=Y(`agents:${a}`,Ze),N=o.useMemo(()=>M.data?.items??[],[M.data]),V=Y(`rigs:${a}`,pt),G=o.useMemo(()=>V.data?.items??[],[V.data]),C=o.useMemo(()=>mt(G),[G]),B=o.useCallback(t=>ut(t.rig,G),[G]),O=o.useMemo(()=>A.length===0?N:N.filter(t=>B(t)===A),[N,B,A]);o.useEffect(()=>{if(Q){if(O.length===0){y.length>0&&$("");return}O.some(t=>t.name===y)||$(O[0]?.name??"")}},[Q,O,y]),o.useEffect(()=>{c!==v&&!C.includes(c)&&u(v)},[C,c]);const U=ve,b=Ye({viewKey:"beads",rows:U,projectOf:Qe,searchOf:yt,chips:ye}),{toggleChip:ge}=b,Se=o.useCallback(t=>{t===we&&h(f=>!f),ge(t)},[ge]);ze([Ke.bead],()=>{E()},{coalesceMs:bt}),o.useEffect(()=>{r!==null&&m(r)},[r]);const se=o.useCallback(async(t,f,x)=>{if(!s){re({id:t.id,action:f}),S(null);try{if(f==="close")await gt(t.id,x),R(null),_(""),S({tone:"ok",text:`Closed ${t.id}.`});else{const j=t.assignee?.trim()??"";if(j.length===0)throw new Error("Assigned agent is required before nudging.");await ht(j),S({tone:"ok",text:`Nudged ${j}.`})}await E()}catch(j){S({tone:"error",text:fe(j,`${f} failed`)})}finally{re(null)}}},[s,E]),Ie=o.useCallback(()=>{const t=C[0]??"",f=N.find(x=>t.length===0||B(x)===t);ce(""),de(""),ue(t),$(f?.name??""),W(null),S(null),T(!0)},[N,B,C]),Be=o.useCallback(t=>{if(ue(t),!N.some(x=>x.name===y&&(t.length===0||B(x)===t))){const x=N.find(j=>t.length===0||B(j)===t);$(x?.name??"")}},[N,B,y]),Re=o.useCallback(async()=>{if(!s){oe(!0),W(null);try{const t=await ft({title:q,description:Z,rig:A,target:y});S({tone:"ok",text:`Created ${t.bead.id} and slung to ${y}.`}),T(!1),await E()}catch(t){W(fe(t,"create and sling failed"))}finally{oe(!1)}}},[y,Z,A,q,s,E]),P=o.useMemo(()=>b.groups.flatMap(t=>t.rows),[b.groups]),ne=o.useMemo(()=>rt(P),[P]),Ae=o.useMemo(()=>{const t=new Map;for(const f of b.groups)t.set(f.projectKey,new Set(f.rows.map(x=>x.id)));return t},[b.groups]),Ee=o.useMemo(()=>P.find(t=>t.id===p)??null,[P,p]),_e=o.useMemo(()=>p===null?null:ne.nodes.get(p)??null,[ne,p]),$e=o.useMemo(()=>t=>Ge(n,"beads",t),[n]),Me=o.useCallback(t=>{const f=t.assignee?.trim()??"",x=k!==null,j=k?.id===t.id?k.action.replace("_"," "):null,he=s?J:void 0;return e.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-2",children:[s&&e.jsx(xe,{}),j&&e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:j}),e.jsx(w,{type:"button",size:"sm",tone:"quiet",title:he,disabled:s||x||t.status==="closed",onClick:()=>{_(""),S(null),R(t)},children:"Close"}),e.jsx(w,{type:"button",size:"sm",tone:"quiet",title:he,disabled:s||x||f.length===0,onClick:()=>{se(t,"nudge")},children:"Nudge"})]})},[k,s,se]),Oe=o.useMemo(()=>H?Nt(U,pe,c):"Loading beads.",[U,H,pe,c]),Pe=typeof ee=="number"&&typeof te=="number"&&te{E()},disabled:z,children:z&&!H?"Loading":z?"Refreshing":"Refresh"})]})}),e.jsxs("div",{className:"space-y-2 mb-6 text-body text-fg-muted max-w-prose",children:[Pe&&e.jsx("p",{className:"text-warn",children:e.jsx(je,{tone:"warn",label:`Fetch window covered ${te} of ${ee} store beads. Raise the fetch limit (currently ${Ce??"?"}) if engineering work sits past the window.`})}),c!==v&&e.jsxs("p",{children:["Filtering by rig ",e.jsx("span",{className:"text-accent",children:c}),"."," ",e.jsx("button",{type:"button",onClick:()=>u(v),className:"text-fg-muted hover:text-fg focus-mark underline decoration-dotted underline-offset-2 rounded-sm",children:"Clear"})]}),F&&e.jsx("p",{className:F.tone==="error"?"text-accent":"text-fg-muted",role:F.tone==="error"?"alert":"status",children:F.text})]}),e.jsx(tt,{items:n.byDomain.beads.items,onOpen:m}),e.jsxs("div",{className:"mb-6 space-y-3",children:[e.jsx(Xe,{value:b.search,onChange:b.setSearch,placeholder:"Search beads by id, title, label, assignee",matchCount:b.totalMatches,totalCount:U.length,ariaLabel:"Search beads"}),e.jsxs("div",{className:"flex flex-wrap items-baseline gap-x-8 gap-y-3",children:[e.jsx(Je,{chips:ye,activeIds:b.activeChipIds,onToggle:Se,legend:"Status"}),C.length>1&&e.jsxs("label",{className:"flex items-baseline gap-2 text-label",children:[e.jsx("span",{className:"uppercase tracking-wider text-fg-muted",children:"Rig"}),e.jsxs("select",{value:c,onChange:t=>u(t.target.value),"aria-label":"Rig filter",className:"text-label uppercase tracking-wider text-fg-muted bg-transparent border-0 focus-mark cursor-pointer hover:text-fg transition-colors duration-150 ease-out-quart",children:[e.jsx("option",{value:v,children:"all rigs"}),C.map(t=>e.jsx("option",{value:t,children:t},t))]})]})]})]}),!H&&z?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading beads."}):P.length===0?e.jsx("p",{className:"text-body text-fg-muted italic",children:b.search.length>0||b.activeChipIds.size>0?"No beads match the current search or filter.":"Nothing on the queue right now."}):e.jsx("div",{className:"space-y-12",children:b.groups.map(t=>e.jsx(dt,{label:t.project,count:t.totalInProject,graph:ne,ids:Ae.get(t.projectKey)??xt,selectedId:p,attentionSeverity:$e,onSelect:m},t.projectKey))}),e.jsx(Ue,{open:p!==null,onClose:()=>m(null),beadId:p,initialBead:Ee,depNode:_e,sessions:ke,onOpenBead:m,renderActions:Me}),e.jsx(be,{open:g!==null,onClose:()=>{k===null&&(R(null),_(""))},title:g?`Close ${g.id}`:"Close bead",caption:g?.title,widthClass:"max-w-xl",footer:e.jsxs(e.Fragment,{children:[e.jsx(w,{type:"button",size:"sm",tone:"quiet",disabled:k!==null,onClick:()=>{R(null),_("")},children:"Cancel"}),e.jsx(w,{type:"button",size:"sm",tone:"accent",title:s?J:void 0,disabled:s||g===null||k!==null,onClick:()=>{g&&se(g,"close",le)},children:"Close bead"})]}),children:e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Reason"}),e.jsx("textarea",{value:le,onChange:t=>_(t.target.value),rows:4,placeholder:"Optional close reason",className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark"})]})}),e.jsx(be,{open:Q,onClose:()=>{D||T(!1)},title:"New bead",caption:"Create and sling",widthClass:"max-w-2xl",footer:e.jsxs(e.Fragment,{children:[e.jsx(w,{type:"button",size:"sm",tone:"quiet",disabled:D,onClick:()=>T(!1),children:"Cancel"}),e.jsx(w,{type:"submit",form:"new-bead-form",size:"sm",title:s?J:void 0,disabled:s||D||q.trim().length===0||y.trim().length===0,children:D?"Creating":"Create and sling"})]}),children:e.jsxs("form",{id:"new-bead-form",className:"space-y-5",onSubmit:t=>{t.preventDefault(),Re()},children:[ie&&e.jsx("p",{className:"text-accent",role:"alert",children:ie}),e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Title"}),e.jsx("input",{value:q,onChange:t=>ce(t.target.value),required:!0,className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark"})]}),e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Body"}),e.jsx("textarea",{value:Z,onChange:t=>de(t.target.value),rows:5,className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark"})]}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Rig"}),e.jsxs("select",{value:A,onChange:t=>Be(t.target.value),className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark",children:[C.length===0&&e.jsx("option",{value:"",children:"all rigs"}),C.map(t=>e.jsx("option",{value:t,children:t},t))]})]}),e.jsxs("label",{className:"block space-y-2 text-body",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Agent"}),e.jsx("select",{value:y,onChange:t=>$(t.target.value),required:!0,className:"w-full rounded-sm border border-rule bg-transparent px-3 py-2 text-body text-fg focus-mark",children:O.map(t=>e.jsx("option",{value:t.name,children:t.display_name??t.name},t.name))})]})]})]})})]})}function jt(n){const s=n?.trim();return s&&s.length>0?s:null}function Nt(n,s,l){if(l!==v&&n.length===0)return`No beads on ${l}.`;const a=n.filter(d=>d.status==="open").length,i=n.filter(d=>d.status==="in_progress").length,r=n.filter(d=>d.status==="blocked").length,c=[];if(a>0&&c.push(`${a} open`),i>0&&c.push(`${i} in progress`),r>0&&c.push(`${r} blocked`),c.length===0)return"Nothing on the queue.";let u=`${c.join(", ")}.`;return l!==v&&(u=`${l}: ${u}`),s>n.length&&(u+=` Showing ${n.length} of ${s}.`),u}export{Mt as BeadsPage}; diff --git a/internal/api/dashboardspa/dist/assets/Field-CQOLMLGH.js b/internal/api/dashboardspa/dist/assets/Field-pp_wh5a7.js similarity index 85% rename from internal/api/dashboardspa/dist/assets/Field-CQOLMLGH.js rename to internal/api/dashboardspa/dist/assets/Field-pp_wh5a7.js index da52bada82..92dcba3595 100644 --- a/internal/api/dashboardspa/dist/assets/Field-CQOLMLGH.js +++ b/internal/api/dashboardspa/dist/assets/Field-pp_wh5a7.js @@ -1 +1 @@ -import{j as e}from"./index-zPatq59W.js";function i({label:t,children:s,variant:a="definition"}){return a==="form"?e.jsxs("label",{className:"block space-y-1.5",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:t}),s]}):e.jsxs("div",{children:[e.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint mb-1",children:t}),e.jsx("dd",{className:"text-body text-fg",children:s})]})}export{i as F}; +import{j as e}from"./index-QWRimsO3.js";function i({label:t,children:s,variant:a="definition"}){return a==="form"?e.jsxs("label",{className:"block space-y-1.5",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:t}),s]}):e.jsxs("div",{children:[e.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint mb-1",children:t}),e.jsx("dd",{className:"text-body text-fg",children:s})]})}export{i as F}; diff --git a/internal/api/dashboardspa/dist/assets/FormulaRunDetail-BXhub1du.js b/internal/api/dashboardspa/dist/assets/FormulaRunDetail-BXhub1du.js deleted file mode 100644 index ac94917025..0000000000 --- a/internal/api/dashboardspa/dist/assets/FormulaRunDetail-BXhub1du.js +++ /dev/null @@ -1,12 +0,0 @@ -import{Y as w,Z as O,_ as T,$ as wt,a0 as kt,a1 as pr,a2 as q,a3 as vr,a4 as br,a5 as hn,a6 as yr,j as d,r as A,S as _r,K as wr,a7 as kr,J as St,a8 as Sr,a9 as Nr,aa as Nt,b as jt,x as It,p as Ct,ab as jr,q as Ir,M as Cr,f as Ar,u as Er,ac as Dr,L as Tr,B as Rr,ad as Mr,I as Or,G as On}from"./index-zPatq59W.js";import{P as $r}from"./PageHeader-DGfr1hUc.js";import{u as Pr,R as Fr,B as xr}from"./BeadDetailModal-Df6JvpcR.js";import{u as Br,S as Gr}from"./LiveSessionPeek-BpsrYunI.js";import{S as $n}from"./StageLadder-CwHTgfDd.js";import"./format-fte2CeYD.js";import"./Field-CQOLMLGH.js";import"./constants-vAmcTKRZ.js";import"./time-D9v0saHV.js";const nn=/^(gc|td|th|[a-z]{4})-[a-z0-9-]{1,32}$/,Lr=new Set(["completed","done","failed","skipped"]);function zr(e,t){const n=new Map(e.map(a=>[a.id,a])),r=Ur(t,n),s=new Map;for(const a of e)s.set(a.id,Kr(a,r.get(a.id)??[],n));return e.map(a=>{const i=s.get(a.id)??a.status;return i===a.status?a:{...a,status:i,executionInstances:a.executionInstances.map(u=>u.currentIteration!==!1&&u.status==="pending"?{...u,status:i}:u)}})}function Kr(e,t,n){return e.status!=="pending"?e.status:t.length===0||t.every(s=>{const a=n.get(s);return a?Lr.has(a.status):!1})?"ready":"blocked"}function Ur(e,t){const n=new Map;for(const r of e)!t.has(r.from)||!t.has(r.to)||n.set(r.to,[...n.get(r.to)??[],r.from]);return n}function Hr(e,t,n){const r=Pn(e.logical_edges??[],t,n);return r.length>0?r:Pn(e.deps??[],t,n,Wr(e))}function Pn(e,t,n,r=new Set){const s=new Set(n.filter(o=>o.visibleInGraph!==!1).map(o=>o.id)),a=Vr(e),i=new Set,u=[];for(const o of e){const c=w(o.from),l=w(o.to);if(!c||!l||w(o.kind)==="tracks")continue;const f=t.get(c)??O(c),h=t.get(l)??O(l),g=w(o.kind);if(s.has(f)&&s.has(h)){Et(u,i,f,h,g);continue}s.has(f)&&r.has(l)&&At({edges:u,seen:i,source:f,currentRawId:l,outgoing:a,visible:s,bridgeableHiddenIds:r,physicalToSemantic:t,...g!==void 0?{inheritedKind:g}:{}})}return u}function At({edges:e,seen:t,source:n,currentRawId:r,outgoing:s,visible:a,bridgeableHiddenIds:i,physicalToSemantic:u,inheritedKind:o,visited:c=new Set}){if(!c.has(r)){c.add(r);for(const l of s.get(r)??[]){const f=w(l.to);if(!f)continue;const h=w(l.kind);if(h==="tracks")continue;const g=u.get(f)??O(f),m=h??o;a.has(g)?Et(e,t,n,g,m):i.has(f)&&At({edges:e,seen:t,source:n,currentRawId:f,outgoing:s,visible:a,bridgeableHiddenIds:i,physicalToSemantic:u,...m!==void 0?{inheritedKind:m}:{},visited:c})}}}function Et(e,t,n,r,s){if(n===r)return;const a=s??"dependency",i=`${n}->${r}:${a}`;t.has(i)||(t.add(i),e.push({from:n,to:r,kind:a}))}function Vr(e){const t=new Map;for(const n of e){const r=w(n.from),s=w(n.to);!r||!s||t.set(r,[...t.get(r)??[],n])}return t}function Wr(e){const t=new Set;for(const n of e.beads??[]){const r=w(n.id);if(!r)continue;(w(n.metadata?.["gc.kind"])??w(n.kind))==="scope-check"&&t.add(r)}return t}function Zr(e){const t=new Map,n=new Map,r=new Map;for(const s of e){De(t,s.id,s),De(n,s.alias,s),De(n,s.title,s),De(n,s.session_name,s);const a=w(s.template);a&&r.set(a,[...r.get(a)??[],s])}return{byId:t,byName:n,byTemplate:r}}function Xr(e,t,n={}){if(t==="pending"||t==="ready")return;const r=w(e.assignee),s=Jr(e,r),a=Yr(e,r,s);if(!s&&!a)return;const i=Qr(s,a,r),u=es(i,n.sessionIndex);if(nn.test(u.sessionId))return u}function Jr(e,t){const n=T(e,"session_id")??T(e,"gc.session_id")??T(e,"gc.sessionId")??t;return qr(n)??n}function Yr(e,t,n){return T(e,"session_name")??T(e,"gc.session_name")??T(e,"gc.sessionName")??t??n}function Qr(e,t,n){const r=t??e??"";return{sessionId:e??t??"",sessionName:r,assignee:n??r}}function qr(e){const t=w(e);if(!t)return;if(nn.test(t))return t;const n=t.match(/(?:^|[-_/])((?:gc|td|th|[a-z]{4})-[a-z0-9-]{1,32})$/)?.[1];if(!(!n||!nn.test(n)))return n}function es(e,t){if(!t)return e;const n=ns(e,t);return n?ts(n,e):e}function ns(e,t){for(const n of[e.sessionId,e.sessionName,e.assignee]){const r=w(n);if(!r)continue;const s=t.byId.get(r)??t.byName.get(r)??rs(t.byTemplate.get(r)??[]);if(s)return s}return null}function ts(e,t){return{sessionId:e.id,sessionName:w(e.alias)??w(e.title)??w(e.session_name)??w(e.template)??t.sessionName,assignee:t.assignee||w(e.template)||w(e.alias)||w(e.title)||w(e.session_name)||e.id}}function rs(e){if(e.length===0)return null;const t=e.filter(n=>n.state==="active"||n.running===!0);return t.length===1?t[0]??null:e.length===1?e[0]??null:null}function De(e,t,n){const r=w(t);!r||e.has(r)||e.set(r,n)}function Dt(e){const t=(w(e.status)??"").toLowerCase(),n=T(e,"gc.outcome")?.toLowerCase();return t==="closed"||t==="completed"||t==="done"?n==="fail"||n==="failed"?"failed":n==="skipped"?"skipped":"completed":t==="in_progress"||t==="active"||t==="running"?"active":t==="blocked"?"blocked":t==="ready"?"ready":t==="failed"?"failed":t==="skipped"?"skipped":"pending"}function ss(e,t){return e.some(n=>gn(n.status))?"active":t?.status?t.status:"pending"}function gn(e){return e==="active"||e==="running"}function as(e,t,n,r={}){const s=e.beads.map((l,f)=>os(e.semanticNodeId,l,f,r)).sort(Tt),a=us(s),i=new Set(s.map(l=>$e(l.iteration)).filter(Ue)),u=(a?$e(a.iteration):void 0)??(i.size>0?Math.max(...i):void 0),o=e.loopControlNodeId!==void 0&&u!==void 0&&n!==void 0&&ua)&&t.set(n.loopControlNodeId,s)}return t}function os(e,t,n,r){const s=w(t.id);if(s===void 0)throw new Error(`run node ${e} has a bead with an empty id`);const a=wt(t),i=kt(t),u=Dt(t),o=Xr(t,u,r);return{id:s||`${e}:iteration-${a??0}:attempt-${i??n}`,semanticNodeId:e,beadId:s,iteration:ps(a),attempt:vs(i),label:hs(a,i),status:u,session:bs(u,o),currentIteration:!0,historical:!1}}function us(e){return[...e].sort(Tt).at(-1)}function Tt(e,t){return Fn(e.iteration)-Fn(t.iteration)||xn(e.attempt)-xn(t.attempt)||e.beadId.localeCompare(t.beadId)}function cs(e,t){const n=fs(e),r=ds(e),s=ls(t);return n===0&&s===void 0?{kind:"none"}:{kind:"tracked",count:Math.max(n,1),badge:s===void 0?{kind:"count-only"}:{kind:"bounded",label:s},active:r===void 0?{kind:"idle"}:{kind:"running",value:r}}}function ls(e){const t=e.map(r=>pr(r,"gc.max_attempts")).find(r=>r!==void 0);if(t===void 0)return;const n=new Set(e.map(kt).filter(Ue));return`${Math.max(n.size,1)}/${t}`}function fs(e){return new Set(e.map(n=>mn(n.attempt)).filter(Ue)).size}function ds(e){const t=e.find(n=>gn(n.status));return t?mn(t.attempt):void 0}function hs(e,t){return e!==void 0&&t!==void 0?`iteration ${e}, attempt ${t}`:e!==void 0?`iteration ${e}`:t!==void 0?`attempt ${t}`:"base"}function gs(e){return e===void 0?{kind:"run"}:{kind:"scoped",ref:e}}function ms(e,t,n){return e===void 0||t===0?{kind:"single"}:{kind:"stacked",visibleIteration:e,iterationCount:t,control:n===void 0?{kind:"unknown"}:{kind:"known",id:n}}}function ps(e){return e===void 0?{kind:"base"}:{kind:"loop",value:e}}function vs(e){return e===void 0?{kind:"untracked"}:{kind:"attempt",value:e}}function bs(e,t){return t!==void 0?{kind:"attached",link:t,streamable:!1}:{kind:"none",reason:e==="pending"||e==="ready"?"not_started":"session_unresolved"}}function $e(e){return e.kind==="loop"?e.value:void 0}function mn(e){return e.kind==="attempt"?e.value:void 0}function Fn(e){return $e(e)??0}function xn(e){return mn(e)??0}function Ue(e){return typeof e=="number"&&Number.isFinite(e)}function ys(e,t,n){const s=[...Bn(e),...t.flatMap(a=>Bn(a)),...Gn(e),...t.flatMap(a=>Gn(a)),w(n)].find(a=>a!==void 0);return s===void 0?{kind:"unavailable",reason:"missing_cwd_and_rig_root"}:{kind:"known",path:s}}function Bn(e){return[T(e,"gc.cwd"),T(e,"cwd"),T(e,"gc.work_dir"),T(e,"work_dir")]}function Gn(e){return[T(e,"gc.rig_root"),T(e,"rig_root")]}function _s(e,t,n){const r=ws(t);return r.size===0?[...e]:e.map((s,a)=>({group:s,index:a,rank:s.semanticNodeId===n?-1:ks(s,r,t?.name)})).sort((s,a)=>s.rank-a.rank||s.index-a.index).map(s=>s.group)}function ws(e){const t=e?.preview?.nodes??e?.steps??[],n=new Map;return t.forEach((r,s)=>{for(const a of js(r.id,e?.name))n.has(a)||n.set(a,s)}),n}function ks(e,t,n){let r=Number.POSITIVE_INFINITY;for(const s of Ss(e,n)){const a=t.get(s);a!==void 0&&aNs(n,t))].flatMap(n=>pn(n))}function Ns(e,t){return[w(e.id),T(e,"gc.logical_bead_id")??w(e.logical_bead_id),T(e,"gc.step_id"),q(e)].filter(n=>n!==void 0).flatMap(n=>pn(n,t))}function js(e,t){return pn(e,t)}function pn(e,t){const n=w(e);if(!n)return[];const r=Is(n,t);return Cs([n,r,Ln(n),Ln(r)].map(s=>O(s)))}function Is(e,t){if(!t)return e;const n=`${t}.`;return e.startsWith(n)?e.slice(n.length):e}function Ln(e){return e.replace(/-scope-check$/,"").replace(/\.scope-check$/,"")}function Cs(e){return[...new Set(e)]}const As=new Set(["scope-check","run-finalize","spec"]);function vn(e){return As.has(e)||e==="control"}function Ne(e,t){const n=w(e.id);if(n&&n===t)return t;const r=T(e,"gc.logical_bead_id")??w(e.logical_bead_id);if(r)return O(r);const s=T(e,"gc.step_id");if(s)return O(s);const a=q(e);if(a){const i=Mt(a);if(i)return O(i)}return O(n??"run-node")}function Es(e,t){if(V(e,t)==="run-finalize")return t;const r=T(e,"gc.control_for");if(r){const i=zn(r);if(i)return O(i)}const s=q(e);if(!s)return null;const a=zn(s);return a?O(a):null}function V(e,t){const n=w(e.id);if(n&&n===t)return"run-root";switch(Rt(e)){case"ralph":return"check-loop";case"retry":return"retry";case"scope":case"epic":case"body":return"scope";case"fanout":return"fanout";case"condition":return"condition";case"expand":case"expansion":return"expansion";case"scope-check":return"scope-check";case"run-finalize":return"run-finalize";case"spec":return"spec";case"cleanup":return"control";default:return"step"}}function Ds(e,t){if(t==="check-loop")return"check-loop";const n=Rt(e);return n==="ralph"?"check-loop":n||t}function Ts(e,t){return Ps(w(e.title)??t.replace(/[-_]/g," "))}function Rs(e){switch(e){case"scope-check":return"scope check";case"run-finalize":return"finalize";case"check-loop":case"condition":case"control":case"expansion":case"fanout":case"retry":case"scope":case"spec":case"step":case"unknown":case"run-root":return e.replace(/-/g," ")}}function Ms(e){const t=T(e,"gc.scope_ref")??w(e.scope_ref),n=t?Kn(t,["iteration","run"]):void 0;if(n)return n;const r=q(e);if(r)return Kn(r,["iteration"])}function Rt(e){return T(e,"gc.kind")??T(e,"gc.original_kind")??w(e.kind)??""}function Mt(e){const t=e.split(".").filter(Boolean);if(t.length===0)return;const n=$s(t),r=n.lastIndexOf("iteration");return r>=0&&ri===r&&xe(n[u+1]));if(s<=0)continue;const a=n[s-1];return a?O(a):void 0}}function xe(e){if(!e)return!1;const t=Number.parseInt(e,10);return String(t)===e&&t>0}function Ps(e){return/(^|[^A-Za-z0-9])ralph(?=$|[^A-Za-z0-9])/i.test(e)?e.replace(/[-_]+/g," ").replace(/(^|[^A-Za-z0-9])ralph(?=$|[^A-Za-z0-9])/gi,"$1check loop").replace(/\s+/g," ").trim():e}function Fs(e,t){const n=new Map,r=new Map,s=new Map,a=Hs(e),i=Ls(e,t,a),u=Ks(e,t,i,a);for(const o of e){const c=w(o.id)??"",l=V(o,t),f=i.get(o)?.semanticNodeId??Ne(o,t);if(r.set(c,f),vn(l)){const g=Es(o,t),m=Vs(o,t,u,g);if(m){const p=s.get(m)??[];p.push({id:c||`${m}-${l}`,label:Rs(l),status:Dt(o)}),s.set(m,p)}continue}const h=n.get(f)??[];h.push(o),n.set(f,h)}return{groups:[...n].map(([o,c])=>xs(o,c,t)),physicalToSemantic:r,badgesByTarget:s}}function xs(e,t,n){const r=Bs(t,n),s=V(r,n),a=Un(t,r,u=>T(u,"gc.scope_ref")??w(u.scope_ref)),i=Un(t,r,Ms);return{semanticNodeId:e,title:Ts(r,e),kind:Ds(r,s),constructKind:s,beads:t,...a!==void 0?{scopeRef:a}:{},...i!==void 0?{loopControlNodeId:i}:{}}}function Bs(e,t){const[n]=[...e].sort((r,s)=>{const a=Hn(V(s,t))-Hn(V(r,t));return a!==0?a:Be(r).localeCompare(Be(s))});if(!n)throw new Error("cannot build run node group from zero beads");return n}function Un(e,t,n){return n(t)??Gs(e).map(n).find($t)}function Hn(e){switch(e){case"run-root":return 100;case"check-loop":return 90;case"retry":return 80;case"condition":case"fanout":case"scope":case"expansion":return 70;case"step":return 10;case"control":case"run-finalize":case"scope-check":case"spec":case"unknown":return 0}}function Gs(e){return[...e].sort((t,n)=>Be(t).localeCompare(Be(n)))}function Be(e){return[w(e.id),q(e),w(e.title)].filter($t).join("\0")}function Ls(e,t,n){const r=new Map,s=new Map;for(const i of e){const u=V(i,t);if(vn(u))continue;const o=Ot(i,t,n),c=zs(i,t,o,n);r.set(i,{base:o,disambiguator:c});const l=c??o,f=s.get(o)??new Set;f.add(l),s.set(o,f)}const a=new Map;for(const i of e){const u=r.get(i)??{base:Ne(i,t),disambiguator:void 0},o=s.get(u.base),c=o&&o.size>1&&u.disambiguator?u.disambiguator:u.base;a.set(i,{...u,semanticNodeId:c})}return a}function Ot(e,t,n){const r=w(e.id);if(r&&r===t)return t;const s=T(e,"gc.logical_bead_id")??w(e.logical_bead_id);if(s)return O(s);const a=V(e,t);return(a==="check-loop"||a==="retry")&&r&&n.has(r)?O(r):Ne(e,t)}function zs(e,t,n,r){const s=w(e.id);return s&&r.has(s)&&O(s)===n?n:Pt(e,t)}function Ks(e,t,n,r){const s=new Map;for(const i of e){const u=V(i,t);if(vn(u))continue;const o=n.get(i),c=o?.semanticNodeId??Ne(i,t);for(const l of Us(i,t,c,o,r)){const f=s.get(l)??new Set;f.add(c),s.set(l,f)}}const a=new Map;for(const[i,u]of s)if(u.size===1){const[o]=[...u];o&&a.set(i,o)}return a}function Us(e,t,n,r,s){return[n,Ne(e,t),r?.base??Ot(e,t,s),r?.disambiguator,Pt(e,t),T(e,"gc.step_id"),bn(q(e)),w(e.id)].filter(a=>a!==void 0).map(a=>O(a))}function $t(e){return e!==void 0}function Hs(e){const t=new Set(e.map(r=>w(r.id)).filter(r=>r!==void 0)),n=new Set;for(const r of e){const s=T(r,"gc.logical_bead_id")??w(r.logical_bead_id);s&&t.has(s)&&n.add(s)}return n}function Vs(e,t,n,r){if(V(e,t)==="run-finalize")return t;for(const s of Ws(e,r)){const a=n.get(s);if(a)return a}return r}function Ws(e,t){return[T(e,"gc.control_for"),Zs(e),t??void 0].filter(n=>n!==void 0).flatMap(n=>{const r=Ge(n);return[n,r,O(r)]}).map(n=>O(n))}function Pt(e,t){const n=w(e.id);if(n&&n===t)return t;const r=T(e,"gc.logical_bead_id")??w(e.logical_bead_id);if(r)return O(r);const s=T(e,"gc.step_id");return s?O(s):bn(q(e))}function Zs(e){const t=T(e,"gc.control_for");return t?O(Ge(t)):bn(Ge(q(e)??""))}function bn(e){const t=w(e);if(!t)return;const n=Ge(t),r=n.split(".").filter(Boolean);if(r.length!==0)return r.length===1?O(r[0]??n):O(r.slice(1).join("."))}function Ge(e){return e.replace(/-scope-check$/,"").replace(/\.scope-check$/,"")}const Vn="__run";function Xs(e){const t=new Map;for(const n of e){const r=n.scope.kind==="scoped"?n.scope.ref:Vn,s=t.get(r)??{id:r,label:r===Vn?"Run":r,nodeIds:[]};s.nodeIds.push(n.id),t.set(r,s)}return[...t.values()]}function Js(e){const{groups:t,physicalToSemantic:n,badgesByTarget:r}=Fs(e.beads,e.rootBeadId),s=_s(t,e.formulaDetail,e.rootBeadId),a=is(s),i=Zr(e.sessions??[]),u={sessionIndex:i,scopeRef:e.scopeRef},o=s.map(k=>as(k,r.get(k.semanticNodeId)??[],a.get(k.loopControlNodeId??""),u)),c=Hr(e.raw,n,o),l=zr(o,c),f=ea(e.raw,l,c),h=Qs(e.root,e.formulaDetail),g=e.formulaDetailState??qs(e.root,e.formulaDetail),m=ys(e.root,e.beads,e.rigRoot),p=e.beads.map(Ys),v=vr(p),y=h.kind==="known"?h.name:null,_=br(v,y,p),b={raw:e.raw,runId:e.runId,rootBeadId:e.rootBeadId,rootStoreRef:e.rootStoreRef,resolvedRootStore:e.resolvedRootStore,scopeKind:e.scopeKind,scopeRef:e.scopeRef,title:e.root?.title.trim()||e.runId,formula:h,formulaDetail:g,executionPath:m,beads:e.beads,nodeGroups:s,physicalToSemantic:n,badgesByTarget:r,latestIterationByLoop:a,sessionIndex:i,sessionContext:u,nodes:l,edges:c,lanes:Xs(l),progress:f,phase:v.phase,stages:_};return e.root!==void 0&&(b.root=e.root),b}function Ys(e){const t=T(e,"gc.parent_bead_id"),n={id:e.id,title:e.title,status:e.status,issue_type:e.kind,updated_at:"",metadata:e.metadata};return e.assignee!==void 0&&(n.assignee=e.assignee),t!==void 0&&(n.parent=t),n}function Qs(e,t){const n=hn("state",{root:e,formulaDetail:t});if(n.name!==null){const r=n.source==="title_fallback"?"title_fallback":"metadata";return{kind:"known",name:n.name,source:r}}return{kind:"unavailable",reason:"missing_formula_metadata"}}function qs(e,t){const n=hn("detail",{root:e,formulaDetail:t}),r=n.name;if(r===null)return{kind:"unavailable",reason:"missing_formula_metadata"};const s=n.target;return s?t!==void 0?{kind:"available",name:r,target:s}:{kind:"unavailable",reason:"fetch_failed",name:r,target:s,failure:"upstream_error"}:{kind:"unavailable",reason:"missing_run_target",name:r}}function ea(e,t,n){const r=t.filter(o=>o.visibleInGraph),s=new Set;let a=0,i=0,u=0;for(const o of t)for(const c of o.executionInstances)a+=1,c.session.kind==="attached"&&(i+=1),c.session.kind==="attached"&&c.session.streamable&&(u+=1,s.add(c.session.link.sessionId));return{snapshotVersion:e.snapshot_version,snapshotEventSeq:na(e.snapshot_event_seq),snapshotPartial:e.partial,totalNodeCount:t.length,visibleNodeCount:r.length,edgeCount:n.length,executionInstanceCount:a,sessionLinkCount:i,streamableSessionCount:u,streamableSessionIds:[...s],statusCounts:Wn(r),allStatusCounts:Wn(t)}}function na(e){return typeof e=="number"?{kind:"known",seq:e}:{kind:"unavailable",reason:"supervisor_omitted"}}function Wn(e){const t={};for(const n of e)t[n.status]=(t[n.status]??0)+1;return t}class oe extends Error{reason;constructor(t,n="invalid_snapshot"){super(t),this.name="UnsupportedRunError",this.reason=n}}function ta(e,t){if(!ra(e))throw new oe("run is not a graph.v2 run","not_run_view");const n=w(e.root_bead_id)??"",r=w(e.run_id),s=w(e.root_store_ref),a=w(e.resolved_root_store),i=sa(Array.isArray(e.beads)?e.beads:[]),u=xt(i,n),o=yr(e);if(!r||!s||!a)throw new oe("run snapshot identity is missing or invalid");if(o===null)throw new oe("run scope is missing or invalid");if(!Number.isFinite(e.snapshot_version))throw new oe("run snapshot version is missing or invalid");if(typeof e.partial!="boolean")throw new oe("run partial flag is missing or invalid");const c={raw:e,runId:r,rootBeadId:n,rootStoreRef:s,resolvedRootStore:a,scopeKind:o.scopeKind,scopeRef:o.scopeRef,beads:i};u!==void 0&&(c.root=u),t.rigRoot!==void 0&&(c.rigRoot=t.rigRoot),t.sessions!==void 0&&(c.sessions=t.sessions),t.formulaDetail!==void 0&&(c.formulaDetail=t.formulaDetail),t.formulaDetailState!==void 0&&(c.formulaDetailState=t.formulaDetailState);const l=Js(c),f=e.partial?["supervisor_snapshot_partial"]:[];return{runId:r,rootBeadId:n,rootStoreRef:s,resolvedRootStore:a,scopeKind:o.scopeKind,scopeRef:o.scopeRef,title:l.title,formula:l.formula,formulaDetail:l.formulaDetail,executionPath:l.executionPath,snapshotVersion:e.snapshot_version,snapshotEventSeq:l.progress.snapshotEventSeq,completeness:Ft(f),progress:l.progress,phase:l.phase,stages:l.stages,nodes:l.nodes,edges:l.edges,lanes:l.lanes}}function Ft(e){const t=[...new Set(e)];return t.length===0?{kind:"complete"}:{kind:"partial",reasons:t}}function ra(e){const t=xt(Array.isArray(e.beads)?e.beads:[],e.root_bead_id);return T(t,"gc.formula_contract")==="graph.v2"}function xt(e,t){const n=w(t);if(n)return e.find(r=>w(r.id)===n)}function sa(e){const t=new Set,n=[];for(const r of e){const s=w(r.id);if(s){if(t.has(s))continue;t.add(s)}n.push(r)}return n}const Zn={pending:"pending",ready:"ready",running:"running",active:"running",done:"done",completed:"done",failed:"failed",blocked:"blocked",skipped:"skipped"};function aa({node:e,selected:t,onToggle:n}){const r=ua(e.constructKind),s=ca(e.status),a=e.iterationSummary.kind==="stacked"?`${e.iterationSummary.iterationCount} iterations, showing ${e.iterationSummary.visibleIteration}`:null,i=e.attemptSummary.kind==="tracked"&&e.attemptSummary.badge.kind==="bounded"?` · attempt ${e.attemptSummary.badge.label}${ia(e)}`:"";return d.jsxs("button",{type:"button","aria-pressed":t,onClick:()=>n(e.id),className:`focus-mark w-full text-left px-4 py-3 bg-transparent transition-colors duration-150 ease-out-quart ${r} ${t?"text-fg border-accent bg-surface-tint ring-2 ring-accent/45 ring-offset-2 ring-offset-surface":"text-fg border-rule hover:border-fg-faint hover:bg-surface-tint"}`,children:[d.jsxs("div",{className:"flex items-start justify-between gap-3",children:[d.jsxs("div",{children:[d.jsx("p",{className:"text-body text-fg leading-snug",children:e.title}),d.jsxs("p",{className:"mt-1 text-label uppercase tracking-wider text-fg-faint",children:[oa(e.constructKind),i]})]}),d.jsxs("span",{className:`text-label uppercase tracking-wider shrink-0 ${s}`,children:[la(e.status)," ",Zn[e.status]]})]}),a&&d.jsxs("p",{className:"mt-2 text-label uppercase tracking-wider text-fg-faint tnum",children:["stacked history: ",a]}),e.controlBadges.length>0&&d.jsx("div",{className:"mt-2 flex flex-wrap gap-2",children:e.controlBadges.map(u=>d.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-muted border border-rule px-1.5 py-0.5",children:[u.label,": ",Zn[u.status]]},u.id))})]})}function ia(e){return e.attemptSummary.kind==="tracked"&&e.attemptSummary.active.kind==="running"?` · running attempt ${e.attemptSummary.active.value}`:""}function oa(e){switch(e){case"run-root":return"run root";case"run-finalize":return"finalize";case"step":case"retry":case"check-loop":case"scope":case"condition":case"fanout":case"expansion":case"scope-check":case"spec":case"control":case"unknown":return e.replace(/-/g," ")}}function ua(e){switch(e){case"run-root":return"formula-run-node-shape-root";case"step":case"unknown":return"formula-run-node-shape-step";case"retry":return"formula-run-node-shape-retry";case"check-loop":return"formula-run-node-shape-check-loop";case"scope":return"formula-run-node-shape-scope";case"condition":return"formula-run-node-shape-condition";case"fanout":return"formula-run-node-shape-fanout";case"expansion":return"formula-run-node-shape-expansion";case"scope-check":case"run-finalize":case"spec":case"control":return"formula-run-node-shape-control"}}function ca(e){switch(e){case"failed":case"blocked":return"text-accent";case"active":case"running":case"ready":return"text-fg";case"completed":case"done":return"text-fg-muted";case"pending":case"skipped":return"text-fg-faint"}}function la(e){switch(e){case"completed":case"done":return"✓";case"active":case"running":return"●";case"failed":case"blocked":return"!";case"skipped":return"∅";case"pending":case"ready":return"·"}}function fa({detail:e,selectedNodeId:t,onToggleNode:n}){const r=da(e),s=ha(e);return r.length===0?d.jsx("p",{className:"text-body text-fg-muted italic",children:"No graph nodes have materialized for this formula run."}):d.jsxs("section",{"aria-label":"Formula run graph",children:[d.jsx("div",{className:"flex items-baseline justify-between gap-4",children:d.jsx("h2",{className:"text-title text-fg",children:"Formula Graph"})}),d.jsx("ol",{className:"mt-5 space-y-3 relative",children:r.map((a,i)=>{const u=s.get(a.id),o=i>0?s.get(r[i-1]?.id??""):void 0,c=u!==void 0&&u!==o;return d.jsxs("li",{className:"relative pl-6",children:[c&&d.jsx("p",{className:"mb-1 text-label uppercase tracking-wider text-fg-faint",children:u}),it.visibleInGraph!==!1)}function ha(e){const t=new Map;for(const n of e.lanes)for(const r of n.nodeIds)t.set(r,n.label);return t}function Xn(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable}))),n.push.apply(n,r)}return n}function $(e){for(var t=1;t=0||(l[o]=i[o]);return l})(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}function z(e,t){return ma(e)||(function(n,r){var s=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(s!=null){var a,i,u,o,c=[],l=!0,f=!1;try{if(u=(s=s.call(n)).next,r===0){if(Object(s)!==s)return;l=!1}else for(;!(l=(a=u.call(s)).done)&&(c.push(a.value),c.length!==r);l=!0);}catch(h){f=!0,i=h}finally{try{if(!l&&s.return!=null&&(o=s.return(),Object(o)!==o))return}finally{if(f)throw i}}return c}})(e,t)||yn(e,t)||va()}function ga(e){return(function(t){if(Array.isArray(t))return rn(t)})(e)||pa(e)||yn(e)||(function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)})()}function ma(e){if(Array.isArray(e))return e}function pa(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function yn(e,t){if(e){if(typeof e=="string")return rn(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?rn(e,t):void 0}}function rn(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(o){throw o},f:s}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var a,i=!0,u=!1;return{s:function(){n=n.call(e)},n:function(){var o=n.next();return i=o.done,o},e:function(o){u=!0,a=o},f:function(){try{i||n.return==null||n.return()}finally{if(u)throw a}}}}var Te=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function je(e,t){return e(t={exports:{}},t.exports),t.exports}var L=je((function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],s=0;s-1?y.slice(0,b):k;switch(k){case"diff":p--;break e;case"deleted":case"new":var S=y.slice(b+1);S.indexOf("file mode")===0&&(i[k==="new"?"newMode":"oldMode"]=S.slice(10));break;case"similarity":i.similarity=parseInt(y.split(" ")[2],10);break;case"index":var j=y.slice(b+1).split(" "),N=j[0].split("..");i.oldRevision=N[0],i.newRevision=N[1],j[1]&&(i.oldMode=i.newMode=j[1]);break;case"copy":case"rename":var I=y.slice(b+1);I.indexOf("from")===0?i.oldPath=I.slice(5):i.newPath=I.slice(3),_=k;break;case"---":var C=y.slice(b+1),E=g[++p].slice(4);C==="/dev/null"?(E=E.slice(2),_="add"):E==="/dev/null"?(C=C.slice(2),_="delete"):(_="modify",C=C.slice(2),E=E.slice(2)),C&&(i.oldPath=C),E&&(i.newPath=E),h=5;break e}}i.type=_||"modify"}else if(v.indexOf("Binary")===0)i.isBinary=!0,i.type=v.indexOf("/dev/null and")>=0?"add":v.indexOf("and /dev/null")>=0?"delete":"modify",h=2,i=null;else if(h===5)if(v.indexOf("@@")===0){var D=/^@@\s+-([0-9]+)(,([0-9]+))?\s+\+([0-9]+)(,([0-9]+))?/.exec(v);u={content:v,oldStart:D[1]-0,newStart:D[4]-0,oldLines:D[3]-0||1,newLines:D[6]-0||1,changes:[]},i.hunks.push(u),o=u.oldStart,c=u.newStart}else{var B=v.slice(0,1),M={content:v.slice(1)};switch(B){case"+":M.type="insert",M.isInsert=!0,M.lineNumber=c,c++;break;case"-":M.type="delete",M.isDelete=!0,M.lineNumber=o,o++;break;case" ":M.type="normal",M.isNormal=!0,M.oldLineNumber=o,M.newLineNumber=c,o++,c++;break;case"\\":var P=u.changes[u.changes.length-1];P.isDelete||(i.newEndingNewLine=!1),P.isInsert||(i.oldEndingNewLine=!1)}M.type&&u.changes.push(M)}p++}return f}};e.exports=s})()}));function Ie(e){return e.type==="insert"}function Y(e){return e.type==="delete"}function ye(e){return e.type==="normal"}function ka(e,t){var n=t.nearbySequences==="zip"?(function(r){var s=r.reduce((function(a,i,u){var o=z(a,3),c=o[0],l=o[1],f=o[2];return l?Ie(i)&&f>=0?(c.splice(f+1,0,i),[c,i,f+2]):(c.push(i),[c,i,Y(i)&&Y(l)?f:u]):(c.push(i),[c,i,Y(i)?u:-1])}),[[],null,-1]);return z(s,1)[0]})(e.changes):e.changes;return $($({},e),{},{isPlain:!1,changes:n})}function Sa(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=(function(r){if(r.startsWith("diff --git"))return r;var s=r.indexOf(` -`),a=r.indexOf(` -`,s+1),i=r.slice(0,s),u=r.slice(s+1,a),o=i.split(" ").slice(1,-3).join(" "),c=u.split(" ").slice(1,-3).join(" ");return["diff --git a/".concat(o," b/").concat(c),"index 1111111..2222222 100644","--- a/".concat(o),"+++ b/".concat(c),r.slice(a+1)].join(` -`)})(e.trimStart());return wa.parse(n).map((function(r){return(function(s,a){var i=s.hunks.map((function(u){return ka(u,a)}));return $($({},s),{},{hunks:i})})(r,t)}))}function Na(e){return e[0]}function ja(e){return e[e.length-1]}function sn(e){return["".concat(e,"Start"),"".concat(e,"Lines")]}function _e(e){return e==="old"?function(t){return Ie(t)?-1:ye(t)?t.oldLineNumber:t.lineNumber}:function(t){return Y(t)?-1:ye(t)?t.newLineNumber:t.lineNumber}}function Gt(e,t){return function(n,r){var s=n[e],a=s+n[t];return r>=s&&r=a&&s-1},Ra=function(e,t){var n=this.__data__,r=He(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this};function ue(e){var t=-1,n=e==null?0:e.length;for(this.clear();++tu))return!1;var c=a.get(e),l=a.get(t);if(c&&l)return c==t&&l==e;var f=-1,h=!0,g=2&n?new mi:void 0;for(a.set(e,t),a.set(t,e);++f-1&&e%1==0&&e-1&&e%1==0&&e<=9007199254740991},R={};R["[object Float32Array]"]=R["[object Float64Array]"]=R["[object Int8Array]"]=R["[object Int16Array]"]=R["[object Int32Array]"]=R["[object Uint8Array]"]=R["[object Uint8ClampedArray]"]=R["[object Uint16Array]"]=R["[object Uint32Array]"]=!0,R["[object Arguments]"]=R["[object Array]"]=R["[object ArrayBuffer]"]=R["[object Boolean]"]=R["[object DataView]"]=R["[object Date]"]=R["[object Error]"]=R["[object Function]"]=R["[object Map]"]=R["[object Number]"]=R["[object Object]"]=R["[object RegExp]"]=R["[object Set]"]=R["[object String]"]=R["[object WeakMap]"]=!1;var Ri=function(e){return ge(e)&&kn(e.length)&&!!R[me(e)]},Mi=function(e){return function(t){return e(t)}},rt=je((function(e,t){var n=t&&!t.nodeType&&t,r=n&&e&&!e.nodeType&&e,s=r&&r.exports===n&&Kt.process,a=(function(){try{var i=r&&r.require&&r.require("util").types;return i||s&&s.binding&&s.binding("util")}catch{}})();e.exports=a})),st=rt&&rt.isTypedArray,Jt=st?Mi(st):Ri,Oi=Object.prototype.hasOwnProperty,$i=function(e,t){var n=W(e),r=!n&&Zt(e),s=!n&&!r&&an(e),a=!n&&!r&&!s&&Jt(e),i=n||r||s||a,u=i?Ci(e.length,String):[],o=u.length;for(var c in e)!Oi.call(e,c)||i&&(c=="length"||s&&(c=="offset"||c=="parent")||a&&(c=="buffer"||c=="byteLength"||c=="byteOffset")||Xt(c,o))||u.push(c);return u},Pi=Object.prototype,Fi=function(e){var t=e&&e.constructor;return e===(typeof t=="function"&&t.prototype||Pi)},xi=(function(e,t){return function(n){return e(t(n))}})(Object.keys,Object),Bi=Object.prototype.hasOwnProperty,Gi=function(e){if(!Fi(e))return xi(e);var t=[];for(var n in Object(e))Bi.call(e,n)&&n!="constructor"&&t.push(n);return t},Li=function(e){return e!=null&&kn(e.length)&&!Ht(e)},Sn=function(e){return Li(e)?$i(e):Gi(e)},at=function(e){return ki(e,Sn,Ii)},zi=Object.prototype.hasOwnProperty,Ki=function(e,t,n,r,s,a){var i=1&n,u=at(e),o=u.length;if(o!=at(t).length&&!i)return!1;for(var c=o;c--;){var l=u[c];if(!(i?l in t:zi.call(t,l)))return!1}var f=a.get(e),h=a.get(t);if(f&&h)return f==t&&h==e;var g=!0;a.set(e,t),a.set(t,e);for(var m=i;++c1)return!1;if(e.length===1){var t=z(e,1)[0];return t.type==="text"&&!t.value}return!0}function jo(e){var t=e.changeKey,n=e.text,r=e.tokens,s=e.renderToken,a=he(e,So),i=s?function(u,o){return s(u,ft,o)}:ft;return d.jsx("td",$($({},a),{},{"data-change-key":t,children:r?No(r)?" ":r.map(i):n||" "}))}var rr=A.memo(jo);function sr(e,t){return function(){var n=t==="old"?Cn(e):An(e);return n===-1?void 0:n}}function ar(e,t){return function(n){return e&&n?d.jsx("a",{href:t?"#"+t:void 0,children:n}):n}}function Le(e,t){return t?function(n){e(),t(n)}:e}function dt(e,t,n,r){return A.useMemo((function(){var s=tr(e,(function(a){return function(i){return a&&a(t,i)}}));return s.onMouseEnter=Le(n,s.onMouseEnter),s.onMouseLeave=Le(r,s.onMouseLeave),s}),[e,n,r,t])}function ht(e,t,n,r,s,a,i,u,o){var c={change:t,side:r,inHoverState:u,renderDefault:sr(t,r),wrapInAnchor:ar(s,a)};return d.jsx("td",$($({className:e},i),{},{"data-change-key":n,children:o(c)}))}function Io(e){var t,n,r,s=e.change,a=e.selected,i=e.tokens,u=e.className,o=e.generateLineClassName,c=e.gutterClassName,l=e.codeClassName,f=e.gutterEvents,h=e.codeEvents,g=e.hideGutter,m=e.gutterAnchor,p=e.generateAnchorID,v=e.renderToken,y=e.renderGutter,_=s.type,b=s.content,k=Q(s),S=(t=z(A.useState(!1),2),n=t[0],r=t[1],[n,A.useCallback((function(){return r(!0)}),[]),A.useCallback((function(){return r(!1)}),[])]),j=z(S,3),N=j[0],I=j[1],C=j[2],E=A.useMemo((function(){return{change:s}}),[s]),D=dt(f,E,I,C),B=dt(h,E,I,C),M=p(s),P=o({changes:[s],defaultGenerate:function(){return u}}),G=L("diff-gutter","diff-gutter-".concat(_),c,{"diff-gutter-selected":a}),ae=L("diff-code","diff-code-".concat(_),l,{"diff-code-selected":a});return d.jsxs("tr",{id:M,className:L("diff-line",P),children:[!g&&ht(G,s,k,"old",m,M,D,N,y),!g&&ht(G,s,k,"new",m,M,D,N,y),d.jsx(rr,$({className:ae,changeKey:k,text:b,tokens:i,renderToken:v},B))]})}var Co=A.memo(Io);function Ao(e){var t=e.hideGutter,n=e.element;return d.jsx("tr",{className:"diff-widget",children:d.jsx("td",{colSpan:t?1:3,className:"diff-widget-content",children:n})})}var Eo=["hideGutter","selectedChanges","tokens","lineClassName"],Do=["hunk","widgets","className"];function To(e){var t=e.hunk,n=e.widgets,r=e.className,s=he(e,Do),a=(function(i,u){return i.reduce((function(o,c){var l=Q(c);o.push(["change",l,c]);var f=u[l];return f&&o.push(["widget",l,f]),o}),[])})(t.changes,n);return d.jsx("tbody",{className:L("diff-hunk",r),children:a.map((function(i){return(function(u,o){var c=z(u,3),l=c[0],f=c[1],h=c[2],g=o.hideGutter,m=o.selectedChanges,p=o.tokens,v=o.lineClassName,y=he(o,Eo);if(l==="change"){var _=Y(h)?"old":"new",b=Y(h)?Cn(h):An(h),k=p?p[_][b-1]:null;return d.jsx(Co,$({className:v,change:h,hideGutter:g,selected:m.includes(f),tokens:k},y),"change".concat(f))}return l==="widget"?d.jsx(Ao,{hideGutter:g,element:h},"widget".concat(f)):null})(i,s)}))})}var ir=0;function Me(e,t,n,r){var s=A.useCallback((function(){return t(e)}),[e,t]),a=A.useCallback((function(){return t("")}),[t]);return A.useMemo((function(){var i=tr(r,(function(u){return function(o){return u&&u({side:e,change:n},o)}}));return i.onMouseEnter=Le(s,i.onMouseEnter),i.onMouseLeave=Le(a,i.onMouseLeave),i}),[n,r,s,e,a])}function en(e){var t=e.change,n=e.side,r=e.selected,s=e.tokens,a=e.gutterClassName,i=e.codeClassName,u=e.gutterEvents,o=e.codeEvents,c=e.anchorID,l=e.gutterAnchor,f=e.gutterAnchorTarget,h=e.hideGutter,g=e.hover,m=e.renderToken,p=e.renderGutter;if(!t){var v=L("diff-gutter","diff-gutter-omit",a),y=L("diff-code","diff-code-omit",i);return[!h&&d.jsx("td",{className:v},"gutter"),d.jsx("td",{className:y},"code")]}var _=t.type,b=t.content,k=Q(t),S=n===ir?"old":"new",j=$({id:c||void 0,className:L("diff-gutter","diff-gutter-".concat(_),tn({"diff-gutter-selected":r},"diff-line-hover-"+S,g),a),children:p({change:t,side:S,inHoverState:g,renderDefault:sr(t,S),wrapInAnchor:ar(l,f)})},u),N=L("diff-code","diff-code-".concat(_),tn({"diff-code-selected":r},"diff-line-hover-"+S,g),i);return[!h&&d.jsx("td",$($({},j),{},{"data-change-key":k}),"gutter"),d.jsx(rr,$({className:N,changeKey:k,text:b,tokens:s,renderToken:m},o),"code")]}function Ro(e){var t=e.className,n=e.oldChange,r=e.newChange,s=e.oldSelected,a=e.newSelected,i=e.oldTokens,u=e.newTokens,o=e.monotonous,c=e.gutterClassName,l=e.codeClassName,f=e.gutterEvents,h=e.codeEvents,g=e.hideGutter,m=e.generateAnchorID,p=e.generateLineClassName,v=e.gutterAnchor,y=e.renderToken,_=e.renderGutter,b=z(A.useState(""),2),k=b[0],S=b[1],j=Me("old",S,n,f),N=Me("new",S,r,f),I=Me("old",S,n,h),C=Me("new",S,r,h),E=n&&m(n),D=r&&m(r),B=p({changes:[n,r],defaultGenerate:function(){return t}}),M={monotonous:o,hideGutter:g,gutterClassName:c,codeClassName:l,gutterEvents:f,codeEvents:h,renderToken:y,renderGutter:_},P=$($({},M),{},{change:n,side:ir,selected:s,tokens:i,gutterEvents:j,codeEvents:I,anchorID:E,gutterAnchor:v,gutterAnchorTarget:E,hover:k==="old"}),G=$($({},M),{},{change:r,side:1,selected:a,tokens:u,gutterEvents:N,codeEvents:C,anchorID:n===r?null:D,gutterAnchor:v,gutterAnchorTarget:n===r?E:D,hover:k==="new"});if(o)return d.jsx("tr",{className:L("diff-line",B),children:en(n?P:G)});var ae=(function(ie,pe){return ie&&!pe?"diff-line-old-only":!ie&&pe?"diff-line-new-only":ie===pe?"diff-line-normal":"diff-line-compare"})(n,r);return d.jsxs("tr",{className:L("diff-line",ae,B),children:[en(P),en(G)]})}var Mo=A.memo(Ro);function Oo(e){var t=e.hideGutter,n=e.oldElement,r=e.newElement;return e.monotonous?d.jsx("tr",{className:"diff-widget",children:d.jsx("td",{colSpan:t?1:2,className:"diff-widget-content",children:n||r})}):n===r?d.jsx("tr",{className:"diff-widget",children:d.jsx("td",{colSpan:t?2:4,className:"diff-widget-content",children:n})}):d.jsxs("tr",{className:"diff-widget",children:[d.jsx("td",{colSpan:t?1:2,className:"diff-widget-content",children:n}),d.jsx("td",{colSpan:t?1:2,className:"diff-widget-content",children:r})]})}var $o=["selectedChanges","monotonous","hideGutter","tokens","lineClassName"],Po=["hunk","widgets","className"];function Oe(e,t){return(e?Q(e):"00")+(t?Q(t):"00")}function Fo(e){var t=e.hunk,n=e.widgets,r=e.className,s=he(e,Po),a=(function(i,u){for(var o=function(y){if(!y)return null;var _=Q(y);return u[_]||null},c=[],l=0;lr.length?n:r,o=n.length>r.length?r:n,c=u.indexOf(o);if(c!=-1)return i=[new t.Diff(1,u.substring(0,c)),new t.Diff(0,o),new t.Diff(1,u.substring(c+o.length))],n.length>r.length&&(i[0][0]=i[2][0]=-1),i;if(o.length==1)return[new t.Diff(-1,n),new t.Diff(1,r)];var l=this.diff_halfMatch_(n,r);if(l){var f=l[0],h=l[1],g=l[2],m=l[3],p=l[4],v=this.diff_main(f,g,s,a),y=this.diff_main(h,m,s,a);return v.concat([new t.Diff(0,p)],y)}return s&&n.length>100&&r.length>100?this.diff_lineMode_(n,r,a):this.diff_bisect_(n,r,a)},t.prototype.diff_lineMode_=function(n,r,s){var a=this.diff_linesToChars_(n,r);n=a.chars1,r=a.chars2;var i=a.lineArray,u=this.diff_main(n,r,!1,s);this.diff_charsToLines_(u,i),this.diff_cleanupSemantic(u),u.push(new t.Diff(0,""));for(var o=0,c=0,l=0,f="",h="";o=1&&l>=1){u.splice(o-c-l,c+l),o=o-c-l;for(var g=this.diff_main(f,h,!1,s),m=g.length-1;m>=0;m--)u.splice(o,0,g[m]);o+=g.length}l=0,c=0,f="",h=""}o++}return u.pop(),u},t.prototype.diff_bisect_=function(n,r,s){for(var a=n.length,i=r.length,u=Math.ceil((a+i)/2),o=u,c=2*u,l=new Array(c),f=new Array(c),h=0;hs);b++){for(var k=-b+p;k<=b-v;k+=2){for(var S=o+k,j=(D=k==-b||k!=b&&l[S-1]a)v+=2;else if(j>i)p+=2;else if(m&&(C=o+g-k)>=0&&C=(I=a-f[C]))return this.diff_bisectSplit_(n,r,D,j,s)}for(var N=-b+y;N<=b-_;N+=2){for(var I,C=o+N,E=(I=N==-b||N!=b&&f[C-1]a)_+=2;else if(E>i)y+=2;else if(!m&&(S=o+g-N)>=0&&S=(I=a-I))return this.diff_bisectSplit_(n,r,D,j,s)}}}return[new t.Diff(-1,n),new t.Diff(1,r)]},t.prototype.diff_bisectSplit_=function(n,r,s,a,i){var u=n.substring(0,s),o=r.substring(0,a),c=n.substring(s),l=r.substring(a),f=this.diff_main(u,o,!1,i),h=this.diff_main(c,l,!1,i);return f.concat(h)},t.prototype.diff_linesToChars_=function(n,r){var s=[],a={};function i(c){for(var l="",f=0,h=-1,g=s.length;ha?n=n.substring(s-a):sr.length?n:r,a=n.length>r.length?r:n;if(s.length<4||2*a.length=p.length?[_,b,k,S,I]:null}var o,c,l,f,h,g=u(s,a,Math.ceil(s.length/4)),m=u(s,a,Math.ceil(s.length/2));return g||m?(o=m?g&&g[4].length>m[4].length?g:m:g,n.length>r.length?(c=o[0],l=o[1],f=o[2],h=o[3]):(f=o[0],h=o[1],c=o[2],l=o[3]),[c,l,f,h,o[4]]):null},t.prototype.diff_cleanupSemantic=function(n){for(var r=!1,s=[],a=0,i=null,u=0,o=0,c=0,l=0,f=0;u0?s[a-1]:-1,o=0,c=0,l=0,f=0,i=null,r=!0)),u++;for(r&&this.diff_cleanupMerge(n),this.diff_cleanupSemanticLossless(n),u=1;u=p?(m>=h.length/2||m>=g.length/2)&&(n.splice(u,0,new t.Diff(0,g.substring(0,m))),n[u-1][1]=h.substring(0,h.length-m),n[u+1][1]=g.substring(m),u++):(p>=h.length/2||p>=g.length/2)&&(n.splice(u,0,new t.Diff(0,h.substring(0,p))),n[u-1][0]=1,n[u-1][1]=g.substring(0,g.length-p),n[u+1][0]=-1,n[u+1][1]=h.substring(p),u++),u++}u++}},t.prototype.diff_cleanupSemanticLossless=function(n){function r(p,v){if(!p||!v)return 6;var y=p.charAt(p.length-1),_=v.charAt(0),b=y.match(t.nonAlphaNumericRegex_),k=_.match(t.nonAlphaNumericRegex_),S=b&&y.match(t.whitespaceRegex_),j=k&&_.match(t.whitespaceRegex_),N=S&&y.match(t.linebreakRegex_),I=j&&_.match(t.linebreakRegex_),C=N&&p.match(t.blanklineEndRegex_),E=I&&v.match(t.blanklineStartRegex_);return C||E?5:N||I?4:b&&!S&&j?3:S||j?2:b||k?1:0}for(var s=1;s=g&&(g=m,l=a,f=i,h=u)}n[s-1][1]!=l&&(l?n[s-1][1]=l:(n.splice(s-1,1),s--),n[s][1]=f,h?n[s+1][1]=h:(n.splice(s+1,1),s--))}s++}},t.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/,t.whitespaceRegex_=/\s/,t.linebreakRegex_=/[\r\n]/,t.blanklineEndRegex_=/\n\r?\n$/,t.blanklineStartRegex_=/^\r?\n\r?\n/,t.prototype.diff_cleanupEfficiency=function(n){for(var r=!1,s=[],a=0,i=null,u=0,o=!1,c=!1,l=!1,f=!1;u0?s[a-1]:-1,l=f=!1),r=!0)),u++;r&&this.diff_cleanupMerge(n)},t.prototype.diff_cleanupMerge=function(n){n.push(new t.Diff(0,""));for(var r,s=0,a=0,i=0,u="",o="";s1?(a!==0&&i!==0&&((r=this.diff_commonPrefix(o,u))!==0&&(s-a-i>0&&n[s-a-i-1][0]==0?n[s-a-i-1][1]+=o.substring(0,r):(n.splice(0,0,new t.Diff(0,o.substring(0,r))),s++),o=o.substring(r),u=u.substring(r)),(r=this.diff_commonSuffix(o,u))!==0&&(n[s][1]=o.substring(o.length-r)+n[s][1],o=o.substring(0,o.length-r),u=u.substring(0,u.length-r))),s-=a+i,n.splice(s,a+i),u.length&&(n.splice(s,0,new t.Diff(-1,u)),s++),o.length&&(n.splice(s,0,new t.Diff(1,o)),s++),s++):s!==0&&n[s-1][0]==0?(n[s-1][1]+=n[s][1],n.splice(s,1)):s++,i=0,a=0,u="",o=""}n[n.length-1][1]===""&&n.pop();var c=!1;for(s=1;sr));s++)u=a,o=i;return n.length!=s&&n[s][0]===-1?o:o+(r-u)},t.prototype.diff_prettyHtml=function(n){for(var r=[],s=/&/g,a=//g,u=/\n/g,o=0;o");switch(c){case 1:r[o]=''+l+"";break;case-1:r[o]=''+l+"";break;case 0:r[o]=""+l+""}}return r.join("")},t.prototype.diff_text1=function(n){for(var r=[],s=0;sthis.Match_MaxBits)throw new Error("Pattern too long for this browser.");var a=this.match_alphabet_(r),i=this;function u(j,N){var I=j/r.length,C=Math.abs(s-N);return i.Match_Distance?I+C/i.Match_Distance:C?1:I}var o=this.Match_Threshold,c=n.indexOf(r,s);c!=-1&&(o=Math.min(u(0,c),o),(c=n.lastIndexOf(r,s+r.length))!=-1&&(o=Math.min(u(0,c),o)));var l,f,h=1<=v;b--){var k=a[n.charAt(b-1)];if(_[b]=p===0?(_[b+1]<<1|1)&k:(_[b+1]<<1|1)&k|(g[b+1]|g[b])<<1|1|g[b+1],_[b]&h){var S=u(p,b-1);if(S<=o){if(o=S,!((c=b-1)>s))break;v=Math.max(1,2*s-c)}}}if(u(p+1,s)>o)break;g=_}return c},t.prototype.match_alphabet_=function(n){for(var r={},s=0;s2&&(this.diff_cleanupSemantic(i),this.diff_cleanupEfficiency(i));else if(n&&typeof n=="object"&&r===void 0&&s===void 0)i=n,a=this.diff_text1(i);else if(typeof n=="string"&&r&&typeof r=="object"&&s===void 0)a=n,i=r;else{if(typeof n!="string"||typeof r!="string"||!s||typeof s!="object")throw new Error("Unknown call format to patch_make.");a=n,i=s}if(i.length===0)return[];for(var u=[],o=new t.patch_obj,c=0,l=0,f=0,h=a,g=a,m=0;m=2*this.Patch_Margin&&c&&(this.patch_addContext_(o,h),u.push(o),o=new t.patch_obj,c=0,h=g,l=f)}p!==1&&(l+=v.length),p!==-1&&(f+=v.length)}return c&&(this.patch_addContext_(o,h),u.push(o)),u},t.prototype.patch_deepCopy=function(n){for(var r=[],s=0;sthis.Match_MaxBits?(o=this.match_main(r,f.substring(0,this.Match_MaxBits),l))!=-1&&((h=this.match_main(r,f.substring(f.length-this.Match_MaxBits),l+f.length-this.Match_MaxBits))==-1||o>=h)&&(o=-1):o=this.match_main(r,f,l),o==-1)i[u]=!1,a-=n[u].length2-n[u].length1;else if(i[u]=!0,a=o-l,f==(c=h==-1?r.substring(o,o+f.length):r.substring(o,h+this.Match_MaxBits)))r=r.substring(0,o)+this.diff_text2(n[u].diffs)+r.substring(o+f.length);else{var g=this.diff_main(f,c,!1);if(f.length>this.Match_MaxBits&&this.diff_levenshtein(g)/f.length>this.Patch_DeleteThreshold)i[u]=!1;else{this.diff_cleanupSemanticLossless(g);for(var m,p=0,v=0;vu[0][1].length){var o=r-u[0][1].length;u[0][1]=s.substring(u[0][1].length)+u[0][1],i.start1-=o,i.start2-=o,i.length1+=o,i.length2+=o}return(u=(i=n[n.length-1]).diffs).length==0||u[u.length-1][0]!=0?(u.push(new t.Diff(0,s)),i.length1+=r,i.length2+=r):r>u[u.length-1][1].length&&(o=r-u[u.length-1][1].length,u[u.length-1][1]+=s.substring(0,o),i.length1+=o,i.length2+=o),s},t.prototype.patch_splitMax=function(n){for(var r=this.Match_MaxBits,s=0;s2*r?(c.length1+=h.length,i+=h.length,l=!1,c.diffs.push(new t.Diff(f,h)),a.diffs.shift()):(h=h.substring(0,r-c.length1-this.Patch_Margin),c.length1+=h.length,i+=h.length,f===0?(c.length2+=h.length,u+=h.length):l=!1,c.diffs.push(new t.Diff(f,h)),h==a.diffs[0][1]?a.diffs.shift():a.diffs[0][1]=a.diffs[0][1].substring(h.length))}o=(o=this.diff_text2(c.diffs)).substring(o.length-this.Patch_Margin);var g=this.diff_text1(a.diffs).substring(0,this.Patch_Margin);g!==""&&(c.length1+=g.length,c.length2+=g.length,c.diffs.length!==0&&c.diffs[c.diffs.length-1][0]===0?c.diffs[c.diffs.length-1][1]+=g:c.diffs.push(new t.Diff(0,g))),l||n.splice(++s,0,c)}}},t.prototype.patch_toText=function(n){for(var r=[],s=0;sVo(e.patch),[e.patch]);return d.jsxs("section",{children:[d.jsxs("div",{className:"flex items-baseline justify-between gap-4 flex-wrap",children:[d.jsx("h3",{className:"text-body font-semibold text-fg",children:"Local Changes"}),d.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:[e.changedFiles.length," changed file",e.changedFiles.length===1?"":"s"]})]}),e.rootPath.kind==="known"&&d.jsx("p",{className:"mt-1 text-label text-fg-faint break-all",children:e.rootPath.path}),d.jsx("p",{className:"mt-2 text-label uppercase tracking-wider text-fg-muted",children:Zo(e.comparison)}),t.length===0?d.jsx("p",{className:"mt-5 text-body text-fg-muted italic",children:"No renderable patch in this work tree."}):d.jsx("div",{className:"formula-run-diff-view mt-5 space-y-3",children:t.map(n=>d.jsx(Ho,{file:n},`${n.oldRevision}:${n.newRevision}:${ur(n)}`))}),e.truncated&&d.jsx("p",{className:"mt-3 text-label uppercase tracking-wider text-fg-faint",children:"Diff truncated at the backend output cap."})]})}function Ho({file:e}){const t=Xo(e.hunks);return d.jsxs("details",{className:"border-y border-rule py-2",open:!0,children:[d.jsxs("summary",{className:"cursor-pointer list-none text-label uppercase tracking-wider text-fg-muted",children:[d.jsx("span",{className:"font-medium normal-case tracking-normal text-body text-fg",children:ur(e)}),d.jsxs("span",{className:"ml-3 tnum text-fg-faint",children:["+",t.additions," -",t.deletions]})]}),e.hunks.length===0||e.isBinary?d.jsx("p",{className:"mt-3 text-body text-fg-muted italic",children:"No textual hunks."}):d.jsx("div",{className:"mt-3 overflow-auto",children:d.jsx(zo,{viewType:"unified",diffType:e.type,hunks:e.hunks,renderGutter:Wo,children:n=>n.map(r=>d.jsx(or,{hunk:r},Jo(r)))})})]})}function Vo(e){if(e.trim().length===0)return[];try{return Sa(e,{nearbySequences:"zip"})}catch{return[]}}function Wo({change:e,side:t,renderDefault:n}){return e.type==="insert"&&t==="old"?d.jsx("span",{className:"diff-gutter-sign","aria-hidden":!0,children:"+"}):e.type==="delete"&&t==="new"?d.jsx("span",{className:"diff-gutter-sign","aria-hidden":!0,children:"-"}):n()}function Zo(e){return e.kind==="upstream"?`Compared with ${e.ref} at ${e.mergeBase.slice(0,12)}.`:e.kind==="head"&&e.reason==="no_upstream"?"No upstream branch is configured; showing changes relative to HEAD plus untracked files.":e.kind==="head"?"Upstream comparison failed; showing changes relative to HEAD plus untracked files.":"Comparison unavailable."}function ur(e){const t=mt(e.oldPath),n=mt(e.newPath);return e.type==="delete"?t:e.type==="rename"&&t!==n?`${t} -> ${n}`:n||t}function mt(e){return e.replace(/^[ab]\//,"")}function Xo(e){let t=0,n=0;for(const r of e)for(const s of r.changes)s.type==="insert"&&(t+=1),s.type==="delete"&&(n+=1);return{additions:t,deletions:n}}function Jo(e){return`${e.oldStart}:${e.newStart}:${e.content}`}function Yo({node:e,visible:t}){const n=A.useMemo(()=>e?.executionInstances.sort(ze)??[],[e]),r=A.useMemo(()=>nu(n),[n]),[s,a]=A.useState(null);if(A.useEffect(()=>{a(r?U(r):null)},[e?.id,r]),!e)return d.jsx("p",{className:"text-body text-fg-muted italic",children:"Select a node to inspect its session."});if(n.length===0)return d.jsx("p",{className:"text-body text-fg-muted italic",children:pt(e)});const i=n.find(l=>U(l)===s)??r??n[0],u=i?Se(i):"base",o=tu(n),c=n.filter(l=>Se(l)===u);return i?d.jsxs("section",{children:[d.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[d.jsx("h3",{className:"text-body font-semibold text-fg",children:e.title}),(e.historicalOnly||i?.historical)&&d.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:e.historicalOnly?"historical-only":"historical"})]}),o.length>1&&d.jsxs("div",{className:"mt-3 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-label",role:"radiogroup","aria-label":"Iterations",children:[d.jsx("span",{className:"uppercase tracking-wider text-fg-faint",children:"Iterations"}),o.map(l=>{const f=l.instances.at(-1);if(!f)return null;const h=l.iteration==="base"?"Base":`Iteration ${l.iteration}`,g=l.iteration===u;return d.jsxs("span",{className:"flex items-baseline gap-1",children:[d.jsx("span",{"aria-hidden":!0,className:"text-fg-faint",children:"·"}),d.jsx("button",{type:"button",role:"radio","aria-checked":g,className:`focus-mark rounded-sm px-0.5 uppercase tracking-wider ${g?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,onClick:()=>a(U(f)),children:h})]},h)})]}),c.length>1&&d.jsxs("div",{className:"mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-label",role:"radiogroup","aria-label":"Attempts",children:[d.jsx("span",{className:"uppercase tracking-wider text-fg-faint",children:"Attempts"}),c.map(l=>d.jsxs("span",{className:"flex items-baseline gap-1",children:[d.jsx("span",{"aria-hidden":!0,className:"text-fg-faint",children:"·"}),d.jsxs("button",{type:"button",role:"radio","aria-checked":U(l)===U(i),className:`focus-mark rounded-sm px-0.5 uppercase tracking-wider ${U(l)===U(i)?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,onClick:()=>a(U(l)),children:["Attempt ",fn(l)]})]},U(l)))]}),d.jsxs("dl",{className:"mt-4 grid grid-cols-[max-content_minmax(0,1fr)] gap-x-3 gap-y-1 text-label",children:[d.jsx("dt",{className:"uppercase tracking-wider text-fg-faint",children:"Execution instance"}),d.jsx("dd",{className:"break-all text-fg-muted tnum",children:i.id}),d.jsx("dt",{className:"uppercase tracking-wider text-fg-faint",children:"Bead"}),d.jsx("dd",{className:"break-all text-fg-muted tnum",children:i.beadId})]}),d.jsx(Qo,{instance:i,visible:t})]}):d.jsx("p",{className:"text-body text-fg-muted italic",children:pt(e)})}function Qo({instance:e,visible:t}){const n=e.session.kind==="attached"?e.session:null,r=n?.link.sessionId??null,s=t&&!!n?.streamable,a=Br(r,s);if(n===null)return d.jsx("p",{className:"mt-5 text-body text-fg-muted italic",children:eu(e)});const i=qo(a.stream),u=a.status==="loading",o=a.status==="ready"?a.result:null,c=a.status==="failed"?a.error:null,l=a.status==="ready"&&a.stream.status==="degraded"?a.stream.error:null;return d.jsxs("div",{className:"mt-5 space-y-4",children:[n?.streamable&&d.jsx("div",{className:"flex justify-end",children:d.jsx(_r,{tone:i.tone,label:i.label,title:`Session stream: ${a.stream.status}`,className:"text-label uppercase tracking-wider"})}),l!==null&&d.jsx("p",{className:"text-accent",role:"alert",children:l}),d.jsx(Gr,{loading:u,error:c,result:o})]})}function qo(e){switch(e.status){case"open":return{tone:"ok",label:"live"};case"connecting":return{tone:"warn",label:"connecting"};case"closed":return{tone:"stuck",label:"offline"};case"degraded":return{tone:"warn",label:"degraded"};case"idle":return{tone:"neutral",label:"snapshot"}}}function pt(e){const t=e.executionInstances.filter(r=>r.session.kind==="none");return t.some(r=>r.currentIteration&&r.session.kind==="none"&&r.session.reason==="session_unresolved"&&cr(r.status))?"Session unresolved for the current running node.":t.some(r=>r.session.kind==="none"&&r.session.reason==="session_unresolved")?"Session unresolved for this node.":"This node has not started a session yet."}function eu(e){return e.session.kind==="attached"?"":e.currentIteration&&e.session.reason==="session_unresolved"&&cr(e.status)?"Session unresolved for the current running node.":e.session.reason==="session_unresolved"?"Session unresolved for this node.":"This node has not started a session yet."}function cr(e){return e==="active"||e==="running"}function nu(e){return e.find(t=>t.session.kind==="attached"&&t.session.streamable)??[...e].filter(t=>t.session.kind==="attached").sort(ze).at(-1)??[...e].sort(ze).at(-1)}function tu(e){const t=new Map;for(const n of e){const r=Se(n);t.set(r,[...t.get(r)??[],n])}return[...t.entries()].map(([n,r])=>({iteration:n,instances:r.sort(ze)})).sort((n,r)=>Ke(n.iteration)-Ke(r.iteration))}function ze(e,t){return Ke(Se(e))-Ke(Se(t))||fn(e)-fn(t)||e.id.localeCompare(t.id)}function U(e){return e.id}function Se(e){return e.iteration.kind==="loop"?e.iteration.value:"base"}function Ke(e){return e==="base"?0:e}function fn(e){return e.attempt.kind==="attempt"?e.attempt.value:1}function ru({tab:e,diff:t,selectedNode:n}){return e==="session"?d.jsx(Yo,{node:n,visible:!0}):d.jsx(su,{diff:t})}function su({diff:e}){switch(e.kind){case"idle":return d.jsx("p",{className:"text-body text-fg-muted italic",children:"Local changes are not loaded for this run."});case"loading":return d.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading local changes."});case"failed":return d.jsx("p",{className:"text-body text-accent",role:"alert",children:e.error});case"ready":return d.jsxs(d.Fragment,{children:[e.refreshState.kind==="failed"&&d.jsx("p",{className:"mb-4 text-body text-accent",role:"alert",children:e.refreshState.error}),e.refreshState.kind==="refreshing"&&d.jsx("p",{className:"mb-4 text-label uppercase tracking-wider text-fg-faint",role:"status",children:"Refreshing local changes"}),d.jsx(Ko,{diff:e.diff})]})}}function au({diff:e,selectedNode:t}){const[n,r]=A.useState("diff"),s=`run-evidence-tab-${n}`;return d.jsxs("section",{"aria-label":"Run evidence",children:[d.jsxs("div",{className:"flex items-baseline gap-2 text-label",role:"tablist","aria-label":"Run evidence views",children:[d.jsx(vt,{id:"run-evidence-tab-diff",controls:"run-evidence-panel",active:n==="diff",onClick:()=>r("diff"),children:"Diff"}),d.jsx("span",{"aria-hidden":!0,className:"text-fg-faint",children:"·"}),d.jsx(vt,{id:"run-evidence-tab-session",controls:"run-evidence-panel",active:n==="session",onClick:()=>r("session"),children:"Session"})]}),d.jsx("div",{id:"run-evidence-panel",role:"tabpanel","aria-labelledby":s,className:"pt-5",children:d.jsx(ru,{tab:n,diff:e,selectedNode:t})})]})}function vt({id:e,controls:t,active:n,disabled:r=!1,onClick:s,children:a}){return d.jsx("button",{id:e,type:"button",role:"tab","aria-selected":n,"aria-controls":t,"aria-disabled":r||void 0,disabled:r,className:`focus-mark rounded-sm px-0.5 uppercase tracking-wider ${r?"cursor-not-allowed text-fg-faint":n?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,onClick:s,children:a})}function iu(e,t){const n=e.runIds.size===0||e.runIds.has(t.runId),r=e.rootBeadIds.size===0||e.rootBeadIds.has(t.rootBeadId);return n&&r}function ou(e){const t={runIds:new Set,rootBeadIds:new Set};return J(e,t),J(x(e.run),t),J(x(e.payload),t),J(x(x(e.payload)?.run),t),J(x(e.bead),t),J(x(x(e.payload)?.bead),t),J(x(e.root),t),J(x(x(e.payload)?.root),t),dn(x(e.metadata),t),dn(x(x(e.payload)?.metadata),t),t}function J(e,t){e&&(H(t.runIds,e.run_id),H(t.runIds,e.workflow_id),H(t.rootBeadIds,e.root_bead_id),dn(x(e.metadata),t))}function dn(e,t){e&&(H(t.runIds,e["gc.run_id"]),H(t.runIds,e["gc.workflow_id"]),H(t.runIds,e.run_id),H(t.runIds,e.workflow_id),H(t.rootBeadIds,e["gc.root_bead_id"]),H(t.rootBeadIds,e.root_bead_id))}function H(e,t){if(typeof t!="string")return;const n=t.trim();n&&e.add(n)}function x(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0}function uu(e,t,n){const[r,s]=A.useState({nodeId:null,routeKey:"",source:"route"});A.useEffect(()=>{if(!e)return;const c=cu(e,t);s(l=>l.routeKey===n&&(l.source==="user"||l.nodeId===c)?l:{nodeId:c,routeKey:n,source:"route"})},[e,n,t]);const a=A.useCallback(()=>{s(c=>({nodeId:null,routeKey:c.routeKey,source:"user"}))},[]);A.useEffect(()=>{const c=l=>{l.key==="Escape"&&a()};return window.addEventListener("keydown",c),()=>window.removeEventListener("keydown",c)},[a]);const i=A.useCallback(c=>{s(l=>({nodeId:l.nodeId===c?null:c,routeKey:n,source:"user"}))},[n]),u=r.nodeId,o=A.useMemo(()=>e?.nodes.find(c=>c.id===u)??null,[e,u]);return{selectedNodeId:u,selectedNode:o,toggleNode:i,clearSelection:a}}function cu(e,t){return t&&e.nodes.some(n=>n.id===t)?t:null}const lu=6e4;async function fu(e,t,n){const r=wr("load supervisor formula run detail"),s=bu(t,n),a=kr(lu),i=St(),[u,o]=await Promise.all([Sr(()=>a.workflowRun(r,e,s)),du(r)]),c=gu(u),l=await hu(i,r,c,s),f=ta(c,{sessions:o.sessions,formulaDetailState:l.state,...l.kind==="available"?{formulaDetail:l.detail}:{}}),h=[...f.completeness.kind==="partial"?f.completeness.reasons:[],...o.kind==="unavailable"?["session_list_failed"]:[],...l.kind==="unavailable"?[pu(l.state.reason)]:[]];return{...f,completeness:Ft(h)}}async function du(e){try{const t=await St().listSessions(e);return{kind:"available",sessions:Nr(t)}}catch{return{kind:"unavailable",sessions:[]}}}async function hu(e,t,n,r){const s=n.beads?.find(o=>o.id===n.root_bead_id),a=hn("route",{root:s}),i=a.name??void 0,u=a.target??void 0;if(i===void 0)return{kind:"unavailable",state:{kind:"unavailable",reason:"missing_formula_metadata"}};if(u===void 0)return{kind:"unavailable",state:{kind:"unavailable",reason:"missing_run_target",name:i}};try{return{kind:"available",detail:mu(await e.formulaDetail(t,i,{target:u,...r??{}})),state:{kind:"available",name:i,target:u}}}catch(o){return{kind:"unavailable",state:{kind:"unavailable",reason:"fetch_failed",name:i,target:u,failure:vu(o)}}}}function gu(e){const t={run_id:e.workflow_id,root_bead_id:e.root_bead_id,root_store_ref:e.root_store_ref,resolved_root_store:e.resolved_root_store,scope_kind:e.scope_kind,scope_ref:e.scope_ref,snapshot_version:e.snapshot_version,partial:e.partial,stores_scanned:e.stores_scanned,beads:e.beads,deps:e.deps,logical_nodes:e.logical_nodes,logical_edges:e.logical_edges,scope_groups:e.scope_groups};return e.snapshot_event_seq!==void 0&&(t.snapshot_event_seq=e.snapshot_event_seq),t}function mu(e){const t={name:e.name},n={};return Array.isArray(e.preview.nodes)&&(n.nodes=e.preview.nodes),Array.isArray(e.preview.edges)&&(n.edges=e.preview.edges),(n.nodes!==void 0||n.edges!==void 0)&&(t.preview=n),Array.isArray(e.steps)&&(t.steps=e.steps),Array.isArray(e.deps)&&(t.deps=e.deps),t}function pu(e){switch(e){case"missing_formula_metadata":return"formula_detail_missing_formula_metadata";case"missing_run_target":return"formula_detail_missing_run_target";case"fetch_failed":return"formula_detail_fetch_failed"}}function vu(e){return e instanceof Nt&&e.status===404?"not_found":"upstream_error"}function bu(e,t){if(e===void 0&&t===void 0)return;const n={};return e!==void 0&&(n.scope_kind=e),t!==void 0&&(n.scope_ref=t),n}function yu(e,t,n){const r=Nu(e,t,n),{data:s,loading:a,error:i,refresh:u}=jt(r,()=>_u(e,t,n),{onError:o=>{e!==void 0&&Su("load detail",e,o)}});return e===void 0?{kind:"idle",refresh:wu}:s?.kind==="loaded"?{kind:"ready",detail:s.detail,refresh:u,refreshState:ku(a,i)}:s?.kind==="unsupported"?{kind:"unsupported",refresh:u}:s?.kind==="not_found"?{kind:"not_found",refresh:u}:i!==null?{kind:"failed",error:i,refresh:u}:{kind:"loading",refresh:u}}async function _u(e,t,n){if(!e)return{kind:"unrequested"};const r={};t!==void 0&&(r.scopeKind=t),n!==void 0&&(r.scopeRef=n);try{return{kind:"loaded",detail:await fu(e,r.scopeKind,r.scopeRef)}}catch(s){if(s instanceof oe&&s.reason==="not_run_view")return{kind:"unsupported"};if(s instanceof Nt&&s.status===404)return{kind:"not_found"};throw s}}async function wu(){}function ku(e,t){return t!==null?{kind:"failed",error:t}:e?{kind:"refreshing"}:{kind:"idle"}}function Su(e,t,n){It({component:"formula-run-detail",operation:e,message:`${t}: ${Ct(n)}`})}function Nu(e,t,n){return["formula-run",e??"missing",t??"default",n??"default"].map(encodeURIComponent).join(":")}function ju(e,t,n,r){const s=Eu(e,t,n,r),{data:a,loading:i,error:u,refresh:o}=jt(s,()=>bt(e,t,n,r),{refreshFetcher:()=>bt(e,t,n,r,!0),onError:c=>{e!==void 0&&Au("load diff",e,c)}});return e===void 0||t===void 0?{kind:"idle",refresh:Iu}:a?.kind==="loaded"?{kind:"ready",diff:a.diff,refresh:o,refreshState:Cu(i,u)}:u!==null?{kind:"failed",error:u,refresh:o}:{kind:"loading",refresh:o}}async function bt(e,t,n,r,s){if(!e||t===void 0)return{kind:"unrequested"};const a={};return n!==void 0&&(a.scopeKind=n),r!==void 0&&(a.scopeRef=r),s&&(a.refresh=!0),{kind:"loaded",diff:await jr.runDiff(e,{executionPath:t},a)}}async function Iu(){}function Cu(e,t){return t!==null?{kind:"failed",error:t}:e?{kind:"refreshing"}:{kind:"idle"}}function Au(e,t,n){It({component:"formula-run-detail",operation:e,message:`${t}: ${Ct(n)}`})}function Eu(e,t,n,r){return["formula-run-diff",e??"missing",Du(t),n??"default",r??"default"].join(":")}function Du(e){return e===void 0?"path:missing":e.kind==="known"?`path:${e.path}`:`path:${e.reason}`}const Tu=[On.bead,On.session],Ru=[],Mu=["completed","done","failed","skipped"],Ou=["pending","ready","running","active","blocked"];function rc(){const{runId:e}=Ir(),[t]=Cr(),n=Vu(t),r=n.ok?n.scope:void 0,s=n.ok?null:n.error,a=t.get("node"),i=[e??"",r?.scopeKind??"",r?.scopeRef??"",a??""].join("\0"),u=yu(s?void 0:e,r?.scopeKind,r?.scopeRef),o=u.kind==="ready"?u:null,c=o?.detail??null,l=u.kind==="unsupported",f=u.kind==="not_found",h=ju(s||c===null?void 0:e,c?.executionPath,r?.scopeKind,r?.scopeRef),g=u.kind==="loading",m=o!==null&&o.refreshState.kind==="refreshing"||h.kind==="ready"&&h.refreshState.kind==="refreshing",p=c!==null&&h.kind==="loading",v=g||m||p,y=u.kind==="failed"?u.error:o!==null&&o.refreshState.kind==="failed"?o.refreshState.error:null;Ar(s?Ru:Tu,()=>{yt(u.refresh,h.refresh)},{matches:P=>{if(c===null)return!1;const G=ou(P);return Pu(c.progress)&&$u(G)?!1:iu(G,{runId:c.runId,rootBeadId:c.rootBeadId})}});const _=s??y,{selectedNodeId:b,selectedNode:k,toggleNode:S}=uu(c,a,i),j=Pr(c?.rootBeadId??null),[N,I]=A.useState(null),C=Er(),E=Or(),[D]=A.useState(()=>Dr(`runs:summary:${E??"no-city"}`)),B=A.useMemo(()=>{if(!e)return null;const P=D&&D.status!=="error"?D.data:null;return P==null?null:[...P.lanes,...P.blockedLanes].find(G=>G.id===e)??null},[D,e]),M=c?`${c.progress.visibleNodeCount} nodes. ${Wu(c.progress)}. Local changes are shown for the run execution folder.`:g&&!s||l||f?void 0:"Formula run unavailable.";return d.jsxs("section",{children:[d.jsx($r,{title:c?.title??"Formula Run",synopsis:M,meta:d.jsxs(d.Fragment,{children:[d.jsx(Tr,{to:"/runs",className:"focus-mark text-label uppercase tracking-wider text-fg-muted hover:text-fg",children:"Runs"}),_&&c&&d.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:_}),c&&d.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:Bu(c)}),d.jsx(Rr,{size:"sm",onClick:()=>{yt(u.refresh,h.refresh)},disabled:v||!!s,children:m?"Refreshing":"Refresh"})]})}),v&&!s&&!c?B?d.jsxs(d.Fragment,{children:[d.jsx($n,{stages:B.stages,label:B.title}),d.jsx("p",{className:"text-body text-fg-muted italic mt-8",children:"Loading run detail."})]}):d.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading formula run."}):l?d.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"Detailed step view isn’t available for this run (v1/wisp runs are list-only) — this run appears in the run list only."}):f?d.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"This run’s detail snapshot was not found. It may be a v1/wisp run, a completed run whose snapshot wasn’t retained, or no longer available."}):_&&!c?d.jsx("p",{className:"text-body text-accent",role:"alert",children:_}):o?d.jsxs(d.Fragment,{children:[d.jsx(Fu,{detail:o.detail}),d.jsx($n,{stages:o.detail.stages,label:o.detail.title}),d.jsx(Lu,{detail:o.detail}),d.jsxs("div",{className:"mt-8 grid gap-10 lg:grid-cols-[minmax(0,0.95fr)_minmax(22rem,1.05fr)]",children:[d.jsx(fa,{detail:o.detail,selectedNodeId:b,onToggleNode:S}),d.jsx(au,{diff:h,selectedNode:k})]}),d.jsx(Fr,{view:j.view,loading:j.loading,error:j.error,now:C,onOpenBead:I}),d.jsx(xr,{open:N!==null,onClose:()=>I(null),beadId:N,onOpenBead:I})]}):null]})}async function yt(e,t){await Promise.all([e(),t()])}function $u(e){return e.runIds.size===0&&e.rootBeadIds.size===0}function Pu(e){return e.visibleNodeCount<=0||Ou.reduce((r,s)=>r+(e.statusCounts[s]??0),0)>0?!1:Mu.reduce((r,s)=>r+(e.statusCounts[s]??0),0)>=e.visibleNodeCount}function Fu({detail:e}){const t=Gu(e.formulaDetail);return d.jsxs("dl",{className:"grid gap-x-8 gap-y-3 sm:grid-cols-2 lg:grid-cols-4",children:[d.jsx(xu,{formula:e.formula}),t!==null&&d.jsx(de,{label:"Formula Detail",value:t}),d.jsx(de,{label:"Root",value:e.rootBeadId}),d.jsx(de,{label:"Scope",value:`${e.scopeKind}:${e.scopeRef}`}),d.jsx(de,{label:"Store",value:e.resolvedRootStore||e.rootStoreRef||"unknown"})]})}function de({label:e,value:t}){return d.jsxs("div",{children:[d.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint",children:e}),d.jsx("dd",{className:"text-body text-fg break-all tnum",children:t})]})}const _t="name inferred from bead title — supervisor did not set gc.formula on this graph.v2 root";function xu({formula:e}){if(e.kind!=="known")return d.jsx(de,{label:"Formula",value:"metadata missing"});switch(e.source){case"metadata":return d.jsx(de,{label:"Formula",value:e.name});case"title_fallback":return d.jsxs("div",{children:[d.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Formula"}),d.jsxs("dd",{className:"text-body text-warn break-all tnum",title:_t,"aria-label":`${e.name} (${_t})`,children:[e.name,d.jsx("span",{className:"ml-2 text-label uppercase tracking-wider text-warn",children:"inferred from bead title"})]})]});default:return e.source}}function Bu(e){return e.snapshotEventSeq.kind==="known"?`v${e.snapshotVersion} · seq ${e.snapshotEventSeq.seq}`:`v${e.snapshotVersion}`}function Gu(e){return e.kind==="available"?`available for ${e.target}`:e.reason==="missing_formula_metadata"?null:e.reason==="missing_run_target"?`missing run target for ${e.name}`:`${e.failure} for ${e.target}`}function Lu({detail:e}){if(e.completeness.kind!=="partial")return null;const t=zu(e.completeness.reasons);return t.length===0?null:d.jsxs("p",{className:"mt-5 text-label uppercase tracking-wider text-warn",role:"status",children:["Partial run data: ",Uu(t),"."]})}function zu(e){return e.filter(t=>!Ku(t))}function Ku(e){switch(e){case"formula_detail_missing_formula_metadata":case"formula_detail_missing_run_target":case"formula_detail_fetch_failed":return!0;case"supervisor_snapshot_partial":case"runtime_bead_read_failed":case"session_list_failed":return!1}}function Uu(e){return e.map(Hu).join(", ")}function Hu(e){switch(e){case"supervisor_snapshot_partial":return"supervisor snapshot is partial";case"runtime_bead_read_failed":return"runtime bead refresh failed";case"session_list_failed":return"session list failed";case"formula_detail_missing_formula_metadata":return"formula metadata is missing";case"formula_detail_missing_run_target":return"formula run target is missing";case"formula_detail_fetch_failed":return"formula detail fetch failed"}}function Vu(e){const t=e.getAll("scope_kind"),n=e.getAll("scope_ref");if(t.length>1||n.length>1)return{ok:!1,error:"Invalid run scope query."};const r=t[0],s=n[0];return r===void 0&&s===void 0?{ok:!0}:r===void 0||s===void 0?{ok:!1,error:"Invalid run scope query."}:r!=="city"&&r!=="rig"?{ok:!1,error:"Invalid run scope query."}:Mr.test(s)?{ok:!0,scope:{scopeKind:r,scopeRef:s}}:{ok:!1,error:"Invalid run scope query."}}function Wu(e){const t=[ne(e,["active","running"],"running"),ne(e,["completed","done"],"done"),ne(e,"ready","ready"),ne(e,"blocked","blocked"),ne(e,"failed","failed"),ne(e,"skipped","skipped"),ne(e,"pending","pending")].filter(n=>n!==null);return t.length>0?t.join(", "):"No node status yet"}function ne(e,t,n){const s=(typeof t=="string"?[t]:t).reduce((a,i)=>a+(e.statusCounts[i]??0),0);return s>0?`${s} ${n}`:null}export{rc as FormulaRunDetailPage}; diff --git a/internal/api/dashboardspa/dist/assets/FormulaRunDetail-DtW7ktOr.js b/internal/api/dashboardspa/dist/assets/FormulaRunDetail-DtW7ktOr.js new file mode 100644 index 0000000000..60d20f35b4 --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/FormulaRunDetail-DtW7ktOr.js @@ -0,0 +1,12 @@ +import{j as f,r as A,S as kr,X as Kn,Y as Ue,b as Hn,x as Vn,p as Wn,q as Cr,K as Ar,f as Er,u as Tr,Z as Dr,L as Or,B as Mr,H as Ir,G as mn}from"./index-QWRimsO3.js";import{P as Pr}from"./PageHeader-CxbYmkHZ.js";import{u as Rr,R as $r,B as Br}from"./BeadDetailModal-BtQMJz2-.js";import{u as Fr,S as xr}from"./LiveSessionPeek-m6YywWBh.js";import{S as vn}from"./StageLadder-DgEuJnhe.js";import"./format-fte2CeYD.js";import"./Field-pp_wh5a7.js";import"./constants-CVFL5iaz.js";import"./time-D9v0saHV.js";const Gr=/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$/,bn={pending:"pending",ready:"ready",running:"running",active:"running",done:"done",completed:"done",failed:"failed",blocked:"blocked",skipped:"skipped"};function Lr({node:e,selected:r,onToggle:n}){const t=Kr(e.constructKind),a=Hr(e.status),s=e.iterationSummary.kind==="stacked"?`${e.iterationSummary.iterationCount} iterations, showing ${e.iterationSummary.visibleIteration}`:null,i=e.attemptSummary.kind==="tracked"&&e.attemptSummary.badge.kind==="bounded"?` · attempt ${e.attemptSummary.badge.label}${Ur(e)}`:"";return f.jsxs("button",{type:"button","aria-pressed":r,onClick:()=>n(e.id),className:`focus-mark w-full text-left px-4 py-3 bg-transparent transition-colors duration-150 ease-out-quart ${t} ${r?"text-fg border-accent bg-surface-tint ring-2 ring-accent/45 ring-offset-2 ring-offset-surface":"text-fg border-rule hover:border-fg-faint hover:bg-surface-tint"}`,children:[f.jsxs("div",{className:"flex items-start justify-between gap-3",children:[f.jsxs("div",{children:[f.jsx("p",{className:"text-body text-fg leading-snug",children:e.title}),f.jsxs("p",{className:"mt-1 text-label uppercase tracking-wider text-fg-faint",children:[zr(e.constructKind),i]})]}),f.jsxs("span",{className:`text-label uppercase tracking-wider shrink-0 ${a}`,children:[Vr(e.status)," ",bn[e.status]]})]}),s&&f.jsxs("p",{className:"mt-2 text-label uppercase tracking-wider text-fg-faint tnum",children:["stacked history: ",s]}),e.controlBadges.length>0&&f.jsx("div",{className:"mt-2 flex flex-wrap gap-2",children:e.controlBadges.map(l=>f.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-muted border border-rule px-1.5 py-0.5",children:[l.label,": ",bn[l.status]]},l.id))})]})}function Ur(e){return e.attemptSummary.kind==="tracked"&&e.attemptSummary.active.kind==="running"?` · running attempt ${e.attemptSummary.active.value}`:""}function zr(e){switch(e){case"run-root":return"run root";case"run-finalize":return"finalize";case"step":case"retry":case"check-loop":case"scope":case"condition":case"fanout":case"expansion":case"scope-check":case"spec":case"control":case"unknown":return e.replace(/-/g," ")}}function Kr(e){switch(e){case"run-root":return"formula-run-node-shape-root";case"step":case"unknown":return"formula-run-node-shape-step";case"retry":return"formula-run-node-shape-retry";case"check-loop":return"formula-run-node-shape-check-loop";case"scope":return"formula-run-node-shape-scope";case"condition":return"formula-run-node-shape-condition";case"fanout":return"formula-run-node-shape-fanout";case"expansion":return"formula-run-node-shape-expansion";case"scope-check":case"run-finalize":case"spec":case"control":return"formula-run-node-shape-control"}}function Hr(e){switch(e){case"failed":case"blocked":return"text-accent";case"active":case"running":case"ready":return"text-fg";case"completed":case"done":return"text-fg-muted";case"pending":case"skipped":return"text-fg-faint"}}function Vr(e){switch(e){case"completed":case"done":return"✓";case"active":case"running":return"●";case"failed":case"blocked":return"!";case"skipped":return"∅";case"pending":case"ready":return"·"}}function Wr({detail:e,selectedNodeId:r,onToggleNode:n}){const t=Xr(e),a=Zr(e);return t.length===0?f.jsx("p",{className:"text-body text-fg-muted italic",children:"No graph nodes have materialized for this formula run."}):f.jsxs("section",{"aria-label":"Formula run graph",children:[f.jsx("div",{className:"flex items-baseline justify-between gap-4",children:f.jsx("h2",{className:"text-title text-fg",children:"Formula Graph"})}),f.jsx("ol",{className:"mt-5 space-y-3 relative",children:t.map((s,i)=>{const l=a.get(s.id),o=i>0?a.get(t[i-1]?.id??""):void 0,u=l!==void 0&&l!==o;return f.jsxs("li",{className:"relative pl-6",children:[u&&f.jsx("p",{className:"mb-1 text-label uppercase tracking-wider text-fg-faint",children:l}),ir.visibleInGraph!==!1)}function Zr(e){const r=new Map;for(const n of e.lanes)for(const t of n.nodeIds)r.set(t,n.label);return r}function pn(e,r){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);r&&(t=t.filter((function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.push.apply(n,t)}return n}function M(e){for(var r=1;r=0||(c[o]=i[o]);return c})(e,r);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(t=0;t=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}function x(e,r){return Jr(e)||(function(n,t){var a=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(a!=null){var s,i,l,o,u=[],c=!0,d=!1;try{if(l=(a=a.call(n)).next,t===0){if(Object(a)!==a)return;c=!1}else for(;!(c=(s=l.call(a)).done)&&(u.push(s.value),u.length!==t);c=!0);}catch(h){d=!0,i=h}finally{try{if(!c&&a.return!=null&&(o=a.return(),Object(o)!==o))return}finally{if(d)throw i}}return u}})(e,r)||qe(e,r)||qr()}function Yr(e){return(function(r){if(Array.isArray(r))return Ke(r)})(e)||Qr(e)||qe(e)||(function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)})()}function Jr(e){if(Array.isArray(e))return e}function Qr(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function qe(e,r){if(e){if(typeof e=="string")return Ke(e,r);var n=Object.prototype.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Ke(e,r):void 0}}function Ke(e,r){(r==null||r>e.length)&&(r=e.length);for(var n=0,t=new Array(r);n=e.length?{done:!0}:{done:!1,value:e[t++]}},e:function(o){throw o},f:a}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var s,i=!0,l=!1;return{s:function(){n=n.call(e)},n:function(){var o=n.next();return i=o.done,o},e:function(o){l=!0,s=o},f:function(){try{i||n.return==null||n.return()}finally{if(l)throw s}}}}var je=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function pe(e,r){return e(r={exports:{}},r.exports),r.exports}var F=pe((function(e){(function(){var r={}.hasOwnProperty;function n(){for(var t=[],a=0;a-1?y.slice(0,p):j;switch(j){case"diff":b--;break e;case"deleted":case"new":var _=y.slice(p+1);_.indexOf("file mode")===0&&(i[j==="new"?"newMode":"oldMode"]=_.slice(10));break;case"similarity":i.similarity=parseInt(y.split(" ")[2],10);break;case"index":var S=y.slice(p+1).split(" "),N=S[0].split("..");i.oldRevision=N[0],i.newRevision=N[1],S[1]&&(i.oldMode=i.newMode=S[1]);break;case"copy":case"rename":var k=y.slice(p+1);k.indexOf("from")===0?i.oldPath=k.slice(5):i.newPath=k.slice(3),w=j;break;case"---":var C=y.slice(p+1),E=g[++b].slice(4);C==="/dev/null"?(E=E.slice(2),w="add"):E==="/dev/null"?(C=C.slice(2),w="delete"):(w="modify",C=C.slice(2),E=E.slice(2)),C&&(i.oldPath=C),E&&(i.newPath=E),h=5;break e}}i.type=w||"modify"}else if(v.indexOf("Binary")===0)i.isBinary=!0,i.type=v.indexOf("/dev/null and")>=0?"add":v.indexOf("and /dev/null")>=0?"delete":"modify",h=2,i=null;else if(h===5)if(v.indexOf("@@")===0){var T=/^@@\s+-([0-9]+)(,([0-9]+))?\s+\+([0-9]+)(,([0-9]+))?/.exec(v);l={content:v,oldStart:T[1]-0,newStart:T[4]-0,oldLines:T[3]-0||1,newLines:T[6]-0||1,changes:[]},i.hunks.push(l),o=l.oldStart,u=l.newStart}else{var $=v.slice(0,1),O={content:v.slice(1)};switch($){case"+":O.type="insert",O.isInsert=!0,O.lineNumber=u,u++;break;case"-":O.type="delete",O.isDelete=!0,O.lineNumber=o,o++;break;case" ":O.type="normal",O.isNormal=!0,O.oldLineNumber=o,O.newLineNumber=u,o++,u++;break;case"\\":var I=l.changes[l.changes.length-1];I.isDelete||(i.newEndingNewLine=!1),I.isInsert||(i.oldEndingNewLine=!1)}O.type&&l.changes.push(O)}b++}return d}};e.exports=a})()}));function ye(e){return e.type==="insert"}function W(e){return e.type==="delete"}function he(e){return e.type==="normal"}function at(e,r){var n=r.nearbySequences==="zip"?(function(t){var a=t.reduce((function(s,i,l){var o=x(s,3),u=o[0],c=o[1],d=o[2];return c?ye(i)&&d>=0?(u.splice(d+1,0,i),[u,i,d+2]):(u.push(i),[u,i,W(i)&&W(c)?d:l]):(u.push(i),[u,i,W(i)?l:-1])}),[[],null,-1]);return x(a,1)[0]})(e.changes):e.changes;return M(M({},e),{},{isPlain:!1,changes:n})}function st(e){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=(function(t){if(t.startsWith("diff --git"))return t;var a=t.indexOf(` +`),s=t.indexOf(` +`,a+1),i=t.slice(0,a),l=t.slice(a+1,s),o=i.split(" ").slice(1,-3).join(" "),u=l.split(" ").slice(1,-3).join(" ");return["diff --git a/".concat(o," b/").concat(u),"index 1111111..2222222 100644","--- a/".concat(o),"+++ b/".concat(u),t.slice(s+1)].join(` +`)})(e.trimStart());return tt.parse(n).map((function(t){return(function(a,s){var i=a.hunks.map((function(l){return at(l,s)}));return M(M({},a),{},{hunks:i})})(t,r)}))}function it(e){return e[0]}function ot(e){return e[e.length-1]}function He(e){return["".concat(e,"Start"),"".concat(e,"Lines")]}function ge(e){return e==="old"?function(r){return ye(r)?-1:he(r)?r.oldLineNumber:r.lineNumber}:function(r){return W(r)?-1:he(r)?r.newLineNumber:r.lineNumber}}function Zn(e,r){return function(n,t){var a=n[e],s=a+n[r];return t>=a&&t=s&&a-1},gt=function(e,r){var n=this.__data__,t=Me(n,e);return t<0?(++this.size,n.push([e,r])):n[t][1]=r,this};function re(e){var r=-1,n=e==null?0:e.length;for(this.clear();++rl))return!1;var u=s.get(e),c=s.get(r);if(u&&c)return u==r&&c==e;var d=-1,h=!0,g=2&n?new Jt:void 0;for(s.set(e,r),s.set(r,e);++d-1&&e%1==0&&e-1&&e%1==0&&e<=9007199254740991},D={};D["[object Float32Array]"]=D["[object Float64Array]"]=D["[object Int8Array]"]=D["[object Int16Array]"]=D["[object Int32Array]"]=D["[object Uint8Array]"]=D["[object Uint8ClampedArray]"]=D["[object Uint16Array]"]=D["[object Uint32Array]"]=!0,D["[object Arguments]"]=D["[object Array]"]=D["[object ArrayBuffer]"]=D["[object Boolean]"]=D["[object DataView]"]=D["[object Date]"]=D["[object Error]"]=D["[object Function]"]=D["[object Map]"]=D["[object Number]"]=D["[object Object]"]=D["[object RegExp]"]=D["[object Set]"]=D["[object String]"]=D["[object WeakMap]"]=!1;var ga=function(e){return le(e)&&rn(e.length)&&!!D[ue(e)]},ma=function(e){return function(r){return e(r)}},Cn=pe((function(e,r){var n=r&&!r.nodeType&&r,t=n&&e&&!e.nodeType&&e,a=t&&t.exports===n&&Qn.process,s=(function(){try{var i=t&&t.require&&t.require("util").types;return i||a&&a.binding&&a.binding("util")}catch{}})();e.exports=s})),An=Cn&&Cn.isTypedArray,sr=An?ma(An):ga,va=Object.prototype.hasOwnProperty,ba=function(e,r){var n=z(e),t=!n&&tr(e),a=!n&&!t&&Ve(e),s=!n&&!t&&!a&&sr(e),i=n||t||a||s,l=i?ua(e.length,String):[],o=l.length;for(var u in e)!va.call(e,u)||i&&(u=="length"||a&&(u=="offset"||u=="parent")||s&&(u=="buffer"||u=="byteLength"||u=="byteOffset")||ar(u,o))||l.push(u);return l},pa=Object.prototype,ya=function(e){var r=e&&e.constructor;return e===(typeof r=="function"&&r.prototype||pa)},wa=(function(e,r){return function(n){return e(r(n))}})(Object.keys,Object),_a=Object.prototype.hasOwnProperty,Na=function(e){if(!ya(e))return wa(e);var r=[];for(var n in Object(e))_a.call(e,n)&&n!="constructor"&&r.push(n);return r},ja=function(e){return e!=null&&rn(e.length)&&!er(e)},tn=function(e){return ja(e)?ba(e):Na(e)},En=function(e){return aa(e,tn,la)},Sa=Object.prototype.hasOwnProperty,ka=function(e,r,n,t,a,s){var i=1&n,l=En(e),o=l.length;if(o!=En(r).length&&!i)return!1;for(var u=o;u--;){var c=l[u];if(!(i?c in r:Sa.call(r,c)))return!1}var d=s.get(e),h=s.get(r);if(d&&h)return d==r&&h==e;var g=!0;s.set(e,r),s.set(r,e);for(var m=i;++u1)return!1;if(e.length===1){var r=x(e,1)[0];return r.type==="text"&&!r.value}return!0}function is(e){var r=e.changeKey,n=e.text,t=e.tokens,a=e.renderToken,s=oe(e,as),i=a?function(l,o){return a(l,Pn,o)}:Pn;return f.jsx("td",M(M({},s),{},{"data-change-key":r,children:t?ss(t)?" ":t.map(i):n||" "}))}var dr=A.memo(is);function hr(e,r){return function(){var n=r==="old"?ln(e):un(e);return n===-1?void 0:n}}function gr(e,r){return function(n){return e&&n?f.jsx("a",{href:r?"#"+r:void 0,children:n}):n}}function Te(e,r){return r?function(n){e(),r(n)}:e}function Rn(e,r,n,t){return A.useMemo((function(){var a=fr(e,(function(s){return function(i){return s&&s(r,i)}}));return a.onMouseEnter=Te(n,a.onMouseEnter),a.onMouseLeave=Te(t,a.onMouseLeave),a}),[e,n,t,r])}function $n(e,r,n,t,a,s,i,l,o){var u={change:r,side:t,inHoverState:l,renderDefault:hr(r,t),wrapInAnchor:gr(a,s)};return f.jsx("td",M(M({className:e},i),{},{"data-change-key":n,children:o(u)}))}function os(e){var r,n,t,a=e.change,s=e.selected,i=e.tokens,l=e.className,o=e.generateLineClassName,u=e.gutterClassName,c=e.codeClassName,d=e.gutterEvents,h=e.codeEvents,g=e.hideGutter,m=e.gutterAnchor,b=e.generateAnchorID,v=e.renderToken,y=e.renderGutter,w=a.type,p=a.content,j=X(a),_=(r=x(A.useState(!1),2),n=r[0],t=r[1],[n,A.useCallback((function(){return t(!0)}),[]),A.useCallback((function(){return t(!1)}),[])]),S=x(_,3),N=S[0],k=S[1],C=S[2],E=A.useMemo((function(){return{change:a}}),[a]),T=Rn(d,E,k,C),$=Rn(h,E,k,C),O=b(a),I=o({changes:[a],defaultGenerate:function(){return l}}),B=F("diff-gutter","diff-gutter-".concat(w),u,{"diff-gutter-selected":s}),ee=F("diff-code","diff-code-".concat(w),c,{"diff-code-selected":s});return f.jsxs("tr",{id:O,className:F("diff-line",I),children:[!g&&$n(B,a,j,"old",m,O,T,N,y),!g&&$n(B,a,j,"new",m,O,T,N,y),f.jsx(dr,M({className:ee,changeKey:j,text:p,tokens:i,renderToken:v},$))]})}var ls=A.memo(os);function us(e){var r=e.hideGutter,n=e.element;return f.jsx("tr",{className:"diff-widget",children:f.jsx("td",{colSpan:r?1:3,className:"diff-widget-content",children:n})})}var cs=["hideGutter","selectedChanges","tokens","lineClassName"],fs=["hunk","widgets","className"];function ds(e){var r=e.hunk,n=e.widgets,t=e.className,a=oe(e,fs),s=(function(i,l){return i.reduce((function(o,u){var c=X(u);o.push(["change",c,u]);var d=l[c];return d&&o.push(["widget",c,d]),o}),[])})(r.changes,n);return f.jsx("tbody",{className:F("diff-hunk",t),children:s.map((function(i){return(function(l,o){var u=x(l,3),c=u[0],d=u[1],h=u[2],g=o.hideGutter,m=o.selectedChanges,b=o.tokens,v=o.lineClassName,y=oe(o,cs);if(c==="change"){var w=W(h)?"old":"new",p=W(h)?ln(h):un(h),j=b?b[w][p-1]:null;return f.jsx(ls,M({className:v,change:h,hideGutter:g,selected:m.includes(d),tokens:j},y),"change".concat(d))}return c==="widget"?f.jsx(us,{hideGutter:g,element:h},"widget".concat(d)):null})(i,a)}))})}var mr=0;function ke(e,r,n,t){var a=A.useCallback((function(){return r(e)}),[e,r]),s=A.useCallback((function(){return r("")}),[r]);return A.useMemo((function(){var i=fr(t,(function(l){return function(o){return l&&l({side:e,change:n},o)}}));return i.onMouseEnter=Te(a,i.onMouseEnter),i.onMouseLeave=Te(s,i.onMouseLeave),i}),[n,t,a,e,s])}function Le(e){var r=e.change,n=e.side,t=e.selected,a=e.tokens,s=e.gutterClassName,i=e.codeClassName,l=e.gutterEvents,o=e.codeEvents,u=e.anchorID,c=e.gutterAnchor,d=e.gutterAnchorTarget,h=e.hideGutter,g=e.hover,m=e.renderToken,b=e.renderGutter;if(!r){var v=F("diff-gutter","diff-gutter-omit",s),y=F("diff-code","diff-code-omit",i);return[!h&&f.jsx("td",{className:v},"gutter"),f.jsx("td",{className:y},"code")]}var w=r.type,p=r.content,j=X(r),_=n===mr?"old":"new",S=M({id:u||void 0,className:F("diff-gutter","diff-gutter-".concat(w),ze({"diff-gutter-selected":t},"diff-line-hover-"+_,g),s),children:b({change:r,side:_,inHoverState:g,renderDefault:hr(r,_),wrapInAnchor:gr(c,d)})},l),N=F("diff-code","diff-code-".concat(w),ze({"diff-code-selected":t},"diff-line-hover-"+_,g),i);return[!h&&f.jsx("td",M(M({},S),{},{"data-change-key":j}),"gutter"),f.jsx(dr,M({className:N,changeKey:j,text:p,tokens:a,renderToken:m},o),"code")]}function hs(e){var r=e.className,n=e.oldChange,t=e.newChange,a=e.oldSelected,s=e.newSelected,i=e.oldTokens,l=e.newTokens,o=e.monotonous,u=e.gutterClassName,c=e.codeClassName,d=e.gutterEvents,h=e.codeEvents,g=e.hideGutter,m=e.generateAnchorID,b=e.generateLineClassName,v=e.gutterAnchor,y=e.renderToken,w=e.renderGutter,p=x(A.useState(""),2),j=p[0],_=p[1],S=ke("old",_,n,d),N=ke("new",_,t,d),k=ke("old",_,n,h),C=ke("new",_,t,h),E=n&&m(n),T=t&&m(t),$=b({changes:[n,t],defaultGenerate:function(){return r}}),O={monotonous:o,hideGutter:g,gutterClassName:u,codeClassName:c,gutterEvents:d,codeEvents:h,renderToken:y,renderGutter:w},I=M(M({},O),{},{change:n,side:mr,selected:a,tokens:i,gutterEvents:S,codeEvents:k,anchorID:E,gutterAnchor:v,gutterAnchorTarget:E,hover:j==="old"}),B=M(M({},O),{},{change:t,side:1,selected:s,tokens:l,gutterEvents:N,codeEvents:C,anchorID:n===t?null:T,gutterAnchor:v,gutterAnchorTarget:n===t?E:T,hover:j==="new"});if(o)return f.jsx("tr",{className:F("diff-line",$),children:Le(n?I:B)});var ee=(function(ne,ce){return ne&&!ce?"diff-line-old-only":!ne&&ce?"diff-line-new-only":ne===ce?"diff-line-normal":"diff-line-compare"})(n,t);return f.jsxs("tr",{className:F("diff-line",ee,$),children:[Le(I),Le(B)]})}var gs=A.memo(hs);function ms(e){var r=e.hideGutter,n=e.oldElement,t=e.newElement;return e.monotonous?f.jsx("tr",{className:"diff-widget",children:f.jsx("td",{colSpan:r?1:2,className:"diff-widget-content",children:n||t})}):n===t?f.jsx("tr",{className:"diff-widget",children:f.jsx("td",{colSpan:r?2:4,className:"diff-widget-content",children:n})}):f.jsxs("tr",{className:"diff-widget",children:[f.jsx("td",{colSpan:r?1:2,className:"diff-widget-content",children:n}),f.jsx("td",{colSpan:r?1:2,className:"diff-widget-content",children:t})]})}var vs=["selectedChanges","monotonous","hideGutter","tokens","lineClassName"],bs=["hunk","widgets","className"];function Ce(e,r){return(e?X(e):"00")+(r?X(r):"00")}function ps(e){var r=e.hunk,n=e.widgets,t=e.className,a=oe(e,bs),s=(function(i,l){for(var o=function(y){if(!y)return null;var w=X(y);return l[w]||null},u=[],c=0;ct.length?n:t,o=n.length>t.length?t:n,u=l.indexOf(o);if(u!=-1)return i=[new r.Diff(1,l.substring(0,u)),new r.Diff(0,o),new r.Diff(1,l.substring(u+o.length))],n.length>t.length&&(i[0][0]=i[2][0]=-1),i;if(o.length==1)return[new r.Diff(-1,n),new r.Diff(1,t)];var c=this.diff_halfMatch_(n,t);if(c){var d=c[0],h=c[1],g=c[2],m=c[3],b=c[4],v=this.diff_main(d,g,a,s),y=this.diff_main(h,m,a,s);return v.concat([new r.Diff(0,b)],y)}return a&&n.length>100&&t.length>100?this.diff_lineMode_(n,t,s):this.diff_bisect_(n,t,s)},r.prototype.diff_lineMode_=function(n,t,a){var s=this.diff_linesToChars_(n,t);n=s.chars1,t=s.chars2;var i=s.lineArray,l=this.diff_main(n,t,!1,a);this.diff_charsToLines_(l,i),this.diff_cleanupSemantic(l),l.push(new r.Diff(0,""));for(var o=0,u=0,c=0,d="",h="";o=1&&c>=1){l.splice(o-u-c,u+c),o=o-u-c;for(var g=this.diff_main(d,h,!1,a),m=g.length-1;m>=0;m--)l.splice(o,0,g[m]);o+=g.length}c=0,u=0,d="",h=""}o++}return l.pop(),l},r.prototype.diff_bisect_=function(n,t,a){for(var s=n.length,i=t.length,l=Math.ceil((s+i)/2),o=l,u=2*l,c=new Array(u),d=new Array(u),h=0;ha);p++){for(var j=-p+b;j<=p-v;j+=2){for(var _=o+j,S=(T=j==-p||j!=p&&c[_-1]s)v+=2;else if(S>i)b+=2;else if(m&&(C=o+g-j)>=0&&C=(k=s-d[C]))return this.diff_bisectSplit_(n,t,T,S,a)}for(var N=-p+y;N<=p-w;N+=2){for(var k,C=o+N,E=(k=N==-p||N!=p&&d[C-1]s)w+=2;else if(E>i)y+=2;else if(!m&&(_=o+g-N)>=0&&_=(k=s-k))return this.diff_bisectSplit_(n,t,T,S,a)}}}return[new r.Diff(-1,n),new r.Diff(1,t)]},r.prototype.diff_bisectSplit_=function(n,t,a,s,i){var l=n.substring(0,a),o=t.substring(0,s),u=n.substring(a),c=t.substring(s),d=this.diff_main(l,o,!1,i),h=this.diff_main(u,c,!1,i);return d.concat(h)},r.prototype.diff_linesToChars_=function(n,t){var a=[],s={};function i(u){for(var c="",d=0,h=-1,g=a.length;hs?n=n.substring(a-s):at.length?n:t,s=n.length>t.length?t:n;if(a.length<4||2*s.length=b.length?[w,p,j,_,k]:null}var o,u,c,d,h,g=l(a,s,Math.ceil(a.length/4)),m=l(a,s,Math.ceil(a.length/2));return g||m?(o=m?g&&g[4].length>m[4].length?g:m:g,n.length>t.length?(u=o[0],c=o[1],d=o[2],h=o[3]):(d=o[0],h=o[1],u=o[2],c=o[3]),[u,c,d,h,o[4]]):null},r.prototype.diff_cleanupSemantic=function(n){for(var t=!1,a=[],s=0,i=null,l=0,o=0,u=0,c=0,d=0;l0?a[s-1]:-1,o=0,u=0,c=0,d=0,i=null,t=!0)),l++;for(t&&this.diff_cleanupMerge(n),this.diff_cleanupSemanticLossless(n),l=1;l=b?(m>=h.length/2||m>=g.length/2)&&(n.splice(l,0,new r.Diff(0,g.substring(0,m))),n[l-1][1]=h.substring(0,h.length-m),n[l+1][1]=g.substring(m),l++):(b>=h.length/2||b>=g.length/2)&&(n.splice(l,0,new r.Diff(0,h.substring(0,b))),n[l-1][0]=1,n[l-1][1]=g.substring(0,g.length-b),n[l+1][0]=-1,n[l+1][1]=h.substring(b),l++),l++}l++}},r.prototype.diff_cleanupSemanticLossless=function(n){function t(b,v){if(!b||!v)return 6;var y=b.charAt(b.length-1),w=v.charAt(0),p=y.match(r.nonAlphaNumericRegex_),j=w.match(r.nonAlphaNumericRegex_),_=p&&y.match(r.whitespaceRegex_),S=j&&w.match(r.whitespaceRegex_),N=_&&y.match(r.linebreakRegex_),k=S&&w.match(r.linebreakRegex_),C=N&&b.match(r.blanklineEndRegex_),E=k&&v.match(r.blanklineStartRegex_);return C||E?5:N||k?4:p&&!_&&S?3:_||S?2:p||j?1:0}for(var a=1;a=g&&(g=m,c=s,d=i,h=l)}n[a-1][1]!=c&&(c?n[a-1][1]=c:(n.splice(a-1,1),a--),n[a][1]=d,h?n[a+1][1]=h:(n.splice(a+1,1),a--))}a++}},r.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/,r.whitespaceRegex_=/\s/,r.linebreakRegex_=/[\r\n]/,r.blanklineEndRegex_=/\n\r?\n$/,r.blanklineStartRegex_=/^\r?\n\r?\n/,r.prototype.diff_cleanupEfficiency=function(n){for(var t=!1,a=[],s=0,i=null,l=0,o=!1,u=!1,c=!1,d=!1;l0?a[s-1]:-1,c=d=!1),t=!0)),l++;t&&this.diff_cleanupMerge(n)},r.prototype.diff_cleanupMerge=function(n){n.push(new r.Diff(0,""));for(var t,a=0,s=0,i=0,l="",o="";a1?(s!==0&&i!==0&&((t=this.diff_commonPrefix(o,l))!==0&&(a-s-i>0&&n[a-s-i-1][0]==0?n[a-s-i-1][1]+=o.substring(0,t):(n.splice(0,0,new r.Diff(0,o.substring(0,t))),a++),o=o.substring(t),l=l.substring(t)),(t=this.diff_commonSuffix(o,l))!==0&&(n[a][1]=o.substring(o.length-t)+n[a][1],o=o.substring(0,o.length-t),l=l.substring(0,l.length-t))),a-=s+i,n.splice(a,s+i),l.length&&(n.splice(a,0,new r.Diff(-1,l)),a++),o.length&&(n.splice(a,0,new r.Diff(1,o)),a++),a++):a!==0&&n[a-1][0]==0?(n[a-1][1]+=n[a][1],n.splice(a,1)):a++,i=0,s=0,l="",o=""}n[n.length-1][1]===""&&n.pop();var u=!1;for(a=1;at));a++)l=s,o=i;return n.length!=a&&n[a][0]===-1?o:o+(t-l)},r.prototype.diff_prettyHtml=function(n){for(var t=[],a=/&/g,s=//g,l=/\n/g,o=0;o");switch(u){case 1:t[o]=''+c+"";break;case-1:t[o]=''+c+"";break;case 0:t[o]=""+c+""}}return t.join("")},r.prototype.diff_text1=function(n){for(var t=[],a=0;athis.Match_MaxBits)throw new Error("Pattern too long for this browser.");var s=this.match_alphabet_(t),i=this;function l(S,N){var k=S/t.length,C=Math.abs(a-N);return i.Match_Distance?k+C/i.Match_Distance:C?1:k}var o=this.Match_Threshold,u=n.indexOf(t,a);u!=-1&&(o=Math.min(l(0,u),o),(u=n.lastIndexOf(t,a+t.length))!=-1&&(o=Math.min(l(0,u),o)));var c,d,h=1<=v;p--){var j=s[n.charAt(p-1)];if(w[p]=b===0?(w[p+1]<<1|1)&j:(w[p+1]<<1|1)&j|(g[p+1]|g[p])<<1|1|g[p+1],w[p]&h){var _=l(b,p-1);if(_<=o){if(o=_,!((u=p-1)>a))break;v=Math.max(1,2*a-u)}}}if(l(b+1,a)>o)break;g=w}return u},r.prototype.match_alphabet_=function(n){for(var t={},a=0;a2&&(this.diff_cleanupSemantic(i),this.diff_cleanupEfficiency(i));else if(n&&typeof n=="object"&&t===void 0&&a===void 0)i=n,s=this.diff_text1(i);else if(typeof n=="string"&&t&&typeof t=="object"&&a===void 0)s=n,i=t;else{if(typeof n!="string"||typeof t!="string"||!a||typeof a!="object")throw new Error("Unknown call format to patch_make.");s=n,i=a}if(i.length===0)return[];for(var l=[],o=new r.patch_obj,u=0,c=0,d=0,h=s,g=s,m=0;m=2*this.Patch_Margin&&u&&(this.patch_addContext_(o,h),l.push(o),o=new r.patch_obj,u=0,h=g,c=d)}b!==1&&(c+=v.length),b!==-1&&(d+=v.length)}return u&&(this.patch_addContext_(o,h),l.push(o)),l},r.prototype.patch_deepCopy=function(n){for(var t=[],a=0;athis.Match_MaxBits?(o=this.match_main(t,d.substring(0,this.Match_MaxBits),c))!=-1&&((h=this.match_main(t,d.substring(d.length-this.Match_MaxBits),c+d.length-this.Match_MaxBits))==-1||o>=h)&&(o=-1):o=this.match_main(t,d,c),o==-1)i[l]=!1,s-=n[l].length2-n[l].length1;else if(i[l]=!0,s=o-c,d==(u=h==-1?t.substring(o,o+d.length):t.substring(o,h+this.Match_MaxBits)))t=t.substring(0,o)+this.diff_text2(n[l].diffs)+t.substring(o+d.length);else{var g=this.diff_main(d,u,!1);if(d.length>this.Match_MaxBits&&this.diff_levenshtein(g)/d.length>this.Patch_DeleteThreshold)i[l]=!1;else{this.diff_cleanupSemanticLossless(g);for(var m,b=0,v=0;vl[0][1].length){var o=t-l[0][1].length;l[0][1]=a.substring(l[0][1].length)+l[0][1],i.start1-=o,i.start2-=o,i.length1+=o,i.length2+=o}return(l=(i=n[n.length-1]).diffs).length==0||l[l.length-1][0]!=0?(l.push(new r.Diff(0,a)),i.length1+=t,i.length2+=t):t>l[l.length-1][1].length&&(o=t-l[l.length-1][1].length,l[l.length-1][1]+=a.substring(0,o),i.length1+=o,i.length2+=o),a},r.prototype.patch_splitMax=function(n){for(var t=this.Match_MaxBits,a=0;a2*t?(u.length1+=h.length,i+=h.length,c=!1,u.diffs.push(new r.Diff(d,h)),s.diffs.shift()):(h=h.substring(0,t-u.length1-this.Patch_Margin),u.length1+=h.length,i+=h.length,d===0?(u.length2+=h.length,l+=h.length):c=!1,u.diffs.push(new r.Diff(d,h)),h==s.diffs[0][1]?s.diffs.shift():s.diffs[0][1]=s.diffs[0][1].substring(h.length))}o=(o=this.diff_text2(u.diffs)).substring(o.length-this.Patch_Margin);var g=this.diff_text1(s.diffs).substring(0,this.Patch_Margin);g!==""&&(u.length1+=g.length,u.length2+=g.length,u.diffs.length!==0&&u.diffs[u.diffs.length-1][0]===0?u.diffs[u.diffs.length-1][1]+=g:u.diffs.push(new r.Diff(0,g))),c||n.splice(++a,0,u)}}},r.prototype.patch_toText=function(n){for(var t=[],a=0;aAs(e.patch),[e.patch]);return f.jsxs("section",{children:[f.jsxs("div",{className:"flex items-baseline justify-between gap-4 flex-wrap",children:[f.jsx("h3",{className:"text-body font-semibold text-fg",children:"Local Changes"}),f.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:[e.changedFiles.length," changed file",e.changedFiles.length===1?"":"s"]})]}),e.rootPath.kind==="known"&&f.jsx("p",{className:"mt-1 text-label text-fg-faint break-all",children:e.rootPath.path}),f.jsx("p",{className:"mt-2 text-label uppercase tracking-wider text-fg-muted",children:Ts(e.comparison)}),r.length===0?f.jsx("p",{className:"mt-5 text-body text-fg-muted italic",children:"No renderable patch in this work tree."}):f.jsx("div",{className:"formula-run-diff-view mt-5 space-y-3",children:r.map(n=>f.jsx(Cs,{file:n},`${n.oldRevision}:${n.newRevision}:${br(n)}`))}),e.truncated&&f.jsx("p",{className:"mt-3 text-label uppercase tracking-wider text-fg-faint",children:"Diff truncated at the backend output cap."})]})}function Cs({file:e}){const r=Ds(e.hunks);return f.jsxs("details",{className:"border-y border-rule py-2",open:!0,children:[f.jsxs("summary",{className:"cursor-pointer list-none text-label uppercase tracking-wider text-fg-muted",children:[f.jsx("span",{className:"font-medium normal-case tracking-normal text-body text-fg",children:br(e)}),f.jsxs("span",{className:"ml-3 tnum text-fg-faint",children:["+",r.additions," -",r.deletions]})]}),e.hunks.length===0||e.isBinary?f.jsx("p",{className:"mt-3 text-body text-fg-muted italic",children:"No textual hunks."}):f.jsx("div",{className:"mt-3 overflow-auto",children:f.jsx(js,{viewType:"unified",diffType:e.type,hunks:e.hunks,renderGutter:Es,children:n=>n.map(t=>f.jsx(vr,{hunk:t},Os(t)))})})]})}function As(e){if(e.trim().length===0)return[];try{return st(e,{nearbySequences:"zip"})}catch{return[]}}function Es({change:e,side:r,renderDefault:n}){return e.type==="insert"&&r==="old"?f.jsx("span",{className:"diff-gutter-sign","aria-hidden":!0,children:"+"}):e.type==="delete"&&r==="new"?f.jsx("span",{className:"diff-gutter-sign","aria-hidden":!0,children:"-"}):n()}function Ts(e){return e.kind==="upstream"?`Compared with ${e.ref} at ${e.mergeBase.slice(0,12)}.`:e.kind==="head"&&e.reason==="no_upstream"?"No upstream branch is configured; showing changes relative to HEAD plus untracked files.":e.kind==="head"?"Upstream comparison failed; showing changes relative to HEAD plus untracked files.":"Comparison unavailable."}function br(e){const r=Fn(e.oldPath),n=Fn(e.newPath);return e.type==="delete"?r:e.type==="rename"&&r!==n?`${r} -> ${n}`:n||r}function Fn(e){return e.replace(/^[ab]\//,"")}function Ds(e){let r=0,n=0;for(const t of e)for(const a of t.changes)a.type==="insert"&&(r+=1),a.type==="delete"&&(n+=1);return{additions:r,deletions:n}}function Os(e){return`${e.oldStart}:${e.newStart}:${e.content}`}function Ms({node:e,visible:r}){const n=A.useMemo(()=>e?.executionInstances.sort(De)??[],[e]),t=A.useMemo(()=>$s(n),[n]),[a,s]=A.useState(null);if(A.useEffect(()=>{s(t?L(t):null)},[e?.id,t]),!e)return f.jsx("p",{className:"text-body text-fg-muted italic",children:"Select a node to inspect its session."});if(n.length===0)return f.jsx("p",{className:"text-body text-fg-muted italic",children:xn(e)});const i=n.find(c=>L(c)===a)??t??n[0],l=i?be(i):"base",o=Bs(n),u=n.filter(c=>be(c)===l);return i?f.jsxs("section",{children:[f.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[f.jsx("h3",{className:"text-body font-semibold text-fg",children:e.title}),(e.historicalOnly||i?.historical)&&f.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:e.historicalOnly?"historical-only":"historical"})]}),o.length>1&&f.jsxs("div",{className:"mt-3 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-label",role:"radiogroup","aria-label":"Iterations",children:[f.jsx("span",{className:"uppercase tracking-wider text-fg-faint",children:"Iterations"}),o.map(c=>{const d=c.instances.at(-1);if(!d)return null;const h=c.iteration==="base"?"Base":`Iteration ${c.iteration}`,g=c.iteration===l;return f.jsxs("span",{className:"flex items-baseline gap-1",children:[f.jsx("span",{"aria-hidden":!0,className:"text-fg-faint",children:"·"}),f.jsx("button",{type:"button",role:"radio","aria-checked":g,className:`focus-mark rounded-sm px-0.5 uppercase tracking-wider ${g?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,onClick:()=>s(L(d)),children:h})]},h)})]}),u.length>1&&f.jsxs("div",{className:"mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-label",role:"radiogroup","aria-label":"Attempts",children:[f.jsx("span",{className:"uppercase tracking-wider text-fg-faint",children:"Attempts"}),u.map(c=>f.jsxs("span",{className:"flex items-baseline gap-1",children:[f.jsx("span",{"aria-hidden":!0,className:"text-fg-faint",children:"·"}),f.jsxs("button",{type:"button",role:"radio","aria-checked":L(c)===L(i),className:`focus-mark rounded-sm px-0.5 uppercase tracking-wider ${L(c)===L(i)?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,onClick:()=>s(L(c)),children:["Attempt ",Je(c)]})]},L(c)))]}),f.jsxs("dl",{className:"mt-4 grid grid-cols-[max-content_minmax(0,1fr)] gap-x-3 gap-y-1 text-label",children:[f.jsx("dt",{className:"uppercase tracking-wider text-fg-faint",children:"Execution instance"}),f.jsx("dd",{className:"break-all text-fg-muted tnum",children:i.id}),f.jsx("dt",{className:"uppercase tracking-wider text-fg-faint",children:"Bead"}),f.jsx("dd",{className:"break-all text-fg-muted tnum",children:i.beadId})]}),f.jsx(Is,{instance:i,visible:r})]}):f.jsx("p",{className:"text-body text-fg-muted italic",children:xn(e)})}function Is({instance:e,visible:r}){const n=e.session.kind==="attached"?e.session:null,t=n?.link.sessionId??null,a=r&&!!n?.streamable,s=Fr(t,a);if(n===null)return f.jsx("p",{className:"mt-5 text-body text-fg-muted italic",children:Rs(e)});const i=Ps(s.stream),l=s.status==="loading",o=s.status==="ready"?s.result:null,u=s.status==="failed"?s.error:null,c=s.status==="ready"&&s.stream.status==="degraded"?s.stream.error:null;return f.jsxs("div",{className:"mt-5 space-y-4",children:[n?.streamable&&f.jsx("div",{className:"flex justify-end",children:f.jsx(kr,{tone:i.tone,label:i.label,title:`Session stream: ${s.stream.status}`,className:"text-label uppercase tracking-wider"})}),c!==null&&f.jsx("p",{className:"text-accent",role:"alert",children:c}),f.jsx(xr,{loading:l,error:u,result:o})]})}function Ps(e){switch(e.status){case"open":return{tone:"ok",label:"live"};case"connecting":return{tone:"warn",label:"connecting"};case"closed":return{tone:"stuck",label:"offline"};case"degraded":return{tone:"warn",label:"degraded"};case"idle":return{tone:"neutral",label:"snapshot"}}}function xn(e){const r=e.executionInstances.filter(t=>t.session.kind==="none");return r.some(t=>t.currentIteration&&t.session.kind==="none"&&t.session.reason==="session_unresolved"&&pr(t.status))?"Session unresolved for the current running node.":r.some(t=>t.session.kind==="none"&&t.session.reason==="session_unresolved")?"Session unresolved for this node.":"This node has not started a session yet."}function Rs(e){return e.session.kind==="attached"?"":e.currentIteration&&e.session.reason==="session_unresolved"&&pr(e.status)?"Session unresolved for the current running node.":e.session.reason==="session_unresolved"?"Session unresolved for this node.":"This node has not started a session yet."}function pr(e){return e==="active"||e==="running"}function $s(e){return e.find(r=>r.session.kind==="attached"&&r.session.streamable)??[...e].filter(r=>r.session.kind==="attached").sort(De).at(-1)??[...e].sort(De).at(-1)}function Bs(e){const r=new Map;for(const n of e){const t=be(n);r.set(t,[...r.get(t)??[],n])}return[...r.entries()].map(([n,t])=>({iteration:n,instances:t.sort(De)})).sort((n,t)=>Oe(n.iteration)-Oe(t.iteration))}function De(e,r){return Oe(be(e))-Oe(be(r))||Je(e)-Je(r)||e.id.localeCompare(r.id)}function L(e){return e.id}function be(e){return e.iteration.kind==="loop"?e.iteration.value:"base"}function Oe(e){return e==="base"?0:e}function Je(e){return e.attempt.kind==="attempt"?e.attempt.value:1}function Fs({tab:e,diff:r,selectedNode:n}){return e==="session"?f.jsx(Ms,{node:n,visible:!0}):f.jsx(xs,{diff:r})}function xs({diff:e}){switch(e.kind){case"idle":return f.jsx("p",{className:"text-body text-fg-muted italic",children:"Local changes are not loaded for this run."});case"loading":return f.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading local changes."});case"failed":return f.jsx("p",{className:"text-body text-accent",role:"alert",children:e.error});case"ready":return f.jsxs(f.Fragment,{children:[e.refreshState.kind==="failed"&&f.jsx("p",{className:"mb-4 text-body text-accent",role:"alert",children:e.refreshState.error}),e.refreshState.kind==="refreshing"&&f.jsx("p",{className:"mb-4 text-label uppercase tracking-wider text-fg-faint",role:"status",children:"Refreshing local changes"}),f.jsx(Ss,{diff:e.diff})]})}}function Gs({diff:e,selectedNode:r}){const[n,t]=A.useState("diff"),a=`run-evidence-tab-${n}`;return f.jsxs("section",{"aria-label":"Run evidence",children:[f.jsxs("div",{className:"flex items-baseline gap-2 text-label",role:"tablist","aria-label":"Run evidence views",children:[f.jsx(Gn,{id:"run-evidence-tab-diff",controls:"run-evidence-panel",active:n==="diff",onClick:()=>t("diff"),children:"Diff"}),f.jsx("span",{"aria-hidden":!0,className:"text-fg-faint",children:"·"}),f.jsx(Gn,{id:"run-evidence-tab-session",controls:"run-evidence-panel",active:n==="session",onClick:()=>t("session"),children:"Session"})]}),f.jsx("div",{id:"run-evidence-panel",role:"tabpanel","aria-labelledby":a,className:"pt-5",children:f.jsx(Fs,{tab:n,diff:e,selectedNode:r})})]})}function Gn({id:e,controls:r,active:n,disabled:t=!1,onClick:a,children:s}){return f.jsx("button",{id:e,type:"button",role:"tab","aria-selected":n,"aria-controls":r,"aria-disabled":t||void 0,disabled:t,className:`focus-mark rounded-sm px-0.5 uppercase tracking-wider ${t?"cursor-not-allowed text-fg-faint":n?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,onClick:a,children:s})}function Ls(e,r){const n=e.runIds.size===0||e.runIds.has(r.runId),t=e.rootBeadIds.size===0||e.rootBeadIds.has(r.rootBeadId);return n&&t}function Us(e){const r={runIds:new Set,rootBeadIds:new Set};return V(e,r),V(R(e.run),r),V(R(e.payload),r),V(R(R(e.payload)?.run),r),V(R(e.bead),r),V(R(R(e.payload)?.bead),r),V(R(e.root),r),V(R(R(e.payload)?.root),r),Qe(R(e.metadata),r),Qe(R(R(e.payload)?.metadata),r),r}function V(e,r){e&&(U(r.runIds,e.run_id),U(r.runIds,e.workflow_id),U(r.rootBeadIds,e.root_bead_id),Qe(R(e.metadata),r))}function Qe(e,r){e&&(U(r.runIds,e["gc.run_id"]),U(r.runIds,e["gc.workflow_id"]),U(r.runIds,e.run_id),U(r.runIds,e.workflow_id),U(r.rootBeadIds,e["gc.root_bead_id"]),U(r.rootBeadIds,e.root_bead_id))}function U(e,r){if(typeof r!="string")return;const n=r.trim();n&&e.add(n)}function R(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0}function zs(e,r,n){const[t,a]=A.useState({nodeId:null,routeKey:"",source:"route"});A.useEffect(()=>{if(!e)return;const u=Ks(e,r);a(c=>c.routeKey===n&&(c.source==="user"||c.nodeId===u)?c:{nodeId:u,routeKey:n,source:"route"})},[e,n,r]);const s=A.useCallback(()=>{a(u=>({nodeId:null,routeKey:u.routeKey,source:"user"}))},[]);A.useEffect(()=>{const u=c=>{c.key==="Escape"&&s()};return window.addEventListener("keydown",u),()=>window.removeEventListener("keydown",u)},[s]);const i=A.useCallback(u=>{a(c=>({nodeId:c.nodeId===u?null:u,routeKey:n,source:"user"}))},[n]),l=t.nodeId,o=A.useMemo(()=>e?.nodes.find(u=>u.id===l)??null,[e,l]);return{selectedNodeId:l,selectedNode:o,toggleNode:i,clearSelection:s}}function Ks(e,r){return r&&e.nodes.some(n=>n.id===r)?r:null}const Hs=[600,1200,2400];async function Vs(e){for(let r=0;;r+=1)try{return await Kn.runDetail(e)}catch(n){const t=Hs[r];if(t!==void 0&&Ws(n)){await Xs(t);continue}throw n}}function Ws(e){return e instanceof Ue?e.status>=500:e instanceof TypeError}function Xs(e){return new Promise(r=>setTimeout(r,e))}function Zs(e,r,n){const t=ei(e,r,n),{data:a,loading:s,error:i,refresh:l}=Hn(t,()=>Ys(e),{onError:o=>{e!==void 0&&qs("load detail",e,o)}});return e===void 0?{kind:"idle",refresh:Js}:a?.kind==="loaded"?{kind:"ready",detail:a.detail,refresh:l,refreshState:Qs(s,i)}:a?.kind==="unsupported"?{kind:"unsupported",refresh:l}:a?.kind==="not_found"?{kind:"not_found",refresh:l}:i!==null?{kind:"failed",error:i,refresh:l}:{kind:"loading",refresh:l}}async function Ys(e){if(!e)return{kind:"unrequested"};try{return{kind:"loaded",detail:await Vs(e)}}catch(r){if(r instanceof Ue&&r.status===422&&r.reason==="not_run_view")return{kind:"unsupported"};if(r instanceof Ue&&r.status===404)return{kind:"not_found"};throw r}}async function Js(){}function Qs(e,r){return r!==null?{kind:"failed",error:r}:e?{kind:"refreshing"}:{kind:"idle"}}function qs(e,r,n){Vn({component:"formula-run-detail",operation:e,message:`${r}: ${Wn(n)}`})}function ei(e,r,n){return["formula-run",e??"missing",r??"default",n??"default"].map(encodeURIComponent).join(":")}function ni(e,r,n,t){const a=si(e,r,n,t),{data:s,loading:i,error:l,refresh:o}=Hn(a,()=>Ln(e,r,n,t),{refreshFetcher:()=>Ln(e,r,n,t,!0),onError:u=>{e!==void 0&&ai("load diff",e,u)}});return e===void 0||r===void 0?{kind:"idle",refresh:ri}:s?.kind==="loaded"?{kind:"ready",diff:s.diff,refresh:o,refreshState:ti(i,l)}:l!==null?{kind:"failed",error:l,refresh:o}:{kind:"loading",refresh:o}}async function Ln(e,r,n,t,a){if(!e||r===void 0)return{kind:"unrequested"};const s={};return n!==void 0&&(s.scopeKind=n),t!==void 0&&(s.scopeRef=t),a&&(s.refresh=!0),{kind:"loaded",diff:await Kn.runDiff(e,{executionPath:r},s)}}async function ri(){}function ti(e,r){return r!==null?{kind:"failed",error:r}:e?{kind:"refreshing"}:{kind:"idle"}}function ai(e,r,n){Vn({component:"formula-run-detail",operation:e,message:`${r}: ${Wn(n)}`})}function si(e,r,n,t){return["formula-run-diff",e??"missing",ii(r),n??"default",t??"default"].join(":")}function ii(e){return e===void 0?"path:missing":e.kind==="known"?`path:${e.path}`:`path:${e.reason}`}const oi=[mn.bead,mn.session],li=[],ui=["completed","done","failed","skipped"],ci=["pending","ready","running","active","blocked"];function Ii(){const{runId:e}=Cr(),[r]=Ar(),n=Ni(r),t=n.ok?n.scope:void 0,a=n.ok?null:n.error,s=r.get("node"),i=[e??"",t?.scopeKind??"",t?.scopeRef??"",s??""].join("\0"),l=Zs(a?void 0:e,t?.scopeKind,t?.scopeRef),o=l.kind==="ready"?l:null,u=o?.detail??null,c=l.kind==="unsupported",d=l.kind==="not_found",h=ni(a||u===null?void 0:e,u?.executionPath,t?.scopeKind,t?.scopeRef),g=l.kind==="loading",m=o!==null&&o.refreshState.kind==="refreshing"||h.kind==="ready"&&h.refreshState.kind==="refreshing",b=u!==null&&h.kind==="loading",v=g||m||b,y=l.kind==="failed"?l.error:o!==null&&o.refreshState.kind==="failed"?o.refreshState.error:null;Er(a?li:oi,()=>{Un(l.refresh,h.refresh)},{matches:I=>{if(u===null)return!1;const B=Us(I);return di(u.progress)&&fi(B)?!1:Ls(B,{runId:u.runId,rootBeadId:u.rootBeadId})}});const w=a??y,{selectedNodeId:p,selectedNode:j,toggleNode:_}=zs(u,s,i),S=Rr(u?.rootBeadId??null),[N,k]=A.useState(null),C=Tr(),E=Ir(),[T]=A.useState(()=>Dr(`runs:summary:${E??"no-city"}`)),$=A.useMemo(()=>{if(!e)return null;const I=T&&T.status!=="error"?T.data:null;return I==null?null:[...I.lanes,...I.blockedLanes].find(B=>B.id===e)??null},[T,e]),O=u?`${u.progress.visibleNodeCount} nodes. ${ji(u.progress)}. Local changes are shown for the run execution folder.`:g&&!a||c||d?void 0:"Formula run unavailable.";return f.jsxs("section",{children:[f.jsx(Pr,{title:u?.title??"Formula Run",synopsis:O,meta:f.jsxs(f.Fragment,{children:[f.jsx(Or,{to:"/runs",className:"focus-mark text-label uppercase tracking-wider text-fg-muted hover:text-fg",children:"Runs"}),w&&u&&f.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:w}),u&&f.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:mi(u)}),f.jsx(Mr,{size:"sm",onClick:()=>{Un(l.refresh,h.refresh)},disabled:v||!!a,children:m?"Refreshing":"Refresh"})]})}),v&&!a&&!u?$?f.jsxs(f.Fragment,{children:[f.jsx(vn,{stages:$.stages,label:$.title}),f.jsx("p",{className:"text-body text-fg-muted italic mt-8",children:"Loading run detail."})]}):f.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading formula run."}):c?f.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"Detailed step view isn’t available for this run (v1/wisp runs are list-only) — this run appears in the run list only."}):d?f.jsx("p",{className:"text-body text-fg-muted",role:"status",children:"This run’s detail snapshot was not found. It may be a v1/wisp run, a completed run whose snapshot wasn’t retained, or no longer available."}):w&&!u?f.jsx("p",{className:"text-body text-accent",role:"alert",children:w}):o?f.jsxs(f.Fragment,{children:[f.jsx(hi,{detail:o.detail}),f.jsx(vn,{stages:o.detail.stages,label:o.detail.title}),f.jsx(bi,{detail:o.detail}),f.jsxs("div",{className:"mt-8 grid gap-10 lg:grid-cols-[minmax(0,0.95fr)_minmax(22rem,1.05fr)]",children:[f.jsx(Wr,{detail:o.detail,selectedNodeId:p,onToggleNode:_}),f.jsx(Gs,{diff:h,selectedNode:j})]}),f.jsx($r,{view:S.view,loading:S.loading,error:S.error,now:C,onOpenBead:k}),f.jsx(Br,{open:N!==null,onClose:()=>k(null),beadId:N,onOpenBead:k})]}):null]})}async function Un(e,r){await Promise.all([e(),r()])}function fi(e){return e.runIds.size===0&&e.rootBeadIds.size===0}function di(e){return e.visibleNodeCount<=0||ci.reduce((t,a)=>t+(e.statusCounts[a]??0),0)>0?!1:ui.reduce((t,a)=>t+(e.statusCounts[a]??0),0)>=e.visibleNodeCount}function hi({detail:e}){const r=vi(e.formulaDetail);return f.jsxs("dl",{className:"grid gap-x-8 gap-y-3 sm:grid-cols-2 lg:grid-cols-4",children:[f.jsx(gi,{formula:e.formula}),r!==null&&f.jsx(ie,{label:"Formula Detail",value:r}),f.jsx(ie,{label:"Root",value:e.rootBeadId}),f.jsx(ie,{label:"Scope",value:`${e.scopeKind}:${e.scopeRef}`}),f.jsx(ie,{label:"Store",value:e.resolvedRootStore||e.rootStoreRef||"unknown"})]})}function ie({label:e,value:r}){return f.jsxs("div",{children:[f.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint",children:e}),f.jsx("dd",{className:"text-body text-fg break-all tnum",children:r})]})}const zn="name inferred from bead title — supervisor did not set gc.formula on this graph.v2 root";function gi({formula:e}){if(e.kind!=="known")return f.jsx(ie,{label:"Formula",value:"metadata missing"});switch(e.source){case"metadata":return f.jsx(ie,{label:"Formula",value:e.name});case"title_fallback":return f.jsxs("div",{children:[f.jsx("dt",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Formula"}),f.jsxs("dd",{className:"text-body text-warn break-all tnum",title:zn,"aria-label":`${e.name} (${zn})`,children:[e.name,f.jsx("span",{className:"ml-2 text-label uppercase tracking-wider text-warn",children:"inferred from bead title"})]})]});default:return e.source}}function mi(e){return e.snapshotEventSeq.kind==="known"?`v${e.snapshotVersion} · seq ${e.snapshotEventSeq.seq}`:`v${e.snapshotVersion}`}function vi(e){return e.kind==="available"?`available for ${e.target}`:e.reason==="missing_formula_metadata"?null:e.reason==="missing_run_target"?`missing run target for ${e.name}`:`${e.failure} for ${e.target}`}function bi({detail:e}){if(e.completeness.kind!=="partial")return null;const r=pi(e.completeness.reasons);return r.length===0?null:f.jsxs("p",{className:"mt-5 text-label uppercase tracking-wider text-warn",role:"status",children:["Partial run data: ",wi(r),"."]})}function pi(e){return e.filter(r=>!yi(r))}function yi(e){switch(e){case"formula_detail_missing_formula_metadata":case"formula_detail_missing_run_target":case"formula_detail_fetch_failed":return!0;case"supervisor_snapshot_partial":case"runtime_bead_read_failed":case"session_list_failed":return!1}}function wi(e){return e.map(_i).join(", ")}function _i(e){switch(e){case"supervisor_snapshot_partial":return"supervisor snapshot is partial";case"runtime_bead_read_failed":return"runtime bead refresh failed";case"session_list_failed":return"session list failed";case"formula_detail_missing_formula_metadata":return"formula metadata is missing";case"formula_detail_missing_run_target":return"formula run target is missing";case"formula_detail_fetch_failed":return"formula detail fetch failed"}}function Ni(e){const r=e.getAll("scope_kind"),n=e.getAll("scope_ref");if(r.length>1||n.length>1)return{ok:!1,error:"Invalid run scope query."};const t=r[0],a=n[0];return t===void 0&&a===void 0?{ok:!0}:t===void 0||a===void 0?{ok:!1,error:"Invalid run scope query."}:t!=="city"&&t!=="rig"?{ok:!1,error:"Invalid run scope query."}:Gr.test(a)?{ok:!0,scope:{scopeKind:t,scopeRef:a}}:{ok:!1,error:"Invalid run scope query."}}function ji(e){const r=[Y(e,["active","running"],"running"),Y(e,["completed","done"],"done"),Y(e,"ready","ready"),Y(e,"blocked","blocked"),Y(e,"failed","failed"),Y(e,"skipped","skipped"),Y(e,"pending","pending")].filter(n=>n!==null);return r.length>0?r.join(", "):"No node status yet"}function Y(e,r,n){const a=(typeof r=="string"?[r]:r).reduce((s,i)=>s+(e.statusCounts[i]??0),0);return a>0?`${a} ${n}`:null}export{Ii as FormulaRunDetailPage}; diff --git a/internal/api/dashboardspa/dist/assets/Health-B0fm2qWB.js b/internal/api/dashboardspa/dist/assets/Health-B0fm2qWB.js deleted file mode 100644 index 1805219f89..0000000000 --- a/internal/api/dashboardspa/dist/assets/Health-B0fm2qWB.js +++ /dev/null @@ -1 +0,0 @@ -import{a as Y,b as v,r as Z,j as t,B as ee,ab as y,z as E,S as V,I as F,a7 as te}from"./index-zPatq59W.js";import{p as $,d as ae}from"./routeHighlight-B30gQO2o.js";import{P as se}from"./PageHeader-DGfr1hUc.js";import{u as le}from"./useVisibleRefresh-DdfUjLcH.js";import{a as x}from"./format-fte2CeYD.js";import{a as ne}from"./time-D9v0saHV.js";const re=2500;function Ee(){const e=Y(),a=F(),s=v("health:system",ye),o=v(`health:supervisor:${a??"no-city"}`,_e),r=v(`health:status:${a??"no-city"}`,we),c=v("health:local-tools",Ne),b=v(`health:dolt-noms-trend:${a??"no-city"}`,Se),p=v(`health:rig-store:${a??"no-city"}`,ke),d=s.refresh,_=o.refresh,w=r.refresh,N=c.refresh,C=b.refresh,L=p.refresh,W=s.loading||o.loading||r.loading||c.loading||b.loading||p.loading,T=[s.error,o.error,r.error,c.error,b.error,p.error].filter(J=>J!==null).join("; ")||null,D=Z.useCallback(async()=>{await Promise.all([d(),_(),w(),N(),C(),L()])},[C,N,L,_,w,d]),m=s.data??null,n=m?.status==="available"?m.data:null,S=m?.status==="unavailable"?m.error:null,i=o.data??null,k=r.data??null,U=c.data??null,u=b.data??null,f=p.data??null,B=f?pe(f):void 0,R=m!==null||i!==null||k!==null||U!==null||u!==null||f!==null,M=n?He(n):void 0,q=$(e,"health",["health:supervisor-"]),Q=$(e,"health",["health:load-","health:memory-"]),X=$(e,"health",["health:dashboard-"]),G=$(e,"health",["health:dolt-noms-"]);return le(D,3e4),t.jsxs("section",{children:[t.jsx(se,{title:"Health",synopsis:R?$e(n,i):"Reading state from the supervisor.",meta:t.jsxs(t.Fragment,{children:[T&&t.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:T}),t.jsx(ee,{size:"sm",onClick:()=>{D()},children:W&&!R?"Loading":"Refresh"})]})}),R?t.jsxs("div",{className:"space-y-12",children:[t.jsx(h,{title:"Supervisor",attention:q,...i?{status:Re(i)}:{},children:i===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading supervisor state."}):i.status==="available"?t.jsxs(g,{children:[i.data.city!==void 0?t.jsx(l,{label:"City",value:i.data.city}):t.jsx(l,{label:"City",value:"not reported by supervisor",tone:"warn"}),i.data.version!==void 0?t.jsx(l,{label:"Version",value:i.data.version}):t.jsx(l,{label:"Version",value:"not reported by supervisor",tone:"warn"}),t.jsx(l,{label:"Uptime",value:j(i.data.uptime_sec)}),t.jsx(l,{label:"Status",value:i.data.status})]}):t.jsx("p",{className:"text-body text-accent",children:"Supervisor not reachable. The dashboard shell stays up; live data is stale."})}),t.jsx(h,{title:"Host",attention:Q,...M?{status:M}:{},children:m===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading dashboard host health."}):n===null?t.jsxs("p",{className:"text-body text-accent",children:["Dashboard host health unavailable",S?`: ${S}`:"","."]}):t.jsxs(g,{children:[t.jsx(l,{label:"CPUs",value:n.host.cpu_count.toString()}),t.jsx(l,{label:"Load (1m, 5m, 15m)",value:`${n.host.load_avg_1.toFixed(2)}, ${n.host.load_avg_5.toFixed(2)}, ${n.host.load_avg_15.toFixed(2)}`,...n.host.load_avg_1>n.host.cpu_count?{tone:"warn"}:{}}),t.jsx(l,{label:"Memory free",value:`${x(n.host.free_mem_bytes)} of ${x(n.host.total_mem_bytes)}`,...n.host.free_mem_bytes/n.host.total_mem_bytes<.1?{tone:"warn"}:{}}),t.jsx(l,{label:"Host uptime",value:j(n.host.uptime_sec)})]})}),t.jsx(h,{title:"Admin process",attention:X,children:m===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading dashboard process health."}):n===null?t.jsxs("p",{className:"text-body text-accent",children:["Dashboard process health unavailable",S?`: ${S}`:"","."]}):t.jsxs(g,{children:[t.jsx(l,{label:"PID",value:n.admin.pid.toString()}),t.jsx(l,{label:"Uptime",value:j(n.admin.uptime_sec)}),t.jsx(l,{label:"RSS",value:x(n.admin.rss_bytes)}),t.jsx(l,{label:"Heap used",value:x(n.admin.heap_used_bytes)}),t.jsx(l,{label:"Node",value:n.admin.node_version})]})}),t.jsx(h,{title:"Tool versions",children:t.jsx(oe,{state:U})}),t.jsx(h,{title:"Diagnostics",children:t.jsxs("div",{className:"space-y-8",children:[t.jsx(ce,{usage:K(k)}),t.jsx(ue,{usage:Ce(k)})]})}),t.jsx(h,{title:"Bead stores · per rig",meta:be(f),...B?{status:B}:{},children:t.jsx(de,{report:f})}),t.jsx(h,{title:"Store thresholds",children:t.jsx(ve,{comparison:Le(k)})}),t.jsx(h,{title:"Dolt-noms · 24 h",attention:G,meta:u&&u.samples.length>0?`${u.samples.length} samples`:void 0,children:u===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading."}):u.available?u.samples.length===0?t.jsx("p",{className:"text-body text-fg-muted italic",children:"No samples yet. Backend just started; next sample in ten minutes or less."}):t.jsx(ge,{samples:u.samples}):t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Dolt-noms metric unavailable: ",je(u.reason),"."]})})]}):t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading."})]})}function h({title:e,status:a,meta:s,attention:o,children:r}){return t.jsxs("section",{...ae(o??null),children:[t.jsxs("header",{className:"flex items-baseline justify-between gap-4 mb-4 pb-2 border-b border-rule",children:[t.jsx("h2",{className:"text-headline font-semibold text-fg",children:e}),t.jsxs("div",{className:"flex items-baseline gap-4",children:[s&&t.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:s}),a&&t.jsx(V,{tone:a.tone,label:a.label})]})]}),r]})}function g({children:e}){return t.jsx("dl",{className:"grid grid-cols-[max-content_1fr] gap-x-8 gap-y-3 max-w-prose",children:e})}function l({label:e,value:a,tone:s}){const o=s==="warn"?"text-warn":s==="stuck"?"text-accent":"text-fg";return t.jsxs(t.Fragment,{children:[t.jsx("dt",{className:"text-body text-fg-muted",children:e}),t.jsx("dd",{className:`text-body tnum font-medium ${o}`,children:a})]})}function oe({state:e}){if(e===null)return t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading tool versions."});if(e.status==="unavailable")return t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Tool versions unavailable: ",e.error,"."]});const a=[{label:"gc",tool:e.data.gc},{label:"bd",tool:e.data.beads},{label:"dolt",tool:e.data.dolt}];return t.jsxs("div",{className:"grid grid-cols-[1fr_max-content] gap-x-8 gap-y-3 max-w-prose",children:[t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Tool"}),t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted text-right",children:"Installed"}),a.map(s=>t.jsx(ie,{label:s.label,tool:s.tool},s.label))]})}function ie({label:e,tool:a}){return t.jsxs("div",{className:"contents","data-tool-version-row":e,children:[t.jsx("div",{className:"text-body text-fg",children:e}),t.jsx("div",{className:"text-right",children:a.status==="available"?t.jsx("span",{className:"text-body tnum font-medium text-fg",children:a.version}):t.jsxs("div",{className:"space-y-1",children:[t.jsx("div",{className:"text-body tnum font-medium text-warn",children:"unavailable"}),t.jsx("div",{className:"text-label text-fg-muted normal-case",children:a.reason})]})})]})}function ce({usage:e}){if(e.status==="unavailable")return t.jsx(O,{heading:"Dolt usage",reason:e.reason});const a=e.value;return t.jsxs("div",{className:"space-y-2",children:[t.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Dolt usage"}),e.stale!==void 0&&t.jsx(H,{message:e.stale}),t.jsxs(g,{children:[t.jsx(l,{label:"On-disk size",value:x(Te(a.size_bytes))}),t.jsx(l,{label:"Live rows",value:a.live_rows.toLocaleString()}),t.jsx(l,{label:"MB per row",value:a.ratio_mb_per_row.toString()}),t.jsx(l,{label:"Last maintenance",value:a.last_gc_status??"not reported",...a.last_gc_status!==void 0&&a.last_gc_status!=="success"?{tone:"warn"}:{}}),a.last_gc_at!==void 0&&t.jsx(l,{label:"Last maintenance at",value:ne(a.last_gc_at)}),t.jsx(l,{label:"Store path",value:a.path})]})]})}function ue({usage:e}){if(e.status==="unavailable")return t.jsx(O,{heading:"Beads usage",reason:e.reason});const a=e.value;return t.jsxs("div",{className:"space-y-2",children:[t.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Beads usage"}),e.stale!==void 0&&t.jsx(H,{message:e.stale}),t.jsxs(g,{children:[t.jsx(l,{label:"Open",value:a.open.toString()}),t.jsx(l,{label:"Ready",value:a.ready.toString()}),t.jsx(l,{label:"In progress",value:a.in_progress.toString()})]})]})}function de({report:e}){if(e===null)return t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading per-rig store health."});if(!e.available&&e.rigs.length===0)return t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Per-rig store health unavailable: ",P(e.reason),"."]});const a=[...e.rigs].sort((s,o)=>A(o.rollup)-A(s.rollup));return t.jsxs("div",{className:"space-y-6 max-w-prose",children:[!e.available&&t.jsxs("p",{className:"text-body text-warn italic",children:["Showing the last sample; refresh failed: ",P(e.reason),"."]}),a.map(s=>t.jsx(he,{rig:s},s.rig))]})}function he({rig:e}){const a=xe(e);return t.jsxs("div",{className:"space-y-2 border-b border-rule pb-4 last:border-b-0",children:[t.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[t.jsx("span",{className:"text-body font-medium text-fg",children:e.rig}),t.jsx(V,{tone:a.tone,label:a.label})]}),t.jsxs("dl",{className:"grid grid-cols-[max-content_1fr] gap-x-6 gap-y-1",children:[t.jsx(l,{label:"Dolt server",value:me(e),...e.doltConnected===!1?{tone:"stuck"}:{}}),e.issueCount!==null&&t.jsx(l,{label:"Live issues",value:e.issueCount.toLocaleString()})]}),e.problems.length>0&&t.jsx("ul",{className:"space-y-1",children:e.problems.map(s=>t.jsxs("li",{className:`text-label ${s.status==="error"?"text-accent":"text-warn"}`,children:[s.name,": ",s.message]},`${s.category}/${s.name}`))}),e.note!==void 0&&t.jsx("p",{className:"text-label text-fg-muted italic",children:e.note})]})}function me(e){const a=e.doltEndpoint??"no endpoint reported";return e.doltConnected===!0?`up · ${a}`:e.doltConnected===!1?`DOWN · ${a}`:`unknown · ${a}`}function xe(e){switch(e.rollup){case"ok":return{tone:"ok",label:"healthy"};case"warn":return{tone:"warn",label:"warnings"};case"down":return e.reachable?e.doltConnected===!1?{tone:"stuck",label:"dolt down"}:{tone:"stuck",label:"errors"}:{tone:"stuck",label:"unreachable"}}}function A(e){return e==="down"?2:e==="warn"?1:0}function be(e){if(e===null||e.rigs.length===0)return;const a={ok:0,warn:0,down:0};for(const s of e.rigs)a[s.rollup]+=1;return`${a.ok} ok · ${a.warn} warn · ${a.down} down`}function pe(e){if(e.rigs.some(a=>a.rollup==="down"))return{tone:"stuck",label:"attention"};if(e.rigs.some(a=>a.rollup==="warn"))return{tone:"warn",label:"warnings"};if(e.rigs.length>0)return{tone:"ok",label:"healthy"}}function P(e){switch(e){case"not_sampled_yet":return"backend just started; first sample is in flight";case"rig_list_failed":return"the supervisor rig list could not be read";case"fetch_failed":return"the dashboard backend could not be reached"}}function ve({comparison:e}){return e.status==="unavailable"?t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Comparison unavailable: ",e.reason,"."]}):t.jsxs("div",{className:"space-y-2",children:[e.stale!==void 0&&t.jsx(H,{message:e.stale}),t.jsxs("div",{className:"grid grid-cols-[1fr_max-content_max-content] gap-x-8 gap-y-3 max-w-prose",children:[t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Setting"}),t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted text-right",children:"Recommended"}),t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted text-right",children:"Loaded"}),e.value.map(a=>t.jsx(fe,{row:a},a.label))]})]})}function fe({row:e}){const a=e.withinRecommendation?"text-fg":"text-warn";return t.jsxs("div",{className:`contents ${a}`,"data-comparison-row":e.label,children:[t.jsxs("div",{className:`text-body ${a}`,children:[e.label,!e.withinRecommendation&&t.jsx("span",{className:"text-label uppercase tracking-wider text-warn",children:" · over"})]}),t.jsx("div",{className:"text-body tnum text-fg-muted text-right",children:e.recommended}),t.jsx("div",{className:`text-body tnum font-medium text-right ${a}`,children:e.loaded})]})}function O({heading:e,reason:a}){return t.jsxs("div",{className:"space-y-2",children:[t.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:e}),t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Unavailable: ",a,"."]})]})}function H({message:e}){return t.jsx("p",{className:"text-body text-warn italic",children:e})}function ge({samples:e}){if(e.length===0)return null;const a=Math.max(...e.map(d=>d.bytes)),s=Math.min(...e.map(d=>d.bytes)),o=a-s||1,r=600,c=60,b=e.length>1?r/(e.length-1):r,p=e.map((d,_)=>{const w=_*b,N=c-(d.bytes-s)/o*c;return`${w.toFixed(1)},${N.toFixed(1)}`}).join(" ");return t.jsxs("div",{className:"space-y-3 max-w-prose",children:[t.jsx("svg",{viewBox:`0 0 ${r} ${c}`,preserveAspectRatio:"none",className:"w-full h-16","aria-label":"24 hour dolt-noms size trend",children:t.jsx("polyline",{fill:"none",stroke:"currentColor",strokeWidth:"1",className:"text-accent",points:p})}),t.jsxs("div",{className:"flex items-baseline justify-between text-label uppercase tracking-wider text-fg-muted tnum",children:[t.jsxs("span",{children:["min ",x(s)]}),t.jsxs("span",{children:["max ",x(a)]})]})]})}function je(e){switch(e){case"store_health_absent":return"supervisor is not reporting store_health; samples resume when it recovers";case"sample_failed":return"latest supervisor status read failed; check the backend log"}}async function ye(){try{return{status:"available",data:await y.systemHealth()}}catch(e){return{status:"unavailable",error:E(e,"dashboard host health unavailable")}}}async function _e(){const e=F();if(e===null)throw new Error("Health page loaded before an active city was resolved");try{return{status:"available",data:await te(re).cityHealth(e)}}catch{return{status:"unavailable",error:"supervisor health unavailable"}}}function z(e){switch(e){case"not_sampled_yet":return"supervisor status sample is warming up; data appears after the next backend sample";case"status_read_failed":return"latest supervisor status read failed; check the backend log"}}function I(e){return`Showing the last sample; refresh failed: ${z(e)}.`}async function we(){try{const e=await y.supervisorStatus();return e.available?{status:"available",data:e.status,staleReason:null}:e.status!==null?{status:"available",data:e.status,staleReason:e.reason}:{status:"unavailable",error:z(e.reason)}}catch(e){return{status:"unavailable",error:E(e,"supervisor status unavailable")}}}async function Ne(){try{return{status:"available",data:await y.localToolVersions()}}catch{return{status:"unavailable",error:"local tool versions unavailable"}}}async function Se(){try{return await y.doltTrend()}catch{return{available:!1,reason:"sample_failed",samples:[]}}}async function ke(){try{return await y.rigStoreHealth()}catch{return{available:!1,reason:"fetch_failed",rigs:[]}}}function $e(e,a){const s=[];if(a===null)s.push("Supervisor state still loading.");else if(a.status==="available"){const r=a.data,c=r.status==="ok"?"healthy":r.status;r.city!==void 0?s.push(`Supervisor ${c} on ${r.city}, uptime ${j(r.uptime_sec)}.`):s.push(`Supervisor ${c}, uptime ${j(r.uptime_sec)}.`)}else s.push("Supervisor unreachable.");if(e===null)return s.push("Host health unavailable."),s.join(" ");const o=Math.round(100*(1-e.host.free_mem_bytes/e.host.total_mem_bytes));return s.push(`Memory at ${o}%; ${e.host.cpu_count} CPUs averaging ${e.host.load_avg_1.toFixed(2)} load.`),s.join(" ")}function Re(e){return e.status==="unavailable"?{tone:"stuck",label:"offline"}:e.data.status==="ok"?{tone:"ok",label:"healthy"}:{tone:"warn",label:e.data.status}}function He(e){const a=e.host.free_mem_bytes/e.host.total_mem_bytes;if(a<.05)return{tone:"stuck",label:"memory critical"};if(a<.1)return{tone:"warn",label:"memory low"};if(e.host.load_avg_1>e.host.cpu_count*1.5)return{tone:"warn",label:"load high"}}function K(e){if(e===null)return{status:"unavailable",reason:"supervisor status still loading"};if(e.status==="unavailable")return{status:"unavailable",reason:e.error};const a=e.data.store_health;return a===void 0?{status:"unavailable",reason:"supervisor did not report store_health"}:{status:"available",value:a,source:"supervisor status.store_health",...e.staleReason!==null?{stale:I(e.staleReason)}:{}}}function Ce(e){return e===null?{status:"unavailable",reason:"supervisor status still loading"}:e.status==="unavailable"?{status:"unavailable",reason:e.error}:{status:"available",value:e.data.work,source:"supervisor status.work",...e.staleReason!==null?{stale:I(e.staleReason)}:{}}}function Le(e){const a=K(e);if(a.status==="unavailable")return{status:"unavailable",reason:a.reason};const s=a.value;return{status:"available",source:"supervisor status.store_health (threshold vs actual)",...a.stale!==void 0?{stale:a.stale}:{},value:[{label:"Dolt MB-per-row ratio",recommended:`<= ${s.threshold_mb_per_row}`,loaded:String(s.ratio_mb_per_row),withinRecommendation:!s.warning}]}}function Te(e){return typeof e=="bigint"?Number(e):e}function j(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.round(e/60)}m`;if(e<86400)return`${Math.round(e/3600)}h`;const a=Math.floor(e/86400),s=Math.round(e%86400/3600);return s>0?`${a}d ${s}h`:`${a}d`}export{Ee as HealthPage}; diff --git a/internal/api/dashboardspa/dist/assets/Health-RykINz8c.js b/internal/api/dashboardspa/dist/assets/Health-RykINz8c.js new file mode 100644 index 0000000000..8ff5c56741 --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/Health-RykINz8c.js @@ -0,0 +1 @@ +import{a as Y,b as v,r as Z,j as t,B as ee,X as y,z as E,S as V,H as F,a9 as te}from"./index-QWRimsO3.js";import{p as $,d as ae}from"./routeHighlight-B30gQO2o.js";import{P as se}from"./PageHeader-CxbYmkHZ.js";import{u as le}from"./useVisibleRefresh-Bm_cCiAg.js";import{a as x}from"./format-fte2CeYD.js";import{a as ne}from"./time-D9v0saHV.js";const re=2500;function Ee(){const e=Y(),a=F(),s=v("health:system",ye),o=v(`health:supervisor:${a??"no-city"}`,_e),r=v(`health:status:${a??"no-city"}`,we),c=v("health:local-tools",Ne),b=v(`health:dolt-noms-trend:${a??"no-city"}`,Se),p=v(`health:rig-store:${a??"no-city"}`,ke),d=s.refresh,_=o.refresh,w=r.refresh,N=c.refresh,C=b.refresh,L=p.refresh,W=s.loading||o.loading||r.loading||c.loading||b.loading||p.loading,T=[s.error,o.error,r.error,c.error,b.error,p.error].filter(J=>J!==null).join("; ")||null,D=Z.useCallback(async()=>{await Promise.all([d(),_(),w(),N(),C(),L()])},[C,N,L,_,w,d]),m=s.data??null,n=m?.status==="available"?m.data:null,S=m?.status==="unavailable"?m.error:null,i=o.data??null,k=r.data??null,U=c.data??null,u=b.data??null,f=p.data??null,B=f?pe(f):void 0,R=m!==null||i!==null||k!==null||U!==null||u!==null||f!==null,M=n?He(n):void 0,X=$(e,"health",["health:supervisor-"]),q=$(e,"health",["health:load-","health:memory-"]),Q=$(e,"health",["health:dashboard-"]),G=$(e,"health",["health:dolt-noms-"]);return le(D,3e4),t.jsxs("section",{children:[t.jsx(se,{title:"Health",synopsis:R?$e(n,i):"Reading state from the supervisor.",meta:t.jsxs(t.Fragment,{children:[T&&t.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:T}),t.jsx(ee,{size:"sm",onClick:()=>{D()},children:W&&!R?"Loading":"Refresh"})]})}),R?t.jsxs("div",{className:"space-y-12",children:[t.jsx(h,{title:"Supervisor",attention:X,...i?{status:Re(i)}:{},children:i===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading supervisor state."}):i.status==="available"?t.jsxs(g,{children:[i.data.city!==void 0?t.jsx(l,{label:"City",value:i.data.city}):t.jsx(l,{label:"City",value:"not reported by supervisor",tone:"warn"}),i.data.version!==void 0?t.jsx(l,{label:"Version",value:i.data.version}):t.jsx(l,{label:"Version",value:"not reported by supervisor",tone:"warn"}),t.jsx(l,{label:"Uptime",value:j(i.data.uptime_sec)}),t.jsx(l,{label:"Status",value:i.data.status})]}):t.jsx("p",{className:"text-body text-accent",children:"Supervisor not reachable. The dashboard shell stays up; live data is stale."})}),t.jsx(h,{title:"Host",attention:q,...M?{status:M}:{},children:m===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading dashboard host health."}):n===null?t.jsxs("p",{className:"text-body text-accent",children:["Dashboard host health unavailable",S?`: ${S}`:"","."]}):t.jsxs(g,{children:[t.jsx(l,{label:"CPUs",value:n.host.cpu_count.toString()}),t.jsx(l,{label:"Load (1m, 5m, 15m)",value:`${n.host.load_avg_1.toFixed(2)}, ${n.host.load_avg_5.toFixed(2)}, ${n.host.load_avg_15.toFixed(2)}`,...n.host.load_avg_1>n.host.cpu_count?{tone:"warn"}:{}}),t.jsx(l,{label:"Memory free",value:`${x(n.host.free_mem_bytes)} of ${x(n.host.total_mem_bytes)}`,...n.host.free_mem_bytes/n.host.total_mem_bytes<.1?{tone:"warn"}:{}}),t.jsx(l,{label:"Host uptime",value:j(n.host.uptime_sec)})]})}),t.jsx(h,{title:"Admin process",attention:Q,children:m===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading dashboard process health."}):n===null?t.jsxs("p",{className:"text-body text-accent",children:["Dashboard process health unavailable",S?`: ${S}`:"","."]}):t.jsxs(g,{children:[t.jsx(l,{label:"PID",value:n.admin.pid.toString()}),t.jsx(l,{label:"Uptime",value:j(n.admin.uptime_sec)}),t.jsx(l,{label:"RSS",value:x(n.admin.rss_bytes)}),t.jsx(l,{label:"Heap used",value:x(n.admin.heap_used_bytes)}),t.jsx(l,{label:"Node",value:n.admin.node_version})]})}),t.jsx(h,{title:"Tool versions",children:t.jsx(oe,{state:U})}),t.jsx(h,{title:"Diagnostics",children:t.jsxs("div",{className:"space-y-8",children:[t.jsx(ce,{usage:K(k)}),t.jsx(ue,{usage:Ce(k)})]})}),t.jsx(h,{title:"Bead stores · per rig",meta:be(f),...B?{status:B}:{},children:t.jsx(de,{report:f})}),t.jsx(h,{title:"Store thresholds",children:t.jsx(ve,{comparison:Le(k)})}),t.jsx(h,{title:"Dolt-noms · 24 h",attention:G,meta:u&&u.samples.length>0?`${u.samples.length} samples`:void 0,children:u===null?t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading."}):u.available?u.samples.length===0?t.jsx("p",{className:"text-body text-fg-muted italic",children:"No samples yet. Backend just started; next sample in ten minutes or less."}):t.jsx(ge,{samples:u.samples}):t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Dolt-noms metric unavailable: ",je(u.reason),"."]})})]}):t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading."})]})}function h({title:e,status:a,meta:s,attention:o,children:r}){return t.jsxs("section",{...ae(o??null),children:[t.jsxs("header",{className:"flex items-baseline justify-between gap-4 mb-4 pb-2 border-b border-rule",children:[t.jsx("h2",{className:"text-headline font-semibold text-fg",children:e}),t.jsxs("div",{className:"flex items-baseline gap-4",children:[s&&t.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:s}),a&&t.jsx(V,{tone:a.tone,label:a.label})]})]}),r]})}function g({children:e}){return t.jsx("dl",{className:"grid grid-cols-[max-content_1fr] gap-x-8 gap-y-3 max-w-prose",children:e})}function l({label:e,value:a,tone:s}){const o=s==="warn"?"text-warn":s==="stuck"?"text-accent":"text-fg";return t.jsxs(t.Fragment,{children:[t.jsx("dt",{className:"text-body text-fg-muted",children:e}),t.jsx("dd",{className:`text-body tnum font-medium ${o}`,children:a})]})}function oe({state:e}){if(e===null)return t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading tool versions."});if(e.status==="unavailable")return t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Tool versions unavailable: ",e.error,"."]});const a=[{label:"gc",tool:e.data.gc},{label:"bd",tool:e.data.beads},{label:"dolt",tool:e.data.dolt}];return t.jsxs("div",{className:"grid grid-cols-[1fr_max-content] gap-x-8 gap-y-3 max-w-prose",children:[t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Tool"}),t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted text-right",children:"Installed"}),a.map(s=>t.jsx(ie,{label:s.label,tool:s.tool},s.label))]})}function ie({label:e,tool:a}){return t.jsxs("div",{className:"contents","data-tool-version-row":e,children:[t.jsx("div",{className:"text-body text-fg",children:e}),t.jsx("div",{className:"text-right",children:a.status==="available"?t.jsx("span",{className:"text-body tnum font-medium text-fg",children:a.version}):t.jsxs("div",{className:"space-y-1",children:[t.jsx("div",{className:"text-body tnum font-medium text-warn",children:"unavailable"}),t.jsx("div",{className:"text-label text-fg-muted normal-case",children:a.reason})]})})]})}function ce({usage:e}){if(e.status==="unavailable")return t.jsx(O,{heading:"Dolt usage",reason:e.reason});const a=e.value;return t.jsxs("div",{className:"space-y-2",children:[t.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Dolt usage"}),e.stale!==void 0&&t.jsx(H,{message:e.stale}),t.jsxs(g,{children:[t.jsx(l,{label:"On-disk size",value:x(Te(a.size_bytes))}),t.jsx(l,{label:"Live rows",value:a.live_rows.toLocaleString()}),t.jsx(l,{label:"MB per row",value:a.ratio_mb_per_row.toString()}),t.jsx(l,{label:"Last maintenance",value:a.last_gc_status??"not reported",...a.last_gc_status!==void 0&&a.last_gc_status!=="success"?{tone:"warn"}:{}}),a.last_gc_at!==void 0&&t.jsx(l,{label:"Last maintenance at",value:ne(a.last_gc_at)}),t.jsx(l,{label:"Store path",value:a.path})]})]})}function ue({usage:e}){if(e.status==="unavailable")return t.jsx(O,{heading:"Beads usage",reason:e.reason});const a=e.value;return t.jsxs("div",{className:"space-y-2",children:[t.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Beads usage"}),e.stale!==void 0&&t.jsx(H,{message:e.stale}),t.jsxs(g,{children:[t.jsx(l,{label:"Open",value:a.open.toString()}),t.jsx(l,{label:"Ready",value:a.ready.toString()}),t.jsx(l,{label:"In progress",value:a.in_progress.toString()})]})]})}function de({report:e}){if(e===null)return t.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading per-rig store health."});if(!e.available&&e.rigs.length===0)return t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Per-rig store health unavailable: ",P(e.reason),"."]});const a=[...e.rigs].sort((s,o)=>A(o.rollup)-A(s.rollup));return t.jsxs("div",{className:"space-y-6 max-w-prose",children:[!e.available&&t.jsxs("p",{className:"text-body text-warn italic",children:["Showing the last sample; refresh failed: ",P(e.reason),"."]}),a.map(s=>t.jsx(he,{rig:s},s.rig))]})}function he({rig:e}){const a=xe(e);return t.jsxs("div",{className:"space-y-2 border-b border-rule pb-4 last:border-b-0",children:[t.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[t.jsx("span",{className:"text-body font-medium text-fg",children:e.rig}),t.jsx(V,{tone:a.tone,label:a.label})]}),t.jsxs("dl",{className:"grid grid-cols-[max-content_1fr] gap-x-6 gap-y-1",children:[t.jsx(l,{label:"Dolt server",value:me(e),...e.doltConnected===!1?{tone:"stuck"}:{}}),e.issueCount!==null&&t.jsx(l,{label:"Live issues",value:e.issueCount.toLocaleString()})]}),e.problems.length>0&&t.jsx("ul",{className:"space-y-1",children:e.problems.map(s=>t.jsxs("li",{className:`text-label ${s.status==="error"?"text-accent":"text-warn"}`,children:[s.name,": ",s.message]},`${s.category}/${s.name}`))}),e.note!==void 0&&t.jsx("p",{className:"text-label text-fg-muted italic",children:e.note})]})}function me(e){const a=e.doltEndpoint??"no endpoint reported";return e.doltConnected===!0?`up · ${a}`:e.doltConnected===!1?`DOWN · ${a}`:`unknown · ${a}`}function xe(e){switch(e.rollup){case"ok":return{tone:"ok",label:"healthy"};case"warn":return{tone:"warn",label:"warnings"};case"down":return e.reachable?e.doltConnected===!1?{tone:"stuck",label:"dolt down"}:{tone:"stuck",label:"errors"}:{tone:"stuck",label:"unreachable"}}}function A(e){return e==="down"?2:e==="warn"?1:0}function be(e){if(e===null||e.rigs.length===0)return;const a={ok:0,warn:0,down:0};for(const s of e.rigs)a[s.rollup]+=1;return`${a.ok} ok · ${a.warn} warn · ${a.down} down`}function pe(e){if(e.rigs.some(a=>a.rollup==="down"))return{tone:"stuck",label:"attention"};if(e.rigs.some(a=>a.rollup==="warn"))return{tone:"warn",label:"warnings"};if(e.rigs.length>0)return{tone:"ok",label:"healthy"}}function P(e){switch(e){case"not_sampled_yet":return"backend just started; first sample is in flight";case"rig_list_failed":return"the supervisor rig list could not be read";case"fetch_failed":return"the dashboard backend could not be reached"}}function ve({comparison:e}){return e.status==="unavailable"?t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Comparison unavailable: ",e.reason,"."]}):t.jsxs("div",{className:"space-y-2",children:[e.stale!==void 0&&t.jsx(H,{message:e.stale}),t.jsxs("div",{className:"grid grid-cols-[1fr_max-content_max-content] gap-x-8 gap-y-3 max-w-prose",children:[t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Setting"}),t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted text-right",children:"Recommended"}),t.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted text-right",children:"Loaded"}),e.value.map(a=>t.jsx(fe,{row:a},a.label))]})]})}function fe({row:e}){const a=e.withinRecommendation?"text-fg":"text-warn";return t.jsxs("div",{className:`contents ${a}`,"data-comparison-row":e.label,children:[t.jsxs("div",{className:`text-body ${a}`,children:[e.label,!e.withinRecommendation&&t.jsx("span",{className:"text-label uppercase tracking-wider text-warn",children:" · over"})]}),t.jsx("div",{className:"text-body tnum text-fg-muted text-right",children:e.recommended}),t.jsx("div",{className:`text-body tnum font-medium text-right ${a}`,children:e.loaded})]})}function O({heading:e,reason:a}){return t.jsxs("div",{className:"space-y-2",children:[t.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-muted",children:e}),t.jsxs("p",{className:"text-body text-fg-muted italic",children:["Unavailable: ",a,"."]})]})}function H({message:e}){return t.jsx("p",{className:"text-body text-warn italic",children:e})}function ge({samples:e}){if(e.length===0)return null;const a=Math.max(...e.map(d=>d.bytes)),s=Math.min(...e.map(d=>d.bytes)),o=a-s||1,r=600,c=60,b=e.length>1?r/(e.length-1):r,p=e.map((d,_)=>{const w=_*b,N=c-(d.bytes-s)/o*c;return`${w.toFixed(1)},${N.toFixed(1)}`}).join(" ");return t.jsxs("div",{className:"space-y-3 max-w-prose",children:[t.jsx("svg",{viewBox:`0 0 ${r} ${c}`,preserveAspectRatio:"none",className:"w-full h-16","aria-label":"24 hour dolt-noms size trend",children:t.jsx("polyline",{fill:"none",stroke:"currentColor",strokeWidth:"1",className:"text-accent",points:p})}),t.jsxs("div",{className:"flex items-baseline justify-between text-label uppercase tracking-wider text-fg-muted tnum",children:[t.jsxs("span",{children:["min ",x(s)]}),t.jsxs("span",{children:["max ",x(a)]})]})]})}function je(e){switch(e){case"store_health_absent":return"supervisor is not reporting store_health; samples resume when it recovers";case"sample_failed":return"latest supervisor status read failed; check the backend log"}}async function ye(){try{return{status:"available",data:await y.systemHealth()}}catch(e){return{status:"unavailable",error:E(e,"dashboard host health unavailable")}}}async function _e(){const e=F();if(e===null)throw new Error("Health page loaded before an active city was resolved");try{return{status:"available",data:await te(re).cityHealth(e)}}catch{return{status:"unavailable",error:"supervisor health unavailable"}}}function z(e){switch(e){case"not_sampled_yet":return"supervisor status sample is warming up; data appears after the next backend sample";case"status_read_failed":return"latest supervisor status read failed; check the backend log"}}function I(e){return`Showing the last sample; refresh failed: ${z(e)}.`}async function we(){try{const e=await y.supervisorStatus();return e.available?{status:"available",data:e.status,staleReason:null}:e.status!==null?{status:"available",data:e.status,staleReason:e.reason}:{status:"unavailable",error:z(e.reason)}}catch(e){return{status:"unavailable",error:E(e,"supervisor status unavailable")}}}async function Ne(){try{return{status:"available",data:await y.localToolVersions()}}catch{return{status:"unavailable",error:"local tool versions unavailable"}}}async function Se(){try{return await y.doltTrend()}catch{return{available:!1,reason:"sample_failed",samples:[]}}}async function ke(){try{return await y.rigStoreHealth()}catch{return{available:!1,reason:"fetch_failed",rigs:[]}}}function $e(e,a){const s=[];if(a===null)s.push("Supervisor state still loading.");else if(a.status==="available"){const r=a.data,c=r.status==="ok"?"healthy":r.status;r.city!==void 0?s.push(`Supervisor ${c} on ${r.city}, uptime ${j(r.uptime_sec)}.`):s.push(`Supervisor ${c}, uptime ${j(r.uptime_sec)}.`)}else s.push("Supervisor unreachable.");if(e===null)return s.push("Host health unavailable."),s.join(" ");const o=Math.round(100*(1-e.host.free_mem_bytes/e.host.total_mem_bytes));return s.push(`Memory at ${o}%; ${e.host.cpu_count} CPUs averaging ${e.host.load_avg_1.toFixed(2)} load.`),s.join(" ")}function Re(e){return e.status==="unavailable"?{tone:"stuck",label:"offline"}:e.data.status==="ok"?{tone:"ok",label:"healthy"}:{tone:"warn",label:e.data.status}}function He(e){const a=e.host.free_mem_bytes/e.host.total_mem_bytes;if(a<.05)return{tone:"stuck",label:"memory critical"};if(a<.1)return{tone:"warn",label:"memory low"};if(e.host.load_avg_1>e.host.cpu_count*1.5)return{tone:"warn",label:"load high"}}function K(e){if(e===null)return{status:"unavailable",reason:"supervisor status still loading"};if(e.status==="unavailable")return{status:"unavailable",reason:e.error};const a=e.data.store_health;return a===void 0?{status:"unavailable",reason:"supervisor did not report store_health"}:{status:"available",value:a,source:"supervisor status.store_health",...e.staleReason!==null?{stale:I(e.staleReason)}:{}}}function Ce(e){return e===null?{status:"unavailable",reason:"supervisor status still loading"}:e.status==="unavailable"?{status:"unavailable",reason:e.error}:{status:"available",value:e.data.work,source:"supervisor status.work",...e.staleReason!==null?{stale:I(e.staleReason)}:{}}}function Le(e){const a=K(e);if(a.status==="unavailable")return{status:"unavailable",reason:a.reason};const s=a.value;return{status:"available",source:"supervisor status.store_health (threshold vs actual)",...a.stale!==void 0?{stale:a.stale}:{},value:[{label:"Dolt MB-per-row ratio",recommended:`<= ${s.threshold_mb_per_row}`,loaded:String(s.ratio_mb_per_row),withinRecommendation:!s.warning}]}}function Te(e){return typeof e=="bigint"?Number(e):e}function j(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.round(e/60)}m`;if(e<86400)return`${Math.round(e/3600)}h`;const a=Math.floor(e/86400),s=Math.round(e%86400/3600);return s>0?`${a}d ${s}h`:`${a}d`}export{Ee as HealthPage}; diff --git a/internal/api/dashboardspa/dist/assets/LiveSessionPeek-BpsrYunI.js b/internal/api/dashboardspa/dist/assets/LiveSessionPeek-m6YywWBh.js similarity index 72% rename from internal/api/dashboardspa/dist/assets/LiveSessionPeek-BpsrYunI.js rename to internal/api/dashboardspa/dist/assets/LiveSessionPeek-m6YywWBh.js index 38949d8522..b0becd9209 100644 --- a/internal/api/dashboardspa/dist/assets/LiveSessionPeek-BpsrYunI.js +++ b/internal/api/dashboardspa/dist/assets/LiveSessionPeek-m6YywWBh.js @@ -1,4 +1,4 @@ -import{r as d,ai as O,J as I,p as C,x as L,aj as A,I as $,j as l,S as B}from"./index-zPatq59W.js";import{a as M,b as U,f as v}from"./time-D9v0saHV.js";import{a as D}from"./format-fte2CeYD.js";import{P as F}from"./constants-vAmcTKRZ.js";function q(t,e){const[s,r]=d.useState({status:"idle",stream:{status:"idle"}}),n=d.useRef(!1);return d.useEffect(()=>{if(n.current=!1,!t){r({status:"idle",stream:{status:"idle"}});return}let i=!1,a=null;const u=e&&typeof EventSource<"u";return r({status:"loading",stream:{status:u?"connecting":"idle"}}),O(t).then(c=>{if(!i&&(r({status:"ready",result:c,stream:{status:u?"connecting":"idle"}}),u)){a=new EventSource(I().sessionStreamUrl(G("open supervisor session stream"),t),{withCredentials:!0}),a.onopen=()=>{i||r(p=>p.status==="ready"?{...p,stream:{status:"open"}}:p)};const f=p=>{if(i)return;const h=z(p.data);h.kind==="invalid"&&H(t,n),r(_=>{const m=_.status==="ready"?_.result:c;return h.kind==="invalid"?{status:"ready",result:m,stream:{status:"degraded",error:h.error}}:h.kind==="snapshot"?{status:"ready",result:h.result,stream:{status:"open"}}:{status:"ready",result:{...m,turns:[...m.turns,h.turn],total_chars:m.total_chars+h.turn.text.length,captured_at:new Date().toISOString()},stream:{status:"open"}}})};a.onmessage=f,a.addEventListener("turn",f),a.onerror=()=>{if(i)return;const p=a?.readyState===EventSource.CLOSED?"closed":"connecting";r(h=>h.status==="ready"?{...h,stream:{status:p}}:h)}}},c=>{i||(N("load transcript",t,c),r({status:"failed",error:C(c)||"Failed to load session.",stream:{status:"idle"}}))}),()=>{i=!0,a?.close()}},[t,e]),s}function H(t,e){e.current||(e.current=!0,N("parse stream event",t,b))}function N(t,e,s){L({component:"session-stream",operation:t,message:`${e}: ${C(s)}`})}function G(t){const e=$();if(e===null)throw new Error(`${t} called before an active city was resolved`);return e}const b="Malformed session stream event.";function z(t){let e;try{e=JSON.parse(t)}catch{return{kind:"invalid",error:b}}if(!T(e))return{kind:"invalid",error:b};const s=V(e);return s?{kind:"snapshot",result:s}:typeof e.text!="string"?{kind:"invalid",error:b}:{kind:"turn",turn:{role:typeof e.role=="string"?e.role:"assistant",text:e.text}}}function V(t){if(!Array.isArray(t.turns))return null;const e=t.turns.flatMap(i=>!T(i)||typeof i.text!="string"?[]:[{role:typeof i.role=="string"?i.role:"assistant",text:i.text}]);if(e.length!==t.turns.length)return null;const s=typeof t.session_id=="string"?t.session_id:typeof t.id=="string"?t.id:"";if(!s)return null;const r=typeof t.total_chars=="number"?t.total_chars:e.reduce((i,a)=>i+a.text.length,0);return{...A({id:s,template:typeof t.template=="string"?t.template:"",provider:typeof t.provider=="string"?t.provider:"",format:typeof t.format=="string"?t.format:"conversation",turns:e},typeof t.captured_at=="string"?t.captured_at:new Date().toISOString()),total_chars:r,truncated:t.truncated===!0}}function T(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}var S=function(t,e){return Object.defineProperty?Object.defineProperty(t,"raw",{value:e}):t.raw=e,t},o;(function(t){t[t.EOS=0]="EOS",t[t.Text=1]="Text",t[t.Incomplete=2]="Incomplete",t[t.ESC=3]="ESC",t[t.Unknown=4]="Unknown",t[t.SGR=5]="SGR",t[t.OSCURL=6]="OSCURL"})(o||(o={}));class J{constructor(){this.VERSION="6.0.6",this.setup_palettes(),this._use_classes=!1,this.bold=!1,this.faint=!1,this.italic=!1,this.underline=!1,this.fg=this.bg=null,this._buffer="",this._url_allowlist={http:1,https:1},this._escape_html=!0,this.boldStyle="font-weight:bold",this.faintStyle="opacity:0.7",this.italicStyle="font-style:italic",this.underlineStyle="text-decoration:underline"}set use_classes(e){this._use_classes=e}get use_classes(){return this._use_classes}set url_allowlist(e){this._url_allowlist=e}get url_allowlist(){return this._url_allowlist}set escape_html(e){this._escape_html=e}get escape_html(){return this._escape_html}set boldStyle(e){this._boldStyle=e}get boldStyle(){return this._boldStyle}set faintStyle(e){this._faintStyle=e}get faintStyle(){return this._faintStyle}set italicStyle(e){this._italicStyle=e}get italicStyle(){return this._italicStyle}set underlineStyle(e){this._underlineStyle=e}get underlineStyle(){return this._underlineStyle}setup_palettes(){this.ansi_colors=[[{rgb:[0,0,0],class_name:"ansi-black"},{rgb:[187,0,0],class_name:"ansi-red"},{rgb:[0,187,0],class_name:"ansi-green"},{rgb:[187,187,0],class_name:"ansi-yellow"},{rgb:[0,0,187],class_name:"ansi-blue"},{rgb:[187,0,187],class_name:"ansi-magenta"},{rgb:[0,187,187],class_name:"ansi-cyan"},{rgb:[255,255,255],class_name:"ansi-white"}],[{rgb:[85,85,85],class_name:"ansi-bright-black"},{rgb:[255,85,85],class_name:"ansi-bright-red"},{rgb:[0,255,0],class_name:"ansi-bright-green"},{rgb:[255,255,85],class_name:"ansi-bright-yellow"},{rgb:[85,85,255],class_name:"ansi-bright-blue"},{rgb:[255,85,255],class_name:"ansi-bright-magenta"},{rgb:[85,255,255],class_name:"ansi-bright-cyan"},{rgb:[255,255,255],class_name:"ansi-bright-white"}]],this.palette_256=[],this.ansi_colors.forEach(r=>{r.forEach(n=>{this.palette_256.push(n)})});let e=[0,95,135,175,215,255];for(let r=0;r<6;++r)for(let n=0;n<6;++n)for(let i=0;i<6;++i){let a={rgb:[e[r],e[n],e[i]],class_name:"truecolor"};this.palette_256.push(a)}let s=8;for(let r=0;r<24;++r,s+=10){let n={rgb:[s,s,s],class_name:"truecolor"};this.palette_256.push(n)}}escape_txt_for_html(e){return this._escape_html?e.replace(/[&<>"']/gm,s=>{if(s==="&")return"&";if(s==="<")return"<";if(s===">")return">";if(s==='"')return""";if(s==="'")return"'"}):e}append_buffer(e){var s=this._buffer+e;this._buffer=s}get_next_packet(){var e={kind:o.EOS,text:"",url:""},s=this._buffer.length;if(s==0)return e;var r=this._buffer.indexOf("\x1B");if(r==-1)return e.kind=o.Text,e.text=this._buffer,this._buffer="",e;if(r>0)return e.kind=o.Text,e.text=this._buffer.slice(0,r),this._buffer=this._buffer.slice(r),e;if(r==0){if(s<3)return e.kind=o.Incomplete,e;var n=this._buffer.charAt(1);if(n!="["&&n!="]"&&n!="(")return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;if(n=="["){this._csi_regex||(this._csi_regex=y(w||(w=S([` +import{r as d,a2 as O,I,p as C,x as L,a3 as A,H as $,j as l,S as B}from"./index-QWRimsO3.js";import{a as M,b as U,f as v}from"./time-D9v0saHV.js";import{a as D}from"./format-fte2CeYD.js";import{P as F}from"./constants-CVFL5iaz.js";function q(t,e){const[s,r]=d.useState({status:"idle",stream:{status:"idle"}}),n=d.useRef(!1);return d.useEffect(()=>{if(n.current=!1,!t){r({status:"idle",stream:{status:"idle"}});return}let i=!1,a=null;const u=e&&typeof EventSource<"u";return r({status:"loading",stream:{status:u?"connecting":"idle"}}),O(t).then(c=>{if(!i&&(r({status:"ready",result:c,stream:{status:u?"connecting":"idle"}}),u)){a=new EventSource(I().sessionStreamUrl(G("open supervisor session stream"),t),{withCredentials:!0}),a.onopen=()=>{i||r(p=>p.status==="ready"?{...p,stream:{status:"open"}}:p)};const f=p=>{if(i)return;const h=z(p.data);h.kind==="invalid"&&H(t,n),r(_=>{const m=_.status==="ready"?_.result:c;return h.kind==="invalid"?{status:"ready",result:m,stream:{status:"degraded",error:h.error}}:h.kind==="snapshot"?{status:"ready",result:h.result,stream:{status:"open"}}:{status:"ready",result:{...m,turns:[...m.turns,h.turn],total_chars:m.total_chars+h.turn.text.length,captured_at:new Date().toISOString()},stream:{status:"open"}}})};a.onmessage=f,a.addEventListener("turn",f),a.onerror=()=>{if(i)return;const p=a?.readyState===EventSource.CLOSED?"closed":"connecting";r(h=>h.status==="ready"?{...h,stream:{status:p}}:h)}}},c=>{i||(N("load transcript",t,c),r({status:"failed",error:C(c)||"Failed to load session.",stream:{status:"idle"}}))}),()=>{i=!0,a?.close()}},[t,e]),s}function H(t,e){e.current||(e.current=!0,N("parse stream event",t,b))}function N(t,e,s){L({component:"session-stream",operation:t,message:`${e}: ${C(s)}`})}function G(t){const e=$();if(e===null)throw new Error(`${t} called before an active city was resolved`);return e}const b="Malformed session stream event.";function z(t){let e;try{e=JSON.parse(t)}catch{return{kind:"invalid",error:b}}if(!T(e))return{kind:"invalid",error:b};const s=V(e);return s?{kind:"snapshot",result:s}:typeof e.text!="string"?{kind:"invalid",error:b}:{kind:"turn",turn:{role:typeof e.role=="string"?e.role:"assistant",text:e.text}}}function V(t){if(!Array.isArray(t.turns))return null;const e=t.turns.flatMap(i=>!T(i)||typeof i.text!="string"?[]:[{role:typeof i.role=="string"?i.role:"assistant",text:i.text}]);if(e.length!==t.turns.length)return null;const s=typeof t.session_id=="string"?t.session_id:typeof t.id=="string"?t.id:"";if(!s)return null;const r=typeof t.total_chars=="number"?t.total_chars:e.reduce((i,a)=>i+a.text.length,0);return{...A({id:s,template:typeof t.template=="string"?t.template:"",provider:typeof t.provider=="string"?t.provider:"",format:typeof t.format=="string"?t.format:"conversation",turns:e},typeof t.captured_at=="string"?t.captured_at:new Date().toISOString()),total_chars:r,truncated:t.truncated===!0}}function T(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}var S=function(t,e){return Object.defineProperty?Object.defineProperty(t,"raw",{value:e}):t.raw=e,t},o;(function(t){t[t.EOS=0]="EOS",t[t.Text=1]="Text",t[t.Incomplete=2]="Incomplete",t[t.ESC=3]="ESC",t[t.Unknown=4]="Unknown",t[t.SGR=5]="SGR",t[t.OSCURL=6]="OSCURL"})(o||(o={}));class P{constructor(){this.VERSION="6.0.6",this.setup_palettes(),this._use_classes=!1,this.bold=!1,this.faint=!1,this.italic=!1,this.underline=!1,this.fg=this.bg=null,this._buffer="",this._url_allowlist={http:1,https:1},this._escape_html=!0,this.boldStyle="font-weight:bold",this.faintStyle="opacity:0.7",this.italicStyle="font-style:italic",this.underlineStyle="text-decoration:underline"}set use_classes(e){this._use_classes=e}get use_classes(){return this._use_classes}set url_allowlist(e){this._url_allowlist=e}get url_allowlist(){return this._url_allowlist}set escape_html(e){this._escape_html=e}get escape_html(){return this._escape_html}set boldStyle(e){this._boldStyle=e}get boldStyle(){return this._boldStyle}set faintStyle(e){this._faintStyle=e}get faintStyle(){return this._faintStyle}set italicStyle(e){this._italicStyle=e}get italicStyle(){return this._italicStyle}set underlineStyle(e){this._underlineStyle=e}get underlineStyle(){return this._underlineStyle}setup_palettes(){this.ansi_colors=[[{rgb:[0,0,0],class_name:"ansi-black"},{rgb:[187,0,0],class_name:"ansi-red"},{rgb:[0,187,0],class_name:"ansi-green"},{rgb:[187,187,0],class_name:"ansi-yellow"},{rgb:[0,0,187],class_name:"ansi-blue"},{rgb:[187,0,187],class_name:"ansi-magenta"},{rgb:[0,187,187],class_name:"ansi-cyan"},{rgb:[255,255,255],class_name:"ansi-white"}],[{rgb:[85,85,85],class_name:"ansi-bright-black"},{rgb:[255,85,85],class_name:"ansi-bright-red"},{rgb:[0,255,0],class_name:"ansi-bright-green"},{rgb:[255,255,85],class_name:"ansi-bright-yellow"},{rgb:[85,85,255],class_name:"ansi-bright-blue"},{rgb:[255,85,255],class_name:"ansi-bright-magenta"},{rgb:[85,255,255],class_name:"ansi-bright-cyan"},{rgb:[255,255,255],class_name:"ansi-bright-white"}]],this.palette_256=[],this.ansi_colors.forEach(r=>{r.forEach(n=>{this.palette_256.push(n)})});let e=[0,95,135,175,215,255];for(let r=0;r<6;++r)for(let n=0;n<6;++n)for(let i=0;i<6;++i){let a={rgb:[e[r],e[n],e[i]],class_name:"truecolor"};this.palette_256.push(a)}let s=8;for(let r=0;r<24;++r,s+=10){let n={rgb:[s,s,s],class_name:"truecolor"};this.palette_256.push(n)}}escape_txt_for_html(e){return this._escape_html?e.replace(/[&<>"']/gm,s=>{if(s==="&")return"&";if(s==="<")return"<";if(s===">")return">";if(s==='"')return""";if(s==="'")return"'"}):e}append_buffer(e){var s=this._buffer+e;this._buffer=s}get_next_packet(){var e={kind:o.EOS,text:"",url:""},s=this._buffer.length;if(s==0)return e;var r=this._buffer.indexOf("\x1B");if(r==-1)return e.kind=o.Text,e.text=this._buffer,this._buffer="",e;if(r>0)return e.kind=o.Text,e.text=this._buffer.slice(0,r),this._buffer=this._buffer.slice(r),e;if(r==0){if(s<3)return e.kind=o.Incomplete,e;var n=this._buffer.charAt(1);if(n!="["&&n!="]"&&n!="(")return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;if(n=="["){this._csi_regex||(this._csi_regex=y(w||(w=S([` ^ # beginning of line # # First attempt @@ -32,7 +32,7 @@ import{r as d,ai as O,J as I,p as C,x as L,aj as A,I as $,j as l,S as B}from"./i [\\x20-\\x7e]* # anything legal ([\\x00-\\x1f:]) # anything illegal ) - `]))));let a=this._buffer.match(this._csi_regex);if(a===null)return e.kind=o.Incomplete,e;if(a[4])return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;a[1]!=""||a[3]!="m"?e.kind=o.Unknown:e.kind=o.SGR,e.text=a[2];var i=a[0].length;return this._buffer=this._buffer.slice(i),e}else if(n=="]"){if(s<4)return e.kind=o.Incomplete,e;if(this._buffer.charAt(2)!="8"||this._buffer.charAt(3)!=";")return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;this._osc_st||(this._osc_st=P(E||(E=S([` + `]))));let a=this._buffer.match(this._csi_regex);if(a===null)return e.kind=o.Incomplete,e;if(a[4])return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;a[1]!=""||a[3]!="m"?e.kind=o.Unknown:e.kind=o.SGR,e.text=a[2];var i=a[0].length;return this._buffer=this._buffer.slice(i),e}else if(n=="]"){if(s<4)return e.kind=o.Incomplete,e;if(this._buffer.charAt(2)!="8"||this._buffer.charAt(3)!=";")return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;this._osc_st||(this._osc_st=Z(E||(E=S([` (?: # legal sequence (\x1B\\) # ESC | # alternate (\x07) # BEL (what xterm did) @@ -95,4 +95,4 @@ import{r as d,ai as O,J as I,p as C,x as L,aj as A,I as $,j as l,S as B}from"./i | # alternate (?:\\x07) # BEL (what xterm did) ) - `]))));let a=this._buffer.match(this._osc_regex);if(a===null)return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;e.kind=o.OSCURL,e.url=a[1],e.text=a[2];var i=a[0].length;return this._buffer=this._buffer.slice(i),e}else if(n=="(")return e.kind=o.Unknown,this._buffer=this._buffer.slice(3),e}}ansi_to_html(e){this.append_buffer(e);for(var s=[];;){var r=this.get_next_packet();if(r.kind==o.EOS||r.kind==o.Incomplete)break;r.kind==o.ESC||r.kind==o.Unknown||(r.kind==o.Text?s.push(this.transform_to_html(this.with_state(r))):r.kind==o.SGR?this.process_ansi(r):r.kind==o.OSCURL&&s.push(this.process_hyperlink(r)))}return s.join("")}with_state(e){return{bold:this.bold,faint:this.faint,italic:this.italic,underline:this.underline,fg:this.fg,bg:this.bg,text:e.text}}process_ansi(e){let s=e.text.split(";");for(;s.length>0;){let r=s.shift(),n=parseInt(r,10);if(isNaN(n)||n===0)this.fg=null,this.bg=null,this.bold=!1,this.faint=!1,this.italic=!1,this.underline=!1;else if(n===1)this.bold=!0;else if(n===2)this.faint=!0;else if(n===3)this.italic=!0;else if(n===4)this.underline=!0;else if(n===21)this.bold=!1;else if(n===22)this.faint=!1,this.bold=!1;else if(n===23)this.italic=!1;else if(n===24)this.underline=!1;else if(n===39)this.fg=null;else if(n===49)this.bg=null;else if(n>=30&&n<38)this.fg=this.ansi_colors[0][n-30];else if(n>=40&&n<48)this.bg=this.ansi_colors[0][n-40];else if(n>=90&&n<98)this.fg=this.ansi_colors[1][n-90];else if(n>=100&&n<108)this.bg=this.ansi_colors[1][n-100];else if((n===38||n===48)&&s.length>0){let i=n===38,a=s.shift();if(a==="5"&&s.length>0){let u=parseInt(s.shift(),10);u>=0&&u<=255&&(i?this.fg=this.palette_256[u]:this.bg=this.palette_256[u])}if(a==="2"&&s.length>2){let u=parseInt(s.shift(),10),c=parseInt(s.shift(),10),f=parseInt(s.shift(),10);if(u>=0&&u<=255&&c>=0&&c<=255&&f>=0&&f<=255){let p={rgb:[u,c,f],class_name:"truecolor"};i?this.fg=p:this.bg=p}}}}}transform_to_html(e){let s=e.text;if(s.length===0||(s=this.escape_txt_for_html(s),!e.bold&&!e.italic&&!e.faint&&!e.underline&&e.fg===null&&e.bg===null))return s;let r=[],n=[],i=e.fg,a=e.bg;e.bold&&r.push(this._boldStyle),e.faint&&r.push(this._faintStyle),e.italic&&r.push(this._italicStyle),e.underline&&r.push(this._underlineStyle),this._use_classes?(i&&(i.class_name!=="truecolor"?n.push(`${i.class_name}-fg`):r.push(`color:rgb(${i.rgb.join(",")})`)),a&&(a.class_name!=="truecolor"?n.push(`${a.class_name}-bg`):r.push(`background-color:rgb(${a.rgb.join(",")})`))):(i&&r.push(`color:rgb(${i.rgb.join(",")})`),a&&r.push(`background-color:rgb(${a.rgb})`));let u="",c="";return n.length&&(u=` class="${n.join(" ")}"`),r.length&&(c=` style="${r.join(";")}"`),`${s}`}process_hyperlink(e){let s=e.url.split(":");return s.length<1||!this._url_allowlist[s[0]]?"":`${this.escape_txt_for_html(e.text)}`}}function y(t,...e){let s=t.raw[0],r=/^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm,n=s.replace(r,"");return new RegExp(n)}function P(t,...e){let s=t.raw[0],r=/^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm,n=s.replace(r,"");return new RegExp(n,"g")}var w,E,j;const Z=/\x1b\][^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)/g,W=/\x1b\[[?0-9;]*[a-ln-zA-Z]/g,X=/\x1b(?!\[[?0-9;]*m)[@-Z\\-_]?/g,Q=/[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f\x7f-\x9f]/g;function Y(t){return t.replace(Z,"").replace(W,"").replace(X,"").replace(Q,"")}function K({children:t,caption:e}){const[s,r]=d.useState(!1),n=d.useRef(null),i=d.useRef(null),a=d.useRef(!0),[u,c]=d.useState(!1),f=d.useId(),p=s?i:n;d.useEffect(()=>{const g=p.current;!g||!a.current||(g.scrollTop=g.scrollHeight)});const h=d.useCallback(g=>{const x=g.currentTarget;a.current=x.scrollHeight-x.scrollTop-x.clientHeight<8},[]);d.useEffect(()=>{if(!s){c(!1);return}if(window.matchMedia("(prefers-reduced-motion: reduce)").matches){c(!0);return}const g=requestAnimationFrame(()=>c(!0));return()=>cancelAnimationFrame(g)},[s]),d.useEffect(()=>{if(!s)return;const g=x=>{x.key==="Escape"&&(x.stopPropagation(),r(!1),a.current=!0)};return document.addEventListener("keydown",g,!0),()=>document.removeEventListener("keydown",g,!0)},[s]);const _=()=>{a.current=!0,r(!0)},m=()=>{a.current=!0,r(!1)};return s?l.jsxs(l.Fragment,{children:[l.jsx("div",{className:"fixed inset-0 z-[60] bg-fg/30","aria-hidden":"true",onClick:m}),l.jsxs("div",{role:"dialog","aria-modal":"true","aria-labelledby":f,className:["fixed inset-[5%] z-[61] flex flex-col","bg-surface border border-rule rounded-md","transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none",u?"opacity-100 scale-100":"opacity-0 scale-95"].join(" "),onClick:g=>g.stopPropagation(),children:[l.jsxs("div",{className:"px-5 pt-4 pb-3 border-b border-rule shrink-0 space-y-1",children:[l.jsx("h2",{id:f,className:"text-label uppercase tracking-wider text-fg-faint",children:"Chat Transcript"}),l.jsxs("div",{className:"flex items-baseline gap-3",children:[e&&l.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:e}),l.jsx("button",{type:"button",onClick:m,"aria-label":"Collapse transcript",className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out focus-mark rounded-sm px-1 ml-auto",children:"collapse ×"})]})]}),l.jsx("div",{ref:i,onScroll:h,className:"flex-1 overflow-y-auto p-5",children:t})]})]}):l.jsxs("div",{children:[l.jsx("header",{className:"flex items-baseline justify-end mb-4 gap-3",children:l.jsxs("div",{className:"flex items-baseline gap-3 min-w-0",children:[e&&l.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:e}),l.jsx("button",{type:"button",onClick:_,"aria-label":"Expand transcript",className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out focus-mark rounded-sm shrink-0",children:"expand ⤢"})]})}),l.jsx("div",{ref:n,onScroll:h,className:"h-96 overflow-y-auto p-4",children:t})]})}const k=512,ee=/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?(?!\d)/;function te(t){if(t.length===0)return null;const s=(t.length>k?t.slice(0,k):t).match(ee);return s?s[0]:null}function se({loading:t,error:e,result:s,caption:r}){if(t&&s===null)return l.jsx("p",{className:"text-fg-muted italic",children:"Fetching transcript."});if(e)return l.jsx("p",{className:"text-accent",role:"alert",children:e});if(!s)return null;if(s.turns.length===0)return l.jsx("p",{className:"text-fg-muted italic",children:"No turns in this session yet."});const n=Date.now();return l.jsx(K,{...r!==void 0?{caption:r}:{},children:l.jsxs("div",{className:"space-y-6",children:[r===void 0&&l.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint tnum",title:s.captured_at,children:M(s.captured_at)}),l.jsxs("p",{className:"text-label uppercase tracking-wider text-warn",children:["▲ ",F]}),l.jsx("ol",{className:"space-y-5",children:s.turns.map((i,a)=>l.jsx(ne,{turn:i,index:a,now:n},a))}),s.truncated&&l.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:["Some turns truncated at the per-turn or total cap. Run"," ",l.jsx("code",{className:"text-fg-muted",children:"gc session peek"})," in a terminal for the full transcript."]})]})})}function ne({turn:t,index:e,now:s}){const r=d.useMemo(()=>re(t.text),[t.text]),n=d.useMemo(()=>te(t.text),[t.text]);return l.jsxs("li",{children:[l.jsxs("header",{className:"flex items-start justify-between gap-3 pb-2 border-b border-rule mb-2",children:[l.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:["#",(e+1).toString().padStart(2,"0")]}),l.jsxs("div",{className:"flex flex-col items-end leading-tight",title:n??void 0,children:[l.jsx("span",{className:"text-body text-fg tnum",children:U(n)}),l.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:v(n,s)}),l.jsx(ie,{role:t.role})]})]}),l.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed overflow-x-auto text-fg",children:r})]})}function re(t){const e=Y(t),s=new J;s.use_classes=!0;const r=s.ansi_to_html(e);if(typeof DOMParser>"u")return[e];const n=new DOMParser().parseFromString(`${r}`,"text/html");return Array.from(n.body.childNodes).map((i,a)=>R(i,String(a)))}function R(t,e){if(t.nodeType===3)return t.textContent??"";if(t.nodeType!==1)return null;const s=t,r=Array.from(s.childNodes).map((n,i)=>R(n,`${e}-${i}`));return s.tagName.toLowerCase()==="br"?l.jsx("br",{},e):s.tagName.toLowerCase()!=="span"?l.jsx("span",{children:r},e):l.jsx("span",{className:s.getAttribute("class")??void 0,children:r},e)}function ie({role:t}){const e=ae(t);return l.jsx("span",{className:`text-label uppercase tracking-wider font-medium ${e}`,children:t.replace(/_/g," ")})}function ae(t){switch(t){case"assistant":return"text-accent";case"user":return"text-fg";case"system":return"text-warn";case"tool_use":case"tool_result":return"text-fg-muted";default:return"text-fg-faint"}}function de({sessionId:t,stream:e,showBadge:s=!0,showCaption:r=!1}){const n=q(t,e),i=n.status==="ready"?n.result:null,a=n.status==="loading",u=n.status==="failed"?n.error:null,c=le(n.stream),f=[];return r&&i&&(f.push(`${i.turns.length} turn(s)`),f.push(D(i.total_chars,"chars")),f.push(`captured ${v(i.captured_at,Date.now())}`)),l.jsxs("div",{className:"space-y-4",children:[s&&l.jsx("div",{className:"flex justify-end",children:l.jsx(B,{tone:c.tone,label:c.label,title:`Session stream: ${n.stream.status}`,className:"text-label uppercase tracking-wider"})}),l.jsx(se,{loading:a,error:u,result:i,...f.length>0?{caption:f.join(" · ")}:{}})]})}function le(t){switch(typeof t=="string"?t:t.status){case"open":return{tone:"ok",label:"live"};case"connecting":return{tone:"warn",label:"connecting"};case"closed":return{tone:"stuck",label:"offline"};case"degraded":return{tone:"warn",label:"degraded"};case"idle":return{tone:"neutral",label:"snapshot"}}}function he(t){return t===null?!1:t.running===!0||t.state==="active"||t.state==="running"}function pe(t){return t===null||!t.session?!1:t.running===!0||t.state==="active"||t.state==="running"}export{de as L,se as S,he as a,pe as i,q as u}; + `]))));let a=this._buffer.match(this._osc_regex);if(a===null)return e.kind=o.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;e.kind=o.OSCURL,e.url=a[1],e.text=a[2];var i=a[0].length;return this._buffer=this._buffer.slice(i),e}else if(n=="(")return e.kind=o.Unknown,this._buffer=this._buffer.slice(3),e}}ansi_to_html(e){this.append_buffer(e);for(var s=[];;){var r=this.get_next_packet();if(r.kind==o.EOS||r.kind==o.Incomplete)break;r.kind==o.ESC||r.kind==o.Unknown||(r.kind==o.Text?s.push(this.transform_to_html(this.with_state(r))):r.kind==o.SGR?this.process_ansi(r):r.kind==o.OSCURL&&s.push(this.process_hyperlink(r)))}return s.join("")}with_state(e){return{bold:this.bold,faint:this.faint,italic:this.italic,underline:this.underline,fg:this.fg,bg:this.bg,text:e.text}}process_ansi(e){let s=e.text.split(";");for(;s.length>0;){let r=s.shift(),n=parseInt(r,10);if(isNaN(n)||n===0)this.fg=null,this.bg=null,this.bold=!1,this.faint=!1,this.italic=!1,this.underline=!1;else if(n===1)this.bold=!0;else if(n===2)this.faint=!0;else if(n===3)this.italic=!0;else if(n===4)this.underline=!0;else if(n===21)this.bold=!1;else if(n===22)this.faint=!1,this.bold=!1;else if(n===23)this.italic=!1;else if(n===24)this.underline=!1;else if(n===39)this.fg=null;else if(n===49)this.bg=null;else if(n>=30&&n<38)this.fg=this.ansi_colors[0][n-30];else if(n>=40&&n<48)this.bg=this.ansi_colors[0][n-40];else if(n>=90&&n<98)this.fg=this.ansi_colors[1][n-90];else if(n>=100&&n<108)this.bg=this.ansi_colors[1][n-100];else if((n===38||n===48)&&s.length>0){let i=n===38,a=s.shift();if(a==="5"&&s.length>0){let u=parseInt(s.shift(),10);u>=0&&u<=255&&(i?this.fg=this.palette_256[u]:this.bg=this.palette_256[u])}if(a==="2"&&s.length>2){let u=parseInt(s.shift(),10),c=parseInt(s.shift(),10),f=parseInt(s.shift(),10);if(u>=0&&u<=255&&c>=0&&c<=255&&f>=0&&f<=255){let p={rgb:[u,c,f],class_name:"truecolor"};i?this.fg=p:this.bg=p}}}}}transform_to_html(e){let s=e.text;if(s.length===0||(s=this.escape_txt_for_html(s),!e.bold&&!e.italic&&!e.faint&&!e.underline&&e.fg===null&&e.bg===null))return s;let r=[],n=[],i=e.fg,a=e.bg;e.bold&&r.push(this._boldStyle),e.faint&&r.push(this._faintStyle),e.italic&&r.push(this._italicStyle),e.underline&&r.push(this._underlineStyle),this._use_classes?(i&&(i.class_name!=="truecolor"?n.push(`${i.class_name}-fg`):r.push(`color:rgb(${i.rgb.join(",")})`)),a&&(a.class_name!=="truecolor"?n.push(`${a.class_name}-bg`):r.push(`background-color:rgb(${a.rgb.join(",")})`))):(i&&r.push(`color:rgb(${i.rgb.join(",")})`),a&&r.push(`background-color:rgb(${a.rgb})`));let u="",c="";return n.length&&(u=` class="${n.join(" ")}"`),r.length&&(c=` style="${r.join(";")}"`),`${s}`}process_hyperlink(e){let s=e.url.split(":");return s.length<1||!this._url_allowlist[s[0]]?"":`${this.escape_txt_for_html(e.text)}`}}function y(t,...e){let s=t.raw[0],r=/^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm,n=s.replace(r,"");return new RegExp(n)}function Z(t,...e){let s=t.raw[0],r=/^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm,n=s.replace(r,"");return new RegExp(n,"g")}var w,E,j;const J=/\x1b\][^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)/g,W=/\x1b\[[?0-9;]*[a-ln-zA-Z]/g,X=/\x1b(?!\[[?0-9;]*m)[@-Z\\-_]?/g,Q=/[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f\x7f-\x9f]/g;function Y(t){return t.replace(J,"").replace(W,"").replace(X,"").replace(Q,"")}function K({children:t,caption:e}){const[s,r]=d.useState(!1),n=d.useRef(null),i=d.useRef(null),a=d.useRef(!0),[u,c]=d.useState(!1),f=d.useId(),p=s?i:n;d.useEffect(()=>{const g=p.current;!g||!a.current||(g.scrollTop=g.scrollHeight)});const h=d.useCallback(g=>{const x=g.currentTarget;a.current=x.scrollHeight-x.scrollTop-x.clientHeight<8},[]);d.useEffect(()=>{if(!s){c(!1);return}if(window.matchMedia("(prefers-reduced-motion: reduce)").matches){c(!0);return}const g=requestAnimationFrame(()=>c(!0));return()=>cancelAnimationFrame(g)},[s]),d.useEffect(()=>{if(!s)return;const g=x=>{x.key==="Escape"&&(x.stopPropagation(),r(!1),a.current=!0)};return document.addEventListener("keydown",g,!0),()=>document.removeEventListener("keydown",g,!0)},[s]);const _=()=>{a.current=!0,r(!0)},m=()=>{a.current=!0,r(!1)};return s?l.jsxs(l.Fragment,{children:[l.jsx("div",{className:"fixed inset-0 z-[60] bg-fg/30","aria-hidden":"true",onClick:m}),l.jsxs("div",{role:"dialog","aria-modal":"true","aria-labelledby":f,className:["fixed inset-[5%] z-[61] flex flex-col","bg-surface border border-rule rounded-md","transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none",u?"opacity-100 scale-100":"opacity-0 scale-95"].join(" "),onClick:g=>g.stopPropagation(),children:[l.jsxs("div",{className:"px-5 pt-4 pb-3 border-b border-rule shrink-0 space-y-1",children:[l.jsx("h2",{id:f,className:"text-label uppercase tracking-wider text-fg-faint",children:"Chat Transcript"}),l.jsxs("div",{className:"flex items-baseline gap-3",children:[e&&l.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:e}),l.jsx("button",{type:"button",onClick:m,"aria-label":"Collapse transcript",className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out focus-mark rounded-sm px-1 ml-auto",children:"collapse ×"})]})]}),l.jsx("div",{ref:i,onScroll:h,className:"flex-1 overflow-y-auto p-5",children:t})]})]}):l.jsxs("div",{children:[l.jsx("header",{className:"flex items-baseline justify-end mb-4 gap-3",children:l.jsxs("div",{className:"flex items-baseline gap-3 min-w-0",children:[e&&l.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum shrink-0",children:e}),l.jsx("button",{type:"button",onClick:_,"aria-label":"Expand transcript",className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out focus-mark rounded-sm shrink-0",children:"expand ⤢"})]})}),l.jsx("div",{ref:n,onScroll:h,className:"h-96 overflow-y-auto p-4",children:t})]})}const k=512,ee=/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?(?!\d)/;function te(t){if(t.length===0)return null;const s=(t.length>k?t.slice(0,k):t).match(ee);return s?s[0]:null}function se({loading:t,error:e,result:s,caption:r}){if(t&&s===null)return l.jsx("p",{className:"text-fg-muted italic",children:"Fetching transcript."});if(e)return l.jsx("p",{className:"text-accent",role:"alert",children:e});if(!s)return null;if(s.turns.length===0)return l.jsx("p",{className:"text-fg-muted italic",children:"No turns in this session yet."});const n=Date.now();return l.jsx(K,{...r!==void 0?{caption:r}:{},children:l.jsxs("div",{className:"space-y-6",children:[r===void 0&&l.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint tnum",title:s.captured_at,children:M(s.captured_at)}),l.jsxs("p",{className:"text-label uppercase tracking-wider text-warn",children:["▲ ",F]}),l.jsx("ol",{className:"space-y-5",children:s.turns.map((i,a)=>l.jsx(ne,{turn:i,index:a,now:n},a))}),s.truncated&&l.jsxs("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:["Some turns truncated at the per-turn or total cap. Run"," ",l.jsx("code",{className:"text-fg-muted",children:"gc session peek"})," in a terminal for the full transcript."]})]})})}function ne({turn:t,index:e,now:s}){const r=d.useMemo(()=>re(t.text),[t.text]),n=d.useMemo(()=>te(t.text),[t.text]);return l.jsxs("li",{children:[l.jsxs("header",{className:"flex items-start justify-between gap-3 pb-2 border-b border-rule mb-2",children:[l.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:["#",(e+1).toString().padStart(2,"0")]}),l.jsxs("div",{className:"flex flex-col items-end leading-tight",title:n??void 0,children:[l.jsx("span",{className:"text-body text-fg tnum",children:U(n)}),l.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:v(n,s)}),l.jsx(ie,{role:t.role})]})]}),l.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed overflow-x-auto text-fg",children:r})]})}function re(t){const e=Y(t),s=new P;s.use_classes=!0;const r=s.ansi_to_html(e);if(typeof DOMParser>"u")return[e];const n=new DOMParser().parseFromString(`${r}`,"text/html");return Array.from(n.body.childNodes).map((i,a)=>R(i,String(a)))}function R(t,e){if(t.nodeType===3)return t.textContent??"";if(t.nodeType!==1)return null;const s=t,r=Array.from(s.childNodes).map((n,i)=>R(n,`${e}-${i}`));return s.tagName.toLowerCase()==="br"?l.jsx("br",{},e):s.tagName.toLowerCase()!=="span"?l.jsx("span",{children:r},e):l.jsx("span",{className:s.getAttribute("class")??void 0,children:r},e)}function ie({role:t}){const e=ae(t);return l.jsx("span",{className:`text-label uppercase tracking-wider font-medium ${e}`,children:t.replace(/_/g," ")})}function ae(t){switch(t){case"assistant":return"text-accent";case"user":return"text-fg";case"system":return"text-warn";case"tool_use":case"tool_result":return"text-fg-muted";default:return"text-fg-faint"}}function de({sessionId:t,stream:e,showBadge:s=!0,showCaption:r=!1}){const n=q(t,e),i=n.status==="ready"?n.result:null,a=n.status==="loading",u=n.status==="failed"?n.error:null,c=le(n.stream),f=[];return r&&i&&(f.push(`${i.turns.length} turn(s)`),f.push(D(i.total_chars,"chars")),f.push(`captured ${v(i.captured_at,Date.now())}`)),l.jsxs("div",{className:"space-y-4",children:[s&&l.jsx("div",{className:"flex justify-end",children:l.jsx(B,{tone:c.tone,label:c.label,title:`Session stream: ${n.stream.status}`,className:"text-label uppercase tracking-wider"})}),l.jsx(se,{loading:a,error:u,result:i,...f.length>0?{caption:f.join(" · ")}:{}})]})}function le(t){switch(typeof t=="string"?t:t.status){case"open":return{tone:"ok",label:"live"};case"connecting":return{tone:"warn",label:"connecting"};case"closed":return{tone:"stuck",label:"offline"};case"degraded":return{tone:"warn",label:"degraded"};case"idle":return{tone:"neutral",label:"snapshot"}}}function he(t){return t===null?!1:t.running===!0||t.state==="active"||t.state==="running"}function pe(t){return t===null||!t.session?!1:t.running===!0||t.state==="active"||t.state==="running"}export{de as L,se as S,he as a,pe as i,q as u}; diff --git a/internal/api/dashboardspa/dist/assets/Mail-um-BH4TD.js b/internal/api/dashboardspa/dist/assets/Mail-yLXNL53p.js similarity index 97% rename from internal/api/dashboardspa/dist/assets/Mail-um-BH4TD.js rename to internal/api/dashboardspa/dist/assets/Mail-yLXNL53p.js index 22dd8c6a75..b6c73e725a 100644 --- a/internal/api/dashboardspa/dist/assets/Mail-um-BH4TD.js +++ b/internal/api/dashboardspa/dist/assets/Mail-yLXNL53p.js @@ -1,3 +1,3 @@ -import{j as e,r,w as re,N as L,O as qe,J as F,K as B,v as Ce,g as Me,z as ae,R as ne,S as se,B as M,i as _,a as Ue,M as Ye,P as Ae,Q as Le,u as Ke,b as Ve,A as Ge,T as be,U as Qe,V as Je,W as Re,X as Ie}from"./index-zPatq59W.js";import{a as Xe,L as Ze,m as et}from"./projectOf-nApq7eyo.js";import{a as tt,r as je}from"./routeHighlight-B30gQO2o.js";import{u as at,F as st}from"./useListFilters-C9ZhD4ch.js";import{T as rt}from"./Table-DRIQbbRJ.js";import{M as _e,P as nt}from"./constants-vAmcTKRZ.js";import{P as lt}from"./PageHeader-DGfr1hUc.js";import{F as P}from"./Field-CQOLMLGH.js";import{f as it}from"./time-D9v0saHV.js";function q(t){const a=t.trim();if(a.length===0||!a.includes("/")&&!a.includes("\\"))return a;const i=a.split(/[\\/]/).filter(m=>m.length>0),c=i[i.length-1];if(c===void 0)return a;const n=i[i.length-2];if(n===void 0)return c;const l=c.startsWith(`${n}-`)?c.slice(n.length+1):c;return`${Xe(n)} · ${l}`}function ot({collapsed:t,onToggle:a,children:i,className:c="w-full flex items-baseline justify-between gap-4 focus-mark",glyphClassName:n}){return e.jsx("button",{type:"button",onClick:a,className:c,"aria-expanded":!t,children:i({glyph:e.jsx(ct,{collapsed:t,className:n??""})})})}function ct({collapsed:t,className:a=""}){return e.jsx("span",{"aria-hidden":!0,className:`inline-block text-fg-faint transition-transform duration-150 ease-out-quart ${a}`,style:{transform:t?"rotate(-90deg)":"rotate(0deg)"},children:"▾"})}function dt({project:t,count:a,collapsed:i,onToggle:c,collapsible:n=!0}){return n?e.jsx(ot,{collapsed:i,onToggle:c,className:"group flex items-baseline gap-2 w-full text-left focus-mark rounded-sm py-1",glyphClassName:"group-hover:text-fg-muted tnum w-3",children:({glyph:l})=>e.jsxs(e.Fragment,{children:[l,e.jsx("span",{className:"text-title font-medium text-fg group-hover:text-fg",children:t}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:a})]})}):e.jsxs("div",{role:"heading","aria-level":2,className:"flex items-baseline gap-2 py-1 text-label uppercase tracking-wider text-fg-faint",children:[e.jsx("span",{"aria-hidden":!0,children:"·"}),e.jsx("span",{children:t}),e.jsx("span",{"aria-hidden":!0,children:"·"})]})}function ut({groups:t,columns:a,rowKey:i,onToggleProject:c,onRowClick:n,rowProps:l,emptyMessage:m,perProjectEmpty:b,initialSort:h}){return t.length===0?e.jsx("p",{className:"py-10 text-center text-fg-muted italic",children:m}):e.jsx("div",{className:"space-y-8",children:t.map(d=>e.jsxs("section",{children:[e.jsx(dt,{project:d.project,count:d.totalInProject,collapsed:d.collapsed,onToggle:()=>c(d.projectKey),collapsible:d.collapsible}),!d.collapsed&&e.jsx(rt,{columns:a,rows:d.rows,rowKey:i,empty:b??"No items.",...n!==void 0?{onRowClick:n}:{},...l!==void 0?{rowProps:l}:{},...h!==void 0?{initialSort:h}:{}})]},d.projectKey))})}const ye="border-rule pb-6 border-b sm:shrink-0 sm:pr-6 sm:pb-0 sm:border-b-0 sm:border-r";function mt({buckets:t,loading:a,sessionsUnavailable:i,value:c,onChange:n,onReset:l,isOperator:m}){const[b,h]=r.useState(!1),[d,S]=r.useState(""),{operatorAlias:y,operatorWireAlias:u}=re(),A=L(c,y),x=r.useMemo(()=>{const f=d.trim().toLowerCase();return t.map(g=>({tier:g.tier,aliases:g.aliases.filter(v=>v.toLowerCase()===u?!1:f.length===0?!0:L(v,y).toLowerCase().includes(f)||v.toLowerCase().includes(f))})).filter(g=>g.aliases.length>0)},[t,d,y,u]),R=f=>{n(f),h(!1),S("")};return b?e.jsxs("aside",{className:`${ye} sm:w-64`,children:[e.jsx("button",{type:"button",onClick:()=>h(!1),"aria-expanded":!0,className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark rounded-sm",children:"▾ Agents"}),e.jsxs("div",{className:"mt-2 text-label uppercase tracking-wider text-fg-faint",children:[m?"Reading as":e.jsx("span",{className:"text-accent",children:"▲ Reading as"})," ",e.jsx("span",{className:`not-italic ${m?"text-fg-muted":"text-accent"}`,children:A})]}),e.jsx("div",{className:"mt-3 border-b border-rule pb-1",children:e.jsx("input",{type:"search",value:d,onChange:f=>S(f.target.value),placeholder:"Find an agent","aria-label":"Find an agent",autoFocus:!0,className:"w-full bg-transparent border-0 text-body text-fg placeholder:text-fg-faint focus:outline-none focus:ring-0 px-0 py-0.5"})}),e.jsxs("div",{className:"mt-3 max-h-[28rem] overflow-y-auto -mr-2 pr-2 space-y-4",children:[x.length===0?e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:a?"Loading aliases":"No agents match."}):x.map(f=>e.jsxs("div",{children:[e.jsx("div",{className:"text-label uppercase tracking-wider text-fg-faint mb-1",children:qe(f.tier)}),e.jsx("ul",{className:"space-y-0.5",children:f.aliases.map(g=>{const v=g.toLowerCase()===c.toLowerCase();return e.jsx("li",{children:e.jsx("button",{type:"button",onClick:()=>R(g),"aria-current":v,className:`block w-full text-left truncate text-body transition-colors duration-150 ease-out-quart focus-mark rounded-sm py-0.5 ${v?"text-fg font-semibold":"text-fg-muted hover:text-fg"}`,title:L(g,y),children:L(g,y)})},g)})})]},f.tier)),a&&x.length>0&&e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Loading more agents"}),!a&&i&&x.length>0&&(ft(x)?e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Agent list and mail history both unavailable."}):e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Agent list unavailable; showing mail-derived aliases only."}))]}),!m&&e.jsxs("div",{className:"mt-4 pt-3 border-t border-rule space-y-2",children:[e.jsx("button",{type:"button",onClick:l,className:"block text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark underline decoration-dotted underline-offset-2 rounded-sm",children:"Back to operator"}),e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Read-only. Sends always go from the operator."})]})]}):e.jsxs("aside",{className:`${ye} sm:w-44`,children:[e.jsx("button",{type:"button",onClick:()=>h(!0),"aria-expanded":!1,className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark rounded-sm",children:"▸ Agents"}),e.jsx("div",{className:"mt-4 text-label uppercase tracking-wider text-fg-faint",children:m?"Reading as":e.jsx("span",{className:"text-accent",children:"▲ Reading as"})}),e.jsx("div",{className:`mt-1 text-body truncate ${m?"text-fg":"text-accent font-medium"}`,title:A,children:A}),!m&&e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",onClick:l,className:"mt-3 block text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark underline decoration-dotted underline-offset-2 rounded-sm",children:"Back to operator"}),e.jsx("p",{className:"mt-2 text-label uppercase tracking-wider text-fg-faint italic",children:"Read-only. Sends go from the operator."})]})]})}function ft(t){let a=0;for(const i of t)if(a+=i.aliases.length,a>1)return!1;return a<=1}async function pt(t,a){await F().sendMail(B("send supervisor mail"),{...t,from:a})}async function ve(t){await F().markMailRead(B("mark supervisor mail read"),t.id,U(t))}async function we(t){await F().markMailUnread(B("mark supervisor mail unread"),t.id,U(t))}async function xt(t){await F().archiveMail(B("archive supervisor mail"),t.id,U(t))}async function ht(t,a,i){await F().replyMail(B("reply supervisor mail"),t.id,{...a,from:i},U(t))}function U(t){return t.rig===void 0||t.rig.length===0?void 0:{rig:t.rig}}function gt({open:t,onClose:a,onSent:i}){const{viewingAs:c}=Ce(),n=Me(),{operatorAlias:l,operatorWireAlias:m}=re(),[b,h]=r.useState(""),[d,S]=r.useState(""),[y,u]=r.useState(""),[A,x]=r.useState(!1),[R,f]=r.useState(null);r.useEffect(()=>{t||(h(""),S(""),u(""),f(null))},[t]);const g=r.useCallback(async()=>{if(!n){x(!0),f(null);try{await pt({to:b,subject:d,body:y},m),i()}catch(N){f(ae(N,"send failed"))}finally{x(!1)}}},[y,i,n,d,b,m]),v=!n&&c.isOperator&&b.length>0&&d.length>0&&y.length>0&&!A;return e.jsx(_e,{open:t,onClose:a,title:"New message",caption:"Sends from the operator. Reading-as has no effect on the sender.",widthClass:"max-w-2xl",footer:e.jsxs(e.Fragment,{children:[e.jsx(M,{tone:"quiet",size:"sm",onClick:a,children:"Cancel"}),e.jsx(M,{tone:"accent",size:"sm",disabled:!v,title:n?_:void 0,onClick:()=>{g()},children:A?"Sending":"Send"})]}),children:e.jsxs("div",{className:"space-y-4",children:[e.jsx(P,{label:"From",variant:"form",children:e.jsx("input",{type:"text",value:c.isOperator?L(l,l):`${L(l,l)} (reading-as does not change sender)`,disabled:!0,className:"w-full bg-transparent border-0 border-b border-rule pb-1 text-body text-fg-muted italic"})}),e.jsx(P,{label:"To (alias)",variant:"form",children:e.jsx("input",{type:"text",autoFocus:!0,value:b,onChange:N=>h(N.target.value),placeholder:"mayor, mechanic, scix-worker, …",className:"w-full bg-transparent border-0 border-b border-rule pb-1 text-body text-fg placeholder:text-fg-faint focus:border-accent focus:outline-none transition-colors"})}),e.jsx(P,{label:"Subject",variant:"form",children:e.jsx("input",{type:"text",value:d,onChange:N=>S(N.target.value),maxLength:200,className:"w-full bg-transparent border-0 border-b border-rule pb-1 text-body text-fg focus:border-accent focus:outline-none transition-colors"})}),e.jsx(P,{label:"Body",variant:"form",children:e.jsx("textarea",{value:y,onChange:N=>u(N.target.value),rows:10,maxLength:16*1024,className:"w-full bg-surface-tint border border-rule rounded-sm px-3 py-2 text-body text-fg focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/40 resize-y"})}),n&&e.jsx(ne,{}),!c.isOperator&&e.jsx(se,{tone:"warn",label:`Reading as ${L(c.alias,l)}. Sends from this modal are structurally locked to the operator regardless.`}),R&&e.jsx(se,{tone:"stuck",label:R})]})})}function Ne({message:t,attentionSeverity:a=null}){return e.jsxs("article",{...bt(a),className:"space-y-3 pb-4 border-b border-rule last:border-0",children:[e.jsxs("header",{className:"flex items-baseline justify-between gap-3",children:[e.jsxs("div",{className:"text-label uppercase tracking-wider text-fg-muted truncate",children:[e.jsx("span",{className:"text-fg font-medium",children:q(t.from)}),e.jsx("span",{className:"mx-1.5 text-fg-faint",children:"→"}),e.jsx("span",{children:q(t.to)})]}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:jt(t.created_at)})]}),e.jsx("p",{className:"text-title font-semibold text-fg",children:t.subject}),e.jsx(se,{tone:"warn",label:nt}),e.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg overflow-x-auto",children:t.body})]})}function bt(t){return t===null?{}:{"data-attention-severity":t}}function jt(t){const a=Date.parse(t);return Number.isFinite(a)?new Date(a).toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"·"}const ke=[{id:"unread",label:"unread",match:t=>!t.read},{id:"read",label:"read",match:t=>t.read}],yt={id:"needs-you",label:"needs you",match:t=>!t.read&&!Je(t.from)},vt=t=>[t.from,t.to,t.subject,t.rig,t.body.split(` +import{j as e,r,w as re,M as L,N as qe,I as F,J as B,v as Ce,g as Me,z as ae,R as ne,S as se,B as M,i as _,a as Ue,K as Ye,O as Ae,P as Le,u as Ke,b as Ve,A as Ge,Q as be,T as Qe,U as Je,V as Re,W as Ie}from"./index-QWRimsO3.js";import{a as Xe,L as Ze,m as et}from"./projectOf-CAOn7SI-.js";import{a as tt,r as je}from"./routeHighlight-B30gQO2o.js";import{u as at,F as st}from"./useListFilters-D2HcBe10.js";import{T as rt}from"./Table-C94QdmsL.js";import{M as _e,P as nt}from"./constants-CVFL5iaz.js";import{P as lt}from"./PageHeader-CxbYmkHZ.js";import{F as P}from"./Field-pp_wh5a7.js";import{f as it}from"./time-D9v0saHV.js";function q(t){const a=t.trim();if(a.length===0||!a.includes("/")&&!a.includes("\\"))return a;const i=a.split(/[\\/]/).filter(m=>m.length>0),c=i[i.length-1];if(c===void 0)return a;const n=i[i.length-2];if(n===void 0)return c;const l=c.startsWith(`${n}-`)?c.slice(n.length+1):c;return`${Xe(n)} · ${l}`}function ot({collapsed:t,onToggle:a,children:i,className:c="w-full flex items-baseline justify-between gap-4 focus-mark",glyphClassName:n}){return e.jsx("button",{type:"button",onClick:a,className:c,"aria-expanded":!t,children:i({glyph:e.jsx(ct,{collapsed:t,className:n??""})})})}function ct({collapsed:t,className:a=""}){return e.jsx("span",{"aria-hidden":!0,className:`inline-block text-fg-faint transition-transform duration-150 ease-out-quart ${a}`,style:{transform:t?"rotate(-90deg)":"rotate(0deg)"},children:"▾"})}function dt({project:t,count:a,collapsed:i,onToggle:c,collapsible:n=!0}){return n?e.jsx(ot,{collapsed:i,onToggle:c,className:"group flex items-baseline gap-2 w-full text-left focus-mark rounded-sm py-1",glyphClassName:"group-hover:text-fg-muted tnum w-3",children:({glyph:l})=>e.jsxs(e.Fragment,{children:[l,e.jsx("span",{className:"text-title font-medium text-fg group-hover:text-fg",children:t}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:a})]})}):e.jsxs("div",{role:"heading","aria-level":2,className:"flex items-baseline gap-2 py-1 text-label uppercase tracking-wider text-fg-faint",children:[e.jsx("span",{"aria-hidden":!0,children:"·"}),e.jsx("span",{children:t}),e.jsx("span",{"aria-hidden":!0,children:"·"})]})}function ut({groups:t,columns:a,rowKey:i,onToggleProject:c,onRowClick:n,rowProps:l,emptyMessage:m,perProjectEmpty:b,initialSort:h}){return t.length===0?e.jsx("p",{className:"py-10 text-center text-fg-muted italic",children:m}):e.jsx("div",{className:"space-y-8",children:t.map(d=>e.jsxs("section",{children:[e.jsx(dt,{project:d.project,count:d.totalInProject,collapsed:d.collapsed,onToggle:()=>c(d.projectKey),collapsible:d.collapsible}),!d.collapsed&&e.jsx(rt,{columns:a,rows:d.rows,rowKey:i,empty:b??"No items.",...n!==void 0?{onRowClick:n}:{},...l!==void 0?{rowProps:l}:{},...h!==void 0?{initialSort:h}:{}})]},d.projectKey))})}const ye="border-rule pb-6 border-b sm:shrink-0 sm:pr-6 sm:pb-0 sm:border-b-0 sm:border-r";function mt({buckets:t,loading:a,sessionsUnavailable:i,value:c,onChange:n,onReset:l,isOperator:m}){const[b,h]=r.useState(!1),[d,S]=r.useState(""),{operatorAlias:y,operatorWireAlias:u}=re(),A=L(c,y),x=r.useMemo(()=>{const f=d.trim().toLowerCase();return t.map(g=>({tier:g.tier,aliases:g.aliases.filter(v=>v.toLowerCase()===u?!1:f.length===0?!0:L(v,y).toLowerCase().includes(f)||v.toLowerCase().includes(f))})).filter(g=>g.aliases.length>0)},[t,d,y,u]),R=f=>{n(f),h(!1),S("")};return b?e.jsxs("aside",{className:`${ye} sm:w-64`,children:[e.jsx("button",{type:"button",onClick:()=>h(!1),"aria-expanded":!0,className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark rounded-sm",children:"▾ Agents"}),e.jsxs("div",{className:"mt-2 text-label uppercase tracking-wider text-fg-faint",children:[m?"Reading as":e.jsx("span",{className:"text-accent",children:"▲ Reading as"})," ",e.jsx("span",{className:`not-italic ${m?"text-fg-muted":"text-accent"}`,children:A})]}),e.jsx("div",{className:"mt-3 border-b border-rule pb-1",children:e.jsx("input",{type:"search",value:d,onChange:f=>S(f.target.value),placeholder:"Find an agent","aria-label":"Find an agent",autoFocus:!0,className:"w-full bg-transparent border-0 text-body text-fg placeholder:text-fg-faint focus:outline-none focus:ring-0 px-0 py-0.5"})}),e.jsxs("div",{className:"mt-3 max-h-[28rem] overflow-y-auto -mr-2 pr-2 space-y-4",children:[x.length===0?e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:a?"Loading aliases":"No agents match."}):x.map(f=>e.jsxs("div",{children:[e.jsx("div",{className:"text-label uppercase tracking-wider text-fg-faint mb-1",children:qe(f.tier)}),e.jsx("ul",{className:"space-y-0.5",children:f.aliases.map(g=>{const v=g.toLowerCase()===c.toLowerCase();return e.jsx("li",{children:e.jsx("button",{type:"button",onClick:()=>R(g),"aria-current":v,className:`block w-full text-left truncate text-body transition-colors duration-150 ease-out-quart focus-mark rounded-sm py-0.5 ${v?"text-fg font-semibold":"text-fg-muted hover:text-fg"}`,title:L(g,y),children:L(g,y)})},g)})})]},f.tier)),a&&x.length>0&&e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Loading more agents"}),!a&&i&&x.length>0&&(ft(x)?e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Agent list and mail history both unavailable."}):e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Agent list unavailable; showing mail-derived aliases only."}))]}),!m&&e.jsxs("div",{className:"mt-4 pt-3 border-t border-rule space-y-2",children:[e.jsx("button",{type:"button",onClick:l,className:"block text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark underline decoration-dotted underline-offset-2 rounded-sm",children:"Back to operator"}),e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint italic",children:"Read-only. Sends always go from the operator."})]})]}):e.jsxs("aside",{className:`${ye} sm:w-44`,children:[e.jsx("button",{type:"button",onClick:()=>h(!0),"aria-expanded":!1,className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark rounded-sm",children:"▸ Agents"}),e.jsx("div",{className:"mt-4 text-label uppercase tracking-wider text-fg-faint",children:m?"Reading as":e.jsx("span",{className:"text-accent",children:"▲ Reading as"})}),e.jsx("div",{className:`mt-1 text-body truncate ${m?"text-fg":"text-accent font-medium"}`,title:A,children:A}),!m&&e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",onClick:l,className:"mt-3 block text-label uppercase tracking-wider text-fg-muted hover:text-fg focus-mark underline decoration-dotted underline-offset-2 rounded-sm",children:"Back to operator"}),e.jsx("p",{className:"mt-2 text-label uppercase tracking-wider text-fg-faint italic",children:"Read-only. Sends go from the operator."})]})]})}function ft(t){let a=0;for(const i of t)if(a+=i.aliases.length,a>1)return!1;return a<=1}async function pt(t,a){await F().sendMail(B("send supervisor mail"),{...t,from:a})}async function ve(t){await F().markMailRead(B("mark supervisor mail read"),t.id,U(t))}async function we(t){await F().markMailUnread(B("mark supervisor mail unread"),t.id,U(t))}async function xt(t){await F().archiveMail(B("archive supervisor mail"),t.id,U(t))}async function ht(t,a,i){await F().replyMail(B("reply supervisor mail"),t.id,{...a,from:i},U(t))}function U(t){return t.rig===void 0||t.rig.length===0?void 0:{rig:t.rig}}function gt({open:t,onClose:a,onSent:i}){const{viewingAs:c}=Ce(),n=Me(),{operatorAlias:l,operatorWireAlias:m}=re(),[b,h]=r.useState(""),[d,S]=r.useState(""),[y,u]=r.useState(""),[A,x]=r.useState(!1),[R,f]=r.useState(null);r.useEffect(()=>{t||(h(""),S(""),u(""),f(null))},[t]);const g=r.useCallback(async()=>{if(!n){x(!0),f(null);try{await pt({to:b,subject:d,body:y},m),i()}catch(N){f(ae(N,"send failed"))}finally{x(!1)}}},[y,i,n,d,b,m]),v=!n&&c.isOperator&&b.length>0&&d.length>0&&y.length>0&&!A;return e.jsx(_e,{open:t,onClose:a,title:"New message",caption:"Sends from the operator. Reading-as has no effect on the sender.",widthClass:"max-w-2xl",footer:e.jsxs(e.Fragment,{children:[e.jsx(M,{tone:"quiet",size:"sm",onClick:a,children:"Cancel"}),e.jsx(M,{tone:"accent",size:"sm",disabled:!v,title:n?_:void 0,onClick:()=>{g()},children:A?"Sending":"Send"})]}),children:e.jsxs("div",{className:"space-y-4",children:[e.jsx(P,{label:"From",variant:"form",children:e.jsx("input",{type:"text",value:c.isOperator?L(l,l):`${L(l,l)} (reading-as does not change sender)`,disabled:!0,className:"w-full bg-transparent border-0 border-b border-rule pb-1 text-body text-fg-muted italic"})}),e.jsx(P,{label:"To (alias)",variant:"form",children:e.jsx("input",{type:"text",autoFocus:!0,value:b,onChange:N=>h(N.target.value),placeholder:"mayor, mechanic, scix-worker, …",className:"w-full bg-transparent border-0 border-b border-rule pb-1 text-body text-fg placeholder:text-fg-faint focus:border-accent focus:outline-none transition-colors"})}),e.jsx(P,{label:"Subject",variant:"form",children:e.jsx("input",{type:"text",value:d,onChange:N=>S(N.target.value),maxLength:200,className:"w-full bg-transparent border-0 border-b border-rule pb-1 text-body text-fg focus:border-accent focus:outline-none transition-colors"})}),e.jsx(P,{label:"Body",variant:"form",children:e.jsx("textarea",{value:y,onChange:N=>u(N.target.value),rows:10,maxLength:16*1024,className:"w-full bg-surface-tint border border-rule rounded-sm px-3 py-2 text-body text-fg focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/40 resize-y"})}),n&&e.jsx(ne,{}),!c.isOperator&&e.jsx(se,{tone:"warn",label:`Reading as ${L(c.alias,l)}. Sends from this modal are structurally locked to the operator regardless.`}),R&&e.jsx(se,{tone:"stuck",label:R})]})})}function Ne({message:t,attentionSeverity:a=null}){return e.jsxs("article",{...bt(a),className:"space-y-3 pb-4 border-b border-rule last:border-0",children:[e.jsxs("header",{className:"flex items-baseline justify-between gap-3",children:[e.jsxs("div",{className:"text-label uppercase tracking-wider text-fg-muted truncate",children:[e.jsx("span",{className:"text-fg font-medium",children:q(t.from)}),e.jsx("span",{className:"mx-1.5 text-fg-faint",children:"→"}),e.jsx("span",{children:q(t.to)})]}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:jt(t.created_at)})]}),e.jsx("p",{className:"text-title font-semibold text-fg",children:t.subject}),e.jsx(se,{tone:"warn",label:nt}),e.jsx("pre",{className:"text-body whitespace-pre-wrap leading-relaxed text-fg overflow-x-auto",children:t.body})]})}function bt(t){return t===null?{}:{"data-attention-severity":t}}function jt(t){const a=Date.parse(t);return Number.isFinite(a)?new Date(a).toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"·"}const ke=[{id:"unread",label:"unread",match:t=>!t.read},{id:"read",label:"read",match:t=>t.read}],yt={id:"needs-you",label:"needs you",match:t=>!t.read&&!Je(t.from)},vt=t=>[t.from,t.to,t.subject,t.rig,t.body.split(` `)[0]],wt=1e3;function Ft(){const t=Ue(),a=Me(),i=re(),[c]=Ye(),n=At(c.get("message")),{viewingAs:l,setAlias:m,resetToOperator:b,aliasBuckets:h,aliasesLoading:d,sessionsUnavailable:S,loadAliases:y}=Ce(),[u,A]=r.useState(()=>n===null?"inbox":"all"),[x,R]=r.useState(()=>n===null?Ae:wt),[f,g]=r.useState(Le);r.useEffect(()=>{y()},[y]);const v=Ke(),{data:N,loading:le,error:Y,refresh:O}=Ve(`mail:${u}:${l.alias}:${i.operatorWireAlias}:${x}:${f}`,()=>Ge(u,l.alias,i,x,f,v)),j=r.useMemo(()=>N?.items??[],[N]),[ie,I]=r.useState(null);r.useEffect(()=>{Y&&I(Y)},[Y]);const[w,T]=r.useState(null),[K,W]=r.useState([]),[$e,oe]=r.useState(!1),V=r.useRef(null),[H,G]=r.useState(""),[E,ce]=r.useState(null),[Te,Q]=r.useState(!1),[$,z]=r.useState(()=>new Set),[Ee,de]=r.useState(null),J=r.useCallback(async s=>{if(T(s),W([]),G(""),I(null),!!s.thread_id){oe(!0);try{const o=await be(s.thread_id,l.alias,i,x);W(o.items)}catch(o){I(o instanceof Error?o.message:"thread failed")}finally{oe(!1)}}},[x,l.alias,i]);r.useEffect(()=>{if(n===null){V.current=null;return}if(V.current===n)return;const s=j.find(o=>o.id===n);s!==void 0&&(V.current=n,J(s))},[j,J,n]);const X=r.useCallback(async s=>{const o=w;if(o!==null&&!a){ce(s),I(null);try{if(s==="read")await ve(o),T({...o,read:!0});else if(s==="unread")await we(o),T({...o,read:!1});else if(s==="archive")await xt(o),T(null),W([]);else{const p=H.trim();if(p.length===0)return;if(await ht(o,{body:p},i.operatorWireAlias),G(""),o.thread_id){const De=await be(o.thread_id,l.alias,i,x);W(De.items)}}await O()}catch(p){I(ae(p,`${s} failed`))}finally{ce(null)}}},[x,a,O,H,w,l.alias,i]),ue=r.useMemo(()=>[{key:"from",label:"From",sortable:!0,sortValue:s=>q(s.from),render:s=>e.jsx("span",{className:"text-fg-muted",children:q(s.from)}),className:"w-48"},{key:"subject",label:"Subject",sortable:!0,sortValue:s=>s.subject,render:s=>e.jsxs("div",{className:"min-w-0",children:[e.jsx("p",{className:`truncate ${s.read?"text-fg-muted":"text-fg font-medium"}`,children:s.subject}),e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-faint mt-1 truncate",children:s.body.split(` `)[0]??""})]})},{key:"created_at",label:"When",sortable:!0,sortValue:s=>s.created_at,render:s=>e.jsx("span",{className:"tnum text-fg-muted",children:it(s.created_at,v)}),className:"w-24",align:"right"}],[v]),D=r.useMemo(()=>L(l.alias,i.operatorAlias),[l.alias,i.operatorAlias]),Z=r.useMemo(()=>u==="inbox"&&l.isOperator?Qe(j).length:0,[u,j,l.isOperator]),Pe=r.useMemo(()=>{const s=u==="all"?"all mail":u==="inbox"?"inbox":"sent";if(j.length===0)return`${Oe(s)} empty for ${D}.`;const o=u==="sent"?0:j.filter(p=>!p.read).length;return u==="inbox"&&l.isOperator?o===0?`${j.length} in inbox, all read.`:Z>0?`${j.length} in inbox, ${Z} need you of ${o} unread.`:`${j.length} in inbox, ${o} unread, none need you.`:o>0?`${j.length} in ${s}, ${o} unread.`:`${j.length} in ${s}.`},[u,j,D,Z,l.isOperator]),me=r.useMemo(()=>l.isOperator?[yt,...ke]:ke,[l.isOperator]),k=at({viewKey:`mail:${u}`,rows:j,projectOf:et,searchOf:vt,chips:me}),fe=u!=="sent",C=r.useMemo(()=>k.groups.flatMap(s=>s.rows),[k.groups]),pe=r.useMemo(()=>C.reduce((s,o)=>$.has(o.id)?s+1:s,0),[C,$]),ee=C.length>0&&pe===C.length;r.useEffect(()=>{z(new Set)},[u,l.alias]);const xe=r.useCallback(s=>{z(o=>{const p=new Set(o);return p.has(s)?p.delete(s):p.add(s),p})},[]),Fe=r.useCallback(()=>{z(ee?new Set:new Set(C.map(s=>s.id)))},[ee,C]),he=r.useCallback(async s=>{if(a)return;const o=C.filter(p=>$.has(p.id)&&p.read!==s);if(o.length!==0){de(s?"read":"unread"),I(null);try{await Promise.all(o.map(p=>s?ve(p):we(p))),z(new Set)}catch(p){I(ae(p,`bulk mark ${s?"read":"unread"} failed`))}finally{de(null),await O()}}},[a,C,$,O]),Be=r.useMemo(()=>({key:"__select",label:"",className:"w-8",render:s=>e.jsx("input",{type:"checkbox",className:"h-3.5 w-3.5 translate-y-[2px] cursor-pointer accent-fg focus-mark",checked:$.has(s.id),onChange:()=>xe(s.id),onClick:o=>o.stopPropagation(),"aria-label":`select mail: ${s.subject}`})}),[$,xe]),We=fe?[Be,...ue]:ue,He=r.useMemo(()=>s=>tt(je(t,"mail",s.id)),[t]),ge=r.useCallback(s=>je(t,"mail",s.id),[t]),te=u==="sent"?[]:me,ze=a||w===null||H.trim().length===0||E!==null||!l.isOperator;return e.jsxs("section",{children:[e.jsx(lt,{title:"Mail",synopsis:Pe,meta:e.jsxs(e.Fragment,{children:[ie&&e.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:ie}),a&&e.jsx(ne,{}),e.jsx(M,{size:"sm",onClick:()=>Q(!0),disabled:a||!l.isOperator,title:a?_:l.isOperator?"Compose a new message (sends as the operator)":"Switch back to the operator to compose",children:"Compose"}),e.jsx(M,{size:"sm",onClick:()=>{O()},disabled:le,children:le?"Refreshing":"Refresh"})]})}),e.jsxs("div",{className:"flex flex-col gap-8 sm:flex-row sm:items-start",children:[e.jsx(mt,{buckets:h,loading:d,sessionsUnavailable:S,value:l.alias,onChange:m,onReset:b,isOperator:l.isOperator}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("div",{className:"mb-6",children:e.jsx(Nt,{box:u,onChange:A})}),e.jsxs("div",{className:"mb-6 space-y-3",children:[e.jsx(Ze,{value:k.search,onChange:k.setSearch,placeholder:"Search mail by sender, subject, rig",matchCount:k.totalMatches,totalCount:j.length,ariaLabel:"Search mail"}),te.length>0&&e.jsxs("div",{className:"flex items-baseline justify-between gap-4 flex-wrap",children:[e.jsx(st,{chips:te,activeIds:k.activeChipIds,onToggle:k.toggleChip,legend:"Read state"}),e.jsx(Se,{limit:x,onLimitChange:R,onWindowChange:g,window:f})]}),te.length===0&&e.jsx("div",{className:"flex justify-end",children:e.jsx(Se,{limit:x,onLimitChange:R,onWindowChange:g,window:f})})]}),fe&&C.length>0&&e.jsx("div",{className:"mb-6",children:e.jsx(kt,{selectedCount:pe,allSelected:ee,onToggleAll:Fe,onMarkRead:()=>{he(!0)},onMarkUnread:()=>{he(!1)},bulkInFlight:Ee,readOnly:a})}),e.jsx(ut,{groups:k.groups,columns:We,rowKey:s=>s.id,onToggleProject:k.toggleProject,onRowClick:s=>{J(s)},rowProps:He,emptyMessage:k.search.length>0||k.activeChipIds.size>0?"No messages match the current search or filter.":`${u==="inbox"?"Inbox":"Sent"} empty for ${D}.`,perProjectEmpty:"No messages in this project.",initialSort:{key:"created_at",dir:"desc"}})]})]}),e.jsx(_e,{open:w!==null,onClose:()=>T(null),title:w?.subject??"Thread",caption:`Reading as ${D}, ${K.length} message(s)`,widthClass:"max-w-3xl",footer:w===null?null:e.jsxs(e.Fragment,{children:[e.jsx(M,{tone:"quiet",size:"sm",title:a?_:void 0,disabled:a||E!==null,onClick:()=>{X(w.read?"unread":"read")},children:w.read?"Mark unread":"Mark read"}),e.jsx(M,{tone:"quiet",size:"sm",title:a?_:void 0,disabled:a||E!==null,onClick:()=>{X("archive")},children:E==="archive"?"Archiving":"Archive"}),e.jsx(M,{tone:"accent",size:"sm",title:a?_:void 0,disabled:ze,onClick:()=>{X("reply")},children:E==="reply"?"Replying":"Reply"})]}),children:e.jsxs("div",{className:"space-y-6",children:[$e?e.jsx("p",{className:"text-fg-muted italic",children:"Loading thread."}):K.length===0&&w?e.jsx(Ne,{message:w,attentionSeverity:ge(w)}):e.jsx("ol",{className:"space-y-6",children:K.map(s=>e.jsx("li",{children:e.jsx(Ne,{message:s,attentionSeverity:ge(s)})},s.id))}),w!==null&&e.jsx(P,{label:"Reply",variant:"form",children:e.jsx("textarea",{value:H,onChange:s=>G(s.target.value),rows:5,maxLength:16*1024,title:a?_:void 0,disabled:a||!l.isOperator,className:"w-full bg-surface-tint border border-rule rounded-sm px-3 py-2 text-body text-fg focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/40 resize-y disabled:opacity-50"})})]})}),e.jsx(gt,{open:Te,onClose:()=>Q(!1),onSent:()=>{Q(!1),u==="sent"&&O()}})]})}function Nt({box:t,onChange:a}){return e.jsx("div",{className:"flex items-baseline gap-6",children:["inbox","sent","all"].map(i=>e.jsx("button",{type:"button",onClick:()=>a(i),className:`text-title transition-colors duration-150 ease-out-quart focus-mark rounded-sm ${t===i?"text-fg font-semibold":"text-fg-muted hover:text-fg"}`,children:i==="all"?"All":Oe(i)},i))})}function kt({selectedCount:t,allSelected:a,onToggleAll:i,onMarkRead:c,onMarkUnread:n,bulkInFlight:l,readOnly:m}){const b=r.useRef(null),h=t>0;r.useEffect(()=>{b.current!==null&&(b.current.indeterminate=h&&!a)},[h,a]);const d=l!==null,S=m?_:void 0;return e.jsxs("div",{className:"flex items-baseline justify-between gap-4 flex-wrap border-b border-rule pb-3",role:"region","aria-label":"bulk mail selection",children:[e.jsxs("label",{className:"flex items-baseline gap-2 text-label uppercase tracking-wider text-fg-muted cursor-pointer",children:[e.jsx("input",{ref:b,type:"checkbox",className:"h-3.5 w-3.5 translate-y-[2px] cursor-pointer accent-fg focus-mark",checked:a,onChange:i,"aria-label":"select all mail"}),e.jsx("span",{children:h?`${t} selected`:"Select all"})]}),h&&e.jsxs("div",{className:"flex items-baseline gap-3",children:[m&&e.jsx(ne,{}),e.jsx(M,{size:"sm",tone:"quiet",onClick:c,disabled:m||d,title:S,children:l==="read"?"Marking":"Mark read"}),e.jsx(M,{size:"sm",tone:"quiet",onClick:n,disabled:m||d,title:S,children:l==="unread"?"Marking":"Mark unread"})]})]})}function Se({limit:t,onLimitChange:a,onWindowChange:i,window:c}){return e.jsxs("div",{className:"flex items-baseline gap-3 flex-wrap",children:[e.jsxs("label",{className:"flex items-baseline gap-2 text-label uppercase tracking-wider text-fg-muted",children:[e.jsx("span",{children:"Window"}),e.jsx("select",{"aria-label":"Mail time window",value:c,onChange:n=>i(Ct(n.target.value)),className:"bg-transparent border border-rule rounded-sm px-2 py-1 text-label uppercase tracking-wider text-fg-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/40",children:Re.map(n=>e.jsx("option",{value:n,children:Mt(n)},n))})]}),e.jsxs("label",{className:"flex items-baseline gap-2 text-label uppercase tracking-wider text-fg-muted",children:[e.jsx("span",{children:"History"}),e.jsx("select",{"aria-label":"Mail history limit",value:t,onChange:n=>a(St(n.target.value)),className:"bg-transparent border border-rule rounded-sm px-2 py-1 text-label uppercase tracking-wider text-fg-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/40",children:Ie.map(n=>e.jsxs("option",{value:n,children:["Recent ",n]},n))})]})]})}function St(t){const a=Number(t);return Ie.includes(a)?a:Ae}function Ct(t){return Re.includes(t)?t:Le}function Mt(t){return t==="24h"?"Last 24h":t==="7d"?"Last 7d":"All time"}function At(t){const a=t?.trim();return a&&a.length>0?a:null}function Oe(t){return t.charAt(0).toUpperCase()+t.slice(1)}export{Ft as MailPage}; diff --git a/internal/api/dashboardspa/dist/assets/PageHeader-DGfr1hUc.js b/internal/api/dashboardspa/dist/assets/PageHeader-CxbYmkHZ.js similarity index 89% rename from internal/api/dashboardspa/dist/assets/PageHeader-DGfr1hUc.js rename to internal/api/dashboardspa/dist/assets/PageHeader-CxbYmkHZ.js index aa7b9ae83d..07606c6c7a 100644 --- a/internal/api/dashboardspa/dist/assets/PageHeader-DGfr1hUc.js +++ b/internal/api/dashboardspa/dist/assets/PageHeader-CxbYmkHZ.js @@ -1 +1 @@ -import{j as e}from"./index-zPatq59W.js";function d({title:t,synopsis:s,meta:a,className:r=""}){return e.jsxs("header",{className:`grid grid-cols-1 items-start gap-x-6 gap-y-4 mb-10 md:grid-cols-[minmax(0,1fr)_auto] md:items-end ${r}`,children:[e.jsxs("div",{className:"min-w-0 space-y-2",children:[e.jsx("h1",{className:"text-display font-semibold tracking-tighter text-fg leading-[1.05]",children:t}),s&&e.jsx("p",{className:"text-body text-fg-muted max-w-prose",children:s})]}),a&&e.jsx("div",{className:"flex flex-wrap items-center gap-4 text-label uppercase tracking-wider md:justify-end",children:a})]})}export{d as P}; +import{j as e}from"./index-QWRimsO3.js";function d({title:t,synopsis:s,meta:a,className:r=""}){return e.jsxs("header",{className:`grid grid-cols-1 items-start gap-x-6 gap-y-4 mb-10 md:grid-cols-[minmax(0,1fr)_auto] md:items-end ${r}`,children:[e.jsxs("div",{className:"min-w-0 space-y-2",children:[e.jsx("h1",{className:"text-display font-semibold tracking-tighter text-fg leading-[1.05]",children:t}),s&&e.jsx("p",{className:"text-body text-fg-muted max-w-prose",children:s})]}),a&&e.jsx("div",{className:"flex flex-wrap items-center gap-4 text-label uppercase tracking-wider md:justify-end",children:a})]})}export{d as P}; diff --git a/internal/api/dashboardspa/dist/assets/Runs-C9G772Th.js b/internal/api/dashboardspa/dist/assets/Runs-C9G772Th.js new file mode 100644 index 0000000000..41fb76c91c --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/Runs-C9G772Th.js @@ -0,0 +1 @@ +import{j as e,L as B,a4 as O,r as x,a5 as D,a as M,a6 as U,K as z,u as V,B as w}from"./index-QWRimsO3.js";import{b as F,r as Y}from"./routeHighlight-B30gQO2o.js";import{P as K}from"./PageHeader-CxbYmkHZ.js";import{S as Q,P as q}from"./SseIndicator-DDbpxu-X.js";import{f as _}from"./time-D9v0saHV.js";import{S as G}from"./StageLadder-DgEuJnhe.js";const f=8;function W(t){return t==="blocked"?"text-accent":t==="complete"?"text-fg-muted":"text-fg"}function I({lane:t,now:n,attentionSeverity:r=null,blocked:s}){const a=Object.entries(t.statusCounts).sort((l,c)=>k(l[0]).localeCompare(k(c[0]))),{className:i="",...d}=F(r);return e.jsxs("li",{...d,className:`py-4 transition-colors duration-150 ease-out-quart ${i}`,children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[e.jsx("span",{className:`text-label uppercase tracking-wider ${W(t.phase)}`,children:t.phaseLabel}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum tabular-nums",title:t.updatedAt.status==="available"?t.updatedAt.at:t.updatedAt.error,children:t.updatedAt.status==="available"?_(t.updatedAt.at,n):"·"})]}),e.jsx(B,{to:O(t.id,t.scope),className:"focus-mark mt-1 block text-body text-fg leading-snug hover:text-accent",children:t.title}),(t.external.status!=="unavailable"||t.formula.status==="known")&&e.jsxs("div",{className:"mt-1 flex items-baseline gap-x-4 gap-y-1 flex-wrap text-label",children:[t.external.status!=="unavailable"&&(t.external.status==="available"?e.jsx("a",{href:t.external.url,target:"_blank",rel:"noreferrer",className:"text-fg-muted uppercase tracking-wider hover:text-fg focus-mark",children:t.external.label}):e.jsx("span",{className:"text-fg-muted uppercase tracking-wider",children:t.external.label})),t.formula.status==="known"&&e.jsx("span",{className:"text-fg-faint tnum",children:t.formula.name})]}),e.jsx(G,{stages:t.stages,label:t.title}),e.jsxs("div",{className:"mt-2 flex items-baseline gap-x-4 gap-y-1 flex-wrap text-label",children:[e.jsx("span",{className:"text-fg-faint tnum",title:"run root bead",children:t.id}),t.activeAssignees.length>0&&e.jsxs("span",{className:"text-fg-muted lowercase tracking-normal",children:[e.jsx("span",{className:"uppercase tracking-wider text-fg-faint",children:"on "}),t.activeAssignees.join(", ")]}),a.length>0&&e.jsx("span",{className:"text-fg-faint uppercase tracking-wider tnum tabular-nums",children:a.map(([l,c])=>`${c} ${l.replace(/_/g," ")}`).join(" · ")})]}),s!==void 0&&e.jsxs("div",{className:"mt-2",children:[e.jsxs("p",{className:"text-body text-fg leading-snug",children:[e.jsx("span",{"aria-hidden":"true",className:"text-accent",children:"✕"})," ",s.reason]}),e.jsx("p",{className:"mt-1 text-body text-fg-muted leading-snug",children:s.remedy})]})]})}function k(t){return`${{blocked:"0",in_progress:"1",open:"2",closed:"3"}[t]??"9"}-${t}`}const X=[["prReview","PR"],["designReview","Design"],["bugfix","Bugfix"],["other","Other"]],C="runs-historical-section",S="runs-historical-list",R="runs-active-list",h=5;function J({source:t,now:n,showHistory:r,attentionSeverity:s}){if(t.status==="error")return e.jsxs("section",{children:[e.jsx(A,{summary:null}),e.jsx("p",{className:"mt-8 text-body text-fg-muted italic",children:`Run data unavailable: ${t.error}.`})]});const a=t.data;return e.jsxs("section",{children:[e.jsx(A,{summary:a}),e.jsx(Z,{summary:a,now:n,...s===void 0?{}:{attentionSeverity:s}}),e.jsx(se,{summary:a,now:n,...s===void 0?{}:{attentionSeverity:s}}),r&&e.jsx(ae,{summary:a,now:n,...s===void 0?{}:{attentionSeverity:s}})]})}function Z({summary:t,now:n,attentionSeverity:r}){const[s,a]=x.useState(!1);if(t.lanes.length===0){if(t.lanesPartial===!0)return e.jsx("p",{className:"mt-8 text-body text-fg-muted italic",children:"Run sources were partially unavailable; the lane set may be incomplete."});const l=t.totalHistorical>0?` (${t.totalHistorical} completed.)`:"";return e.jsx("p",{className:"mt-8 text-body text-fg-muted italic",children:`No active formula runs.${l}`})}const i=s?t.lanes:t.lanes.slice(0,f),d=ee(i);return e.jsxs(e.Fragment,{children:[e.jsx("div",{id:R,children:d.map(({rig:l,lanes:c})=>e.jsxs("div",{className:"mt-6",children:[e.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint",children:te(l)}),e.jsx(H,{lanes:c,now:n,...r===void 0?{}:{attentionSeverity:r}})]},l))}),t.lanes.length>f&&e.jsx("button",{type:"button",onClick:()=>a(l=>!l),"aria-expanded":s,"aria-controls":R,className:"mt-3 text-label uppercase tracking-wider text-fg-faint tnum hover:text-fg focus-mark",children:s?"Show fewer":`Show ${t.lanes.length-f} more runs`})]})}function H({lanes:t,now:n,attentionSeverity:r,listId:s}){return e.jsx("ol",{...s===void 0?{}:{id:s},className:"mt-3 divide-y divide-rule",children:t.map(a=>e.jsx(I,{lane:a,now:n,...r===void 0?{}:{attentionSeverity:r(a)}},a.id))})}function ee(t){const n=[],r=new Map;for(const s of t){const a=s.scope.status==="available"&&s.scope.kind==="rig"?s.scope.rootStoreRef:"city";let i=r.get(a);i===void 0&&(i=[],r.set(a,i),n.push(a)),i.push(s)}return n.map(s=>({rig:s,lanes:r.get(s)}))}function te(t){return t.replace(/^rig:/,"")}function se({summary:t,now:n,attentionSeverity:r}){const s=new Map(D(t.blockedLanes).map(a=>[a.id,a]));return s.size===0?null:e.jsxs("section",{"aria-label":"Blocked runs",className:"mt-12",children:[e.jsxs("h2",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:["Blocked (",s.size,")"]}),e.jsx("ol",{className:"mt-3 divide-y divide-rule",children:t.blockedLanes.map(a=>{const i=s.get(a.id);return e.jsx(I,{lane:a,now:n,...r===void 0?{}:{attentionSeverity:r(a)},...i===void 0?{}:{blocked:i}},a.id)})})]})}function ae({summary:t,now:n,attentionSeverity:r}){const[s,a]=x.useState(!1),i=t.historicalLanes,d=s?i:i.slice(0,h);return e.jsxs("section",{id:C,"aria-label":"Historical runs",className:"mt-12",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Historical"}),i.length===0?e.jsx("p",{className:"mt-3 text-body text-fg-muted italic",children:"No completed runs in the current window."}):e.jsxs(e.Fragment,{children:[e.jsx(H,{lanes:d,now:n,listId:S,...r===void 0?{}:{attentionSeverity:r}}),i.length>h&&e.jsx("button",{type:"button",onClick:()=>a(l=>!l),"aria-expanded":s,"aria-controls":S,className:"mt-3 text-label uppercase tracking-wider text-fg-faint tnum hover:text-fg focus-mark",children:s?"Show fewer":`Show ${i.length-h} more`}),t.totalHistorical>i.length&&e.jsxs("p",{className:"mt-3 text-label uppercase tracking-wider text-fg-faint tnum",children:["Showing ",i.length," most-recent of ",t.totalHistorical]})]})]})}function A({summary:t}){const n=t?.runCounts.total??0,r=t?.runCounts.blocked??0;return e.jsx("header",{className:"space-y-2",children:e.jsxs("div",{className:"flex items-baseline gap-x-6 gap-y-2 flex-wrap",children:[e.jsx(g,{label:"Active",value:n,tone:"strong"}),X.map(([s,a])=>e.jsx(g,{label:a,value:t?.runCounts[s]??0,tone:"muted"},s)),r>0&&e.jsx(g,{label:"Blocked",value:r,tone:"muted"})]})})}function g({label:t,value:n,tone:r}){const s=r==="strong"?"text-fg":"text-fg-muted";return e.jsxs("div",{className:"flex flex-col",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:t}),e.jsx("span",{className:`text-title tnum ${s}`,children:n})]})}const re=C,L="Phase grammar: intake, implementation, review, approval, finalization.",b="history",y="1";function xe(){const t=M(),{source:n,loading:r,error:s,refresh:a,sseState:i}=U(),[d,l]=z(),c=d.get(b)===y,j=V(),o=n??null,N=o?.status==="fresh"||o?.status==="fixture"||o?.status==="stale"?o.data:null,u=N?.totalHistorical??0,$=N?.lanesPartial===!0,P=x.useCallback(()=>{l(m=>{const p=new URLSearchParams(m);return c?p.delete(b):p.set(b,y),p},{replace:!1})},[c,l]),E=x.useCallback(m=>Y(t,"runs",m.id),[t]),T=ne(n),v=o?o.status==="fresh"?null:o.status==="fixture"?"fixture data":o.status==="error"?"live data unavailable":o.fetchedAt?`stale ${_(o.fetchedAt,j)} ago`:"stale":null;return e.jsxs("section",{children:[e.jsx(K,{title:"Formula Runs",synopsis:T,className:"md:items-start",meta:e.jsxs(e.Fragment,{children:[s&&e.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:s}),v!==null&&e.jsx("span",{className:`text-label uppercase tracking-wider tnum ${o?.status==="error"?"text-accent":"text-fg-faint"}`,children:v}),e.jsxs("div",{className:"grid w-full min-w-[18rem] grid-cols-[7rem_minmax(6.5rem,1fr)] items-center gap-x-4 gap-y-3 sm:w-[34rem] sm:grid-cols-[7rem_6.5rem_10rem_7rem]",children:[e.jsx(Q,{state:i}),e.jsx("span",{children:$?e.jsx(q,{glyph:"◐",label:"runs partial",title:"one or more rigs' recent runs were unavailable; the lane set may be incomplete"}):e.jsx("span",{"aria-hidden":"true",className:"invisible normal-case text-body text-warn",children:"runs partial"})}),e.jsx(w,{size:"sm",className:"w-full justify-center",onClick:P,disabled:!c&&u===0,"aria-expanded":c,...c?{"aria-controls":re}:{},"aria-label":c?"Hide historical formula runs.":u===0?"No completed formula runs in the current window.":`Show ${u} completed formula runs.`,children:c?"Hide history":u>0?`Show history (${u})`:"Show history"}),e.jsx(w,{size:"sm",className:"w-full justify-center",onClick:()=>{a()},disabled:r,children:r?"Refreshing":"Refresh"})]})]})}),n===void 0||o===null?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading formula runs."}):e.jsx(J,{source:o,now:j,showHistory:c,attentionSeverity:E})]})}function ne(t){return t===void 0?"Loading formula run lanes.":t.status!=="error"?`${t.data.totalActive} active runs across the supervisor's bead store. ${L}`:`Run counts unavailable: ${t.error}. ${L}`}export{xe as RunsPage}; diff --git a/internal/api/dashboardspa/dist/assets/Runs-CTNTp8Tf.js b/internal/api/dashboardspa/dist/assets/Runs-CTNTp8Tf.js deleted file mode 100644 index 03ab308b75..0000000000 --- a/internal/api/dashboardspa/dist/assets/Runs-CTNTp8Tf.js +++ /dev/null @@ -1 +0,0 @@ -import{j as e,L as B,ak as O,r as x,al as f,am as M,a as D,an as U,M as z,u as V,B as w}from"./index-zPatq59W.js";import{b as F,r as Y}from"./routeHighlight-B30gQO2o.js";import{P as Q}from"./PageHeader-DGfr1hUc.js";import{S as q,P as G}from"./SseIndicator-BpC5bgiy.js";import{f as _}from"./time-D9v0saHV.js";import{S as K}from"./StageLadder-CwHTgfDd.js";function W(t){return t==="blocked"?"text-accent":t==="complete"?"text-fg-muted":"text-fg"}function I({lane:t,now:n,attentionSeverity:r=null,blocked:s}){const a=Object.entries(t.statusCounts).sort((l,c)=>k(l[0]).localeCompare(k(c[0]))),{className:i="",...d}=F(r);return e.jsxs("li",{...d,className:`py-4 transition-colors duration-150 ease-out-quart ${i}`,children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-4",children:[e.jsx("span",{className:`text-label uppercase tracking-wider ${W(t.phase)}`,children:t.phaseLabel}),e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum tabular-nums",title:t.updatedAt.status==="available"?t.updatedAt.at:t.updatedAt.error,children:t.updatedAt.status==="available"?_(t.updatedAt.at,n):"·"})]}),e.jsx(B,{to:O(t.id,t.scope),className:"focus-mark mt-1 block text-body text-fg leading-snug hover:text-accent",children:t.title}),(t.external.status!=="unavailable"||t.formula.status==="known")&&e.jsxs("div",{className:"mt-1 flex items-baseline gap-x-4 gap-y-1 flex-wrap text-label",children:[t.external.status!=="unavailable"&&(t.external.status==="available"?e.jsx("a",{href:t.external.url,target:"_blank",rel:"noreferrer",className:"text-fg-muted uppercase tracking-wider hover:text-fg focus-mark",children:t.external.label}):e.jsx("span",{className:"text-fg-muted uppercase tracking-wider",children:t.external.label})),t.formula.status==="known"&&e.jsx("span",{className:"text-fg-faint tnum",children:t.formula.name})]}),e.jsx(K,{stages:t.stages,label:t.title}),e.jsxs("div",{className:"mt-2 flex items-baseline gap-x-4 gap-y-1 flex-wrap text-label",children:[e.jsx("span",{className:"text-fg-faint tnum",title:"run root bead",children:t.id}),t.activeAssignees.length>0&&e.jsxs("span",{className:"text-fg-muted lowercase tracking-normal",children:[e.jsx("span",{className:"uppercase tracking-wider text-fg-faint",children:"on "}),t.activeAssignees.join(", ")]}),a.length>0&&e.jsx("span",{className:"text-fg-faint uppercase tracking-wider tnum tabular-nums",children:a.map(([l,c])=>`${c} ${l.replace(/_/g," ")}`).join(" · ")})]}),s!==void 0&&e.jsxs("div",{className:"mt-2",children:[e.jsxs("p",{className:"text-body text-fg leading-snug",children:[e.jsx("span",{"aria-hidden":"true",className:"text-accent",children:"✕"})," ",s.reason]}),e.jsx("p",{className:"mt-1 text-body text-fg-muted leading-snug",children:s.remedy})]})]})}function k(t){return`${{blocked:"0",in_progress:"1",open:"2",closed:"3"}[t]??"9"}-${t}`}const X=[["prReview","PR"],["designReview","Design"],["bugfix","Bugfix"],["other","Other"]],C="runs-historical-section",S="runs-historical-list",R="runs-active-list",h=5;function J({source:t,now:n,showHistory:r,attentionSeverity:s}){if(t.status==="error")return e.jsxs("section",{children:[e.jsx(A,{summary:null}),e.jsx("p",{className:"mt-8 text-body text-fg-muted italic",children:`Run data unavailable: ${t.error}.`})]});const a=t.data;return e.jsxs("section",{children:[e.jsx(A,{summary:a}),e.jsx(Z,{summary:a,now:n,...s===void 0?{}:{attentionSeverity:s}}),e.jsx(se,{summary:a,now:n,...s===void 0?{}:{attentionSeverity:s}}),r&&e.jsx(ae,{summary:a,now:n,...s===void 0?{}:{attentionSeverity:s}})]})}function Z({summary:t,now:n,attentionSeverity:r}){const[s,a]=x.useState(!1);if(t.lanes.length===0){if(t.lanesPartial===!0)return e.jsx("p",{className:"mt-8 text-body text-fg-muted italic",children:"Run sources were partially unavailable; the lane set may be incomplete."});const l=t.totalHistorical>0?` (${t.totalHistorical} completed.)`:"";return e.jsx("p",{className:"mt-8 text-body text-fg-muted italic",children:`No active formula runs.${l}`})}const i=s?t.lanes:t.lanes.slice(0,f),d=ee(i);return e.jsxs(e.Fragment,{children:[e.jsx("div",{id:R,children:d.map(({rig:l,lanes:c})=>e.jsxs("div",{className:"mt-6",children:[e.jsx("h3",{className:"text-label uppercase tracking-wider text-fg-faint",children:te(l)}),e.jsx(H,{lanes:c,now:n,...r===void 0?{}:{attentionSeverity:r}})]},l))}),t.lanes.length>f&&e.jsx("button",{type:"button",onClick:()=>a(l=>!l),"aria-expanded":s,"aria-controls":R,className:"mt-3 text-label uppercase tracking-wider text-fg-faint tnum hover:text-fg focus-mark",children:s?"Show fewer":`Show ${t.lanes.length-f} more runs`})]})}function H({lanes:t,now:n,attentionSeverity:r,listId:s}){return e.jsx("ol",{...s===void 0?{}:{id:s},className:"mt-3 divide-y divide-rule",children:t.map(a=>e.jsx(I,{lane:a,now:n,...r===void 0?{}:{attentionSeverity:r(a)}},a.id))})}function ee(t){const n=[],r=new Map;for(const s of t){const a=s.scope.status==="available"&&s.scope.kind==="rig"?s.scope.rootStoreRef:"city";let i=r.get(a);i===void 0&&(i=[],r.set(a,i),n.push(a)),i.push(s)}return n.map(s=>({rig:s,lanes:r.get(s)}))}function te(t){return t.replace(/^rig:/,"")}function se({summary:t,now:n,attentionSeverity:r}){const s=new Map(M(t.blockedLanes).map(a=>[a.id,a]));return s.size===0?null:e.jsxs("section",{"aria-label":"Blocked runs",className:"mt-12",children:[e.jsxs("h2",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:["Blocked (",s.size,")"]}),e.jsx("ol",{className:"mt-3 divide-y divide-rule",children:t.blockedLanes.map(a=>{const i=s.get(a.id);return e.jsx(I,{lane:a,now:n,...r===void 0?{}:{attentionSeverity:r(a)},...i===void 0?{}:{blocked:i}},a.id)})})]})}function ae({summary:t,now:n,attentionSeverity:r}){const[s,a]=x.useState(!1),i=t.historicalLanes,d=s?i:i.slice(0,h);return e.jsxs("section",{id:C,"aria-label":"Historical runs",className:"mt-12",children:[e.jsx("h2",{className:"text-label uppercase tracking-wider text-fg-faint",children:"Historical"}),i.length===0?e.jsx("p",{className:"mt-3 text-body text-fg-muted italic",children:"No completed runs in the current window."}):e.jsxs(e.Fragment,{children:[e.jsx(H,{lanes:d,now:n,listId:S,...r===void 0?{}:{attentionSeverity:r}}),i.length>h&&e.jsx("button",{type:"button",onClick:()=>a(l=>!l),"aria-expanded":s,"aria-controls":S,className:"mt-3 text-label uppercase tracking-wider text-fg-faint tnum hover:text-fg focus-mark",children:s?"Show fewer":`Show ${i.length-h} more`}),t.totalHistorical>i.length&&e.jsxs("p",{className:"mt-3 text-label uppercase tracking-wider text-fg-faint tnum",children:["Showing ",i.length," most-recent of ",t.totalHistorical]})]})]})}function A({summary:t}){const n=t?.runCounts.total??0,r=t?.runCounts.blocked??0;return e.jsx("header",{className:"space-y-2",children:e.jsxs("div",{className:"flex items-baseline gap-x-6 gap-y-2 flex-wrap",children:[e.jsx(g,{label:"Active",value:n,tone:"strong"}),X.map(([s,a])=>e.jsx(g,{label:a,value:t?.runCounts[s]??0,tone:"muted"},s)),r>0&&e.jsx(g,{label:"Blocked",value:r,tone:"muted"})]})})}function g({label:t,value:n,tone:r}){const s=r==="strong"?"text-fg":"text-fg-muted";return e.jsxs("div",{className:"flex flex-col",children:[e.jsx("span",{className:"text-label uppercase tracking-wider text-fg-faint",children:t}),e.jsx("span",{className:`text-title tnum ${s}`,children:n})]})}const re=C,L="Phase grammar: intake, implementation, review, approval, finalization.",b="history",y="1";function xe(){const t=D(),{source:n,loading:r,error:s,refresh:a,sseState:i}=U(),[d,l]=z(),c=d.get(b)===y,j=V(),o=n??null,N=o?.status==="fresh"||o?.status==="fixture"||o?.status==="stale"?o.data:null,u=N?.totalHistorical??0,$=N?.lanesPartial===!0,P=x.useCallback(()=>{l(m=>{const p=new URLSearchParams(m);return c?p.delete(b):p.set(b,y),p},{replace:!1})},[c,l]),E=x.useCallback(m=>Y(t,"runs",m.id),[t]),T=ne(n),v=o?o.status==="fresh"?null:o.status==="fixture"?"fixture data":o.status==="error"?"live data unavailable":o.fetchedAt?`stale ${_(o.fetchedAt,j)} ago`:"stale":null;return e.jsxs("section",{children:[e.jsx(Q,{title:"Formula Runs",synopsis:T,className:"md:items-start",meta:e.jsxs(e.Fragment,{children:[s&&e.jsx("span",{className:"normal-case text-body text-accent",role:"alert",children:s}),v!==null&&e.jsx("span",{className:`text-label uppercase tracking-wider tnum ${o?.status==="error"?"text-accent":"text-fg-faint"}`,children:v}),e.jsxs("div",{className:"grid w-full min-w-[18rem] grid-cols-[7rem_minmax(6.5rem,1fr)] items-center gap-x-4 gap-y-3 sm:w-[34rem] sm:grid-cols-[7rem_6.5rem_10rem_7rem]",children:[e.jsx(q,{state:i}),e.jsx("span",{children:$?e.jsx(G,{glyph:"◐",label:"runs partial",title:"one or more rigs' recent runs were unavailable; the lane set may be incomplete"}):e.jsx("span",{"aria-hidden":"true",className:"invisible normal-case text-body text-warn",children:"runs partial"})}),e.jsx(w,{size:"sm",className:"w-full justify-center",onClick:P,disabled:!c&&u===0,"aria-expanded":c,...c?{"aria-controls":re}:{},"aria-label":c?"Hide historical formula runs.":u===0?"No completed formula runs in the current window.":`Show ${u} completed formula runs.`,children:c?"Hide history":u>0?`Show history (${u})`:"Show history"}),e.jsx(w,{size:"sm",className:"w-full justify-center",onClick:()=>{a()},disabled:r,children:r?"Refreshing":"Refresh"})]})]})}),n===void 0||o===null?e.jsx("p",{className:"text-body text-fg-muted italic",children:"Loading formula runs."}):e.jsx(J,{source:o,now:j,showHistory:c,attentionSeverity:E})]})}function ne(t){return t===void 0?"Loading formula run lanes.":t.status!=="error"?`${t.data.totalActive} active runs across the supervisor's bead store. ${L}`:`Run counts unavailable: ${t.error}. ${L}`}export{xe as RunsPage}; diff --git a/internal/api/dashboardspa/dist/assets/SseIndicator-BpC5bgiy.js b/internal/api/dashboardspa/dist/assets/SseIndicator-DDbpxu-X.js similarity index 88% rename from internal/api/dashboardspa/dist/assets/SseIndicator-BpC5bgiy.js rename to internal/api/dashboardspa/dist/assets/SseIndicator-DDbpxu-X.js index c651c63dca..d051fbcced 100644 --- a/internal/api/dashboardspa/dist/assets/SseIndicator-BpC5bgiy.js +++ b/internal/api/dashboardspa/dist/assets/SseIndicator-DDbpxu-X.js @@ -1 +1 @@ -import{j as a,S as t}from"./index-zPatq59W.js";function i({label:n,title:e,show:r=!0,glyph:o}){return r?a.jsxs("span",{className:"normal-case text-body text-warn",role:"status",title:e,children:[o!==void 0&&a.jsxs("span",{"aria-hidden":"true",children:[o," "]}),n]}):null}function c({state:n}){const e=n==="open"?"ok":n==="connecting"||n==="degraded"?"warn":"stuck",r=n==="open"?"live":n==="connecting"?"connecting":n==="degraded"?"degraded":"offline";return a.jsx(t,{tone:e,label:r,title:`SSE stream: ${n}`,className:"w-28"})}export{i as P,c as S}; +import{j as a,S as t}from"./index-QWRimsO3.js";function i({label:n,title:e,show:r=!0,glyph:o}){return r?a.jsxs("span",{className:"normal-case text-body text-warn",role:"status",title:e,children:[o!==void 0&&a.jsxs("span",{"aria-hidden":"true",children:[o," "]}),n]}):null}function c({state:n}){const e=n==="open"?"ok":n==="connecting"||n==="degraded"?"warn":"stuck",r=n==="open"?"live":n==="connecting"?"connecting":n==="degraded"?"degraded":"offline";return a.jsx(t,{tone:e,label:r,title:`SSE stream: ${n}`,className:"w-28"})}export{i as P,c as S}; diff --git a/internal/api/dashboardspa/dist/assets/StageLadder-CwHTgfDd.js b/internal/api/dashboardspa/dist/assets/StageLadder-DgEuJnhe.js similarity index 91% rename from internal/api/dashboardspa/dist/assets/StageLadder-CwHTgfDd.js rename to internal/api/dashboardspa/dist/assets/StageLadder-DgEuJnhe.js index 66928aa1b9..7dc9001cbb 100644 --- a/internal/api/dashboardspa/dist/assets/StageLadder-CwHTgfDd.js +++ b/internal/api/dashboardspa/dist/assets/StageLadder-DgEuJnhe.js @@ -1 +1 @@ -import{j as t}from"./index-zPatq59W.js";const n={pending:"·",active:"⬣",complete:"◆",blocked:"✕"},c={pending:"text-fg-faint",active:"text-fg",complete:"text-fg-muted",blocked:"text-accent"},s={pending:"text-fg-muted",active:"text-fg",complete:"text-fg-muted",blocked:"text-accent"};function r({stages:a,label:l}){return a.length===0?null:t.jsx("ol",{className:"mt-2 flex items-baseline gap-x-2 flex-wrap","aria-label":`${l} stages`,children:a.map(e=>t.jsxs("li",{className:`text-label uppercase tracking-wider ${c[e.status]}`,title:`${e.label}: ${e.status}`,children:[t.jsx("span",{"aria-hidden":"true",children:n[e.status]})," ",t.jsx("span",{className:s[e.status],children:e.label})]},e.key))})}export{r as S}; +import{j as t}from"./index-QWRimsO3.js";const n={pending:"·",active:"⬣",complete:"◆",blocked:"✕"},c={pending:"text-fg-faint",active:"text-fg",complete:"text-fg-muted",blocked:"text-accent"},s={pending:"text-fg-muted",active:"text-fg",complete:"text-fg-muted",blocked:"text-accent"};function r({stages:a,label:l}){return a.length===0?null:t.jsx("ol",{className:"mt-2 flex items-baseline gap-x-2 flex-wrap","aria-label":`${l} stages`,children:a.map(e=>t.jsxs("li",{className:`text-label uppercase tracking-wider ${c[e.status]}`,title:`${e.label}: ${e.status}`,children:[t.jsx("span",{"aria-hidden":"true",children:n[e.status]})," ",t.jsx("span",{className:s[e.status],children:e.label})]},e.key))})}export{r as S}; diff --git a/internal/api/dashboardspa/dist/assets/Table-DRIQbbRJ.js b/internal/api/dashboardspa/dist/assets/Table-C94QdmsL.js similarity index 96% rename from internal/api/dashboardspa/dist/assets/Table-DRIQbbRJ.js rename to internal/api/dashboardspa/dist/assets/Table-C94QdmsL.js index 114614c5ed..7c6d345670 100644 --- a/internal/api/dashboardspa/dist/assets/Table-DRIQbbRJ.js +++ b/internal/api/dashboardspa/dist/assets/Table-C94QdmsL.js @@ -1 +1 @@ -import{r as x,j as t}from"./index-zPatq59W.js";function y({columns:i,rows:d,rowKey:f,onRowClick:c,rowProps:h,empty:b,initialSort:g}){const[n,p]=x.useState(g??null),m=x.useMemo(()=>{if(n===null)return d;const e=i.find(r=>r.key===n.key);if(!e||!e.sortable)return d;const s=e.sortValue??(r=>String(e.render(r)??"")),a=n.dir==="asc"?1:-1;return[...d].sort((r,u)=>{const l=s(r),o=s(u);return l===o?0:l==null?-a:o==null?a:lo?a:0})},[d,i,n]),N=e=>{p(s=>s?.key!==e?{key:e,dir:"asc"}:{key:e,dir:s.dir==="asc"?"desc":"asc"})};return t.jsx("div",{className:"overflow-x-auto",children:t.jsxs("table",{className:"w-full text-body tnum",children:[t.jsx("thead",{children:t.jsx("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:i.map(e=>{const s=n?.key===e.key,a=e.align==="right"?"text-right":"text-left";return t.jsx("th",{scope:"col",className:`pb-3 pr-6 font-medium select-none ${a} ${e.className??""}`,children:e.sortable?t.jsxs("button",{type:"button",onClick:()=>N(e.key),className:"inline-flex items-center gap-1 hover:text-fg transition-colors duration-150 ease-out-quart focus-mark rounded-sm",children:[e.label,s&&t.jsx("span",{"aria-hidden":!0,className:"text-accent",children:n?.dir==="asc"?"↑":"↓"})]}):e.label},e.key)})})}),t.jsx("tbody",{children:m.length===0?t.jsx("tr",{children:t.jsx("td",{colSpan:i.length,className:"py-10 text-center text-fg-muted italic",children:b??"No data"})}):m.map(e=>{const{className:s="",...a}=h?.(e)??{};return x.createElement("tr",{...a,key:f(e),onClick:c?()=>c(e):void 0,className:`border-b border-rule transition-colors duration-150 ease-out-quart ${c?"cursor-pointer hover:bg-surface-tint":""} ${s}`},i.map(r=>{const u=r.align==="right"?"text-right":"text-left";return t.jsx("td",{className:`py-3 pr-6 align-baseline ${u} ${r.className??""}`,children:r.render(e)},r.key)}))})})]})})}export{y as T}; +import{r as x,j as t}from"./index-QWRimsO3.js";function y({columns:i,rows:d,rowKey:f,onRowClick:c,rowProps:h,empty:b,initialSort:g}){const[n,p]=x.useState(g??null),m=x.useMemo(()=>{if(n===null)return d;const e=i.find(r=>r.key===n.key);if(!e||!e.sortable)return d;const s=e.sortValue??(r=>String(e.render(r)??"")),a=n.dir==="asc"?1:-1;return[...d].sort((r,u)=>{const l=s(r),o=s(u);return l===o?0:l==null?-a:o==null?a:lo?a:0})},[d,i,n]),N=e=>{p(s=>s?.key!==e?{key:e,dir:"asc"}:{key:e,dir:s.dir==="asc"?"desc":"asc"})};return t.jsx("div",{className:"overflow-x-auto",children:t.jsxs("table",{className:"w-full text-body tnum",children:[t.jsx("thead",{children:t.jsx("tr",{className:"border-b border-rule text-label uppercase tracking-wider text-fg-muted",children:i.map(e=>{const s=n?.key===e.key,a=e.align==="right"?"text-right":"text-left";return t.jsx("th",{scope:"col",className:`pb-3 pr-6 font-medium select-none ${a} ${e.className??""}`,children:e.sortable?t.jsxs("button",{type:"button",onClick:()=>N(e.key),className:"inline-flex items-center gap-1 hover:text-fg transition-colors duration-150 ease-out-quart focus-mark rounded-sm",children:[e.label,s&&t.jsx("span",{"aria-hidden":!0,className:"text-accent",children:n?.dir==="asc"?"↑":"↓"})]}):e.label},e.key)})})}),t.jsx("tbody",{children:m.length===0?t.jsx("tr",{children:t.jsx("td",{colSpan:i.length,className:"py-10 text-center text-fg-muted italic",children:b??"No data"})}):m.map(e=>{const{className:s="",...a}=h?.(e)??{};return x.createElement("tr",{...a,key:f(e),onClick:c?()=>c(e):void 0,className:`border-b border-rule transition-colors duration-150 ease-out-quart ${c?"cursor-pointer hover:bg-surface-tint":""} ${s}`},i.map(r=>{const u=r.align==="right"?"text-right":"text-left";return t.jsx("td",{className:`py-3 pr-6 align-baseline ${u} ${r.className??""}`,children:r.render(e)},r.key)}))})})]})})}export{y as T}; diff --git a/internal/api/dashboardspa/dist/assets/agentReads-NY5zZttz.js b/internal/api/dashboardspa/dist/assets/agentReads-BA8TH08X.js similarity index 80% rename from internal/api/dashboardspa/dist/assets/agentReads-NY5zZttz.js rename to internal/api/dashboardspa/dist/assets/agentReads-BA8TH08X.js index df66a94a08..c17fb90bac 100644 --- a/internal/api/dashboardspa/dist/assets/agentReads-NY5zZttz.js +++ b/internal/api/dashboardspa/dist/assets/agentReads-BA8TH08X.js @@ -1 +1 @@ -import{J as e,K as i}from"./index-zPatq59W.js";async function n(){const r=await e().listAgents(i("list supervisor agents"));return{...r,items:r.items??[]}}async function a(r){const t=r.trim();if(t.length===0)throw new Error("agent alias is required");return e().agentPrime(i("fetch supervisor agent prime"),t)}export{a as f,n as l}; +import{I as e,J as i}from"./index-QWRimsO3.js";async function n(){const r=await e().listAgents(i("list supervisor agents"));return{...r,items:r.items??[]}}async function a(r){const t=r.trim();if(t.length===0)throw new Error("agent alias is required");return e().agentPrime(i("fetch supervisor agent prime"),t)}export{a as f,n as l}; diff --git a/internal/api/dashboardspa/dist/assets/constants-vAmcTKRZ.js b/internal/api/dashboardspa/dist/assets/constants-CVFL5iaz.js similarity index 95% rename from internal/api/dashboardspa/dist/assets/constants-vAmcTKRZ.js rename to internal/api/dashboardspa/dist/assets/constants-CVFL5iaz.js index 4a71f7d0b3..c282a12dce 100644 --- a/internal/api/dashboardspa/dist/assets/constants-vAmcTKRZ.js +++ b/internal/api/dashboardspa/dist/assets/constants-CVFL5iaz.js @@ -1 +1 @@ -import{r as o,j as e}from"./index-zPatq59W.js";function m({open:s,onClose:t,title:i,caption:a,children:l,footer:n,widthClass:d="max-w-3xl"}){return o.useEffect(()=>{if(!s)return;const r=c=>{c.key==="Escape"&&t()};return document.addEventListener("keydown",r),()=>document.removeEventListener("keydown",r)},[s,t]),s?e.jsx("div",{role:"dialog","aria-modal":"true",className:"fixed inset-0 z-50 flex items-start sm:items-center justify-center bg-fg/30 p-3 sm:p-6",onClick:t,children:e.jsxs("div",{className:`w-full ${d} bg-surface border border-rule rounded-md flex flex-col max-h-[90vh]`,onClick:r=>r.stopPropagation(),children:[e.jsxs("div",{className:"flex items-start justify-between gap-3 px-5 py-4 border-b border-rule",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("h2",{className:"text-title font-semibold text-fg truncate",children:i}),a&&e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-muted mt-1 truncate",children:a})]}),e.jsx("button",{type:"button",onClick:t,"aria-label":"Close",className:"text-fg-muted hover:text-fg transition-colors duration-150 ease-out-quart focus-mark text-lg leading-none px-1",children:"×"})]}),e.jsx("div",{className:"flex-1 overflow-auto p-5 text-body text-fg",children:l}),n&&e.jsx("div",{className:"border-t border-rule px-5 py-3 flex items-center justify-end gap-3",children:n})]})}):null}const u="Content is agent-generated and may contain misleading instructions.";export{m as M,u as P}; +import{r as o,j as e}from"./index-QWRimsO3.js";function m({open:s,onClose:t,title:i,caption:a,children:l,footer:n,widthClass:d="max-w-3xl"}){return o.useEffect(()=>{if(!s)return;const r=c=>{c.key==="Escape"&&t()};return document.addEventListener("keydown",r),()=>document.removeEventListener("keydown",r)},[s,t]),s?e.jsx("div",{role:"dialog","aria-modal":"true",className:"fixed inset-0 z-50 flex items-start sm:items-center justify-center bg-fg/30 p-3 sm:p-6",onClick:t,children:e.jsxs("div",{className:`w-full ${d} bg-surface border border-rule rounded-md flex flex-col max-h-[90vh]`,onClick:r=>r.stopPropagation(),children:[e.jsxs("div",{className:"flex items-start justify-between gap-3 px-5 py-4 border-b border-rule",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("h2",{className:"text-title font-semibold text-fg truncate",children:i}),a&&e.jsx("p",{className:"text-label uppercase tracking-wider text-fg-muted mt-1 truncate",children:a})]}),e.jsx("button",{type:"button",onClick:t,"aria-label":"Close",className:"text-fg-muted hover:text-fg transition-colors duration-150 ease-out-quart focus-mark text-lg leading-none px-1",children:"×"})]}),e.jsx("div",{className:"flex-1 overflow-auto p-5 text-body text-fg",children:l}),n&&e.jsx("div",{className:"border-t border-rule px-5 py-3 flex items-center justify-end gap-3",children:n})]})}):null}const u="Content is agent-generated and may contain misleading instructions.";export{m as M,u as P}; diff --git a/internal/api/dashboardspa/dist/assets/index-QWRimsO3.js b/internal/api/dashboardspa/dist/assets/index-QWRimsO3.js new file mode 100644 index 0000000000..00450de9e1 --- /dev/null +++ b/internal/api/dashboardspa/dist/assets/index-QWRimsO3.js @@ -0,0 +1,73 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Activity-CiU9eU59.js","assets/routeHighlight-B30gQO2o.js","assets/PageHeader-CxbYmkHZ.js","assets/time-D9v0saHV.js","assets/useVisibleRefresh-Bm_cCiAg.js","assets/Health-RykINz8c.js","assets/format-fte2CeYD.js","assets/Agents-CEUrRSz0.js","assets/context-window-Cu9zl36t.js","assets/projectOf-CAOn7SI-.js","assets/constants-CVFL5iaz.js","assets/SseIndicator-DDbpxu-X.js","assets/LiveSessionPeek-m6YywWBh.js","assets/Table-C94QdmsL.js","assets/agentReads-BA8TH08X.js","assets/AgentDetail-UxV6LhC9.js","assets/BeadDetailModal-BtQMJz2-.js","assets/Field-pp_wh5a7.js","assets/AmbientHome-DYE5iAQP.js","assets/Beads-m4fbNWDo.js","assets/useListFilters-D2HcBe10.js","assets/Mail-yLXNL53p.js","assets/FormulaRunDetail-DtW7ktOr.js","assets/StageLadder-DgEuJnhe.js","assets/Runs-C9G772Th.js"])))=>i.map(i=>d[i]); +function Pg(t,r){for(var i=0;is[u]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const u of document.querySelectorAll('link[rel="modulepreload"]'))s(u);new MutationObserver(u=>{for(const p of u)if(p.type==="childList")for(const d of p.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&s(d)}).observe(document,{childList:!0,subtree:!0});function i(u){const p={};return u.integrity&&(p.integrity=u.integrity),u.referrerPolicy&&(p.referrerPolicy=u.referrerPolicy),u.crossOrigin==="use-credentials"?p.credentials="include":u.crossOrigin==="anonymous"?p.credentials="omit":p.credentials="same-origin",p}function s(u){if(u.ep)return;u.ep=!0;const p=i(u);fetch(u.href,p)}})();function qf(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var Al={exports:{}},Vo={},Ol={exports:{}},he={};var Mp;function Ng(){if(Mp)return he;Mp=1;var t=Symbol.for("react.element"),r=Symbol.for("react.portal"),i=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),u=Symbol.for("react.profiler"),p=Symbol.for("react.provider"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),g=Symbol.for("react.suspense"),y=Symbol.for("react.memo"),E=Symbol.for("react.lazy"),S=Symbol.iterator;function T(z){return z===null||typeof z!="object"?null:(z=S&&z[S]||z["@@iterator"],typeof z=="function"?z:null)}var A={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},D=Object.assign,W={};function O(z,F,me){this.props=z,this.context=F,this.refs=W,this.updater=me||A}O.prototype.isReactComponent={},O.prototype.setState=function(z,F){if(typeof z!="object"&&typeof z!="function"&&z!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,z,F,"setState")},O.prototype.forceUpdate=function(z){this.updater.enqueueForceUpdate(this,z,"forceUpdate")};function H(){}H.prototype=O.prototype;function oe(z,F,me){this.props=z,this.context=F,this.refs=W,this.updater=me||A}var Q=oe.prototype=new H;Q.constructor=oe,D(Q,O.prototype),Q.isPureReactComponent=!0;var G=Array.isArray,ee=Object.prototype.hasOwnProperty,ue={current:null},de={key:!0,ref:!0,__self:!0,__source:!0};function pe(z,F,me){var ge,we={},xe=null,Ce=null;if(F!=null)for(ge in F.ref!==void 0&&(Ce=F.ref),F.key!==void 0&&(xe=""+F.key),F)ee.call(F,ge)&&!de.hasOwnProperty(ge)&&(we[ge]=F[ge]);var ke=arguments.length-2;if(ke===1)we.children=me;else if(1>>1,F=J[z];if(0>>1;zu(we,X))xeu(Ce,we)?(J[z]=Ce,J[xe]=X,z=xe):(J[z]=we,J[ge]=X,z=ge);else if(xeu(Ce,X))J[z]=Ce,J[xe]=X,z=xe;else break e}}return le}function u(J,le){var X=J.sortIndex-le.sortIndex;return X!==0?X:J.id-le.id}if(typeof performance=="object"&&typeof performance.now=="function"){var p=performance;t.unstable_now=function(){return p.now()}}else{var d=Date,m=d.now();t.unstable_now=function(){return d.now()-m}}var g=[],y=[],E=1,S=null,T=3,A=!1,D=!1,W=!1,O=typeof setTimeout=="function"?setTimeout:null,H=typeof clearTimeout=="function"?clearTimeout:null,oe=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Q(J){for(var le=i(y);le!==null;){if(le.callback===null)s(y);else if(le.startTime<=J)s(y),le.sortIndex=le.expirationTime,r(g,le);else break;le=i(y)}}function G(J){if(W=!1,Q(J),!D)if(i(g)!==null)D=!0,vt(ee);else{var le=i(y);le!==null&&qe(G,le.startTime-J)}}function ee(J,le){D=!1,W&&(W=!1,H(pe),pe=-1),A=!0;var X=T;try{for(Q(le),S=i(g);S!==null&&(!(S.expirationTime>le)||J&&!Ze());){var z=S.callback;if(typeof z=="function"){S.callback=null,T=S.priorityLevel;var F=z(S.expirationTime<=le);le=t.unstable_now(),typeof F=="function"?S.callback=F:S===i(g)&&s(g),Q(le)}else s(g);S=i(g)}if(S!==null)var me=!0;else{var ge=i(y);ge!==null&&qe(G,ge.startTime-le),me=!1}return me}finally{S=null,T=X,A=!1}}var ue=!1,de=null,pe=-1,Re=5,ye=-1;function Ze(){return!(t.unstable_now()-yeJ||125z?(J.sortIndex=X,r(y,J),i(g)===null&&J===i(y)&&(W?(H(pe),pe=-1):W=!0,qe(G,X-z))):(J.sortIndex=F,r(g,J),D||A||(D=!0,vt(ee))),J},t.unstable_shouldYield=Ze,t.unstable_wrapCallback=function(J){var le=T;return function(){var X=T;T=le;try{return J.apply(this,arguments)}finally{T=X}}}})(Ll)),Ll}var Vp;function Lg(){return Vp||(Vp=1,$l.exports=$g()),$l.exports}var Wp;function Dg(){if(Wp)return xt;Wp=1;var t=iu(),r=Lg();function i(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,a=1;a"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),g=Object.prototype.hasOwnProperty,y=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,E={},S={};function T(e){return g.call(S,e)?!0:g.call(E,e)?!1:y.test(e)?S[e]=!0:(E[e]=!0,!1)}function A(e,n,a,l){if(a!==null&&a.type===0)return!1;switch(typeof n){case"function":case"symbol":return!0;case"boolean":return l?!1:a!==null?!a.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function D(e,n,a,l){if(n===null||typeof n>"u"||A(e,n,a,l))return!0;if(l)return!1;if(a!==null)switch(a.type){case 3:return!n;case 4:return n===!1;case 5:return isNaN(n);case 6:return isNaN(n)||1>n}return!1}function W(e,n,a,l,c,f,v){this.acceptsBooleans=n===2||n===3||n===4,this.attributeName=l,this.attributeNamespace=c,this.mustUseProperty=a,this.propertyName=e,this.type=n,this.sanitizeURL=f,this.removeEmptyString=v}var O={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){O[e]=new W(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var n=e[0];O[n]=new W(n,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){O[e]=new W(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){O[e]=new W(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){O[e]=new W(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){O[e]=new W(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){O[e]=new W(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){O[e]=new W(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){O[e]=new W(e,5,!1,e.toLowerCase(),null,!1,!1)});var H=/[\-:]([a-z])/g;function oe(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var n=e.replace(H,oe);O[n]=new W(n,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var n=e.replace(H,oe);O[n]=new W(n,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var n=e.replace(H,oe);O[n]=new W(n,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){O[e]=new W(e,1,!1,e.toLowerCase(),null,!1,!1)}),O.xlinkHref=new W("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){O[e]=new W(e,1,!1,e.toLowerCase(),null,!0,!0)});function Q(e,n,a,l){var c=O.hasOwnProperty(n)?O[n]:null;(c!==null?c.type!==0:l||!(2_||c[v]!==f[_]){var I=` +`+c[v].replace(" at new "," at ");return e.displayName&&I.includes("")&&(I=I.replace("",e.displayName)),I}while(1<=v&&0<=_);break}}}finally{me=!1,Error.prepareStackTrace=a}return(e=e?e.displayName||e.name:"")?F(e):""}function we(e){switch(e.tag){case 5:return F(e.type);case 16:return F("Lazy");case 13:return F("Suspense");case 19:return F("SuspenseList");case 0:case 2:case 15:return e=ge(e.type,!1),e;case 11:return e=ge(e.type.render,!1),e;case 1:return e=ge(e.type,!0),e;default:return""}}function xe(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case de:return"Fragment";case ue:return"Portal";case Re:return"Profiler";case pe:return"StrictMode";case et:return"Suspense";case Qe:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ze:return(e.displayName||"Context")+".Consumer";case ye:return(e._context.displayName||"Context")+".Provider";case Ke:var n=e.render;return e=e.displayName,e||(e=n.displayName||n.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case kt:return n=e.displayName||null,n!==null?n:xe(e.type)||"Memo";case vt:n=e._payload,e=e._init;try{return xe(e(n))}catch{}}return null}function Ce(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=n.render,e=e.displayName||e.name||"",n.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return xe(n);case 8:return n===pe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof n=="function")return n.displayName||n.name||null;if(typeof n=="string")return n}return null}function ke(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ae(e){var n=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function bt(e){var n=Ae(e)?"checked":"value",a=Object.getOwnPropertyDescriptor(e.constructor.prototype,n),l=""+e[n];if(!e.hasOwnProperty(n)&&typeof a<"u"&&typeof a.get=="function"&&typeof a.set=="function"){var c=a.get,f=a.set;return Object.defineProperty(e,n,{configurable:!0,get:function(){return c.call(this)},set:function(v){l=""+v,f.call(this,v)}}),Object.defineProperty(e,n,{enumerable:a.enumerable}),{getValue:function(){return l},setValue:function(v){l=""+v},stopTracking:function(){e._valueTracker=null,delete e[n]}}}}function ri(e){e._valueTracker||(e._valueTracker=bt(e))}function Wu(e){if(!e)return!1;var n=e._valueTracker;if(!n)return!0;var a=n.getValue(),l="";return e&&(l=Ae(e)?e.checked?"true":"false":e.value),e=l,e!==a?(n.setValue(e),!0):!1}function oi(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Fa(e,n){var a=n.checked;return X({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:a??e._wrapperState.initialChecked})}function Hu(e,n){var a=n.defaultValue==null?"":n.defaultValue,l=n.checked!=null?n.checked:n.defaultChecked;a=ke(n.value!=null?n.value:a),e._wrapperState={initialChecked:l,initialValue:a,controlled:n.type==="checkbox"||n.type==="radio"?n.checked!=null:n.value!=null}}function Gu(e,n){n=n.checked,n!=null&&Q(e,"checked",n,!1)}function Ua(e,n){Gu(e,n);var a=ke(n.value),l=n.type;if(a!=null)l==="number"?(a===0&&e.value===""||e.value!=a)&&(e.value=""+a):e.value!==""+a&&(e.value=""+a);else if(l==="submit"||l==="reset"){e.removeAttribute("value");return}n.hasOwnProperty("value")?Za(e,n.type,a):n.hasOwnProperty("defaultValue")&&Za(e,n.type,ke(n.defaultValue)),n.checked==null&&n.defaultChecked!=null&&(e.defaultChecked=!!n.defaultChecked)}function Ju(e,n,a){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var l=n.type;if(!(l!=="submit"&&l!=="reset"||n.value!==void 0&&n.value!==null))return;n=""+e._wrapperState.initialValue,a||n===e.value||(e.value=n),e.defaultValue=n}a=e.name,a!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,a!==""&&(e.name=a)}function Za(e,n,a){(n!=="number"||oi(e.ownerDocument)!==e)&&(a==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+a&&(e.defaultValue=""+a))}var io=Array.isArray;function _r(e,n,a,l){if(e=e.options,n){n={};for(var c=0;c"+n.valueOf().toString()+"",n=ii.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;n.firstChild;)e.appendChild(n.firstChild)}});function ao(e,n){if(n){var a=e.firstChild;if(a&&a===e.lastChild&&a.nodeType===3){a.nodeValue=n;return}}e.textContent=n}var so={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},jv=["Webkit","ms","Moz","O"];Object.keys(so).forEach(function(e){jv.forEach(function(n){n=n+e.charAt(0).toUpperCase()+e.substring(1),so[n]=so[e]})});function tc(e,n,a){return n==null||typeof n=="boolean"||n===""?"":a||typeof n!="number"||n===0||so.hasOwnProperty(e)&&so[e]?(""+n).trim():n+"px"}function nc(e,n){e=e.style;for(var a in n)if(n.hasOwnProperty(a)){var l=a.indexOf("--")===0,c=tc(a,n[a],l);a==="float"&&(a="cssFloat"),l?e.setProperty(a,c):e[a]=c}}var $v=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Wa(e,n){if(n){if($v[e]&&(n.children!=null||n.dangerouslySetInnerHTML!=null))throw Error(i(137,e));if(n.dangerouslySetInnerHTML!=null){if(n.children!=null)throw Error(i(60));if(typeof n.dangerouslySetInnerHTML!="object"||!("__html"in n.dangerouslySetInnerHTML))throw Error(i(61))}if(n.style!=null&&typeof n.style!="object")throw Error(i(62))}}function Ha(e,n){if(e.indexOf("-")===-1)return typeof n.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ga=null;function Ja(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ka=null,wr=null,xr=null;function rc(e){if(e=Bo(e)){if(typeof Ka!="function")throw Error(i(280));var n=e.stateNode;n&&(n=Ci(n),Ka(e.stateNode,e.type,n))}}function oc(e){wr?xr?xr.push(e):xr=[e]:wr=e}function ic(){if(wr){var e=wr,n=xr;if(xr=wr=null,rc(e),n)for(e=0;e>>=0,e===0?32:31-(Gv(e)/Jv|0)|0}var ci=64,di=4194304;function po(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function pi(e,n){var a=e.pendingLanes;if(a===0)return 0;var l=0,c=e.suspendedLanes,f=e.pingedLanes,v=a&268435455;if(v!==0){var _=v&~c;_!==0?l=po(_):(f&=v,f!==0&&(l=po(f)))}else v=a&~c,v!==0?l=po(v):f!==0&&(l=po(f));if(l===0)return 0;if(n!==0&&n!==l&&(n&c)===0&&(c=l&-l,f=n&-n,c>=f||c===16&&(f&4194240)!==0))return n;if((l&4)!==0&&(l|=a&16),n=e.entangledLanes,n!==0)for(e=e.entanglements,n&=l;0a;a++)n.push(e);return n}function fo(e,n,a){e.pendingLanes|=n,n!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,n=31-Ut(n),e[n]=a}function Xv(e,n){var a=e.pendingLanes&~n;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=n,e.mutableReadLanes&=n,e.entangledLanes&=n,n=e.entanglements;var l=e.eventTimes;for(e=e.expirationTimes;0=xo),Nc=" ",Ac=!1;function Oc(e,n){switch(e){case"keyup":return zh.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function jc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Sr=!1;function Th(e,n){switch(e){case"compositionend":return jc(n);case"keypress":return n.which!==32?null:(Ac=!0,Nc);case"textInput":return e=n.data,e===Nc&&Ac?null:e;default:return null}}function Bh(e,n){if(Sr)return e==="compositionend"||!ms&&Oc(e,n)?(e=zc(),gi=ls=bn=null,Sr=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:a,offset:n-e};e=l}e:{for(;a;){if(a.nextSibling){a=a.nextSibling;break e}a=a.parentNode}a=void 0}a=Zc(a)}}function Vc(e,n){return e&&n?e===n?!0:e&&e.nodeType===3?!1:n&&n.nodeType===3?Vc(e,n.parentNode):"contains"in e?e.contains(n):e.compareDocumentPosition?!!(e.compareDocumentPosition(n)&16):!1:!1}function Wc(){for(var e=window,n=oi();n instanceof e.HTMLIFrameElement;){try{var a=typeof n.contentWindow.location.href=="string"}catch{a=!1}if(a)e=n.contentWindow;else break;n=oi(e.document)}return n}function gs(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return n&&(n==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||n==="textarea"||e.contentEditable==="true")}function Dh(e){var n=Wc(),a=e.focusedElem,l=e.selectionRange;if(n!==a&&a&&a.ownerDocument&&Vc(a.ownerDocument.documentElement,a)){if(l!==null&&gs(a)){if(n=l.start,e=l.end,e===void 0&&(e=n),"selectionStart"in a)a.selectionStart=n,a.selectionEnd=Math.min(e,a.value.length);else if(e=(n=a.ownerDocument||document)&&n.defaultView||window,e.getSelection){e=e.getSelection();var c=a.textContent.length,f=Math.min(l.start,c);l=l.end===void 0?f:Math.min(l.end,c),!e.extend&&f>l&&(c=l,l=f,f=c),c=qc(a,f);var v=qc(a,l);c&&v&&(e.rangeCount!==1||e.anchorNode!==c.node||e.anchorOffset!==c.offset||e.focusNode!==v.node||e.focusOffset!==v.offset)&&(n=n.createRange(),n.setStart(c.node,c.offset),e.removeAllRanges(),f>l?(e.addRange(n),e.extend(v.node,v.offset)):(n.setEnd(v.node,v.offset),e.addRange(n)))}}for(n=[],e=a;e=e.parentNode;)e.nodeType===1&&n.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof a.focus=="function"&&a.focus(),a=0;a=document.documentMode,kr=null,ys=null,ko=null,_s=!1;function Hc(e,n,a){var l=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;_s||kr==null||kr!==oi(l)||(l=kr,"selectionStart"in l&&gs(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),ko&&So(ko,l)||(ko=l,l=ki(ys,"onSelect"),0Br||(e.current=Rs[Br],Rs[Br]=null,Br--)}function Te(e,n){Br++,Rs[Br]=e.current,e.current=n}var Bn={},st=Tn(Bn),ht=Tn(!1),er=Bn;function Rr(e,n){var a=e.type.contextTypes;if(!a)return Bn;var l=e.stateNode;if(l&&l.__reactInternalMemoizedUnmaskedChildContext===n)return l.__reactInternalMemoizedMaskedChildContext;var c={},f;for(f in a)c[f]=n[f];return l&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=n,e.__reactInternalMemoizedMaskedChildContext=c),c}function gt(e){return e=e.childContextTypes,e!=null}function Ti(){Ne(ht),Ne(st)}function ld(e,n,a){if(st.current!==Bn)throw Error(i(168));Te(st,n),Te(ht,a)}function ud(e,n,a){var l=e.stateNode;if(n=n.childContextTypes,typeof l.getChildContext!="function")return a;l=l.getChildContext();for(var c in l)if(!(c in n))throw Error(i(108,Ce(e)||"Unknown",c));return X({},a,l)}function Bi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Bn,er=st.current,Te(st,e),Te(ht,ht.current),!0}function cd(e,n,a){var l=e.stateNode;if(!l)throw Error(i(169));a?(e=ud(e,n,er),l.__reactInternalMemoizedMergedChildContext=e,Ne(ht),Ne(st),Te(st,e)):Ne(ht),Te(ht,a)}var sn=null,Ri=!1,Ps=!1;function dd(e){sn===null?sn=[e]:sn.push(e)}function Qh(e){Ri=!0,dd(e)}function Rn(){if(!Ps&&sn!==null){Ps=!0;var e=0,n=be;try{var a=sn;for(be=1;e>=v,c-=v,ln=1<<32-Ut(n)+c|a<ce?(rt=se,se=null):rt=se.sibling;var Ee=M(C,se,B[ce],V);if(Ee===null){se===null&&(se=rt);break}e&&se&&Ee.alternate===null&&n(C,se),k=f(Ee,k,ce),ae===null?re=Ee:ae.sibling=Ee,ae=Ee,se=rt}if(ce===B.length)return a(C,se),Oe&&nr(C,ce),re;if(se===null){for(;cece?(rt=se,se=null):rt=se.sibling;var Mn=M(C,se,Ee.value,V);if(Mn===null){se===null&&(se=rt);break}e&&se&&Mn.alternate===null&&n(C,se),k=f(Mn,k,ce),ae===null?re=Mn:ae.sibling=Mn,ae=Mn,se=rt}if(Ee.done)return a(C,se),Oe&&nr(C,ce),re;if(se===null){for(;!Ee.done;ce++,Ee=B.next())Ee=q(C,Ee.value,V),Ee!==null&&(k=f(Ee,k,ce),ae===null?re=Ee:ae.sibling=Ee,ae=Ee);return Oe&&nr(C,ce),re}for(se=l(C,se);!Ee.done;ce++,Ee=B.next())Ee=K(se,C,ce,Ee.value,V),Ee!==null&&(e&&Ee.alternate!==null&&se.delete(Ee.key===null?ce:Ee.key),k=f(Ee,k,ce),ae===null?re=Ee:ae.sibling=Ee,ae=Ee);return e&&se.forEach(function(Rg){return n(C,Rg)}),Oe&&nr(C,ce),re}function He(C,k,B,V){if(typeof B=="object"&&B!==null&&B.type===de&&B.key===null&&(B=B.props.children),typeof B=="object"&&B!==null){switch(B.$$typeof){case ee:e:{for(var re=B.key,ae=k;ae!==null;){if(ae.key===re){if(re=B.type,re===de){if(ae.tag===7){a(C,ae.sibling),k=c(ae,B.props.children),k.return=C,C=k;break e}}else if(ae.elementType===re||typeof re=="object"&&re!==null&&re.$$typeof===vt&&gd(re)===ae.type){a(C,ae.sibling),k=c(ae,B.props),k.ref=Ro(C,ae,B),k.return=C,C=k;break e}a(C,ae);break}else n(C,ae);ae=ae.sibling}B.type===de?(k=cr(B.props.children,C.mode,V,B.key),k.return=C,C=k):(V=ia(B.type,B.key,B.props,null,C.mode,V),V.ref=Ro(C,k,B),V.return=C,C=V)}return v(C);case ue:e:{for(ae=B.key;k!==null;){if(k.key===ae)if(k.tag===4&&k.stateNode.containerInfo===B.containerInfo&&k.stateNode.implementation===B.implementation){a(C,k.sibling),k=c(k,B.children||[]),k.return=C,C=k;break e}else{a(C,k);break}else n(C,k);k=k.sibling}k=Tl(B,C.mode,V),k.return=C,C=k}return v(C);case vt:return ae=B._init,He(C,k,ae(B._payload),V)}if(io(B))return te(C,k,B,V);if(le(B))return ne(C,k,B,V);Oi(C,B)}return typeof B=="string"&&B!==""||typeof B=="number"?(B=""+B,k!==null&&k.tag===6?(a(C,k.sibling),k=c(k,B),k.return=C,C=k):(a(C,k),k=Cl(B,C.mode,V),k.return=C,C=k),v(C)):a(C,k)}return He}var Or=yd(!0),_d=yd(!1),ji=Tn(null),$i=null,jr=null,Ls=null;function Ds(){Ls=jr=$i=null}function Ms(e){var n=ji.current;Ne(ji),e._currentValue=n}function Fs(e,n,a){for(;e!==null;){var l=e.alternate;if((e.childLanes&n)!==n?(e.childLanes|=n,l!==null&&(l.childLanes|=n)):l!==null&&(l.childLanes&n)!==n&&(l.childLanes|=n),e===a)break;e=e.return}}function $r(e,n){$i=e,Ls=jr=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&n)!==0&&(yt=!0),e.firstContext=null)}function jt(e){var n=e._currentValue;if(Ls!==e)if(e={context:e,memoizedValue:n,next:null},jr===null){if($i===null)throw Error(i(308));jr=e,$i.dependencies={lanes:0,firstContext:e}}else jr=jr.next=e;return n}var rr=null;function Us(e){rr===null?rr=[e]:rr.push(e)}function wd(e,n,a,l){var c=n.interleaved;return c===null?(a.next=a,Us(n)):(a.next=c.next,c.next=a),n.interleaved=a,cn(e,l)}function cn(e,n){e.lanes|=n;var a=e.alternate;for(a!==null&&(a.lanes|=n),a=e,e=e.return;e!==null;)e.childLanes|=n,a=e.alternate,a!==null&&(a.childLanes|=n),a=e,e=e.return;return a.tag===3?a.stateNode:null}var Pn=!1;function Zs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function xd(e,n){e=e.updateQueue,n.updateQueue===e&&(n.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function dn(e,n){return{eventTime:e,lane:n,tag:0,payload:null,callback:null,next:null}}function Nn(e,n,a){var l=e.updateQueue;if(l===null)return null;if(l=l.shared,(_e&2)!==0){var c=l.pending;return c===null?n.next=n:(n.next=c.next,c.next=n),l.pending=n,cn(e,a)}return c=l.interleaved,c===null?(n.next=n,Us(l)):(n.next=c.next,c.next=n),l.interleaved=n,cn(e,a)}function Li(e,n,a){if(n=n.updateQueue,n!==null&&(n=n.shared,(a&4194240)!==0)){var l=n.lanes;l&=e.pendingLanes,a|=l,n.lanes=a,rs(e,a)}}function Ed(e,n){var a=e.updateQueue,l=e.alternate;if(l!==null&&(l=l.updateQueue,a===l)){var c=null,f=null;if(a=a.firstBaseUpdate,a!==null){do{var v={eventTime:a.eventTime,lane:a.lane,tag:a.tag,payload:a.payload,callback:a.callback,next:null};f===null?c=f=v:f=f.next=v,a=a.next}while(a!==null);f===null?c=f=n:f=f.next=n}else c=f=n;a={baseState:l.baseState,firstBaseUpdate:c,lastBaseUpdate:f,shared:l.shared,effects:l.effects},e.updateQueue=a;return}e=a.lastBaseUpdate,e===null?a.firstBaseUpdate=n:e.next=n,a.lastBaseUpdate=n}function Di(e,n,a,l){var c=e.updateQueue;Pn=!1;var f=c.firstBaseUpdate,v=c.lastBaseUpdate,_=c.shared.pending;if(_!==null){c.shared.pending=null;var I=_,R=I.next;I.next=null,v===null?f=R:v.next=R,v=I;var U=e.alternate;U!==null&&(U=U.updateQueue,_=U.lastBaseUpdate,_!==v&&(_===null?U.firstBaseUpdate=R:_.next=R,U.lastBaseUpdate=I))}if(f!==null){var q=c.baseState;v=0,U=R=I=null,_=f;do{var M=_.lane,K=_.eventTime;if((l&M)===M){U!==null&&(U=U.next={eventTime:K,lane:0,tag:_.tag,payload:_.payload,callback:_.callback,next:null});e:{var te=e,ne=_;switch(M=n,K=a,ne.tag){case 1:if(te=ne.payload,typeof te=="function"){q=te.call(K,q,M);break e}q=te;break e;case 3:te.flags=te.flags&-65537|128;case 0:if(te=ne.payload,M=typeof te=="function"?te.call(K,q,M):te,M==null)break e;q=X({},q,M);break e;case 2:Pn=!0}}_.callback!==null&&_.lane!==0&&(e.flags|=64,M=c.effects,M===null?c.effects=[_]:M.push(_))}else K={eventTime:K,lane:M,tag:_.tag,payload:_.payload,callback:_.callback,next:null},U===null?(R=U=K,I=q):U=U.next=K,v|=M;if(_=_.next,_===null){if(_=c.shared.pending,_===null)break;M=_,_=M.next,M.next=null,c.lastBaseUpdate=M,c.shared.pending=null}}while(!0);if(U===null&&(I=q),c.baseState=I,c.firstBaseUpdate=R,c.lastBaseUpdate=U,n=c.shared.interleaved,n!==null){c=n;do v|=c.lane,c=c.next;while(c!==n)}else f===null&&(c.shared.lanes=0);ar|=v,e.lanes=v,e.memoizedState=q}}function Id(e,n,a){if(e=n.effects,n.effects=null,e!==null)for(n=0;na?a:4,e(!0);var l=Gs.transition;Gs.transition={};try{e(!1),n()}finally{be=a,Gs.transition=l}}function Ud(){return $t().memoizedState}function tg(e,n,a){var l=$n(e);if(a={lane:l,action:a,hasEagerState:!1,eagerState:null,next:null},Zd(e))qd(n,a);else if(a=wd(e,n,a,l),a!==null){var c=pt();Gt(a,e,l,c),Vd(a,n,l)}}function ng(e,n,a){var l=$n(e),c={lane:l,action:a,hasEagerState:!1,eagerState:null,next:null};if(Zd(e))qd(n,c);else{var f=e.alternate;if(e.lanes===0&&(f===null||f.lanes===0)&&(f=n.lastRenderedReducer,f!==null))try{var v=n.lastRenderedState,_=f(v,a);if(c.hasEagerState=!0,c.eagerState=_,Zt(_,v)){var I=n.interleaved;I===null?(c.next=c,Us(n)):(c.next=I.next,I.next=c),n.interleaved=c;return}}catch{}a=wd(e,n,c,l),a!==null&&(c=pt(),Gt(a,e,l,c),Vd(a,n,l))}}function Zd(e){var n=e.alternate;return e===Me||n!==null&&n===Me}function qd(e,n){Oo=Ui=!0;var a=e.pending;a===null?n.next=n:(n.next=a.next,a.next=n),e.pending=n}function Vd(e,n,a){if((a&4194240)!==0){var l=n.lanes;l&=e.pendingLanes,a|=l,n.lanes=a,rs(e,a)}}var Vi={readContext:jt,useCallback:lt,useContext:lt,useEffect:lt,useImperativeHandle:lt,useInsertionEffect:lt,useLayoutEffect:lt,useMemo:lt,useReducer:lt,useRef:lt,useState:lt,useDebugValue:lt,useDeferredValue:lt,useTransition:lt,useMutableSource:lt,useSyncExternalStore:lt,useId:lt,unstable_isNewReconciler:!1},rg={readContext:jt,useCallback:function(e,n){return nn().memoizedState=[e,n===void 0?null:n],e},useContext:jt,useEffect:Ad,useImperativeHandle:function(e,n,a){return a=a!=null?a.concat([e]):null,Zi(4194308,4,$d.bind(null,n,e),a)},useLayoutEffect:function(e,n){return Zi(4194308,4,e,n)},useInsertionEffect:function(e,n){return Zi(4,2,e,n)},useMemo:function(e,n){var a=nn();return n=n===void 0?null:n,e=e(),a.memoizedState=[e,n],e},useReducer:function(e,n,a){var l=nn();return n=a!==void 0?a(n):n,l.memoizedState=l.baseState=n,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},l.queue=e,e=e.dispatch=tg.bind(null,Me,e),[l.memoizedState,e]},useRef:function(e){var n=nn();return e={current:e},n.memoizedState=e},useState:Pd,useDebugValue:tl,useDeferredValue:function(e){return nn().memoizedState=e},useTransition:function(){var e=Pd(!1),n=e[0];return e=eg.bind(null,e[1]),nn().memoizedState=e,[n,e]},useMutableSource:function(){},useSyncExternalStore:function(e,n,a){var l=Me,c=nn();if(Oe){if(a===void 0)throw Error(i(407));a=a()}else{if(a=n(),nt===null)throw Error(i(349));(ir&30)!==0||zd(l,n,a)}c.memoizedState=a;var f={value:a,getSnapshot:n};return c.queue=f,Ad(Td.bind(null,l,f,e),[e]),l.flags|=2048,Lo(9,Cd.bind(null,l,f,a,n),void 0,null),a},useId:function(){var e=nn(),n=nt.identifierPrefix;if(Oe){var a=un,l=ln;a=(l&~(1<<32-Ut(l)-1)).toString(32)+a,n=":"+n+"R"+a,a=jo++,0<\/script>",e=e.removeChild(e.firstChild)):typeof l.is=="string"?e=v.createElement(a,{is:l.is}):(e=v.createElement(a),a==="select"&&(v=e,l.multiple?v.multiple=!0:l.size&&(v.size=l.size))):e=v.createElementNS(e,a),e[en]=n,e[To]=l,dp(e,n,!1,!1),n.stateNode=e;e:{switch(v=Ha(a,l),a){case"dialog":Pe("cancel",e),Pe("close",e),c=l;break;case"iframe":case"object":case"embed":Pe("load",e),c=l;break;case"video":case"audio":for(c=0;cUr&&(n.flags|=128,l=!0,Do(f,!1),n.lanes=4194304)}else{if(!l)if(e=Mi(v),e!==null){if(n.flags|=128,l=!0,a=e.updateQueue,a!==null&&(n.updateQueue=a,n.flags|=4),Do(f,!0),f.tail===null&&f.tailMode==="hidden"&&!v.alternate&&!Oe)return ut(n),null}else 2*We()-f.renderingStartTime>Ur&&a!==1073741824&&(n.flags|=128,l=!0,Do(f,!1),n.lanes=4194304);f.isBackwards?(v.sibling=n.child,n.child=v):(a=f.last,a!==null?a.sibling=v:n.child=v,f.last=v)}return f.tail!==null?(n=f.tail,f.rendering=n,f.tail=n.sibling,f.renderingStartTime=We(),n.sibling=null,a=De.current,Te(De,l?a&1|2:a&1),n):(ut(n),null);case 22:case 23:return kl(),l=n.memoizedState!==null,e!==null&&e.memoizedState!==null!==l&&(n.flags|=8192),l&&(n.mode&1)!==0?(Bt&1073741824)!==0&&(ut(n),n.subtreeFlags&6&&(n.flags|=8192)):ut(n),null;case 24:return null;case 25:return null}throw Error(i(156,n.tag))}function dg(e,n){switch(As(n),n.tag){case 1:return gt(n.type)&&Ti(),e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 3:return Lr(),Ne(ht),Ne(st),Hs(),e=n.flags,(e&65536)!==0&&(e&128)===0?(n.flags=e&-65537|128,n):null;case 5:return Vs(n),null;case 13:if(Ne(De),e=n.memoizedState,e!==null&&e.dehydrated!==null){if(n.alternate===null)throw Error(i(340));Ar()}return e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 19:return Ne(De),null;case 4:return Lr(),null;case 10:return Ms(n.type._context),null;case 22:case 23:return kl(),null;case 24:return null;default:return null}}var Ji=!1,ct=!1,pg=typeof WeakSet=="function"?WeakSet:Set,Y=null;function Mr(e,n){var a=e.ref;if(a!==null)if(typeof a=="function")try{a(null)}catch(l){Ve(e,n,l)}else a.current=null}function fl(e,n,a){try{a()}catch(l){Ve(e,n,l)}}var mp=!1;function fg(e,n){if(ks=vi,e=Wc(),gs(e)){if("selectionStart"in e)var a={start:e.selectionStart,end:e.selectionEnd};else e:{a=(a=e.ownerDocument)&&a.defaultView||window;var l=a.getSelection&&a.getSelection();if(l&&l.rangeCount!==0){a=l.anchorNode;var c=l.anchorOffset,f=l.focusNode;l=l.focusOffset;try{a.nodeType,f.nodeType}catch{a=null;break e}var v=0,_=-1,I=-1,R=0,U=0,q=e,M=null;t:for(;;){for(var K;q!==a||c!==0&&q.nodeType!==3||(_=v+c),q!==f||l!==0&&q.nodeType!==3||(I=v+l),q.nodeType===3&&(v+=q.nodeValue.length),(K=q.firstChild)!==null;)M=q,q=K;for(;;){if(q===e)break t;if(M===a&&++R===c&&(_=v),M===f&&++U===l&&(I=v),(K=q.nextSibling)!==null)break;q=M,M=q.parentNode}q=K}a=_===-1||I===-1?null:{start:_,end:I}}else a=null}a=a||{start:0,end:0}}else a=null;for(bs={focusedElem:e,selectionRange:a},vi=!1,Y=n;Y!==null;)if(n=Y,e=n.child,(n.subtreeFlags&1028)!==0&&e!==null)e.return=n,Y=e;else for(;Y!==null;){n=Y;try{var te=n.alternate;if((n.flags&1024)!==0)switch(n.tag){case 0:case 11:case 15:break;case 1:if(te!==null){var ne=te.memoizedProps,He=te.memoizedState,C=n.stateNode,k=C.getSnapshotBeforeUpdate(n.elementType===n.type?ne:Vt(n.type,ne),He);C.__reactInternalSnapshotBeforeUpdate=k}break;case 3:var B=n.stateNode.containerInfo;B.nodeType===1?B.textContent="":B.nodeType===9&&B.documentElement&&B.removeChild(B.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(i(163))}}catch(V){Ve(n,n.return,V)}if(e=n.sibling,e!==null){e.return=n.return,Y=e;break}Y=n.return}return te=mp,mp=!1,te}function Mo(e,n,a){var l=n.updateQueue;if(l=l!==null?l.lastEffect:null,l!==null){var c=l=l.next;do{if((c.tag&e)===e){var f=c.destroy;c.destroy=void 0,f!==void 0&&fl(n,a,f)}c=c.next}while(c!==l)}}function Ki(e,n){if(n=n.updateQueue,n=n!==null?n.lastEffect:null,n!==null){var a=n=n.next;do{if((a.tag&e)===e){var l=a.create;a.destroy=l()}a=a.next}while(a!==n)}}function ml(e){var n=e.ref;if(n!==null){var a=e.stateNode;e.tag,e=a,typeof n=="function"?n(e):n.current=e}}function vp(e){var n=e.alternate;n!==null&&(e.alternate=null,vp(n)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(n=e.stateNode,n!==null&&(delete n[en],delete n[To],delete n[Bs],delete n[Jh],delete n[Kh])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function hp(e){return e.tag===5||e.tag===3||e.tag===4}function gp(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||hp(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function vl(e,n,a){var l=e.tag;if(l===5||l===6)e=e.stateNode,n?a.nodeType===8?a.parentNode.insertBefore(e,n):a.insertBefore(e,n):(a.nodeType===8?(n=a.parentNode,n.insertBefore(e,a)):(n=a,n.appendChild(e)),a=a._reactRootContainer,a!=null||n.onclick!==null||(n.onclick=zi));else if(l!==4&&(e=e.child,e!==null))for(vl(e,n,a),e=e.sibling;e!==null;)vl(e,n,a),e=e.sibling}function hl(e,n,a){var l=e.tag;if(l===5||l===6)e=e.stateNode,n?a.insertBefore(e,n):a.appendChild(e);else if(l!==4&&(e=e.child,e!==null))for(hl(e,n,a),e=e.sibling;e!==null;)hl(e,n,a),e=e.sibling}var it=null,Wt=!1;function An(e,n,a){for(a=a.child;a!==null;)yp(e,n,a),a=a.sibling}function yp(e,n,a){if(Xt&&typeof Xt.onCommitFiberUnmount=="function")try{Xt.onCommitFiberUnmount(ui,a)}catch{}switch(a.tag){case 5:ct||Mr(a,n);case 6:var l=it,c=Wt;it=null,An(e,n,a),it=l,Wt=c,it!==null&&(Wt?(e=it,a=a.stateNode,e.nodeType===8?e.parentNode.removeChild(a):e.removeChild(a)):it.removeChild(a.stateNode));break;case 18:it!==null&&(Wt?(e=it,a=a.stateNode,e.nodeType===8?Ts(e.parentNode,a):e.nodeType===1&&Ts(e,a),yo(e)):Ts(it,a.stateNode));break;case 4:l=it,c=Wt,it=a.stateNode.containerInfo,Wt=!0,An(e,n,a),it=l,Wt=c;break;case 0:case 11:case 14:case 15:if(!ct&&(l=a.updateQueue,l!==null&&(l=l.lastEffect,l!==null))){c=l=l.next;do{var f=c,v=f.destroy;f=f.tag,v!==void 0&&((f&2)!==0||(f&4)!==0)&&fl(a,n,v),c=c.next}while(c!==l)}An(e,n,a);break;case 1:if(!ct&&(Mr(a,n),l=a.stateNode,typeof l.componentWillUnmount=="function"))try{l.props=a.memoizedProps,l.state=a.memoizedState,l.componentWillUnmount()}catch(_){Ve(a,n,_)}An(e,n,a);break;case 21:An(e,n,a);break;case 22:a.mode&1?(ct=(l=ct)||a.memoizedState!==null,An(e,n,a),ct=l):An(e,n,a);break;default:An(e,n,a)}}function _p(e){var n=e.updateQueue;if(n!==null){e.updateQueue=null;var a=e.stateNode;a===null&&(a=e.stateNode=new pg),n.forEach(function(l){var c=Eg.bind(null,e,l);a.has(l)||(a.add(l),l.then(c,c))})}}function Ht(e,n){var a=n.deletions;if(a!==null)for(var l=0;lc&&(c=v),l&=~f}if(l=c,l=We()-l,l=(120>l?120:480>l?480:1080>l?1080:1920>l?1920:3e3>l?3e3:4320>l?4320:1960*vg(l/1960))-l,10e?16:e,jn===null)var l=!1;else{if(e=jn,jn=null,ta=0,(_e&6)!==0)throw Error(i(331));var c=_e;for(_e|=4,Y=e.current;Y!==null;){var f=Y,v=f.child;if((Y.flags&16)!==0){var _=f.deletions;if(_!==null){for(var I=0;I<_.length;I++){var R=_[I];for(Y=R;Y!==null;){var U=Y;switch(U.tag){case 0:case 11:case 15:Mo(8,U,f)}var q=U.child;if(q!==null)q.return=U,Y=q;else for(;Y!==null;){U=Y;var M=U.sibling,K=U.return;if(vp(U),U===R){Y=null;break}if(M!==null){M.return=K,Y=M;break}Y=K}}}var te=f.alternate;if(te!==null){var ne=te.child;if(ne!==null){te.child=null;do{var He=ne.sibling;ne.sibling=null,ne=He}while(ne!==null)}}Y=f}}if((f.subtreeFlags&2064)!==0&&v!==null)v.return=f,Y=v;else e:for(;Y!==null;){if(f=Y,(f.flags&2048)!==0)switch(f.tag){case 0:case 11:case 15:Mo(9,f,f.return)}var C=f.sibling;if(C!==null){C.return=f.return,Y=C;break e}Y=f.return}}var k=e.current;for(Y=k;Y!==null;){v=Y;var B=v.child;if((v.subtreeFlags&2064)!==0&&B!==null)B.return=v,Y=B;else e:for(v=k;Y!==null;){if(_=Y,(_.flags&2048)!==0)try{switch(_.tag){case 0:case 11:case 15:Ki(9,_)}}catch(re){Ve(_,_.return,re)}if(_===v){Y=null;break e}var V=_.sibling;if(V!==null){V.return=_.return,Y=V;break e}Y=_.return}}if(_e=c,Rn(),Xt&&typeof Xt.onPostCommitFiberRoot=="function")try{Xt.onPostCommitFiberRoot(ui,e)}catch{}l=!0}return l}finally{be=a,Lt.transition=n}}return!1}function Rp(e,n,a){n=Dr(a,n),n=Jd(e,n,1),e=Nn(e,n,1),n=pt(),e!==null&&(fo(e,1,n),wt(e,n))}function Ve(e,n,a){if(e.tag===3)Rp(e,e,a);else for(;n!==null;){if(n.tag===3){Rp(n,e,a);break}else if(n.tag===1){var l=n.stateNode;if(typeof n.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(On===null||!On.has(l))){e=Dr(a,e),e=Kd(n,e,1),n=Nn(n,e,1),e=pt(),n!==null&&(fo(n,1,e),wt(n,e));break}}n=n.return}}function wg(e,n,a){var l=e.pingCache;l!==null&&l.delete(n),n=pt(),e.pingedLanes|=e.suspendedLanes&a,nt===e&&(at&a)===a&&(Xe===4||Xe===3&&(at&130023424)===at&&500>We()-_l?lr(e,0):yl|=a),wt(e,n)}function Pp(e,n){n===0&&((e.mode&1)===0?n=1:(n=di,di<<=1,(di&130023424)===0&&(di=4194304)));var a=pt();e=cn(e,n),e!==null&&(fo(e,n,a),wt(e,a))}function xg(e){var n=e.memoizedState,a=0;n!==null&&(a=n.retryLane),Pp(e,a)}function Eg(e,n){var a=0;switch(e.tag){case 13:var l=e.stateNode,c=e.memoizedState;c!==null&&(a=c.retryLane);break;case 19:l=e.stateNode;break;default:throw Error(i(314))}l!==null&&l.delete(n),Pp(e,a)}var Np;Np=function(e,n,a){if(e!==null)if(e.memoizedProps!==n.pendingProps||ht.current)yt=!0;else{if((e.lanes&a)===0&&(n.flags&128)===0)return yt=!1,ug(e,n,a);yt=(e.flags&131072)!==0}else yt=!1,Oe&&(n.flags&1048576)!==0&&pd(n,Ni,n.index);switch(n.lanes=0,n.tag){case 2:var l=n.type;Gi(e,n),e=n.pendingProps;var c=Rr(n,st.current);$r(n,a),c=Ks(null,n,l,e,c,a);var f=Qs();return n.flags|=1,typeof c=="object"&&c!==null&&typeof c.render=="function"&&c.$$typeof===void 0?(n.tag=1,n.memoizedState=null,n.updateQueue=null,gt(l)?(f=!0,Bi(n)):f=!1,n.memoizedState=c.state!==null&&c.state!==void 0?c.state:null,Zs(n),c.updater=Wi,n.stateNode=c,c._reactInternals=n,rl(n,l,e,a),n=sl(null,n,l,!0,f,a)):(n.tag=0,Oe&&f&&Ns(n),dt(null,n,c,a),n=n.child),n;case 16:l=n.elementType;e:{switch(Gi(e,n),e=n.pendingProps,c=l._init,l=c(l._payload),n.type=l,c=n.tag=Sg(l),e=Vt(l,e),c){case 0:n=al(null,n,l,e,a);break e;case 1:n=ip(null,n,l,e,a);break e;case 11:n=ep(null,n,l,e,a);break e;case 14:n=tp(null,n,l,Vt(l.type,e),a);break e}throw Error(i(306,l,""))}return n;case 0:return l=n.type,c=n.pendingProps,c=n.elementType===l?c:Vt(l,c),al(e,n,l,c,a);case 1:return l=n.type,c=n.pendingProps,c=n.elementType===l?c:Vt(l,c),ip(e,n,l,c,a);case 3:e:{if(ap(n),e===null)throw Error(i(387));l=n.pendingProps,f=n.memoizedState,c=f.element,xd(e,n),Di(n,l,null,a);var v=n.memoizedState;if(l=v.element,f.isDehydrated)if(f={element:l,isDehydrated:!1,cache:v.cache,pendingSuspenseBoundaries:v.pendingSuspenseBoundaries,transitions:v.transitions},n.updateQueue.baseState=f,n.memoizedState=f,n.flags&256){c=Dr(Error(i(423)),n),n=sp(e,n,l,a,c);break e}else if(l!==c){c=Dr(Error(i(424)),n),n=sp(e,n,l,a,c);break e}else for(Tt=Cn(n.stateNode.containerInfo.firstChild),Ct=n,Oe=!0,qt=null,a=_d(n,null,l,a),n.child=a;a;)a.flags=a.flags&-3|4096,a=a.sibling;else{if(Ar(),l===c){n=pn(e,n,a);break e}dt(e,n,l,a)}n=n.child}return n;case 5:return Sd(n),e===null&&js(n),l=n.type,c=n.pendingProps,f=e!==null?e.memoizedProps:null,v=c.children,zs(l,c)?v=null:f!==null&&zs(l,f)&&(n.flags|=32),op(e,n),dt(e,n,v,a),n.child;case 6:return e===null&&js(n),null;case 13:return lp(e,n,a);case 4:return qs(n,n.stateNode.containerInfo),l=n.pendingProps,e===null?n.child=Or(n,null,l,a):dt(e,n,l,a),n.child;case 11:return l=n.type,c=n.pendingProps,c=n.elementType===l?c:Vt(l,c),ep(e,n,l,c,a);case 7:return dt(e,n,n.pendingProps,a),n.child;case 8:return dt(e,n,n.pendingProps.children,a),n.child;case 12:return dt(e,n,n.pendingProps.children,a),n.child;case 10:e:{if(l=n.type._context,c=n.pendingProps,f=n.memoizedProps,v=c.value,Te(ji,l._currentValue),l._currentValue=v,f!==null)if(Zt(f.value,v)){if(f.children===c.children&&!ht.current){n=pn(e,n,a);break e}}else for(f=n.child,f!==null&&(f.return=n);f!==null;){var _=f.dependencies;if(_!==null){v=f.child;for(var I=_.firstContext;I!==null;){if(I.context===l){if(f.tag===1){I=dn(-1,a&-a),I.tag=2;var R=f.updateQueue;if(R!==null){R=R.shared;var U=R.pending;U===null?I.next=I:(I.next=U.next,U.next=I),R.pending=I}}f.lanes|=a,I=f.alternate,I!==null&&(I.lanes|=a),Fs(f.return,a,n),_.lanes|=a;break}I=I.next}}else if(f.tag===10)v=f.type===n.type?null:f.child;else if(f.tag===18){if(v=f.return,v===null)throw Error(i(341));v.lanes|=a,_=v.alternate,_!==null&&(_.lanes|=a),Fs(v,a,n),v=f.sibling}else v=f.child;if(v!==null)v.return=f;else for(v=f;v!==null;){if(v===n){v=null;break}if(f=v.sibling,f!==null){f.return=v.return,v=f;break}v=v.return}f=v}dt(e,n,c.children,a),n=n.child}return n;case 9:return c=n.type,l=n.pendingProps.children,$r(n,a),c=jt(c),l=l(c),n.flags|=1,dt(e,n,l,a),n.child;case 14:return l=n.type,c=Vt(l,n.pendingProps),c=Vt(l.type,c),tp(e,n,l,c,a);case 15:return np(e,n,n.type,n.pendingProps,a);case 17:return l=n.type,c=n.pendingProps,c=n.elementType===l?c:Vt(l,c),Gi(e,n),n.tag=1,gt(l)?(e=!0,Bi(n)):e=!1,$r(n,a),Hd(n,l,c),rl(n,l,c,a),sl(null,n,l,!0,e,a);case 19:return cp(e,n,a);case 22:return rp(e,n,a)}throw Error(i(156,n.tag))};function Ap(e,n){return fc(e,n)}function Ig(e,n,a,l){this.tag=e,this.key=a,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=n,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=l,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Dt(e,n,a,l){return new Ig(e,n,a,l)}function zl(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Sg(e){if(typeof e=="function")return zl(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ke)return 11;if(e===kt)return 14}return 2}function Dn(e,n){var a=e.alternate;return a===null?(a=Dt(e.tag,n,e.key,e.mode),a.elementType=e.elementType,a.type=e.type,a.stateNode=e.stateNode,a.alternate=e,e.alternate=a):(a.pendingProps=n,a.type=e.type,a.flags=0,a.subtreeFlags=0,a.deletions=null),a.flags=e.flags&14680064,a.childLanes=e.childLanes,a.lanes=e.lanes,a.child=e.child,a.memoizedProps=e.memoizedProps,a.memoizedState=e.memoizedState,a.updateQueue=e.updateQueue,n=e.dependencies,a.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext},a.sibling=e.sibling,a.index=e.index,a.ref=e.ref,a}function ia(e,n,a,l,c,f){var v=2;if(l=e,typeof e=="function")zl(e)&&(v=1);else if(typeof e=="string")v=5;else e:switch(e){case de:return cr(a.children,c,f,n);case pe:v=8,c|=8;break;case Re:return e=Dt(12,a,n,c|2),e.elementType=Re,e.lanes=f,e;case et:return e=Dt(13,a,n,c),e.elementType=et,e.lanes=f,e;case Qe:return e=Dt(19,a,n,c),e.elementType=Qe,e.lanes=f,e;case qe:return aa(a,c,f,n);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ye:v=10;break e;case Ze:v=9;break e;case Ke:v=11;break e;case kt:v=14;break e;case vt:v=16,l=null;break e}throw Error(i(130,e==null?e:typeof e,""))}return n=Dt(v,a,n,c),n.elementType=e,n.type=l,n.lanes=f,n}function cr(e,n,a,l){return e=Dt(7,e,l,n),e.lanes=a,e}function aa(e,n,a,l){return e=Dt(22,e,l,n),e.elementType=qe,e.lanes=a,e.stateNode={isHidden:!1},e}function Cl(e,n,a){return e=Dt(6,e,null,n),e.lanes=a,e}function Tl(e,n,a){return n=Dt(4,e.children!==null?e.children:[],e.key,n),n.lanes=a,n.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},n}function kg(e,n,a,l,c){this.tag=n,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=ns(0),this.expirationTimes=ns(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ns(0),this.identifierPrefix=l,this.onRecoverableError=c,this.mutableSourceEagerHydrationData=null}function Bl(e,n,a,l,c,f,v,_,I){return e=new kg(e,n,a,_,I),n===1?(n=1,f===!0&&(n|=8)):n=0,f=Dt(3,null,null,n),e.current=f,f.stateNode=e,f.memoizedState={element:l,isDehydrated:a,cache:null,transitions:null,pendingSuspenseBoundaries:null},Zs(f),e}function bg(e,n,a){var l=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(r){console.error(r)}}return t(),jl.exports=Dg(),jl.exports}var Gp;function Mg(){if(Gp)return fa;Gp=1;var t=Wf();return fa.createRoot=t.createRoot,fa.hydrateRoot=t.hydrateRoot,fa}var Fg=Mg();const Ug=qf(Fg);Wf();function Go(){return Go=Object.assign?Object.assign.bind():function(t){for(var r=1;r"u")throw new Error(r)}function au(t,r){if(!t){typeof console<"u"&&console.warn(r);try{throw new Error(r)}catch{}}}function qg(){return Math.random().toString(36).substr(2,8)}function Kp(t,r){return{usr:t.state,key:t.key,idx:r}}function ql(t,r,i,s){return i===void 0&&(i=null),Go({pathname:typeof t=="string"?t:t.pathname,search:"",hash:""},typeof r=="string"?to(r):r,{state:i,key:r&&r.key||s||qg()})}function wa(t){let{pathname:r="/",search:i="",hash:s=""}=t;return i&&i!=="?"&&(r+=i.charAt(0)==="?"?i:"?"+i),s&&s!=="#"&&(r+=s.charAt(0)==="#"?s:"#"+s),r}function to(t){let r={};if(t){let i=t.indexOf("#");i>=0&&(r.hash=t.substr(i),t=t.substr(0,i));let s=t.indexOf("?");s>=0&&(r.search=t.substr(s),t=t.substr(0,s)),t&&(r.pathname=t)}return r}function Vg(t,r,i,s){s===void 0&&(s={});let{window:u=document.defaultView,v5Compat:p=!1}=s,d=u.history,m=Un.Pop,g=null,y=E();y==null&&(y=0,d.replaceState(Go({},d.state,{idx:y}),""));function E(){return(d.state||{idx:null}).idx}function S(){m=Un.Pop;let O=E(),H=O==null?null:O-y;y=O,g&&g({action:m,location:W.location,delta:H})}function T(O,H){m=Un.Push;let oe=ql(W.location,O,H);y=E()+1;let Q=Kp(oe,y),G=W.createHref(oe);try{d.pushState(Q,"",G)}catch(ee){if(ee instanceof DOMException&&ee.name==="DataCloneError")throw ee;u.location.assign(G)}p&&g&&g({action:m,location:W.location,delta:1})}function A(O,H){m=Un.Replace;let oe=ql(W.location,O,H);y=E();let Q=Kp(oe,y),G=W.createHref(oe);d.replaceState(Q,"",G),p&&g&&g({action:m,location:W.location,delta:0})}function D(O){let H=u.location.origin!=="null"?u.location.origin:u.location.href,oe=typeof O=="string"?O:wa(O);return oe=oe.replace(/ $/,"%20"),Fe(H,"No window.location.(origin|href) available to create URL for href: "+oe),new URL(oe,H)}let W={get action(){return m},get location(){return t(u,d)},listen(O){if(g)throw new Error("A history only accepts one active listener");return u.addEventListener(Jp,S),g=O,()=>{u.removeEventListener(Jp,S),g=null}},createHref(O){return r(u,O)},createURL:D,encodeLocation(O){let H=D(O);return{pathname:H.pathname,search:H.search,hash:H.hash}},push:T,replace:A,go(O){return d.go(O)}};return W}var Qp;(function(t){t.data="data",t.deferred="deferred",t.redirect="redirect",t.error="error"})(Qp||(Qp={}));function Wg(t,r,i){return i===void 0&&(i="/"),Hg(t,r,i)}function Hg(t,r,i,s){let u=typeof r=="string"?to(r):r,p=Qr(u.pathname||"/",i);if(p==null)return null;let d=Hf(t);Gg(d);let m=null,g=i0(p);for(let y=0;m==null&&y{let g={relativePath:m===void 0?p.path||"":m,caseSensitive:p.caseSensitive===!0,childrenIndex:d,route:p};g.relativePath.startsWith("/")&&(Fe(g.relativePath.startsWith(s),'Absolute route path "'+g.relativePath+'" nested under path '+('"'+s+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),g.relativePath=g.relativePath.slice(s.length));let y=qn([s,g.relativePath]),E=i.concat(g);p.children&&p.children.length>0&&(Fe(p.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+y+'".')),Hf(p.children,r,E,y)),!(p.path==null&&!p.index)&&r.push({path:y,score:t0(y,p.index),routesMeta:E})};return t.forEach((p,d)=>{var m;if(p.path===""||!((m=p.path)!=null&&m.includes("?")))u(p,d);else for(let g of Gf(p.path))u(p,d,g)}),r}function Gf(t){let r=t.split("/");if(r.length===0)return[];let[i,...s]=r,u=i.endsWith("?"),p=i.replace(/\?$/,"");if(s.length===0)return u?[p,""]:[p];let d=Gf(s.join("/")),m=[];return m.push(...d.map(g=>g===""?p:[p,g].join("/"))),u&&m.push(...d),m.map(g=>t.startsWith("/")&&g===""?"/":g)}function Gg(t){t.sort((r,i)=>r.score!==i.score?i.score-r.score:n0(r.routesMeta.map(s=>s.childrenIndex),i.routesMeta.map(s=>s.childrenIndex)))}const Jg=/^:[\w-]+$/,Kg=3,Qg=2,Yg=1,Xg=10,e0=-2,Yp=t=>t==="*";function t0(t,r){let i=t.split("/"),s=i.length;return i.some(Yp)&&(s+=e0),r&&(s+=Qg),i.filter(u=>!Yp(u)).reduce((u,p)=>u+(Jg.test(p)?Kg:p===""?Yg:Xg),s)}function n0(t,r){return t.length===r.length&&t.slice(0,-1).every((s,u)=>s===r[u])?t[t.length-1]-r[r.length-1]:0}function r0(t,r,i){let{routesMeta:s}=t,u={},p="/",d=[];for(let m=0;m{let{paramName:T,isOptional:A}=E;if(T==="*"){let W=m[S]||"";d=p.slice(0,p.length-W.length).replace(/(.)\/+$/,"$1")}const D=m[S];return A&&!D?y[T]=void 0:y[T]=(D||"").replace(/%2F/g,"/"),y},{}),pathname:p,pathnameBase:d,pattern:t}}function o0(t,r,i){r===void 0&&(r=!1),i===void 0&&(i=!0),au(t==="*"||!t.endsWith("*")||t.endsWith("/*"),'Route path "'+t+'" will be treated as if it were '+('"'+t.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+t.replace(/\*$/,"/*")+'".'));let s=[],u="^"+t.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(d,m,g)=>(s.push({paramName:m,isOptional:g!=null}),g?"/?([^\\/]+)?":"/([^\\/]+)"));return t.endsWith("*")?(s.push({paramName:"*"}),u+=t==="*"||t==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):i?u+="\\/*$":t!==""&&t!=="/"&&(u+="(?:(?=\\/|$))"),[new RegExp(u,r?void 0:"i"),s]}function i0(t){try{return t.split("/").map(r=>decodeURIComponent(r).replace(/\//g,"%2F")).join("/")}catch(r){return au(!1,'The URL path "'+t+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+r+").")),t}}function Qr(t,r){if(r==="/")return t;if(!t.toLowerCase().startsWith(r.toLowerCase()))return null;let i=r.endsWith("/")?r.length-1:r.length,s=t.charAt(i);return s&&s!=="/"?null:t.slice(i)||"/"}const a0=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,s0=t=>a0.test(t);function l0(t,r){r===void 0&&(r="/");let{pathname:i,search:s="",hash:u=""}=typeof t=="string"?to(t):t,p;if(i)if(s0(i))p=i;else{if(i.includes("//")){let d=i;i=Jf(i),au(!1,"Pathnames cannot have embedded double slashes - normalizing "+(d+" -> "+i))}i.startsWith("/")?p=Xp(i.substring(1),"/"):p=Xp(i,r)}else p=r;return{pathname:p,search:d0(s),hash:p0(u)}}function Xp(t,r){let i=r.replace(/\/+$/,"").split("/");return t.split("/").forEach(u=>{u===".."?i.length>1&&i.pop():u!=="."&&i.push(u)}),i.length>1?i.join("/"):"/"}function Dl(t,r,i,s){return"Cannot include a '"+t+"' character in a manually specified "+("`to."+r+"` field ["+JSON.stringify(s)+"]. Please separate it out to the ")+("`to."+i+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function u0(t){return t.filter((r,i)=>i===0||r.route.path&&r.route.path.length>0)}function su(t,r){let i=u0(t);return r?i.map((s,u)=>u===i.length-1?s.pathname:s.pathnameBase):i.map(s=>s.pathnameBase)}function lu(t,r,i,s){s===void 0&&(s=!1);let u;typeof t=="string"?u=to(t):(u=Go({},t),Fe(!u.pathname||!u.pathname.includes("?"),Dl("?","pathname","search",u)),Fe(!u.pathname||!u.pathname.includes("#"),Dl("#","pathname","hash",u)),Fe(!u.search||!u.search.includes("#"),Dl("#","search","hash",u)));let p=t===""||u.pathname==="",d=p?"/":u.pathname,m;if(d==null)m=i;else{let S=r.length-1;if(!s&&d.startsWith("..")){let T=d.split("/");for(;T[0]==="..";)T.shift(),S-=1;u.pathname=T.join("/")}m=S>=0?r[S]:"/"}let g=l0(u,m),y=d&&d!=="/"&&d.endsWith("/"),E=(p||d===".")&&i.endsWith("/");return!g.pathname.endsWith("/")&&(y||E)&&(g.pathname+="/"),g}const Jf=t=>t.replace(/\/\/+/g,"/"),qn=t=>Jf(t.join("/")),c0=t=>t.replace(/\/+$/,"").replace(/^\/*/,"/"),d0=t=>!t||t==="?"?"":t.startsWith("?")?t:"?"+t,p0=t=>!t||t==="#"?"":t.startsWith("#")?t:"#"+t;function f0(t){return t!=null&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.internal=="boolean"&&"data"in t}const Kf=["post","put","patch","delete"];new Set(Kf);const m0=["get",...Kf];new Set(m0);function Jo(){return Jo=Object.assign?Object.assign.bind():function(t){for(var r=1;r{m.current=!0}),b.useCallback(function(y,E){if(E===void 0&&(E={}),!m.current)return;if(typeof y=="number"){s.go(y);return}let S=lu(y,JSON.parse(d),p,E.relative==="path");t==null&&r!=="/"&&(S.pathname=S.pathname==="/"?r:qn([r,S.pathname])),(E.replace?s.replace:s.push)(S,E.state,E)},[r,s,d,p,t])}function A6(){let{matches:t}=b.useContext(yn),r=t[t.length-1];return r?r.params:{}}function Ta(t,r){let{relative:i}=r===void 0?{}:r,{future:s}=b.useContext(gn),{matches:u}=b.useContext(yn),{pathname:p}=_n(),d=JSON.stringify(su(u,s.v7_relativeSplatPath));return b.useMemo(()=>lu(t,JSON.parse(d),p,i==="path"),[t,d,p,i])}function g0(t,r){return y0(t,r)}function y0(t,r,i,s){no()||Fe(!1);let{navigator:u}=b.useContext(gn),{matches:p}=b.useContext(yn),d=p[p.length-1],m=d?d.params:{};d&&d.pathname;let g=d?d.pathnameBase:"/";d&&d.route;let y=_n(),E;if(r){var S;let O=typeof r=="string"?to(r):r;g==="/"||(S=O.pathname)!=null&&S.startsWith(g)||Fe(!1),E=O}else E=y;let T=E.pathname||"/",A=T;if(g!=="/"){let O=g.replace(/^\//,"").split("/");A="/"+T.replace(/^\//,"").split("/").slice(O.length).join("/")}let D=Wg(t,{pathname:A}),W=I0(D&&D.map(O=>Object.assign({},O,{params:Object.assign({},m,O.params),pathname:qn([g,u.encodeLocation?u.encodeLocation(O.pathname).pathname:O.pathname]),pathnameBase:O.pathnameBase==="/"?g:qn([g,u.encodeLocation?u.encodeLocation(O.pathnameBase).pathname:O.pathnameBase])})),p,i,s);return r&&W?b.createElement(Ca.Provider,{value:{location:Jo({pathname:"/",search:"",hash:"",state:null,key:"default"},E),navigationType:Un.Pop}},W):W}function _0(){let t=z0(),r=f0(t)?t.status+" "+t.statusText:t instanceof Error?t.message:JSON.stringify(t),i=t instanceof Error?t.stack:null,u={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return b.createElement(b.Fragment,null,b.createElement("h2",null,"Unexpected Application Error!"),b.createElement("h3",{style:{fontStyle:"italic"}},r),i?b.createElement("pre",{style:u},i):null,null)}const w0=b.createElement(_0,null);class x0 extends b.Component{constructor(r){super(r),this.state={location:r.location,revalidation:r.revalidation,error:r.error}}static getDerivedStateFromError(r){return{error:r}}static getDerivedStateFromProps(r,i){return i.location!==r.location||i.revalidation!=="idle"&&r.revalidation==="idle"?{error:r.error,location:r.location,revalidation:r.revalidation}:{error:r.error!==void 0?r.error:i.error,location:i.location,revalidation:r.revalidation||i.revalidation}}componentDidCatch(r,i){console.error("React Router caught the following error during render",r,i)}render(){return this.state.error!==void 0?b.createElement(yn.Provider,{value:this.props.routeContext},b.createElement(Yf.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function E0(t){let{routeContext:r,match:i,children:s}=t,u=b.useContext(za);return u&&u.static&&u.staticContext&&(i.route.errorElement||i.route.ErrorBoundary)&&(u.staticContext._deepestRenderedBoundaryId=i.route.id),b.createElement(yn.Provider,{value:r},s)}function I0(t,r,i,s){var u;if(r===void 0&&(r=[]),i===void 0&&(i=null),s===void 0&&(s=null),t==null){var p;if(!i)return null;if(i.errors)t=i.matches;else if((p=s)!=null&&p.v7_partialHydration&&r.length===0&&!i.initialized&&i.matches.length>0)t=i.matches;else return null}let d=t,m=(u=i)==null?void 0:u.errors;if(m!=null){let E=d.findIndex(S=>S.route.id&&m?.[S.route.id]!==void 0);E>=0||Fe(!1),d=d.slice(0,Math.min(d.length,E+1))}let g=!1,y=-1;if(i&&s&&s.v7_partialHydration)for(let E=0;E=0?d=d.slice(0,y+1):d=[d[0]];break}}}return d.reduceRight((E,S,T)=>{let A,D=!1,W=null,O=null;i&&(A=m&&S.route.id?m[S.route.id]:void 0,W=S.route.errorElement||w0,g&&(y<0&&T===0?(T0("route-fallback"),D=!0,O=null):y===T&&(D=!0,O=S.route.hydrateFallbackElement||null)));let H=r.concat(d.slice(0,T+1)),oe=()=>{let Q;return A?Q=W:D?Q=O:S.route.Component?Q=b.createElement(S.route.Component,null):S.route.element?Q=S.route.element:Q=E,b.createElement(E0,{match:S,routeContext:{outlet:E,matches:H,isDataRoute:i!=null},children:Q})};return i&&(S.route.ErrorBoundary||S.route.errorElement||T===0)?b.createElement(x0,{location:i.location,revalidation:i.revalidation,component:W,error:A,children:oe(),routeContext:{outlet:null,matches:H,isDataRoute:!0}}):oe()},null)}var em=(function(t){return t.UseBlocker="useBlocker",t.UseRevalidator="useRevalidator",t.UseNavigateStable="useNavigate",t})(em||{}),tm=(function(t){return t.UseBlocker="useBlocker",t.UseLoaderData="useLoaderData",t.UseActionData="useActionData",t.UseRouteError="useRouteError",t.UseNavigation="useNavigation",t.UseRouteLoaderData="useRouteLoaderData",t.UseMatches="useMatches",t.UseRevalidator="useRevalidator",t.UseNavigateStable="useNavigate",t.UseRouteId="useRouteId",t})(tm||{});function S0(t){let r=b.useContext(za);return r||Fe(!1),r}function k0(t){let r=b.useContext(Qf);return r||Fe(!1),r}function b0(t){let r=b.useContext(yn);return r||Fe(!1),r}function nm(t){let r=b0(),i=r.matches[r.matches.length-1];return i.route.id||Fe(!1),i.route.id}function z0(){var t;let r=b.useContext(Yf),i=k0(),s=nm();return r!==void 0?r:(t=i.errors)==null?void 0:t[s]}function C0(){let{router:t}=S0(em.UseNavigateStable),r=nm(tm.UseNavigateStable),i=b.useRef(!1);return Xf(()=>{i.current=!0}),b.useCallback(function(u,p){p===void 0&&(p={}),i.current&&(typeof u=="number"?t.navigate(u):t.navigate(u,Jo({fromRouteId:r},p)))},[t,r])}const ef={};function T0(t,r,i){ef[t]||(ef[t]=!0)}function B0(t,r){t?.v7_startTransition,t?.v7_relativeSplatPath}function R0(t){let{to:r,replace:i,state:s,relative:u}=t;no()||Fe(!1);let{future:p,static:d}=b.useContext(gn),{matches:m}=b.useContext(yn),{pathname:g}=_n(),y=uu(),E=lu(r,su(m,p.v7_relativeSplatPath),g,u==="path"),S=JSON.stringify(E);return b.useEffect(()=>y(JSON.parse(S),{replace:i,state:s,relative:u}),[y,S,u,i,s]),null}function on(t){Fe(!1)}function P0(t){let{basename:r="/",children:i=null,location:s,navigationType:u=Un.Pop,navigator:p,static:d=!1,future:m}=t;no()&&Fe(!1);let g=r.replace(/^\/*/,"/"),y=b.useMemo(()=>({basename:g,navigator:p,static:d,future:Jo({v7_relativeSplatPath:!1},m)}),[g,m,p,d]);typeof s=="string"&&(s=to(s));let{pathname:E="/",search:S="",hash:T="",state:A=null,key:D="default"}=s,W=b.useMemo(()=>{let O=Qr(E,g);return O==null?null:{location:{pathname:O,search:S,hash:T,state:A,key:D},navigationType:u}},[g,E,S,T,A,D,u]);return W==null?null:b.createElement(gn.Provider,{value:y},b.createElement(Ca.Provider,{children:i,value:W}))}function N0(t){let{children:r,location:i}=t;return g0(Wl(r),i)}new Promise(()=>{});function Wl(t,r){r===void 0&&(r=[]);let i=[];return b.Children.forEach(t,(s,u)=>{if(!b.isValidElement(s))return;let p=[...r,u];if(s.type===b.Fragment){i.push.apply(i,Wl(s.props.children,p));return}s.type!==on&&Fe(!1),!s.props.index||!s.props.children||Fe(!1);let d={id:s.props.id||p.join("-"),caseSensitive:s.props.caseSensitive,element:s.props.element,Component:s.props.Component,index:s.props.index,path:s.props.path,loader:s.props.loader,action:s.props.action,errorElement:s.props.errorElement,ErrorBoundary:s.props.ErrorBoundary,hasErrorBoundary:s.props.ErrorBoundary!=null||s.props.errorElement!=null,shouldRevalidate:s.props.shouldRevalidate,handle:s.props.handle,lazy:s.props.lazy};s.props.children&&(d.children=Wl(s.props.children,p)),i.push(d)}),i}function xa(){return xa=Object.assign?Object.assign.bind():function(t){for(var r=1;r{let s=t[i];return r.concat(Array.isArray(s)?s.map(u=>[i,u]):[[i,s]])},[]))}function j0(t,r){let i=Hl(t);return r&&r.forEach((s,u)=>{i.has(u)||r.getAll(u).forEach(p=>{i.append(u,p)})}),i}const $0=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],L0=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],D0="6";try{window.__reactRouterVersion=D0}catch{}const M0=b.createContext({isTransitioning:!1}),F0="startTransition",tf=jg[F0];function U0(t){let{basename:r,children:i,future:s,window:u}=t,p=b.useRef();p.current==null&&(p.current=Zg({window:u,v5Compat:!0}));let d=p.current,[m,g]=b.useState({action:d.action,location:d.location}),{v7_startTransition:y}=s||{},E=b.useCallback(S=>{y&&tf?tf(()=>g(S)):g(S)},[g,y]);return b.useLayoutEffect(()=>d.listen(E),[d,E]),b.useEffect(()=>B0(s),[s]),b.createElement(P0,{basename:r,children:i,location:m.location,navigationType:m.action,navigator:d,future:s})}const Z0=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",q0=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,V0=b.forwardRef(function(r,i){let{onClick:s,relative:u,reloadDocument:p,replace:d,state:m,target:g,to:y,preventScrollReset:E,viewTransition:S}=r,T=rm(r,$0),{basename:A}=b.useContext(gn),D,W=!1;if(typeof y=="string"&&q0.test(y)&&(D=y,Z0))try{let Q=new URL(window.location.href),G=y.startsWith("//")?new URL(Q.protocol+y):new URL(y),ee=Qr(G.pathname,A);G.origin===Q.origin&&ee!=null?y=ee+G.search+G.hash:W=!0}catch{}let O=v0(y,{relative:u}),H=G0(y,{replace:d,state:m,target:g,preventScrollReset:E,relative:u,viewTransition:S});function oe(Q){s&&s(Q),Q.defaultPrevented||H(Q)}return b.createElement("a",xa({},T,{href:D||O,onClick:W||p?s:oe,ref:i,target:g}))}),W0=b.forwardRef(function(r,i){let{"aria-current":s="page",caseSensitive:u=!1,className:p="",end:d=!1,style:m,to:g,viewTransition:y,children:E}=r,S=rm(r,L0),T=Ta(g,{relative:S.relative}),A=_n(),D=b.useContext(Qf),{navigator:W,basename:O}=b.useContext(gn),H=D!=null&&J0(T)&&y===!0,oe=W.encodeLocation?W.encodeLocation(T).pathname:T.pathname,Q=A.pathname,G=D&&D.navigation&&D.navigation.location?D.navigation.location.pathname:null;u||(Q=Q.toLowerCase(),G=G?G.toLowerCase():null,oe=oe.toLowerCase()),G&&O&&(G=Qr(G,O)||G);const ee=oe!=="/"&&oe.endsWith("/")?oe.length-1:oe.length;let ue=Q===oe||!d&&Q.startsWith(oe)&&Q.charAt(ee)==="/",de=G!=null&&(G===oe||!d&&G.startsWith(oe)&&G.charAt(oe.length)==="/"),pe={isActive:ue,isPending:de,isTransitioning:H},Re=ue?s:void 0,ye;typeof p=="function"?ye=p(pe):ye=[p,ue?"active":null,de?"pending":null,H?"transitioning":null].filter(Boolean).join(" ");let Ze=typeof m=="function"?m(pe):m;return b.createElement(V0,xa({},S,{"aria-current":Re,className:ye,ref:i,style:Ze,to:g,viewTransition:y}),typeof E=="function"?E(pe):E)});var Gl;(function(t){t.UseScrollRestoration="useScrollRestoration",t.UseSubmit="useSubmit",t.UseSubmitFetcher="useSubmitFetcher",t.UseFetcher="useFetcher",t.useViewTransitionState="useViewTransitionState"})(Gl||(Gl={}));var nf;(function(t){t.UseFetcher="useFetcher",t.UseFetchers="useFetchers",t.UseScrollRestoration="useScrollRestoration"})(nf||(nf={}));function H0(t){let r=b.useContext(za);return r||Fe(!1),r}function G0(t,r){let{target:i,replace:s,state:u,preventScrollReset:p,relative:d,viewTransition:m}=r===void 0?{}:r,g=uu(),y=_n(),E=Ta(t,{relative:d});return b.useCallback(S=>{if(O0(S,i)){S.preventDefault();let T=s!==void 0?s:wa(y)===wa(E);g(t,{replace:T,state:u,preventScrollReset:p,relative:d,viewTransition:m})}},[y,g,E,s,u,i,t,p,d,m])}function O6(t){let r=b.useRef(Hl(t)),i=b.useRef(!1),s=_n(),u=b.useMemo(()=>j0(s.search,i.current?null:r.current),[s.search]),p=uu(),d=b.useCallback((m,g)=>{const y=Hl(typeof m=="function"?m(u):m);i.current=!0,p("?"+y,g)},[p,u]);return[u,d]}function J0(t,r){r===void 0&&(r={});let i=b.useContext(M0);i==null&&Fe(!1);let{basename:s}=H0(Gl.useViewTransitionState),u=Ta(t,{relative:r.relative});if(!i.isTransitioning)return!1;let p=Qr(i.currentLocation.pathname,s)||i.currentLocation.pathname,d=Qr(i.nextLocation.pathname,s)||i.nextLocation.pathname;return Vl(u.pathname,d)!=null||Vl(u.pathname,p)!=null}const K0=new Set(["failed","errored","stuck","crashed"]),Q0=new Set(["rate-limited","rate_limited","waiting"]),Y0={"awaiting-input":"respond",errored:"reset","rate-limited":"nudge",stalled:"nudge"};function X0(t,r){const i=new Map;for(const u of r)i.set(u.agentName,u.prompt);const s=[];for(const u of t){const p=i.has(u.name),d=ey(u,p);d!==null&&s.push({name:u.name,reason:d,detail:ny(u,d,i.get(u.name)),action:Y0[d]})}return s}function ey(t,r){if(r)return"awaiting-input";const i=t.state.toLowerCase();return K0.has(i)?"errored":Q0.has(i)?"rate-limited":ty(t,i)?"stalled":null}function ty(t,r){return r==="detached"?!0:t.running&&t.session===void 0}function ny(t,r,i){switch(r){case"awaiting-input":return ry(i);case"errored":return`Exited ${t.state}.`;case"rate-limited":return"Throttled by a provider limit.";case"stalled":return t.state.toLowerCase()==="detached"?"Detached from its session.":"Running with no live session."}}function ry(t){if(t===void 0)return"Awaiting your decision.";const r=t.split(` +`,1)[0]?.trim()??"";return r.length>0?r:"Awaiting your decision."}function oy(t){return t.filter(r=>r.phase==="blocked").map(r=>({id:r.id,title:r.title,reason:iy(r),remedy:ay(r),scope:r.scope}))}function iy(t){const r=sy(t);if(r!==null)return`Blocked at ${r}`;const i=t.statusCounts.blocked??0;return i>0?`${i} blocked step${i===1?"":"s"}`:"Blocked, awaiting operator"}function ay(t){return t.activeAssignees.length===0?"No worker assigned. Claim or dispatch one.":"Open run detail to review the blocked step."}function sy(t){if(t.progress.status==="active_step"||t.progress.status==="stage_only"){const r=t.progress.stage;if(r.status==="available")return r.label}return null}const om=/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i,ly={bead:"bead.",session:"session."};function Wr(t){return t instanceof Error?t.message:typeof t=="string"?t:"unknown error"}function uy(t){if(!t)return"";let r=t.length;for(;r>0&&t.charCodeAt(r-1)===47;)r--;const i=t.slice(0,r);return i.slice(i.lastIndexOf("/")+1)||i}const cy="polecat";function dy(t){return uy(t).toLowerCase().includes(cy)}function py(t){return t.filter(r=>!r.read&&!dy(r.from))}const fy="modulepreload",my=function(t){return"/"+t},rf={},wn=function(r,i,s){let u=Promise.resolve();if(i&&i.length>0){let g=function(y){return Promise.all(y.map(E=>Promise.resolve(E).then(S=>({status:"fulfilled",value:S}),S=>({status:"rejected",reason:S}))))};document.getElementsByTagName("link");const d=document.querySelector("meta[property=csp-nonce]"),m=d?.nonce||d?.getAttribute("nonce");u=g(i.map(y=>{if(y=my(y),y in rf)return;rf[y]=!0;const E=y.endsWith(".css"),S=E?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${y}"]${S}`))return;const T=document.createElement("link");if(T.rel=E?"stylesheet":fy,E||(T.as="script"),T.crossOrigin="",T.href=y,m&&T.setAttribute("nonce",m),document.head.appendChild(T),E)return new Promise((A,D)=>{T.addEventListener("load",A),T.addEventListener("error",()=>D(new Error(`Unable to preload CSS for ${y}`)))})}))}function p(d){const m=new Event("vite:preloadError",{cancelable:!0});if(m.payload=d,window.dispatchEvent(m),!m.defaultPrevented)throw d}return u.then(d=>{for(const m of d||[])m.status==="rejected"&&p(m.reason);return r().catch(p)})};let Ko=null;function vy(t){if(!om.test(t))throw new Error(`invalid city name: ${t}`);Ko=t}function Ba(){return Ko}function xn(t){const r=Ko;if(r===null)throw new Error(`${t} called before an active city was resolved`);return r}function dr(t){if(Ko===null)throw new Error(`cityPath("${t}") called before an active city was resolved`);return`/api/city/${encodeURIComponent(Ko)}${t}`}async function hy(t,r,i,s){const u={Accept:"application/json"};s!==void 0&&(u["Content-Type"]="application/json"),t!=="GET"&&(u["X-GC-Request"]="dashboard");const p={method:t,headers:u,credentials:"same-origin"};s!==void 0&&(p.body=JSON.stringify(s));const d=await fetch(r,p);if(!d.ok){const g=await d.text(),y=gy(g),E=y?.error??(g.trim()||d.statusText||`HTTP ${d.status}`);throw new im(d.status,E,y?.kind,y?.reason)}let m;try{m=await d.json()}catch(g){throw new am(r,`body must be valid JSON: ${_y(g)}`)}return i(m,r)}function gy(t){if(t.trim().length!==0)try{const r=JSON.parse(t);return yy(r)?r:void 0}catch{return}}function yy(t){if(typeof t!="object"||t===null)return!1;const r=t;return typeof r.error!="string"||r.kind!==void 0&&typeof r.kind!="string"?!1:r.reason===void 0||typeof r.reason=="string"}async function Mt(t,r,i,s){return hy(t,r,i,s)}class im extends Error{constructor(r,i,s,u){super(i),this.status=r,this.kind=s,this.reason=u,this.name="ApiClientError"}status;kind;reason}class am extends Error{constructor(r,i){super(`Invalid API response for ${r}: ${i}`),this.url=r,this.detail=i,this.name="ApiResponseDecodeError"}url;detail}function _y(t){return t instanceof Error?t.message:typeof t=="string"?t:"unknown error"}function vr(t,r){throw new am(t,r)}function wy(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function Ra(t,r,i){return wy(t)||vr(r,`${i} must be an object`),t}function Pt(t,r,i,s){typeof t[s]!="string"&&vr(r,`${i}.${s} must be a string`)}function sm(t,r,i,s){const u=t[s];u!==null&&typeof u!="string"&&vr(r,`${i}.${s} must be a string or null`)}function Hn(t,r,i,s){typeof t[s]!="boolean"&&vr(r,`${i}.${s} must be a boolean`)}function of(t,r,i,s){typeof t[s]!="number"&&vr(r,`${i}.${s} must be a number`)}function Nt(t,r,i,s){Array.isArray(t[s])||vr(r,`${i}.${s} must be an array`)}function Et(t,r,i,s){Ra(t[s],r,`${i}.${s}`)}function xy(t,r,i,s){const u=t[s];u!==null&&(!Array.isArray(u)||u.some(p=>typeof p!="string"))&&vr(r,`${i}.${s} must be an array of strings or null`)}function Qt(t,r){return(i,s)=>{const u=Ra(i,s,t);return r?.(u,s),u}}function lm(t,r){return Qt(t,(i,s)=>{Nt(i,s,t,"items"),r?.(i,s)})}const Ey=Qt("health",(t,r)=>{Hn(t,r,"health","ok"),Pt(t,r,"health","ts")}),Iy=lm("commits",(t,r)=>{Pt(t,r,"commits","view")}),Sy=lm("builds",(t,r)=>{sm(t,r,"builds","source"),Hn(t,r,"builds","failed_marker")}),ky=Qt("config",(t,r)=>{Pt(t,r,"config","cityName"),Pt(t,r,"config","cityRoot"),Hn(t,r,"config","useFixtures"),Hn(t,r,"config","readOnly"),Pt(t,r,"config","operatorAlias"),Pt(t,r,"config","operatorWireAlias"),Pt(t,r,"config","decisionLabel"),xy(t,r,"config","enabledModules"),sm(t,r,"config","defaultView")}),by=Qt("system health",(t,r)=>{Et(t,r,"system health","admin"),Et(t,r,"system health","host")});function Ml(t,r,i,s){Et(t,r,i,s);const u=t[s],p=`${i}.${s}`;Pt(u,r,p,"status")}const zy=Qt("local tool versions",(t,r)=>{Ml(t,r,"local tool versions","dolt"),Ml(t,r,"local tool versions","beads"),Ml(t,r,"local tool versions","gc")}),Cy=Qt("dolt trend",(t,r)=>{Hn(t,r,"dolt trend","available"),Nt(t,r,"dolt trend","samples")}),Ty=Qt("rig store health",(t,r)=>{Hn(t,r,"rig store health","available"),Nt(t,r,"rig store health","rigs")});function af(t,r){const i=Ra(t,r,"supervisor status.status");Et(i,r,"supervisor status.status","work")}const By=Qt("supervisor status",(t,r)=>{Hn(t,r,"supervisor status","available"),t.available===!0?(Pt(t,r,"supervisor status","sampledAt"),af(t.status,r)):(Pt(t,r,"supervisor status","reason"),t.status!==null&&af(t.status,r))}),Ry=Qt("run diff",(t,r)=>{Pt(t,r,"run diff","kind"),Et(t,r,"run diff","rootPath"),Et(t,r,"run diff","comparison"),Nt(t,r,"run diff","status"),Nt(t,r,"run diff","changedFiles"),Pt(t,r,"run diff","patch"),Hn(t,r,"run diff","truncated")}),Py=Qt("run summary",(t,r)=>{of(t,r,"run summary","totalActive"),of(t,r,"run summary","totalHistorical"),Nt(t,r,"run summary","lanes"),Nt(t,r,"run summary","historicalLanes"),Nt(t,r,"run summary","blockedLanes"),Nt(t,r,"run summary","recentChanges"),Et(t,r,"run summary","runCounts"),Et(t,r,"run summary","census")}),Ny=Qt("formula run detail",(t,r)=>{Pt(t,r,"formula run detail","runId"),Et(t,r,"formula run detail","formula"),Et(t,r,"formula run detail","formulaDetail"),Et(t,r,"formula run detail","executionPath"),Et(t,r,"formula run detail","snapshotEventSeq"),Et(t,r,"formula run detail","completeness");const i=Ra(t.progress,r,"formula run detail.progress");Et(i,r,"formula run detail.progress","statusCounts"),Nt(t,r,"formula run detail","stages"),Nt(t,r,"formula run detail","nodes"),Nt(t,r,"formula run detail","edges"),Nt(t,r,"formula run detail","lanes")});function Ay(t,r="request failed"){if(t instanceof im){const i={message:t.message,status:t.status};return t.kind!==void 0&&(i.kind=t.kind),i}return t instanceof Error?{message:t.message}:{message:r}}function Jt(t,r="request failed"){const i=Ay(t,r);return i.status===void 0?i.message:`${i.status} ${i.message}`}const Yr={health(){return Mt("GET","/api/health",Ey)},listCommits(t){return Mt("GET",`/api/git/commits?view=${encodeURIComponent(t)}`,Iy)},listBuilds(){return Mt("GET","/api/builds",Sy)},config(){return Mt("GET",dr("/config"),ky)},systemHealth(){return Mt("GET","/api/health/system",by)},localToolVersions(){return Mt("GET","/api/health/local-tools",zy)},doltTrend(){return Mt("GET",dr("/dolt-noms/trend"),Cy)},rigStoreHealth(){return Mt("GET",dr("/rig-store-health"),Ty)},supervisorStatus(){return Mt("GET",dr("/supervisor-status"),By)},runDiff(t,r,i){const s=Oy(i);return Mt("POST",dr(`/runs/${encodeURIComponent(t)}/diff${s}`),Ry,r)},runSummary(){return Mt("GET",dr("/runs/summary"),Py)},runDetail(t){return Mt("GET",dr(`/runs/${encodeURIComponent(t)}/detail`),Ny)}};function Oy(t){const r=new URLSearchParams;t?.scopeKind&&t.scopeRef&&(r.set("scope_kind",t.scopeKind),r.set("scope_ref",t.scopeRef));const i=r.toString();return i.length>0?`?${i}`:""}const Xo=["agents","beads","runs","mail","activity","health"],jy=5,$y=new Map(Xo.map((t,r)=>[t,r]));function Jl(t,r={}){const i=Ly(),s=[];let u=0;for(const y of t)for(const E of y.getItems()){s.push({item:E,index:u});const S=i[E.domain],T=[...S.items,E];i[E.domain]={domain:E.domain,attention:S.attention+(E.severity==="attention"?1:0),watch:S.watch+(E.severity==="watch"?1:0),unavailable:S.unavailable+(E.severity==="unavailable"?1:0),severity:E.severity==="unavailable"?S.severity:Dy(S.severity,E.severity),items:T},u+=1}const p=s.sort((y,E)=>My(y.item,E.item)||y.index-E.index).map(({item:y})=>y),d=r.topLimit??jy,m=p.slice(0,d),g=Fy(p.slice(d));return{items:p,topItems:m,overflowByDomain:g,byDomain:i}}function Ly(){const t={};for(const r of Xo)t[r]={domain:r,attention:0,watch:0,unavailable:0,severity:null,items:[]};return t}function Dy(t,r){return t==="attention"||r==="attention"?"attention":"watch"}function My(t,r){return sf(t.severity)-sf(r.severity)||ma(r.current??!0)-ma(t.current??!0)||ma(r.actionable??!1)-ma(t.actionable??!1)||lf(r.updatedAt)-lf(t.updatedAt)||uf(t.domain)-uf(r.domain)}function sf(t){switch(t){case"attention":return 0;case"watch":return 1;case"unavailable":return 2}}function ma(t){return t?1:0}function lf(t){if(t===void 0)return 0;const r=Date.parse(t);return Number.isFinite(r)?r:0}function uf(t){return $y.get(t)??Xo.length}function Fy(t){const r=[];for(const i of Xo){let s=0,u=0,p=0;for(const m of t)m.domain===i&&(m.severity==="attention"?s+=1:m.severity==="watch"?u+=1:p+=1);const d=s+u+p;d>0&&r.push({domain:i,attention:s,watch:u,unavailable:p,total:d})}return r}const Uy=Jl([]),um=b.createContext(Uy);function Zy({contributors:t,topLimit:r,children:i}){const s=b.useMemo(()=>r===void 0?Jl(t):Jl(t,{topLimit:r}),[t,r]);return $.jsx(um.Provider,{value:s,children:i})}function qy(){return b.useContext(um)}const cu=new Map;function Fl(t){return cu.get(t)?.value}function va(t){return cu.get(t)?.fetchedAt}function Vy(t,r){cu.set(t,{value:r,fetchedAt:new Date().toISOString()})}function mn(t,r,i){const s=b.useRef(r);s.current=r;const u=b.useRef(i?.refreshFetcher);u.current=i?.refreshFetcher;const p=b.useRef(i?.sseRefreshFetcher);p.current=i?.sseRefreshFetcher;const d=b.useRef(i?.onError);d.current=i?.onError;const m=b.useRef(t);m.current=t;const g=b.useRef(0),[y,E]=b.useState(()=>Fl(t)),[S,T]=b.useState(()=>Fl(t)===void 0),[A,D]=b.useState(null),[W,O]=b.useState(()=>va(t)),H=b.useCallback(async G=>{const ee=g.current+1;g.current=ee;const ue=t;T(!0),D(null);try{const de=await G(),pe=g.current===ee,Re=m.current===ue;pe&&Re?(Vy(ue,de),E(de),O(va(ue))):Re&&(E(ye=>ye===void 0?de:ye),O(ye=>ye??va(ue)??new Date().toISOString()))}catch(de){g.current===ee&&(D(de instanceof Error?de.message:"failed to load"),d.current?.(de))}finally{g.current===ee&&T(!1)}},[t]),oe=b.useCallback(()=>H(u.current??s.current),[H]),Q=b.useCallback(()=>H(p.current??u.current??s.current),[H]);return b.useEffect(()=>{const G=Fl(t);return E(G),T(G===void 0),O(va(t)),H(s.current),()=>{g.current+=1}},[t,H]),{data:y,loading:S,error:A,fetchedAt:W,refresh:oe,cheapRefresh:Q}}var Wy=async(t,r)=>{let i=typeof r=="function"?await r(t):r;if(i)return t.scheme==="bearer"?`Bearer ${i}`:t.scheme==="basic"?`Basic ${btoa(i)}`:i},Hy={bodySerializer:t=>JSON.stringify(t,(r,i)=>typeof i=="bigint"?i.toString():i)},Gy=t=>{switch(t){case"label":return".";case"matrix":return";";case"simple":return",";default:return"&"}},Jy=t=>{switch(t){case"form":return",";case"pipeDelimited":return"|";case"spaceDelimited":return"%20";default:return","}},Ky=t=>{switch(t){case"label":return".";case"matrix":return";";case"simple":return",";default:return"&"}},cm=({allowReserved:t,explode:r,name:i,style:s,value:u})=>{if(!r){let m=(t?u:u.map(g=>encodeURIComponent(g))).join(Jy(s));switch(s){case"label":return`.${m}`;case"matrix":return`;${i}=${m}`;case"simple":return m;default:return`${i}=${m}`}}let p=Gy(s),d=u.map(m=>s==="label"||s==="simple"?t?m:encodeURIComponent(m):Pa({allowReserved:t,name:i,value:m})).join(p);return s==="label"||s==="matrix"?p+d:d},Pa=({allowReserved:t,name:r,value:i})=>{if(i==null)return"";if(typeof i=="object")throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");return`${r}=${t?i:encodeURIComponent(i)}`},dm=({allowReserved:t,explode:r,name:i,style:s,value:u,valueOnly:p})=>{if(u instanceof Date)return p?u.toISOString():`${i}=${u.toISOString()}`;if(s!=="deepObject"&&!r){let g=[];Object.entries(u).forEach(([E,S])=>{g=[...g,E,t?S:encodeURIComponent(S)]});let y=g.join(",");switch(s){case"form":return`${i}=${y}`;case"label":return`.${y}`;case"matrix":return`;${i}=${y}`;default:return y}}let d=Ky(s),m=Object.entries(u).map(([g,y])=>Pa({allowReserved:t,name:s==="deepObject"?`${i}[${g}]`:g,value:y})).join(d);return s==="label"||s==="matrix"?d+m:m},Qy=/\{[^{}]+\}/g,Yy=({path:t,url:r})=>{let i=r,s=r.match(Qy);if(s)for(let u of s){let p=!1,d=u.substring(1,u.length-1),m="simple";d.endsWith("*")&&(p=!0,d=d.substring(0,d.length-1)),d.startsWith(".")?(d=d.substring(1),m="label"):d.startsWith(";")&&(d=d.substring(1),m="matrix");let g=t[d];if(g==null)continue;if(Array.isArray(g)){i=i.replace(u,cm({explode:p,name:d,style:m,value:g}));continue}if(typeof g=="object"){i=i.replace(u,dm({explode:p,name:d,style:m,value:g,valueOnly:!0}));continue}if(m==="matrix"){i=i.replace(u,`;${Pa({name:d,value:g})}`);continue}let y=encodeURIComponent(m==="label"?`.${g}`:g);i=i.replace(u,y)}return i},pm=({allowReserved:t,array:r,object:i}={})=>s=>{let u=[];if(s&&typeof s=="object")for(let p in s){let d=s[p];if(d!=null)if(Array.isArray(d)){let m=cm({allowReserved:t,explode:!0,name:p,style:"form",value:d,...r});m&&u.push(m)}else if(typeof d=="object"){let m=dm({allowReserved:t,explode:!0,name:p,style:"deepObject",value:d,...i});m&&u.push(m)}else{let m=Pa({allowReserved:t,name:p,value:d});m&&u.push(m)}}return u.join("&")},Xy=t=>{if(!t)return"stream";let r=t.split(";")[0]?.trim();if(r){if(r.startsWith("application/json")||r.endsWith("+json"))return"json";if(r==="multipart/form-data")return"formData";if(["application/","audio/","image/","video/"].some(i=>r.startsWith(i)))return"blob";if(r.startsWith("text/"))return"text"}},e7=async({security:t,...r})=>{for(let i of t){let s=await Wy(i,r.auth);if(!s)continue;let u=i.name??"Authorization";switch(i.in){case"query":r.query||(r.query={}),r.query[u]=s;break;case"cookie":r.headers.append("Cookie",`${u}=${s}`);break;default:r.headers.set(u,s);break}return}},cf=t=>t7({baseUrl:t.baseUrl,path:t.path,query:t.query,querySerializer:typeof t.querySerializer=="function"?t.querySerializer:pm(t.querySerializer),url:t.url}),t7=({baseUrl:t,path:r,query:i,querySerializer:s,url:u})=>{let p=u.startsWith("/")?u:`/${u}`,d=(t??"")+p;r&&(d=Yy({path:r,url:d}));let m=i?s(i):"";return m.startsWith("?")&&(m=m.substring(1)),m&&(d+=`?${m}`),d},df=(t,r)=>{let i={...t,...r};return i.baseUrl?.endsWith("/")&&(i.baseUrl=i.baseUrl.substring(0,i.baseUrl.length-1)),i.headers=fm(t.headers,r.headers),i},fm=(...t)=>{let r=new Headers;for(let i of t){if(!i||typeof i!="object")continue;let s=i instanceof Headers?i.entries():Object.entries(i);for(let[u,p]of s)if(p===null)r.delete(u);else if(Array.isArray(p))for(let d of p)r.append(u,d);else p!==void 0&&r.set(u,typeof p=="object"?JSON.stringify(p):p)}return r},Ul=class{_fns;constructor(){this._fns=[]}clear(){this._fns=[]}getInterceptorIndex(t){return typeof t=="number"?this._fns[t]?t:-1:this._fns.indexOf(t)}exists(t){let r=this.getInterceptorIndex(t);return!!this._fns[r]}eject(t){let r=this.getInterceptorIndex(t);this._fns[r]&&(this._fns[r]=null)}update(t,r){let i=this.getInterceptorIndex(t);return this._fns[i]?(this._fns[i]=r,t):!1}use(t){return this._fns=[...this._fns,t],this._fns.length-1}},n7=()=>({error:new Ul,request:new Ul,response:new Ul}),r7=pm({allowReserved:!1,array:{explode:!0,style:"form"},object:{explode:!0,style:"deepObject"}}),o7={"Content-Type":"application/json"},mm=(t={})=>({...Hy,headers:o7,parseAs:"auto",querySerializer:r7,...t}),vm=(t={})=>{let r=df(mm(),t),i=()=>({...r}),s=d=>(r=df(r,d),i()),u=n7(),p=async d=>{let m={...r,...d,fetch:d.fetch??r.fetch??globalThis.fetch,headers:fm(r.headers,d.headers)};m.security&&await e7({...m,security:m.security}),m.body&&m.bodySerializer&&(m.body=m.bodySerializer(m.body)),(m.body===void 0||m.body==="")&&m.headers.delete("Content-Type");let g=cf(m),y={redirect:"follow",...m},E=new Request(g,y);for(let O of u.request._fns)O&&(E=await O(E,m));let S=m.fetch,T=await S(E);for(let O of u.response._fns)O&&(T=await O(T,E,m));let A={request:E,response:T};if(T.ok){if(T.status===204||T.headers.get("Content-Length")==="0")return m.responseStyle==="data"?{}:{data:{},...A};let O=(m.parseAs==="auto"?Xy(T.headers.get("Content-Type")):m.parseAs)??"json";if(O==="stream")return m.responseStyle==="data"?T.body:{data:T.body,...A};let H=await T[O]();return O==="json"&&(m.responseValidator&&await m.responseValidator(H),m.responseTransformer&&(H=await m.responseTransformer(H))),m.responseStyle==="data"?H:{data:H,...A}}let D=await T.text();try{D=JSON.parse(D)}catch{}let W=D;for(let O of u.error._fns)O&&(W=await O(D,T,E,m));if(W=W||{},m.throwOnError)throw W;return m.responseStyle==="data"?void 0:{error:W,...A}};return{buildUrl:cf,connect:d=>p({...d,method:"CONNECT"}),delete:d=>p({...d,method:"DELETE"}),get:d=>p({...d,method:"GET"}),getConfig:i,head:d=>p({...d,method:"HEAD"}),interceptors:u,options:d=>p({...d,method:"OPTIONS"}),patch:d=>p({...d,method:"PATCH"}),post:d=>p({...d,method:"POST"}),put:d=>p({...d,method:"PUT"}),request:p,setConfig:s,trace:d=>p({...d,method:"TRACE"})}};const Se=vm(mm()),i7=t=>(t?.client??Se).get({url:"/health",...t}),a7=t=>(t?.client??Se).get({url:"/v0/cities",...t}),s7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/agent/{base}/prime",...t}),l7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/agent/{base}/{action}",...t}),u7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/agent/{dir}/{base}/prime",...t}),c7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/agent/{dir}/{base}/{action}",...t}),d7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/agents",...t}),p7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/bead/{id}",...t}),f7=t=>(t.client??Se).patch({url:"/v0/city/{cityName}/bead/{id}",...t,headers:{"Content-Type":"application/json",...t.headers}}),m7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/bead/{id}/close",...t,headers:{"Content-Type":"application/json",...t.headers}}),v7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/beads",...t}),h7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/beads",...t,headers:{"Content-Type":"application/json",...t.headers}}),g7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/events",...t}),y7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/formulas/feed",...t}),_7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/formulas/{name}",...t}),w7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/health",...t}),x7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/mail",...t}),E7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/mail",...t,headers:{"Content-Type":"application/json",...t.headers}}),I7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/mail/thread/{id}",...t}),S7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/archive",...t}),k7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/mark-unread",...t}),b7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/read",...t}),z7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/reply",...t,headers:{"Content-Type":"application/json",...t.headers}}),C7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/rigs",...t}),T7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/session/{id}/pending",...t}),B7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/session/{id}/respond",...t,headers:{"Content-Type":"application/json",...t.headers}}),R7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/session/{id}/transcript",...t}),P7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/sessions",...t}),N7=t=>(t.client??Se).post({url:"/v0/city/{cityName}/sling",...t,headers:{"Content-Type":"application/json",...t.headers}}),A7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/status",...t}),O7=t=>(t.client??Se).get({url:"/v0/city/{cityName}/workflow/{workflow_id}",...t});var pf;function j(t,r,i){function s(m,g){if(m._zod||Object.defineProperty(m,"_zod",{value:{def:g,constr:d,traits:new Set},enumerable:!1}),m._zod.traits.has(t))return;m._zod.traits.add(t),r(m,g);const y=d.prototype,E=Object.keys(y);for(let S=0;Si?.Parent&&m instanceof i.Parent?!0:m?._zod?.traits?.has(t)}),Object.defineProperty(d,"name",{value:t}),d}class Hr extends Error{constructor(){super("Encountered Promise during synchronous parse. Use .parseAsync() instead.")}}class hm extends Error{constructor(r){super(`Encountered unidirectional transform during encode: ${r}`),this.name="ZodEncodeError"}}(pf=globalThis).__zod_globalConfig??(pf.__zod_globalConfig={});const du=globalThis.__zod_globalConfig;function vn(t){return du}function gm(t){const r=Object.values(t).filter(s=>typeof s=="number");return Object.entries(t).filter(([s,u])=>r.indexOf(+s)===-1).map(([s,u])=>u)}function Kl(t,r){return typeof r=="bigint"?r.toString():r}function Na(t){return{get value(){{const r=t();return Object.defineProperty(this,"value",{value:r}),r}}}}function pu(t){return t==null}function fu(t){const r=t.startsWith("^")?1:0,i=t.endsWith("$")?t.length-1:t.length;return t.slice(r,i)}function j7(t,r){const i=t/r,s=Math.round(i),u=Number.EPSILON*Math.max(Math.abs(i),1);return Math.abs(i-s){};function Qo(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}const L7=Na(()=>{if(du.jitless||typeof navigator<"u"&&navigator?.userAgent?.includes("Cloudflare"))return!1;try{const t=Function;return new t(""),!0}catch{return!1}});function Xr(t){if(Qo(t)===!1)return!1;const r=t.constructor;if(r===void 0||typeof r!="function")return!0;const i=r.prototype;return!(Qo(i)===!1||Object.prototype.hasOwnProperty.call(i,"isPrototypeOf")===!1)}function _m(t){return Xr(t)?{...t}:Array.isArray(t)?[...t]:t instanceof Map?new Map(t):t instanceof Set?new Set(t):t}const D7=new Set(["string","number","symbol"]);function eo(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Kn(t,r,i){const s=new t._zod.constr(r??t._zod.def);return(!r||i?.parent)&&(s._zod.parent=t),s}function ie(t){const r=t;if(!r)return{};if(typeof r=="string")return{error:()=>r};if(r?.message!==void 0){if(r?.error!==void 0)throw new Error("Cannot specify both `message` and `error` params");r.error=r.message}return delete r.message,typeof r.error=="string"?{...r,error:()=>r.error}:r}function M7(t){return Object.keys(t).filter(r=>t[r]._zod.optin==="optional"&&t[r]._zod.optout==="optional")}const F7={safeint:[Number.MIN_SAFE_INTEGER,Number.MAX_SAFE_INTEGER],int32:[-2147483648,2147483647],uint32:[0,4294967295],float32:[-34028234663852886e22,34028234663852886e22],float64:[-Number.MAX_VALUE,Number.MAX_VALUE]};function U7(t,r){const i=t._zod.def,s=i.checks;if(s&&s.length>0)throw new Error(".pick() cannot be used on object schemas containing refinements");const p=Jn(t._zod.def,{get shape(){const d={};for(const m in r){if(!(m in i.shape))throw new Error(`Unrecognized key: "${m}"`);r[m]&&(d[m]=i.shape[m])}return hr(this,"shape",d),d},checks:[]});return Kn(t,p)}function Z7(t,r){const i=t._zod.def,s=i.checks;if(s&&s.length>0)throw new Error(".omit() cannot be used on object schemas containing refinements");const p=Jn(t._zod.def,{get shape(){const d={...t._zod.def.shape};for(const m in r){if(!(m in i.shape))throw new Error(`Unrecognized key: "${m}"`);r[m]&&delete d[m]}return hr(this,"shape",d),d},checks:[]});return Kn(t,p)}function q7(t,r){if(!Xr(r))throw new Error("Invalid input to extend: expected a plain object");const i=t._zod.def.checks;if(i&&i.length>0){const p=t._zod.def.shape;for(const d in r)if(Object.getOwnPropertyDescriptor(p,d)!==void 0)throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.")}const u=Jn(t._zod.def,{get shape(){const p={...t._zod.def.shape,...r};return hr(this,"shape",p),p}});return Kn(t,u)}function V7(t,r){if(!Xr(r))throw new Error("Invalid input to safeExtend: expected a plain object");const i=Jn(t._zod.def,{get shape(){const s={...t._zod.def.shape,...r};return hr(this,"shape",s),s}});return Kn(t,i)}function W7(t,r){if(t._zod.def.checks?.length)throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");const i=Jn(t._zod.def,{get shape(){const s={...t._zod.def.shape,...r._zod.def.shape};return hr(this,"shape",s),s},get catchall(){return r._zod.def.catchall},checks:r._zod.def.checks??[]});return Kn(t,i)}function H7(t,r,i){const u=r._zod.def.checks;if(u&&u.length>0)throw new Error(".partial() cannot be used on object schemas containing refinements");const d=Jn(r._zod.def,{get shape(){const m=r._zod.def.shape,g={...m};if(i)for(const y in i){if(!(y in m))throw new Error(`Unrecognized key: "${y}"`);i[y]&&(g[y]=t?new t({type:"optional",innerType:m[y]}):m[y])}else for(const y in m)g[y]=t?new t({type:"optional",innerType:m[y]}):m[y];return hr(this,"shape",g),g},checks:[]});return Kn(r,d)}function G7(t,r,i){const s=Jn(r._zod.def,{get shape(){const u=r._zod.def.shape,p={...u};if(i)for(const d in i){if(!(d in p))throw new Error(`Unrecognized key: "${d}"`);i[d]&&(p[d]=new t({type:"nonoptional",innerType:u[d]}))}else for(const d in u)p[d]=new t({type:"nonoptional",innerType:u[d]});return hr(this,"shape",p),p}});return Kn(r,s)}function qr(t,r=0){if(t.aborted===!0)return!0;for(let i=r;i{var s;return(s=i).path??(s.path=[]),i.path.unshift(t),i})}function ha(t){return typeof t=="string"?t:t?.message}function hn(t,r,i){const s=t.message?t.message:ha(t.inst?._zod.def?.error?.(t))??ha(r?.error?.(t))??ha(i.customError?.(t))??ha(i.localeError?.(t))??"Invalid input",{inst:u,continue:p,input:d,...m}=t;return m.path??(m.path=[]),m.message=s,r?.reportInput&&(m.input=d),m}function mu(t){return Array.isArray(t)?"array":typeof t=="string"?"string":"unknown"}function Yo(...t){const[r,i,s]=t;return typeof r=="string"?{message:r,code:"custom",input:i,inst:s}:{...r}}const wm=(t,r)=>{t.name="$ZodError",Object.defineProperty(t,"_zod",{value:t._zod,enumerable:!1}),Object.defineProperty(t,"issues",{value:r,enumerable:!1}),t.message=JSON.stringify(r,Kl,2),Object.defineProperty(t,"toString",{value:()=>t.message,enumerable:!1})},xm=j("$ZodError",wm),Em=j("$ZodError",wm,{Parent:Error});function K7(t,r=i=>i.message){const i={},s=[];for(const u of t.issues)u.path.length>0?(i[u.path[0]]=i[u.path[0]]||[],i[u.path[0]].push(r(u))):s.push(r(u));return{formErrors:s,fieldErrors:i}}function Q7(t,r=i=>i.message){const i={_errors:[]},s=(u,p=[])=>{for(const d of u.issues)if(d.code==="invalid_union"&&d.errors.length)d.errors.map(m=>s({issues:m},[...p,...d.path]));else if(d.code==="invalid_key")s({issues:d.issues},[...p,...d.path]);else if(d.code==="invalid_element")s({issues:d.issues},[...p,...d.path]);else{const m=[...p,...d.path];if(m.length===0)i._errors.push(r(d));else{let g=i,y=0;for(;y(r,i,s,u)=>{const p=s?{...s,async:!1}:{async:!1},d=r._zod.run({value:i,issues:[]},p);if(d instanceof Promise)throw new Hr;if(d.issues.length){const m=new(u?.Err??t)(d.issues.map(g=>hn(g,p,vn())));throw ym(m,u?.callee),m}return d.value},hu=t=>async(r,i,s,u)=>{const p=s?{...s,async:!0}:{async:!0};let d=r._zod.run({value:i,issues:[]},p);if(d instanceof Promise&&(d=await d),d.issues.length){const m=new(u?.Err??t)(d.issues.map(g=>hn(g,p,vn())));throw ym(m,u?.callee),m}return d.value},Aa=t=>(r,i,s)=>{const u=s?{...s,async:!1}:{async:!1},p=r._zod.run({value:i,issues:[]},u);if(p instanceof Promise)throw new Hr;return p.issues.length?{success:!1,error:new(t??xm)(p.issues.map(d=>hn(d,u,vn())))}:{success:!0,data:p.value}},Y7=Aa(Em),Oa=t=>async(r,i,s)=>{const u=s?{...s,async:!0}:{async:!0};let p=r._zod.run({value:i,issues:[]},u);return p instanceof Promise&&(p=await p),p.issues.length?{success:!1,error:new t(p.issues.map(d=>hn(d,u,vn())))}:{success:!0,data:p.value}},X7=Oa(Em),e2=t=>(r,i,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return vu(t)(r,i,u)},t2=t=>(r,i,s)=>vu(t)(r,i,s),n2=t=>async(r,i,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return hu(t)(r,i,u)},r2=t=>async(r,i,s)=>hu(t)(r,i,s),o2=t=>(r,i,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return Aa(t)(r,i,u)},i2=t=>(r,i,s)=>Aa(t)(r,i,s),a2=t=>async(r,i,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return Oa(t)(r,i,u)},s2=t=>async(r,i,s)=>Oa(t)(r,i,s),l2=/^[cC][0-9a-z]{6,}$/,u2=/^[0-9a-z]+$/,c2=/^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/,d2=/^[0-9a-vA-V]{20}$/,p2=/^[A-Za-z0-9]{27}$/,f2=/^[a-zA-Z0-9_-]{21}$/,m2=/^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/,v2=/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/,vf=t=>t?new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${t}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`):/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/,h2=/^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/,g2="^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$";function y2(){return new RegExp(g2,"u")}const _2=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,w2=/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/,x2=/^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/,E2=/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,I2=/^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/,Im=/^[A-Za-z0-9_-]*$/,S2=/^https?$/,k2=/^\+[1-9]\d{6,14}$/,Sm="(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))",b2=new RegExp(`^${Sm}$`);function km(t){const r="(?:[01]\\d|2[0-3]):[0-5]\\d";return typeof t.precision=="number"?t.precision===-1?`${r}`:t.precision===0?`${r}:[0-5]\\d`:`${r}:[0-5]\\d\\.\\d{${t.precision}}`:`${r}(?::[0-5]\\d(?:\\.\\d+)?)?`}function z2(t){return new RegExp(`^${km(t)}$`)}function C2(t){const r=km({precision:t.precision}),i=["Z"];t.local&&i.push(""),t.offset&&i.push("([+-](?:[01]\\d|2[0-3]):[0-5]\\d)");const s=`${r}(?:${i.join("|")})`;return new RegExp(`^${Sm}T(?:${s})$`)}const T2=t=>{const r=t?`[\\s\\S]{${t?.minimum??0},${t?.maximum??""}}`:"[\\s\\S]*";return new RegExp(`^${r}$`)},B2=/^-?\d+n?$/,R2=/^-?\d+$/,bm=/^-?\d+(?:\.\d+)?$/,P2=/^(?:true|false)$/i,N2=/^[^A-Z]*$/,A2=/^[^a-z]*$/,St=j("$ZodCheck",(t,r)=>{var i;t._zod??(t._zod={}),t._zod.def=r,(i=t._zod).onattach??(i.onattach=[])}),zm={number:"number",bigint:"bigint",object:"date"},Cm=j("$ZodCheckLessThan",(t,r)=>{St.init(t,r);const i=zm[typeof r.value];t._zod.onattach.push(s=>{const u=s._zod.bag,p=(r.inclusive?u.maximum:u.exclusiveMaximum)??Number.POSITIVE_INFINITY;r.value{(r.inclusive?s.value<=r.value:s.value{St.init(t,r);const i=zm[typeof r.value];t._zod.onattach.push(s=>{const u=s._zod.bag,p=(r.inclusive?u.minimum:u.exclusiveMinimum)??Number.NEGATIVE_INFINITY;r.value>p&&(r.inclusive?u.minimum=r.value:u.exclusiveMinimum=r.value)}),t._zod.check=s=>{(r.inclusive?s.value>=r.value:s.value>r.value)||s.issues.push({origin:i,code:"too_small",minimum:typeof r.value=="object"?r.value.getTime():r.value,input:s.value,inclusive:r.inclusive,inst:t,continue:!r.abort})}}),O2=j("$ZodCheckMultipleOf",(t,r)=>{St.init(t,r),t._zod.onattach.push(i=>{var s;(s=i._zod.bag).multipleOf??(s.multipleOf=r.value)}),t._zod.check=i=>{if(typeof i.value!=typeof r.value)throw new Error("Cannot mix number and bigint in multiple_of check.");(typeof i.value=="bigint"?i.value%r.value===BigInt(0):j7(i.value,r.value)===0)||i.issues.push({origin:typeof i.value,code:"not_multiple_of",divisor:r.value,input:i.value,inst:t,continue:!r.abort})}}),j2=j("$ZodCheckNumberFormat",(t,r)=>{St.init(t,r),r.format=r.format||"float64";const i=r.format?.includes("int"),s=i?"int":"number",[u,p]=F7[r.format];t._zod.onattach.push(d=>{const m=d._zod.bag;m.format=r.format,m.minimum=u,m.maximum=p,i&&(m.pattern=R2)}),t._zod.check=d=>{const m=d.value;if(i){if(!Number.isInteger(m)){d.issues.push({expected:s,format:r.format,code:"invalid_type",continue:!1,input:m,inst:t});return}if(!Number.isSafeInteger(m)){m>0?d.issues.push({input:m,code:"too_big",maximum:Number.MAX_SAFE_INTEGER,note:"Integers must be within the safe integer range.",inst:t,origin:s,inclusive:!0,continue:!r.abort}):d.issues.push({input:m,code:"too_small",minimum:Number.MIN_SAFE_INTEGER,note:"Integers must be within the safe integer range.",inst:t,origin:s,inclusive:!0,continue:!r.abort});return}}mp&&d.issues.push({origin:"number",input:m,code:"too_big",maximum:p,inclusive:!0,inst:t,continue:!r.abort})}}),$2=j("$ZodCheckMaxLength",(t,r)=>{var i;St.init(t,r),(i=t._zod.def).when??(i.when=s=>{const u=s.value;return!pu(u)&&u.length!==void 0}),t._zod.onattach.push(s=>{const u=s._zod.bag.maximum??Number.POSITIVE_INFINITY;r.maximum{const u=s.value;if(u.length<=r.maximum)return;const d=mu(u);s.issues.push({origin:d,code:"too_big",maximum:r.maximum,inclusive:!0,input:u,inst:t,continue:!r.abort})}}),L2=j("$ZodCheckMinLength",(t,r)=>{var i;St.init(t,r),(i=t._zod.def).when??(i.when=s=>{const u=s.value;return!pu(u)&&u.length!==void 0}),t._zod.onattach.push(s=>{const u=s._zod.bag.minimum??Number.NEGATIVE_INFINITY;r.minimum>u&&(s._zod.bag.minimum=r.minimum)}),t._zod.check=s=>{const u=s.value;if(u.length>=r.minimum)return;const d=mu(u);s.issues.push({origin:d,code:"too_small",minimum:r.minimum,inclusive:!0,input:u,inst:t,continue:!r.abort})}}),D2=j("$ZodCheckLengthEquals",(t,r)=>{var i;St.init(t,r),(i=t._zod.def).when??(i.when=s=>{const u=s.value;return!pu(u)&&u.length!==void 0}),t._zod.onattach.push(s=>{const u=s._zod.bag;u.minimum=r.length,u.maximum=r.length,u.length=r.length}),t._zod.check=s=>{const u=s.value,p=u.length;if(p===r.length)return;const d=mu(u),m=p>r.length;s.issues.push({origin:d,...m?{code:"too_big",maximum:r.length}:{code:"too_small",minimum:r.length},inclusive:!0,exact:!0,input:s.value,inst:t,continue:!r.abort})}}),ja=j("$ZodCheckStringFormat",(t,r)=>{var i,s;St.init(t,r),t._zod.onattach.push(u=>{const p=u._zod.bag;p.format=r.format,r.pattern&&(p.patterns??(p.patterns=new Set),p.patterns.add(r.pattern))}),r.pattern?(i=t._zod).check??(i.check=u=>{r.pattern.lastIndex=0,!r.pattern.test(u.value)&&u.issues.push({origin:"string",code:"invalid_format",format:r.format,input:u.value,...r.pattern?{pattern:r.pattern.toString()}:{},inst:t,continue:!r.abort})}):(s=t._zod).check??(s.check=()=>{})}),M2=j("$ZodCheckRegex",(t,r)=>{ja.init(t,r),t._zod.check=i=>{r.pattern.lastIndex=0,!r.pattern.test(i.value)&&i.issues.push({origin:"string",code:"invalid_format",format:"regex",input:i.value,pattern:r.pattern.toString(),inst:t,continue:!r.abort})}}),F2=j("$ZodCheckLowerCase",(t,r)=>{r.pattern??(r.pattern=N2),ja.init(t,r)}),U2=j("$ZodCheckUpperCase",(t,r)=>{r.pattern??(r.pattern=A2),ja.init(t,r)}),Z2=j("$ZodCheckIncludes",(t,r)=>{St.init(t,r);const i=eo(r.includes),s=new RegExp(typeof r.position=="number"?`^.{${r.position}}${i}`:i);r.pattern=s,t._zod.onattach.push(u=>{const p=u._zod.bag;p.patterns??(p.patterns=new Set),p.patterns.add(s)}),t._zod.check=u=>{u.value.includes(r.includes,r.position)||u.issues.push({origin:"string",code:"invalid_format",format:"includes",includes:r.includes,input:u.value,inst:t,continue:!r.abort})}}),q2=j("$ZodCheckStartsWith",(t,r)=>{St.init(t,r);const i=new RegExp(`^${eo(r.prefix)}.*`);r.pattern??(r.pattern=i),t._zod.onattach.push(s=>{const u=s._zod.bag;u.patterns??(u.patterns=new Set),u.patterns.add(i)}),t._zod.check=s=>{s.value.startsWith(r.prefix)||s.issues.push({origin:"string",code:"invalid_format",format:"starts_with",prefix:r.prefix,input:s.value,inst:t,continue:!r.abort})}}),V2=j("$ZodCheckEndsWith",(t,r)=>{St.init(t,r);const i=new RegExp(`.*${eo(r.suffix)}$`);r.pattern??(r.pattern=i),t._zod.onattach.push(s=>{const u=s._zod.bag;u.patterns??(u.patterns=new Set),u.patterns.add(i)}),t._zod.check=s=>{s.value.endsWith(r.suffix)||s.issues.push({origin:"string",code:"invalid_format",format:"ends_with",suffix:r.suffix,input:s.value,inst:t,continue:!r.abort})}}),W2=j("$ZodCheckOverwrite",(t,r)=>{St.init(t,r),t._zod.check=i=>{i.value=r.tx(i.value)}});class H2{constructor(r=[]){this.content=[],this.indent=0,this&&(this.args=r)}indented(r){this.indent+=1,r(this),this.indent-=1}write(r){if(typeof r=="function"){r(this,{execution:"sync"}),r(this,{execution:"async"});return}const s=r.split(` +`).filter(d=>d),u=Math.min(...s.map(d=>d.length-d.trimStart().length)),p=s.map(d=>d.slice(u)).map(d=>" ".repeat(this.indent*2)+d);for(const d of p)this.content.push(d)}compile(){const r=Function,i=this?.args,u=[...(this?.content??[""]).map(p=>` ${p}`)];return new r(...i,u.join(` +`))}}const G2={major:4,minor:4,patch:3},je=j("$ZodType",(t,r)=>{var i;t??(t={}),t._zod.def=r,t._zod.bag=t._zod.bag||{},t._zod.version=G2;const s=[...t._zod.def.checks??[]];t._zod.traits.has("$ZodCheck")&&s.unshift(t);for(const u of s)for(const p of u._zod.onattach)p(t);if(s.length===0)(i=t._zod).deferred??(i.deferred=[]),t._zod.deferred?.push(()=>{t._zod.run=t._zod.parse});else{const u=(d,m,g)=>{let y=qr(d),E;for(const S of m){if(S._zod.def.when){if(J7(d)||!S._zod.def.when(d))continue}else if(y)continue;const T=d.issues.length,A=S._zod.check(d);if(A instanceof Promise&&g?.async===!1)throw new Hr;if(E||A instanceof Promise)E=(E??Promise.resolve()).then(async()=>{await A,d.issues.length!==T&&(y||(y=qr(d,T)))});else{if(d.issues.length===T)continue;y||(y=qr(d,T))}}return E?E.then(()=>d):d},p=(d,m,g)=>{if(qr(d))return d.aborted=!0,d;const y=u(m,s,g);if(y instanceof Promise){if(g.async===!1)throw new Hr;return y.then(E=>t._zod.parse(E,g))}return t._zod.parse(y,g)};t._zod.run=(d,m)=>{if(m.skipChecks)return t._zod.parse(d,m);if(m.direction==="backward"){const y=t._zod.parse({value:d.value,issues:[]},{...m,skipChecks:!0});return y instanceof Promise?y.then(E=>p(E,d,m)):p(y,d,m)}const g=t._zod.parse(d,m);if(g instanceof Promise){if(m.async===!1)throw new Hr;return g.then(y=>u(y,s,m))}return u(g,s,m)}}ze(t,"~standard",()=>({validate:u=>{try{const p=Y7(t,u);return p.success?{value:p.data}:{issues:p.error?.issues}}catch{return X7(t,u).then(d=>d.success?{value:d.data}:{issues:d.error?.issues})}},vendor:"zod",version:1}))}),gu=j("$ZodString",(t,r)=>{je.init(t,r),t._zod.pattern=[...t?._zod.bag?.patterns??[]].pop()??T2(t._zod.bag),t._zod.parse=(i,s)=>{if(r.coerce)try{i.value=String(i.value)}catch{}return typeof i.value=="string"||i.issues.push({expected:"string",code:"invalid_type",input:i.value,inst:t}),i}}),$e=j("$ZodStringFormat",(t,r)=>{ja.init(t,r),gu.init(t,r)}),J2=j("$ZodGUID",(t,r)=>{r.pattern??(r.pattern=v2),$e.init(t,r)}),K2=j("$ZodUUID",(t,r)=>{if(r.version){const s={v1:1,v2:2,v3:3,v4:4,v5:5,v6:6,v7:7,v8:8}[r.version];if(s===void 0)throw new Error(`Invalid UUID version: "${r.version}"`);r.pattern??(r.pattern=vf(s))}else r.pattern??(r.pattern=vf());$e.init(t,r)}),Q2=j("$ZodEmail",(t,r)=>{r.pattern??(r.pattern=h2),$e.init(t,r)}),Y2=j("$ZodURL",(t,r)=>{$e.init(t,r),t._zod.check=i=>{try{const s=i.value.trim();if(!r.normalize&&r.protocol?.source===S2.source&&!/^https?:\/\//i.test(s)){i.issues.push({code:"invalid_format",format:"url",note:"Invalid URL format",input:i.value,inst:t,continue:!r.abort});return}const u=new URL(s);r.hostname&&(r.hostname.lastIndex=0,r.hostname.test(u.hostname)||i.issues.push({code:"invalid_format",format:"url",note:"Invalid hostname",pattern:r.hostname.source,input:i.value,inst:t,continue:!r.abort})),r.protocol&&(r.protocol.lastIndex=0,r.protocol.test(u.protocol.endsWith(":")?u.protocol.slice(0,-1):u.protocol)||i.issues.push({code:"invalid_format",format:"url",note:"Invalid protocol",pattern:r.protocol.source,input:i.value,inst:t,continue:!r.abort})),r.normalize?i.value=u.href:i.value=s;return}catch{i.issues.push({code:"invalid_format",format:"url",input:i.value,inst:t,continue:!r.abort})}}}),X2=j("$ZodEmoji",(t,r)=>{r.pattern??(r.pattern=y2()),$e.init(t,r)}),e3=j("$ZodNanoID",(t,r)=>{r.pattern??(r.pattern=f2),$e.init(t,r)}),t3=j("$ZodCUID",(t,r)=>{r.pattern??(r.pattern=l2),$e.init(t,r)}),n3=j("$ZodCUID2",(t,r)=>{r.pattern??(r.pattern=u2),$e.init(t,r)}),r3=j("$ZodULID",(t,r)=>{r.pattern??(r.pattern=c2),$e.init(t,r)}),o3=j("$ZodXID",(t,r)=>{r.pattern??(r.pattern=d2),$e.init(t,r)}),i3=j("$ZodKSUID",(t,r)=>{r.pattern??(r.pattern=p2),$e.init(t,r)}),a3=j("$ZodISODateTime",(t,r)=>{r.pattern??(r.pattern=C2(r)),$e.init(t,r)}),s3=j("$ZodISODate",(t,r)=>{r.pattern??(r.pattern=b2),$e.init(t,r)}),l3=j("$ZodISOTime",(t,r)=>{r.pattern??(r.pattern=z2(r)),$e.init(t,r)}),u3=j("$ZodISODuration",(t,r)=>{r.pattern??(r.pattern=m2),$e.init(t,r)}),c3=j("$ZodIPv4",(t,r)=>{r.pattern??(r.pattern=_2),$e.init(t,r),t._zod.bag.format="ipv4"}),d3=j("$ZodIPv6",(t,r)=>{r.pattern??(r.pattern=w2),$e.init(t,r),t._zod.bag.format="ipv6",t._zod.check=i=>{try{new URL(`http://[${i.value}]`)}catch{i.issues.push({code:"invalid_format",format:"ipv6",input:i.value,inst:t,continue:!r.abort})}}}),p3=j("$ZodCIDRv4",(t,r)=>{r.pattern??(r.pattern=x2),$e.init(t,r)}),f3=j("$ZodCIDRv6",(t,r)=>{r.pattern??(r.pattern=E2),$e.init(t,r),t._zod.check=i=>{const s=i.value.split("/");try{if(s.length!==2)throw new Error;const[u,p]=s;if(!p)throw new Error;const d=Number(p);if(`${d}`!==p)throw new Error;if(d<0||d>128)throw new Error;new URL(`http://[${u}]`)}catch{i.issues.push({code:"invalid_format",format:"cidrv6",input:i.value,inst:t,continue:!r.abort})}}});function Bm(t){if(t==="")return!0;if(/\s/.test(t)||t.length%4!==0)return!1;try{return atob(t),!0}catch{return!1}}const m3=j("$ZodBase64",(t,r)=>{r.pattern??(r.pattern=I2),$e.init(t,r),t._zod.bag.contentEncoding="base64",t._zod.check=i=>{Bm(i.value)||i.issues.push({code:"invalid_format",format:"base64",input:i.value,inst:t,continue:!r.abort})}});function v3(t){if(!Im.test(t))return!1;const r=t.replace(/[-_]/g,s=>s==="-"?"+":"/"),i=r.padEnd(Math.ceil(r.length/4)*4,"=");return Bm(i)}const h3=j("$ZodBase64URL",(t,r)=>{r.pattern??(r.pattern=Im),$e.init(t,r),t._zod.bag.contentEncoding="base64url",t._zod.check=i=>{v3(i.value)||i.issues.push({code:"invalid_format",format:"base64url",input:i.value,inst:t,continue:!r.abort})}}),g3=j("$ZodE164",(t,r)=>{r.pattern??(r.pattern=k2),$e.init(t,r)});function y3(t,r=null){try{const i=t.split(".");if(i.length!==3)return!1;const[s]=i;if(!s)return!1;const u=JSON.parse(atob(s));return!("typ"in u&&u?.typ!=="JWT"||!u.alg||r&&(!("alg"in u)||u.alg!==r))}catch{return!1}}const _3=j("$ZodJWT",(t,r)=>{$e.init(t,r),t._zod.check=i=>{y3(i.value,r.alg)||i.issues.push({code:"invalid_format",format:"jwt",input:i.value,inst:t,continue:!r.abort})}}),Rm=j("$ZodNumber",(t,r)=>{je.init(t,r),t._zod.pattern=t._zod.bag.pattern??bm,t._zod.parse=(i,s)=>{if(r.coerce)try{i.value=Number(i.value)}catch{}const u=i.value;if(typeof u=="number"&&!Number.isNaN(u)&&Number.isFinite(u))return i;const p=typeof u=="number"?Number.isNaN(u)?"NaN":Number.isFinite(u)?void 0:"Infinity":void 0;return i.issues.push({expected:"number",code:"invalid_type",input:u,inst:t,...p?{received:p}:{}}),i}}),w3=j("$ZodNumberFormat",(t,r)=>{j2.init(t,r),Rm.init(t,r)}),x3=j("$ZodBoolean",(t,r)=>{je.init(t,r),t._zod.pattern=P2,t._zod.parse=(i,s)=>{if(r.coerce)try{i.value=!!i.value}catch{}const u=i.value;return typeof u=="boolean"||i.issues.push({expected:"boolean",code:"invalid_type",input:u,inst:t}),i}}),E3=j("$ZodBigInt",(t,r)=>{je.init(t,r),t._zod.pattern=B2,t._zod.parse=(i,s)=>{if(r.coerce)try{i.value=BigInt(i.value)}catch{}return typeof i.value=="bigint"||i.issues.push({expected:"bigint",code:"invalid_type",input:i.value,inst:t}),i}}),I3=j("$ZodUnknown",(t,r)=>{je.init(t,r),t._zod.parse=i=>i}),S3=j("$ZodNever",(t,r)=>{je.init(t,r),t._zod.parse=(i,s)=>(i.issues.push({expected:"never",code:"invalid_type",input:i.value,inst:t}),i)});function hf(t,r,i){t.issues.length&&r.issues.push(...Vr(i,t.issues)),r.value[i]=t.value}const k3=j("$ZodArray",(t,r)=>{je.init(t,r),t._zod.parse=(i,s)=>{const u=i.value;if(!Array.isArray(u))return i.issues.push({expected:"array",code:"invalid_type",input:u,inst:t}),i;i.value=Array(u.length);const p=[];for(let d=0;dhf(y,i,d))):hf(g,i,d)}return p.length?Promise.all(p).then(()=>i):i}});function Ea(t,r,i,s,u,p){const d=i in s;if(t.issues.length){if(u&&p&&!d)return;r.issues.push(...Vr(i,t.issues))}if(!d&&!u){t.issues.length||r.issues.push({code:"invalid_type",expected:"nonoptional",input:void 0,path:[i]});return}t.value===void 0?d&&(r.value[i]=void 0):r.value[i]=t.value}function Pm(t){const r=Object.keys(t.shape);for(const s of r)if(!t.shape?.[s]?._zod?.traits?.has("$ZodType"))throw new Error(`Invalid element at key "${s}": expected a Zod schema`);const i=M7(t.shape);return{...t,keys:r,keySet:new Set(r),numKeys:r.length,optionalKeys:new Set(i)}}function Nm(t,r,i,s,u,p){const d=[],m=u.keySet,g=u.catchall._zod,y=g.def.type,E=g.optin==="optional",S=g.optout==="optional";for(const T in r){if(T==="__proto__"||m.has(T))continue;if(y==="never"){d.push(T);continue}const A=g.run({value:r[T],issues:[]},s);A instanceof Promise?t.push(A.then(D=>Ea(D,i,T,r,E,S))):Ea(A,i,T,r,E,S)}return d.length&&i.issues.push({code:"unrecognized_keys",keys:d,input:r,inst:p}),t.length?Promise.all(t).then(()=>i):i}const b3=j("$ZodObject",(t,r)=>{if(je.init(t,r),!Object.getOwnPropertyDescriptor(r,"shape")?.get){const m=r.shape;Object.defineProperty(r,"shape",{get:()=>{const g={...m};return Object.defineProperty(r,"shape",{value:g}),g}})}const s=Na(()=>Pm(r));ze(t._zod,"propValues",()=>{const m=r.shape,g={};for(const y in m){const E=m[y]._zod;if(E.values){g[y]??(g[y]=new Set);for(const S of E.values)g[y].add(S)}}return g});const u=Qo,p=r.catchall;let d;t._zod.parse=(m,g)=>{d??(d=s.value);const y=m.value;if(!u(y))return m.issues.push({expected:"object",code:"invalid_type",input:y,inst:t}),m;m.value={};const E=[],S=d.shape;for(const T of d.keys){const A=S[T],D=A._zod.optin==="optional",W=A._zod.optout==="optional",O=A._zod.run({value:y[T],issues:[]},g);O instanceof Promise?E.push(O.then(H=>Ea(H,m,T,y,D,W))):Ea(O,m,T,y,D,W)}return p?Nm(E,y,m,g,s.value,t):E.length?Promise.all(E).then(()=>m):m}}),z3=j("$ZodObjectJIT",(t,r)=>{b3.init(t,r);const i=t._zod.parse,s=Na(()=>Pm(r)),u=T=>{const A=new H2(["shape","payload","ctx"]),D=s.value,W=Q=>{const G=mf(Q);return`shape[${G}]._zod.run({ value: input[${G}], issues: [] }, ctx)`};A.write("const input = payload.value;");const O=Object.create(null);let H=0;for(const Q of D.keys)O[Q]=`key_${H++}`;A.write("const newResult = {};");for(const Q of D.keys){const G=O[Q],ee=mf(Q),ue=T[Q],de=ue?._zod?.optin==="optional",pe=ue?._zod?.optout==="optional";A.write(`const ${G} = ${W(Q)};`),de&&pe?A.write(` + if (${G}.issues.length) { + if (${ee} in input) { + payload.issues = payload.issues.concat(${G}.issues.map(iss => ({ + ...iss, + path: iss.path ? [${ee}, ...iss.path] : [${ee}] + }))); + } + } + + if (${G}.value === undefined) { + if (${ee} in input) { + newResult[${ee}] = undefined; + } + } else { + newResult[${ee}] = ${G}.value; + } + + `):de?A.write(` + if (${G}.issues.length) { + payload.issues = payload.issues.concat(${G}.issues.map(iss => ({ + ...iss, + path: iss.path ? [${ee}, ...iss.path] : [${ee}] + }))); + } + + if (${G}.value === undefined) { + if (${ee} in input) { + newResult[${ee}] = undefined; + } + } else { + newResult[${ee}] = ${G}.value; + } + + `):A.write(` + const ${G}_present = ${ee} in input; + if (${G}.issues.length) { + payload.issues = payload.issues.concat(${G}.issues.map(iss => ({ + ...iss, + path: iss.path ? [${ee}, ...iss.path] : [${ee}] + }))); + } + if (!${G}_present && !${G}.issues.length) { + payload.issues.push({ + code: "invalid_type", + expected: "nonoptional", + input: undefined, + path: [${ee}] + }); + } + + if (${G}_present) { + if (${G}.value === undefined) { + newResult[${ee}] = undefined; + } else { + newResult[${ee}] = ${G}.value; + } + } + + `)}A.write("payload.value = newResult;"),A.write("return payload;");const oe=A.compile();return(Q,G)=>oe(T,Q,G)};let p;const d=Qo,m=!du.jitless,y=m&&L7.value,E=r.catchall;let S;t._zod.parse=(T,A)=>{S??(S=s.value);const D=T.value;return d(D)?m&&y&&A?.async===!1&&A.jitless!==!0?(p||(p=u(r.shape)),T=p(T,A),E?Nm([],D,T,A,S,t):T):i(T,A):(T.issues.push({expected:"object",code:"invalid_type",input:D,inst:t}),T)}});function gf(t,r,i,s){for(const p of t)if(p.issues.length===0)return r.value=p.value,r;const u=t.filter(p=>!qr(p));return u.length===1?(r.value=u[0].value,u[0]):(r.issues.push({code:"invalid_union",input:r.value,inst:i,errors:t.map(p=>p.issues.map(d=>hn(d,s,vn())))}),r)}const Am=j("$ZodUnion",(t,r)=>{je.init(t,r),ze(t._zod,"optin",()=>r.options.some(s=>s._zod.optin==="optional")?"optional":void 0),ze(t._zod,"optout",()=>r.options.some(s=>s._zod.optout==="optional")?"optional":void 0),ze(t._zod,"values",()=>{if(r.options.every(s=>s._zod.values))return new Set(r.options.flatMap(s=>Array.from(s._zod.values)))}),ze(t._zod,"pattern",()=>{if(r.options.every(s=>s._zod.pattern)){const s=r.options.map(u=>u._zod.pattern);return new RegExp(`^(${s.map(u=>fu(u.source)).join("|")})$`)}});const i=r.options.length===1?r.options[0]._zod.run:null;t._zod.parse=(s,u)=>{if(i)return i(s,u);let p=!1;const d=[];for(const m of r.options){const g=m._zod.run({value:s.value,issues:[]},u);if(g instanceof Promise)d.push(g),p=!0;else{if(g.issues.length===0)return g;d.push(g)}}return p?Promise.all(d).then(m=>gf(m,s,t,u)):gf(d,s,t,u)}}),C3=j("$ZodDiscriminatedUnion",(t,r)=>{r.inclusive=!1,Am.init(t,r);const i=t._zod.parse;ze(t._zod,"propValues",()=>{const u={};for(const p of r.options){const d=p._zod.propValues;if(!d||Object.keys(d).length===0)throw new Error(`Invalid discriminated union option at index "${r.options.indexOf(p)}"`);for(const[m,g]of Object.entries(d)){u[m]||(u[m]=new Set);for(const y of g)u[m].add(y)}}return u});const s=Na(()=>{const u=r.options,p=new Map;for(const d of u){const m=d._zod.propValues?.[r.discriminator];if(!m||m.size===0)throw new Error(`Invalid discriminated union option at index "${r.options.indexOf(d)}"`);for(const g of m){if(p.has(g))throw new Error(`Duplicate discriminator value "${String(g)}"`);p.set(g,d)}}return p});t._zod.parse=(u,p)=>{const d=u.value;if(!Qo(d))return u.issues.push({code:"invalid_type",expected:"object",input:d,inst:t}),u;const m=s.value.get(d?.[r.discriminator]);return m?m._zod.run(u,p):r.unionFallback||p.direction==="backward"?i(u,p):(u.issues.push({code:"invalid_union",errors:[],note:"No matching discriminator",discriminator:r.discriminator,options:Array.from(s.value.keys()),input:d,path:[r.discriminator],inst:t}),u)}}),T3=j("$ZodIntersection",(t,r)=>{je.init(t,r),t._zod.parse=(i,s)=>{const u=i.value,p=r.left._zod.run({value:u,issues:[]},s),d=r.right._zod.run({value:u,issues:[]},s);return p instanceof Promise||d instanceof Promise?Promise.all([p,d]).then(([g,y])=>yf(i,g,y)):yf(i,p,d)}});function Ql(t,r){if(t===r)return{valid:!0,data:t};if(t instanceof Date&&r instanceof Date&&+t==+r)return{valid:!0,data:t};if(Xr(t)&&Xr(r)){const i=Object.keys(r),s=Object.keys(t).filter(p=>i.indexOf(p)!==-1),u={...t,...r};for(const p of s){const d=Ql(t[p],r[p]);if(!d.valid)return{valid:!1,mergeErrorPath:[p,...d.mergeErrorPath]};u[p]=d.data}return{valid:!0,data:u}}if(Array.isArray(t)&&Array.isArray(r)){if(t.length!==r.length)return{valid:!1,mergeErrorPath:[]};const i=[];for(let s=0;sm.l&&m.r).map(([m])=>m);if(p.length&&u&&t.issues.push({...u,keys:p}),qr(t))return t;const d=Ql(r.value,i.value);if(!d.valid)throw new Error(`Unmergable intersection. Error path: ${JSON.stringify(d.mergeErrorPath)}`);return t.value=d.data,t}const B3=j("$ZodRecord",(t,r)=>{je.init(t,r),t._zod.parse=(i,s)=>{const u=i.value;if(!Xr(u))return i.issues.push({expected:"record",code:"invalid_type",input:u,inst:t}),i;const p=[],d=r.keyType._zod.values;if(d){i.value={};const m=new Set;for(const y of d)if(typeof y=="string"||typeof y=="number"||typeof y=="symbol"){m.add(typeof y=="number"?y.toString():y);const E=r.keyType._zod.run({value:y,issues:[]},s);if(E instanceof Promise)throw new Error("Async schemas not supported in object keys currently");if(E.issues.length){i.issues.push({code:"invalid_key",origin:"record",issues:E.issues.map(A=>hn(A,s,vn())),input:y,path:[y],inst:t});continue}const S=E.value,T=r.valueType._zod.run({value:u[y],issues:[]},s);T instanceof Promise?p.push(T.then(A=>{A.issues.length&&i.issues.push(...Vr(y,A.issues)),i.value[S]=A.value})):(T.issues.length&&i.issues.push(...Vr(y,T.issues)),i.value[S]=T.value)}let g;for(const y in u)m.has(y)||(g=g??[],g.push(y));g&&g.length>0&&i.issues.push({code:"unrecognized_keys",input:u,inst:t,keys:g})}else{i.value={};for(const m of Reflect.ownKeys(u)){if(m==="__proto__"||!Object.prototype.propertyIsEnumerable.call(u,m))continue;let g=r.keyType._zod.run({value:m,issues:[]},s);if(g instanceof Promise)throw new Error("Async schemas not supported in object keys currently");if(typeof m=="string"&&bm.test(m)&&g.issues.length){const S=r.keyType._zod.run({value:Number(m),issues:[]},s);if(S instanceof Promise)throw new Error("Async schemas not supported in object keys currently");S.issues.length===0&&(g=S)}if(g.issues.length){r.mode==="loose"?i.value[m]=u[m]:i.issues.push({code:"invalid_key",origin:"record",issues:g.issues.map(S=>hn(S,s,vn())),input:m,path:[m],inst:t});continue}const E=r.valueType._zod.run({value:u[m],issues:[]},s);E instanceof Promise?p.push(E.then(S=>{S.issues.length&&i.issues.push(...Vr(m,S.issues)),i.value[g.value]=S.value})):(E.issues.length&&i.issues.push(...Vr(m,E.issues)),i.value[g.value]=E.value)}}return p.length?Promise.all(p).then(()=>i):i}}),R3=j("$ZodEnum",(t,r)=>{je.init(t,r);const i=gm(r.entries),s=new Set(i);t._zod.values=s,t._zod.pattern=new RegExp(`^(${i.filter(u=>D7.has(typeof u)).map(u=>typeof u=="string"?eo(u):u.toString()).join("|")})$`),t._zod.parse=(u,p)=>{const d=u.value;return s.has(d)||u.issues.push({code:"invalid_value",values:i,input:d,inst:t}),u}}),P3=j("$ZodLiteral",(t,r)=>{if(je.init(t,r),r.values.length===0)throw new Error("Cannot create literal schema with no valid values");const i=new Set(r.values);t._zod.values=i,t._zod.pattern=new RegExp(`^(${r.values.map(s=>typeof s=="string"?eo(s):s?eo(s.toString()):String(s)).join("|")})$`),t._zod.parse=(s,u)=>{const p=s.value;return i.has(p)||s.issues.push({code:"invalid_value",values:r.values,input:p,inst:t}),s}}),N3=j("$ZodTransform",(t,r)=>{je.init(t,r),t._zod.optin="optional",t._zod.parse=(i,s)=>{if(s.direction==="backward")throw new hm(t.constructor.name);const u=r.transform(i.value,i);if(s.async)return(u instanceof Promise?u:Promise.resolve(u)).then(d=>(i.value=d,i.fallback=!0,i));if(u instanceof Promise)throw new Hr;return i.value=u,i.fallback=!0,i}});function _f(t,r){return r===void 0&&(t.issues.length||t.fallback)?{issues:[],value:void 0}:t}const Om=j("$ZodOptional",(t,r)=>{je.init(t,r),t._zod.optin="optional",t._zod.optout="optional",ze(t._zod,"values",()=>r.innerType._zod.values?new Set([...r.innerType._zod.values,void 0]):void 0),ze(t._zod,"pattern",()=>{const i=r.innerType._zod.pattern;return i?new RegExp(`^(${fu(i.source)})?$`):void 0}),t._zod.parse=(i,s)=>{if(r.innerType._zod.optin==="optional"){const u=i.value,p=r.innerType._zod.run(i,s);return p instanceof Promise?p.then(d=>_f(d,u)):_f(p,u)}return i.value===void 0?i:r.innerType._zod.run(i,s)}}),A3=j("$ZodExactOptional",(t,r)=>{Om.init(t,r),ze(t._zod,"values",()=>r.innerType._zod.values),ze(t._zod,"pattern",()=>r.innerType._zod.pattern),t._zod.parse=(i,s)=>r.innerType._zod.run(i,s)}),O3=j("$ZodNullable",(t,r)=>{je.init(t,r),ze(t._zod,"optin",()=>r.innerType._zod.optin),ze(t._zod,"optout",()=>r.innerType._zod.optout),ze(t._zod,"pattern",()=>{const i=r.innerType._zod.pattern;return i?new RegExp(`^(${fu(i.source)}|null)$`):void 0}),ze(t._zod,"values",()=>r.innerType._zod.values?new Set([...r.innerType._zod.values,null]):void 0),t._zod.parse=(i,s)=>i.value===null?i:r.innerType._zod.run(i,s)}),j3=j("$ZodDefault",(t,r)=>{je.init(t,r),t._zod.optin="optional",ze(t._zod,"values",()=>r.innerType._zod.values),t._zod.parse=(i,s)=>{if(s.direction==="backward")return r.innerType._zod.run(i,s);if(i.value===void 0)return i.value=r.defaultValue,i;const u=r.innerType._zod.run(i,s);return u instanceof Promise?u.then(p=>wf(p,r)):wf(u,r)}});function wf(t,r){return t.value===void 0&&(t.value=r.defaultValue),t}const $3=j("$ZodPrefault",(t,r)=>{je.init(t,r),t._zod.optin="optional",ze(t._zod,"values",()=>r.innerType._zod.values),t._zod.parse=(i,s)=>(s.direction==="backward"||i.value===void 0&&(i.value=r.defaultValue),r.innerType._zod.run(i,s))}),L3=j("$ZodNonOptional",(t,r)=>{je.init(t,r),ze(t._zod,"values",()=>{const i=r.innerType._zod.values;return i?new Set([...i].filter(s=>s!==void 0)):void 0}),t._zod.parse=(i,s)=>{const u=r.innerType._zod.run(i,s);return u instanceof Promise?u.then(p=>xf(p,t)):xf(u,t)}});function xf(t,r){return!t.issues.length&&t.value===void 0&&t.issues.push({code:"invalid_type",expected:"nonoptional",input:t.value,inst:r}),t}const D3=j("$ZodCatch",(t,r)=>{je.init(t,r),t._zod.optin="optional",ze(t._zod,"optout",()=>r.innerType._zod.optout),ze(t._zod,"values",()=>r.innerType._zod.values),t._zod.parse=(i,s)=>{if(s.direction==="backward")return r.innerType._zod.run(i,s);const u=r.innerType._zod.run(i,s);return u instanceof Promise?u.then(p=>(i.value=p.value,p.issues.length&&(i.value=r.catchValue({...i,error:{issues:p.issues.map(d=>hn(d,s,vn()))},input:i.value}),i.issues=[],i.fallback=!0),i)):(i.value=u.value,u.issues.length&&(i.value=r.catchValue({...i,error:{issues:u.issues.map(p=>hn(p,s,vn()))},input:i.value}),i.issues=[],i.fallback=!0),i)}}),M3=j("$ZodPipe",(t,r)=>{je.init(t,r),ze(t._zod,"values",()=>r.in._zod.values),ze(t._zod,"optin",()=>r.in._zod.optin),ze(t._zod,"optout",()=>r.out._zod.optout),ze(t._zod,"propValues",()=>r.in._zod.propValues),t._zod.parse=(i,s)=>{if(s.direction==="backward"){const p=r.out._zod.run(i,s);return p instanceof Promise?p.then(d=>ga(d,r.in,s)):ga(p,r.in,s)}const u=r.in._zod.run(i,s);return u instanceof Promise?u.then(p=>ga(p,r.out,s)):ga(u,r.out,s)}});function ga(t,r,i){return t.issues.length?(t.aborted=!0,t):r._zod.run({value:t.value,issues:t.issues,fallback:t.fallback},i)}const F3=j("$ZodReadonly",(t,r)=>{je.init(t,r),ze(t._zod,"propValues",()=>r.innerType._zod.propValues),ze(t._zod,"values",()=>r.innerType._zod.values),ze(t._zod,"optin",()=>r.innerType?._zod?.optin),ze(t._zod,"optout",()=>r.innerType?._zod?.optout),t._zod.parse=(i,s)=>{if(s.direction==="backward")return r.innerType._zod.run(i,s);const u=r.innerType._zod.run(i,s);return u instanceof Promise?u.then(Ef):Ef(u)}});function Ef(t){return t.value=Object.freeze(t.value),t}const U3=j("$ZodCustom",(t,r)=>{St.init(t,r),je.init(t,r),t._zod.parse=(i,s)=>i,t._zod.check=i=>{const s=i.value,u=r.fn(s);if(u instanceof Promise)return u.then(p=>If(p,i,s,t));If(u,i,s,t)}});function If(t,r,i,s){if(!t){const u={code:"custom",input:i,inst:s,path:[...s._zod.def.path??[]],continue:!s._zod.def.abort};s._zod.def.params&&(u.params=s._zod.def.params),r.issues.push(Yo(u))}}var Sf;class Z3{constructor(){this._map=new WeakMap,this._idmap=new Map}add(r,...i){const s=i[0];return this._map.set(r,s),s&&typeof s=="object"&&"id"in s&&this._idmap.set(s.id,r),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(r){const i=this._map.get(r);return i&&typeof i=="object"&&"id"in i&&this._idmap.delete(i.id),this._map.delete(r),this}get(r){const i=r._zod.parent;if(i){const s={...this.get(i)??{}};delete s.id;const u={...s,...this._map.get(r)};return Object.keys(u).length?u:void 0}return this._map.get(r)}has(r){return this._map.has(r)}}function q3(){return new Z3}(Sf=globalThis).__zod_globalRegistry??(Sf.__zod_globalRegistry=q3());const Wo=globalThis.__zod_globalRegistry;function V3(t,r){return new t({type:"string",...ie(r)})}function W3(t,r){return new t({type:"string",format:"email",check:"string_format",abort:!1,...ie(r)})}function kf(t,r){return new t({type:"string",format:"guid",check:"string_format",abort:!1,...ie(r)})}function H3(t,r){return new t({type:"string",format:"uuid",check:"string_format",abort:!1,...ie(r)})}function G3(t,r){return new t({type:"string",format:"uuid",check:"string_format",abort:!1,version:"v4",...ie(r)})}function J3(t,r){return new t({type:"string",format:"uuid",check:"string_format",abort:!1,version:"v6",...ie(r)})}function K3(t,r){return new t({type:"string",format:"uuid",check:"string_format",abort:!1,version:"v7",...ie(r)})}function jm(t,r){return new t({type:"string",format:"url",check:"string_format",abort:!1,...ie(r)})}function Q3(t,r){return new t({type:"string",format:"emoji",check:"string_format",abort:!1,...ie(r)})}function Y3(t,r){return new t({type:"string",format:"nanoid",check:"string_format",abort:!1,...ie(r)})}function X3(t,r){return new t({type:"string",format:"cuid",check:"string_format",abort:!1,...ie(r)})}function e_(t,r){return new t({type:"string",format:"cuid2",check:"string_format",abort:!1,...ie(r)})}function t_(t,r){return new t({type:"string",format:"ulid",check:"string_format",abort:!1,...ie(r)})}function n_(t,r){return new t({type:"string",format:"xid",check:"string_format",abort:!1,...ie(r)})}function r_(t,r){return new t({type:"string",format:"ksuid",check:"string_format",abort:!1,...ie(r)})}function o_(t,r){return new t({type:"string",format:"ipv4",check:"string_format",abort:!1,...ie(r)})}function i_(t,r){return new t({type:"string",format:"ipv6",check:"string_format",abort:!1,...ie(r)})}function a_(t,r){return new t({type:"string",format:"cidrv4",check:"string_format",abort:!1,...ie(r)})}function s_(t,r){return new t({type:"string",format:"cidrv6",check:"string_format",abort:!1,...ie(r)})}function l_(t,r){return new t({type:"string",format:"base64",check:"string_format",abort:!1,...ie(r)})}function u_(t,r){return new t({type:"string",format:"base64url",check:"string_format",abort:!1,...ie(r)})}function c_(t,r){return new t({type:"string",format:"e164",check:"string_format",abort:!1,...ie(r)})}function d_(t,r){return new t({type:"string",format:"jwt",check:"string_format",abort:!1,...ie(r)})}function p_(t,r){return new t({type:"string",format:"datetime",check:"string_format",offset:!1,local:!1,precision:null,...ie(r)})}function f_(t,r){return new t({type:"string",format:"date",check:"string_format",...ie(r)})}function m_(t,r){return new t({type:"string",format:"time",check:"string_format",precision:null,...ie(r)})}function v_(t,r){return new t({type:"string",format:"duration",check:"string_format",...ie(r)})}function h_(t,r){return new t({type:"number",checks:[],...ie(r)})}function g_(t,r){return new t({type:"number",check:"number_format",abort:!1,format:"safeint",...ie(r)})}function y_(t,r){return new t({type:"boolean",...ie(r)})}function __(t,r){return new t({type:"bigint",coerce:!0,...ie(r)})}function w_(t){return new t({type:"unknown"})}function x_(t,r){return new t({type:"never",...ie(r)})}function Ia(t,r){return new Cm({check:"less_than",...ie(r),value:t,inclusive:!1})}function Gr(t,r){return new Cm({check:"less_than",...ie(r),value:t,inclusive:!0})}function Sa(t,r){return new Tm({check:"greater_than",...ie(r),value:t,inclusive:!1})}function Fn(t,r){return new Tm({check:"greater_than",...ie(r),value:t,inclusive:!0})}function Yl(t,r){return new O2({check:"multiple_of",...ie(r),value:t})}function $m(t,r){return new $2({check:"max_length",...ie(r),maximum:t})}function ka(t,r){return new L2({check:"min_length",...ie(r),minimum:t})}function Lm(t,r){return new D2({check:"length_equals",...ie(r),length:t})}function E_(t,r){return new M2({check:"string_format",format:"regex",...ie(r),pattern:t})}function I_(t){return new F2({check:"string_format",format:"lowercase",...ie(t)})}function S_(t){return new U2({check:"string_format",format:"uppercase",...ie(t)})}function k_(t,r){return new Z2({check:"string_format",format:"includes",...ie(r),includes:t})}function b_(t,r){return new q2({check:"string_format",format:"starts_with",...ie(r),prefix:t})}function z_(t,r){return new V2({check:"string_format",format:"ends_with",...ie(r),suffix:t})}function ro(t){return new W2({check:"overwrite",tx:t})}function C_(t){return ro(r=>r.normalize(t))}function T_(){return ro(t=>t.trim())}function B_(){return ro(t=>t.toLowerCase())}function R_(){return ro(t=>t.toUpperCase())}function P_(){return ro(t=>$7(t))}function N_(t,r,i){return new t({type:"array",element:r,...ie(i)})}function A_(t,r,i){return new t({type:"custom",check:"custom",fn:r,...ie(i)})}function O_(t,r){const i=j_(s=>(s.addIssue=u=>{if(typeof u=="string")s.issues.push(Yo(u,s.value,i._zod.def));else{const p=u;p.fatal&&(p.continue=!1),p.code??(p.code="custom"),p.input??(p.input=s.value),p.inst??(p.inst=i),p.continue??(p.continue=!i._zod.def.abort),s.issues.push(Yo(p))}},t(s.value,s)),r);return i}function j_(t,r){const i=new St({check:"custom",...ie(r)});return i._zod.check=t,i}function Dm(t){let r=t?.target??"draft-2020-12";return r==="draft-4"&&(r="draft-04"),r==="draft-7"&&(r="draft-07"),{processors:t.processors??{},metadataRegistry:t?.metadata??Wo,target:r,unrepresentable:t?.unrepresentable??"throw",override:t?.override??(()=>{}),io:t?.io??"output",counter:0,seen:new Map,cycles:t?.cycles??"ref",reused:t?.reused??"inline",external:t?.external??void 0}}function Je(t,r,i={path:[],schemaPath:[]}){var s;const u=t._zod.def,p=r.seen.get(t);if(p)return p.count++,i.schemaPath.includes(t)&&(p.cycle=i.path),p.schema;const d={schema:{},count:1,cycle:void 0,path:i.path};r.seen.set(t,d);const m=t._zod.toJSONSchema?.();if(m)d.schema=m;else{const E={...i,schemaPath:[...i.schemaPath,t],path:i.path};if(t._zod.processJSONSchema)t._zod.processJSONSchema(r,d.schema,E);else{const T=d.schema,A=r.processors[u.type];if(!A)throw new Error(`[toJSONSchema]: Non-representable type encountered: ${u.type}`);A(t,r,T,E)}const S=t._zod.parent;S&&(d.ref||(d.ref=S),Je(S,r,E),r.seen.get(S).isParent=!0)}const g=r.metadataRegistry.get(t);return g&&Object.assign(d.schema,g),r.io==="input"&&ft(t)&&(delete d.schema.examples,delete d.schema.default),r.io==="input"&&"_prefault"in d.schema&&((s=d.schema).default??(s.default=d.schema._prefault)),delete d.schema._prefault,r.seen.get(t).schema}function Mm(t,r){const i=t.seen.get(r);if(!i)throw new Error("Unprocessed schema. This is a bug in Zod.");const s=new Map;for(const d of t.seen.entries()){const m=t.metadataRegistry.get(d[0])?.id;if(m){const g=s.get(m);if(g&&g!==d[0])throw new Error(`Duplicate schema id "${m}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);s.set(m,d[0])}}const u=d=>{const m=t.target==="draft-2020-12"?"$defs":"definitions";if(t.external){const S=t.external.registry.get(d[0])?.id,T=t.external.uri??(D=>D);if(S)return{ref:T(S)};const A=d[1].defId??d[1].schema.id??`schema${t.counter++}`;return d[1].defId=A,{defId:A,ref:`${T("__shared")}#/${m}/${A}`}}if(d[1]===i)return{ref:"#"};const y=`#/${m}/`,E=d[1].schema.id??`__schema${t.counter++}`;return{defId:E,ref:y+E}},p=d=>{if(d[1].schema.$ref)return;const m=d[1],{ref:g,defId:y}=u(d);m.def={...m.schema},y&&(m.defId=y);const E=m.schema;for(const S in E)delete E[S];E.$ref=g};if(t.cycles==="throw")for(const d of t.seen.entries()){const m=d[1];if(m.cycle)throw new Error(`Cycle detected: #/${m.cycle?.join("/")}/ + +Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.`)}for(const d of t.seen.entries()){const m=d[1];if(r===d[0]){p(d);continue}if(t.external){const y=t.external.registry.get(d[0])?.id;if(r!==d[0]&&y){p(d);continue}}if(t.metadataRegistry.get(d[0])?.id){p(d);continue}if(m.cycle){p(d);continue}if(m.count>1&&t.reused==="ref"){p(d);continue}}}function Fm(t,r){const i=t.seen.get(r);if(!i)throw new Error("Unprocessed schema. This is a bug in Zod.");const s=m=>{const g=t.seen.get(m);if(g.ref===null)return;const y=g.def??g.schema,E={...y},S=g.ref;if(g.ref=null,S){s(S);const A=t.seen.get(S),D=A.schema;if(D.$ref&&(t.target==="draft-07"||t.target==="draft-04"||t.target==="openapi-3.0")?(y.allOf=y.allOf??[],y.allOf.push(D)):Object.assign(y,D),Object.assign(y,E),m._zod.parent===S)for(const O in y)O==="$ref"||O==="allOf"||O in E||delete y[O];if(D.$ref&&A.def)for(const O in y)O==="$ref"||O==="allOf"||O in A.def&&JSON.stringify(y[O])===JSON.stringify(A.def[O])&&delete y[O]}const T=m._zod.parent;if(T&&T!==S){s(T);const A=t.seen.get(T);if(A?.schema.$ref&&(y.$ref=A.schema.$ref,A.def))for(const D in y)D==="$ref"||D==="allOf"||D in A.def&&JSON.stringify(y[D])===JSON.stringify(A.def[D])&&delete y[D]}t.override({zodSchema:m,jsonSchema:y,path:g.path??[]})};for(const m of[...t.seen.entries()].reverse())s(m[0]);const u={};if(t.target==="draft-2020-12"?u.$schema="https://json-schema.org/draft/2020-12/schema":t.target==="draft-07"?u.$schema="http://json-schema.org/draft-07/schema#":t.target==="draft-04"?u.$schema="http://json-schema.org/draft-04/schema#":t.target,t.external?.uri){const m=t.external.registry.get(r)?.id;if(!m)throw new Error("Schema is missing an `id` property");u.$id=t.external.uri(m)}Object.assign(u,i.def??i.schema);const p=t.metadataRegistry.get(r)?.id;p!==void 0&&u.id===p&&delete u.id;const d=t.external?.defs??{};for(const m of t.seen.entries()){const g=m[1];g.def&&g.defId&&(g.def.id===g.defId&&delete g.def.id,d[g.defId]=g.def)}t.external||Object.keys(d).length>0&&(t.target==="draft-2020-12"?u.$defs=d:u.definitions=d);try{const m=JSON.parse(JSON.stringify(u));return Object.defineProperty(m,"~standard",{value:{...r["~standard"],jsonSchema:{input:ba(r,"input",t.processors),output:ba(r,"output",t.processors)}},enumerable:!1,writable:!1}),m}catch{throw new Error("Error converting schema to JSON.")}}function ft(t,r){const i=r??{seen:new Set};if(i.seen.has(t))return!1;i.seen.add(t);const s=t._zod.def;if(s.type==="transform")return!0;if(s.type==="array")return ft(s.element,i);if(s.type==="set")return ft(s.valueType,i);if(s.type==="lazy")return ft(s.getter(),i);if(s.type==="promise"||s.type==="optional"||s.type==="nonoptional"||s.type==="nullable"||s.type==="readonly"||s.type==="default"||s.type==="prefault")return ft(s.innerType,i);if(s.type==="intersection")return ft(s.left,i)||ft(s.right,i);if(s.type==="record"||s.type==="map")return ft(s.keyType,i)||ft(s.valueType,i);if(s.type==="pipe")return t._zod.traits.has("$ZodCodec")?!0:ft(s.in,i)||ft(s.out,i);if(s.type==="object"){for(const u in s.shape)if(ft(s.shape[u],i))return!0;return!1}if(s.type==="union"){for(const u of s.options)if(ft(u,i))return!0;return!1}if(s.type==="tuple"){for(const u of s.items)if(ft(u,i))return!0;return!!(s.rest&&ft(s.rest,i))}return!1}const $_=(t,r={})=>i=>{const s=Dm({...i,processors:r});return Je(t,s),Mm(s,t),Fm(s,t)},ba=(t,r,i={})=>s=>{const{libraryOptions:u,target:p}=s??{},d=Dm({...u??{},target:p,io:r,processors:i});return Je(t,d),Mm(d,t),Fm(d,t)},L_={guid:"uuid",url:"uri",datetime:"date-time",json_string:"json-string",regex:""},D_=(t,r,i,s)=>{const u=i;u.type="string";const{minimum:p,maximum:d,format:m,patterns:g,contentEncoding:y}=t._zod.bag;if(typeof p=="number"&&(u.minLength=p),typeof d=="number"&&(u.maxLength=d),m&&(u.format=L_[m]??m,u.format===""&&delete u.format,m==="time"&&delete u.format),y&&(u.contentEncoding=y),g&&g.size>0){const E=[...g];E.length===1?u.pattern=E[0].source:E.length>1&&(u.allOf=[...E.map(S=>({...r.target==="draft-07"||r.target==="draft-04"||r.target==="openapi-3.0"?{type:"string"}:{},pattern:S.source}))])}},M_=(t,r,i,s)=>{const u=i,{minimum:p,maximum:d,format:m,multipleOf:g,exclusiveMaximum:y,exclusiveMinimum:E}=t._zod.bag;typeof m=="string"&&m.includes("int")?u.type="integer":u.type="number";const S=typeof E=="number"&&E>=(p??Number.NEGATIVE_INFINITY),T=typeof y=="number"&&y<=(d??Number.POSITIVE_INFINITY),A=r.target==="draft-04"||r.target==="openapi-3.0";S?A?(u.minimum=E,u.exclusiveMinimum=!0):u.exclusiveMinimum=E:typeof p=="number"&&(u.minimum=p),T?A?(u.maximum=y,u.exclusiveMaximum=!0):u.exclusiveMaximum=y:typeof d=="number"&&(u.maximum=d),typeof g=="number"&&(u.multipleOf=g)},F_=(t,r,i,s)=>{i.type="boolean"},U_=(t,r,i,s)=>{if(r.unrepresentable==="throw")throw new Error("BigInt cannot be represented in JSON Schema")},Z_=(t,r,i,s)=>{i.not={}},q_=(t,r,i,s)=>{},V_=(t,r,i,s)=>{const u=t._zod.def,p=gm(u.entries);p.every(d=>typeof d=="number")&&(i.type="number"),p.every(d=>typeof d=="string")&&(i.type="string"),i.enum=p},W_=(t,r,i,s)=>{const u=t._zod.def,p=[];for(const d of u.values)if(d===void 0){if(r.unrepresentable==="throw")throw new Error("Literal `undefined` cannot be represented in JSON Schema")}else if(typeof d=="bigint"){if(r.unrepresentable==="throw")throw new Error("BigInt literals cannot be represented in JSON Schema");p.push(Number(d))}else p.push(d);if(p.length!==0)if(p.length===1){const d=p[0];i.type=d===null?"null":typeof d,r.target==="draft-04"||r.target==="openapi-3.0"?i.enum=[d]:i.const=d}else p.every(d=>typeof d=="number")&&(i.type="number"),p.every(d=>typeof d=="string")&&(i.type="string"),p.every(d=>typeof d=="boolean")&&(i.type="boolean"),p.every(d=>d===null)&&(i.type="null"),i.enum=p},H_=(t,r,i,s)=>{if(r.unrepresentable==="throw")throw new Error("Custom types cannot be represented in JSON Schema")},G_=(t,r,i,s)=>{if(r.unrepresentable==="throw")throw new Error("Transforms cannot be represented in JSON Schema")},J_=(t,r,i,s)=>{const u=i,p=t._zod.def,{minimum:d,maximum:m}=t._zod.bag;typeof d=="number"&&(u.minItems=d),typeof m=="number"&&(u.maxItems=m),u.type="array",u.items=Je(p.element,r,{...s,path:[...s.path,"items"]})},K_=(t,r,i,s)=>{const u=i,p=t._zod.def;u.type="object",u.properties={};const d=p.shape;for(const y in d)u.properties[y]=Je(d[y],r,{...s,path:[...s.path,"properties",y]});const m=new Set(Object.keys(d)),g=new Set([...m].filter(y=>{const E=p.shape[y]._zod;return r.io==="input"?E.optin===void 0:E.optout===void 0}));g.size>0&&(u.required=Array.from(g)),p.catchall?._zod.def.type==="never"?u.additionalProperties=!1:p.catchall?p.catchall&&(u.additionalProperties=Je(p.catchall,r,{...s,path:[...s.path,"additionalProperties"]})):r.io==="output"&&(u.additionalProperties=!1)},Q_=(t,r,i,s)=>{const u=t._zod.def,p=u.inclusive===!1,d=u.options.map((m,g)=>Je(m,r,{...s,path:[...s.path,p?"oneOf":"anyOf",g]}));p?i.oneOf=d:i.anyOf=d},Y_=(t,r,i,s)=>{const u=t._zod.def,p=Je(u.left,r,{...s,path:[...s.path,"allOf",0]}),d=Je(u.right,r,{...s,path:[...s.path,"allOf",1]}),m=y=>"allOf"in y&&Object.keys(y).length===1,g=[...m(p)?p.allOf:[p],...m(d)?d.allOf:[d]];i.allOf=g},X_=(t,r,i,s)=>{const u=i,p=t._zod.def;u.type="object";const d=p.keyType,g=d._zod.bag?.patterns;if(p.mode==="loose"&&g&&g.size>0){const E=Je(p.valueType,r,{...s,path:[...s.path,"patternProperties","*"]});u.patternProperties={};for(const S of g)u.patternProperties[S.source]=E}else(r.target==="draft-07"||r.target==="draft-2020-12")&&(u.propertyNames=Je(p.keyType,r,{...s,path:[...s.path,"propertyNames"]})),u.additionalProperties=Je(p.valueType,r,{...s,path:[...s.path,"additionalProperties"]});const y=d._zod.values;if(y){const E=[...y].filter(S=>typeof S=="string"||typeof S=="number");E.length>0&&(u.required=E)}},e8=(t,r,i,s)=>{const u=t._zod.def,p=Je(u.innerType,r,s),d=r.seen.get(t);r.target==="openapi-3.0"?(d.ref=u.innerType,i.nullable=!0):i.anyOf=[p,{type:"null"}]},t8=(t,r,i,s)=>{const u=t._zod.def;Je(u.innerType,r,s);const p=r.seen.get(t);p.ref=u.innerType},n8=(t,r,i,s)=>{const u=t._zod.def;Je(u.innerType,r,s);const p=r.seen.get(t);p.ref=u.innerType,i.default=JSON.parse(JSON.stringify(u.defaultValue))},r8=(t,r,i,s)=>{const u=t._zod.def;Je(u.innerType,r,s);const p=r.seen.get(t);p.ref=u.innerType,r.io==="input"&&(i._prefault=JSON.parse(JSON.stringify(u.defaultValue)))},o8=(t,r,i,s)=>{const u=t._zod.def;Je(u.innerType,r,s);const p=r.seen.get(t);p.ref=u.innerType;let d;try{d=u.catchValue(void 0)}catch{throw new Error("Dynamic catch values are not supported in JSON Schema")}i.default=d},i8=(t,r,i,s)=>{const u=t._zod.def,p=u.in._zod.traits.has("$ZodTransform"),d=r.io==="input"?p?u.out:u.in:u.out;Je(d,r,s);const m=r.seen.get(t);m.ref=d},a8=(t,r,i,s)=>{const u=t._zod.def;Je(u.innerType,r,s);const p=r.seen.get(t);p.ref=u.innerType,i.readOnly=!0},Um=(t,r,i,s)=>{const u=t._zod.def;Je(u.innerType,r,s);const p=r.seen.get(t);p.ref=u.innerType},s8=j("ZodISODateTime",(t,r)=>{a3.init(t,r),Ue.init(t,r)});function N(t){return p_(s8,t)}const l8=j("ZodISODate",(t,r)=>{s3.init(t,r),Ue.init(t,r)});function u8(t){return f_(l8,t)}const c8=j("ZodISOTime",(t,r)=>{l3.init(t,r),Ue.init(t,r)});function d8(t){return m_(c8,t)}const p8=j("ZodISODuration",(t,r)=>{u3.init(t,r),Ue.init(t,r)});function f8(t){return v_(p8,t)}const m8=(t,r)=>{xm.init(t,r),t.name="ZodError",Object.defineProperties(t,{format:{value:i=>Q7(t,i)},flatten:{value:i=>K7(t,i)},addIssue:{value:i=>{t.issues.push(i),t.message=JSON.stringify(t.issues,Kl,2)}},addIssues:{value:i=>{t.issues.push(...i),t.message=JSON.stringify(t.issues,Kl,2)}},isEmpty:{get(){return t.issues.length===0}}})},Ft=j("ZodError",m8,{Parent:Error}),v8=vu(Ft),h8=hu(Ft),g8=Aa(Ft),y8=Oa(Ft),_8=e2(Ft),w8=t2(Ft),x8=n2(Ft),E8=r2(Ft),I8=o2(Ft),S8=i2(Ft),k8=a2(Ft),b8=s2(Ft),bf=new WeakMap;function ei(t,r,i){const s=Object.getPrototypeOf(t);let u=bf.get(s);if(u||(u=new Set,bf.set(s,u)),!u.has(r)){u.add(r);for(const p in i){const d=i[p];Object.defineProperty(s,p,{configurable:!0,enumerable:!1,get(){const m=d.bind(this);return Object.defineProperty(this,p,{configurable:!0,writable:!0,enumerable:!0,value:m}),m},set(m){Object.defineProperty(this,p,{configurable:!0,writable:!0,enumerable:!0,value:m})}})}}}const Le=j("ZodType",(t,r)=>(je.init(t,r),Object.assign(t["~standard"],{jsonSchema:{input:ba(t,"input"),output:ba(t,"output")}}),t.toJSONSchema=$_(t,{}),t.def=r,t.type=r.type,Object.defineProperty(t,"_def",{value:r}),t.parse=(i,s)=>v8(t,i,s,{callee:t.parse}),t.safeParse=(i,s)=>g8(t,i,s),t.parseAsync=async(i,s)=>h8(t,i,s,{callee:t.parseAsync}),t.safeParseAsync=async(i,s)=>y8(t,i,s),t.spa=t.safeParseAsync,t.encode=(i,s)=>_8(t,i,s),t.decode=(i,s)=>w8(t,i,s),t.encodeAsync=async(i,s)=>x8(t,i,s),t.decodeAsync=async(i,s)=>E8(t,i,s),t.safeEncode=(i,s)=>I8(t,i,s),t.safeDecode=(i,s)=>S8(t,i,s),t.safeEncodeAsync=async(i,s)=>k8(t,i,s),t.safeDecodeAsync=async(i,s)=>b8(t,i,s),ei(t,"ZodType",{check(...i){const s=this.def;return this.clone(Jn(s,{checks:[...s.checks??[],...i.map(u=>typeof u=="function"?{_zod:{check:u,def:{check:"custom"},onattach:[]}}:u)]}),{parent:!0})},with(...i){return this.check(...i)},clone(i,s){return Kn(this,i,s)},brand(){return this},register(i,s){return i.add(this,s),this},refine(i,s){return this.check(gw(i,s))},superRefine(i,s){return this.check(yw(i,s))},overwrite(i){return this.check(ro(i))},optional(){return Bf(this)},exactOptional(){return ow(this)},nullable(){return Rf(this)},nullish(){return Bf(Rf(this))},nonoptional(i){return cw(this,i)},array(){return P(this)},or(i){return Qn([this,i])},and(i){return X8(this,i)},transform(i){return Pf(this,nw(i))},default(i){return sw(this,i)},prefault(i){return uw(this,i)},catch(i){return pw(this,i)},pipe(i){return Pf(this,i)},readonly(){return vw(this)},describe(i){const s=this.clone();return Wo.add(s,{description:i}),s},meta(...i){if(i.length===0)return Wo.get(this);const s=this.clone();return Wo.add(s,i[0]),s},isOptional(){return this.safeParse(void 0).success},isNullable(){return this.safeParse(null).success},apply(i){return i(this)}}),Object.defineProperty(t,"description",{get(){return Wo.get(t)?.description},configurable:!0}),t)),Zm=j("_ZodString",(t,r)=>{gu.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(s,u,p)=>D_(t,s,u);const i=t._zod.bag;t.format=i.format??null,t.minLength=i.minimum??null,t.maxLength=i.maximum??null,ei(t,"_ZodString",{regex(...s){return this.check(E_(...s))},includes(...s){return this.check(k_(...s))},startsWith(...s){return this.check(b_(...s))},endsWith(...s){return this.check(z_(...s))},min(...s){return this.check(ka(...s))},max(...s){return this.check($m(...s))},length(...s){return this.check(Lm(...s))},nonempty(...s){return this.check(ka(1,...s))},lowercase(s){return this.check(I_(s))},uppercase(s){return this.check(S_(s))},trim(){return this.check(T_())},normalize(...s){return this.check(C_(...s))},toLowerCase(){return this.check(B_())},toUpperCase(){return this.check(R_())},slugify(){return this.check(P_())}})}),z8=j("ZodString",(t,r)=>{gu.init(t,r),Zm.init(t,r),t.email=i=>t.check(W3(C8,i)),t.url=i=>t.check(jm(qm,i)),t.jwt=i=>t.check(d_(Z8,i)),t.emoji=i=>t.check(Q3(T8,i)),t.guid=i=>t.check(kf(zf,i)),t.uuid=i=>t.check(H3(ya,i)),t.uuidv4=i=>t.check(G3(ya,i)),t.uuidv6=i=>t.check(J3(ya,i)),t.uuidv7=i=>t.check(K3(ya,i)),t.nanoid=i=>t.check(Y3(B8,i)),t.guid=i=>t.check(kf(zf,i)),t.cuid=i=>t.check(X3(R8,i)),t.cuid2=i=>t.check(e_(P8,i)),t.ulid=i=>t.check(t_(N8,i)),t.base64=i=>t.check(l_(M8,i)),t.base64url=i=>t.check(u_(F8,i)),t.xid=i=>t.check(n_(A8,i)),t.ksuid=i=>t.check(r_(O8,i)),t.ipv4=i=>t.check(o_(j8,i)),t.ipv6=i=>t.check(i_($8,i)),t.cidrv4=i=>t.check(a_(L8,i)),t.cidrv6=i=>t.check(s_(D8,i)),t.e164=i=>t.check(c_(U8,i)),t.datetime=i=>t.check(N(i)),t.date=i=>t.check(u8(i)),t.time=i=>t.check(d8(i)),t.duration=i=>t.check(f8(i))});function o(t){return V3(z8,t)}const Ue=j("ZodStringFormat",(t,r)=>{$e.init(t,r),Zm.init(t,r)}),C8=j("ZodEmail",(t,r)=>{Q2.init(t,r),Ue.init(t,r)}),zf=j("ZodGUID",(t,r)=>{J2.init(t,r),Ue.init(t,r)}),ya=j("ZodUUID",(t,r)=>{K2.init(t,r),Ue.init(t,r)}),qm=j("ZodURL",(t,r)=>{Y2.init(t,r),Ue.init(t,r)});function Cf(t){return jm(qm,t)}const T8=j("ZodEmoji",(t,r)=>{X2.init(t,r),Ue.init(t,r)}),B8=j("ZodNanoID",(t,r)=>{e3.init(t,r),Ue.init(t,r)}),R8=j("ZodCUID",(t,r)=>{t3.init(t,r),Ue.init(t,r)}),P8=j("ZodCUID2",(t,r)=>{n3.init(t,r),Ue.init(t,r)}),N8=j("ZodULID",(t,r)=>{r3.init(t,r),Ue.init(t,r)}),A8=j("ZodXID",(t,r)=>{o3.init(t,r),Ue.init(t,r)}),O8=j("ZodKSUID",(t,r)=>{i3.init(t,r),Ue.init(t,r)}),j8=j("ZodIPv4",(t,r)=>{c3.init(t,r),Ue.init(t,r)}),$8=j("ZodIPv6",(t,r)=>{d3.init(t,r),Ue.init(t,r)}),L8=j("ZodCIDRv4",(t,r)=>{p3.init(t,r),Ue.init(t,r)}),D8=j("ZodCIDRv6",(t,r)=>{f3.init(t,r),Ue.init(t,r)}),M8=j("ZodBase64",(t,r)=>{m3.init(t,r),Ue.init(t,r)}),F8=j("ZodBase64URL",(t,r)=>{h3.init(t,r),Ue.init(t,r)}),U8=j("ZodE164",(t,r)=>{g3.init(t,r),Ue.init(t,r)}),Z8=j("ZodJWT",(t,r)=>{_3.init(t,r),Ue.init(t,r)}),Vm=j("ZodNumber",(t,r)=>{Rm.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(s,u,p)=>M_(t,s,u),ei(t,"ZodNumber",{gt(s,u){return this.check(Sa(s,u))},gte(s,u){return this.check(Fn(s,u))},min(s,u){return this.check(Fn(s,u))},lt(s,u){return this.check(Ia(s,u))},lte(s,u){return this.check(Gr(s,u))},max(s,u){return this.check(Gr(s,u))},int(s){return this.check(Be(s))},safe(s){return this.check(Be(s))},positive(s){return this.check(Sa(0,s))},nonnegative(s){return this.check(Fn(0,s))},negative(s){return this.check(Ia(0,s))},nonpositive(s){return this.check(Gr(0,s))},multipleOf(s,u){return this.check(Yl(s,u))},step(s,u){return this.check(Yl(s,u))},finite(){return this}});const i=t._zod.bag;t.minValue=Math.max(i.minimum??Number.NEGATIVE_INFINITY,i.exclusiveMinimum??Number.NEGATIVE_INFINITY)??null,t.maxValue=Math.min(i.maximum??Number.POSITIVE_INFINITY,i.exclusiveMaximum??Number.POSITIVE_INFINITY)??null,t.isInt=(i.format??"").includes("int")||Number.isSafeInteger(i.multipleOf??.5),t.isFinite=!0,t.format=i.format??null});function pr(t){return h_(Vm,t)}const q8=j("ZodNumberFormat",(t,r)=>{w3.init(t,r),Vm.init(t,r)});function Be(t){return g_(q8,t)}const V8=j("ZodBoolean",(t,r)=>{x3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>F_(t,i,s)});function Z(t){return y_(V8,t)}const W8=j("ZodBigInt",(t,r)=>{E3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(s,u,p)=>U_(t,s),t.gte=(s,u)=>t.check(Fn(s,u)),t.min=(s,u)=>t.check(Fn(s,u)),t.gt=(s,u)=>t.check(Sa(s,u)),t.gte=(s,u)=>t.check(Fn(s,u)),t.min=(s,u)=>t.check(Fn(s,u)),t.lt=(s,u)=>t.check(Ia(s,u)),t.lte=(s,u)=>t.check(Gr(s,u)),t.max=(s,u)=>t.check(Gr(s,u)),t.positive=s=>t.check(Sa(BigInt(0),s)),t.negative=s=>t.check(Ia(BigInt(0),s)),t.nonpositive=s=>t.check(Gr(BigInt(0),s)),t.nonnegative=s=>t.check(Fn(BigInt(0),s)),t.multipleOf=(s,u)=>t.check(Yl(s,u));const i=t._zod.bag;t.minValue=i.minimum??null,t.maxValue=i.maximum??null,t.format=i.format??null}),H8=j("ZodUnknown",(t,r)=>{I3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>q_()});function Gn(){return w_(H8)}const G8=j("ZodNever",(t,r)=>{S3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>Z_(t,i,s)});function $a(t){return x_(G8,t)}const J8=j("ZodArray",(t,r)=>{k3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>J_(t,i,s,u),t.element=r.element,ei(t,"ZodArray",{min(i,s){return this.check(ka(i,s))},nonempty(i){return this.check(ka(1,i))},max(i,s){return this.check($m(i,s))},length(i,s){return this.check(Lm(i,s))},unwrap(){return this.element}})});function P(t,r){return N_(J8,t,r)}const K8=j("ZodObject",(t,r)=>{z3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>K_(t,i,s,u),ze(t,"shape",()=>r.shape),ei(t,"ZodObject",{keyof(){return Kt(Object.keys(this._zod.def.shape))},catchall(i){return this.clone({...this._zod.def,catchall:i})},passthrough(){return this.clone({...this._zod.def,catchall:Gn()})},loose(){return this.clone({...this._zod.def,catchall:Gn()})},strict(){return this.clone({...this._zod.def,catchall:$a()})},strip(){return this.clone({...this._zod.def,catchall:void 0})},extend(i){return q7(this,i)},safeExtend(i){return V7(this,i)},merge(i){return W7(this,i)},pick(i){return U7(this,i)},omit(i){return Z7(this,i)},partial(...i){return H7(Gm,this,i[0])},required(...i){return G7(Jm,this,i[0])}})});function h(t,r){const i={type:"object",shape:t??{},...ie(r)};return new K8(i)}const Wm=j("ZodUnion",(t,r)=>{Am.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>Q_(t,i,s,u),t.options=r.options});function Qn(t,r){return new Wm({type:"union",options:t,...ie(r)})}const Q8=j("ZodDiscriminatedUnion",(t,r)=>{Wm.init(t,r),C3.init(t,r)});function Hm(t,r,i){return new Q8({type:"union",options:r,discriminator:t,...ie(i)})}const Y8=j("ZodIntersection",(t,r)=>{T3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>Y_(t,i,s,u)});function X8(t,r){return new Y8({type:"intersection",left:t,right:r})}const Tf=j("ZodRecord",(t,r)=>{B3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>X_(t,i,s,u),t.keyType=r.keyType,t.valueType=r.valueType});function fe(t,r,i){return!r||!r._zod?new Tf({type:"record",keyType:o(),valueType:t,...ie(r)}):new Tf({type:"record",keyType:t,valueType:r,...ie(i)})}const Xl=j("ZodEnum",(t,r)=>{R3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(s,u,p)=>V_(t,s,u),t.enum=r.entries,t.options=Object.values(r.entries);const i=new Set(Object.keys(r.entries));t.extract=(s,u)=>{const p={};for(const d of s)if(i.has(d))p[d]=r.entries[d];else throw new Error(`Key ${d} not found in enum`);return new Xl({...r,checks:[],...ie(u),entries:p})},t.exclude=(s,u)=>{const p={...r.entries};for(const d of s)if(i.has(d))delete p[d];else throw new Error(`Key ${d} not found in enum`);return new Xl({...r,checks:[],...ie(u),entries:p})}});function Kt(t,r){const i=Array.isArray(t)?Object.fromEntries(t.map(s=>[s,s])):t;return new Xl({type:"enum",entries:i,...ie(r)})}const ew=j("ZodLiteral",(t,r)=>{P3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>W_(t,i,s),t.values=new Set(r.values),Object.defineProperty(t,"value",{get(){if(r.values.length>1)throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");return r.values[0]}})});function x(t,r){return new ew({type:"literal",values:Array.isArray(t)?t:[t],...ie(r)})}const tw=j("ZodTransform",(t,r)=>{N3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>G_(t,i),t._zod.parse=(i,s)=>{if(s.direction==="backward")throw new hm(t.constructor.name);i.addIssue=p=>{if(typeof p=="string")i.issues.push(Yo(p,i.value,r));else{const d=p;d.fatal&&(d.continue=!1),d.code??(d.code="custom"),d.input??(d.input=i.value),d.inst??(d.inst=t),i.issues.push(Yo(d))}};const u=r.transform(i.value,i);return u instanceof Promise?u.then(p=>(i.value=p,i.fallback=!0,i)):(i.value=u,i.fallback=!0,i)}});function nw(t){return new tw({type:"transform",transform:t})}const Gm=j("ZodOptional",(t,r)=>{Om.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>Um(t,i,s,u),t.unwrap=()=>t._zod.def.innerType});function Bf(t){return new Gm({type:"optional",innerType:t})}const rw=j("ZodExactOptional",(t,r)=>{A3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>Um(t,i,s,u),t.unwrap=()=>t._zod.def.innerType});function ow(t){return new rw({type:"optional",innerType:t})}const iw=j("ZodNullable",(t,r)=>{O3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>e8(t,i,s,u),t.unwrap=()=>t._zod.def.innerType});function Rf(t){return new iw({type:"nullable",innerType:t})}const aw=j("ZodDefault",(t,r)=>{j3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>n8(t,i,s,u),t.unwrap=()=>t._zod.def.innerType,t.removeDefault=t.unwrap});function sw(t,r){return new aw({type:"default",innerType:t,get defaultValue(){return typeof r=="function"?r():_m(r)}})}const lw=j("ZodPrefault",(t,r)=>{$3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>r8(t,i,s,u),t.unwrap=()=>t._zod.def.innerType});function uw(t,r){return new lw({type:"prefault",innerType:t,get defaultValue(){return typeof r=="function"?r():_m(r)}})}const Jm=j("ZodNonOptional",(t,r)=>{L3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>t8(t,i,s,u),t.unwrap=()=>t._zod.def.innerType});function cw(t,r){return new Jm({type:"nonoptional",innerType:t,...ie(r)})}const dw=j("ZodCatch",(t,r)=>{D3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>o8(t,i,s,u),t.unwrap=()=>t._zod.def.innerType,t.removeCatch=t.unwrap});function pw(t,r){return new dw({type:"catch",innerType:t,catchValue:typeof r=="function"?r:()=>r})}const fw=j("ZodPipe",(t,r)=>{M3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>i8(t,i,s,u),t.in=r.in,t.out=r.out});function Pf(t,r){return new fw({type:"pipe",in:t,out:r})}const mw=j("ZodReadonly",(t,r)=>{F3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>a8(t,i,s,u),t.unwrap=()=>t._zod.def.innerType});function vw(t){return new mw({type:"readonly",innerType:t})}const hw=j("ZodCustom",(t,r)=>{U3.init(t,r),Le.init(t,r),t._zod.processJSONSchema=(i,s,u)=>H_(t,i)});function gw(t,r={}){return A_(hw,t,r)}function yw(t,r){return O_(t,r)}function w(t){return __(W8,t)}const _w=h({MaxMessageLength:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SupportsAttachments:Z(),SupportsChildConversations:Z()}),ti=h({account_id:o(),provider:o()});h({dir:o().optional(),name:o().min(1),provider:o().min(1),scope:o().optional()});h({agent:o(),status:o()});const ww=h({agent_id:o(),parent_tool_use_id:o()});h({dir:o().optional(),env:fe(o(),o()).optional(),name:o().optional(),scope:o().optional(),suspended:Z().optional(),tmux_alias:o().optional(),work_dir:o().optional()});h({agent:o(),bytes:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),prompt:o()});h({provider:o().optional(),scope:o().optional(),suspended:Z().optional()});h({provider:o().optional(),scope:o().optional(),suspended:Z().optional()});const xw=h({dir:o().optional(),is_pool:Z().optional(),name:o(),origin:o(),provider:o().optional(),scope:o().optional(),suspended:Z()}),Ew=h({acp_args:P(o()).optional(),acp_command:o().optional(),args:P(o()).nullish(),command:o().optional(),display_name:o().optional(),env:fe(o(),o()).optional(),origin:o(),prompt_flag:o().optional(),prompt_mode:o().optional(),ready_delay_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});h({event_cursor:o(),request_id:o(),status:o()});h({event_cursor:o(),request_id:o()});h({assignee:o().optional()});h({reason:o().max(1024).optional()});h({assignee:o().optional(),description:o().optional(),labels:P(o()).nullish(),metadata:fe(o(),o()).optional(),parent:o().optional(),priority:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),rig:o().optional(),title:o().min(1),type:o().optional()});h({assignee:o().optional(),description:o().optional(),labels:P(o()).nullish(),metadata:fe(o(),o()).optional(),parent:o().nullish(),priority:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),remove_labels:P(o()).nullish(),status:o().optional(),title:o().optional(),type:o().optional()});const Iw=Kt(["active","ended"]),yu=h({conversation_id:o(),provider:o(),session_id:o()});h({bootstrap_profile:Kt(["k8s-cell","kubernetes","kubernetes-cell","single-host-compat"]).optional(),dir:o().min(1),provider:o().min(1).optional(),start_command:o().optional()});const _u=h({name:o(),path:o(),request_id:o()});h({agent_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),name:o(),path:o(),provider:o().optional(),rig_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_template:o().optional(),suspended:Z(),uptime_sec:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:o().optional()});const Sw=h({error:o().optional(),name:o(),path:o(),phases_completed:P(o()).nullish(),running:Z(),status:o().optional()}),ni=h({name:o(),path:o()});h({suspended:Z().optional()});const wu=h({name:o(),path:o(),request_id:o()}),kw=h({dir:o().optional(),is_pool:Z().optional(),name:o(),provider:o().optional(),scope:o().optional(),suspended:Z()}),bw=h({agents:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),providers:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),rigs:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({agents:P(xw).nullable(),patches:bw,providers:fe(o(),Ew)});const zw=h({agent_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),provider_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),rig_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),Cw=h({name:o(),path:o(),prefix:o().optional(),suspended:Z()});h({errors:P(o()).nullable(),valid:Z(),warnings:P(o()).nullable()});h({GroupID:o(),Handle:o(),ID:o(),Metadata:fe(o(),o()),Public:Z(),SessionID:o()});const Tw=Kt(["dm","room","thread"]),Yt=h({account_id:o(),conversation_id:o(),kind:Tw,parent_conversation_id:o().optional(),provider:o(),scope_id:o()});h({items:P(o()).nullish()});h({closed:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),complete:Z(),convoy_id:o(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({items:P(o()).nullish(),rig:o().optional(),title:o().min(1)});const Bw=h({closed:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({items:P(o()).nullish()});const Rw=h({BindingGeneration:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),Conversation:Yt,ID:o(),LastMessageID:o(),LastPublishedAt:N({offset:!0}),Metadata:fe(o(),o()),SchemaVersion:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SessionID:o(),SourceSessionID:o()}),Pw=h({depends_on_id:o(),issue_id:o(),type:o()}),fr=h({assignee:o().optional(),created_at:N({offset:!0}),dependencies:P(Pw).nullish(),description:o().optional(),ephemeral:Z().optional(),from:o().optional(),id:o(),issue_type:o(),labels:P(o()).nullish(),metadata:fe(o(),o()).optional(),needs:P(o()).nullish(),parent:o().optional(),priority:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullish(),ref:o().optional(),status:o(),title:o(),updated_at:N({offset:!0}).optional()});h({children:P(fr).nullable()});const gr=h({bead:fr});h({children:P(fr).nullish(),convoy:fr.optional(),progress:Bw.optional()});const Nw=h({location:o().optional(),message:o().optional(),value:Gn().optional()});h({detail:o().optional(),errors:P(Nw).nullish(),instance:Cf().optional(),status:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),title:o().optional(),type:Cf().optional().default("about:blank")});h({status:o()});h({actor:o().min(1),message:o().optional(),subject:o().optional(),type:o().min(1)});const Aw=h({seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),ts:N({offset:!0}),type:o()}),Ow=h({compression_status:Kt(["pending","complete"]),first_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),last_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),path:o()});h({anchor_event:Aw.optional(),archive:Ow.optional(),reason:o().optional(),rotated:Z()});h({account_id:o().min(1),callback_url:o().optional(),capabilities:_w.optional(),name:o().optional(),provider:o().min(1)});h({account_id:o(),name:o(),provider:o(),status:o()});h({account_id:o().min(1),provider:o().min(1)});h({conversation:Yt.optional(),metadata:fe(o(),o()).optional(),session_id:o().min(1)});h({default_handle:o().optional(),metadata:fe(o(),o()).optional(),mode:o().optional(),root_conversation:Yt.optional()});h({conversation:Yt.optional(),idempotency_key:o().optional(),reply_to_message_id:o().optional(),session_id:o().min(1),text:o().optional()});h({group_id:o().min(1),handle:o().min(1)});h({group_id:o().min(1),handle:o().min(1),metadata:fe(o(),o()).optional(),public:Z().optional(),session_id:o().min(1)});h({conversation:Yt.optional(),sequence:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),session_id:o().min(1)});h({conversation:Yt.optional(),session_id:o().min(1)});const Km=h({display_name:o(),id:o(),is_bot:Z()}),Qm=h({mime_type:o(),provider_id:o(),url:o()}),Ym=h({actor:Km,attachments:P(Qm).nullish(),conversation:Yt,dedup_key:o().optional(),explicit_target:o().optional(),provider_message_id:o(),received_at:N({offset:!0}),reply_to_message_id:o().optional(),text:o()});h({account_id:o().optional(),message:Ym.optional(),payload:o().optional(),provider:o().optional()});const jw=h({account_id:o(),name:o(),provider:o()}),$w=h({AllowUntargetedPublication:Z(),Enabled:Z(),MaxPeerTriggeredPublishes:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),MaxTotalPeerDeliveries:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({DefaultHandle:o(),FanoutPolicy:$w,ID:o(),LastAddressedHandle:o(),Metadata:fe(o(),o()),Mode:o(),RootConversation:Yt,SchemaVersion:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({scope_kind:o().optional(),scope_ref:o().optional(),target:o().min(1),vars:fe(o(),o()).optional()});const Xm=h({from:o(),kind:o().optional(),to:o()}),Lw=h({id:o(),kind:o(),scope_ref:o().optional(),title:o()}),Dw=h({edges:P(Xm).nullable(),nodes:P(Lw).nullable()}),ev=h({started_at:o(),status:o(),target:o(),updated_at:o(),workflow_id:o()});h({formula:o(),partial:Z(),partial_errors:P(o()).nullish(),recent_runs:P(ev).nullable(),run_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Mw=h({assignee:o().optional(),id:o(),kind:o(),labels:P(o()).nullish(),metadata:fe(o(),o()).optional(),title:o(),type:o().optional()}),tv=h({default:Gn().optional(),description:o().optional(),enum:P(o()).nullish(),name:o(),pattern:o().optional(),required:Z().optional(),type:o()});h({deps:P(Xm).nullable(),description:o(),name:o(),preview:Dw,steps:P(Mw).nullable(),var_defs:P(tv).nullable(),version:o()});const Fw=h({description:o(),name:o(),recent_runs:P(ev).nullable(),run_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),var_defs:P(tv).nullable(),version:o()});h({items:P(Fw).nullable(),partial:Z(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Uw=h({ahead:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),behind:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),branch:o(),changed_files:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),clean:Z()}),xu=h({conversation_id:o(),mode:o(),provider:o()}),Zw=h({Match:o(),TargetSessionID:o(),UpdateCursor:Z()});h({city:o().optional(),status:o(),uptime_sec:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:o().optional()});const oo=h({timestamp:o()}),Eu=h({actor:o(),conversation_id:o(),provider:o(),target_session:o()});h({items:P(fr).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({items:P(jw).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const qw=fe(o(),$a());h({partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),unread:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({body:o().optional(),from:o().optional(),subject:o().optional()});h({body:o().optional(),from:o().optional(),rig:o().optional(),subject:o().min(1),to:o().min(1)});const nv=h({body:o(),cc:P(o()).nullish(),created_at:N({offset:!0}),from:o(),id:o(),priority:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),read:Z(),reply_to:o().optional(),rig:o().optional(),subject:o(),thread_id:o().optional(),to:o()}),mt=h({message:nv.optional(),rig:o()});h({items:P(nv).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const rv=h({attached_bead_id:o().optional(),bead_id:o().optional(),detail_available:Z().optional(),id:o(),logical_bead_id:o().optional(),root_bead_id:o().optional(),root_store_ref:o().optional(),run_detail_available:Z().optional(),scope_kind:o(),scope_ref:o(),started_at:o(),status:o(),store_ref:o().optional(),target:o(),title:o(),type:o(),updated_at:o(),workflow_id:o().optional()});h({items:P(rv).nullable(),partial:Z(),partial_errors:P(o()).nullish()});const ve=fe(o(),$a());h({status:o()});h({id:o().optional(),status:o()});const Vw=h({label:o(),value:o()}),Ww=h({due:Z(),last_run:o().optional(),last_run_outcome:o().optional(),name:o(),reason:o(),rig:o().optional(),scoped_name:o()});h({checks:P(Ww).nullable()});h({bead_id:o(),created_at:o(),labels:P(o()).nullable(),output:o(),store_ref:o()});const Hw=h({bead_id:o(),capture_output:Z(),created_at:o(),duration_ms:o().optional(),error:o().optional(),exit_code:o().optional(),has_output:Z(),labels:P(o()).nullable(),name:o(),rig:o().optional(),scoped_name:o(),signal:o().optional(),store_ref:o(),wisp_root_id:o().optional()});h({entries:P(Hw).nullable()});const Gw=h({capture_output:Z(),check:o().optional(),description:o().optional(),enabled:Z(),exec:o().optional(),formula:o().optional(),gate:o().optional(),interval:o().optional(),name:o(),on:o().optional(),pool:o().optional(),rig:o().optional(),schedule:o().optional(),scoped_name:o(),timeout:o().optional(),timeout_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),trigger:o().optional(),type:o()});h({orders:P(Gw).nullable()});h({items:P(rv).nullable(),partial:Z(),partial_errors:P(o()).nullish()});const Iu=h({conversation_id:o(),message_id:o(),provider:o(),session:o()}),Su=h({role:o(),text:o(),timestamp:o().optional()}),Jw=h({name:o(),path:o().optional(),ref:o().optional(),source:o().optional()});h({packs:P(Jw).nullable()});const La=h({has_older_messages:Z(),returned_message_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total_compactions:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total_message_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),truncated_before_message:o().optional()}),ov=h({agent:o(),format:o(),pagination:La.optional(),turns:P(Su).nullable()});h({agent_patch:o().optional(),provider_patch:o().optional(),rig_patch:o().optional(),status:o()});h({agent_patch:o().optional(),provider_patch:o().optional(),rig_patch:o().optional(),status:o()});const ku=h({kind:o(),metadata:fe(o(),o()).optional(),options:P(o()).nullish(),prompt:o().optional(),request_id:o()}),Kw=h({Check:o().nullable(),DrainTimeout:o().nullable(),Max:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),Min:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),OnBoot:o().nullable(),OnDeath:o().nullable()}),Qw=h({AppendFragments:P(o()).nullable(),Attach:Z().nullable(),DefaultSlingFormula:o().nullable(),DependsOn:P(o()).nullable(),Dir:o(),Env:fe(o(),o()),EnvRemove:P(o()).nullable(),HooksInstalled:Z().nullable(),IdleTimeout:o().nullable(),InjectAssignedSkills:Z().nullable(),InjectFragments:P(o()).nullable(),InjectFragmentsAppend:P(o()).nullable(),InstallAgentHooks:P(o()).nullable(),InstallAgentHooksAppend:P(o()).nullable(),Lifecycle:o().nullable(),MCP:P(o()).nullable(),MCPAppend:P(o()).nullable(),MaxActiveSessions:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),MaxSessionAge:o().nullable(),MaxSessionAgeJitter:o().nullable(),MinActiveSessions:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),MouseMode:o().nullable(),Name:o(),Nudge:o().nullable(),OptionDefaults:fe(o(),o()),OverlayDir:o().nullable(),Pool:Kw,PreStart:P(o()).nullable(),PreStartAppend:P(o()).nullable(),PromptTemplate:o().nullable(),Provider:o().nullable(),ResumeCommand:o().nullable(),ScaleCheck:o().nullable(),Scope:o().nullable(),Session:o().nullable(),SessionLive:P(o()).nullable(),SessionLiveAppend:P(o()).nullable(),SessionSetup:P(o()).nullable(),SessionSetupAppend:P(o()).nullable(),SessionSetupScript:o().nullable(),Skills:P(o()).nullable(),SkillsAppend:P(o()).nullable(),SleepAfterIdle:o().nullable(),StartCommand:o().nullable(),Suspended:Z().nullable(),TmuxAlias:o().nullable(),WakeMode:o().nullable(),WorkDir:o().nullable()});h({items:P(Qw).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const bu=h({host:o(),port:o(),scope_kind:o(),scope_name:o(),source:o(),user:o()}),zu=h({layer:o(),new_id:o(),old_id:o().optional(),scope_root:o(),source:o()});h({acp_args:P(o()).nullish(),acp_command:o().optional(),args:P(o()).nullish(),args_append:P(o()).nullish(),base:o().optional(),command:o().optional(),display_name:o().optional(),env:fe(o(),o()).optional(),name:o().min(1),option_defaults:fe(o(),o()).optional(),options_schema_merge:o().optional(),prompt_flag:o().optional(),prompt_mode:o().optional(),ready_delay_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});h({provider:o(),status:o()});const Yw=h({choices:P(Vw).nullable(),default:o(),key:o(),label:o(),type:o()}),Xw=h({ACPArgs:P(o()).nullable(),ACPCommand:o().nullable(),AcceptStartupDialogs:Z().nullable(),Args:P(o()).nullable(),ArgsAppend:P(o()).nullable(),Base:o().nullable(),Command:o().nullable(),Env:fe(o(),o()),EnvRemove:P(o()).nullable(),Name:o(),OptionsSchemaMerge:o().nullable(),PromptFlag:o().nullable(),PromptMode:o().nullable(),ReadyDelayMs:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),Replace:Z()});h({items:P(Xw).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({accept_startup_dialogs:Z().optional(),acp_args:P(o()).nullish(),acp_command:o().optional(),args:P(o()).nullish(),command:o().optional(),env:fe(o(),o()).optional(),name:o().optional(),prompt_flag:o().optional(),prompt_mode:o().optional(),ready_delay_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});const e5=h({builtin:Z(),city_level:Z(),display_name:o().optional(),effective_defaults:fe(o(),o()).optional(),name:o(),options_schema:P(Yw).nullish()});h({items:P(e5).nullable(),next_cursor:o().optional(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const t5=h({detail:o().optional(),display_name:o(),status:o()});h({providers:fe(o(),t5)});const n5=h({acp_args:P(o()).optional(),acp_command:o().optional(),args:P(o()).nullish(),builtin:Z(),city_level:Z(),command:o().optional(),display_name:o().optional(),env:fe(o(),o()).optional(),name:o(),prompt_flag:o().optional(),prompt_mode:o().optional(),ready_delay_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});h({items:P(n5).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const r5=h({acp_args:P(o()).optional(),acp_command:o().optional(),args:P(o()).nullish(),command:o().optional(),display_name:o().optional(),env:fe(o(),o()).optional(),prompt_flag:o().optional(),prompt_mode:o().optional(),ready_delay_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});h({acp_args:P(o()).nullish(),acp_command:o().optional(),args:P(o()).nullish(),args_append:P(o()).nullish(),base:o().optional(),command:o().optional(),display_name:o().optional(),env:fe(o(),o()).optional(),option_defaults:fe(o(),o()).optional(),options_schema_merge:o().optional(),prompt_flag:o().optional(),prompt_mode:o().optional(),ready_delay_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});const o5=h({Conversation:Yt,Delivered:Z(),FailureKind:o(),MessageID:o(),Metadata:fe(o(),o()),RetryAfter:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),i5=h({detail:o().optional(),display_name:o(),kind:o(),name:o(),status:o()});h({items:fe(o(),i5)});const Cu=h({error_code:o(),error_message:o(),operation:Kt(["city.create","city.unregister","session.create","session.message","session.submit"]),request_id:o()});h({action:o(),failed:P(o()).nullish(),killed:P(o()).nullish(),rig:o(),status:o()});h({default_branch:o().optional(),name:o().min(1),path:o().min(1),prefix:o().optional()});h({rig:o(),status:o()});const a5=h({DefaultBranch:o().nullable(),FormulaVars:fe(o(),o()),Name:o(),Path:o().nullable(),Prefix:o().nullable(),Suspended:Z().nullable()});h({items:P(a5).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({default_branch:o().optional(),name:o().optional(),path:o().optional(),prefix:o().optional(),suspended:Z().optional()});const s5=h({agent_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),default_branch:o().optional(),git:Uw.optional(),last_activity:N({offset:!0}).optional(),name:o(),path:o(),prefix:o().optional(),running_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),suspended:Z()});h({items:P(s5).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({default_branch:o().optional(),path:o().optional(),prefix:o().optional(),suspended:Z().optional()});const Tu=h({prior_archive:o(),prior_first_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),prior_last_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),l5=fe(o(),$a());h({action:o(),service:o(),status:o()});const iv=h({activity:o()});h({messages:P(Gn()).nullable(),status:o().optional()});h({agents:P(ww).nullable()});const Bu=h({BindingGeneration:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),BoundAt:N({offset:!0}),Conversation:Yt,ExpiresAt:N({offset:!0}).nullable(),ID:o(),Metadata:fe(o(),o()),SchemaVersion:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SessionID:o(),Status:Iw});h({unbound:P(Bu).nullable()});h({items:P(Bu).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({alias:o().optional(),async:Z().optional(),kind:o().optional(),message:o().optional(),name:o().optional(),options:fe(o(),o()).optional(),project_id:o().optional(),session_name:o().optional(),title:o().optional()});const Ru=h({bead_id:o(),bead_status:o().optional(),reason:o().optional(),session_id:o(),template:o().optional()}),u5=h({attached:Z(),last_activity:N({offset:!0}).optional(),name:o()}),c5=h({active_bead:o().optional(),activity:o().optional(),available:Z(),context_pct:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),context_window:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),description:o().optional(),display_name:o().optional(),last_output:o().optional(),model:o().optional(),name:o(),pool:o().optional(),provider:o().optional(),rig:o().optional(),running:Z(),session:u5.optional(),state:o(),suspended:Z(),unavailable_reason:o().optional()});h({items:P(c5).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const yr=h({reason:o().optional(),session_id:o(),template:o().optional()});h({message:o().min(1).regex(/\S/)});const Pu=h({request_id:o(),session_id:o()});h({alias:o().optional(),title:o().min(1).optional()});h({pending:ku.optional(),supported:Z()});h({permission_mode:o().min(1).regex(/\S/)});const av=Gn();h({title:o().min(1)});h({action:o().min(1),metadata:fe(o(),o()).optional(),request_id:o().optional(),text:o().optional()});h({id:o(),status:o()});Qn([iv,ku,oo]);const d5=h({format:o(),id:o(),pagination:La.optional(),provider:o(),template:o(),turns:P(Su).nullable()}),p5=h({format:o(),id:o(),messages:P(av).nullable(),pagination:La.optional(),provider:o(),template:o()}),Nu=h({intent:o(),queued:Z(),request_id:o(),session_id:o()});h({format:o(),id:o(),messages:P(av).nullish(),pagination:La.optional(),provider:o(),template:o(),turns:P(Su).nullish()});h({attached_bead_id:o().optional(),bead:o().optional(),force:Z().optional(),formula:o().optional(),rig:o().optional(),scope_kind:o().optional(),scope_ref:o().optional(),target:o().min(1),title:o().optional(),vars:fe(o(),o()).optional()});h({attached_bead_id:o().optional(),bead:o().optional(),formula:o().optional(),mode:o().optional(),root_bead_id:o().optional(),status:o(),target:o(),warnings:P(o()).nullish(),workflow_id:o().optional()});const f5=h({allow_websockets:Z().optional(),hostname:o().optional(),kind:o().optional(),local_state:o(),mount_path:o(),publication_state:o(),publish_mode:o(),reason:o().optional(),service_name:o(),state:o().optional(),state_root:o(),updated_at:N({offset:!0}),url:o().optional(),visibility:o().optional(),workflow_contract:o().optional()});h({items:P(f5).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const m5=h({quarantined:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),running:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),suspended:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),v5=h({draining:Z().optional(),expanded:Z().optional(),group_name:o().optional(),name:o(),qualified_name:o(),running:Z(),scale_label:o().optional(),scope:o(),session_name:o().optional(),suspended:Z()}),h5=h({total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),unread:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),g5=h({identity:o(),mode:o(),status:o()}),y5=h({suspended:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),_5=h({name:o(),path:o(),suspended:Z()}),w5=h({active:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),suspended:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),x5=h({last_gc_at:o().optional(),last_gc_status:o().optional(),live_rows:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),path:o(),ratio_mb_per_row:pr(),size_bytes:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),threshold_mb_per_row:pr(),warning:Z()}),E5=h({in_progress:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),open:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),ready:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({agent_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),agent_details:P(v5).nullish(),agents:m5,mail:h5,name:o(),named_session_details:P(g5).nullish(),partial:Z().optional(),partial_errors:P(o()).nullish(),path:o(),rig_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),rig_details:P(_5).nullish(),rigs:y5,running:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_counts_detail:w5.optional(),store_health:x5.optional(),suspended:Z(),uptime_sec:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:o().optional(),work:E5});const Au=h({after_bytes:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),before_bytes:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),duration_s:pr(),snapshot_path:o()}),Ou=h({duration_s:pr(),error_msg:o(),snapshot_path:o().optional(),stage:o()}),I5=h({supports_follow_up:Z(),supports_interrupt_now:Z()}),sv=h({active_bead:o().optional(),activity:o().optional(),agent_kind:o().optional(),alias:o().optional(),attached:Z(),configured_named_session:Z().optional(),context_pct:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),context_window:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),created_at:o(),display_name:o().optional(),id:o(),kind:o().optional(),last_active:o().optional(),last_nudge_delivered_at:o().optional(),last_output:o().optional(),metadata:fe(o(),o()).optional(),model:o().optional(),options:fe(o(),o()).optional(),pool:o().optional(),provider:o(),reason:o().optional(),rig:o().optional(),running:Z(),session_name:o(),state:o(),submission_capabilities:I5.optional(),template:o(),title:o()});h({items:P(sv).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const ju=h({request_id:o(),session:sv}),S5=Kt(["default","follow_up","interrupt_now"]);h({intent:S5.optional(),message:o().min(1).regex(/\S/)});h({items:P(Sw).nullable(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const $u=h({avg60:pr(),consecutive_skips:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),max_consecutive_skips:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),outcome:o(),threshold:pr(),trigger:o().optional()}),Lu=h({client_addr:o().optional(),mode:Kt(["destructive","preserve_sessions","unknown"]),signal:o().optional(),source:Kt(["signal","socket_stop"])}),k5=h({phase:o().optional(),phases_completed:P(o()).nullish(),ready:Z()});h({build_id:o().optional(),cities_running:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),cities_total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),startup:k5.optional(),status:o(),uptime_sec:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:o()});const b5=Kt(["inbound","outbound"]),z5=Kt(["live","hydrated"]),Du=h({Actor:Km,Attachments:P(Qm).nullable(),Conversation:Yt,CreatedAt:N({offset:!0}),ExplicitTarget:o(),ID:o(),Kind:b5,Metadata:fe(o(),o()),Provenance:z5,ProviderMessageID:o(),ReplyToMessageID:o(),SchemaVersion:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),Sequence:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SourceSessionID:o(),Text:o()});h({Binding:Bu,GroupRoute:Zw,Message:Ym,TargetSessionID:o(),TranscriptEntry:Du});h({items:P(Du).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({DeliveryContext:Rw,Receipt:o5,TranscriptEntry:Du});const Mu=h({count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o()}),Fu=h({agent_name:o().optional(),bead_id:o().optional(),cache_creation_tokens:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),cache_read_tokens:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),completion_tokens:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),cost_usd_estimate:pr().optional(),delivered:Z().optional(),duration_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),error:o().optional(),finished_at:N({offset:!0}),latency_ms:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),model:o().optional(),op_id:o(),operation:o(),prompt_sha:o().optional(),prompt_tokens:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),prompt_version:o().optional(),provider:o().optional(),queued:Z().optional(),result:o(),session_id:o().optional(),session_name:o().optional(),started_at:N({offset:!0}),template:o().optional(),transport:o().optional()}),lv=Qn([ti,gr,yu,_u,ni,wu,xu,Eu,mt,ve,Iu,bu,zu,Cu,Tu,ju,Ru,yr,Pu,Nu,Au,Ou,$u,Lu,Mu,Fu]),C5=h({active_attempt:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),attempt_count:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),max_attempts:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()}),uv=h({assignee:o().optional(),attempt:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),id:o(),kind:o(),logical_bead_id:o().optional(),metadata:fe(o(),o()),scope_ref:o().optional(),status:o(),step_ref:o().optional(),title:o()});h({closed:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),deleted:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),partial:Z().optional(),partial_errors:P(o()).nullish(),workflow_id:o()});const eu=h({from:o(),kind:o().optional(),to:o()});h({beads:P(fr).nullable(),deps:P(eu).nullable(),root:fr});const L=h({attempt_summary:C5.optional(),bead:uv,changed_fields:P(o()).nullable(),event_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),event_ts:o(),event_type:o(),logical_node_id:o(),requires_resync:Z().optional(),root_bead_id:o(),root_store_ref:o(),scope_kind:o(),scope_ref:o(),type:o(),watch_generation:o(),workflow_id:o(),workflow_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({actor:o(),message:o().optional(),payload:lv.optional(),run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:o(),workflow:L.optional()});h({actor:o(),city:o(),message:o().optional(),payload:lv.optional(),run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:o(),workflow:L.optional()});const T5=h({actor:o(),message:o().optional(),payload:gr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("bead.closed"),workflow:L.optional()}),B5=h({actor:o(),message:o().optional(),payload:gr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("bead.created"),workflow:L.optional()}),R5=h({actor:o(),message:o().optional(),payload:gr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("bead.updated"),workflow:L.optional()}),P5=h({actor:o(),message:o().optional(),payload:ni,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.created"),workflow:L.optional()}),N5=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.resumed"),workflow:L.optional()}),A5=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.suspended"),workflow:L.optional()}),O5=h({actor:o(),message:o().optional(),payload:ni,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.unregister_requested"),workflow:L.optional()}),j5=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("controller.started"),workflow:L.optional()}),$5=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("controller.stopped"),workflow:L.optional()}),L5=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("convoy.closed"),workflow:L.optional()}),D5=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("convoy.created"),workflow:L.optional()}),M5=h({actor:o(),message:o().optional(),payload:Gn(),run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:o(),workflow:L.optional()}),F5=h({actor:o(),message:o().optional(),payload:Tu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("events.rotated"),workflow:L.optional()}),U5=h({actor:o(),message:o().optional(),payload:ti,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.adapter_added"),workflow:L.optional()}),Z5=h({actor:o(),message:o().optional(),payload:ti,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.adapter_removed"),workflow:L.optional()}),q5=h({actor:o(),message:o().optional(),payload:yu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.bound"),workflow:L.optional()}),V5=h({actor:o(),message:o().optional(),payload:xu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.group_created"),workflow:L.optional()}),W5=h({actor:o(),message:o().optional(),payload:Eu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.inbound"),workflow:L.optional()}),H5=h({actor:o(),message:o().optional(),payload:Iu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.outbound"),workflow:L.optional()}),G5=h({actor:o(),message:o().optional(),payload:Mu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.unbound"),workflow:L.optional()}),J5=h({actor:o(),message:o().optional(),payload:Au,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("gc.store.maintenance.done"),workflow:L.optional()}),K5=h({actor:o(),message:o().optional(),payload:Ou,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("gc.store.maintenance.failed"),workflow:L.optional()}),Q5=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.archived"),workflow:L.optional()}),Y5=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.deleted"),workflow:L.optional()}),X5=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.marked_read"),workflow:L.optional()}),ex=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.marked_unread"),workflow:L.optional()}),tx=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.read"),workflow:L.optional()}),nx=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.replied"),workflow:L.optional()}),rx=h({actor:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.sent"),workflow:L.optional()}),ox=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("order.completed"),workflow:L.optional()}),ix=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("order.failed"),workflow:L.optional()}),ax=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("order.fired"),workflow:L.optional()}),sx=h({actor:o(),message:o().optional(),payload:bu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("pg.credential_resolved"),workflow:L.optional()}),lx=h({actor:o(),message:o().optional(),payload:zu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("project.identity.stamped"),workflow:L.optional()}),ux=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("provider.swapped"),workflow:L.optional()}),cx=h({actor:o(),message:o().optional(),payload:Cu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.failed"),workflow:L.optional()}),dx=h({actor:o(),message:o().optional(),payload:_u,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.city.create"),workflow:L.optional()}),px=h({actor:o(),message:o().optional(),payload:wu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.city.unregister"),workflow:L.optional()}),fx=h({actor:o(),message:o().optional(),payload:ju,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.session.create"),workflow:L.optional()}),mx=h({actor:o(),message:o().optional(),payload:Pu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.session.message"),workflow:L.optional()}),vx=h({actor:o(),message:o().optional(),payload:Nu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.session.submit"),workflow:L.optional()}),hx=h({actor:o(),message:o().optional(),payload:yr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.crashed"),workflow:L.optional()}),gx=h({actor:o(),message:o().optional(),payload:Ru,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.drain_acked_with_assigned_work"),workflow:L.optional()}),yx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.draining"),workflow:L.optional()}),_x=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.idle_killed"),workflow:L.optional()}),wx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.max_age_killed"),workflow:L.optional()}),xx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.quarantined"),workflow:L.optional()}),Ex=h({actor:o(),message:o().optional(),payload:yr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.stopped"),workflow:L.optional()}),Ix=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.stranded"),workflow:L.optional()}),Sx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.suspended"),workflow:L.optional()}),kx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.undrained"),workflow:L.optional()}),bx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.updated"),workflow:L.optional()}),zx=h({actor:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.woke"),workflow:L.optional()}),Cx=h({actor:o(),message:o().optional(),payload:yr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.work_query_failed"),workflow:L.optional()}),Tx=h({actor:o(),message:o().optional(),payload:$u,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("supervisor.fs_pressure.skipped_tick"),workflow:L.optional()}),Bx=h({actor:o(),message:o().optional(),payload:Lu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("supervisor.shutdown_requested"),workflow:L.optional()}),Rx=h({actor:o(),message:o().optional(),payload:Fu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("worker.operation"),workflow:L.optional()}),cv=Hm("type",[T5.extend({type:x("bead.closed")}),B5.extend({type:x("bead.created")}),R5.extend({type:x("bead.updated")}),P5.extend({type:x("city.created")}),N5.extend({type:x("city.resumed")}),A5.extend({type:x("city.suspended")}),O5.extend({type:x("city.unregister_requested")}),j5.extend({type:x("controller.started")}),$5.extend({type:x("controller.stopped")}),L5.extend({type:x("convoy.closed")}),D5.extend({type:x("convoy.created")}),F5.extend({type:x("events.rotated")}),U5.extend({type:x("extmsg.adapter_added")}),Z5.extend({type:x("extmsg.adapter_removed")}),q5.extend({type:x("extmsg.bound")}),V5.extend({type:x("extmsg.group_created")}),W5.extend({type:x("extmsg.inbound")}),H5.extend({type:x("extmsg.outbound")}),G5.extend({type:x("extmsg.unbound")}),J5.extend({type:x("gc.store.maintenance.done")}),K5.extend({type:x("gc.store.maintenance.failed")}),Q5.extend({type:x("mail.archived")}),Y5.extend({type:x("mail.deleted")}),X5.extend({type:x("mail.marked_read")}),ex.extend({type:x("mail.marked_unread")}),tx.extend({type:x("mail.read")}),nx.extend({type:x("mail.replied")}),rx.extend({type:x("mail.sent")}),ox.extend({type:x("order.completed")}),ix.extend({type:x("order.failed")}),ax.extend({type:x("order.fired")}),sx.extend({type:x("pg.credential_resolved")}),lx.extend({type:x("project.identity.stamped")}),ux.extend({type:x("provider.swapped")}),cx.extend({type:x("request.failed")}),dx.extend({type:x("request.result.city.create")}),px.extend({type:x("request.result.city.unregister")}),fx.extend({type:x("request.result.session.create")}),mx.extend({type:x("request.result.session.message")}),vx.extend({type:x("request.result.session.submit")}),hx.extend({type:x("session.crashed")}),gx.extend({type:x("session.drain_acked_with_assigned_work")}),yx.extend({type:x("session.draining")}),_x.extend({type:x("session.idle_killed")}),wx.extend({type:x("session.max_age_killed")}),xx.extend({type:x("session.quarantined")}),Ex.extend({type:x("session.stopped")}),Ix.extend({type:x("session.stranded")}),Sx.extend({type:x("session.suspended")}),kx.extend({type:x("session.undrained")}),bx.extend({type:x("session.updated")}),zx.extend({type:x("session.woke")}),Cx.extend({type:x("session.work_query_failed")}),Tx.extend({type:x("supervisor.fs_pressure.skipped_tick")}),Bx.extend({type:x("supervisor.shutdown_requested")}),Rx.extend({type:x("worker.operation")}),M5.extend({type:x("TypedEventStreamEnvelopeCustom")})]);h({items:P(cv).nullable(),next_cursor:o().optional(),partial:Z().optional(),partial_errors:P(o()).nullish(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Px=h({actor:o(),city:o(),message:o().optional(),payload:gr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("bead.closed"),workflow:L.optional()}),Nx=h({actor:o(),city:o(),message:o().optional(),payload:gr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("bead.created"),workflow:L.optional()}),Ax=h({actor:o(),city:o(),message:o().optional(),payload:gr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("bead.updated"),workflow:L.optional()}),Ox=h({actor:o(),city:o(),message:o().optional(),payload:ni,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.created"),workflow:L.optional()}),jx=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.resumed"),workflow:L.optional()}),$x=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.suspended"),workflow:L.optional()}),Lx=h({actor:o(),city:o(),message:o().optional(),payload:ni,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("city.unregister_requested"),workflow:L.optional()}),Dx=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("controller.started"),workflow:L.optional()}),Mx=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("controller.stopped"),workflow:L.optional()}),Fx=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("convoy.closed"),workflow:L.optional()}),Ux=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("convoy.created"),workflow:L.optional()}),Zx=h({actor:o(),city:o(),message:o().optional(),payload:Gn(),run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:o(),workflow:L.optional()}),qx=h({actor:o(),city:o(),message:o().optional(),payload:Tu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("events.rotated"),workflow:L.optional()}),Vx=h({actor:o(),city:o(),message:o().optional(),payload:ti,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.adapter_added"),workflow:L.optional()}),Wx=h({actor:o(),city:o(),message:o().optional(),payload:ti,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.adapter_removed"),workflow:L.optional()}),Hx=h({actor:o(),city:o(),message:o().optional(),payload:yu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.bound"),workflow:L.optional()}),Gx=h({actor:o(),city:o(),message:o().optional(),payload:xu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.group_created"),workflow:L.optional()}),Jx=h({actor:o(),city:o(),message:o().optional(),payload:Eu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.inbound"),workflow:L.optional()}),Kx=h({actor:o(),city:o(),message:o().optional(),payload:Iu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.outbound"),workflow:L.optional()}),Qx=h({actor:o(),city:o(),message:o().optional(),payload:Mu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("extmsg.unbound"),workflow:L.optional()}),Yx=h({actor:o(),city:o(),message:o().optional(),payload:Au,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("gc.store.maintenance.done"),workflow:L.optional()}),Xx=h({actor:o(),city:o(),message:o().optional(),payload:Ou,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("gc.store.maintenance.failed"),workflow:L.optional()}),eE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.archived"),workflow:L.optional()}),tE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.deleted"),workflow:L.optional()}),nE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.marked_read"),workflow:L.optional()}),rE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.marked_unread"),workflow:L.optional()}),oE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.read"),workflow:L.optional()}),iE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.replied"),workflow:L.optional()}),aE=h({actor:o(),city:o(),message:o().optional(),payload:mt,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("mail.sent"),workflow:L.optional()}),sE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("order.completed"),workflow:L.optional()}),lE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("order.failed"),workflow:L.optional()}),uE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("order.fired"),workflow:L.optional()}),cE=h({actor:o(),city:o(),message:o().optional(),payload:bu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("pg.credential_resolved"),workflow:L.optional()}),dE=h({actor:o(),city:o(),message:o().optional(),payload:zu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("project.identity.stamped"),workflow:L.optional()}),pE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("provider.swapped"),workflow:L.optional()}),fE=h({actor:o(),city:o(),message:o().optional(),payload:Cu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.failed"),workflow:L.optional()}),mE=h({actor:o(),city:o(),message:o().optional(),payload:_u,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.city.create"),workflow:L.optional()}),vE=h({actor:o(),city:o(),message:o().optional(),payload:wu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.city.unregister"),workflow:L.optional()}),hE=h({actor:o(),city:o(),message:o().optional(),payload:ju,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.session.create"),workflow:L.optional()}),gE=h({actor:o(),city:o(),message:o().optional(),payload:Pu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.session.message"),workflow:L.optional()}),yE=h({actor:o(),city:o(),message:o().optional(),payload:Nu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("request.result.session.submit"),workflow:L.optional()}),_E=h({actor:o(),city:o(),message:o().optional(),payload:yr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.crashed"),workflow:L.optional()}),wE=h({actor:o(),city:o(),message:o().optional(),payload:Ru,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.drain_acked_with_assigned_work"),workflow:L.optional()}),xE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.draining"),workflow:L.optional()}),EE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.idle_killed"),workflow:L.optional()}),IE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.max_age_killed"),workflow:L.optional()}),SE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.quarantined"),workflow:L.optional()}),kE=h({actor:o(),city:o(),message:o().optional(),payload:yr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.stopped"),workflow:L.optional()}),bE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.stranded"),workflow:L.optional()}),zE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.suspended"),workflow:L.optional()}),CE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.undrained"),workflow:L.optional()}),TE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.updated"),workflow:L.optional()}),BE=h({actor:o(),city:o(),message:o().optional(),payload:ve,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.woke"),workflow:L.optional()}),RE=h({actor:o(),city:o(),message:o().optional(),payload:yr,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("session.work_query_failed"),workflow:L.optional()}),PE=h({actor:o(),city:o(),message:o().optional(),payload:$u,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("supervisor.fs_pressure.skipped_tick"),workflow:L.optional()}),NE=h({actor:o(),city:o(),message:o().optional(),payload:Lu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("supervisor.shutdown_requested"),workflow:L.optional()}),AE=h({actor:o(),city:o(),message:o().optional(),payload:Fu,run_id:o().optional(),seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:o().optional(),step_id:o().optional(),subject:o().optional(),ts:N({offset:!0}),type:x("worker.operation"),workflow:L.optional()}),dv=Hm("type",[Px.extend({type:x("bead.closed")}),Nx.extend({type:x("bead.created")}),Ax.extend({type:x("bead.updated")}),Ox.extend({type:x("city.created")}),jx.extend({type:x("city.resumed")}),$x.extend({type:x("city.suspended")}),Lx.extend({type:x("city.unregister_requested")}),Dx.extend({type:x("controller.started")}),Mx.extend({type:x("controller.stopped")}),Fx.extend({type:x("convoy.closed")}),Ux.extend({type:x("convoy.created")}),qx.extend({type:x("events.rotated")}),Vx.extend({type:x("extmsg.adapter_added")}),Wx.extend({type:x("extmsg.adapter_removed")}),Hx.extend({type:x("extmsg.bound")}),Gx.extend({type:x("extmsg.group_created")}),Jx.extend({type:x("extmsg.inbound")}),Kx.extend({type:x("extmsg.outbound")}),Qx.extend({type:x("extmsg.unbound")}),Yx.extend({type:x("gc.store.maintenance.done")}),Xx.extend({type:x("gc.store.maintenance.failed")}),eE.extend({type:x("mail.archived")}),tE.extend({type:x("mail.deleted")}),nE.extend({type:x("mail.marked_read")}),rE.extend({type:x("mail.marked_unread")}),oE.extend({type:x("mail.read")}),iE.extend({type:x("mail.replied")}),aE.extend({type:x("mail.sent")}),sE.extend({type:x("order.completed")}),lE.extend({type:x("order.failed")}),uE.extend({type:x("order.fired")}),cE.extend({type:x("pg.credential_resolved")}),dE.extend({type:x("project.identity.stamped")}),pE.extend({type:x("provider.swapped")}),fE.extend({type:x("request.failed")}),mE.extend({type:x("request.result.city.create")}),vE.extend({type:x("request.result.city.unregister")}),hE.extend({type:x("request.result.session.create")}),gE.extend({type:x("request.result.session.message")}),yE.extend({type:x("request.result.session.submit")}),_E.extend({type:x("session.crashed")}),wE.extend({type:x("session.drain_acked_with_assigned_work")}),xE.extend({type:x("session.draining")}),EE.extend({type:x("session.idle_killed")}),IE.extend({type:x("session.max_age_killed")}),SE.extend({type:x("session.quarantined")}),kE.extend({type:x("session.stopped")}),bE.extend({type:x("session.stranded")}),zE.extend({type:x("session.suspended")}),CE.extend({type:x("session.undrained")}),TE.extend({type:x("session.updated")}),BE.extend({type:x("session.woke")}),RE.extend({type:x("session.work_query_failed")}),PE.extend({type:x("supervisor.fs_pressure.skipped_tick")}),NE.extend({type:x("supervisor.shutdown_requested")}),AE.extend({type:x("worker.operation")}),Zx.extend({type:x("TypedTaggedEventStreamEnvelopeCustom")})]);h({event_cursor:o(),items:P(dv).nullable(),total:w().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});h({beads:P(uv).nullable(),deps:P(eu).nullable(),logical_edges:P(eu).nullable(),logical_nodes:P(qw).nullable(),partial:Z(),resolved_root_store:o(),root_bead_id:o(),root_store_ref:o(),scope_groups:P(l5).nullable(),scope_kind:o(),scope_ref:o(),snapshot_event_seq:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),snapshot_version:w().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),stores_scanned:P(o()).nullable(),workflow_id:o()});const OE=h({declared_name:o().optional(),declared_prefix:o().optional(),name:o(),prefix:o().optional(),provider:o().optional(),session_template:o().optional(),suspended:Z()});h({agents:P(kw).nullable(),patches:zw.optional(),providers:fe(o(),r5).optional(),rigs:P(Cw).nullable(),workspace:OE});P(Qn([h({data:oo,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()}),h({data:ov,event:x("turn"),id:Be().optional(),retry:Be().optional()})]));P(Qn([h({data:oo,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()}),h({data:ov,event:x("turn"),id:Be().optional(),retry:Be().optional()})]));fe(o(),o());P(Qn([h({data:cv,event:x("event"),id:Be().optional(),retry:Be().optional()}),h({data:oo,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()})]));P(Qn([h({data:iv,event:x("activity"),id:Be().optional(),retry:Be().optional()}),h({data:oo,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()}),h({data:p5,event:x("message").optional(),id:Be().optional(),retry:Be().optional()}),h({data:ku,event:x("pending"),id:Be().optional(),retry:Be().optional()}),h({data:d5,event:x("turn"),id:Be().optional(),retry:Be().optional()})]));P(Qn([h({data:oo,event:x("heartbeat"),id:o().optional(),retry:Be().optional()}),h({data:dv,event:x("tagged_event"),id:o().optional(),retry:Be().optional()})]));class Vn extends Error{constructor(r,i,s){super(i),this.status=r,this.requestId=s}status;requestId;name="SupervisorApiError"}async function Ie(t,r){let i;try{i=await t}catch(p){throw jE(p)}const{response:s}=i;if(s===void 0)throw new Vn(void 0,tu(i.error),void 0);if(!s.ok||i.error!==void 0)throw new Vn(s.status,tu(i.error,s.statusText),s.headers.get("x-gc-request-id")??void 0);const u=i.data;if(u===void 0)throw new Vn(s.status,r,s.headers.get("x-gc-request-id")??void 0);return u}function jE(t){return t instanceof Vn?t:new Vn(void 0,tu(t),void 0)}function tu(t,r="gc supervisor request failed"){if(typeof t=="string"&&t.trim().length>0)return t.trim();if(t instanceof Error&&t.message.trim().length>0)return t.message.trim();if($E(t))for(const i of["error","message","detail"]){const s=t[i];if(typeof s=="string"&&s.trim().length>0)return s.trim()}return r}function $E(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}const LE="";function DE(){const t=globalThis.location?.origin;return typeof t=="string"&&t.length>0&&t!=="null"?t:LE}function ME(t){if(!t.startsWith("/"))return t;const r=globalThis.location?.origin;return typeof r!="string"||r.length===0||r==="null"?t:new URL(t,r).toString().replace(/\/$/,"")}function Nf(t,r,i){const s=t.replace(/\/$/,""),u=new URLSearchParams(i).toString(),p=u.length>0?`${r}?${u}`:r;return s===""?p:s.startsWith("/")?`${s}${p}`:new URL(p,`${s}/`).toString()}const FE=6e4,Rt={"X-GC-Request":"dashboard"};let Af=null;const Of=new Map;function pv(t={}){const r=t.baseUrl??DE(),s={baseUrl:ME(r),headers:{Accept:"application/json"},responseStyle:"fields",throwOnError:!1},u=t.client??vm({...s,fetch:ZE(t.fetch??globalThis.fetch,fv(t.timeoutMs))});return{baseUrl:r,health(){return Ie(i7({client:u}),"gc supervisor health response was empty")},cityHealth(p){return Ie(w7({client:u,path:{cityName:p}}),"gc supervisor city health response was empty")},cityStatus(p){return Ie(A7({client:u,path:{cityName:p}}),"gc supervisor status response was empty")},listCities(){return Ie(a7({client:u}),"gc supervisor cities response was empty")},listAgents(p){return Ie(d7({client:u,path:{cityName:p}}),"gc supervisor agents response was empty")},listRigs(p){return Ie(C7({client:u,path:{cityName:p}}),"gc supervisor rigs response was empty")},listBeads(p,d){return Ie(v7({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor beads response was empty")},listEvents(p,d){return Ie(g7({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor events response was empty")},getBead(p,d){return Ie(p7({client:u,path:{cityName:p,id:d}}),"gc supervisor bead response was empty")},createBead(p,d){return Ie(h7({client:u,path:{cityName:p},headers:Rt,body:d}),"gc supervisor bead create response was empty")},updateBead(p,d,m){return Ie(f7({client:u,path:{cityName:p,id:d},headers:Rt,body:m}),"gc supervisor bead update response was empty")},closeBead(p,d,m){return Ie(m7({client:u,path:{cityName:p,id:d},headers:Rt,...m===void 0?{}:{body:m}}),"gc supervisor bead close response was empty")},nudgeAgent(p,d){const m=jf(d);return"dir"in m?Ie(c7({client:u,path:{cityName:p,dir:m.dir,base:m.base,action:"nudge"},headers:Rt}),"gc supervisor agent nudge response was empty"):Ie(l7({client:u,path:{cityName:p,base:m.base,action:"nudge"},headers:Rt}),"gc supervisor agent nudge response was empty")},agentPrime(p,d){const m=jf(d);return"dir"in m?Ie(u7({client:u,path:{cityName:p,dir:m.dir,base:m.base}}),"gc supervisor agent prime response was empty"):Ie(s7({client:u,path:{cityName:p,base:m.base}}),"gc supervisor agent prime response was empty")},sling(p,d){return Ie(N7({client:u,path:{cityName:p},headers:Rt,body:d}),"gc supervisor sling response was empty")},listMail(p,d){return Ie(x7({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor mail response was empty")},formulaFeed(p,d){return Ie(y7({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor formula feed response was empty")},sendMail(p,d){return Ie(E7({client:u,path:{cityName:p},headers:Rt,body:d}),"gc supervisor mail send response was empty")},mailThread(p,d){return Ie(I7({client:u,path:{cityName:p,id:d}}),"gc supervisor mail thread response was empty")},markMailRead(p,d,m){return Ie(b7({client:u,path:{cityName:p,id:d},headers:Rt,...m===void 0?{}:{query:m}}),"gc supervisor mail mark-read response was empty")},markMailUnread(p,d,m){return Ie(k7({client:u,path:{cityName:p,id:d},headers:Rt,...m===void 0?{}:{query:m}}),"gc supervisor mail mark-unread response was empty")},archiveMail(p,d,m){return Ie(S7({client:u,path:{cityName:p,id:d},headers:Rt,...m===void 0?{}:{query:m}}),"gc supervisor mail archive response was empty")},replyMail(p,d,m,g){return Ie(z7({client:u,path:{cityName:p,id:d},headers:Rt,body:m,...g===void 0?{}:{query:g}}),"gc supervisor mail reply response was empty")},cityEventStreamUrl(p,d){return Nf(r,`/v0/city/${encodeURIComponent(p)}/events/stream`,d===void 0?void 0:{after_seq:d})},sessionStreamUrl(p,d,m){return Nf(r,`/v0/city/${encodeURIComponent(p)}/session/${encodeURIComponent(d)}/stream`,m===void 0?void 0:{after:m})},listSessions(p){return Ie(P7({client:u,path:{cityName:p}}),"gc supervisor sessions response was empty")},sessionPending(p,d){return Ie(T7({client:u,path:{cityName:p,id:d}}),"gc supervisor session pending response was empty")},respondSession(p,d,m){return Ie(B7({client:u,path:{cityName:p,id:d},headers:Rt,body:m}),"gc supervisor session respond response was empty")},sessionTranscript(p,d){return Ie(R7({client:u,path:{cityName:p,id:d},query:{format:"conversation"}}),"gc supervisor transcript response was empty")},workflowRun(p,d,m){return Ie(O7({client:u,path:{cityName:p,workflow_id:d},...m===void 0?{}:{query:m}}),"gc supervisor workflow response was empty")},formulaDetail(p,d,m){return Ie(_7({client:u,path:{cityName:p,name:d},query:m}),"gc supervisor formula detail response was empty")},mutationHeaders(){return{...Rt}}}}function ot(){return Af??=pv(),Af}function UE(t){const r=fv(t),i=Of.get(r);if(i!==void 0)return i;const s=pv({timeoutMs:r});return Of.set(r,s),s}function jf(t){const r=t.trim().split("/");if(r.length===1){const i=r[0];if(i!==void 0&&i!=="")return{base:i}}if(r.length===2){const i=r[0],s=r[1];if(i!==void 0&&i!==""&&s!==void 0&&s!=="")return{dir:i,base:s}}throw new Error(`invalid agent alias: ${t}`)}function fv(t){return typeof t=="number"&&Number.isFinite(t)&&t>0?t:FE}function ZE(t,r){return async(i,s)=>{const u=new AbortController,p=new Vn(void 0,`gc supervisor request timed out after ${r}ms`,void 0),d=qE(i,s);d?.aborted&&u.abort(d.reason);const m=()=>u.abort(d?.reason);d?.addEventListener("abort",m,{once:!0});let g;const y=new Promise((T,A)=>{g=setTimeout(()=>{u.abort(p),A(p)},r)}),E=new Request(i,{...s,signal:u.signal}),S=t(E);try{return await Promise.race([S,y])}finally{g!==void 0&&clearTimeout(g),d?.removeEventListener("abort",m)}}}function qE(t,r){return r?.signal!==void 0?r.signal:t instanceof Request?t.signal:null}async function VE(t,r){const i=xn("list agent pending interactions"),s=WE(r),u=t.flatMap(d=>{const m=d.session?.name;if(m===void 0)return[];const g=s.get(m);return g===void 0?[]:[{agentName:d.name,sessionId:g,sessionName:m}]});return(await Promise.all(u.map(async d=>{const m=await ot().sessionPending(i,d.sessionId);return m.pending===void 0?null:{...d,pending:m.pending}}))).filter(d=>d!==null)}async function j6(t,r){const i=xn("respond to agent pending interaction");return ot().respondSession(i,t,r)}function $6(t){return`gc agent attach ${HE(t)}`}function WE(t){const r=new Map;for(const i of t)i.session_name!==void 0&&r.set(i.session_name,i.id);return r}function HE(t){return/^[A-Za-z0-9_./:-]+$/.test(t)?t:`'${t.replaceAll("'","'\\''")}'`}const GE=1e3,JE=200,KE=1e3,QE=new Set(["feature","bug","task","epic","chore","decision"]);async function YE(t={}){const r=xn("list supervisor beads"),i=t.limit??GE,s=t.rigFilter?.trim()??"",u=t.includeClosed??!1,p=t.includeBookkeeping??!1,d={limit:i,...u?{all:!0}:{},...s.length===0?{}:{rig:s}},m=await ot().listBeads(r,d),g=vv(m.items??[]),y=u?g:g.filter(T=>T.status!=="closed"),E=p?y:y.filter(XE),S=mv(m.total);return{items:E,total:E.length,...S===void 0?{}:{upstream_total:S},upstream_fetched:g.length,fetch_limit:i}}async function L6(t,r={}){const i=xn("list supervisor assigned beads"),s=t4(t),u=r.limit??JE,p=r.includeClosed??!1;if(s.length===0)return{items:[],total:0,upstream_fetched:0,fetch_limit:u};const d=await Promise.all(s.map(y=>ot().listBeads(i,{assignee:y,limit:u,...p?{all:!0}:{}}))),m=vv(d.flatMap(y=>y.items??[])),g=e4(d);return{items:m,total:m.length,...g===void 0?{}:{upstream_total:g},upstream_fetched:m.length,fetch_limit:u}}async function D6(t){const r=xn("fetch supervisor bead");try{return await ot().getBead(r,t)}catch(i){if(!(i instanceof Vn)||i.status!==404)throw i;const u=((await ot().listBeads(r,{limit:KE})).items??[]).find(p=>p.id===t);if(u!==void 0)return u;throw i}}function XE(t){return!(!QE.has(t.issue_type)||Array.isArray(t.labels)&&t.labels.some(r=>r.startsWith("gc:")))}function mv(t){if(typeof t=="number")return t;if(typeof t=="bigint")return Number(t)}function e4(t){let r=0;for(const i of t){const s=mv(i.total);if(s===void 0)return;r+=s}return r}function vv(t){const r=new Set,i=[];for(const s of t)r.has(s.id)||(r.add(s.id),i.push(s));return i}function t4(t){const r=new Set,i=[];for(const s of t){const u=s.trim();u.length===0||r.has(u)||(r.add(u),i.push(u))}return i}const M6=[100,500,1e3],Uu=100,F6=["24h","7d","all"],n4="all",r4={"24h":1440*60*1e3,"7d":10080*60*1e3};async function Zu(t,r,i,s=Uu,u=n4,p=Date.now()){const d=xn("list supervisor mail"),m=await ot().listMail(d,{limit:s}),g=m.items??[],y=i4(o4(g,t,r,i),u,p);return y.sort(l4),{...m,items:y,total:y.length,upstream_total:g.length,upstream_fetched:g.length,fetch_limit:s}}async function U6(t,r,i,s=Uu){const u=xn("fetch supervisor mail thread");try{const p=await ot().mailThread(u,t);return $f(p)}catch(p){if(!(p instanceof Vn)||p.status!==404)throw p;const d=await Zu("all",r,i,s),m=d.items.filter(g=>g.thread_id===t);return $f({...d,items:m,total:m.length})}}function $f(t){const r=s4(t.items??[]).sort(u4);return{...t,items:r,total:r.length}}function o4(t,r,i,s){const u=a4(i,s);return r==="all"?[...t]:r==="inbox"?t.filter(p=>p.to.toLowerCase()===u):t.filter(p=>p.from.toLowerCase()===u)}function i4(t,r,i){if(r==="all")return[...t];const s=i-r4[r];return t.filter(u=>{const p=Date.parse(u.created_at);return Number.isFinite(p)&&p>=s})}function a4(t,r){const i=t.toLowerCase();return i===r.operatorAlias.toLowerCase()?r.operatorWireAlias:i}function s4(t){const r=new Set,i=[];for(const s of t)r.has(s.id)||(r.add(s.id),i.push(s));return i}function l4(t,r){return r.created_at.localeCompare(t.created_at)}function u4(t,r){return t.created_at.localeCompare(r.created_at)}function hv(t,r){if(t===void 0||t.length===0)return null;const i=Date.parse(t);if(!Number.isFinite(i))return null;const s=r-i;return s>=0?s:null}function gv(t){const r=Math.max(1,Math.round(t/36e5));return r<48?`${r}h`:`${Math.round(r/24)}d`}const c4=1440*60*1e3,d4=4320*60*1e3;function p4(t,r){const i=[];for(const s of t.escalations){const u=f4(s);u!==null&&i.push(u)}for(const s of t.beads){const u=m4(s,r);u!==null&&i.push(u)}return i}function f4(t){return t.status==="closed"?null:{beadId:t.id,reason:"escalated",severity:"attention",summary:`${t.title} — escalation raised`,updatedAt:t.updated_at??t.created_at}}function m4(t,r){if(t.status!=="open"||v4(t))return null;const i=hv(t.created_at,r);if(i===null||i=d4;return{beadId:t.id,reason:"ready-unclaimed",severity:s?"attention":"watch",summary:`${t.title} opened ${gv(i)} ago`,updatedAt:t.created_at}}function v4(t){return t.assignee!==void 0&&t.assignee.trim().length>0}function Lf(t,r){const i=`/runs/${encodeURIComponent(t)}`;if(r.status!=="available")return i;const s=new URLSearchParams;return s.set("scope_kind",r.kind),s.set("scope_ref",r.ref),`${i}?${s.toString()}`}const h4={"awaiting-input":"awaiting input",errored:"errored","rate-limited":"rate limited",stalled:"stalled"},g4={respond:"Respond to its prompt.",reset:"Reset the agent.",nudge:"Nudge it to resume."},y4={"awaiting-input":"stuck",errored:"stuck","rate-limited":"warn",stalled:"warn"};function _4(t){return h4[t]}function Z6(t){return g4[t]}function q6(t){return y4[t]}const w4=new Set(["gc.store.maintenance.failed","order.failed","request.failed","session.crashed","session.stranded","session.work_query_failed","supervisor.shutdown_requested"]),x4=new Set(["events.rotated","session.quarantined","session.suspended","supervisor.fs_pressure.skipped_tick"]);function E4(t){return w4.has(t.type)?"attention":x4.has(t.type)?"watch":"event"}function I4(t){return t.message??t.subject??t.type}const S4=1440*60*1e3,k4=30,b4=2e9,z4=1e9,C4=1e9,T4=512e6,B4="gc:escalation",R4="decision.decide";function P4(t={}){return Xo.map(r=>N4(r,t))}function N4(t,r){switch(t){case"activity":return D4(r.activity);case"agents":return j4(r.agents);case"beads":return $4(r.beads);case"health":return A4(r.health);case"mail":return L4(r.mail);case"runs":return O4(r.runs)}}function A4(t){return{id:"health:derived",domain:"health",getItems:()=>Q4(t)}}function O4(t){return{id:"runs:derived",domain:"runs",getItems:()=>M4(t)}}function j4(t){return{id:"agents:derived",domain:"agents",getItems:()=>F4(t)}}function $4(t){return{id:"beads:derived",domain:"beads",getItems:()=>U4(t)}}function L4(t){return{id:"mail:derived",domain:"mail",getItems:()=>W4(t)}}function D4(t){return{id:"activity:derived",domain:"activity",getItems:()=>G4(t)}}function M4(t){const r=[];if(t===void 0)return r;const i={provenance:t.provenance,fetchedAt:t.fetchedAt};if(t.error!==void 0&&t.error.length>0)return r.push(It("runs",{id:"runs:unavailable",title:"Run data unavailable",summary:t.error,href:"/runs"})),r;const s=t.summary;if(s===void 0)return r;s.lanesPartial===!0&&r.push(Ho("runs",{id:"runs:partial",title:"Run list incomplete",href:"/runs"},i));for(const u of[...s.lanes,...s.blockedLanes])u.health.status!=="available"&&r.push(Ho("runs",{id:`runs:${u.id}:health-unavailable`,title:`${u.title} health unavailable`,summary:u.health.error,href:Lf(u.id,u.scope)},i));for(const u of oy(s.blockedLanes))r.push(It("runs",{id:`runs:${u.id}:blocked`,title:`${u.title} blocked`,summary:u.reason,href:Lf(u.id,u.scope)}));return r}function F4(t){const r=[];if(t===void 0)return r;if(t.error!==void 0&&t.error.length>0)return r.push(Ho("agents",{id:"agents:unavailable",title:"Agent data unavailable",summary:t.error,href:"/agents"})),r;t.partial===!0&&r.push(Ho("agents",{id:"agents:partial",title:"Agent list incomplete",href:"/agents"})),t.pendingError!==void 0&&t.pendingError.length>0&&r.push(Ho("agents",{id:"agents:pending-unavailable",title:"Agent pending state unavailable",summary:t.pendingError,href:"/agents"}));const i=(t.pendingInteractions??[]).map(s=>({agentName:s.agentName,...s.pending.prompt===void 0?{}:{prompt:s.pending.prompt}}));for(const s of X0(t.items??[],i))r.push(It("agents",{id:`agents:${s.name}:needs-you`,title:`${s.name} ${_4(s.reason)}`,summary:s.detail,href:`/agents/${encodeURIComponent(s.name)}`}));return r}function U4(t){const r=[];if(t===void 0)return r;t.error!==void 0&&t.error.length>0&&r.push(It("beads",{id:"beads:unavailable",title:"Bead data unavailable",summary:t.error,href:"/beads"})),t.partial===!0&&r.push(Zn("beads",{id:"beads:partial",title:"Bead list incomplete",href:"/beads"})),t.decisionsError!==void 0&&t.decisionsError.length>0&&r.push(It("beads",{id:"beads:decisions-unavailable",title:"Decision queue unavailable",summary:t.decisionsError,href:"/beads"})),t.escalationsError!==void 0&&t.escalationsError.length>0&&r.push(It("beads",{id:"beads:escalations-unavailable",title:"Escalation queue unavailable",summary:t.escalationsError,href:"/beads"}));for(const u of t.decisions??[])r.push(V4(u));const i=t.nowMs??Date.now(),s=(t.items??[]).filter(u=>!q4(u,t.decisionLabel));for(const u of p4({beads:s,escalations:t.escalations??[]},i)){const p=u.severity==="attention"?It:Zn;r.push(p("beads",{id:`beads:${u.beadId}:${u.reason}`,title:`${u.beadId} ${Z4(u.reason)}`,summary:u.summary,href:yv(u.beadId),updatedAt:u.updatedAt}))}return r}function Z4(t){return t==="escalated"?"escalated":"unclaimed"}function yv(t){const r=new URLSearchParams;return r.set("bead",t),`/beads?${r.toString()}`}function q4(t,r){return(t.labels??[]).includes(r)}function V4(t){const r=t.metadata?.[R4];return It("beads",{id:`beads:${t.id}:mayor-decision`,title:t.title,href:yv(t.id),updatedAt:t.updated_at??t.created_at,...r!==void 0&&r.trim().length>0?{summary:r}:{}})}function W4(t){const r=[];if(t===void 0)return r;t.error!==void 0&&t.error.length>0&&r.push(It("mail",{id:"mail:unavailable",title:"Mail data unavailable",summary:t.error,href:"/mail"})),t.partial===!0&&r.push(Zn("mail",{id:"mail:partial",title:"Mail list incomplete",href:"/mail"}));const i=t.nowMs??Date.now();for(const s of py(t.items??[])){const u=hv(s.created_at,i),p=u!==null&&u>=S4;r.push(It("mail",{id:`mail:${s.id}:${p?"unread-stale":"unread"}`,title:s.subject,summary:p?`from ${s.from}, unread for ${gv(u)}`:`from ${s.from}`,href:H4(s.id),updatedAt:s.created_at}))}return r}function H4(t){const r=new URLSearchParams;return r.set("message",t),`/mail?${r.toString()}`}function G4(t){const r=[];if(t===void 0)return r;t.deploysError!==void 0&&t.deploysError.length>0&&r.push(It("activity",{id:"activity:deploys-unavailable",title:"Deploy data unavailable",summary:t.deploysError,href:"/activity"})),t.eventsDegraded!==void 0&&t.eventsDegraded.length>0&&r.push(Zn("activity",{id:"activity:events-degraded",title:"Event stream degraded",summary:t.eventsDegraded,href:"/activity"})),t.eventsError!==void 0&&t.eventsError.length>0&&r.push(Zn("activity",{id:"activity:events-unavailable",title:"Event history unavailable",summary:t.eventsError,href:"/activity"})),t.eventsPartial===!0&&r.push(Zn("activity",{id:"activity:events-partial",title:"Event history incomplete",href:"/activity"})),J4(r,t.events??[]);const i=t.deploys;if(i===void 0)return r;i.failed_marker&&r.push(It("activity",{id:"activity:failed-marker",title:"Deploy failed marker present",href:"/activity"}));for(const s of i.items)s.status==="failed"?r.push(It("activity",{id:`activity:deploy:${s.at}:failed`,title:"Deploy failed",summary:s.detail,href:"/activity",updatedAt:s.at})):s.status==="in-progress"&&r.push(Zn("activity",{id:`activity:deploy:${s.at}:in-progress`,title:"Deploy in progress",summary:s.detail,href:"/activity",updatedAt:s.at}));return r}function J4(t,r){for(const i of r){const s=E4(i);if(s==="event")continue;const u=s==="attention"?It:Zn;t.push(u("activity",{id:`activity:event:${String(i.seq)}:${i.type}`,title:i.type,summary:I4(i),href:K4(i),updatedAt:i.ts}))}}function K4(t){return`/activity?${new URLSearchParams({mode:"events",type:t.type}).toString()}`}function Q4(t){const r=[];return t===void 0||(t.dashboardError!==void 0&&t.dashboardError.length>0&&r.push(Wn({id:"health:dashboard-health-unavailable",title:"Dashboard health unavailable",summary:t.dashboardError})),t.supervisor!==void 0&&Y4(r,t.supervisor),t.system!==void 0&&(X4(r,t.system),eI(r,t.system)),t.trend!==void 0&&!t.trend.available&&r.push(mr({id:"health:dolt-noms-unavailable",title:"Dolt-noms trend unavailable",summary:t.trend.reason}))),r}function Y4(t,r){if(r.status==="unavailable"){t.push(Wn({id:"health:supervisor-unreachable",title:"Supervisor unreachable",summary:r.error}));return}const i=r.data;i.status!=="ok"&&t.push(Wn({id:"health:supervisor-not-ok",title:`Supervisor ${i.status}`})),i.city===void 0&&t.push(mr({id:"health:supervisor-city-missing",title:"Supervisor city missing",summary:"city was absent from generated supervisor health"})),i.version===void 0&&t.push(mr({id:"health:supervisor-version-missing",title:"Supervisor version missing",summary:"version was absent from generated supervisor health"}))}function X4(t,r){const i=r.admin;i.uptime_sec=b4?t.push(Wn({id:"health:dashboard-process-rss-high",title:"Dashboard RSS high",summary:_a(i.rss_bytes)})):i.rss_bytes>=z4&&t.push(mr({id:"health:dashboard-process-rss-elevated",title:"Dashboard RSS elevated",summary:_a(i.rss_bytes)})),i.heap_used_bytes>=C4?t.push(Wn({id:"health:dashboard-process-heap-high",title:"Dashboard heap high",summary:_a(i.heap_used_bytes)})):i.heap_used_bytes>=T4&&t.push(mr({id:"health:dashboard-process-heap-elevated",title:"Dashboard heap elevated",summary:_a(i.heap_used_bytes)}))}function eI(t,r){const i=Df(r.host.free_mem_bytes,r.host.total_mem_bytes);i!==null&&i<.05?t.push(Wn({id:"health:memory-critical",title:"Host memory critical",summary:`${Math.round(i*100)}% free`})):i!==null&&i<.1&&t.push(mr({id:"health:memory-low",title:"Host memory low",summary:`${Math.round(i*100)}% free`}));const s=Df(r.host.load_avg_1,r.host.cpu_count);s!==null&&s>1.5?t.push(Wn({id:"health:load-high",title:"Host load high",summary:`${r.host.load_avg_1.toFixed(2)} load across ${r.host.cpu_count} CPUs`})):s!==null&&s>1&&t.push(mr({id:"health:load-elevated",title:"Host load elevated",summary:`${r.host.load_avg_1.toFixed(2)} load across ${r.host.cpu_count} CPUs`}))}function _a(t){return t>=1e9?`${(t/1e9).toFixed(1)} GB`:t>=1e6?`${Math.round(t/1e6)} MB`:t>=1e3?`${Math.round(t/1e3)} KB`:`${t} B`}function Df(t,r){return r<=0?null:t/r}function Wn(t){return{domain:"health",severity:"attention",href:"/health",current:!0,actionable:!0,...t}}function It(t,r){return{domain:t,severity:"attention",current:!0,actionable:!0,...r}}function Zn(t,r){return{domain:t,severity:"watch",current:!0,actionable:!1,...r}}function Ho(t,r,i){return{domain:t,severity:"unavailable",current:!0,actionable:!1,...r,...i?.provenance===void 0?{}:{provenance:i.provenance},...i?.fetchedAt===void 0?{}:{fetchedAt:i.fetchedAt}}}function mr(t){return{domain:"health",severity:"watch",href:"/health",current:!0,actionable:!1,...t}}const tI=1e3,nI=100,rI="24h",oI=2500;function iI(t,r){const i=Ba(),s=i??"no-city",{decisionLabel:u,operatorWireAlias:p}=t,d=b.useMemo(()=>aI(r),[r]),m=mn(`attention:agents:${s}`,()=>sI(i)),g=mn(`attention:beads:${s}:${u}`,()=>lI(i,u)),y=mn(`attention:mail:${s}:${p}`,()=>dI(i,t)),E=mn(`attention:activity:${s}`,()=>pI(i)),S=mn(`attention:health:${s}`,()=>fI(i));return b.useMemo(()=>P4(mI({activity:E.data,agents:m.data,beads:g.data,health:S.data,mail:y.data,runs:d})),[E.data,m.data,g.data,S.data,y.data,d])}function aI(t){if(t!==void 0)return t.status==="error"?{error:t.error,provenance:"error"}:{summary:t.data,provenance:t.status,fetchedAt:t.fetchedAt}}async function sI(t){if(t===null)return{};try{const r=await ot().listAgents(t),i={items:r.items??[],partial:r.partial===!0};try{const s=await ot().listSessions(t);i.pendingInteractions=await VE(r.items??[],s.items??[])}catch(s){i.pendingError=Jt(s,"agent pending state unavailable")}return i}catch(r){return{error:Jt(r,"agent list unavailable")}}}async function lI(t,r){if(t===null)return{decisionLabel:r};const[i,s,u]=await Promise.allSettled([YE({limit:tI}),uI(t,r),cI(t)]),p={nowMs:Date.now(),decisionLabel:r};return i.status==="fulfilled"?(p.items=i.value.items,p.partial=i.value.partial===!0):p.error=Jt(i.reason,"bead list unavailable"),s.status==="fulfilled"?p.decisions=s.value.items??[]:p.decisionsError=Jt(s.reason,"decision queue unavailable"),u.status==="fulfilled"?p.escalations=u.value.items??[]:p.escalationsError=Jt(u.reason,"escalation queue unavailable"),p}async function uI(t,r){return ot().listBeads(t,{label:r,status:"open"})}async function cI(t){return ot().listBeads(t,{label:B4,status:"open"})}async function dI(t,r){if(t===null)return{};try{const i=await Zu("inbox",r.operatorAlias,r,Uu);return{items:i.items??[],nowMs:Date.now(),partial:i.partial===!0}}catch(i){return{error:Jt(i,"mail list unavailable")}}}async function pI(t){const[r,i]=await Promise.allSettled([Yr.listBuilds(),t===null?Promise.resolve(null):ot().listEvents(t,{limit:nI,since:rI})]),s={};return r.status==="fulfilled"?s.deploys=r.value:s.deploysError=Jt(r.reason,"deploy activity unavailable"),i.status==="fulfilled"?i.value!==null&&(s.events=i.value.items??[],s.eventsPartial=i.value.partial===!0,i.value.partial_errors!==null&&i.value.partial_errors!==void 0&&(s.eventsDegraded=i.value.partial_errors.join("; "))):s.eventsError=Jt(i.reason,"event history unavailable"),s}async function fI(t){if(t===null)return{};const[r,i,s]=await Promise.allSettled([Yr.systemHealth(),UE(oI).cityHealth(t),Yr.doltTrend()]),u={},p=[];return r.status==="fulfilled"?u.system=r.value:p.push(Jt(r.reason,"dashboard health unavailable")),i.status==="fulfilled"?u.supervisor={status:"available",data:i.value}:u.supervisor={status:"unavailable",error:Jt(i.reason,"supervisor health unavailable")},s.status==="fulfilled"?u.trend=s.value:p.push(Jt(s.reason,"dolt-noms trend unavailable")),p.length>0&&(u.dashboardError=p.join("; ")),u}function mI(t){const r={};for(const[i,s]of Object.entries(t))s!==void 0&&(r[i]=s);return r}async function Jr(t){const r={Accept:"application/json","Content-Type":"application/json","X-GC-Request":"dashboard"};try{const i=await fetch("/api/client-errors",{method:"POST",headers:r,credentials:"same-origin",keepalive:!0,body:JSON.stringify(t)});return i.ok?{status:"reported"}:{status:"failed",error:`client error report failed with ${i.status}`}}catch(i){return{status:"failed",error:Wr(i)}}}class _v extends b.Component{state={crashed:!1};static getDerivedStateFromError(){return{crashed:!0}}componentDidCatch(r,i){Jr({component:"ErrorBoundary",operation:"componentDidCatch",message:Wr(r)})}render(){return this.state.crashed?$.jsx("main",{className:"max-w-dashboard mx-auto px-4 sm:px-6 lg:px-8 py-12",children:$.jsxs("section",{className:"space-y-4",role:"alert",children:[$.jsx("h1",{className:"text-display font-semibold text-fg",children:"Dashboard view failed."}),$.jsx("p",{className:"text-body text-fg-muted",children:"The error was reported to the local dashboard log. Refresh to retry this view."})]})}):this.props.children}}function vI({label:t,summary:r}){const i=r.attention+r.watch;if(i===0||r.severity===null)return null;const s=i===1?"item":"items";return $.jsx("span",{"aria-label":`${t}: ${i} ${r.severity} ${s}`,className:`ml-1 align-super text-[0.65rem] leading-none tnum ${hI(r.severity)}`,children:i})}function hI(t){return t==="attention"?"text-accent":"text-warn"}function wv(t,r,i){try{const s=qu(t).getItem(r);return s===null?{status:"missing"}:{status:"found",value:s}}catch(s){return Vu(t,"getItem",r,i,s)}}function xv(t,r,i,s){try{return qu(t).setItem(r,i),{status:"stored"}}catch(u){return Vu(t,"setItem",r,s,u)}}function Ev(t,r,i){try{return qu(t).removeItem(r),{status:"stored"}}catch(s){return Vu(t,"removeItem",r,i,s)}}function qu(t){return t==="localStorage"?window.localStorage:window.sessionStorage}function Vu(t,r,i,s,u){const p=Wr(u);return Jr({component:s,operation:`${t}.${r}`,message:`${i}: ${p}`}),{status:"unavailable",error:p}}const nu="gascity:theme",ru="ThemeContext",Iv=b.createContext(null);function gI(){const t=wv("localStorage",nu,ru);return t.status==="found"&&(t.value==="light"||t.value==="dark")?t.value:"system"}function yI(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function _I(t){const r=document.documentElement;t==="system"?r.removeAttribute("data-theme"):r.setAttribute("data-theme",t)}function wI({children:t}){const[r,i]=b.useState(gI),[s,u]=b.useState(yI);b.useEffect(()=>{const y=window.matchMedia("(prefers-color-scheme: dark)"),E=()=>u(y.matches?"dark":"light");return y.addEventListener("change",E),()=>y.removeEventListener("change",E)},[]);const p=r==="system"?s:r,d=b.useCallback(y=>{i(y),y==="system"?Ev("localStorage",nu,ru):xv("localStorage",nu,y,ru),_I(y)},[]),m=b.useCallback(()=>{d(p==="dark"?"light":"dark")},[p,d]),g=b.useMemo(()=>({pref:r,resolved:p,set:d,toggle:m}),[r,p,d,m]);return $.jsx(Iv.Provider,{value:g,children:t})}function xI(){const t=b.useContext(Iv);if(t===null)throw new Error("useTheme must be used inside ");return t}const Sv={operatorAlias:"operator",operatorWireAlias:"human",decisionLabel:"needs/operator"},kv=b.createContext(Sv);function EI({operator:t,children:r}){return $.jsx(kv.Provider,{value:t,children:r})}function bv(){return b.useContext(kv)}function II(t){return t===void 0?Sv:{operatorAlias:t.operatorAlias,operatorWireAlias:t.operatorWireAlias,decisionLabel:t.decisionLabel}}const SI={ok:"text-ok",warn:"text-warn",stuck:"text-accent",neutral:"text-fg-muted"},kI={ok:"●",warn:"▲",stuck:"■",neutral:"·"};function bI({tone:t,label:r,glyph:i,trailing:s,className:u="",title:p}){return $.jsxs("span",{className:`inline-flex items-baseline gap-1.5 text-body ${SI[t]} ${u}`,title:p,children:[$.jsx("span",{"aria-hidden":!0,className:"text-[0.85em] leading-none translate-y-[1px]",children:i??kI[t]}),$.jsx("span",{children:r}),s&&$.jsx("span",{className:"text-fg-faint text-label uppercase tracking-wider",children:s})]})}function V6(t){switch(t){case"closed":return"neutral";case"in_progress":return"ok";case"blocked":return"stuck";default:return"warn"}}function W6(t){switch(t){case"active":case"running":return"ok";case"rate-limited":case"rate_limited":case"waiting":return"warn";case"failed":case"closed":case"errored":case"stuck":return"stuck";default:return"neutral"}}const zv=b.createContext(!1);function zI({readOnly:t,children:r}){return $.jsx(zv.Provider,{value:t,children:r})}function CI(){return b.useContext(zv)}function TI(t,r){return t?t.readOnly:r!==null}const Cv="Read-only mode: mutations are disabled";function H6(){return $.jsx(bI,{tone:"warn",label:"Read-only",title:Cv})}const BI="mayor";function RI(t){const{operator:r,sessionAliases:i,mailFromOrTo:s}=t,u=new Map;for(const A of i){const D=A.toLowerCase();u.has(D)||u.set(D,A)}for(const A of s){const D=A.toLowerCase();u.has(D)||u.set(D,A)}const p=r.toLowerCase(),d=new Set(s.map(A=>A.toLowerCase())),m=[r],g=[],y=[],E=[];for(const[A,D]of u)if(A!==p){if(A===BI){g.push(D);continue}d.has(A)?y.push(D):E.push(D)}const S=(A,D)=>A.toLowerCase().localeCompare(D.toLowerCase());y.sort(S),E.sort(S);const T=[{tier:"you",aliases:m}];return g.length>0&&T.push({tier:"mayor",aliases:g}),y.length>0&&T.push({tier:"active",aliases:y}),E.length>0&&T.push({tier:"other",aliases:E}),T}function PI(t,r){return t===r?"user":t}function G6(t){switch(t){case"you":return"You";case"mayor":return"Mayor";case"active":return"Active";case"other":return"Other"}}async function NI(){return ot().listSessions(xn("list supervisor sessions"))}async function J6(t){const r=await ot().sessionTranscript(xn("fetch supervisor session transcript"),t);return OI(r)}function K6(t){return(t.items??[]).map(AI)}function AI(t){const r={id:t.id,template:t.template,session_name:t.session_name,title:t.title,state:t.state,created_at:t.created_at,attached:t.attached,running:t.running,provider:t.provider};return t.alias!==void 0&&(r.alias=t.alias),t.reason!==void 0&&(r.reason=t.reason),t.display_name!==void 0&&(r.display_name=t.display_name),t.last_active!==void 0&&(r.last_active=t.last_active),t.rig!==void 0&&(r.rig=t.rig),t.pool!==void 0&&(r.pool=t.pool),t.agent_kind!==void 0&&(r.agent_kind=t.agent_kind),t.model!==void 0&&(r.model=t.model),t.context_pct!==void 0&&(r.context_pct=t.context_pct),t.context_window!==void 0&&(r.context_window=t.context_window),t.activity!==void 0&&(r.activity=t.activity),r}function OI(t,r=new Date().toISOString()){const i=t.turns??[];return{...t,turns:i,total_chars:i.reduce((s,u)=>s+u.text.length,0),captured_at:r,truncated:!1}}const ou="gascity.dashboard.viewingAs",Kr="ViewingAsContext",Mf=/^[a-z][a-z0-9_./-]{1,63}$/i,Ff=[3e4,9e4,27e4];function jI(t){if(!Number.isInteger(t)||t<0||t>=Ff.length)return null;const r=Ff[t];return r===void 0?null:r}const Tv=b.createContext(null);function Uf(t){const r=wv("sessionStorage",ou,Kr);if(r.status==="found"){const i=r.value;if(i.length>0&&i.length<=64)return i}return t}function Zl(t,r){t===r?Ev("sessionStorage",ou,Kr):xv("sessionStorage",ou,t,Kr)}function $I({children:t}){const r=bv(),{operatorAlias:i}=r,[s,u]=b.useState(()=>Uf(i)),p=b.useRef(i),[d,m]=b.useState([]),[g,y]=b.useState([]),[E,S]=b.useState(!1),[T,A]=b.useState(!1),D=b.useRef(!1),W=b.useRef(!0),O=b.useRef(null),H=b.useCallback(pe=>{u(pe),Zl(pe,i)},[i]),oe=b.useCallback(()=>{u(i),Zl(i,i)},[i]),Q=b.useCallback(async()=>{try{const pe=await NI();if(!W.current)return!0;const Re=new Set,ye=[];for(const Ze of pe.items??[]){if(typeof Ze.alias!="string"||!Mf.test(Ze.alias))continue;const Ke=Ze.alias.toLowerCase();Re.has(Ke)||(Re.add(Ke),ye.push(Ze.alias))}return m(ye),A(!1),!0}catch(pe){return Jr({component:Kr,operation:"loadAliases.sessions",message:Wr(pe)}),!1}},[]),G=b.useCallback(pe=>{if(!W.current)return;const Re=jI(pe);Re!==null&&(O.current=setTimeout(()=>{O.current=null,W.current&&Q().then(ye=>{W.current&&(ye||G(pe+1))}).catch(ye=>{Jr({component:Kr,operation:"loadAliases.sessionsRetry",message:Wr(ye)})})},Re))},[Q]),ee=b.useCallback(()=>{if(D.current)return;D.current=!0,S(!0);let pe=2;const Re=()=>{pe-=1,pe===0&&W.current&&S(!1)};Q().then(ye=>{W.current&&(ye||(A(!0),G(0)))}).finally(Re),Zu("all",i,r).then(ye=>{if(!W.current)return;const Ze=new Set,Ke=[];for(const et of ye.items)for(const Qe of[et.from,et.to]){if(typeof Qe!="string"||Qe.length===0||!Mf.test(Qe))continue;const kt=Qe.toLowerCase();Ze.has(kt)||(Ze.add(kt),Ke.push(Qe))}y(Ke)}).catch(ye=>{Jr({component:Kr,operation:"loadAliases.mail",message:Wr(ye)})}).finally(Re)},[Q,G,i,r]);b.useEffect(()=>(W.current=!0,()=>{W.current=!1,O.current!==null&&(clearTimeout(O.current),O.current=null)}),[]),b.useEffect(()=>{const pe=p.current;p.current=i,pe!==i&&s===pe&&u(Uf(i))},[i,s]);const ue=b.useMemo(()=>RI({operator:i,sessionAliases:d.includes(s)?d:[...d,s],mailFromOrTo:g}),[d,g,s,i]),de=b.useMemo(()=>({viewingAs:{alias:s,isOperator:s===i},setAlias:H,resetToOperator:oe,aliasBuckets:ue,aliasesLoading:E,sessionsUnavailable:T,loadAliases:ee}),[s,i,H,oe,ue,E,T,ee]);return b.useEffect(()=>{const pe=()=>{document.hidden&&s!==i&&(u(i),Zl(i,i))};return document.addEventListener("visibilitychange",pe),()=>document.removeEventListener("visibilitychange",pe)},[s,i]),$.jsx(Tv.Provider,{value:de,children:t})}function LI(){const t=b.useContext(Tv);if(t===null)throw new Error("useViewingAs must be inside ");return t}const DI={id:"activity",kind:"core",path:"/activity",nav:{label:"Activity",order:55},element:b.lazy(()=>wn(()=>import("./Activity-CiU9eU59.js"),__vite__mapDeps([0,1,2,3,4])).then(t=>({default:t.ActivityPage})))},MI={id:"health",kind:"core",path:"/health",nav:{label:"Health",order:60},element:b.lazy(()=>wn(()=>import("./Health-RykINz8c.js"),__vite__mapDeps([5,1,2,4,6,3])).then(t=>({default:t.HealthPage})))},Bv=[DI,MI],FI={views:"views"};function UI(t,r){console.warn(`[${t}] ${r}`)}function Rv(t,r){const i=new Set(r??[]);return t.filter(s=>s.kind==="core"||i.has(s.id))}const ZI={};function qI(t,r){const i=[];if(r!==null){const d=ZI[r];if(d!==void 0){if(t.some(g=>g.id===d.target))return{view:null,redirectTo:d.redirectTo,source:"env",warnings:i};i.push(`DEFAULT_VIEW="${r}" alias targets the "${d.target}" view, which is not enabled in this deployment (known enabled ids: ${t.map(g=>g.id).join(", ")||"(none)"}); falling through to descriptor / ambient-home`)}else{const m=t.find(g=>g.id===r);if(m!==void 0)return{view:m,source:"env",warnings:i};i.push(`DEFAULT_VIEW="${r}" does not match any enabled view (known enabled ids: ${t.map(g=>g.id).join(", ")||"(none)"}); falling through to descriptor / ambient-home`)}}const s=t.filter(d=>d.defaultRoute===!0),[u,...p]=s;if(u!==void 0&&p.length===0)return{view:u,source:"descriptor",warnings:i};if(u!==void 0){const m=[...s].sort(WI)[0]??u;return i.push(`multiple views declare defaultRoute: true (${s.map(g=>g.id).join(", ")}); picking "${m.id}" by lowest nav.order`),{view:m,source:"descriptor",warnings:i}}return{view:null,source:"fallback",warnings:i}}function VI(t,r){const i=qI(t,r);for(const s of i.warnings)UI(FI.views,s);return i}function WI(t,r){const i=t.nav?.order??Number.POSITIVE_INFINITY,s=r.nav?.order??Number.POSITIVE_INFINITY;return i!==s?i-s:t.id.localeCompare(r.id)}const HI=[{to:"/",label:"Home",end:!0,order:10},{to:"/agents",label:"Agents",order:20},{to:"/beads",label:"Beads",order:30},{to:"/runs",label:"Runs",order:40},{to:"/mail",label:"Mail",order:50}],GI={"/agents":"agents","/beads":"beads","/runs":"runs","/mail":"mail","/activity":"activity","/health":"health"};function JI(){const{resolved:t,toggle:r}=xI(),{viewingAs:i}=LI(),{operatorAlias:s}=bv(),u=CI(),p=qy(),{data:d}=mn("config",()=>Yr.config()),{data:m}=mn("cities",()=>ot().listCities()),g=Ba(),y=m?.items??[],E=g??d?.cityName??"",S=E===""||y.some(H=>H.name===E),T=y.length>1||!S,A=H=>{H!==g&&window.location.assign(`/city/${encodeURIComponent(H)}/`)},D=b.useMemo(()=>{const oe=Rv(Bv,d?.enabledModules??null).flatMap(Q=>Q.nav===null?[]:[{to:Q.path,label:Q.nav.label,end:Q.path==="/",order:Q.nav.order}]);return[...HI,...oe].sort((Q,G)=>Q.order-G.order)},[d?.enabledModules]),{pathname:W}=_n(),O=!i.isOperator&&W.startsWith("/mail");return $.jsx("header",{className:"border-b border-rule",children:$.jsxs("div",{className:"max-w-dashboard mx-auto px-4 sm:px-6 lg:px-8 py-5 flex items-baseline gap-x-6 lg:gap-x-8 gap-y-2 flex-wrap",children:[$.jsxs("div",{className:"flex items-baseline gap-3 min-w-0",children:[$.jsx("span",{className:"text-title font-semibold tracking-tight text-fg",children:"gas city"}),$.jsx("span",{className:"text-fg-muted","aria-hidden":"true",children:"·"}),T?$.jsx("label",{className:"sr-only",htmlFor:"city-switcher",children:"Switch city"}):null,T?$.jsxs("select",{id:"city-switcher",value:E,onChange:H=>A(H.target.value),className:"text-label uppercase tracking-wider text-fg-muted bg-transparent border-0 focus-mark cursor-pointer hover:text-fg transition-colors duration-150 ease-out-quart",children:[!S&&E!==""?$.jsxs("option",{value:E,disabled:!0,children:[E," (unknown)"]}):null,y.map(H=>$.jsxs("option",{value:H.name,children:[H.name,H.running?"":" (stopped)"]},H.name))]}):$.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:E||"city"}),O&&$.jsxs("span",{className:"text-label uppercase tracking-wider text-accent ml-3",children:["· reading as ",PI(i.alias,s)]}),u&&$.jsx("span",{title:Cv,className:"text-label uppercase tracking-wider text-warn ml-3",children:"· read-only"})]}),$.jsx("nav",{className:"flex-1",children:$.jsx("ul",{className:"flex items-baseline gap-x-5 lg:gap-x-7 gap-y-1 flex-wrap",children:D.map(H=>{const oe=GI[H.to];return $.jsx("li",{children:$.jsxs(W0,{to:H.to,end:H.end??!1,className:({isActive:Q})=>["text-title transition-colors duration-150 ease-out-quart focus-mark",Q?"text-fg font-semibold":"text-fg-muted font-medium hover:text-fg"].join(" "),children:[H.label,oe!==void 0&&$.jsx(vI,{label:H.label,summary:p.byDomain[oe]})]})},H.to)})})}),$.jsx("button",{type:"button",onClick:r,"aria-label":`Switch to ${t==="dark"?"light":"dark"} theme`,className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out-quart focus-mark",children:t==="dark"?"Light":"Dark"})]})})}function KI({children:t}){return $.jsxs("div",{className:"min-h-screen bg-surface text-fg antialiased",children:[$.jsx(JI,{}),$.jsx("main",{className:"max-w-dashboard mx-auto px-4 sm:px-6 lg:px-8 py-12",children:t})]})}const Pv=b.createContext(null);function QI({children:t,intervalMs:r=1e3}){const[i,s]=b.useState(()=>Date.now());return b.useEffect(()=>{const u=window.setInterval(()=>{s(Date.now())},r);return()=>{window.clearInterval(u)}},[r]),$.jsx(Pv.Provider,{value:i,children:t})}function Q6(){const t=b.useContext(Pv);if(t===null)throw new Error("useNow must be called inside a NowProvider.");return t}const YI=2e3,XI=2500;function e6(t,r,i={}){const[s,u]=b.useState("connecting"),p=b.useRef(r);p.current=r;const d=b.useRef(i.matches);d.current=i.matches;const m=b.useRef(i.coalesceMs);m.current=i.coalesceMs;const g=t.join(","),y=b.useRef(0),E=b.useRef(null);return b.useEffect(()=>{if(t.length===0){u("closed");return}let S=null,T=!1,A=null,D=null,W=1e3,O=!1;const H=()=>{D!==null&&(clearTimeout(D),D=null)},oe=ue=>{O||(O=!0,t6(ue))},Q=()=>{y.current=Date.now(),p.current()},G=()=>{const ue=m.current??XI,de=Date.now()-y.current;de>=ue?(E.current&&(clearTimeout(E.current),E.current=null),Q()):E.current===null&&(E.current=setTimeout(()=>{E.current=null,T||Q()},ue-de))},ee=()=>{const ue=globalThis.EventSource;if(typeof ue!="function"){u("closed");return}const de=Ba();if(de===null){u("closed");return}const pe=new ue(ot().cityEventStreamUrl(de));S=pe,u("connecting"),D=setTimeout(()=>{T||S!==pe||pe.readyState===ue.CLOSED||u("open")},YI),S.onopen=()=>{T||(H(),u("open"),W=1e3)};const Re=ye=>{if(T)return;let Ze=null;try{Ze=JSON.parse(ye.data)}catch{u("degraded"),oe("invalid JSON");return}if(!n6(Ze)){u("degraded"),oe("missing string event type");return}const Ke=Ze.type;if(typeof Ke!="string"){u("degraded"),oe("missing string event type");return}u("open");for(const et of t)if(Ke.startsWith(et)){const Qe=Ze;(d.current?.(Qe)??!0)&&G();break}};S.onmessage=Re,S.addEventListener("event",Re),S.onerror=()=>{T||(H(),u("closed"),S?.close(),S=null,A=setTimeout(()=>{W=Math.min(W*2,3e4),ee()},W))}};return ee(),()=>{T=!0,A&&clearTimeout(A),H(),E.current&&(clearTimeout(E.current),E.current=null),S?.close()}},[g]),s}function t6(t){Jr({component:"gc-events",operation:"parse event",message:`Malformed gc event payload: ${t}.`})}function n6(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}const r6=60*1e3;async function Da(){const t=new Date().toISOString();try{const r=await Yr.runSummary();return{source:"runs",status:"fresh",fetchedAt:t,staleAt:new Date(Date.parse(t)+r6).toISOString(),error:{kind:"none"},data:r}}catch(r){return{source:"runs",status:"error",error:s6(r,"formula runs unavailable")}}}function o6(){return Da()}function Y6(){return Da()}function i6(){return Da()}function a6(){return Da()}function s6(t,r){return t instanceof Error&&t.message.trim().length>0?t.message:r}const Zf=1e4,l6=[2e3,5e3,1e4];function u6(){const t=Ba(),r=b.useRef(null),i=b.useRef(!1),s=b.useCallback(async()=>{const ee=await o6().catch(de=>({source:"runs",status:"error",error:de instanceof Error?de.message:"formula runs unavailable"}));if(ee.status!=="error")return i.current=!1,ee;const ue=r.current;return ue===null?ee:(i.current=!0,{...ue,status:"stale"})},[]),u=b.useCallback(async()=>{const ee=await i6().catch(de=>({source:"runs",status:"error",error:de instanceof Error?de.message:"formula runs unavailable"}));if(ee.status!=="error")return ee;const ue=r.current;return ue===null?ee:(i.current=!0,{...ue,status:"stale"})},[]),{data:p,loading:d,error:m,refresh:g,cheapRefresh:y}=mn(`runs:summary:${t??"no-city"}`,a6,{refreshFetcher:s,sseRefreshFetcher:u});p!==void 0&&p.status!=="error"&&(r.current=p);const E=p??null,S=b.useRef(null);S.current=E?.status??null;const T=b.useRef(d);T.current=d;const A=b.useRef(0),D=b.useRef(null);b.useEffect(()=>{if(E===null||E.status==="error")return;const ee=t??"no-city";D.current!==ee&&(D.current=ee,g().catch(()=>{D.current=null}))},[t,g,E]);const W=b.useRef(0);b.useEffect(()=>{if(E===null)return;if(!(E.status==="error"?!0:i.current||E.data.lanesPartial===!0&&E.data.lanes.length===0&&E.data.blockedLanes.length===0)){W.current=0;return}const ue=l6[W.current];if(ue===void 0)return;W.current+=1;const de=setTimeout(()=>{g()},ue);return()=>clearTimeout(de)},[E,g]);const O=b.useRef(!1),H=b.useRef(null),oe=b.useCallback(()=>{H.current!==null&&(clearTimeout(H.current),H.current=null),A.current=Date.now(),y().catch(()=>{A.current=0})},[y]),Q=b.useCallback(()=>{if(S.current===null||S.current==="fixture")return;if(T.current){O.current=!0;return}Date.now()-A.current{if(d||!O.current)return;O.current=!1;const ee=Math.max(0,Zf-(Date.now()-A.current));return H.current=setTimeout(oe,ee),()=>{H.current!==null&&(clearTimeout(H.current),H.current=null)}},[d,oe]);const G=e6([ly.bead],Q);return{source:p,loading:d,error:m,refresh:g,sseState:G}}const Nv=b.createContext(null);function c6({children:t}){const r=u6();return $.jsx(Nv.Provider,{value:r,children:t})}function d6(){const t=b.useContext(Nv);if(t===null)throw new Error("useRunSummary must be used within a RunSummaryProvider");return t}const p6=b.lazy(()=>wn(()=>import("./Agents-CEUrRSz0.js"),__vite__mapDeps([7,8,1,9,10,2,11,3,12,6,13,14])).then(t=>({default:t.AgentsPage}))),f6=b.lazy(()=>wn(()=>import("./AgentDetail-UxV6LhC9.js"),__vite__mapDeps([15,16,6,17,12,3,10,2,8,14])).then(t=>({default:t.AgentDetailPage}))),m6=b.lazy(()=>wn(()=>import("./AmbientHome-DYE5iAQP.js"),__vite__mapDeps([18,2])).then(t=>({default:t.AmbientHomePage}))),v6=b.lazy(()=>wn(()=>import("./Beads-m4fbNWDo.js"),__vite__mapDeps([19,1,16,6,17,12,3,10,20,9,2,14])).then(t=>({default:t.BeadsPage}))),h6=b.lazy(()=>wn(()=>import("./Mail-yLXNL53p.js"),__vite__mapDeps([21,9,1,20,13,10,2,17,3])).then(t=>({default:t.MailPage}))),g6=b.lazy(()=>wn(()=>import("./FormulaRunDetail-DtW7ktOr.js"),__vite__mapDeps([22,2,16,6,17,12,3,10,23])).then(t=>({default:t.FormulaRunDetailPage}))),y6=b.lazy(()=>wn(()=>import("./Runs-C9G772Th.js"),__vite__mapDeps([24,1,2,11,3,23])).then(t=>({default:t.RunsPage})));function _6(){const{data:t,error:r}=mn("config",()=>Yr.config()),i=t?.enabledModules??null,s=t?.defaultView??null,u=TI(t,r),p=II(t),d=b.useMemo(()=>Rv(Bv,i),[i]),m=b.useMemo(()=>VI(d,s),[d,s]),g=m.view?.element??null,y=m.redirectTo??null;return $.jsx(EI,{operator:p,children:$.jsx($I,{children:$.jsx(QI,{children:$.jsx(zI,{readOnly:u,children:$.jsx(c6,{children:$.jsx(w6,{operator:p,children:$.jsxs(KI,{children:[r!==null&&$.jsx(E6,{message:r}),$.jsx(x6,{defaultRedirectTo:y,DefaultViewElement:g,enabledViews:d})]})})})})})})})}function w6({operator:t,children:r}){const{source:i}=d6(),s=iI(t,i);return $.jsx(Zy,{contributors:s,children:r})}function x6({defaultRedirectTo:t,DefaultViewElement:r,enabledViews:i}){const{pathname:s}=_n();return $.jsx(_v,{children:$.jsx(b.Suspense,{fallback:null,children:$.jsxs(N0,{children:[$.jsx(on,{path:"/",element:t!==null?$.jsx(R0,{to:t,replace:!0}):r!==null?$.jsx(r,{}):$.jsx(m6,{})}),$.jsx(on,{path:"/agents",element:$.jsx(p6,{})}),$.jsx(on,{path:"/agents/:slug",element:$.jsx(f6,{})}),$.jsx(on,{path:"/beads",element:$.jsx(v6,{})}),$.jsx(on,{path:"/runs",element:$.jsx(y6,{})}),$.jsx(on,{path:"/runs/:runId",element:$.jsx(g6,{})}),$.jsx(on,{path:"/mail",element:$.jsx(h6,{})}),i.map(u=>{const p=u.element;return $.jsx(on,{path:u.path,element:$.jsx(p,{})},u.id)}),$.jsx(on,{path:"*",element:$.jsx(I6,{})})]})})},s)}function E6({message:t}){return $.jsxs("section",{role:"alert",className:"mb-8 border border-warn/40 rounded-sm px-4 py-3 text-body text-fg-muted",children:[$.jsx("span",{className:"text-warn",children:"config unavailable:"})," ",t," · some controls may be disabled until it loads."]})}function I6(){return $.jsxs("section",{"aria-labelledby":"not-found-title",className:"space-y-3",children:[$.jsx("h1",{id:"not-found-title",className:"text-5xl font-semibold tracking-tight text-fg",children:"Page not found"}),$.jsx("p",{className:"text-title text-fg-muted",children:"No dashboard route matches this path."})]})}const S6={default:"border border-rule text-fg-muted hover:text-fg hover:bg-surface-tint",accent:"border border-accent text-accent hover:bg-accent hover:text-surface",quiet:"border border-transparent text-fg-muted hover:text-fg"},k6={sm:"px-2.5 py-1 text-label uppercase tracking-wider",md:"px-3.5 py-1.5 text-body"};function b6({tone:t="default",size:r="sm",className:i="",children:s,...u}){return $.jsx("button",{...u,className:`inline-flex items-center gap-1.5 rounded-sm transition-colors duration-150 ease-out-quart focus-mark disabled:opacity-40 disabled:cursor-not-allowed ${S6[t]} ${k6[r]} ${i}`,children:s})}const z6="https://docs.gascity.com/getting-started/quickstart",C6=/^\/city\/([^/]+)(?:\/|$)/;function T6(t){const r=C6.exec(t);if(r===null)return null;const i=r[1];if(i===void 0)return null;let s;try{s=decodeURIComponent(i)}catch{return null}return om.test(s)?{cityName:s,basename:`/city/${i}`}:null}function B6(){const t=b.useMemo(()=>T6(window.location.pathname),[]),[r,i]=b.useState({phase:"loading"}),[s,u]=b.useState(0),p=b.useCallback(()=>{i({phase:"loading"}),u(d=>d+1)},[]);return b.useEffect(()=>{let d=!1;return i({phase:"loading"}),ot().listCities().then(m=>{if(d)return;const g=m.items??[];if(t!==null){const E=g.some(S=>S.name===t.cityName);i(E?{phase:"mount"}:{phase:"unknown-city",cities:g});return}const y=g[0];if(y===void 0){i({phase:"empty"});return}window.location.replace(`/city/${encodeURIComponent(y.name)}/`)}).catch(m=>{if(!d){if(t!==null){i({phase:"mount"});return}i({phase:"error",message:m instanceof Error?m.message:"failed to load cities"})}}),()=>{d=!0}},[t,s]),t!==null&&r.phase==="mount"?(vy(t.cityName),$.jsx(U0,{basename:t.basename,future:{v7_relativeSplatPath:!0,v7_startTransition:!0},children:$.jsx(_6,{})})):r.phase==="unknown-city"&&t!==null?$.jsx(R6,{cityName:t.cityName,cities:r.cities}):r.phase==="empty"?$.jsx(P6,{}):r.phase==="error"?$.jsx(N6,{message:r.message,onRetry:p}):$.jsx(Ma,{children:$.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Resolving city…"})})}function Ma({children:t}){return $.jsx("div",{className:"min-h-screen bg-surface text-fg antialiased flex items-center justify-center px-6",children:$.jsx("div",{className:"max-w-prose w-full space-y-4",children:t})})}function R6({cityName:t,cities:r}){return $.jsx(Ma,{children:$.jsxs("section",{role:"alert",className:"space-y-4",children:[$.jsxs("h1",{className:"text-display font-semibold text-fg",children:["City “",t,"” is not registered on this supervisor."]}),r.length>0?$.jsxs("div",{className:"space-y-2",children:[$.jsx("p",{className:"text-body text-fg-muted",children:"Available cities:"}),$.jsx("ul",{className:"space-y-1",children:r.map(i=>$.jsxs("li",{children:[$.jsx("a",{href:`/city/${encodeURIComponent(i.name)}/`,className:"text-body text-accent hover:underline focus-mark",children:i.name}),i.running?null:$.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted ml-2",children:"· stopped"})]},i.name))})]}):$.jsx(Av,{})]})})}function P6(){return $.jsx(Ma,{children:$.jsxs("section",{className:"space-y-4",children:[$.jsx("h1",{className:"text-display font-semibold text-fg",children:"No cities are registered on this supervisor."}),$.jsx(Av,{})]})})}function Av(){return $.jsxs("div",{className:"space-y-3",children:[$.jsx("p",{className:"text-body text-fg-muted",children:"Create one from a terminal:"}),$.jsx("pre",{className:"text-body bg-surface-tint rounded-sm px-3 py-2 overflow-x-auto",children:$.jsx("code",{children:"gc init ~/my-city"})}),$.jsxs("p",{className:"text-body text-fg-muted",children:[$.jsx("code",{children:"gc init"})," bootstraps the city directory, registers it with the supervisor, and starts the orchestrator. Then refresh this page. See the"," ",$.jsx("a",{href:z6,target:"_blank",rel:"noreferrer",className:"text-accent hover:underline focus-mark",children:"getting-started guide"})," ","for the full walkthrough."]})]})}function N6({message:t,onRetry:r}){return $.jsx(Ma,{children:$.jsxs("section",{role:"alert",className:"space-y-4",children:[$.jsx("h1",{className:"text-display font-semibold text-fg",children:"Could not load cities."}),$.jsx("p",{className:"text-body text-fg-muted",children:t}),$.jsx(b6,{onClick:r,children:"Retry"})]})})}const Ov=document.getElementById("root");if(!Ov)throw new Error("missing #root");Ug.createRoot(Ov).render($.jsx(Vf.StrictMode,{children:$.jsx(wI,{children:$.jsx(_v,{children:$.jsx(B6,{})})})}));export{Vn as $,Zu as A,b6 as B,Ay as C,wv as D,xv as E,Y6 as F,ly as G,Ba as H,ot as I,xn as J,O6 as K,V0 as L,PI as M,G6 as N,Uu as O,n4 as P,U6 as Q,H6 as R,bI as S,py as T,dy as U,F6 as V,M6 as W,Yr as X,im as Y,Fl as Z,D6 as _,qy as a,K6 as a0,V6 as a1,J6 as a2,OI as a3,Lf as a4,oy as a5,d6 as a6,E4 as a7,I4 as a8,UE as a9,mn as b,YE as c,VE as d,X0 as e,e6 as f,CI as g,j6 as h,Cv as i,$ as j,$6 as k,NI as l,_4 as m,q6 as n,Z6 as o,Wr as p,A6 as q,b as r,W6 as s,uu as t,Q6 as u,LI as v,bv as w,Jr as x,L6 as y,Jt as z}; diff --git a/internal/api/dashboardspa/dist/assets/index-zPatq59W.js b/internal/api/dashboardspa/dist/assets/index-zPatq59W.js deleted file mode 100644 index e8fe4c460f..0000000000 --- a/internal/api/dashboardspa/dist/assets/index-zPatq59W.js +++ /dev/null @@ -1,73 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Activity-Ca_fEMiY.js","assets/routeHighlight-B30gQO2o.js","assets/PageHeader-DGfr1hUc.js","assets/time-D9v0saHV.js","assets/useVisibleRefresh-DdfUjLcH.js","assets/Health-B0fm2qWB.js","assets/format-fte2CeYD.js","assets/Agents-40RuA321.js","assets/context-window-Cu9zl36t.js","assets/projectOf-nApq7eyo.js","assets/constants-vAmcTKRZ.js","assets/SseIndicator-BpC5bgiy.js","assets/LiveSessionPeek-BpsrYunI.js","assets/Table-DRIQbbRJ.js","assets/agentReads-NY5zZttz.js","assets/AgentDetail-B_PEx1iU.js","assets/BeadDetailModal-Df6JvpcR.js","assets/Field-CQOLMLGH.js","assets/AmbientHome-Cvjm2Kmk.js","assets/Beads-BVqefDvL.js","assets/useListFilters-C9ZhD4ch.js","assets/Mail-um-BH4TD.js","assets/FormulaRunDetail-BXhub1du.js","assets/StageLadder-CwHTgfDd.js","assets/Runs-CTNTp8Tf.js"])))=>i.map(i=>d[i]); -function N0(e,n){for(var o=0;os[u]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const u of document.querySelectorAll('link[rel="modulepreload"]'))s(u);new MutationObserver(u=>{for(const p of u)if(p.type==="childList")for(const d of p.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&s(d)}).observe(document,{childList:!0,subtree:!0});function o(u){const p={};return u.integrity&&(p.integrity=u.integrity),u.referrerPolicy&&(p.referrerPolicy=u.referrerPolicy),u.crossOrigin==="use-credentials"?p.credentials="include":u.crossOrigin==="anonymous"?p.credentials="omit":p.credentials="same-origin",p}function s(u){if(u.ep)return;u.ep=!0;const p=o(u);fetch(u.href,p)}})();function Cm(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ql={exports:{}},Ho={},Vl={exports:{}},he={};var gf;function A0(){if(gf)return he;gf=1;var e=Symbol.for("react.element"),n=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),u=Symbol.for("react.profiler"),p=Symbol.for("react.provider"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),y=Symbol.for("react.memo"),w=Symbol.for("react.lazy"),I=Symbol.iterator;function z(T){return T===null||typeof T!="object"?null:(T=I&&T[I]||T["@@iterator"],typeof T=="function"?T:null)}var B={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},D=Object.assign,V={};function A(T,F,me){this.props=T,this.context=F,this.refs=V,this.updater=me||B}A.prototype.isReactComponent={},A.prototype.setState=function(T,F){if(typeof T!="object"&&typeof T!="function"&&T!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,T,F,"setState")},A.prototype.forceUpdate=function(T){this.updater.enqueueForceUpdate(this,T,"forceUpdate")};function H(){}H.prototype=A.prototype;function oe(T,F,me){this.props=T,this.context=F,this.refs=V,this.updater=me||B}var Q=oe.prototype=new H;Q.constructor=oe,D(Q,A.prototype),Q.isPureReactComponent=!0;var K=Array.isArray,Y=Object.prototype.hasOwnProperty,ue={current:null},ce={key:!0,ref:!0,__self:!0,__source:!0};function pe(T,F,me){var ge,we={},Ee=null,Ce=null;if(F!=null)for(ge in F.ref!==void 0&&(Ce=F.ref),F.key!==void 0&&(Ee=""+F.key),F)Y.call(F,ge)&&!ce.hasOwnProperty(ge)&&(we[ge]=F[ge]);var ke=arguments.length-2;if(ke===1)we.children=me;else if(1>>1,F=G[T];if(0>>1;Tu(we,ee))Eeu(Ce,we)?(G[T]=Ce,G[Ee]=ee,T=Ee):(G[T]=we,G[ge]=ee,T=ge);else if(Eeu(Ce,ee))G[T]=Ce,G[Ee]=ee,T=Ee;else break e}}return le}function u(G,le){var ee=G.sortIndex-le.sortIndex;return ee!==0?ee:G.id-le.id}if(typeof performance=="object"&&typeof performance.now=="function"){var p=performance;e.unstable_now=function(){return p.now()}}else{var d=Date,m=d.now();e.unstable_now=function(){return d.now()-m}}var h=[],y=[],w=1,I=null,z=3,B=!1,D=!1,V=!1,A=typeof setTimeout=="function"?setTimeout:null,H=typeof clearTimeout=="function"?clearTimeout:null,oe=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Q(G){for(var le=o(y);le!==null;){if(le.callback===null)s(y);else if(le.startTime<=G)s(y),le.sortIndex=le.expirationTime,n(h,le);else break;le=o(y)}}function K(G){if(V=!1,Q(G),!D)if(o(h)!==null)D=!0,ht(Y);else{var le=o(y);le!==null&&We(K,le.startTime-G)}}function Y(G,le){D=!1,V&&(V=!1,H(pe),pe=-1),B=!0;var ee=z;try{for(Q(le),I=o(h);I!==null&&(!(I.expirationTime>le)||G&&!Ae());){var T=I.callback;if(typeof T=="function"){I.callback=null,z=I.priorityLevel;var F=T(I.expirationTime<=le);le=e.unstable_now(),typeof F=="function"?I.callback=F:I===o(h)&&s(h),Q(le)}else s(h);I=o(h)}if(I!==null)var me=!0;else{var ge=o(y);ge!==null&&We(K,ge.startTime-le),me=!1}return me}finally{I=null,z=ee,B=!1}}var ue=!1,ce=null,pe=-1,Te=5,ye=-1;function Ae(){return!(e.unstable_now()-yeG||125T?(G.sortIndex=ee,n(y,G),o(h)===null&&G===o(y)&&(V?(H(pe),pe=-1):V=!0,We(K,ee-T))):(G.sortIndex=F,n(h,G),D||B||(D=!0,ht(Y))),G},e.unstable_shouldYield=Ae,e.unstable_wrapCallback=function(G){var le=z;return function(){var ee=z;z=le;try{return G.apply(this,arguments)}finally{z=ee}}}})(Kl)),Kl}var xf;function D0(){return xf||(xf=1,Hl.exports=L0()),Hl.exports}var If;function M0(){if(If)return xt;If=1;var e=hu(),n=D0();function o(t){for(var r="https://reactjs.org/docs/error-decoder.html?invariant="+t,a=1;a"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),h=Object.prototype.hasOwnProperty,y=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,w={},I={};function z(t){return h.call(I,t)?!0:h.call(w,t)?!1:y.test(t)?I[t]=!0:(w[t]=!0,!1)}function B(t,r,a,l){if(a!==null&&a.type===0)return!1;switch(typeof r){case"function":case"symbol":return!0;case"boolean":return l?!1:a!==null?!a.acceptsBooleans:(t=t.toLowerCase().slice(0,5),t!=="data-"&&t!=="aria-");default:return!1}}function D(t,r,a,l){if(r===null||typeof r>"u"||B(t,r,a,l))return!0;if(l)return!1;if(a!==null)switch(a.type){case 3:return!r;case 4:return r===!1;case 5:return isNaN(r);case 6:return isNaN(r)||1>r}return!1}function V(t,r,a,l,c,f,v){this.acceptsBooleans=r===2||r===3||r===4,this.attributeName=l,this.attributeNamespace=c,this.mustUseProperty=a,this.propertyName=t,this.type=r,this.sanitizeURL=f,this.removeEmptyString=v}var A={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(t){A[t]=new V(t,0,!1,t,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(t){var r=t[0];A[r]=new V(r,1,!1,t[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(t){A[t]=new V(t,2,!1,t.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(t){A[t]=new V(t,2,!1,t,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(t){A[t]=new V(t,3,!1,t.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(t){A[t]=new V(t,3,!0,t,null,!1,!1)}),["capture","download"].forEach(function(t){A[t]=new V(t,4,!1,t,null,!1,!1)}),["cols","rows","size","span"].forEach(function(t){A[t]=new V(t,6,!1,t,null,!1,!1)}),["rowSpan","start"].forEach(function(t){A[t]=new V(t,5,!1,t.toLowerCase(),null,!1,!1)});var H=/[\-:]([a-z])/g;function oe(t){return t[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(t){var r=t.replace(H,oe);A[r]=new V(r,1,!1,t,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(t){var r=t.replace(H,oe);A[r]=new V(r,1,!1,t,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(t){var r=t.replace(H,oe);A[r]=new V(r,1,!1,t,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(t){A[t]=new V(t,1,!1,t.toLowerCase(),null,!1,!1)}),A.xlinkHref=new V("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(t){A[t]=new V(t,1,!1,t.toLowerCase(),null,!0,!0)});function Q(t,r,a,l){var c=A.hasOwnProperty(r)?A[r]:null;(c!==null?c.type!==0:l||!(2_||c[v]!==f[_]){var S=` -`+c[v].replace(" at new "," at ");return t.displayName&&S.includes("")&&(S=S.replace("",t.displayName)),S}while(1<=v&&0<=_);break}}}finally{me=!1,Error.prepareStackTrace=a}return(t=t?t.displayName||t.name:"")?F(t):""}function we(t){switch(t.tag){case 5:return F(t.type);case 16:return F("Lazy");case 13:return F("Suspense");case 19:return F("SuspenseList");case 0:case 2:case 15:return t=ge(t.type,!1),t;case 11:return t=ge(t.type.render,!1),t;case 1:return t=ge(t.type,!0),t;default:return""}}function Ee(t){if(t==null)return null;if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case ce:return"Fragment";case ue:return"Portal";case Te:return"Profiler";case pe:return"StrictMode";case Oe:return"Suspense";case Xe:return"SuspenseList"}if(typeof t=="object")switch(t.$$typeof){case Ae:return(t.displayName||"Context")+".Consumer";case ye:return(t._context.displayName||"Context")+".Provider";case Ke:var r=t.render;return t=t.displayName,t||(t=r.displayName||r.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case kt:return r=t.displayName||null,r!==null?r:Ee(t.type)||"Memo";case ht:r=t._payload,t=t._init;try{return Ee(t(r))}catch{}}return null}function Ce(t){var r=t.type;switch(t.tag){case 24:return"Cache";case 9:return(r.displayName||"Context")+".Consumer";case 10:return(r._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return t=r.render,t=t.displayName||t.name||"",r.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case 7:return"Fragment";case 5:return r;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Ee(r);case 8:return r===pe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof r=="function")return r.displayName||r.name||null;if(typeof r=="string")return r}return null}function ke(t){switch(typeof t){case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function je(t){var r=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(r==="checkbox"||r==="radio")}function bt(t){var r=je(t)?"checked":"value",a=Object.getOwnPropertyDescriptor(t.constructor.prototype,r),l=""+t[r];if(!t.hasOwnProperty(r)&&typeof a<"u"&&typeof a.get=="function"&&typeof a.set=="function"){var c=a.get,f=a.set;return Object.defineProperty(t,r,{configurable:!0,get:function(){return c.call(this)},set:function(v){l=""+v,f.call(this,v)}}),Object.defineProperty(t,r,{enumerable:a.enumerable}),{getValue:function(){return l},setValue:function(v){l=""+v},stopTracking:function(){t._valueTracker=null,delete t[r]}}}}function di(t){t._valueTracker||(t._valueTracker=bt(t))}function xc(t){if(!t)return!1;var r=t._valueTracker;if(!r)return!0;var a=r.getValue(),l="";return t&&(l=je(t)?t.checked?"true":"false":t.value),t=l,t!==a?(r.setValue(t),!0):!1}function pi(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}function Qa(t,r){var a=r.checked;return ee({},r,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:a??t._wrapperState.initialChecked})}function Ic(t,r){var a=r.defaultValue==null?"":r.defaultValue,l=r.checked!=null?r.checked:r.defaultChecked;a=ke(r.value!=null?r.value:a),t._wrapperState={initialChecked:l,initialValue:a,controlled:r.type==="checkbox"||r.type==="radio"?r.checked!=null:r.value!=null}}function Sc(t,r){r=r.checked,r!=null&&Q(t,"checked",r,!1)}function Ya(t,r){Sc(t,r);var a=ke(r.value),l=r.type;if(a!=null)l==="number"?(a===0&&t.value===""||t.value!=a)&&(t.value=""+a):t.value!==""+a&&(t.value=""+a);else if(l==="submit"||l==="reset"){t.removeAttribute("value");return}r.hasOwnProperty("value")?Xa(t,r.type,a):r.hasOwnProperty("defaultValue")&&Xa(t,r.type,ke(r.defaultValue)),r.checked==null&&r.defaultChecked!=null&&(t.defaultChecked=!!r.defaultChecked)}function kc(t,r,a){if(r.hasOwnProperty("value")||r.hasOwnProperty("defaultValue")){var l=r.type;if(!(l!=="submit"&&l!=="reset"||r.value!==void 0&&r.value!==null))return;r=""+t._wrapperState.initialValue,a||r===t.value||(t.value=r),t.defaultValue=r}a=t.name,a!==""&&(t.name=""),t.defaultChecked=!!t._wrapperState.initialChecked,a!==""&&(t.name=a)}function Xa(t,r,a){(r!=="number"||pi(t.ownerDocument)!==t)&&(a==null?t.defaultValue=""+t._wrapperState.initialValue:t.defaultValue!==""+a&&(t.defaultValue=""+a))}var so=Array.isArray;function yr(t,r,a,l){if(t=t.options,r){r={};for(var c=0;c"+r.valueOf().toString()+"",r=fi.firstChild;t.firstChild;)t.removeChild(t.firstChild);for(;r.firstChild;)t.appendChild(r.firstChild)}});function lo(t,r){if(r){var a=t.firstChild;if(a&&a===t.lastChild&&a.nodeType===3){a.nodeValue=r;return}}t.textContent=r}var uo={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},$h=["Webkit","ms","Moz","O"];Object.keys(uo).forEach(function(t){$h.forEach(function(r){r=r+t.charAt(0).toUpperCase()+t.substring(1),uo[r]=uo[t]})});function Bc(t,r,a){return r==null||typeof r=="boolean"||r===""?"":a||typeof r!="number"||r===0||uo.hasOwnProperty(t)&&uo[t]?(""+r).trim():r+"px"}function Pc(t,r){t=t.style;for(var a in r)if(r.hasOwnProperty(a)){var l=a.indexOf("--")===0,c=Bc(a,r[a],l);a==="float"&&(a="cssFloat"),l?t.setProperty(a,c):t[a]=c}}var Lh=ee({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ns(t,r){if(r){if(Lh[t]&&(r.children!=null||r.dangerouslySetInnerHTML!=null))throw Error(o(137,t));if(r.dangerouslySetInnerHTML!=null){if(r.children!=null)throw Error(o(60));if(typeof r.dangerouslySetInnerHTML!="object"||!("__html"in r.dangerouslySetInnerHTML))throw Error(o(61))}if(r.style!=null&&typeof r.style!="object")throw Error(o(62))}}function rs(t,r){if(t.indexOf("-")===-1)return typeof r.is=="string";switch(t){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var os=null;function is(t){return t=t.target||t.srcElement||window,t.correspondingUseElement&&(t=t.correspondingUseElement),t.nodeType===3?t.parentNode:t}var as=null,_r=null,wr=null;function Nc(t){if(t=Po(t)){if(typeof as!="function")throw Error(o(280));var r=t.stateNode;r&&(r=ji(r),as(t.stateNode,t.type,r))}}function Ac(t){_r?wr?wr.push(t):wr=[t]:_r=t}function Oc(){if(_r){var t=_r,r=wr;if(wr=_r=null,Nc(t),r)for(t=0;t>>=0,t===0?32:31-(Gh(t)/Jh|0)|0}var yi=64,_i=4194304;function mo(t){switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return t&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return t}}function wi(t,r){var a=t.pendingLanes;if(a===0)return 0;var l=0,c=t.suspendedLanes,f=t.pingedLanes,v=a&268435455;if(v!==0){var _=v&~c;_!==0?l=mo(_):(f&=v,f!==0&&(l=mo(f)))}else v=a&~c,v!==0?l=mo(v):f!==0&&(l=mo(f));if(l===0)return 0;if(r!==0&&r!==l&&(r&c)===0&&(c=l&-l,f=r&-r,c>=f||c===16&&(f&4194240)!==0))return r;if((l&4)!==0&&(l|=a&16),r=t.entangledLanes,r!==0)for(t=t.entanglements,r&=l;0a;a++)r.push(t);return r}function vo(t,r,a){t.pendingLanes|=r,r!==536870912&&(t.suspendedLanes=0,t.pingedLanes=0),t=t.eventTimes,r=31-Ut(r),t[r]=a}function eg(t,r){var a=t.pendingLanes&~r;t.pendingLanes=r,t.suspendedLanes=0,t.pingedLanes=0,t.expiredLanes&=r,t.mutableReadLanes&=r,t.entangledLanes&=r,r=t.entanglements;var l=t.eventTimes;for(t=t.expirationTimes;0=Io),ud=" ",cd=!1;function dd(t,r){switch(t){case"keyup":return Tg.indexOf(r.keyCode)!==-1;case"keydown":return r.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pd(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Ir=!1;function Rg(t,r){switch(t){case"compositionend":return pd(r);case"keypress":return r.which!==32?null:(cd=!0,ud);case"textInput":return t=r.data,t===ud&&cd?null:t;default:return null}}function Bg(t,r){if(Ir)return t==="compositionend"||!Ss&&dd(t,r)?(t=rd(),ki=ys=zn=null,Ir=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(r.ctrlKey||r.altKey||r.metaKey)||r.ctrlKey&&r.altKey){if(r.char&&1=r)return{node:a,offset:r-t};t=l}e:{for(;a;){if(a.nextSibling){a=a.nextSibling;break e}a=a.parentNode}a=void 0}a=_d(a)}}function Ed(t,r){return t&&r?t===r?!0:t&&t.nodeType===3?!1:r&&r.nodeType===3?Ed(t,r.parentNode):"contains"in t?t.contains(r):t.compareDocumentPosition?!!(t.compareDocumentPosition(r)&16):!1:!1}function xd(){for(var t=window,r=pi();r instanceof t.HTMLIFrameElement;){try{var a=typeof r.contentWindow.location.href=="string"}catch{a=!1}if(a)t=r.contentWindow;else break;r=pi(t.document)}return r}function zs(t){var r=t&&t.nodeName&&t.nodeName.toLowerCase();return r&&(r==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||r==="textarea"||t.contentEditable==="true")}function Mg(t){var r=xd(),a=t.focusedElem,l=t.selectionRange;if(r!==a&&a&&a.ownerDocument&&Ed(a.ownerDocument.documentElement,a)){if(l!==null&&zs(a)){if(r=l.start,t=l.end,t===void 0&&(t=r),"selectionStart"in a)a.selectionStart=r,a.selectionEnd=Math.min(t,a.value.length);else if(t=(r=a.ownerDocument||document)&&r.defaultView||window,t.getSelection){t=t.getSelection();var c=a.textContent.length,f=Math.min(l.start,c);l=l.end===void 0?f:Math.min(l.end,c),!t.extend&&f>l&&(c=l,l=f,f=c),c=wd(a,f);var v=wd(a,l);c&&v&&(t.rangeCount!==1||t.anchorNode!==c.node||t.anchorOffset!==c.offset||t.focusNode!==v.node||t.focusOffset!==v.offset)&&(r=r.createRange(),r.setStart(c.node,c.offset),t.removeAllRanges(),f>l?(t.addRange(r),t.extend(v.node,v.offset)):(r.setEnd(v.node,v.offset),t.addRange(r)))}}for(r=[],t=a;t=t.parentNode;)t.nodeType===1&&r.push({element:t,left:t.scrollLeft,top:t.scrollTop});for(typeof a.focus=="function"&&a.focus(),a=0;a=document.documentMode,Sr=null,Ts=null,zo=null,Cs=!1;function Id(t,r,a){var l=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;Cs||Sr==null||Sr!==pi(l)||(l=Sr,"selectionStart"in l&&zs(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),zo&&bo(zo,l)||(zo=l,l=Ni(Ts,"onSelect"),0Cr||(t.current=Fs[Cr],Fs[Cr]=null,Cr--)}function Re(t,r){Cr++,Fs[Cr]=t.current,t.current=r}var Bn={},lt=Rn(Bn),gt=Rn(!1),er=Bn;function Rr(t,r){var a=t.type.contextTypes;if(!a)return Bn;var l=t.stateNode;if(l&&l.__reactInternalMemoizedUnmaskedChildContext===r)return l.__reactInternalMemoizedMaskedChildContext;var c={},f;for(f in a)c[f]=r[f];return l&&(t=t.stateNode,t.__reactInternalMemoizedUnmaskedChildContext=r,t.__reactInternalMemoizedMaskedChildContext=c),c}function yt(t){return t=t.childContextTypes,t!=null}function $i(){Ne(gt),Ne(lt)}function Ld(t,r,a){if(lt.current!==Bn)throw Error(o(168));Re(lt,r),Re(gt,a)}function Dd(t,r,a){var l=t.stateNode;if(r=r.childContextTypes,typeof l.getChildContext!="function")return a;l=l.getChildContext();for(var c in l)if(!(c in r))throw Error(o(108,Ce(t)||"Unknown",c));return ee({},a,l)}function Li(t){return t=(t=t.stateNode)&&t.__reactInternalMemoizedMergedChildContext||Bn,er=lt.current,Re(lt,t),Re(gt,gt.current),!0}function Md(t,r,a){var l=t.stateNode;if(!l)throw Error(o(169));a?(t=Dd(t,r,er),l.__reactInternalMemoizedMergedChildContext=t,Ne(gt),Ne(lt),Re(lt,t)):Ne(gt),Re(gt,a)}var sn=null,Di=!1,Us=!1;function Fd(t){sn===null?sn=[t]:sn.push(t)}function Yg(t){Di=!0,Fd(t)}function Pn(){if(!Us&&sn!==null){Us=!0;var t=0,r=be;try{var a=sn;for(be=1;t>=v,c-=v,ln=1<<32-Ut(r)+c|a<de?(ot=se,se=null):ot=se.sibling;var xe=M(C,se,R[de],W);if(xe===null){se===null&&(se=ot);break}t&&se&&xe.alternate===null&&r(C,se),k=f(xe,k,de),ae===null?re=xe:ae.sibling=xe,ae=xe,se=ot}if(de===R.length)return a(C,se),$e&&nr(C,de),re;if(se===null){for(;dede?(ot=se,se=null):ot=se.sibling;var Fn=M(C,se,xe.value,W);if(Fn===null){se===null&&(se=ot);break}t&&se&&Fn.alternate===null&&r(C,se),k=f(Fn,k,de),ae===null?re=Fn:ae.sibling=Fn,ae=Fn,se=ot}if(xe.done)return a(C,se),$e&&nr(C,de),re;if(se===null){for(;!xe.done;de++,xe=R.next())xe=q(C,xe.value,W),xe!==null&&(k=f(xe,k,de),ae===null?re=xe:ae.sibling=xe,ae=xe);return $e&&nr(C,de),re}for(se=l(C,se);!xe.done;de++,xe=R.next())xe=J(se,C,de,xe.value,W),xe!==null&&(t&&xe.alternate!==null&&se.delete(xe.key===null?de:xe.key),k=f(xe,k,de),ae===null?re=xe:ae.sibling=xe,ae=xe);return t&&se.forEach(function(P0){return r(C,P0)}),$e&&nr(C,de),re}function Je(C,k,R,W){if(typeof R=="object"&&R!==null&&R.type===ce&&R.key===null&&(R=R.props.children),typeof R=="object"&&R!==null){switch(R.$$typeof){case Y:e:{for(var re=R.key,ae=k;ae!==null;){if(ae.key===re){if(re=R.type,re===ce){if(ae.tag===7){a(C,ae.sibling),k=c(ae,R.props.children),k.return=C,C=k;break e}}else if(ae.elementType===re||typeof re=="object"&&re!==null&&re.$$typeof===ht&&Hd(re)===ae.type){a(C,ae.sibling),k=c(ae,R.props),k.ref=No(C,ae,R),k.return=C,C=k;break e}a(C,ae);break}else r(C,ae);ae=ae.sibling}R.type===ce?(k=cr(R.props.children,C.mode,W,R.key),k.return=C,C=k):(W=fa(R.type,R.key,R.props,null,C.mode,W),W.ref=No(C,k,R),W.return=C,C=W)}return v(C);case ue:e:{for(ae=R.key;k!==null;){if(k.key===ae)if(k.tag===4&&k.stateNode.containerInfo===R.containerInfo&&k.stateNode.implementation===R.implementation){a(C,k.sibling),k=c(k,R.children||[]),k.return=C,C=k;break e}else{a(C,k);break}else r(C,k);k=k.sibling}k=Dl(R,C.mode,W),k.return=C,C=k}return v(C);case ht:return ae=R._init,Je(C,k,ae(R._payload),W)}if(so(R))return te(C,k,R,W);if(le(R))return ne(C,k,R,W);Zi(C,R)}return typeof R=="string"&&R!==""||typeof R=="number"?(R=""+R,k!==null&&k.tag===6?(a(C,k.sibling),k=c(k,R),k.return=C,C=k):(a(C,k),k=Ll(R,C.mode,W),k.return=C,C=k),v(C)):a(C,k)}return Je}var Ar=Kd(!0),Gd=Kd(!1),qi=Rn(null),Vi=null,Or=null,Ks=null;function Gs(){Ks=Or=Vi=null}function Js(t){var r=qi.current;Ne(qi),t._currentValue=r}function Qs(t,r,a){for(;t!==null;){var l=t.alternate;if((t.childLanes&r)!==r?(t.childLanes|=r,l!==null&&(l.childLanes|=r)):l!==null&&(l.childLanes&r)!==r&&(l.childLanes|=r),t===a)break;t=t.return}}function jr(t,r){Vi=t,Ks=Or=null,t=t.dependencies,t!==null&&t.firstContext!==null&&((t.lanes&r)!==0&&(_t=!0),t.firstContext=null)}function At(t){var r=t._currentValue;if(Ks!==t)if(t={context:t,memoizedValue:r,next:null},Or===null){if(Vi===null)throw Error(o(308));Or=t,Vi.dependencies={lanes:0,firstContext:t}}else Or=Or.next=t;return r}var rr=null;function Ys(t){rr===null?rr=[t]:rr.push(t)}function Jd(t,r,a,l){var c=r.interleaved;return c===null?(a.next=a,Ys(r)):(a.next=c.next,c.next=a),r.interleaved=a,cn(t,l)}function cn(t,r){t.lanes|=r;var a=t.alternate;for(a!==null&&(a.lanes|=r),a=t,t=t.return;t!==null;)t.childLanes|=r,a=t.alternate,a!==null&&(a.childLanes|=r),a=t,t=t.return;return a.tag===3?a.stateNode:null}var Nn=!1;function Xs(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Qd(t,r){t=t.updateQueue,r.updateQueue===t&&(r.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,effects:t.effects})}function dn(t,r){return{eventTime:t,lane:r,tag:0,payload:null,callback:null,next:null}}function An(t,r,a){var l=t.updateQueue;if(l===null)return null;if(l=l.shared,(_e&2)!==0){var c=l.pending;return c===null?r.next=r:(r.next=c.next,c.next=r),l.pending=r,cn(t,a)}return c=l.interleaved,c===null?(r.next=r,Ys(l)):(r.next=c.next,c.next=r),l.interleaved=r,cn(t,a)}function Wi(t,r,a){if(r=r.updateQueue,r!==null&&(r=r.shared,(a&4194240)!==0)){var l=r.lanes;l&=t.pendingLanes,a|=l,r.lanes=a,fs(t,a)}}function Yd(t,r){var a=t.updateQueue,l=t.alternate;if(l!==null&&(l=l.updateQueue,a===l)){var c=null,f=null;if(a=a.firstBaseUpdate,a!==null){do{var v={eventTime:a.eventTime,lane:a.lane,tag:a.tag,payload:a.payload,callback:a.callback,next:null};f===null?c=f=v:f=f.next=v,a=a.next}while(a!==null);f===null?c=f=r:f=f.next=r}else c=f=r;a={baseState:l.baseState,firstBaseUpdate:c,lastBaseUpdate:f,shared:l.shared,effects:l.effects},t.updateQueue=a;return}t=a.lastBaseUpdate,t===null?a.firstBaseUpdate=r:t.next=r,a.lastBaseUpdate=r}function Hi(t,r,a,l){var c=t.updateQueue;Nn=!1;var f=c.firstBaseUpdate,v=c.lastBaseUpdate,_=c.shared.pending;if(_!==null){c.shared.pending=null;var S=_,P=S.next;S.next=null,v===null?f=P:v.next=P,v=S;var U=t.alternate;U!==null&&(U=U.updateQueue,_=U.lastBaseUpdate,_!==v&&(_===null?U.firstBaseUpdate=P:_.next=P,U.lastBaseUpdate=S))}if(f!==null){var q=c.baseState;v=0,U=P=S=null,_=f;do{var M=_.lane,J=_.eventTime;if((l&M)===M){U!==null&&(U=U.next={eventTime:J,lane:0,tag:_.tag,payload:_.payload,callback:_.callback,next:null});e:{var te=t,ne=_;switch(M=r,J=a,ne.tag){case 1:if(te=ne.payload,typeof te=="function"){q=te.call(J,q,M);break e}q=te;break e;case 3:te.flags=te.flags&-65537|128;case 0:if(te=ne.payload,M=typeof te=="function"?te.call(J,q,M):te,M==null)break e;q=ee({},q,M);break e;case 2:Nn=!0}}_.callback!==null&&_.lane!==0&&(t.flags|=64,M=c.effects,M===null?c.effects=[_]:M.push(_))}else J={eventTime:J,lane:M,tag:_.tag,payload:_.payload,callback:_.callback,next:null},U===null?(P=U=J,S=q):U=U.next=J,v|=M;if(_=_.next,_===null){if(_=c.shared.pending,_===null)break;M=_,_=M.next,M.next=null,c.lastBaseUpdate=M,c.shared.pending=null}}while(!0);if(U===null&&(S=q),c.baseState=S,c.firstBaseUpdate=P,c.lastBaseUpdate=U,r=c.shared.interleaved,r!==null){c=r;do v|=c.lane,c=c.next;while(c!==r)}else f===null&&(c.shared.lanes=0);ar|=v,t.lanes=v,t.memoizedState=q}}function Xd(t,r,a){if(t=r.effects,r.effects=null,t!==null)for(r=0;ra?a:4,t(!0);var l=ol.transition;ol.transition={};try{t(!1),r()}finally{be=a,ol.transition=l}}function yp(){return Ot().memoizedState}function n0(t,r,a){var l=Ln(t);if(a={lane:l,action:a,hasEagerState:!1,eagerState:null,next:null},_p(t))wp(r,a);else if(a=Jd(t,r,a,l),a!==null){var c=ft();Kt(a,t,l,c),Ep(a,r,l)}}function r0(t,r,a){var l=Ln(t),c={lane:l,action:a,hasEagerState:!1,eagerState:null,next:null};if(_p(t))wp(r,c);else{var f=t.alternate;if(t.lanes===0&&(f===null||f.lanes===0)&&(f=r.lastRenderedReducer,f!==null))try{var v=r.lastRenderedState,_=f(v,a);if(c.hasEagerState=!0,c.eagerState=_,Zt(_,v)){var S=r.interleaved;S===null?(c.next=c,Ys(r)):(c.next=S.next,S.next=c),r.interleaved=c;return}}catch{}a=Jd(t,r,c,l),a!==null&&(c=ft(),Kt(a,t,l,c),Ep(a,r,l))}}function _p(t){var r=t.alternate;return t===Ze||r!==null&&r===Ze}function wp(t,r){$o=Ji=!0;var a=t.pending;a===null?r.next=r:(r.next=a.next,a.next=r),t.pending=r}function Ep(t,r,a){if((a&4194240)!==0){var l=r.lanes;l&=t.pendingLanes,a|=l,r.lanes=a,fs(t,a)}}var Xi={readContext:At,useCallback:ut,useContext:ut,useEffect:ut,useImperativeHandle:ut,useInsertionEffect:ut,useLayoutEffect:ut,useMemo:ut,useReducer:ut,useRef:ut,useState:ut,useDebugValue:ut,useDeferredValue:ut,useTransition:ut,useMutableSource:ut,useSyncExternalStore:ut,useId:ut,unstable_isNewReconciler:!1},o0={readContext:At,useCallback:function(t,r){return tn().memoizedState=[t,r===void 0?null:r],t},useContext:At,useEffect:cp,useImperativeHandle:function(t,r,a){return a=a!=null?a.concat([t]):null,Qi(4194308,4,fp.bind(null,r,t),a)},useLayoutEffect:function(t,r){return Qi(4194308,4,t,r)},useInsertionEffect:function(t,r){return Qi(4,2,t,r)},useMemo:function(t,r){var a=tn();return r=r===void 0?null:r,t=t(),a.memoizedState=[t,r],t},useReducer:function(t,r,a){var l=tn();return r=a!==void 0?a(r):r,l.memoizedState=l.baseState=r,t={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:r},l.queue=t,t=t.dispatch=n0.bind(null,Ze,t),[l.memoizedState,t]},useRef:function(t){var r=tn();return t={current:t},r.memoizedState=t},useState:lp,useDebugValue:dl,useDeferredValue:function(t){return tn().memoizedState=t},useTransition:function(){var t=lp(!1),r=t[0];return t=t0.bind(null,t[1]),tn().memoizedState=t,[r,t]},useMutableSource:function(){},useSyncExternalStore:function(t,r,a){var l=Ze,c=tn();if($e){if(a===void 0)throw Error(o(407));a=a()}else{if(a=r(),rt===null)throw Error(o(349));(ir&30)!==0||rp(l,r,a)}c.memoizedState=a;var f={value:a,getSnapshot:r};return c.queue=f,cp(ip.bind(null,l,f,t),[t]),l.flags|=2048,Mo(9,op.bind(null,l,f,a,r),void 0,null),a},useId:function(){var t=tn(),r=rt.identifierPrefix;if($e){var a=un,l=ln;a=(l&~(1<<32-Ut(l)-1)).toString(32)+a,r=":"+r+"R"+a,a=Lo++,0<\/script>",t=t.removeChild(t.firstChild)):typeof l.is=="string"?t=v.createElement(a,{is:l.is}):(t=v.createElement(a),a==="select"&&(v=t,l.multiple?v.multiple=!0:l.size&&(v.size=l.size))):t=v.createElementNS(t,a),t[Xt]=r,t[Bo]=l,Fp(t,r,!1,!1),r.stateNode=t;e:{switch(v=rs(a,l),a){case"dialog":Pe("cancel",t),Pe("close",t),c=l;break;case"iframe":case"object":case"embed":Pe("load",t),c=l;break;case"video":case"audio":for(c=0;cFr&&(r.flags|=128,l=!0,Fo(f,!1),r.lanes=4194304)}else{if(!l)if(t=Ki(v),t!==null){if(r.flags|=128,l=!0,a=t.updateQueue,a!==null&&(r.updateQueue=a,r.flags|=4),Fo(f,!0),f.tail===null&&f.tailMode==="hidden"&&!v.alternate&&!$e)return ct(r),null}else 2*Ge()-f.renderingStartTime>Fr&&a!==1073741824&&(r.flags|=128,l=!0,Fo(f,!1),r.lanes=4194304);f.isBackwards?(v.sibling=r.child,r.child=v):(a=f.last,a!==null?a.sibling=v:r.child=v,f.last=v)}return f.tail!==null?(r=f.tail,f.rendering=r,f.tail=r.sibling,f.renderingStartTime=Ge(),r.sibling=null,a=Ue.current,Re(Ue,l?a&1|2:a&1),r):(ct(r),null);case 22:case 23:return Ol(),l=r.memoizedState!==null,t!==null&&t.memoizedState!==null!==l&&(r.flags|=8192),l&&(r.mode&1)!==0?(Rt&1073741824)!==0&&(ct(r),r.subtreeFlags&6&&(r.flags|=8192)):ct(r),null;case 24:return null;case 25:return null}throw Error(o(156,r.tag))}function p0(t,r){switch(qs(r),r.tag){case 1:return yt(r.type)&&$i(),t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 3:return $r(),Ne(gt),Ne(lt),rl(),t=r.flags,(t&65536)!==0&&(t&128)===0?(r.flags=t&-65537|128,r):null;case 5:return tl(r),null;case 13:if(Ne(Ue),t=r.memoizedState,t!==null&&t.dehydrated!==null){if(r.alternate===null)throw Error(o(340));Nr()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 19:return Ne(Ue),null;case 4:return $r(),null;case 10:return Js(r.type._context),null;case 22:case 23:return Ol(),null;case 24:return null;default:return null}}var ra=!1,dt=!1,f0=typeof WeakSet=="function"?WeakSet:Set,X=null;function Dr(t,r){var a=t.ref;if(a!==null)if(typeof a=="function")try{a(null)}catch(l){He(t,r,l)}else a.current=null}function Il(t,r,a){try{a()}catch(l){He(t,r,l)}}var qp=!1;function m0(t,r){if(Os=Ii,t=xd(),zs(t)){if("selectionStart"in t)var a={start:t.selectionStart,end:t.selectionEnd};else e:{a=(a=t.ownerDocument)&&a.defaultView||window;var l=a.getSelection&&a.getSelection();if(l&&l.rangeCount!==0){a=l.anchorNode;var c=l.anchorOffset,f=l.focusNode;l=l.focusOffset;try{a.nodeType,f.nodeType}catch{a=null;break e}var v=0,_=-1,S=-1,P=0,U=0,q=t,M=null;t:for(;;){for(var J;q!==a||c!==0&&q.nodeType!==3||(_=v+c),q!==f||l!==0&&q.nodeType!==3||(S=v+l),q.nodeType===3&&(v+=q.nodeValue.length),(J=q.firstChild)!==null;)M=q,q=J;for(;;){if(q===t)break t;if(M===a&&++P===c&&(_=v),M===f&&++U===l&&(S=v),(J=q.nextSibling)!==null)break;q=M,M=q.parentNode}q=J}a=_===-1||S===-1?null:{start:_,end:S}}else a=null}a=a||{start:0,end:0}}else a=null;for(js={focusedElem:t,selectionRange:a},Ii=!1,X=r;X!==null;)if(r=X,t=r.child,(r.subtreeFlags&1028)!==0&&t!==null)t.return=r,X=t;else for(;X!==null;){r=X;try{var te=r.alternate;if((r.flags&1024)!==0)switch(r.tag){case 0:case 11:case 15:break;case 1:if(te!==null){var ne=te.memoizedProps,Je=te.memoizedState,C=r.stateNode,k=C.getSnapshotBeforeUpdate(r.elementType===r.type?ne:Vt(r.type,ne),Je);C.__reactInternalSnapshotBeforeUpdate=k}break;case 3:var R=r.stateNode.containerInfo;R.nodeType===1?R.textContent="":R.nodeType===9&&R.documentElement&&R.removeChild(R.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(o(163))}}catch(W){He(r,r.return,W)}if(t=r.sibling,t!==null){t.return=r.return,X=t;break}X=r.return}return te=qp,qp=!1,te}function Uo(t,r,a){var l=r.updateQueue;if(l=l!==null?l.lastEffect:null,l!==null){var c=l=l.next;do{if((c.tag&t)===t){var f=c.destroy;c.destroy=void 0,f!==void 0&&Il(r,a,f)}c=c.next}while(c!==l)}}function oa(t,r){if(r=r.updateQueue,r=r!==null?r.lastEffect:null,r!==null){var a=r=r.next;do{if((a.tag&t)===t){var l=a.create;a.destroy=l()}a=a.next}while(a!==r)}}function Sl(t){var r=t.ref;if(r!==null){var a=t.stateNode;t.tag,t=a,typeof r=="function"?r(t):r.current=t}}function Vp(t){var r=t.alternate;r!==null&&(t.alternate=null,Vp(r)),t.child=null,t.deletions=null,t.sibling=null,t.tag===5&&(r=t.stateNode,r!==null&&(delete r[Xt],delete r[Bo],delete r[Ms],delete r[Jg],delete r[Qg])),t.stateNode=null,t.return=null,t.dependencies=null,t.memoizedProps=null,t.memoizedState=null,t.pendingProps=null,t.stateNode=null,t.updateQueue=null}function Wp(t){return t.tag===5||t.tag===3||t.tag===4}function Hp(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||Wp(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.flags&2||t.child===null||t.tag===4)continue e;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function kl(t,r,a){var l=t.tag;if(l===5||l===6)t=t.stateNode,r?a.nodeType===8?a.parentNode.insertBefore(t,r):a.insertBefore(t,r):(a.nodeType===8?(r=a.parentNode,r.insertBefore(t,a)):(r=a,r.appendChild(t)),a=a._reactRootContainer,a!=null||r.onclick!==null||(r.onclick=Oi));else if(l!==4&&(t=t.child,t!==null))for(kl(t,r,a),t=t.sibling;t!==null;)kl(t,r,a),t=t.sibling}function bl(t,r,a){var l=t.tag;if(l===5||l===6)t=t.stateNode,r?a.insertBefore(t,r):a.appendChild(t);else if(l!==4&&(t=t.child,t!==null))for(bl(t,r,a),t=t.sibling;t!==null;)bl(t,r,a),t=t.sibling}var at=null,Wt=!1;function On(t,r,a){for(a=a.child;a!==null;)Kp(t,r,a),a=a.sibling}function Kp(t,r,a){if(Yt&&typeof Yt.onCommitFiberUnmount=="function")try{Yt.onCommitFiberUnmount(gi,a)}catch{}switch(a.tag){case 5:dt||Dr(a,r);case 6:var l=at,c=Wt;at=null,On(t,r,a),at=l,Wt=c,at!==null&&(Wt?(t=at,a=a.stateNode,t.nodeType===8?t.parentNode.removeChild(a):t.removeChild(a)):at.removeChild(a.stateNode));break;case 18:at!==null&&(Wt?(t=at,a=a.stateNode,t.nodeType===8?Ds(t.parentNode,a):t.nodeType===1&&Ds(t,a),wo(t)):Ds(at,a.stateNode));break;case 4:l=at,c=Wt,at=a.stateNode.containerInfo,Wt=!0,On(t,r,a),at=l,Wt=c;break;case 0:case 11:case 14:case 15:if(!dt&&(l=a.updateQueue,l!==null&&(l=l.lastEffect,l!==null))){c=l=l.next;do{var f=c,v=f.destroy;f=f.tag,v!==void 0&&((f&2)!==0||(f&4)!==0)&&Il(a,r,v),c=c.next}while(c!==l)}On(t,r,a);break;case 1:if(!dt&&(Dr(a,r),l=a.stateNode,typeof l.componentWillUnmount=="function"))try{l.props=a.memoizedProps,l.state=a.memoizedState,l.componentWillUnmount()}catch(_){He(a,r,_)}On(t,r,a);break;case 21:On(t,r,a);break;case 22:a.mode&1?(dt=(l=dt)||a.memoizedState!==null,On(t,r,a),dt=l):On(t,r,a);break;default:On(t,r,a)}}function Gp(t){var r=t.updateQueue;if(r!==null){t.updateQueue=null;var a=t.stateNode;a===null&&(a=t.stateNode=new f0),r.forEach(function(l){var c=I0.bind(null,t,l);a.has(l)||(a.add(l),l.then(c,c))})}}function Ht(t,r){var a=r.deletions;if(a!==null)for(var l=0;lc&&(c=v),l&=~f}if(l=c,l=Ge()-l,l=(120>l?120:480>l?480:1080>l?1080:1920>l?1920:3e3>l?3e3:4320>l?4320:1960*h0(l/1960))-l,10t?16:t,$n===null)var l=!1;else{if(t=$n,$n=null,ua=0,(_e&6)!==0)throw Error(o(331));var c=_e;for(_e|=4,X=t.current;X!==null;){var f=X,v=f.child;if((X.flags&16)!==0){var _=f.deletions;if(_!==null){for(var S=0;S<_.length;S++){var P=_[S];for(X=P;X!==null;){var U=X;switch(U.tag){case 0:case 11:case 15:Uo(8,U,f)}var q=U.child;if(q!==null)q.return=U,X=q;else for(;X!==null;){U=X;var M=U.sibling,J=U.return;if(Vp(U),U===P){X=null;break}if(M!==null){M.return=J,X=M;break}X=J}}}var te=f.alternate;if(te!==null){var ne=te.child;if(ne!==null){te.child=null;do{var Je=ne.sibling;ne.sibling=null,ne=Je}while(ne!==null)}}X=f}}if((f.subtreeFlags&2064)!==0&&v!==null)v.return=f,X=v;else e:for(;X!==null;){if(f=X,(f.flags&2048)!==0)switch(f.tag){case 0:case 11:case 15:Uo(9,f,f.return)}var C=f.sibling;if(C!==null){C.return=f.return,X=C;break e}X=f.return}}var k=t.current;for(X=k;X!==null;){v=X;var R=v.child;if((v.subtreeFlags&2064)!==0&&R!==null)R.return=v,X=R;else e:for(v=k;X!==null;){if(_=X,(_.flags&2048)!==0)try{switch(_.tag){case 0:case 11:case 15:oa(9,_)}}catch(re){He(_,_.return,re)}if(_===v){X=null;break e}var W=_.sibling;if(W!==null){W.return=_.return,X=W;break e}X=_.return}}if(_e=c,Pn(),Yt&&typeof Yt.onPostCommitFiberRoot=="function")try{Yt.onPostCommitFiberRoot(gi,t)}catch{}l=!0}return l}finally{be=a,jt.transition=r}}return!1}function lf(t,r,a){r=Lr(a,r),r=kp(t,r,1),t=An(t,r,1),r=ft(),t!==null&&(vo(t,1,r),Et(t,r))}function He(t,r,a){if(t.tag===3)lf(t,t,a);else for(;r!==null;){if(r.tag===3){lf(r,t,a);break}else if(r.tag===1){var l=r.stateNode;if(typeof r.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(jn===null||!jn.has(l))){t=Lr(a,t),t=bp(r,t,1),r=An(r,t,1),t=ft(),r!==null&&(vo(r,1,t),Et(r,t));break}}r=r.return}}function E0(t,r,a){var l=t.pingCache;l!==null&&l.delete(r),r=ft(),t.pingedLanes|=t.suspendedLanes&a,rt===t&&(st&a)===a&&(tt===4||tt===3&&(st&130023424)===st&&500>Ge()-Cl?lr(t,0):Tl|=a),Et(t,r)}function uf(t,r){r===0&&((t.mode&1)===0?r=1:(r=_i,_i<<=1,(_i&130023424)===0&&(_i=4194304)));var a=ft();t=cn(t,r),t!==null&&(vo(t,r,a),Et(t,a))}function x0(t){var r=t.memoizedState,a=0;r!==null&&(a=r.retryLane),uf(t,a)}function I0(t,r){var a=0;switch(t.tag){case 13:var l=t.stateNode,c=t.memoizedState;c!==null&&(a=c.retryLane);break;case 19:l=t.stateNode;break;default:throw Error(o(314))}l!==null&&l.delete(r),uf(t,a)}var cf;cf=function(t,r,a){if(t!==null)if(t.memoizedProps!==r.pendingProps||gt.current)_t=!0;else{if((t.lanes&a)===0&&(r.flags&128)===0)return _t=!1,c0(t,r,a);_t=(t.flags&131072)!==0}else _t=!1,$e&&(r.flags&1048576)!==0&&Ud(r,Fi,r.index);switch(r.lanes=0,r.tag){case 2:var l=r.type;na(t,r),t=r.pendingProps;var c=Rr(r,lt.current);jr(r,a),c=al(null,r,l,t,c,a);var f=sl();return r.flags|=1,typeof c=="object"&&c!==null&&typeof c.render=="function"&&c.$$typeof===void 0?(r.tag=1,r.memoizedState=null,r.updateQueue=null,yt(l)?(f=!0,Li(r)):f=!1,r.memoizedState=c.state!==null&&c.state!==void 0?c.state:null,Xs(r),c.updater=ea,r.stateNode=c,c._reactInternals=r,fl(r,l,t,a),r=gl(null,r,l,!0,f,a)):(r.tag=0,$e&&f&&Zs(r),pt(null,r,c,a),r=r.child),r;case 16:l=r.elementType;e:{switch(na(t,r),t=r.pendingProps,c=l._init,l=c(l._payload),r.type=l,c=r.tag=k0(l),t=Vt(l,t),c){case 0:r=hl(null,r,l,t,a);break e;case 1:r=Op(null,r,l,t,a);break e;case 11:r=Rp(null,r,l,t,a);break e;case 14:r=Bp(null,r,l,Vt(l.type,t),a);break e}throw Error(o(306,l,""))}return r;case 0:return l=r.type,c=r.pendingProps,c=r.elementType===l?c:Vt(l,c),hl(t,r,l,c,a);case 1:return l=r.type,c=r.pendingProps,c=r.elementType===l?c:Vt(l,c),Op(t,r,l,c,a);case 3:e:{if(jp(r),t===null)throw Error(o(387));l=r.pendingProps,f=r.memoizedState,c=f.element,Qd(t,r),Hi(r,l,null,a);var v=r.memoizedState;if(l=v.element,f.isDehydrated)if(f={element:l,isDehydrated:!1,cache:v.cache,pendingSuspenseBoundaries:v.pendingSuspenseBoundaries,transitions:v.transitions},r.updateQueue.baseState=f,r.memoizedState=f,r.flags&256){c=Lr(Error(o(423)),r),r=$p(t,r,l,a,c);break e}else if(l!==c){c=Lr(Error(o(424)),r),r=$p(t,r,l,a,c);break e}else for(Ct=Cn(r.stateNode.containerInfo.firstChild),Tt=r,$e=!0,qt=null,a=Gd(r,null,l,a),r.child=a;a;)a.flags=a.flags&-3|4096,a=a.sibling;else{if(Nr(),l===c){r=pn(t,r,a);break e}pt(t,r,l,a)}r=r.child}return r;case 5:return ep(r),t===null&&Ws(r),l=r.type,c=r.pendingProps,f=t!==null?t.memoizedProps:null,v=c.children,$s(l,c)?v=null:f!==null&&$s(l,f)&&(r.flags|=32),Ap(t,r),pt(t,r,v,a),r.child;case 6:return t===null&&Ws(r),null;case 13:return Lp(t,r,a);case 4:return el(r,r.stateNode.containerInfo),l=r.pendingProps,t===null?r.child=Ar(r,null,l,a):pt(t,r,l,a),r.child;case 11:return l=r.type,c=r.pendingProps,c=r.elementType===l?c:Vt(l,c),Rp(t,r,l,c,a);case 7:return pt(t,r,r.pendingProps,a),r.child;case 8:return pt(t,r,r.pendingProps.children,a),r.child;case 12:return pt(t,r,r.pendingProps.children,a),r.child;case 10:e:{if(l=r.type._context,c=r.pendingProps,f=r.memoizedProps,v=c.value,Re(qi,l._currentValue),l._currentValue=v,f!==null)if(Zt(f.value,v)){if(f.children===c.children&&!gt.current){r=pn(t,r,a);break e}}else for(f=r.child,f!==null&&(f.return=r);f!==null;){var _=f.dependencies;if(_!==null){v=f.child;for(var S=_.firstContext;S!==null;){if(S.context===l){if(f.tag===1){S=dn(-1,a&-a),S.tag=2;var P=f.updateQueue;if(P!==null){P=P.shared;var U=P.pending;U===null?S.next=S:(S.next=U.next,U.next=S),P.pending=S}}f.lanes|=a,S=f.alternate,S!==null&&(S.lanes|=a),Qs(f.return,a,r),_.lanes|=a;break}S=S.next}}else if(f.tag===10)v=f.type===r.type?null:f.child;else if(f.tag===18){if(v=f.return,v===null)throw Error(o(341));v.lanes|=a,_=v.alternate,_!==null&&(_.lanes|=a),Qs(v,a,r),v=f.sibling}else v=f.child;if(v!==null)v.return=f;else for(v=f;v!==null;){if(v===r){v=null;break}if(f=v.sibling,f!==null){f.return=v.return,v=f;break}v=v.return}f=v}pt(t,r,c.children,a),r=r.child}return r;case 9:return c=r.type,l=r.pendingProps.children,jr(r,a),c=At(c),l=l(c),r.flags|=1,pt(t,r,l,a),r.child;case 14:return l=r.type,c=Vt(l,r.pendingProps),c=Vt(l.type,c),Bp(t,r,l,c,a);case 15:return Pp(t,r,r.type,r.pendingProps,a);case 17:return l=r.type,c=r.pendingProps,c=r.elementType===l?c:Vt(l,c),na(t,r),r.tag=1,yt(l)?(t=!0,Li(r)):t=!1,jr(r,a),Ip(r,l,c),fl(r,l,c,a),gl(null,r,l,!0,t,a);case 19:return Mp(t,r,a);case 22:return Np(t,r,a)}throw Error(o(156,r.tag))};function df(t,r){return Zc(t,r)}function S0(t,r,a,l){this.tag=t,this.key=a,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=r,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=l,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function $t(t,r,a,l){return new S0(t,r,a,l)}function $l(t){return t=t.prototype,!(!t||!t.isReactComponent)}function k0(t){if(typeof t=="function")return $l(t)?1:0;if(t!=null){if(t=t.$$typeof,t===Ke)return 11;if(t===kt)return 14}return 2}function Mn(t,r){var a=t.alternate;return a===null?(a=$t(t.tag,r,t.key,t.mode),a.elementType=t.elementType,a.type=t.type,a.stateNode=t.stateNode,a.alternate=t,t.alternate=a):(a.pendingProps=r,a.type=t.type,a.flags=0,a.subtreeFlags=0,a.deletions=null),a.flags=t.flags&14680064,a.childLanes=t.childLanes,a.lanes=t.lanes,a.child=t.child,a.memoizedProps=t.memoizedProps,a.memoizedState=t.memoizedState,a.updateQueue=t.updateQueue,r=t.dependencies,a.dependencies=r===null?null:{lanes:r.lanes,firstContext:r.firstContext},a.sibling=t.sibling,a.index=t.index,a.ref=t.ref,a}function fa(t,r,a,l,c,f){var v=2;if(l=t,typeof t=="function")$l(t)&&(v=1);else if(typeof t=="string")v=5;else e:switch(t){case ce:return cr(a.children,c,f,r);case pe:v=8,c|=8;break;case Te:return t=$t(12,a,r,c|2),t.elementType=Te,t.lanes=f,t;case Oe:return t=$t(13,a,r,c),t.elementType=Oe,t.lanes=f,t;case Xe:return t=$t(19,a,r,c),t.elementType=Xe,t.lanes=f,t;case We:return ma(a,c,f,r);default:if(typeof t=="object"&&t!==null)switch(t.$$typeof){case ye:v=10;break e;case Ae:v=9;break e;case Ke:v=11;break e;case kt:v=14;break e;case ht:v=16,l=null;break e}throw Error(o(130,t==null?t:typeof t,""))}return r=$t(v,a,r,c),r.elementType=t,r.type=l,r.lanes=f,r}function cr(t,r,a,l){return t=$t(7,t,l,r),t.lanes=a,t}function ma(t,r,a,l){return t=$t(22,t,l,r),t.elementType=We,t.lanes=a,t.stateNode={isHidden:!1},t}function Ll(t,r,a){return t=$t(6,t,null,r),t.lanes=a,t}function Dl(t,r,a){return r=$t(4,t.children!==null?t.children:[],t.key,r),r.lanes=a,r.stateNode={containerInfo:t.containerInfo,pendingChildren:null,implementation:t.implementation},r}function b0(t,r,a,l,c){this.tag=r,this.containerInfo=t,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=ps(0),this.expirationTimes=ps(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ps(0),this.identifierPrefix=l,this.onRecoverableError=c,this.mutableSourceEagerHydrationData=null}function Ml(t,r,a,l,c,f,v,_,S){return t=new b0(t,r,a,_,S),r===1?(r=1,f===!0&&(r|=8)):r=0,f=$t(3,null,null,r),t.current=f,f.stateNode=t,f.memoizedState={element:l,isDehydrated:a,cache:null,transitions:null,pendingSuspenseBoundaries:null},Xs(f),t}function z0(t,r,a){var l=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),Wl.exports=M0(),Wl.exports}var kf;function F0(){if(kf)return Ea;kf=1;var e=Bm();return Ea.createRoot=e.createRoot,Ea.hydrateRoot=e.hydrateRoot,Ea}var U0=F0();const Z0=Cm(U0);Bm();function Xo(){return Xo=Object.assign?Object.assign.bind():function(e){for(var n=1;n"u")throw new Error(n)}function gu(e,n){if(!e){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function V0(){return Math.random().toString(36).substr(2,8)}function zf(e,n){return{usr:e.state,key:e.key,idx:n}}function eu(e,n,o,s){return o===void 0&&(o=null),Xo({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof n=="string"?to(n):n,{state:o,key:n&&n.key||s||V0()})}function Ca(e){let{pathname:n="/",search:o="",hash:s=""}=e;return o&&o!=="?"&&(n+=o.charAt(0)==="?"?o:"?"+o),s&&s!=="#"&&(n+=s.charAt(0)==="#"?s:"#"+s),n}function to(e){let n={};if(e){let o=e.indexOf("#");o>=0&&(n.hash=e.substr(o),e=e.substr(0,o));let s=e.indexOf("?");s>=0&&(n.search=e.substr(s),e=e.substr(0,s)),e&&(n.pathname=e)}return n}function W0(e,n,o,s){s===void 0&&(s={});let{window:u=document.defaultView,v5Compat:p=!1}=s,d=u.history,m=Zn.Pop,h=null,y=w();y==null&&(y=0,d.replaceState(Xo({},d.state,{idx:y}),""));function w(){return(d.state||{idx:null}).idx}function I(){m=Zn.Pop;let A=w(),H=A==null?null:A-y;y=A,h&&h({action:m,location:V.location,delta:H})}function z(A,H){m=Zn.Push;let oe=eu(V.location,A,H);y=w()+1;let Q=zf(oe,y),K=V.createHref(oe);try{d.pushState(Q,"",K)}catch(Y){if(Y instanceof DOMException&&Y.name==="DataCloneError")throw Y;u.location.assign(K)}p&&h&&h({action:m,location:V.location,delta:1})}function B(A,H){m=Zn.Replace;let oe=eu(V.location,A,H);y=w();let Q=zf(oe,y),K=V.createHref(oe);d.replaceState(Q,"",K),p&&h&&h({action:m,location:V.location,delta:0})}function D(A){let H=u.location.origin!=="null"?u.location.origin:u.location.href,oe=typeof A=="string"?A:Ca(A);return oe=oe.replace(/ $/,"%20"),qe(H,"No window.location.(origin|href) available to create URL for href: "+oe),new URL(oe,H)}let V={get action(){return m},get location(){return e(u,d)},listen(A){if(h)throw new Error("A history only accepts one active listener");return u.addEventListener(bf,I),h=A,()=>{u.removeEventListener(bf,I),h=null}},createHref(A){return n(u,A)},createURL:D,encodeLocation(A){let H=D(A);return{pathname:H.pathname,search:H.search,hash:H.hash}},push:z,replace:B,go(A){return d.go(A)}};return V}var Tf;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(Tf||(Tf={}));function H0(e,n,o){return o===void 0&&(o="/"),K0(e,n,o)}function K0(e,n,o,s){let u=typeof n=="string"?to(n):n,p=Jr(u.pathname||"/",o);if(p==null)return null;let d=Pm(e);G0(d);let m=null,h=ay(p);for(let y=0;m==null&&y{let h={relativePath:m===void 0?p.path||"":m,caseSensitive:p.caseSensitive===!0,childrenIndex:d,route:p};h.relativePath.startsWith("/")&&(qe(h.relativePath.startsWith(s),'Absolute route path "'+h.relativePath+'" nested under path '+('"'+s+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),h.relativePath=h.relativePath.slice(s.length));let y=Vn([s,h.relativePath]),w=o.concat(h);p.children&&p.children.length>0&&(qe(p.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+y+'".')),Pm(p.children,n,w,y)),!(p.path==null&&!p.index)&&n.push({path:y,score:ny(y,p.index),routesMeta:w})};return e.forEach((p,d)=>{var m;if(p.path===""||!((m=p.path)!=null&&m.includes("?")))u(p,d);else for(let h of Nm(p.path))u(p,d,h)}),n}function Nm(e){let n=e.split("/");if(n.length===0)return[];let[o,...s]=n,u=o.endsWith("?"),p=o.replace(/\?$/,"");if(s.length===0)return u?[p,""]:[p];let d=Nm(s.join("/")),m=[];return m.push(...d.map(h=>h===""?p:[p,h].join("/"))),u&&m.push(...d),m.map(h=>e.startsWith("/")&&h===""?"/":h)}function G0(e){e.sort((n,o)=>n.score!==o.score?o.score-n.score:ry(n.routesMeta.map(s=>s.childrenIndex),o.routesMeta.map(s=>s.childrenIndex)))}const J0=/^:[\w-]+$/,Q0=3,Y0=2,X0=1,ey=10,ty=-2,Cf=e=>e==="*";function ny(e,n){let o=e.split("/"),s=o.length;return o.some(Cf)&&(s+=ty),n&&(s+=Y0),o.filter(u=>!Cf(u)).reduce((u,p)=>u+(J0.test(p)?Q0:p===""?X0:ey),s)}function ry(e,n){return e.length===n.length&&e.slice(0,-1).every((s,u)=>s===n[u])?e[e.length-1]-n[n.length-1]:0}function oy(e,n,o){let{routesMeta:s}=e,u={},p="/",d=[];for(let m=0;m{let{paramName:z,isOptional:B}=w;if(z==="*"){let V=m[I]||"";d=p.slice(0,p.length-V.length).replace(/(.)\/+$/,"$1")}const D=m[I];return B&&!D?y[z]=void 0:y[z]=(D||"").replace(/%2F/g,"/"),y},{}),pathname:p,pathnameBase:d,pattern:e}}function iy(e,n,o){n===void 0&&(n=!1),o===void 0&&(o=!0),gu(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let s=[],u="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(d,m,h)=>(s.push({paramName:m,isOptional:h!=null}),h?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(s.push({paramName:"*"}),u+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):o?u+="\\/*$":e!==""&&e!=="/"&&(u+="(?:(?=\\/|$))"),[new RegExp(u,n?void 0:"i"),s]}function ay(e){try{return e.split("/").map(n=>decodeURIComponent(n).replace(/\//g,"%2F")).join("/")}catch(n){return gu(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+n+").")),e}}function Jr(e,n){if(n==="/")return e;if(!e.toLowerCase().startsWith(n.toLowerCase()))return null;let o=n.endsWith("/")?n.length-1:n.length,s=e.charAt(o);return s&&s!=="/"?null:e.slice(o)||"/"}const sy=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,ly=e=>sy.test(e);function uy(e,n){n===void 0&&(n="/");let{pathname:o,search:s="",hash:u=""}=typeof e=="string"?to(e):e,p;if(o)if(ly(o))p=o;else{if(o.includes("//")){let d=o;o=Am(o),gu(!1,"Pathnames cannot have embedded double slashes - normalizing "+(d+" -> "+o))}o.startsWith("/")?p=Rf(o.substring(1),"/"):p=Rf(o,n)}else p=n;return{pathname:p,search:py(s),hash:fy(u)}}function Rf(e,n){let o=n.replace(/\/+$/,"").split("/");return e.split("/").forEach(u=>{u===".."?o.length>1&&o.pop():u!=="."&&o.push(u)}),o.length>1?o.join("/"):"/"}function Gl(e,n,o,s){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+n+"` field ["+JSON.stringify(s)+"]. Please separate it out to the ")+("`to."+o+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function cy(e){return e.filter((n,o)=>o===0||n.route.path&&n.route.path.length>0)}function yu(e,n){let o=cy(e);return n?o.map((s,u)=>u===o.length-1?s.pathname:s.pathnameBase):o.map(s=>s.pathnameBase)}function _u(e,n,o,s){s===void 0&&(s=!1);let u;typeof e=="string"?u=to(e):(u=Xo({},e),qe(!u.pathname||!u.pathname.includes("?"),Gl("?","pathname","search",u)),qe(!u.pathname||!u.pathname.includes("#"),Gl("#","pathname","hash",u)),qe(!u.search||!u.search.includes("#"),Gl("#","search","hash",u)));let p=e===""||u.pathname==="",d=p?"/":u.pathname,m;if(d==null)m=o;else{let I=n.length-1;if(!s&&d.startsWith("..")){let z=d.split("/");for(;z[0]==="..";)z.shift(),I-=1;u.pathname=z.join("/")}m=I>=0?n[I]:"/"}let h=uy(u,m),y=d&&d!=="/"&&d.endsWith("/"),w=(p||d===".")&&o.endsWith("/");return!h.pathname.endsWith("/")&&(y||w)&&(h.pathname+="/"),h}const Am=e=>e.replace(/\/\/+/g,"/"),Vn=e=>Am(e.join("/")),dy=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),py=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,fy=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function my(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const Om=["post","put","patch","delete"];new Set(Om);const vy=["get",...Om];new Set(vy);function ei(){return ei=Object.assign?Object.assign.bind():function(e){for(var n=1;n{m.current=!0}),b.useCallback(function(y,w){if(w===void 0&&(w={}),!m.current)return;if(typeof y=="number"){s.go(y);return}let I=_u(y,JSON.parse(d),p,w.relative==="path");e==null&&n!=="/"&&(I.pathname=I.pathname==="/"?n:Vn([n,I.pathname])),(w.replace?s.replace:s.push)(I,w.state,w)},[n,s,d,p,e])}function mb(){let{matches:e}=b.useContext(_n),n=e[e.length-1];return n?n.params:{}}function Ma(e,n){let{relative:o}=n===void 0?{}:n,{future:s}=b.useContext(yn),{matches:u}=b.useContext(_n),{pathname:p}=wn(),d=JSON.stringify(yu(u,s.v7_relativeSplatPath));return b.useMemo(()=>_u(e,JSON.parse(d),p,o==="path"),[e,d,p,o])}function yy(e,n){return _y(e,n)}function _y(e,n,o,s){no()||qe(!1);let{navigator:u}=b.useContext(yn),{matches:p}=b.useContext(_n),d=p[p.length-1],m=d?d.params:{};d&&d.pathname;let h=d?d.pathnameBase:"/";d&&d.route;let y=wn(),w;if(n){var I;let A=typeof n=="string"?to(n):n;h==="/"||(I=A.pathname)!=null&&I.startsWith(h)||qe(!1),w=A}else w=y;let z=w.pathname||"/",B=z;if(h!=="/"){let A=h.replace(/^\//,"").split("/");B="/"+z.replace(/^\//,"").split("/").slice(A.length).join("/")}let D=H0(e,{pathname:B}),V=Sy(D&&D.map(A=>Object.assign({},A,{params:Object.assign({},m,A.params),pathname:Vn([h,u.encodeLocation?u.encodeLocation(A.pathname).pathname:A.pathname]),pathnameBase:A.pathnameBase==="/"?h:Vn([h,u.encodeLocation?u.encodeLocation(A.pathnameBase).pathname:A.pathnameBase])})),p,o,s);return n&&V?b.createElement(Da.Provider,{value:{location:ei({pathname:"/",search:"",hash:"",state:null,key:"default"},w),navigationType:Zn.Pop}},V):V}function wy(){let e=Ty(),n=my(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),o=e instanceof Error?e.stack:null,u={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return b.createElement(b.Fragment,null,b.createElement("h2",null,"Unexpected Application Error!"),b.createElement("h3",{style:{fontStyle:"italic"}},n),o?b.createElement("pre",{style:u},o):null,null)}const Ey=b.createElement(wy,null);class xy extends b.Component{constructor(n){super(n),this.state={location:n.location,revalidation:n.revalidation,error:n.error}}static getDerivedStateFromError(n){return{error:n}}static getDerivedStateFromProps(n,o){return o.location!==n.location||o.revalidation!=="idle"&&n.revalidation==="idle"?{error:n.error,location:n.location,revalidation:n.revalidation}:{error:n.error!==void 0?n.error:o.error,location:o.location,revalidation:n.revalidation||o.revalidation}}componentDidCatch(n,o){console.error("React Router caught the following error during render",n,o)}render(){return this.state.error!==void 0?b.createElement(_n.Provider,{value:this.props.routeContext},b.createElement($m.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function Iy(e){let{routeContext:n,match:o,children:s}=e,u=b.useContext(La);return u&&u.static&&u.staticContext&&(o.route.errorElement||o.route.ErrorBoundary)&&(u.staticContext._deepestRenderedBoundaryId=o.route.id),b.createElement(_n.Provider,{value:n},s)}function Sy(e,n,o,s){var u;if(n===void 0&&(n=[]),o===void 0&&(o=null),s===void 0&&(s=null),e==null){var p;if(!o)return null;if(o.errors)e=o.matches;else if((p=s)!=null&&p.v7_partialHydration&&n.length===0&&!o.initialized&&o.matches.length>0)e=o.matches;else return null}let d=e,m=(u=o)==null?void 0:u.errors;if(m!=null){let w=d.findIndex(I=>I.route.id&&m?.[I.route.id]!==void 0);w>=0||qe(!1),d=d.slice(0,Math.min(d.length,w+1))}let h=!1,y=-1;if(o&&s&&s.v7_partialHydration)for(let w=0;w=0?d=d.slice(0,y+1):d=[d[0]];break}}}return d.reduceRight((w,I,z)=>{let B,D=!1,V=null,A=null;o&&(B=m&&I.route.id?m[I.route.id]:void 0,V=I.route.errorElement||Ey,h&&(y<0&&z===0?(Ry("route-fallback"),D=!0,A=null):y===z&&(D=!0,A=I.route.hydrateFallbackElement||null)));let H=n.concat(d.slice(0,z+1)),oe=()=>{let Q;return B?Q=V:D?Q=A:I.route.Component?Q=b.createElement(I.route.Component,null):I.route.element?Q=I.route.element:Q=w,b.createElement(Iy,{match:I,routeContext:{outlet:w,matches:H,isDataRoute:o!=null},children:Q})};return o&&(I.route.ErrorBoundary||I.route.errorElement||z===0)?b.createElement(xy,{location:o.location,revalidation:o.revalidation,component:V,error:B,children:oe(),routeContext:{outlet:null,matches:H,isDataRoute:!0}}):oe()},null)}var Dm=(function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e})(Dm||{}),Mm=(function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e})(Mm||{});function ky(e){let n=b.useContext(La);return n||qe(!1),n}function by(e){let n=b.useContext(jm);return n||qe(!1),n}function zy(e){let n=b.useContext(_n);return n||qe(!1),n}function Fm(e){let n=zy(),o=n.matches[n.matches.length-1];return o.route.id||qe(!1),o.route.id}function Ty(){var e;let n=b.useContext($m),o=by(),s=Fm();return n!==void 0?n:(e=o.errors)==null?void 0:e[s]}function Cy(){let{router:e}=ky(Dm.UseNavigateStable),n=Fm(Mm.UseNavigateStable),o=b.useRef(!1);return Lm(()=>{o.current=!0}),b.useCallback(function(u,p){p===void 0&&(p={}),o.current&&(typeof u=="number"?e.navigate(u):e.navigate(u,ei({fromRouteId:n},p)))},[e,n])}const Bf={};function Ry(e,n,o){Bf[e]||(Bf[e]=!0)}function By(e,n){e?.v7_startTransition,e?.v7_relativeSplatPath}function Py(e){let{to:n,replace:o,state:s,relative:u}=e;no()||qe(!1);let{future:p,static:d}=b.useContext(yn),{matches:m}=b.useContext(_n),{pathname:h}=wn(),y=wu(),w=_u(n,yu(m,p.v7_relativeSplatPath),h,u==="path"),I=JSON.stringify(w);return b.useEffect(()=>y(JSON.parse(I),{replace:o,state:s,relative:u}),[y,I,u,o,s]),null}function on(e){qe(!1)}function Ny(e){let{basename:n="/",children:o=null,location:s,navigationType:u=Zn.Pop,navigator:p,static:d=!1,future:m}=e;no()&&qe(!1);let h=n.replace(/^\/*/,"/"),y=b.useMemo(()=>({basename:h,navigator:p,static:d,future:ei({v7_relativeSplatPath:!1},m)}),[h,m,p,d]);typeof s=="string"&&(s=to(s));let{pathname:w="/",search:I="",hash:z="",state:B=null,key:D="default"}=s,V=b.useMemo(()=>{let A=Jr(w,h);return A==null?null:{location:{pathname:A,search:I,hash:z,state:B,key:D},navigationType:u}},[h,w,I,z,B,D,u]);return V==null?null:b.createElement(yn.Provider,{value:y},b.createElement(Da.Provider,{children:o,value:V}))}function Ay(e){let{children:n,location:o}=e;return yy(nu(n),o)}new Promise(()=>{});function nu(e,n){n===void 0&&(n=[]);let o=[];return b.Children.forEach(e,(s,u)=>{if(!b.isValidElement(s))return;let p=[...n,u];if(s.type===b.Fragment){o.push.apply(o,nu(s.props.children,p));return}s.type!==on&&qe(!1),!s.props.index||!s.props.children||qe(!1);let d={id:s.props.id||p.join("-"),caseSensitive:s.props.caseSensitive,element:s.props.element,Component:s.props.Component,index:s.props.index,path:s.props.path,loader:s.props.loader,action:s.props.action,errorElement:s.props.errorElement,ErrorBoundary:s.props.ErrorBoundary,hasErrorBoundary:s.props.ErrorBoundary!=null||s.props.errorElement!=null,shouldRevalidate:s.props.shouldRevalidate,handle:s.props.handle,lazy:s.props.lazy};s.props.children&&(d.children=nu(s.props.children,p)),o.push(d)}),o}function Ra(){return Ra=Object.assign?Object.assign.bind():function(e){for(var n=1;n{let s=e[o];return n.concat(Array.isArray(s)?s.map(u=>[o,u]):[[o,s]])},[]))}function $y(e,n){let o=ru(e);return n&&n.forEach((s,u)=>{o.has(u)||n.getAll(u).forEach(p=>{o.append(u,p)})}),o}const Ly=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],Dy=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],My="6";try{window.__reactRouterVersion=My}catch{}const Fy=b.createContext({isTransitioning:!1}),Uy="startTransition",Pf=$0[Uy];function Zy(e){let{basename:n,children:o,future:s,window:u}=e,p=b.useRef();p.current==null&&(p.current=q0({window:u,v5Compat:!0}));let d=p.current,[m,h]=b.useState({action:d.action,location:d.location}),{v7_startTransition:y}=s||{},w=b.useCallback(I=>{y&&Pf?Pf(()=>h(I)):h(I)},[h,y]);return b.useLayoutEffect(()=>d.listen(w),[d,w]),b.useEffect(()=>By(s),[s]),b.createElement(Ny,{basename:n,children:o,location:m.location,navigationType:m.action,navigator:d,future:s})}const qy=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Vy=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Wy=b.forwardRef(function(n,o){let{onClick:s,relative:u,reloadDocument:p,replace:d,state:m,target:h,to:y,preventScrollReset:w,viewTransition:I}=n,z=Um(n,Ly),{basename:B}=b.useContext(yn),D,V=!1;if(typeof y=="string"&&Vy.test(y)&&(D=y,qy))try{let Q=new URL(window.location.href),K=y.startsWith("//")?new URL(Q.protocol+y):new URL(y),Y=Jr(K.pathname,B);K.origin===Q.origin&&Y!=null?y=Y+K.search+K.hash:V=!0}catch{}let A=hy(y,{relative:u}),H=Gy(y,{replace:d,state:m,target:h,preventScrollReset:w,relative:u,viewTransition:I});function oe(Q){s&&s(Q),Q.defaultPrevented||H(Q)}return b.createElement("a",Ra({},z,{href:D||A,onClick:V||p?s:oe,ref:o,target:h}))}),Hy=b.forwardRef(function(n,o){let{"aria-current":s="page",caseSensitive:u=!1,className:p="",end:d=!1,style:m,to:h,viewTransition:y,children:w}=n,I=Um(n,Dy),z=Ma(h,{relative:I.relative}),B=wn(),D=b.useContext(jm),{navigator:V,basename:A}=b.useContext(yn),H=D!=null&&Jy(z)&&y===!0,oe=V.encodeLocation?V.encodeLocation(z).pathname:z.pathname,Q=B.pathname,K=D&&D.navigation&&D.navigation.location?D.navigation.location.pathname:null;u||(Q=Q.toLowerCase(),K=K?K.toLowerCase():null,oe=oe.toLowerCase()),K&&A&&(K=Jr(K,A)||K);const Y=oe!=="/"&&oe.endsWith("/")?oe.length-1:oe.length;let ue=Q===oe||!d&&Q.startsWith(oe)&&Q.charAt(Y)==="/",ce=K!=null&&(K===oe||!d&&K.startsWith(oe)&&K.charAt(oe.length)==="/"),pe={isActive:ue,isPending:ce,isTransitioning:H},Te=ue?s:void 0,ye;typeof p=="function"?ye=p(pe):ye=[p,ue?"active":null,ce?"pending":null,H?"transitioning":null].filter(Boolean).join(" ");let Ae=typeof m=="function"?m(pe):m;return b.createElement(Wy,Ra({},I,{"aria-current":Te,className:ye,ref:o,style:Ae,to:h,viewTransition:y}),typeof w=="function"?w(pe):w)});var ou;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(ou||(ou={}));var Nf;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Nf||(Nf={}));function Ky(e){let n=b.useContext(La);return n||qe(!1),n}function Gy(e,n){let{target:o,replace:s,state:u,preventScrollReset:p,relative:d,viewTransition:m}=n===void 0?{}:n,h=wu(),y=wn(),w=Ma(e,{relative:d});return b.useCallback(I=>{if(jy(I,o)){I.preventDefault();let z=s!==void 0?s:Ca(y)===Ca(w);h(e,{replace:z,state:u,preventScrollReset:p,relative:d,viewTransition:m})}},[y,h,w,s,u,o,e,p,d,m])}function vb(e){let n=b.useRef(ru(e)),o=b.useRef(!1),s=wn(),u=b.useMemo(()=>$y(s.search,o.current?null:n.current),[s.search]),p=wu(),d=b.useCallback((m,h)=>{const y=ru(typeof m=="function"?m(u):m);o.current=!0,p("?"+y,h)},[p,u]);return[u,d]}function Jy(e,n){n===void 0&&(n={});let o=b.useContext(Fy);o==null&&qe(!1);let{basename:s}=Ky(ou.useViewTransitionState),u=Ma(e,{relative:n.relative});if(!o.isTransitioning)return!1;let p=Jr(o.currentLocation.pathname,s)||o.currentLocation.pathname,d=Jr(o.nextLocation.pathname,s)||o.nextLocation.pathname;return tu(u.pathname,d)!=null||tu(u.pathname,p)!=null}function Qy(e,n){if(e.length===0||n.length===0)return null;const o=n.filter(s=>s.state==="active");return Af(e,o)??Af(e,n)}function Af(e,n){for(const o of n)if(Yy(o,e))return o;return null}function Yy(e,n){return e.alias===n||e.pool===n||e.alias!==void 0&&Of(e.alias,["/","."])===n||e.session_name!==void 0&&Of(e.session_name,["__","--"])===n}function Of(e,n){let o=-1,s=0;for(const u of n){const p=e.lastIndexOf(u);p>o&&(o=p,s=u.length)}return o<0?e:e.slice(o+s)}const Ba=/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$/;function Eu(e){return e==="city"||e==="rig"?e:null}function Xy(e){const n=Eu(e.scope_kind),o=Qr(e.scope_ref);return n!==null&&o!==null?{scopeKind:n,scopeRef:o}:null}function e7(e){const n=Xy(e);return n===null||!Ba.test(n.scopeRef)?null:{...n,rootStoreRef:Qr(e.root_store_ref)??`${n.scopeKind}:${n.scopeRef}`}}function Zm(e){const n=Qr(e?.["gc.root_store_ref"]),o=Eu(e?.["gc.scope_kind"]),s=Qr(e?.["gc.scope_ref"]);if(o!==null&&s!==null&&Ba.test(s))return{scopeKind:o,scopeRef:s,rootStoreRef:n??`${o}:${s}`};if(n===null)return null;const u=xu(n);return u===null||!Ba.test(u.scopeRef)?null:{...u,rootStoreRef:n}}function xu(e){const n=Qr(e);if(n===null)return null;const o=n.indexOf(":");if(o<=0||o>=n.length-1)return null;const s=Eu(n.slice(0,o)),u=Qr(n.slice(o+1));return s!==null&&u!==null?{scopeKind:s,scopeRef:u}:null}function Qr(e){return typeof e=="string"&&e.trim().length>0?e.trim():null}const t7=/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g,n7=/\x1b\[[?0-9;]*[a-zA-Z]/g,r7=/[\x00-\x1f\x7f-\x9f]/g,o7=/[؜‎‏‪-‮⁦-⁩]/g;function jf(e){return e.replace(t7,"").replace(n7,"").replace(r7,"").replace(o7,"")}const i7=new Set(["failed","errored","stuck","crashed"]),a7=new Set(["rate-limited","rate_limited","waiting"]),s7={"awaiting-input":"respond",errored:"reset","rate-limited":"nudge",stalled:"nudge"};function l7(e,n){const o=new Map;for(const u of n)o.set(u.agentName,u.prompt);const s=[];for(const u of e){const p=o.has(u.name),d=u7(u,p);d!==null&&s.push({name:u.name,reason:d,detail:d7(u,d,o.get(u.name)),action:s7[d]})}return s}function u7(e,n){if(n)return"awaiting-input";const o=e.state.toLowerCase();return i7.has(o)?"errored":a7.has(o)?"rate-limited":c7(e,o)?"stalled":null}function c7(e,n){return n==="detached"?!0:e.running&&e.session===void 0}function d7(e,n,o){switch(n){case"awaiting-input":return p7(o);case"errored":return`Exited ${e.state}.`;case"rate-limited":return"Throttled by a provider limit.";case"stalled":return e.state.toLowerCase()==="detached"?"Detached from its session.":"Running with no live session."}}function p7(e){if(e===void 0)return"Awaiting your decision.";const n=e.split(` -`,1)[0]?.trim()??"";return n.length>0?n:"Awaiting your decision."}function qm(e,n){const o=e?.metadata?.[n];if(typeof o=="string")return ro(o)}function ro(e){return typeof e=="string"&&e.trim().length>0?e.trim():void 0}function f7(e){return qm(e,"gc.step_ref")??ro(e.step_ref)??null}function hb(e){return Iu(e,"gc.iteration")??iu(e,"iteration")??iu(e,"run")}function gb(e){return Iu(e,"gc.attempt")??Su(e.attempt)??iu(e,"attempt")}function yb(e,n){return Iu(e,n)}function _b(e){return e.replace(/(^|[^A-Za-z0-9])ralph(?=$|[^A-Za-z0-9])/gi,"$1check-loop")}function iu(e,n){const o=f7(e);if(!o)return;const s=o.split(".");for(let u=0;u0)return e;if(typeof e!="string"||!/^[1-9]\d*$/.test(e))return;const n=Number.parseInt(e,10);return Number.isSafeInteger(n)?n:void 0}function m7(e){return e.filter(n=>n.phase==="blocked").map(n=>({id:n.id,title:n.title,reason:v7(n),remedy:h7(n),scope:n.scope}))}function v7(e){const n=g7(e);if(n!==null)return`Blocked at ${n}`;const o=e.statusCounts.blocked??0;return o>0?`${o} blocked step${o===1?"":"s"}`:"Blocked, awaiting operator"}function h7(e){return e.activeAssignees.length===0?"No worker assigned. Claim or dispatch one.":"Open run detail to review the blocked step."}function g7(e){if(e.progress.status==="active_step"||e.progress.status==="stage_only"){const n=e.progress.stage;if(n.status==="available")return n.label}return null}function y7(e){if(e.some(o=>o.status==="blocked"||Wm(o).includes("blocked")))return{phase:"blocked",label:"blocked",reviewRound:null};if(e.length>0&&e.every(o=>o.status==="closed"))return{phase:"complete",label:"complete",reviewRound:null};const n=_7(e);return n!==null?n:T7(e)}function _7(e){const n=e.filter(Tu),s=zu(n.filter(p=>p.status==="in_progress"))??D7(n);if(s===null)return null;const u=ti(s);if(u==="review"){const p=ku(e)??Vm(e);return{phase:"review",label:`review round ${p}`,reviewRound:p}}return{phase:u,label:u,reviewRound:null}}function ti(e){const n=E7(e);return Ko(n,I7,{rejectWithLeadUpQualifier:!0})?"approval":Ko(n,S7,{rejectWithLeadUpQualifier:!0})?"finalization":Ko(n,k7)?"review":Ko(n,b7)?"implementation":Ko(n,z7)?"intake":"active"}const w7=/[-._:/]+/;function E7(e){return e.toLowerCase().split(w7).filter(Boolean)}const x7=new Set(["pre","prepare","wait","await","pending","before","for","to"]);function Ko(e,n,o={}){return e.some(s=>n.has(s))?o.rejectWithLeadUpQualifier?!e.some(s=>x7.has(s)):!0:!1}const I7=new Set(["approval","approve","approved","gate"]),S7=new Set(["finalize","finalization","merge","cleanup","publish"]),k7=new Set(["review","reviewer","scorecard","persona","personas","audit","repro","baseline","investigation","classify","classification"]),b7=new Set(["implement","implementation","patch","fixes","work","design"]),z7=new Set(["intake","bootstrap","context","router","request","preflight","setup","rebase"]);function T7(e){if(Go(e,["approval","approved","finalize-scope"]))return{phase:"approval",label:"approval",reviewRound:null};if(Go(e,["post-merge","finalization","finalize"]))return{phase:"finalization",label:"finalization",reviewRound:null};const n=ku(e);if(n!==null||Go(e,["review","reviewer","scorecard"])){const o=n??Vm(e);return{phase:"review",label:`review round ${o}`,reviewRound:o}}return Go(e,["implementation","patch","do-work"])?{phase:"implementation",label:"implementation",reviewRound:null}:Go(e,["intake","load-context","router","request"])?{phase:"intake",label:"intake",reviewRound:null}:{phase:"active",label:"active",reviewRound:null}}function C7(e){const n=Le(e.metadata?.["gc.step_id"]);return[e.title,n].filter(Boolean).join(" ").toLowerCase()}function Go(e,n){return e.some(o=>{const s=C7(o);return n.some(u=>s.includes(u))})}function R7(e){const n=e.metadata??{};for(const[o,s]of Object.entries(n)){const u=o.match(B7);if(u&&u[1]!==void 0)return Number(u[1]);if(N7.test(o)){const d=A7(s);if(d!==null)return d}const p=String(s).match(P7);if(p&&p[1]!==void 0)return Number(p[1])}return null}const B7=/(?:^|\.)(?:iteration|attempt)\.(\d+)$/,P7=/(?:^|\.)(?:iteration|attempt)\.(\d+)$/,N7=/(?:^|\.)(?:iteration|attempt)$/;function ku(e){const n=e.map(R7).filter(o=>o!==null);return n.length===0?null:Math.max(...n)}function Vm(e){const n=e.filter(o=>Wm(o).includes("review")).length;return Math.max(n,1)}function Wm(e){const n=Object.entries(e.metadata??{}).filter(([o])=>!o.startsWith("gc.var.")).map(([o,s])=>`${o} ${String(s)}`).join(" ");return[e.title,e.description,e.status,e.issue_type,e.assignee,e.parent,n].filter(Boolean).join(" ").toLowerCase()}function Le(e){return typeof e=="string"?e.trim():""}function A7(e){const n=Number(e);return Number.isInteger(n)&&n>0?n:null}function bu(e){const n=e.parent??Le(e.metadata?.["gc.parent_bead_id"]),o={id:e.id,title:e.title,status:e.status,issue_type:e.issue_type,updated_at:e.updated_at??e.created_at};return e.description!==void 0&&(o.description=e.description),e.assignee!==void 0&&(o.assignee=e.assignee),n&&(o.parent=n),e.metadata!==void 0&&(o.metadata=e.metadata),o}const xa=[["intake","Intake"],["implementation","Implementation"],["review","Review"],["approval","Approval"],["finalization","Finalization"]];function O7(e,n,o){const s=Hm(n);if(s.length>0)return j7(s,o);if(e.phase==="blocked")return[{key:"blocked",label:"Blocked",status:"blocked"}];if(e.phase==="complete")return xa.map(([p,d])=>({key:p,label:d,status:"complete"}));const u=xa.findIndex(([p])=>p===e.phase);return u<0?xa.map(([p,d])=>({key:p,label:d,status:p==="implementation"?"active":"pending"})):xa.map(([p,d],m)=>({key:p,label:p==="review"&&e.reviewRound!==null?`Review round ${e.reviewRound}`:d,status:mm.status==="in_progress")),u=s?e.findIndex(m=>m.steps.includes(s)):$7(e,o),p=L7(e,o),d=m=>m.steps.some(h=>Fa(o,h).some(y=>y.status==="closed"));return e.map((m,h)=>{let y;return u>=0?y=ho.steps.some(s=>Fa(n,s).some(u=>u.status!=="closed")))}function L7(e,n){let o=-1;return e.forEach((s,u)=>{s.steps.some(p=>Fa(n,p).some(d=>d.status==="closed"))&&(o=u)}),o}function zu(e){return[...e].sort(F7).map(n=>Le(n.metadata?.["gc.step_id"])).find(Boolean)??null}function D7(e){const n=e.map(o=>Le(o.metadata?.["gc.step_id"])).filter(o=>o.length>0);return n.length===0?null:[...n].sort((o,s)=>{const u=Pa(ti(s))-Pa(ti(o));return u!==0?u:os?1:0})[0]}const M7={active:0,intake:1,implementation:2,review:3,approval:4,finalization:5,blocked:6,complete:7};function Pa(e){return M7[e]}function F7(e,n){const o=$f(n.updated_at)-$f(e.updated_at);if(o!==0)return o;const s=Le(e.metadata?.["gc.step_id"]),u=Le(n.metadata?.["gc.step_id"]),p=Pa(ti(u))-Pa(ti(s));return p!==0?p:su?1:0}function $f(e){const n=Date.parse(e);return Number.isNaN(n)?0:n}function Fa(e,n){return e.filter(o=>Le(o.metadata?.["gc.step_id"])===n)}function Tu(e){const n=Le(e.metadata?.["gc.kind"]);return n!=="spec"&&n!=="scope-check"&&n!=="workflow-finalize"}function U7(e,{root:n,formulaDetail:o,issues:s=[]}){const u=W7(n),p=Z7(e,n,s);if(p!==null)return{name:p,source:"metadata",target:u};if(e==="detail"||e==="state"){const m=ro(o?.name);if(m!==void 0)return{name:m,source:"formula_detail",target:u}}const d=q7(e,n);return d!==null?{name:d,source:"title_fallback",target:u}:{name:null,source:null,target:u}}function Z7(e,n,o){return e==="lane"?Lf(o,"pr_review.workflow_formula")??Lf(o,"gc.formula")??null:dr(n,"gc.formula")??dr(n,"gc.formula_name")??null}function q7(e,n){if(n===void 0||dr(n,"gc.formula_contract")!=="graph.v2"||dr(n,"gc.run_target")===void 0||V7(n.status))return null;const o=ro(n.title);return o===void 0||e==="lane"&&!o.startsWith("mol-")?null:o}function V7(e){switch(e.trim().toLowerCase()){case"closed":case"completed":case"done":case"failed":case"skipped":return!0;default:return!1}}function W7(e){return dr(e,"gc.run_target")??dr(e,"gc.routed_to")??ro(e?.assignee)??null}function dr(e,n){return ro(e?.metadata?.[n])}function Lf(e,n){for(const o of e){const s=dr(o,n);if(s!==void 0)return s}}const H7=1,K7=2,Km={attemptClimbMin:H7,thrashDetectedStreak:K7};function G7(e){return e.phase==="approval"||e.phase==="blocked"}function J7(e,n,o={}){const{attemptClimbMin:s}={...Km,...o},u=new Map;for(const p of n){const d=X7(p),m=e.get(p.id),h=m!==void 0&&m.progress.status==="comparable"&&d.status==="comparable"&&m.progress.stepId===d.stepId&&m.progress.stageIndex===d.stageIndex,y=m!==void 0&&m.progress.status==="comparable"&&d.status==="comparable"&&d.attempt-m.progress.attempt>=s,w=h&&y?m.thrashStreak+1:0;u.set(p.id,{progress:d,thrashStreak:w})}return u}function Q7(e){const{thrashDetectedStreak:n}={...Km,...e.thresholds},o=e.lanes.map(s=>{if(!e.sessionsAvailable)return{...s,health:{status:"unavailable",error:"run session list unavailable"}};const u=Y7(s,e.sessions),p=u.status==="resolved",d=s.formulaStageResolved===!0&&p?"known":"inferred",m=e.marks.get(s.id)?.thrashStreak??0,h={phaseConfidence:d,needsOperator:G7(s),stuckNode:e2(s),thrashingDetected:m>=n,session:u.status==="resolved"?t2(u.session):{status:"unresolved",error:u.error}};return{...s,health:{status:"available",data:h}}});return{lanes:o,census:Gm(o)}}function Y7(e,n){for(const o of e.activeAssignees){const s=Qy(o,n);if(s!==null)return{status:"resolved",session:s}}return{status:"unresolved",error:"run session unresolved"}}function X7(e){return e.progress.status!=="active_step"?{status:"not_comparable",error:"run has no active step"}:e.progress.stage.status!=="available"?{status:"not_comparable",error:e.progress.stage.error}:e.progress.attempt.status!=="available"?{status:"not_comparable",error:e.progress.attempt.error}:{status:"comparable",stepId:e.progress.stepId,stageIndex:e.progress.stage.index,attempt:e.progress.attempt.value}}function e2(e){return e.progress.status==="active_step"?{status:"available",id:e.progress.stepId}:{status:"unavailable",error:"active run step unavailable"}}function t2(e){return{status:"resolved",lastActive:e.last_active===void 0?{status:"unavailable",error:"session last_active unavailable"}:{status:"available",at:e.last_active},running:{status:"available",value:e.running},activity:e.activity===void 0?{status:"unavailable",error:"session activity unavailable"}:{status:"available",value:e.activity}}}function n2(){return{intake:0,implementation:0,review:0,approval:0,finalization:0,blocked:0,complete:0,active:0}}function Gm(e){const n=n2();let o=0,s=0,u=0,p=0;for(const d of e)n[d.phase]+=1,d.phase!=="complete"&&(o+=1,d.health.status==="available"&&d.health.data.phaseConfidence==="known"?(u+=1,d.health.data.thrashingDetected===!0&&(p+=1)):s+=1);return{byPhase:n,totalInFlight:o,unverifiable:s,knownDenominator:u,thrashing:p}}const r2=1440*60*1e3;function o2(e,n,o){if(!o||e.phase==="complete"||e.phase==="blocked"||e.progress.status==="active_step"||i2(e)||e.updatedAt.status!=="available")return!1;const s=n-Date.parse(e.updatedAt.at);return Number.isFinite(s)&&s>=r2}function i2(e){return e.health.status==="available"&&e.health.data.session.status==="resolved"}function a2(e,n){return!n.some(o=>o.id===e)}const wb=8,s2=12,l2=50,u2=new Set(["feature","bug","task","epic","chore","decision","molecule"]);function Cu(e,n=new Map,o=!1){const s=new Map;for(const B of e){const D=f2(B),V=s.get(D)??[];V.push(B),s.set(D,V)}const u=Array.from(s.entries()).filter(([B,D])=>!a2(B,D)&&c2(B,D)),p=u.flatMap(([,B])=>B),d=u.map(([B,D])=>p2(B,D,n)).sort(x2),m=d.filter(B=>B.phase!=="complete"&&B.phase!=="blocked"),h=d.filter(B=>B.phase==="complete"),y=h.length,w=h.slice(0,l2),I=d.filter(B=>B.phase==="blocked"),z={totalActive:m.length,totalHistorical:y,runCounts:Jm(m,m.length,I.length),lanes:m,historicalLanes:w,blockedLanes:I,recentChanges:E2(p),census:b2()};return o?{...z,lanesPartial:!0}:z}function c2(e,n){const o=n.find(u=>u.id===e);if(!o)return!1;const s=o.metadata;return Le(s?.["gc.formula_contract"])==="graph.v2"||o.issue_type==="molecule"||Le(s?.["gc.kind"])==="run"||Le(s?.["gc.formula"])!==""}function Jm(e,n,o){const s={total:e.length,visible:n,prReview:0,designReview:0,bugfix:0,blocked:o,other:0};for(const u of e)switch(d2(u.formula)){case"prReview":s.prReview+=1;break;case"designReview":s.designReview+=1;break;case"bugfix":s.bugfix+=1;break;case"other":s.other+=1;break}return s}function d2(e){const n=Qm(e);return n==="mol-adopt-pr-v2"?"prReview":n==="mol-design-review-v2"?"designReview":n==="mol-bug-report-flow-v2"||n==="mol-bug-report-implementation-v2"?"bugfix":"other"}function p2(e,n,o){const s=y7(n),u=w2(n),p=h2(e,n),d=Qm(p),m=O7(s,d,n),h=m.findIndex(A=>A.status==="active"),y=h>=0?m[h]:void 0,w=n.filter(A=>Tu(A)&&A.status==="in_progress"),I=zu(w),z=T2(m,h,I,n),B=Hm(d),D=B.length>0&&z.status==="active_step"&&B.some(A=>A.steps.includes(z.stepId)),V=m2(e,n,o);return{id:e,title:g2(e,n),formula:p,scope:V,external:I2(n),phase:s.phase,phaseLabel:p.status==="known"?y?.label??s.label:s.label,statusCounts:y2(n),activeAssignees:_2(n),updatedAt:u,stages:m,progress:z,formulaStageResolved:D,health:z2()}}function f2(e){const n=v2(e);if(n)return n;const o=e.metadata??{},s=Le(o["gc.root_bead_id"]);if(s)return s;if(Le(o["gc.kind"])==="run"||e.issue_type==="molecule")return e.id;const u=Le(o.molecule_id);return u||e.id}function m2(e,n,o){const s=n.find(h=>h.id===e),u=s?[s,...n.filter(h=>h!==s)]:n,p=Dt(u,"gc.root_store_ref"),d=Zm({...s?.metadata??{},...p?{"gc.root_store_ref":p}:{},"gc.scope_ref":Le(s?.metadata?.["gc.scope_ref"])||Dt(u,"gc.scope_ref")});if(d!==null)return Df(d.scopeKind,d.scopeRef,d.rootStoreRef);const m=o.get(e);return m!==void 0?Df(m.scopeKind,m.scopeRef,p||m.rootStoreRef):{status:"unavailable",error:"run scope metadata unavailable"}}function Df(e,n,o){return{status:"available",kind:e,ref:jf(n),rootStoreRef:jf(o)}}function v2(e){return Le(e.metadata?.["pr_review.run_root_id"])||Le(e.metadata?.["pr_review.workflow_root_id"])||Le(e.metadata?.["bugflow.active_run_id"])||Le(e.metadata?.["bugflow.implementation_run_id"])||Le(e.metadata?.["bugflow.implementation_workflow_id"])||Le(e.metadata?.["design_review.run_root_id"])||Le(e.metadata?.["design_review.workflow_root_id"])}function h2(e,n){const o=n.find(u=>u.id===e),s=U7("lane",{root:o,issues:n});return s.name!==null?{status:"known",name:s.name}:{status:"unavailable",error:"run formula unavailable"}}function Qm(e){return e.status==="known"?e.name:null}function g2(e,n){const o=Dt(n,"pr_review.github_title"),s=Dt(n,"pr_review.pr_number");if(o&&s)return`PR #${s}: ${o}`;const u=Dt(n,"bugflow.github_issue_url"),p=Dt(n,"bugflow.github_issue_number");return u&&p?`Issue #${p}: ${n[0]?.title??e}`:n.find(m=>m.id===e)?.title??n[0]?.title??e}function y2(e){return e.reduce((n,o)=>(n[o.status]=(n[o.status]??0)+1,n),{})}function _2(e){return Array.from(new Set(e.filter(n=>n.status!=="closed").map(n=>n.assignee?.trim()).filter(n=>!!n))).sort()}function w2(e){const n=e.map(o=>o.updated_at).filter(Boolean).sort((o,s)=>Date.parse(s)-Date.parse(o))[0];return n===void 0?{status:"unavailable",error:"run update time unavailable"}:{status:"available",at:n}}function E2(e){return[...e].filter(n=>n.updated_at).sort((n,o)=>Date.parse(o.updated_at)-Date.parse(n.updated_at)).slice(0,s2).map(n=>({id:n.id,title:n.title,status:n.status,updatedAt:n.updated_at}))}function x2(e,n){const o=e.updatedAt.status==="available"?Date.parse(e.updatedAt.at):0;return(n.updatedAt.status==="available"?Date.parse(n.updatedAt.at):0)-o||e.id.localeCompare(n.id)}function I2(e){const n=k2(e),o=S2(e);return n!==null&&o!==null?{status:"available",label:n,url:o}:n!==null?{status:"label_only",label:n}:{status:"unavailable",error:"external reference unavailable"}}function S2(e){const n=Dt(e,"pr_review.pr_url")||Dt(e,"bugflow.github_issue_url");return n&&/^https?:\/\//i.test(n)?n:null}function k2(e){const n=Dt(e,"pr_review.pr_number");if(n)return`PR #${n}`;const o=Dt(e,"bugflow.github_issue_number");return o?`Issue #${o}`:Dt(e,"pr_review.external_ref")||Dt(e,"bugflow.external_ref")||null}function Dt(e,n){return e.map(o=>Le(o.metadata?.[n])).find(Boolean)??""}function b2(){return{status:"unavailable",error:"run health has not been derived"}}function z2(){return{status:"unavailable",error:"run health has not been derived"}}function Ru(e){return Array.isArray(e.labels)&&e.labels.some(n=>n.startsWith("gc:"))?!1:!!(u2.has(e.issue_type)||Le(e.metadata?.["gc.kind"])==="run")}function T2(e,n,o,s){const u=C2(e,n);return o!==null?{status:"active_step",stepId:o,stage:u,attempt:R2(s,o)}:u.status==="available"?{status:"stage_only",stage:u,error:"active run step unavailable"}:{status:"unavailable",error:"run progress unavailable"}}function C2(e,n){const o=e[n];return o===void 0?{status:"unavailable",error:"active run stage unavailable"}:{status:"available",index:n,key:o.key,label:o.label}}function R2(e,n){const o=ku(Fa(e,n));return o===null?{status:"unavailable",error:"run step attempt unavailable"}:{status:"available",value:o}}const Ym=/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i,B2={bead:"bead.",session:"session."};function Vr(e){return e instanceof Error?e.message:typeof e=="string"?e:"unknown error"}function P2(e){if(!e)return"";let n=e.length;for(;n>0&&e.charCodeAt(n-1)===47;)n--;const o=e.slice(0,n);return o.slice(o.lastIndexOf("/")+1)||o}const N2="polecat";function A2(e){return P2(e).toLowerCase().includes(N2)}function O2(e){return e.filter(n=>!n.read&&!A2(n.from))}const j2="modulepreload",$2=function(e){return"/"+e},Mf={},En=function(n,o,s){let u=Promise.resolve();if(o&&o.length>0){let h=function(y){return Promise.all(y.map(w=>Promise.resolve(w).then(I=>({status:"fulfilled",value:I}),I=>({status:"rejected",reason:I}))))};document.getElementsByTagName("link");const d=document.querySelector("meta[property=csp-nonce]"),m=d?.nonce||d?.getAttribute("nonce");u=h(o.map(y=>{if(y=$2(y),y in Mf)return;Mf[y]=!0;const w=y.endsWith(".css"),I=w?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${y}"]${I}`))return;const z=document.createElement("link");if(z.rel=w?"stylesheet":j2,w||(z.as="script"),z.crossOrigin="",z.href=y,m&&z.setAttribute("nonce",m),document.head.appendChild(z),w)return new Promise((B,D)=>{z.addEventListener("load",B),z.addEventListener("error",()=>D(new Error(`Unable to preload CSS for ${y}`)))})}))}function p(d){const m=new Event("vite:preloadError",{cancelable:!0});if(m.payload=d,window.dispatchEvent(m),!m.defaultPrevented)throw d}return u.then(d=>{for(const m of d||[])m.status==="rejected"&&p(m.reason);return n().catch(p)})};let ni=null;function L2(e){if(!Ym.test(e))throw new Error(`invalid city name: ${e}`);ni=e}function Ua(){return ni}function Mt(e){const n=ni;if(n===null)throw new Error(`${e} called before an active city was resolved`);return n}function Jo(e){if(ni===null)throw new Error(`cityPath("${e}") called before an active city was resolved`);return`/api/city/${encodeURIComponent(ni)}${e}`}async function D2(e,n,o,s){const u={Accept:"application/json"};s!==void 0&&(u["Content-Type"]="application/json"),e!=="GET"&&(u["X-GC-Request"]="dashboard");const p={method:e,headers:u,credentials:"same-origin"};s!==void 0&&(p.body=JSON.stringify(s));const d=await fetch(n,p);if(!d.ok){const h=await d.text(),y=M2(h),w=y?.error??(h.trim()||d.statusText||`HTTP ${d.status}`);throw new Xm(d.status,w,y?.kind)}let m;try{m=await d.json()}catch(h){throw new ev(n,`body must be valid JSON: ${U2(h)}`)}return o(m,n)}function M2(e){if(e.trim().length!==0)try{const n=JSON.parse(e);return F2(n)?n:void 0}catch{return}}function F2(e){if(typeof e!="object"||e===null)return!1;const n=e;return typeof n.error!="string"?!1:n.kind===void 0||typeof n.kind=="string"}async function rn(e,n,o,s){return D2(e,n,o,s)}class Xm extends Error{constructor(n,o,s){super(o),this.status=n,this.kind=s,this.name="ApiClientError"}status;kind}class ev extends Error{constructor(n,o){super(`Invalid API response for ${n}: ${o}`),this.url=n,this.detail=o,this.name="ApiResponseDecodeError"}url;detail}function U2(e){return e instanceof Error?e.message:typeof e=="string"?e:"unknown error"}function oo(e,n){throw new ev(e,n)}function Z2(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Bu(e,n,o){return Z2(e)||oo(n,`${o} must be an object`),e}function Lt(e,n,o,s){typeof e[s]!="string"&&oo(n,`${o}.${s} must be a string`)}function tv(e,n,o,s){const u=e[s];u!==null&&typeof u!="string"&&oo(n,`${o}.${s} must be a string or null`)}function Hn(e,n,o,s){typeof e[s]!="boolean"&&oo(n,`${o}.${s} must be a boolean`)}function ri(e,n,o,s){Array.isArray(e[s])||oo(n,`${o}.${s} must be an array`)}function Yr(e,n,o,s){Bu(e[s],n,`${o}.${s}`)}function q2(e,n,o,s){const u=e[s];u!==null&&(!Array.isArray(u)||u.some(p=>typeof p!="string"))&&oo(n,`${o}.${s} must be an array of strings or null`)}function xn(e,n){return(o,s)=>{const u=Bu(o,s,e);return n?.(u,s),u}}function nv(e,n){return xn(e,(o,s)=>{ri(o,s,e,"items"),n?.(o,s)})}const V2=xn("health",(e,n)=>{Hn(e,n,"health","ok"),Lt(e,n,"health","ts")}),W2=nv("commits",(e,n)=>{Lt(e,n,"commits","view")}),H2=nv("builds",(e,n)=>{tv(e,n,"builds","source"),Hn(e,n,"builds","failed_marker")}),K2=xn("config",(e,n)=>{Lt(e,n,"config","cityName"),Lt(e,n,"config","cityRoot"),Hn(e,n,"config","useFixtures"),Hn(e,n,"config","readOnly"),Lt(e,n,"config","operatorAlias"),Lt(e,n,"config","operatorWireAlias"),Lt(e,n,"config","decisionLabel"),q2(e,n,"config","enabledModules"),tv(e,n,"config","defaultView")}),G2=xn("system health",(e,n)=>{Yr(e,n,"system health","admin"),Yr(e,n,"system health","host")});function Jl(e,n,o,s){Yr(e,n,o,s);const u=e[s],p=`${o}.${s}`;Lt(u,n,p,"status")}const J2=xn("local tool versions",(e,n)=>{Jl(e,n,"local tool versions","dolt"),Jl(e,n,"local tool versions","beads"),Jl(e,n,"local tool versions","gc")}),Q2=xn("dolt trend",(e,n)=>{Hn(e,n,"dolt trend","available"),ri(e,n,"dolt trend","samples")}),Y2=xn("rig store health",(e,n)=>{Hn(e,n,"rig store health","available"),ri(e,n,"rig store health","rigs")});function Ff(e,n){const o=Bu(e,n,"supervisor status.status");Yr(o,n,"supervisor status.status","work")}const X2=xn("supervisor status",(e,n)=>{Hn(e,n,"supervisor status","available"),e.available===!0?(Lt(e,n,"supervisor status","sampledAt"),Ff(e.status,n)):(Lt(e,n,"supervisor status","reason"),e.status!==null&&Ff(e.status,n))}),e3=xn("run diff",(e,n)=>{Lt(e,n,"run diff","kind"),Yr(e,n,"run diff","rootPath"),Yr(e,n,"run diff","comparison"),ri(e,n,"run diff","status"),ri(e,n,"run diff","changedFiles"),Lt(e,n,"run diff","patch"),Hn(e,n,"run diff","truncated")});function t3(e,n="request failed"){if(e instanceof Xm){const o={message:e.message,status:e.status};return e.kind!==void 0&&(o.kind=e.kind),o}return e instanceof Error?{message:e.message}:{message:n}}function Gt(e,n="request failed"){const o=t3(e,n);return o.status===void 0?o.message:`${o.status} ${o.message}`}const oi={health(){return rn("GET","/api/health",V2)},listCommits(e){return rn("GET",`/api/git/commits?view=${encodeURIComponent(e)}`,W2)},listBuilds(){return rn("GET","/api/builds",H2)},config(){return rn("GET",Jo("/config"),K2)},systemHealth(){return rn("GET","/api/health/system",G2)},localToolVersions(){return rn("GET","/api/health/local-tools",J2)},doltTrend(){return rn("GET",Jo("/dolt-noms/trend"),Q2)},rigStoreHealth(){return rn("GET",Jo("/rig-store-health"),Y2)},supervisorStatus(){return rn("GET",Jo("/supervisor-status"),X2)},runDiff(e,n,o){const s=n3(o);return rn("POST",Jo(`/runs/${encodeURIComponent(e)}/diff${s}`),e3,n)}};function n3(e){const n=new URLSearchParams;e?.scopeKind&&e.scopeRef&&(n.set("scope_kind",e.scopeKind),n.set("scope_ref",e.scopeRef));const o=n.toString();return o.length>0?`?${o}`:""}const si=["agents","beads","runs","mail","activity","health"],r3=5,o3=new Map(si.map((e,n)=>[e,n]));function au(e,n={}){const o=i3(),s=[];let u=0;for(const y of e)for(const w of y.getItems()){s.push({item:w,index:u});const I=o[w.domain],z=[...I.items,w];o[w.domain]={domain:w.domain,attention:I.attention+(w.severity==="attention"?1:0),watch:I.watch+(w.severity==="watch"?1:0),unavailable:I.unavailable+(w.severity==="unavailable"?1:0),severity:w.severity==="unavailable"?I.severity:a3(I.severity,w.severity),items:z},u+=1}const p=s.sort((y,w)=>s3(y.item,w.item)||y.index-w.index).map(({item:y})=>y),d=n.topLimit??r3,m=p.slice(0,d),h=l3(p.slice(d));return{items:p,topItems:m,overflowByDomain:h,byDomain:o}}function i3(){const e={};for(const n of si)e[n]={domain:n,attention:0,watch:0,unavailable:0,severity:null,items:[]};return e}function a3(e,n){return e==="attention"||n==="attention"?"attention":"watch"}function s3(e,n){return Uf(e.severity)-Uf(n.severity)||Ia(n.current??!0)-Ia(e.current??!0)||Ia(n.actionable??!1)-Ia(e.actionable??!1)||Zf(n.updatedAt)-Zf(e.updatedAt)||qf(e.domain)-qf(n.domain)}function Uf(e){switch(e){case"attention":return 0;case"watch":return 1;case"unavailable":return 2}}function Ia(e){return e?1:0}function Zf(e){if(e===void 0)return 0;const n=Date.parse(e);return Number.isFinite(n)?n:0}function qf(e){return o3.get(e)??si.length}function l3(e){const n=[];for(const o of si){let s=0,u=0,p=0;for(const m of e)m.domain===o&&(m.severity==="attention"?s+=1:m.severity==="watch"?u+=1:p+=1);const d=s+u+p;d>0&&n.push({domain:o,attention:s,watch:u,unavailable:p,total:d})}return n}const u3=au([]),rv=b.createContext(u3);function c3({contributors:e,topLimit:n,children:o}){const s=b.useMemo(()=>n===void 0?au(e):au(e,{topLimit:n}),[e,n]);return $.jsx(rv.Provider,{value:s,children:o})}function d3(){return b.useContext(rv)}const Pu=new Map;function Ql(e){return Pu.get(e)?.value}function Sa(e){return Pu.get(e)?.fetchedAt}function p3(e,n){Pu.set(e,{value:n,fetchedAt:new Date().toISOString()})}function mn(e,n,o){const s=b.useRef(n);s.current=n;const u=b.useRef(o?.refreshFetcher);u.current=o?.refreshFetcher;const p=b.useRef(o?.sseRefreshFetcher);p.current=o?.sseRefreshFetcher;const d=b.useRef(o?.onError);d.current=o?.onError;const m=b.useRef(e);m.current=e;const h=b.useRef(0),[y,w]=b.useState(()=>Ql(e)),[I,z]=b.useState(()=>Ql(e)===void 0),[B,D]=b.useState(null),[V,A]=b.useState(()=>Sa(e)),H=b.useCallback(async K=>{const Y=h.current+1;h.current=Y;const ue=e;z(!0),D(null);try{const ce=await K(),pe=h.current===Y,Te=m.current===ue;pe&&Te?(p3(ue,ce),w(ce),A(Sa(ue))):Te&&(w(ye=>ye===void 0?ce:ye),A(ye=>ye??Sa(ue)??new Date().toISOString()))}catch(ce){h.current===Y&&(D(ce instanceof Error?ce.message:"failed to load"),d.current?.(ce))}finally{h.current===Y&&z(!1)}},[e]),oe=b.useCallback(()=>H(u.current??s.current),[H]),Q=b.useCallback(()=>H(p.current??u.current??s.current),[H]);return b.useEffect(()=>{const K=Ql(e);return w(K),z(K===void 0),A(Sa(e)),H(s.current),()=>{h.current+=1}},[e,H]),{data:y,loading:I,error:B,fetchedAt:V,refresh:oe,cheapRefresh:Q}}var f3=async(e,n)=>{let o=typeof n=="function"?await n(e):n;if(o)return e.scheme==="bearer"?`Bearer ${o}`:e.scheme==="basic"?`Basic ${btoa(o)}`:o},m3={bodySerializer:e=>JSON.stringify(e,(n,o)=>typeof o=="bigint"?o.toString():o)},v3=e=>{switch(e){case"label":return".";case"matrix":return";";case"simple":return",";default:return"&"}},h3=e=>{switch(e){case"form":return",";case"pipeDelimited":return"|";case"spaceDelimited":return"%20";default:return","}},g3=e=>{switch(e){case"label":return".";case"matrix":return";";case"simple":return",";default:return"&"}},ov=({allowReserved:e,explode:n,name:o,style:s,value:u})=>{if(!n){let m=(e?u:u.map(h=>encodeURIComponent(h))).join(h3(s));switch(s){case"label":return`.${m}`;case"matrix":return`;${o}=${m}`;case"simple":return m;default:return`${o}=${m}`}}let p=v3(s),d=u.map(m=>s==="label"||s==="simple"?e?m:encodeURIComponent(m):Za({allowReserved:e,name:o,value:m})).join(p);return s==="label"||s==="matrix"?p+d:d},Za=({allowReserved:e,name:n,value:o})=>{if(o==null)return"";if(typeof o=="object")throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");return`${n}=${e?o:encodeURIComponent(o)}`},iv=({allowReserved:e,explode:n,name:o,style:s,value:u,valueOnly:p})=>{if(u instanceof Date)return p?u.toISOString():`${o}=${u.toISOString()}`;if(s!=="deepObject"&&!n){let h=[];Object.entries(u).forEach(([w,I])=>{h=[...h,w,e?I:encodeURIComponent(I)]});let y=h.join(",");switch(s){case"form":return`${o}=${y}`;case"label":return`.${y}`;case"matrix":return`;${o}=${y}`;default:return y}}let d=g3(s),m=Object.entries(u).map(([h,y])=>Za({allowReserved:e,name:s==="deepObject"?`${o}[${h}]`:h,value:y})).join(d);return s==="label"||s==="matrix"?d+m:m},y3=/\{[^{}]+\}/g,_3=({path:e,url:n})=>{let o=n,s=n.match(y3);if(s)for(let u of s){let p=!1,d=u.substring(1,u.length-1),m="simple";d.endsWith("*")&&(p=!0,d=d.substring(0,d.length-1)),d.startsWith(".")?(d=d.substring(1),m="label"):d.startsWith(";")&&(d=d.substring(1),m="matrix");let h=e[d];if(h==null)continue;if(Array.isArray(h)){o=o.replace(u,ov({explode:p,name:d,style:m,value:h}));continue}if(typeof h=="object"){o=o.replace(u,iv({explode:p,name:d,style:m,value:h,valueOnly:!0}));continue}if(m==="matrix"){o=o.replace(u,`;${Za({name:d,value:h})}`);continue}let y=encodeURIComponent(m==="label"?`.${h}`:h);o=o.replace(u,y)}return o},av=({allowReserved:e,array:n,object:o}={})=>s=>{let u=[];if(s&&typeof s=="object")for(let p in s){let d=s[p];if(d!=null)if(Array.isArray(d)){let m=ov({allowReserved:e,explode:!0,name:p,style:"form",value:d,...n});m&&u.push(m)}else if(typeof d=="object"){let m=iv({allowReserved:e,explode:!0,name:p,style:"deepObject",value:d,...o});m&&u.push(m)}else{let m=Za({allowReserved:e,name:p,value:d});m&&u.push(m)}}return u.join("&")},w3=e=>{if(!e)return"stream";let n=e.split(";")[0]?.trim();if(n){if(n.startsWith("application/json")||n.endsWith("+json"))return"json";if(n==="multipart/form-data")return"formData";if(["application/","audio/","image/","video/"].some(o=>n.startsWith(o)))return"blob";if(n.startsWith("text/"))return"text"}},E3=async({security:e,...n})=>{for(let o of e){let s=await f3(o,n.auth);if(!s)continue;let u=o.name??"Authorization";switch(o.in){case"query":n.query||(n.query={}),n.query[u]=s;break;case"cookie":n.headers.append("Cookie",`${u}=${s}`);break;default:n.headers.set(u,s);break}return}},Vf=e=>x3({baseUrl:e.baseUrl,path:e.path,query:e.query,querySerializer:typeof e.querySerializer=="function"?e.querySerializer:av(e.querySerializer),url:e.url}),x3=({baseUrl:e,path:n,query:o,querySerializer:s,url:u})=>{let p=u.startsWith("/")?u:`/${u}`,d=(e??"")+p;n&&(d=_3({path:n,url:d}));let m=o?s(o):"";return m.startsWith("?")&&(m=m.substring(1)),m&&(d+=`?${m}`),d},Wf=(e,n)=>{let o={...e,...n};return o.baseUrl?.endsWith("/")&&(o.baseUrl=o.baseUrl.substring(0,o.baseUrl.length-1)),o.headers=sv(e.headers,n.headers),o},sv=(...e)=>{let n=new Headers;for(let o of e){if(!o||typeof o!="object")continue;let s=o instanceof Headers?o.entries():Object.entries(o);for(let[u,p]of s)if(p===null)n.delete(u);else if(Array.isArray(p))for(let d of p)n.append(u,d);else p!==void 0&&n.set(u,typeof p=="object"?JSON.stringify(p):p)}return n},Yl=class{_fns;constructor(){this._fns=[]}clear(){this._fns=[]}getInterceptorIndex(e){return typeof e=="number"?this._fns[e]?e:-1:this._fns.indexOf(e)}exists(e){let n=this.getInterceptorIndex(e);return!!this._fns[n]}eject(e){let n=this.getInterceptorIndex(e);this._fns[n]&&(this._fns[n]=null)}update(e,n){let o=this.getInterceptorIndex(e);return this._fns[o]?(this._fns[o]=n,e):!1}use(e){return this._fns=[...this._fns,e],this._fns.length-1}},I3=()=>({error:new Yl,request:new Yl,response:new Yl}),S3=av({allowReserved:!1,array:{explode:!0,style:"form"},object:{explode:!0,style:"deepObject"}}),k3={"Content-Type":"application/json"},lv=(e={})=>({...m3,headers:k3,parseAs:"auto",querySerializer:S3,...e}),uv=(e={})=>{let n=Wf(lv(),e),o=()=>({...n}),s=d=>(n=Wf(n,d),o()),u=I3(),p=async d=>{let m={...n,...d,fetch:d.fetch??n.fetch??globalThis.fetch,headers:sv(n.headers,d.headers)};m.security&&await E3({...m,security:m.security}),m.body&&m.bodySerializer&&(m.body=m.bodySerializer(m.body)),(m.body===void 0||m.body==="")&&m.headers.delete("Content-Type");let h=Vf(m),y={redirect:"follow",...m},w=new Request(h,y);for(let A of u.request._fns)A&&(w=await A(w,m));let I=m.fetch,z=await I(w);for(let A of u.response._fns)A&&(z=await A(z,w,m));let B={request:w,response:z};if(z.ok){if(z.status===204||z.headers.get("Content-Length")==="0")return m.responseStyle==="data"?{}:{data:{},...B};let A=(m.parseAs==="auto"?w3(z.headers.get("Content-Type")):m.parseAs)??"json";if(A==="stream")return m.responseStyle==="data"?z.body:{data:z.body,...B};let H=await z[A]();return A==="json"&&(m.responseValidator&&await m.responseValidator(H),m.responseTransformer&&(H=await m.responseTransformer(H))),m.responseStyle==="data"?H:{data:H,...B}}let D=await z.text();try{D=JSON.parse(D)}catch{}let V=D;for(let A of u.error._fns)A&&(V=await A(D,z,w,m));if(V=V||{},m.throwOnError)throw V;return m.responseStyle==="data"?void 0:{error:V,...B}};return{buildUrl:Vf,connect:d=>p({...d,method:"CONNECT"}),delete:d=>p({...d,method:"DELETE"}),get:d=>p({...d,method:"GET"}),getConfig:o,head:d=>p({...d,method:"HEAD"}),interceptors:u,options:d=>p({...d,method:"OPTIONS"}),patch:d=>p({...d,method:"PATCH"}),post:d=>p({...d,method:"POST"}),put:d=>p({...d,method:"PUT"}),request:p,setConfig:s,trace:d=>p({...d,method:"TRACE"})}};const Se=uv(lv()),b3=e=>(e?.client??Se).get({url:"/health",...e}),z3=e=>(e?.client??Se).get({url:"/v0/cities",...e}),T3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/agent/{base}/prime",...e}),C3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/agent/{base}/{action}",...e}),R3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/agent/{dir}/{base}/prime",...e}),B3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/agent/{dir}/{base}/{action}",...e}),P3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/agents",...e}),N3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/bead/{id}",...e}),A3=e=>(e.client??Se).patch({url:"/v0/city/{cityName}/bead/{id}",...e,headers:{"Content-Type":"application/json",...e.headers}}),O3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/bead/{id}/close",...e,headers:{"Content-Type":"application/json",...e.headers}}),j3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/beads",...e}),$3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/beads",...e,headers:{"Content-Type":"application/json",...e.headers}}),L3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/events",...e}),D3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/formulas/feed",...e}),M3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/formulas/{name}",...e}),F3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/health",...e}),U3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/mail",...e}),Z3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/mail",...e,headers:{"Content-Type":"application/json",...e.headers}}),q3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/mail/thread/{id}",...e}),V3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/archive",...e}),W3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/mark-unread",...e}),H3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/read",...e}),K3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/mail/{id}/reply",...e,headers:{"Content-Type":"application/json",...e.headers}}),G3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/rigs",...e}),J3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/session/{id}/pending",...e}),Q3=e=>(e.client??Se).post({url:"/v0/city/{cityName}/session/{id}/respond",...e,headers:{"Content-Type":"application/json",...e.headers}}),Y3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/session/{id}/transcript",...e}),X3=e=>(e.client??Se).get({url:"/v0/city/{cityName}/sessions",...e}),e_=e=>(e.client??Se).post({url:"/v0/city/{cityName}/sling",...e,headers:{"Content-Type":"application/json",...e.headers}}),t_=e=>(e.client??Se).get({url:"/v0/city/{cityName}/status",...e}),n_=e=>(e.client??Se).get({url:"/v0/city/{cityName}/workflow/{workflow_id}",...e});var Hf;function j(e,n,o){function s(m,h){if(m._zod||Object.defineProperty(m,"_zod",{value:{def:h,constr:d,traits:new Set},enumerable:!1}),m._zod.traits.has(e))return;m._zod.traits.add(e),n(m,h);const y=d.prototype,w=Object.keys(y);for(let I=0;Io?.Parent&&m instanceof o.Parent?!0:m?._zod?.traits?.has(e)}),Object.defineProperty(d,"name",{value:e}),d}class Wr extends Error{constructor(){super("Encountered Promise during synchronous parse. Use .parseAsync() instead.")}}class cv extends Error{constructor(n){super(`Encountered unidirectional transform during encode: ${n}`),this.name="ZodEncodeError"}}(Hf=globalThis).__zod_globalConfig??(Hf.__zod_globalConfig={});const Nu=globalThis.__zod_globalConfig;function hn(e){return Nu}function dv(e){const n=Object.values(e).filter(s=>typeof s=="number");return Object.entries(e).filter(([s,u])=>n.indexOf(+s)===-1).map(([s,u])=>u)}function su(e,n){return typeof n=="bigint"?n.toString():n}function qa(e){return{get value(){{const n=e();return Object.defineProperty(this,"value",{value:n}),n}}}}function Au(e){return e==null}function Ou(e){const n=e.startsWith("^")?1:0,o=e.endsWith("$")?e.length-1:e.length;return e.slice(n,o)}function r_(e,n){const o=e/n,s=Math.round(o),u=Number.EPSILON*Math.max(Math.abs(o),1);return Math.abs(o-s){};function ii(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}const i_=qa(()=>{if(Nu.jitless||typeof navigator<"u"&&navigator?.userAgent?.includes("Cloudflare"))return!1;try{const e=Function;return new e(""),!0}catch{return!1}});function Xr(e){if(ii(e)===!1)return!1;const n=e.constructor;if(n===void 0||typeof n!="function")return!0;const o=n.prototype;return!(ii(o)===!1||Object.prototype.hasOwnProperty.call(o,"isPrototypeOf")===!1)}function fv(e){return Xr(e)?{...e}:Array.isArray(e)?[...e]:e instanceof Map?new Map(e):e instanceof Set?new Set(e):e}const a_=new Set(["string","number","symbol"]);function eo(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Jn(e,n,o){const s=new e._zod.constr(n??e._zod.def);return(!n||o?.parent)&&(s._zod.parent=e),s}function ie(e){const n=e;if(!n)return{};if(typeof n=="string")return{error:()=>n};if(n?.message!==void 0){if(n?.error!==void 0)throw new Error("Cannot specify both `message` and `error` params");n.error=n.message}return delete n.message,typeof n.error=="string"?{...n,error:()=>n.error}:n}function s_(e){return Object.keys(e).filter(n=>e[n]._zod.optin==="optional"&&e[n]._zod.optout==="optional")}const l_={safeint:[Number.MIN_SAFE_INTEGER,Number.MAX_SAFE_INTEGER],int32:[-2147483648,2147483647],uint32:[0,4294967295],float32:[-34028234663852886e22,34028234663852886e22],float64:[-Number.MAX_VALUE,Number.MAX_VALUE]};function u_(e,n){const o=e._zod.def,s=o.checks;if(s&&s.length>0)throw new Error(".pick() cannot be used on object schemas containing refinements");const p=Gn(e._zod.def,{get shape(){const d={};for(const m in n){if(!(m in o.shape))throw new Error(`Unrecognized key: "${m}"`);n[m]&&(d[m]=o.shape[m])}return vr(this,"shape",d),d},checks:[]});return Jn(e,p)}function c_(e,n){const o=e._zod.def,s=o.checks;if(s&&s.length>0)throw new Error(".omit() cannot be used on object schemas containing refinements");const p=Gn(e._zod.def,{get shape(){const d={...e._zod.def.shape};for(const m in n){if(!(m in o.shape))throw new Error(`Unrecognized key: "${m}"`);n[m]&&delete d[m]}return vr(this,"shape",d),d},checks:[]});return Jn(e,p)}function d_(e,n){if(!Xr(n))throw new Error("Invalid input to extend: expected a plain object");const o=e._zod.def.checks;if(o&&o.length>0){const p=e._zod.def.shape;for(const d in n)if(Object.getOwnPropertyDescriptor(p,d)!==void 0)throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.")}const u=Gn(e._zod.def,{get shape(){const p={...e._zod.def.shape,...n};return vr(this,"shape",p),p}});return Jn(e,u)}function p_(e,n){if(!Xr(n))throw new Error("Invalid input to safeExtend: expected a plain object");const o=Gn(e._zod.def,{get shape(){const s={...e._zod.def.shape,...n};return vr(this,"shape",s),s}});return Jn(e,o)}function f_(e,n){if(e._zod.def.checks?.length)throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");const o=Gn(e._zod.def,{get shape(){const s={...e._zod.def.shape,...n._zod.def.shape};return vr(this,"shape",s),s},get catchall(){return n._zod.def.catchall},checks:n._zod.def.checks??[]});return Jn(e,o)}function m_(e,n,o){const u=n._zod.def.checks;if(u&&u.length>0)throw new Error(".partial() cannot be used on object schemas containing refinements");const d=Gn(n._zod.def,{get shape(){const m=n._zod.def.shape,h={...m};if(o)for(const y in o){if(!(y in m))throw new Error(`Unrecognized key: "${y}"`);o[y]&&(h[y]=e?new e({type:"optional",innerType:m[y]}):m[y])}else for(const y in m)h[y]=e?new e({type:"optional",innerType:m[y]}):m[y];return vr(this,"shape",h),h},checks:[]});return Jn(n,d)}function v_(e,n,o){const s=Gn(n._zod.def,{get shape(){const u=n._zod.def.shape,p={...u};if(o)for(const d in o){if(!(d in p))throw new Error(`Unrecognized key: "${d}"`);o[d]&&(p[d]=new e({type:"nonoptional",innerType:u[d]}))}else for(const d in u)p[d]=new e({type:"nonoptional",innerType:u[d]});return vr(this,"shape",p),p}});return Jn(n,s)}function Zr(e,n=0){if(e.aborted===!0)return!0;for(let o=n;o{var s;return(s=o).path??(s.path=[]),o.path.unshift(e),o})}function ka(e){return typeof e=="string"?e:e?.message}function gn(e,n,o){const s=e.message?e.message:ka(e.inst?._zod.def?.error?.(e))??ka(n?.error?.(e))??ka(o.customError?.(e))??ka(o.localeError?.(e))??"Invalid input",{inst:u,continue:p,input:d,...m}=e;return m.path??(m.path=[]),m.message=s,n?.reportInput&&(m.input=d),m}function ju(e){return Array.isArray(e)?"array":typeof e=="string"?"string":"unknown"}function ai(...e){const[n,o,s]=e;return typeof n=="string"?{message:n,code:"custom",input:o,inst:s}:{...n}}const mv=(e,n)=>{e.name="$ZodError",Object.defineProperty(e,"_zod",{value:e._zod,enumerable:!1}),Object.defineProperty(e,"issues",{value:n,enumerable:!1}),e.message=JSON.stringify(n,su,2),Object.defineProperty(e,"toString",{value:()=>e.message,enumerable:!1})},vv=j("$ZodError",mv),hv=j("$ZodError",mv,{Parent:Error});function g_(e,n=o=>o.message){const o={},s=[];for(const u of e.issues)u.path.length>0?(o[u.path[0]]=o[u.path[0]]||[],o[u.path[0]].push(n(u))):s.push(n(u));return{formErrors:s,fieldErrors:o}}function y_(e,n=o=>o.message){const o={_errors:[]},s=(u,p=[])=>{for(const d of u.issues)if(d.code==="invalid_union"&&d.errors.length)d.errors.map(m=>s({issues:m},[...p,...d.path]));else if(d.code==="invalid_key")s({issues:d.issues},[...p,...d.path]);else if(d.code==="invalid_element")s({issues:d.issues},[...p,...d.path]);else{const m=[...p,...d.path];if(m.length===0)o._errors.push(n(d));else{let h=o,y=0;for(;y(n,o,s,u)=>{const p=s?{...s,async:!1}:{async:!1},d=n._zod.run({value:o,issues:[]},p);if(d instanceof Promise)throw new Wr;if(d.issues.length){const m=new(u?.Err??e)(d.issues.map(h=>gn(h,p,hn())));throw pv(m,u?.callee),m}return d.value},Lu=e=>async(n,o,s,u)=>{const p=s?{...s,async:!0}:{async:!0};let d=n._zod.run({value:o,issues:[]},p);if(d instanceof Promise&&(d=await d),d.issues.length){const m=new(u?.Err??e)(d.issues.map(h=>gn(h,p,hn())));throw pv(m,u?.callee),m}return d.value},Va=e=>(n,o,s)=>{const u=s?{...s,async:!1}:{async:!1},p=n._zod.run({value:o,issues:[]},u);if(p instanceof Promise)throw new Wr;return p.issues.length?{success:!1,error:new(e??vv)(p.issues.map(d=>gn(d,u,hn())))}:{success:!0,data:p.value}},__=Va(hv),Wa=e=>async(n,o,s)=>{const u=s?{...s,async:!0}:{async:!0};let p=n._zod.run({value:o,issues:[]},u);return p instanceof Promise&&(p=await p),p.issues.length?{success:!1,error:new e(p.issues.map(d=>gn(d,u,hn())))}:{success:!0,data:p.value}},w_=Wa(hv),E_=e=>(n,o,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return $u(e)(n,o,u)},x_=e=>(n,o,s)=>$u(e)(n,o,s),I_=e=>async(n,o,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return Lu(e)(n,o,u)},S_=e=>async(n,o,s)=>Lu(e)(n,o,s),k_=e=>(n,o,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return Va(e)(n,o,u)},b_=e=>(n,o,s)=>Va(e)(n,o,s),z_=e=>async(n,o,s)=>{const u=s?{...s,direction:"backward"}:{direction:"backward"};return Wa(e)(n,o,u)},T_=e=>async(n,o,s)=>Wa(e)(n,o,s),C_=/^[cC][0-9a-z]{6,}$/,R_=/^[0-9a-z]+$/,B_=/^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/,P_=/^[0-9a-vA-V]{20}$/,N_=/^[A-Za-z0-9]{27}$/,A_=/^[a-zA-Z0-9_-]{21}$/,O_=/^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/,j_=/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/,Jf=e=>e?new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${e}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`):/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/,$_=/^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/,L_="^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$";function D_(){return new RegExp(L_,"u")}const M_=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,F_=/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/,U_=/^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/,Z_=/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,q_=/^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/,gv=/^[A-Za-z0-9_-]*$/,V_=/^https?$/,W_=/^\+[1-9]\d{6,14}$/,yv="(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))",H_=new RegExp(`^${yv}$`);function _v(e){const n="(?:[01]\\d|2[0-3]):[0-5]\\d";return typeof e.precision=="number"?e.precision===-1?`${n}`:e.precision===0?`${n}:[0-5]\\d`:`${n}:[0-5]\\d\\.\\d{${e.precision}}`:`${n}(?::[0-5]\\d(?:\\.\\d+)?)?`}function K_(e){return new RegExp(`^${_v(e)}$`)}function G_(e){const n=_v({precision:e.precision}),o=["Z"];e.local&&o.push(""),e.offset&&o.push("([+-](?:[01]\\d|2[0-3]):[0-5]\\d)");const s=`${n}(?:${o.join("|")})`;return new RegExp(`^${yv}T(?:${s})$`)}const J_=e=>{const n=e?`[\\s\\S]{${e?.minimum??0},${e?.maximum??""}}`:"[\\s\\S]*";return new RegExp(`^${n}$`)},Q_=/^-?\d+n?$/,Y_=/^-?\d+$/,wv=/^-?\d+(?:\.\d+)?$/,X_=/^(?:true|false)$/i,e8=/^[^A-Z]*$/,t8=/^[^a-z]*$/,St=j("$ZodCheck",(e,n)=>{var o;e._zod??(e._zod={}),e._zod.def=n,(o=e._zod).onattach??(o.onattach=[])}),Ev={number:"number",bigint:"bigint",object:"date"},xv=j("$ZodCheckLessThan",(e,n)=>{St.init(e,n);const o=Ev[typeof n.value];e._zod.onattach.push(s=>{const u=s._zod.bag,p=(n.inclusive?u.maximum:u.exclusiveMaximum)??Number.POSITIVE_INFINITY;n.value{(n.inclusive?s.value<=n.value:s.value{St.init(e,n);const o=Ev[typeof n.value];e._zod.onattach.push(s=>{const u=s._zod.bag,p=(n.inclusive?u.minimum:u.exclusiveMinimum)??Number.NEGATIVE_INFINITY;n.value>p&&(n.inclusive?u.minimum=n.value:u.exclusiveMinimum=n.value)}),e._zod.check=s=>{(n.inclusive?s.value>=n.value:s.value>n.value)||s.issues.push({origin:o,code:"too_small",minimum:typeof n.value=="object"?n.value.getTime():n.value,input:s.value,inclusive:n.inclusive,inst:e,continue:!n.abort})}}),n8=j("$ZodCheckMultipleOf",(e,n)=>{St.init(e,n),e._zod.onattach.push(o=>{var s;(s=o._zod.bag).multipleOf??(s.multipleOf=n.value)}),e._zod.check=o=>{if(typeof o.value!=typeof n.value)throw new Error("Cannot mix number and bigint in multiple_of check.");(typeof o.value=="bigint"?o.value%n.value===BigInt(0):r_(o.value,n.value)===0)||o.issues.push({origin:typeof o.value,code:"not_multiple_of",divisor:n.value,input:o.value,inst:e,continue:!n.abort})}}),r8=j("$ZodCheckNumberFormat",(e,n)=>{St.init(e,n),n.format=n.format||"float64";const o=n.format?.includes("int"),s=o?"int":"number",[u,p]=l_[n.format];e._zod.onattach.push(d=>{const m=d._zod.bag;m.format=n.format,m.minimum=u,m.maximum=p,o&&(m.pattern=Y_)}),e._zod.check=d=>{const m=d.value;if(o){if(!Number.isInteger(m)){d.issues.push({expected:s,format:n.format,code:"invalid_type",continue:!1,input:m,inst:e});return}if(!Number.isSafeInteger(m)){m>0?d.issues.push({input:m,code:"too_big",maximum:Number.MAX_SAFE_INTEGER,note:"Integers must be within the safe integer range.",inst:e,origin:s,inclusive:!0,continue:!n.abort}):d.issues.push({input:m,code:"too_small",minimum:Number.MIN_SAFE_INTEGER,note:"Integers must be within the safe integer range.",inst:e,origin:s,inclusive:!0,continue:!n.abort});return}}mp&&d.issues.push({origin:"number",input:m,code:"too_big",maximum:p,inclusive:!0,inst:e,continue:!n.abort})}}),o8=j("$ZodCheckMaxLength",(e,n)=>{var o;St.init(e,n),(o=e._zod.def).when??(o.when=s=>{const u=s.value;return!Au(u)&&u.length!==void 0}),e._zod.onattach.push(s=>{const u=s._zod.bag.maximum??Number.POSITIVE_INFINITY;n.maximum{const u=s.value;if(u.length<=n.maximum)return;const d=ju(u);s.issues.push({origin:d,code:"too_big",maximum:n.maximum,inclusive:!0,input:u,inst:e,continue:!n.abort})}}),i8=j("$ZodCheckMinLength",(e,n)=>{var o;St.init(e,n),(o=e._zod.def).when??(o.when=s=>{const u=s.value;return!Au(u)&&u.length!==void 0}),e._zod.onattach.push(s=>{const u=s._zod.bag.minimum??Number.NEGATIVE_INFINITY;n.minimum>u&&(s._zod.bag.minimum=n.minimum)}),e._zod.check=s=>{const u=s.value;if(u.length>=n.minimum)return;const d=ju(u);s.issues.push({origin:d,code:"too_small",minimum:n.minimum,inclusive:!0,input:u,inst:e,continue:!n.abort})}}),a8=j("$ZodCheckLengthEquals",(e,n)=>{var o;St.init(e,n),(o=e._zod.def).when??(o.when=s=>{const u=s.value;return!Au(u)&&u.length!==void 0}),e._zod.onattach.push(s=>{const u=s._zod.bag;u.minimum=n.length,u.maximum=n.length,u.length=n.length}),e._zod.check=s=>{const u=s.value,p=u.length;if(p===n.length)return;const d=ju(u),m=p>n.length;s.issues.push({origin:d,...m?{code:"too_big",maximum:n.length}:{code:"too_small",minimum:n.length},inclusive:!0,exact:!0,input:s.value,inst:e,continue:!n.abort})}}),Ha=j("$ZodCheckStringFormat",(e,n)=>{var o,s;St.init(e,n),e._zod.onattach.push(u=>{const p=u._zod.bag;p.format=n.format,n.pattern&&(p.patterns??(p.patterns=new Set),p.patterns.add(n.pattern))}),n.pattern?(o=e._zod).check??(o.check=u=>{n.pattern.lastIndex=0,!n.pattern.test(u.value)&&u.issues.push({origin:"string",code:"invalid_format",format:n.format,input:u.value,...n.pattern?{pattern:n.pattern.toString()}:{},inst:e,continue:!n.abort})}):(s=e._zod).check??(s.check=()=>{})}),s8=j("$ZodCheckRegex",(e,n)=>{Ha.init(e,n),e._zod.check=o=>{n.pattern.lastIndex=0,!n.pattern.test(o.value)&&o.issues.push({origin:"string",code:"invalid_format",format:"regex",input:o.value,pattern:n.pattern.toString(),inst:e,continue:!n.abort})}}),l8=j("$ZodCheckLowerCase",(e,n)=>{n.pattern??(n.pattern=e8),Ha.init(e,n)}),u8=j("$ZodCheckUpperCase",(e,n)=>{n.pattern??(n.pattern=t8),Ha.init(e,n)}),c8=j("$ZodCheckIncludes",(e,n)=>{St.init(e,n);const o=eo(n.includes),s=new RegExp(typeof n.position=="number"?`^.{${n.position}}${o}`:o);n.pattern=s,e._zod.onattach.push(u=>{const p=u._zod.bag;p.patterns??(p.patterns=new Set),p.patterns.add(s)}),e._zod.check=u=>{u.value.includes(n.includes,n.position)||u.issues.push({origin:"string",code:"invalid_format",format:"includes",includes:n.includes,input:u.value,inst:e,continue:!n.abort})}}),d8=j("$ZodCheckStartsWith",(e,n)=>{St.init(e,n);const o=new RegExp(`^${eo(n.prefix)}.*`);n.pattern??(n.pattern=o),e._zod.onattach.push(s=>{const u=s._zod.bag;u.patterns??(u.patterns=new Set),u.patterns.add(o)}),e._zod.check=s=>{s.value.startsWith(n.prefix)||s.issues.push({origin:"string",code:"invalid_format",format:"starts_with",prefix:n.prefix,input:s.value,inst:e,continue:!n.abort})}}),p8=j("$ZodCheckEndsWith",(e,n)=>{St.init(e,n);const o=new RegExp(`.*${eo(n.suffix)}$`);n.pattern??(n.pattern=o),e._zod.onattach.push(s=>{const u=s._zod.bag;u.patterns??(u.patterns=new Set),u.patterns.add(o)}),e._zod.check=s=>{s.value.endsWith(n.suffix)||s.issues.push({origin:"string",code:"invalid_format",format:"ends_with",suffix:n.suffix,input:s.value,inst:e,continue:!n.abort})}}),f8=j("$ZodCheckOverwrite",(e,n)=>{St.init(e,n),e._zod.check=o=>{o.value=n.tx(o.value)}});class m8{constructor(n=[]){this.content=[],this.indent=0,this&&(this.args=n)}indented(n){this.indent+=1,n(this),this.indent-=1}write(n){if(typeof n=="function"){n(this,{execution:"sync"}),n(this,{execution:"async"});return}const s=n.split(` -`).filter(d=>d),u=Math.min(...s.map(d=>d.length-d.trimStart().length)),p=s.map(d=>d.slice(u)).map(d=>" ".repeat(this.indent*2)+d);for(const d of p)this.content.push(d)}compile(){const n=Function,o=this?.args,u=[...(this?.content??[""]).map(p=>` ${p}`)];return new n(...o,u.join(` -`))}}const v8={major:4,minor:4,patch:3},De=j("$ZodType",(e,n)=>{var o;e??(e={}),e._zod.def=n,e._zod.bag=e._zod.bag||{},e._zod.version=v8;const s=[...e._zod.def.checks??[]];e._zod.traits.has("$ZodCheck")&&s.unshift(e);for(const u of s)for(const p of u._zod.onattach)p(e);if(s.length===0)(o=e._zod).deferred??(o.deferred=[]),e._zod.deferred?.push(()=>{e._zod.run=e._zod.parse});else{const u=(d,m,h)=>{let y=Zr(d),w;for(const I of m){if(I._zod.def.when){if(h_(d)||!I._zod.def.when(d))continue}else if(y)continue;const z=d.issues.length,B=I._zod.check(d);if(B instanceof Promise&&h?.async===!1)throw new Wr;if(w||B instanceof Promise)w=(w??Promise.resolve()).then(async()=>{await B,d.issues.length!==z&&(y||(y=Zr(d,z)))});else{if(d.issues.length===z)continue;y||(y=Zr(d,z))}}return w?w.then(()=>d):d},p=(d,m,h)=>{if(Zr(d))return d.aborted=!0,d;const y=u(m,s,h);if(y instanceof Promise){if(h.async===!1)throw new Wr;return y.then(w=>e._zod.parse(w,h))}return e._zod.parse(y,h)};e._zod.run=(d,m)=>{if(m.skipChecks)return e._zod.parse(d,m);if(m.direction==="backward"){const y=e._zod.parse({value:d.value,issues:[]},{...m,skipChecks:!0});return y instanceof Promise?y.then(w=>p(w,d,m)):p(y,d,m)}const h=e._zod.parse(d,m);if(h instanceof Promise){if(m.async===!1)throw new Wr;return h.then(y=>u(y,s,m))}return u(h,s,m)}}ze(e,"~standard",()=>({validate:u=>{try{const p=__(e,u);return p.success?{value:p.data}:{issues:p.error?.issues}}catch{return w_(e,u).then(d=>d.success?{value:d.data}:{issues:d.error?.issues})}},vendor:"zod",version:1}))}),Du=j("$ZodString",(e,n)=>{De.init(e,n),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??J_(e._zod.bag),e._zod.parse=(o,s)=>{if(n.coerce)try{o.value=String(o.value)}catch{}return typeof o.value=="string"||o.issues.push({expected:"string",code:"invalid_type",input:o.value,inst:e}),o}}),Me=j("$ZodStringFormat",(e,n)=>{Ha.init(e,n),Du.init(e,n)}),h8=j("$ZodGUID",(e,n)=>{n.pattern??(n.pattern=j_),Me.init(e,n)}),g8=j("$ZodUUID",(e,n)=>{if(n.version){const s={v1:1,v2:2,v3:3,v4:4,v5:5,v6:6,v7:7,v8:8}[n.version];if(s===void 0)throw new Error(`Invalid UUID version: "${n.version}"`);n.pattern??(n.pattern=Jf(s))}else n.pattern??(n.pattern=Jf());Me.init(e,n)}),y8=j("$ZodEmail",(e,n)=>{n.pattern??(n.pattern=$_),Me.init(e,n)}),_8=j("$ZodURL",(e,n)=>{Me.init(e,n),e._zod.check=o=>{try{const s=o.value.trim();if(!n.normalize&&n.protocol?.source===V_.source&&!/^https?:\/\//i.test(s)){o.issues.push({code:"invalid_format",format:"url",note:"Invalid URL format",input:o.value,inst:e,continue:!n.abort});return}const u=new URL(s);n.hostname&&(n.hostname.lastIndex=0,n.hostname.test(u.hostname)||o.issues.push({code:"invalid_format",format:"url",note:"Invalid hostname",pattern:n.hostname.source,input:o.value,inst:e,continue:!n.abort})),n.protocol&&(n.protocol.lastIndex=0,n.protocol.test(u.protocol.endsWith(":")?u.protocol.slice(0,-1):u.protocol)||o.issues.push({code:"invalid_format",format:"url",note:"Invalid protocol",pattern:n.protocol.source,input:o.value,inst:e,continue:!n.abort})),n.normalize?o.value=u.href:o.value=s;return}catch{o.issues.push({code:"invalid_format",format:"url",input:o.value,inst:e,continue:!n.abort})}}}),w8=j("$ZodEmoji",(e,n)=>{n.pattern??(n.pattern=D_()),Me.init(e,n)}),E8=j("$ZodNanoID",(e,n)=>{n.pattern??(n.pattern=A_),Me.init(e,n)}),x8=j("$ZodCUID",(e,n)=>{n.pattern??(n.pattern=C_),Me.init(e,n)}),I8=j("$ZodCUID2",(e,n)=>{n.pattern??(n.pattern=R_),Me.init(e,n)}),S8=j("$ZodULID",(e,n)=>{n.pattern??(n.pattern=B_),Me.init(e,n)}),k8=j("$ZodXID",(e,n)=>{n.pattern??(n.pattern=P_),Me.init(e,n)}),b8=j("$ZodKSUID",(e,n)=>{n.pattern??(n.pattern=N_),Me.init(e,n)}),z8=j("$ZodISODateTime",(e,n)=>{n.pattern??(n.pattern=G_(n)),Me.init(e,n)}),T8=j("$ZodISODate",(e,n)=>{n.pattern??(n.pattern=H_),Me.init(e,n)}),C8=j("$ZodISOTime",(e,n)=>{n.pattern??(n.pattern=K_(n)),Me.init(e,n)}),R8=j("$ZodISODuration",(e,n)=>{n.pattern??(n.pattern=O_),Me.init(e,n)}),B8=j("$ZodIPv4",(e,n)=>{n.pattern??(n.pattern=M_),Me.init(e,n),e._zod.bag.format="ipv4"}),P8=j("$ZodIPv6",(e,n)=>{n.pattern??(n.pattern=F_),Me.init(e,n),e._zod.bag.format="ipv6",e._zod.check=o=>{try{new URL(`http://[${o.value}]`)}catch{o.issues.push({code:"invalid_format",format:"ipv6",input:o.value,inst:e,continue:!n.abort})}}}),N8=j("$ZodCIDRv4",(e,n)=>{n.pattern??(n.pattern=U_),Me.init(e,n)}),A8=j("$ZodCIDRv6",(e,n)=>{n.pattern??(n.pattern=Z_),Me.init(e,n),e._zod.check=o=>{const s=o.value.split("/");try{if(s.length!==2)throw new Error;const[u,p]=s;if(!p)throw new Error;const d=Number(p);if(`${d}`!==p)throw new Error;if(d<0||d>128)throw new Error;new URL(`http://[${u}]`)}catch{o.issues.push({code:"invalid_format",format:"cidrv6",input:o.value,inst:e,continue:!n.abort})}}});function Sv(e){if(e==="")return!0;if(/\s/.test(e)||e.length%4!==0)return!1;try{return atob(e),!0}catch{return!1}}const O8=j("$ZodBase64",(e,n)=>{n.pattern??(n.pattern=q_),Me.init(e,n),e._zod.bag.contentEncoding="base64",e._zod.check=o=>{Sv(o.value)||o.issues.push({code:"invalid_format",format:"base64",input:o.value,inst:e,continue:!n.abort})}});function j8(e){if(!gv.test(e))return!1;const n=e.replace(/[-_]/g,s=>s==="-"?"+":"/"),o=n.padEnd(Math.ceil(n.length/4)*4,"=");return Sv(o)}const $8=j("$ZodBase64URL",(e,n)=>{n.pattern??(n.pattern=gv),Me.init(e,n),e._zod.bag.contentEncoding="base64url",e._zod.check=o=>{j8(o.value)||o.issues.push({code:"invalid_format",format:"base64url",input:o.value,inst:e,continue:!n.abort})}}),L8=j("$ZodE164",(e,n)=>{n.pattern??(n.pattern=W_),Me.init(e,n)});function D8(e,n=null){try{const o=e.split(".");if(o.length!==3)return!1;const[s]=o;if(!s)return!1;const u=JSON.parse(atob(s));return!("typ"in u&&u?.typ!=="JWT"||!u.alg||n&&(!("alg"in u)||u.alg!==n))}catch{return!1}}const M8=j("$ZodJWT",(e,n)=>{Me.init(e,n),e._zod.check=o=>{D8(o.value,n.alg)||o.issues.push({code:"invalid_format",format:"jwt",input:o.value,inst:e,continue:!n.abort})}}),kv=j("$ZodNumber",(e,n)=>{De.init(e,n),e._zod.pattern=e._zod.bag.pattern??wv,e._zod.parse=(o,s)=>{if(n.coerce)try{o.value=Number(o.value)}catch{}const u=o.value;if(typeof u=="number"&&!Number.isNaN(u)&&Number.isFinite(u))return o;const p=typeof u=="number"?Number.isNaN(u)?"NaN":Number.isFinite(u)?void 0:"Infinity":void 0;return o.issues.push({expected:"number",code:"invalid_type",input:u,inst:e,...p?{received:p}:{}}),o}}),F8=j("$ZodNumberFormat",(e,n)=>{r8.init(e,n),kv.init(e,n)}),U8=j("$ZodBoolean",(e,n)=>{De.init(e,n),e._zod.pattern=X_,e._zod.parse=(o,s)=>{if(n.coerce)try{o.value=!!o.value}catch{}const u=o.value;return typeof u=="boolean"||o.issues.push({expected:"boolean",code:"invalid_type",input:u,inst:e}),o}}),Z8=j("$ZodBigInt",(e,n)=>{De.init(e,n),e._zod.pattern=Q_,e._zod.parse=(o,s)=>{if(n.coerce)try{o.value=BigInt(o.value)}catch{}return typeof o.value=="bigint"||o.issues.push({expected:"bigint",code:"invalid_type",input:o.value,inst:e}),o}}),q8=j("$ZodUnknown",(e,n)=>{De.init(e,n),e._zod.parse=o=>o}),V8=j("$ZodNever",(e,n)=>{De.init(e,n),e._zod.parse=(o,s)=>(o.issues.push({expected:"never",code:"invalid_type",input:o.value,inst:e}),o)});function Qf(e,n,o){e.issues.length&&n.issues.push(...qr(o,e.issues)),n.value[o]=e.value}const W8=j("$ZodArray",(e,n)=>{De.init(e,n),e._zod.parse=(o,s)=>{const u=o.value;if(!Array.isArray(u))return o.issues.push({expected:"array",code:"invalid_type",input:u,inst:e}),o;o.value=Array(u.length);const p=[];for(let d=0;dQf(y,o,d))):Qf(h,o,d)}return p.length?Promise.all(p).then(()=>o):o}});function Na(e,n,o,s,u,p){const d=o in s;if(e.issues.length){if(u&&p&&!d)return;n.issues.push(...qr(o,e.issues))}if(!d&&!u){e.issues.length||n.issues.push({code:"invalid_type",expected:"nonoptional",input:void 0,path:[o]});return}e.value===void 0?d&&(n.value[o]=void 0):n.value[o]=e.value}function bv(e){const n=Object.keys(e.shape);for(const s of n)if(!e.shape?.[s]?._zod?.traits?.has("$ZodType"))throw new Error(`Invalid element at key "${s}": expected a Zod schema`);const o=s_(e.shape);return{...e,keys:n,keySet:new Set(n),numKeys:n.length,optionalKeys:new Set(o)}}function zv(e,n,o,s,u,p){const d=[],m=u.keySet,h=u.catchall._zod,y=h.def.type,w=h.optin==="optional",I=h.optout==="optional";for(const z in n){if(z==="__proto__"||m.has(z))continue;if(y==="never"){d.push(z);continue}const B=h.run({value:n[z],issues:[]},s);B instanceof Promise?e.push(B.then(D=>Na(D,o,z,n,w,I))):Na(B,o,z,n,w,I)}return d.length&&o.issues.push({code:"unrecognized_keys",keys:d,input:n,inst:p}),e.length?Promise.all(e).then(()=>o):o}const H8=j("$ZodObject",(e,n)=>{if(De.init(e,n),!Object.getOwnPropertyDescriptor(n,"shape")?.get){const m=n.shape;Object.defineProperty(n,"shape",{get:()=>{const h={...m};return Object.defineProperty(n,"shape",{value:h}),h}})}const s=qa(()=>bv(n));ze(e._zod,"propValues",()=>{const m=n.shape,h={};for(const y in m){const w=m[y]._zod;if(w.values){h[y]??(h[y]=new Set);for(const I of w.values)h[y].add(I)}}return h});const u=ii,p=n.catchall;let d;e._zod.parse=(m,h)=>{d??(d=s.value);const y=m.value;if(!u(y))return m.issues.push({expected:"object",code:"invalid_type",input:y,inst:e}),m;m.value={};const w=[],I=d.shape;for(const z of d.keys){const B=I[z],D=B._zod.optin==="optional",V=B._zod.optout==="optional",A=B._zod.run({value:y[z],issues:[]},h);A instanceof Promise?w.push(A.then(H=>Na(H,m,z,y,D,V))):Na(A,m,z,y,D,V)}return p?zv(w,y,m,h,s.value,e):w.length?Promise.all(w).then(()=>m):m}}),K8=j("$ZodObjectJIT",(e,n)=>{H8.init(e,n);const o=e._zod.parse,s=qa(()=>bv(n)),u=z=>{const B=new m8(["shape","payload","ctx"]),D=s.value,V=Q=>{const K=Gf(Q);return`shape[${K}]._zod.run({ value: input[${K}], issues: [] }, ctx)`};B.write("const input = payload.value;");const A=Object.create(null);let H=0;for(const Q of D.keys)A[Q]=`key_${H++}`;B.write("const newResult = {};");for(const Q of D.keys){const K=A[Q],Y=Gf(Q),ue=z[Q],ce=ue?._zod?.optin==="optional",pe=ue?._zod?.optout==="optional";B.write(`const ${K} = ${V(Q)};`),ce&&pe?B.write(` - if (${K}.issues.length) { - if (${Y} in input) { - payload.issues = payload.issues.concat(${K}.issues.map(iss => ({ - ...iss, - path: iss.path ? [${Y}, ...iss.path] : [${Y}] - }))); - } - } - - if (${K}.value === undefined) { - if (${Y} in input) { - newResult[${Y}] = undefined; - } - } else { - newResult[${Y}] = ${K}.value; - } - - `):ce?B.write(` - if (${K}.issues.length) { - payload.issues = payload.issues.concat(${K}.issues.map(iss => ({ - ...iss, - path: iss.path ? [${Y}, ...iss.path] : [${Y}] - }))); - } - - if (${K}.value === undefined) { - if (${Y} in input) { - newResult[${Y}] = undefined; - } - } else { - newResult[${Y}] = ${K}.value; - } - - `):B.write(` - const ${K}_present = ${Y} in input; - if (${K}.issues.length) { - payload.issues = payload.issues.concat(${K}.issues.map(iss => ({ - ...iss, - path: iss.path ? [${Y}, ...iss.path] : [${Y}] - }))); - } - if (!${K}_present && !${K}.issues.length) { - payload.issues.push({ - code: "invalid_type", - expected: "nonoptional", - input: undefined, - path: [${Y}] - }); - } - - if (${K}_present) { - if (${K}.value === undefined) { - newResult[${Y}] = undefined; - } else { - newResult[${Y}] = ${K}.value; - } - } - - `)}B.write("payload.value = newResult;"),B.write("return payload;");const oe=B.compile();return(Q,K)=>oe(z,Q,K)};let p;const d=ii,m=!Nu.jitless,y=m&&i_.value,w=n.catchall;let I;e._zod.parse=(z,B)=>{I??(I=s.value);const D=z.value;return d(D)?m&&y&&B?.async===!1&&B.jitless!==!0?(p||(p=u(n.shape)),z=p(z,B),w?zv([],D,z,B,I,e):z):o(z,B):(z.issues.push({expected:"object",code:"invalid_type",input:D,inst:e}),z)}});function Yf(e,n,o,s){for(const p of e)if(p.issues.length===0)return n.value=p.value,n;const u=e.filter(p=>!Zr(p));return u.length===1?(n.value=u[0].value,u[0]):(n.issues.push({code:"invalid_union",input:n.value,inst:o,errors:e.map(p=>p.issues.map(d=>gn(d,s,hn())))}),n)}const Tv=j("$ZodUnion",(e,n)=>{De.init(e,n),ze(e._zod,"optin",()=>n.options.some(s=>s._zod.optin==="optional")?"optional":void 0),ze(e._zod,"optout",()=>n.options.some(s=>s._zod.optout==="optional")?"optional":void 0),ze(e._zod,"values",()=>{if(n.options.every(s=>s._zod.values))return new Set(n.options.flatMap(s=>Array.from(s._zod.values)))}),ze(e._zod,"pattern",()=>{if(n.options.every(s=>s._zod.pattern)){const s=n.options.map(u=>u._zod.pattern);return new RegExp(`^(${s.map(u=>Ou(u.source)).join("|")})$`)}});const o=n.options.length===1?n.options[0]._zod.run:null;e._zod.parse=(s,u)=>{if(o)return o(s,u);let p=!1;const d=[];for(const m of n.options){const h=m._zod.run({value:s.value,issues:[]},u);if(h instanceof Promise)d.push(h),p=!0;else{if(h.issues.length===0)return h;d.push(h)}}return p?Promise.all(d).then(m=>Yf(m,s,e,u)):Yf(d,s,e,u)}}),G8=j("$ZodDiscriminatedUnion",(e,n)=>{n.inclusive=!1,Tv.init(e,n);const o=e._zod.parse;ze(e._zod,"propValues",()=>{const u={};for(const p of n.options){const d=p._zod.propValues;if(!d||Object.keys(d).length===0)throw new Error(`Invalid discriminated union option at index "${n.options.indexOf(p)}"`);for(const[m,h]of Object.entries(d)){u[m]||(u[m]=new Set);for(const y of h)u[m].add(y)}}return u});const s=qa(()=>{const u=n.options,p=new Map;for(const d of u){const m=d._zod.propValues?.[n.discriminator];if(!m||m.size===0)throw new Error(`Invalid discriminated union option at index "${n.options.indexOf(d)}"`);for(const h of m){if(p.has(h))throw new Error(`Duplicate discriminator value "${String(h)}"`);p.set(h,d)}}return p});e._zod.parse=(u,p)=>{const d=u.value;if(!ii(d))return u.issues.push({code:"invalid_type",expected:"object",input:d,inst:e}),u;const m=s.value.get(d?.[n.discriminator]);return m?m._zod.run(u,p):n.unionFallback||p.direction==="backward"?o(u,p):(u.issues.push({code:"invalid_union",errors:[],note:"No matching discriminator",discriminator:n.discriminator,options:Array.from(s.value.keys()),input:d,path:[n.discriminator],inst:e}),u)}}),J8=j("$ZodIntersection",(e,n)=>{De.init(e,n),e._zod.parse=(o,s)=>{const u=o.value,p=n.left._zod.run({value:u,issues:[]},s),d=n.right._zod.run({value:u,issues:[]},s);return p instanceof Promise||d instanceof Promise?Promise.all([p,d]).then(([h,y])=>Xf(o,h,y)):Xf(o,p,d)}});function lu(e,n){if(e===n)return{valid:!0,data:e};if(e instanceof Date&&n instanceof Date&&+e==+n)return{valid:!0,data:e};if(Xr(e)&&Xr(n)){const o=Object.keys(n),s=Object.keys(e).filter(p=>o.indexOf(p)!==-1),u={...e,...n};for(const p of s){const d=lu(e[p],n[p]);if(!d.valid)return{valid:!1,mergeErrorPath:[p,...d.mergeErrorPath]};u[p]=d.data}return{valid:!0,data:u}}if(Array.isArray(e)&&Array.isArray(n)){if(e.length!==n.length)return{valid:!1,mergeErrorPath:[]};const o=[];for(let s=0;sm.l&&m.r).map(([m])=>m);if(p.length&&u&&e.issues.push({...u,keys:p}),Zr(e))return e;const d=lu(n.value,o.value);if(!d.valid)throw new Error(`Unmergable intersection. Error path: ${JSON.stringify(d.mergeErrorPath)}`);return e.value=d.data,e}const Q8=j("$ZodRecord",(e,n)=>{De.init(e,n),e._zod.parse=(o,s)=>{const u=o.value;if(!Xr(u))return o.issues.push({expected:"record",code:"invalid_type",input:u,inst:e}),o;const p=[],d=n.keyType._zod.values;if(d){o.value={};const m=new Set;for(const y of d)if(typeof y=="string"||typeof y=="number"||typeof y=="symbol"){m.add(typeof y=="number"?y.toString():y);const w=n.keyType._zod.run({value:y,issues:[]},s);if(w instanceof Promise)throw new Error("Async schemas not supported in object keys currently");if(w.issues.length){o.issues.push({code:"invalid_key",origin:"record",issues:w.issues.map(B=>gn(B,s,hn())),input:y,path:[y],inst:e});continue}const I=w.value,z=n.valueType._zod.run({value:u[y],issues:[]},s);z instanceof Promise?p.push(z.then(B=>{B.issues.length&&o.issues.push(...qr(y,B.issues)),o.value[I]=B.value})):(z.issues.length&&o.issues.push(...qr(y,z.issues)),o.value[I]=z.value)}let h;for(const y in u)m.has(y)||(h=h??[],h.push(y));h&&h.length>0&&o.issues.push({code:"unrecognized_keys",input:u,inst:e,keys:h})}else{o.value={};for(const m of Reflect.ownKeys(u)){if(m==="__proto__"||!Object.prototype.propertyIsEnumerable.call(u,m))continue;let h=n.keyType._zod.run({value:m,issues:[]},s);if(h instanceof Promise)throw new Error("Async schemas not supported in object keys currently");if(typeof m=="string"&&wv.test(m)&&h.issues.length){const I=n.keyType._zod.run({value:Number(m),issues:[]},s);if(I instanceof Promise)throw new Error("Async schemas not supported in object keys currently");I.issues.length===0&&(h=I)}if(h.issues.length){n.mode==="loose"?o.value[m]=u[m]:o.issues.push({code:"invalid_key",origin:"record",issues:h.issues.map(I=>gn(I,s,hn())),input:m,path:[m],inst:e});continue}const w=n.valueType._zod.run({value:u[m],issues:[]},s);w instanceof Promise?p.push(w.then(I=>{I.issues.length&&o.issues.push(...qr(m,I.issues)),o.value[h.value]=I.value})):(w.issues.length&&o.issues.push(...qr(m,w.issues)),o.value[h.value]=w.value)}}return p.length?Promise.all(p).then(()=>o):o}}),Y8=j("$ZodEnum",(e,n)=>{De.init(e,n);const o=dv(n.entries),s=new Set(o);e._zod.values=s,e._zod.pattern=new RegExp(`^(${o.filter(u=>a_.has(typeof u)).map(u=>typeof u=="string"?eo(u):u.toString()).join("|")})$`),e._zod.parse=(u,p)=>{const d=u.value;return s.has(d)||u.issues.push({code:"invalid_value",values:o,input:d,inst:e}),u}}),X8=j("$ZodLiteral",(e,n)=>{if(De.init(e,n),n.values.length===0)throw new Error("Cannot create literal schema with no valid values");const o=new Set(n.values);e._zod.values=o,e._zod.pattern=new RegExp(`^(${n.values.map(s=>typeof s=="string"?eo(s):s?eo(s.toString()):String(s)).join("|")})$`),e._zod.parse=(s,u)=>{const p=s.value;return o.has(p)||s.issues.push({code:"invalid_value",values:n.values,input:p,inst:e}),s}}),ew=j("$ZodTransform",(e,n)=>{De.init(e,n),e._zod.optin="optional",e._zod.parse=(o,s)=>{if(s.direction==="backward")throw new cv(e.constructor.name);const u=n.transform(o.value,o);if(s.async)return(u instanceof Promise?u:Promise.resolve(u)).then(d=>(o.value=d,o.fallback=!0,o));if(u instanceof Promise)throw new Wr;return o.value=u,o.fallback=!0,o}});function em(e,n){return n===void 0&&(e.issues.length||e.fallback)?{issues:[],value:void 0}:e}const Cv=j("$ZodOptional",(e,n)=>{De.init(e,n),e._zod.optin="optional",e._zod.optout="optional",ze(e._zod,"values",()=>n.innerType._zod.values?new Set([...n.innerType._zod.values,void 0]):void 0),ze(e._zod,"pattern",()=>{const o=n.innerType._zod.pattern;return o?new RegExp(`^(${Ou(o.source)})?$`):void 0}),e._zod.parse=(o,s)=>{if(n.innerType._zod.optin==="optional"){const u=o.value,p=n.innerType._zod.run(o,s);return p instanceof Promise?p.then(d=>em(d,u)):em(p,u)}return o.value===void 0?o:n.innerType._zod.run(o,s)}}),tw=j("$ZodExactOptional",(e,n)=>{Cv.init(e,n),ze(e._zod,"values",()=>n.innerType._zod.values),ze(e._zod,"pattern",()=>n.innerType._zod.pattern),e._zod.parse=(o,s)=>n.innerType._zod.run(o,s)}),nw=j("$ZodNullable",(e,n)=>{De.init(e,n),ze(e._zod,"optin",()=>n.innerType._zod.optin),ze(e._zod,"optout",()=>n.innerType._zod.optout),ze(e._zod,"pattern",()=>{const o=n.innerType._zod.pattern;return o?new RegExp(`^(${Ou(o.source)}|null)$`):void 0}),ze(e._zod,"values",()=>n.innerType._zod.values?new Set([...n.innerType._zod.values,null]):void 0),e._zod.parse=(o,s)=>o.value===null?o:n.innerType._zod.run(o,s)}),rw=j("$ZodDefault",(e,n)=>{De.init(e,n),e._zod.optin="optional",ze(e._zod,"values",()=>n.innerType._zod.values),e._zod.parse=(o,s)=>{if(s.direction==="backward")return n.innerType._zod.run(o,s);if(o.value===void 0)return o.value=n.defaultValue,o;const u=n.innerType._zod.run(o,s);return u instanceof Promise?u.then(p=>tm(p,n)):tm(u,n)}});function tm(e,n){return e.value===void 0&&(e.value=n.defaultValue),e}const ow=j("$ZodPrefault",(e,n)=>{De.init(e,n),e._zod.optin="optional",ze(e._zod,"values",()=>n.innerType._zod.values),e._zod.parse=(o,s)=>(s.direction==="backward"||o.value===void 0&&(o.value=n.defaultValue),n.innerType._zod.run(o,s))}),iw=j("$ZodNonOptional",(e,n)=>{De.init(e,n),ze(e._zod,"values",()=>{const o=n.innerType._zod.values;return o?new Set([...o].filter(s=>s!==void 0)):void 0}),e._zod.parse=(o,s)=>{const u=n.innerType._zod.run(o,s);return u instanceof Promise?u.then(p=>nm(p,e)):nm(u,e)}});function nm(e,n){return!e.issues.length&&e.value===void 0&&e.issues.push({code:"invalid_type",expected:"nonoptional",input:e.value,inst:n}),e}const aw=j("$ZodCatch",(e,n)=>{De.init(e,n),e._zod.optin="optional",ze(e._zod,"optout",()=>n.innerType._zod.optout),ze(e._zod,"values",()=>n.innerType._zod.values),e._zod.parse=(o,s)=>{if(s.direction==="backward")return n.innerType._zod.run(o,s);const u=n.innerType._zod.run(o,s);return u instanceof Promise?u.then(p=>(o.value=p.value,p.issues.length&&(o.value=n.catchValue({...o,error:{issues:p.issues.map(d=>gn(d,s,hn()))},input:o.value}),o.issues=[],o.fallback=!0),o)):(o.value=u.value,u.issues.length&&(o.value=n.catchValue({...o,error:{issues:u.issues.map(p=>gn(p,s,hn()))},input:o.value}),o.issues=[],o.fallback=!0),o)}}),sw=j("$ZodPipe",(e,n)=>{De.init(e,n),ze(e._zod,"values",()=>n.in._zod.values),ze(e._zod,"optin",()=>n.in._zod.optin),ze(e._zod,"optout",()=>n.out._zod.optout),ze(e._zod,"propValues",()=>n.in._zod.propValues),e._zod.parse=(o,s)=>{if(s.direction==="backward"){const p=n.out._zod.run(o,s);return p instanceof Promise?p.then(d=>ba(d,n.in,s)):ba(p,n.in,s)}const u=n.in._zod.run(o,s);return u instanceof Promise?u.then(p=>ba(p,n.out,s)):ba(u,n.out,s)}});function ba(e,n,o){return e.issues.length?(e.aborted=!0,e):n._zod.run({value:e.value,issues:e.issues,fallback:e.fallback},o)}const lw=j("$ZodReadonly",(e,n)=>{De.init(e,n),ze(e._zod,"propValues",()=>n.innerType._zod.propValues),ze(e._zod,"values",()=>n.innerType._zod.values),ze(e._zod,"optin",()=>n.innerType?._zod?.optin),ze(e._zod,"optout",()=>n.innerType?._zod?.optout),e._zod.parse=(o,s)=>{if(s.direction==="backward")return n.innerType._zod.run(o,s);const u=n.innerType._zod.run(o,s);return u instanceof Promise?u.then(rm):rm(u)}});function rm(e){return e.value=Object.freeze(e.value),e}const uw=j("$ZodCustom",(e,n)=>{St.init(e,n),De.init(e,n),e._zod.parse=(o,s)=>o,e._zod.check=o=>{const s=o.value,u=n.fn(s);if(u instanceof Promise)return u.then(p=>om(p,o,s,e));om(u,o,s,e)}});function om(e,n,o,s){if(!e){const u={code:"custom",input:o,inst:s,path:[...s._zod.def.path??[]],continue:!s._zod.def.abort};s._zod.def.params&&(u.params=s._zod.def.params),n.issues.push(ai(u))}}var im;class cw{constructor(){this._map=new WeakMap,this._idmap=new Map}add(n,...o){const s=o[0];return this._map.set(n,s),s&&typeof s=="object"&&"id"in s&&this._idmap.set(s.id,n),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(n){const o=this._map.get(n);return o&&typeof o=="object"&&"id"in o&&this._idmap.delete(o.id),this._map.delete(n),this}get(n){const o=n._zod.parent;if(o){const s={...this.get(o)??{}};delete s.id;const u={...s,...this._map.get(n)};return Object.keys(u).length?u:void 0}return this._map.get(n)}has(n){return this._map.has(n)}}function dw(){return new cw}(im=globalThis).__zod_globalRegistry??(im.__zod_globalRegistry=dw());const Qo=globalThis.__zod_globalRegistry;function pw(e,n){return new e({type:"string",...ie(n)})}function fw(e,n){return new e({type:"string",format:"email",check:"string_format",abort:!1,...ie(n)})}function am(e,n){return new e({type:"string",format:"guid",check:"string_format",abort:!1,...ie(n)})}function mw(e,n){return new e({type:"string",format:"uuid",check:"string_format",abort:!1,...ie(n)})}function vw(e,n){return new e({type:"string",format:"uuid",check:"string_format",abort:!1,version:"v4",...ie(n)})}function hw(e,n){return new e({type:"string",format:"uuid",check:"string_format",abort:!1,version:"v6",...ie(n)})}function gw(e,n){return new e({type:"string",format:"uuid",check:"string_format",abort:!1,version:"v7",...ie(n)})}function Rv(e,n){return new e({type:"string",format:"url",check:"string_format",abort:!1,...ie(n)})}function yw(e,n){return new e({type:"string",format:"emoji",check:"string_format",abort:!1,...ie(n)})}function _w(e,n){return new e({type:"string",format:"nanoid",check:"string_format",abort:!1,...ie(n)})}function ww(e,n){return new e({type:"string",format:"cuid",check:"string_format",abort:!1,...ie(n)})}function Ew(e,n){return new e({type:"string",format:"cuid2",check:"string_format",abort:!1,...ie(n)})}function xw(e,n){return new e({type:"string",format:"ulid",check:"string_format",abort:!1,...ie(n)})}function Iw(e,n){return new e({type:"string",format:"xid",check:"string_format",abort:!1,...ie(n)})}function Sw(e,n){return new e({type:"string",format:"ksuid",check:"string_format",abort:!1,...ie(n)})}function kw(e,n){return new e({type:"string",format:"ipv4",check:"string_format",abort:!1,...ie(n)})}function bw(e,n){return new e({type:"string",format:"ipv6",check:"string_format",abort:!1,...ie(n)})}function zw(e,n){return new e({type:"string",format:"cidrv4",check:"string_format",abort:!1,...ie(n)})}function Tw(e,n){return new e({type:"string",format:"cidrv6",check:"string_format",abort:!1,...ie(n)})}function Cw(e,n){return new e({type:"string",format:"base64",check:"string_format",abort:!1,...ie(n)})}function Rw(e,n){return new e({type:"string",format:"base64url",check:"string_format",abort:!1,...ie(n)})}function Bw(e,n){return new e({type:"string",format:"e164",check:"string_format",abort:!1,...ie(n)})}function Pw(e,n){return new e({type:"string",format:"jwt",check:"string_format",abort:!1,...ie(n)})}function Nw(e,n){return new e({type:"string",format:"datetime",check:"string_format",offset:!1,local:!1,precision:null,...ie(n)})}function Aw(e,n){return new e({type:"string",format:"date",check:"string_format",...ie(n)})}function Ow(e,n){return new e({type:"string",format:"time",check:"string_format",precision:null,...ie(n)})}function jw(e,n){return new e({type:"string",format:"duration",check:"string_format",...ie(n)})}function $w(e,n){return new e({type:"number",checks:[],...ie(n)})}function Lw(e,n){return new e({type:"number",check:"number_format",abort:!1,format:"safeint",...ie(n)})}function Dw(e,n){return new e({type:"boolean",...ie(n)})}function Mw(e,n){return new e({type:"bigint",coerce:!0,...ie(n)})}function Fw(e){return new e({type:"unknown"})}function Uw(e,n){return new e({type:"never",...ie(n)})}function Aa(e,n){return new xv({check:"less_than",...ie(n),value:e,inclusive:!1})}function Hr(e,n){return new xv({check:"less_than",...ie(n),value:e,inclusive:!0})}function Oa(e,n){return new Iv({check:"greater_than",...ie(n),value:e,inclusive:!1})}function Un(e,n){return new Iv({check:"greater_than",...ie(n),value:e,inclusive:!0})}function uu(e,n){return new n8({check:"multiple_of",...ie(n),value:e})}function Bv(e,n){return new o8({check:"max_length",...ie(n),maximum:e})}function ja(e,n){return new i8({check:"min_length",...ie(n),minimum:e})}function Pv(e,n){return new a8({check:"length_equals",...ie(n),length:e})}function Zw(e,n){return new s8({check:"string_format",format:"regex",...ie(n),pattern:e})}function qw(e){return new l8({check:"string_format",format:"lowercase",...ie(e)})}function Vw(e){return new u8({check:"string_format",format:"uppercase",...ie(e)})}function Ww(e,n){return new c8({check:"string_format",format:"includes",...ie(n),includes:e})}function Hw(e,n){return new d8({check:"string_format",format:"starts_with",...ie(n),prefix:e})}function Kw(e,n){return new p8({check:"string_format",format:"ends_with",...ie(n),suffix:e})}function io(e){return new f8({check:"overwrite",tx:e})}function Gw(e){return io(n=>n.normalize(e))}function Jw(){return io(e=>e.trim())}function Qw(){return io(e=>e.toLowerCase())}function Yw(){return io(e=>e.toUpperCase())}function Xw(){return io(e=>o_(e))}function e5(e,n,o){return new e({type:"array",element:n,...ie(o)})}function t5(e,n,o){return new e({type:"custom",check:"custom",fn:n,...ie(o)})}function n5(e,n){const o=r5(s=>(s.addIssue=u=>{if(typeof u=="string")s.issues.push(ai(u,s.value,o._zod.def));else{const p=u;p.fatal&&(p.continue=!1),p.code??(p.code="custom"),p.input??(p.input=s.value),p.inst??(p.inst=o),p.continue??(p.continue=!o._zod.def.abort),s.issues.push(ai(p))}},e(s.value,s)),n);return o}function r5(e,n){const o=new St({check:"custom",...ie(n)});return o._zod.check=e,o}function Nv(e){let n=e?.target??"draft-2020-12";return n==="draft-4"&&(n="draft-04"),n==="draft-7"&&(n="draft-07"),{processors:e.processors??{},metadataRegistry:e?.metadata??Qo,target:n,unrepresentable:e?.unrepresentable??"throw",override:e?.override??(()=>{}),io:e?.io??"output",counter:0,seen:new Map,cycles:e?.cycles??"ref",reused:e?.reused??"inline",external:e?.external??void 0}}function Ye(e,n,o={path:[],schemaPath:[]}){var s;const u=e._zod.def,p=n.seen.get(e);if(p)return p.count++,o.schemaPath.includes(e)&&(p.cycle=o.path),p.schema;const d={schema:{},count:1,cycle:void 0,path:o.path};n.seen.set(e,d);const m=e._zod.toJSONSchema?.();if(m)d.schema=m;else{const w={...o,schemaPath:[...o.schemaPath,e],path:o.path};if(e._zod.processJSONSchema)e._zod.processJSONSchema(n,d.schema,w);else{const z=d.schema,B=n.processors[u.type];if(!B)throw new Error(`[toJSONSchema]: Non-representable type encountered: ${u.type}`);B(e,n,z,w)}const I=e._zod.parent;I&&(d.ref||(d.ref=I),Ye(I,n,w),n.seen.get(I).isParent=!0)}const h=n.metadataRegistry.get(e);return h&&Object.assign(d.schema,h),n.io==="input"&&mt(e)&&(delete d.schema.examples,delete d.schema.default),n.io==="input"&&"_prefault"in d.schema&&((s=d.schema).default??(s.default=d.schema._prefault)),delete d.schema._prefault,n.seen.get(e).schema}function Av(e,n){const o=e.seen.get(n);if(!o)throw new Error("Unprocessed schema. This is a bug in Zod.");const s=new Map;for(const d of e.seen.entries()){const m=e.metadataRegistry.get(d[0])?.id;if(m){const h=s.get(m);if(h&&h!==d[0])throw new Error(`Duplicate schema id "${m}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);s.set(m,d[0])}}const u=d=>{const m=e.target==="draft-2020-12"?"$defs":"definitions";if(e.external){const I=e.external.registry.get(d[0])?.id,z=e.external.uri??(D=>D);if(I)return{ref:z(I)};const B=d[1].defId??d[1].schema.id??`schema${e.counter++}`;return d[1].defId=B,{defId:B,ref:`${z("__shared")}#/${m}/${B}`}}if(d[1]===o)return{ref:"#"};const y=`#/${m}/`,w=d[1].schema.id??`__schema${e.counter++}`;return{defId:w,ref:y+w}},p=d=>{if(d[1].schema.$ref)return;const m=d[1],{ref:h,defId:y}=u(d);m.def={...m.schema},y&&(m.defId=y);const w=m.schema;for(const I in w)delete w[I];w.$ref=h};if(e.cycles==="throw")for(const d of e.seen.entries()){const m=d[1];if(m.cycle)throw new Error(`Cycle detected: #/${m.cycle?.join("/")}/ - -Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.`)}for(const d of e.seen.entries()){const m=d[1];if(n===d[0]){p(d);continue}if(e.external){const y=e.external.registry.get(d[0])?.id;if(n!==d[0]&&y){p(d);continue}}if(e.metadataRegistry.get(d[0])?.id){p(d);continue}if(m.cycle){p(d);continue}if(m.count>1&&e.reused==="ref"){p(d);continue}}}function Ov(e,n){const o=e.seen.get(n);if(!o)throw new Error("Unprocessed schema. This is a bug in Zod.");const s=m=>{const h=e.seen.get(m);if(h.ref===null)return;const y=h.def??h.schema,w={...y},I=h.ref;if(h.ref=null,I){s(I);const B=e.seen.get(I),D=B.schema;if(D.$ref&&(e.target==="draft-07"||e.target==="draft-04"||e.target==="openapi-3.0")?(y.allOf=y.allOf??[],y.allOf.push(D)):Object.assign(y,D),Object.assign(y,w),m._zod.parent===I)for(const A in y)A==="$ref"||A==="allOf"||A in w||delete y[A];if(D.$ref&&B.def)for(const A in y)A==="$ref"||A==="allOf"||A in B.def&&JSON.stringify(y[A])===JSON.stringify(B.def[A])&&delete y[A]}const z=m._zod.parent;if(z&&z!==I){s(z);const B=e.seen.get(z);if(B?.schema.$ref&&(y.$ref=B.schema.$ref,B.def))for(const D in y)D==="$ref"||D==="allOf"||D in B.def&&JSON.stringify(y[D])===JSON.stringify(B.def[D])&&delete y[D]}e.override({zodSchema:m,jsonSchema:y,path:h.path??[]})};for(const m of[...e.seen.entries()].reverse())s(m[0]);const u={};if(e.target==="draft-2020-12"?u.$schema="https://json-schema.org/draft/2020-12/schema":e.target==="draft-07"?u.$schema="http://json-schema.org/draft-07/schema#":e.target==="draft-04"?u.$schema="http://json-schema.org/draft-04/schema#":e.target,e.external?.uri){const m=e.external.registry.get(n)?.id;if(!m)throw new Error("Schema is missing an `id` property");u.$id=e.external.uri(m)}Object.assign(u,o.def??o.schema);const p=e.metadataRegistry.get(n)?.id;p!==void 0&&u.id===p&&delete u.id;const d=e.external?.defs??{};for(const m of e.seen.entries()){const h=m[1];h.def&&h.defId&&(h.def.id===h.defId&&delete h.def.id,d[h.defId]=h.def)}e.external||Object.keys(d).length>0&&(e.target==="draft-2020-12"?u.$defs=d:u.definitions=d);try{const m=JSON.parse(JSON.stringify(u));return Object.defineProperty(m,"~standard",{value:{...n["~standard"],jsonSchema:{input:$a(n,"input",e.processors),output:$a(n,"output",e.processors)}},enumerable:!1,writable:!1}),m}catch{throw new Error("Error converting schema to JSON.")}}function mt(e,n){const o=n??{seen:new Set};if(o.seen.has(e))return!1;o.seen.add(e);const s=e._zod.def;if(s.type==="transform")return!0;if(s.type==="array")return mt(s.element,o);if(s.type==="set")return mt(s.valueType,o);if(s.type==="lazy")return mt(s.getter(),o);if(s.type==="promise"||s.type==="optional"||s.type==="nonoptional"||s.type==="nullable"||s.type==="readonly"||s.type==="default"||s.type==="prefault")return mt(s.innerType,o);if(s.type==="intersection")return mt(s.left,o)||mt(s.right,o);if(s.type==="record"||s.type==="map")return mt(s.keyType,o)||mt(s.valueType,o);if(s.type==="pipe")return e._zod.traits.has("$ZodCodec")?!0:mt(s.in,o)||mt(s.out,o);if(s.type==="object"){for(const u in s.shape)if(mt(s.shape[u],o))return!0;return!1}if(s.type==="union"){for(const u of s.options)if(mt(u,o))return!0;return!1}if(s.type==="tuple"){for(const u of s.items)if(mt(u,o))return!0;return!!(s.rest&&mt(s.rest,o))}return!1}const o5=(e,n={})=>o=>{const s=Nv({...o,processors:n});return Ye(e,s),Av(s,e),Ov(s,e)},$a=(e,n,o={})=>s=>{const{libraryOptions:u,target:p}=s??{},d=Nv({...u??{},target:p,io:n,processors:o});return Ye(e,d),Av(d,e),Ov(d,e)},i5={guid:"uuid",url:"uri",datetime:"date-time",json_string:"json-string",regex:""},a5=(e,n,o,s)=>{const u=o;u.type="string";const{minimum:p,maximum:d,format:m,patterns:h,contentEncoding:y}=e._zod.bag;if(typeof p=="number"&&(u.minLength=p),typeof d=="number"&&(u.maxLength=d),m&&(u.format=i5[m]??m,u.format===""&&delete u.format,m==="time"&&delete u.format),y&&(u.contentEncoding=y),h&&h.size>0){const w=[...h];w.length===1?u.pattern=w[0].source:w.length>1&&(u.allOf=[...w.map(I=>({...n.target==="draft-07"||n.target==="draft-04"||n.target==="openapi-3.0"?{type:"string"}:{},pattern:I.source}))])}},s5=(e,n,o,s)=>{const u=o,{minimum:p,maximum:d,format:m,multipleOf:h,exclusiveMaximum:y,exclusiveMinimum:w}=e._zod.bag;typeof m=="string"&&m.includes("int")?u.type="integer":u.type="number";const I=typeof w=="number"&&w>=(p??Number.NEGATIVE_INFINITY),z=typeof y=="number"&&y<=(d??Number.POSITIVE_INFINITY),B=n.target==="draft-04"||n.target==="openapi-3.0";I?B?(u.minimum=w,u.exclusiveMinimum=!0):u.exclusiveMinimum=w:typeof p=="number"&&(u.minimum=p),z?B?(u.maximum=y,u.exclusiveMaximum=!0):u.exclusiveMaximum=y:typeof d=="number"&&(u.maximum=d),typeof h=="number"&&(u.multipleOf=h)},l5=(e,n,o,s)=>{o.type="boolean"},u5=(e,n,o,s)=>{if(n.unrepresentable==="throw")throw new Error("BigInt cannot be represented in JSON Schema")},c5=(e,n,o,s)=>{o.not={}},d5=(e,n,o,s)=>{},p5=(e,n,o,s)=>{const u=e._zod.def,p=dv(u.entries);p.every(d=>typeof d=="number")&&(o.type="number"),p.every(d=>typeof d=="string")&&(o.type="string"),o.enum=p},f5=(e,n,o,s)=>{const u=e._zod.def,p=[];for(const d of u.values)if(d===void 0){if(n.unrepresentable==="throw")throw new Error("Literal `undefined` cannot be represented in JSON Schema")}else if(typeof d=="bigint"){if(n.unrepresentable==="throw")throw new Error("BigInt literals cannot be represented in JSON Schema");p.push(Number(d))}else p.push(d);if(p.length!==0)if(p.length===1){const d=p[0];o.type=d===null?"null":typeof d,n.target==="draft-04"||n.target==="openapi-3.0"?o.enum=[d]:o.const=d}else p.every(d=>typeof d=="number")&&(o.type="number"),p.every(d=>typeof d=="string")&&(o.type="string"),p.every(d=>typeof d=="boolean")&&(o.type="boolean"),p.every(d=>d===null)&&(o.type="null"),o.enum=p},m5=(e,n,o,s)=>{if(n.unrepresentable==="throw")throw new Error("Custom types cannot be represented in JSON Schema")},v5=(e,n,o,s)=>{if(n.unrepresentable==="throw")throw new Error("Transforms cannot be represented in JSON Schema")},h5=(e,n,o,s)=>{const u=o,p=e._zod.def,{minimum:d,maximum:m}=e._zod.bag;typeof d=="number"&&(u.minItems=d),typeof m=="number"&&(u.maxItems=m),u.type="array",u.items=Ye(p.element,n,{...s,path:[...s.path,"items"]})},g5=(e,n,o,s)=>{const u=o,p=e._zod.def;u.type="object",u.properties={};const d=p.shape;for(const y in d)u.properties[y]=Ye(d[y],n,{...s,path:[...s.path,"properties",y]});const m=new Set(Object.keys(d)),h=new Set([...m].filter(y=>{const w=p.shape[y]._zod;return n.io==="input"?w.optin===void 0:w.optout===void 0}));h.size>0&&(u.required=Array.from(h)),p.catchall?._zod.def.type==="never"?u.additionalProperties=!1:p.catchall?p.catchall&&(u.additionalProperties=Ye(p.catchall,n,{...s,path:[...s.path,"additionalProperties"]})):n.io==="output"&&(u.additionalProperties=!1)},y5=(e,n,o,s)=>{const u=e._zod.def,p=u.inclusive===!1,d=u.options.map((m,h)=>Ye(m,n,{...s,path:[...s.path,p?"oneOf":"anyOf",h]}));p?o.oneOf=d:o.anyOf=d},_5=(e,n,o,s)=>{const u=e._zod.def,p=Ye(u.left,n,{...s,path:[...s.path,"allOf",0]}),d=Ye(u.right,n,{...s,path:[...s.path,"allOf",1]}),m=y=>"allOf"in y&&Object.keys(y).length===1,h=[...m(p)?p.allOf:[p],...m(d)?d.allOf:[d]];o.allOf=h},w5=(e,n,o,s)=>{const u=o,p=e._zod.def;u.type="object";const d=p.keyType,h=d._zod.bag?.patterns;if(p.mode==="loose"&&h&&h.size>0){const w=Ye(p.valueType,n,{...s,path:[...s.path,"patternProperties","*"]});u.patternProperties={};for(const I of h)u.patternProperties[I.source]=w}else(n.target==="draft-07"||n.target==="draft-2020-12")&&(u.propertyNames=Ye(p.keyType,n,{...s,path:[...s.path,"propertyNames"]})),u.additionalProperties=Ye(p.valueType,n,{...s,path:[...s.path,"additionalProperties"]});const y=d._zod.values;if(y){const w=[...y].filter(I=>typeof I=="string"||typeof I=="number");w.length>0&&(u.required=w)}},E5=(e,n,o,s)=>{const u=e._zod.def,p=Ye(u.innerType,n,s),d=n.seen.get(e);n.target==="openapi-3.0"?(d.ref=u.innerType,o.nullable=!0):o.anyOf=[p,{type:"null"}]},x5=(e,n,o,s)=>{const u=e._zod.def;Ye(u.innerType,n,s);const p=n.seen.get(e);p.ref=u.innerType},I5=(e,n,o,s)=>{const u=e._zod.def;Ye(u.innerType,n,s);const p=n.seen.get(e);p.ref=u.innerType,o.default=JSON.parse(JSON.stringify(u.defaultValue))},S5=(e,n,o,s)=>{const u=e._zod.def;Ye(u.innerType,n,s);const p=n.seen.get(e);p.ref=u.innerType,n.io==="input"&&(o._prefault=JSON.parse(JSON.stringify(u.defaultValue)))},k5=(e,n,o,s)=>{const u=e._zod.def;Ye(u.innerType,n,s);const p=n.seen.get(e);p.ref=u.innerType;let d;try{d=u.catchValue(void 0)}catch{throw new Error("Dynamic catch values are not supported in JSON Schema")}o.default=d},b5=(e,n,o,s)=>{const u=e._zod.def,p=u.in._zod.traits.has("$ZodTransform"),d=n.io==="input"?p?u.out:u.in:u.out;Ye(d,n,s);const m=n.seen.get(e);m.ref=d},z5=(e,n,o,s)=>{const u=e._zod.def;Ye(u.innerType,n,s);const p=n.seen.get(e);p.ref=u.innerType,o.readOnly=!0},jv=(e,n,o,s)=>{const u=e._zod.def;Ye(u.innerType,n,s);const p=n.seen.get(e);p.ref=u.innerType},T5=j("ZodISODateTime",(e,n)=>{z8.init(e,n),Ve.init(e,n)});function O(e){return Nw(T5,e)}const C5=j("ZodISODate",(e,n)=>{T8.init(e,n),Ve.init(e,n)});function R5(e){return Aw(C5,e)}const B5=j("ZodISOTime",(e,n)=>{C8.init(e,n),Ve.init(e,n)});function P5(e){return Ow(B5,e)}const N5=j("ZodISODuration",(e,n)=>{R8.init(e,n),Ve.init(e,n)});function A5(e){return jw(N5,e)}const O5=(e,n)=>{vv.init(e,n),e.name="ZodError",Object.defineProperties(e,{format:{value:o=>y_(e,o)},flatten:{value:o=>g_(e,o)},addIssue:{value:o=>{e.issues.push(o),e.message=JSON.stringify(e.issues,su,2)}},addIssues:{value:o=>{e.issues.push(...o),e.message=JSON.stringify(e.issues,su,2)}},isEmpty:{get(){return e.issues.length===0}}})},Ft=j("ZodError",O5,{Parent:Error}),j5=$u(Ft),$5=Lu(Ft),L5=Va(Ft),D5=Wa(Ft),M5=E_(Ft),F5=x_(Ft),U5=I_(Ft),Z5=S_(Ft),q5=k_(Ft),V5=b_(Ft),W5=z_(Ft),H5=T_(Ft),sm=new WeakMap;function li(e,n,o){const s=Object.getPrototypeOf(e);let u=sm.get(s);if(u||(u=new Set,sm.set(s,u)),!u.has(n)){u.add(n);for(const p in o){const d=o[p];Object.defineProperty(s,p,{configurable:!0,enumerable:!1,get(){const m=d.bind(this);return Object.defineProperty(this,p,{configurable:!0,writable:!0,enumerable:!0,value:m}),m},set(m){Object.defineProperty(this,p,{configurable:!0,writable:!0,enumerable:!0,value:m})}})}}}const Fe=j("ZodType",(e,n)=>(De.init(e,n),Object.assign(e["~standard"],{jsonSchema:{input:$a(e,"input"),output:$a(e,"output")}}),e.toJSONSchema=o5(e,{}),e.def=n,e.type=n.type,Object.defineProperty(e,"_def",{value:n}),e.parse=(o,s)=>j5(e,o,s,{callee:e.parse}),e.safeParse=(o,s)=>L5(e,o,s),e.parseAsync=async(o,s)=>$5(e,o,s,{callee:e.parseAsync}),e.safeParseAsync=async(o,s)=>D5(e,o,s),e.spa=e.safeParseAsync,e.encode=(o,s)=>M5(e,o,s),e.decode=(o,s)=>F5(e,o,s),e.encodeAsync=async(o,s)=>U5(e,o,s),e.decodeAsync=async(o,s)=>Z5(e,o,s),e.safeEncode=(o,s)=>q5(e,o,s),e.safeDecode=(o,s)=>V5(e,o,s),e.safeEncodeAsync=async(o,s)=>W5(e,o,s),e.safeDecodeAsync=async(o,s)=>H5(e,o,s),li(e,"ZodType",{check(...o){const s=this.def;return this.clone(Gn(s,{checks:[...s.checks??[],...o.map(u=>typeof u=="function"?{_zod:{check:u,def:{check:"custom"},onattach:[]}}:u)]}),{parent:!0})},with(...o){return this.check(...o)},clone(o,s){return Jn(this,o,s)},brand(){return this},register(o,s){return o.add(this,s),this},refine(o,s){return this.check(LE(o,s))},superRefine(o,s){return this.check(DE(o,s))},overwrite(o){return this.check(io(o))},optional(){return dm(this)},exactOptional(){return kE(this)},nullable(){return pm(this)},nullish(){return dm(pm(this))},nonoptional(o){return BE(this,o)},array(){return N(this)},or(o){return Qn([this,o])},and(o){return wE(this,o)},transform(o){return fm(this,IE(o))},default(o){return TE(this,o)},prefault(o){return RE(this,o)},catch(o){return NE(this,o)},pipe(o){return fm(this,o)},readonly(){return jE(this)},describe(o){const s=this.clone();return Qo.add(s,{description:o}),s},meta(...o){if(o.length===0)return Qo.get(this);const s=this.clone();return Qo.add(s,o[0]),s},isOptional(){return this.safeParse(void 0).success},isNullable(){return this.safeParse(null).success},apply(o){return o(this)}}),Object.defineProperty(e,"description",{get(){return Qo.get(e)?.description},configurable:!0}),e)),$v=j("_ZodString",(e,n)=>{Du.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(s,u,p)=>a5(e,s,u);const o=e._zod.bag;e.format=o.format??null,e.minLength=o.minimum??null,e.maxLength=o.maximum??null,li(e,"_ZodString",{regex(...s){return this.check(Zw(...s))},includes(...s){return this.check(Ww(...s))},startsWith(...s){return this.check(Hw(...s))},endsWith(...s){return this.check(Kw(...s))},min(...s){return this.check(ja(...s))},max(...s){return this.check(Bv(...s))},length(...s){return this.check(Pv(...s))},nonempty(...s){return this.check(ja(1,...s))},lowercase(s){return this.check(qw(s))},uppercase(s){return this.check(Vw(s))},trim(){return this.check(Jw())},normalize(...s){return this.check(Gw(...s))},toLowerCase(){return this.check(Qw())},toUpperCase(){return this.check(Yw())},slugify(){return this.check(Xw())}})}),K5=j("ZodString",(e,n)=>{Du.init(e,n),$v.init(e,n),e.email=o=>e.check(fw(G5,o)),e.url=o=>e.check(Rv(Lv,o)),e.jwt=o=>e.check(Pw(cE,o)),e.emoji=o=>e.check(yw(J5,o)),e.guid=o=>e.check(am(lm,o)),e.uuid=o=>e.check(mw(za,o)),e.uuidv4=o=>e.check(vw(za,o)),e.uuidv6=o=>e.check(hw(za,o)),e.uuidv7=o=>e.check(gw(za,o)),e.nanoid=o=>e.check(_w(Q5,o)),e.guid=o=>e.check(am(lm,o)),e.cuid=o=>e.check(ww(Y5,o)),e.cuid2=o=>e.check(Ew(X5,o)),e.ulid=o=>e.check(xw(eE,o)),e.base64=o=>e.check(Cw(sE,o)),e.base64url=o=>e.check(Rw(lE,o)),e.xid=o=>e.check(Iw(tE,o)),e.ksuid=o=>e.check(Sw(nE,o)),e.ipv4=o=>e.check(kw(rE,o)),e.ipv6=o=>e.check(bw(oE,o)),e.cidrv4=o=>e.check(zw(iE,o)),e.cidrv6=o=>e.check(Tw(aE,o)),e.e164=o=>e.check(Bw(uE,o)),e.datetime=o=>e.check(O(o)),e.date=o=>e.check(R5(o)),e.time=o=>e.check(P5(o)),e.duration=o=>e.check(A5(o))});function i(e){return pw(K5,e)}const Ve=j("ZodStringFormat",(e,n)=>{Me.init(e,n),$v.init(e,n)}),G5=j("ZodEmail",(e,n)=>{y8.init(e,n),Ve.init(e,n)}),lm=j("ZodGUID",(e,n)=>{h8.init(e,n),Ve.init(e,n)}),za=j("ZodUUID",(e,n)=>{g8.init(e,n),Ve.init(e,n)}),Lv=j("ZodURL",(e,n)=>{_8.init(e,n),Ve.init(e,n)});function um(e){return Rv(Lv,e)}const J5=j("ZodEmoji",(e,n)=>{w8.init(e,n),Ve.init(e,n)}),Q5=j("ZodNanoID",(e,n)=>{E8.init(e,n),Ve.init(e,n)}),Y5=j("ZodCUID",(e,n)=>{x8.init(e,n),Ve.init(e,n)}),X5=j("ZodCUID2",(e,n)=>{I8.init(e,n),Ve.init(e,n)}),eE=j("ZodULID",(e,n)=>{S8.init(e,n),Ve.init(e,n)}),tE=j("ZodXID",(e,n)=>{k8.init(e,n),Ve.init(e,n)}),nE=j("ZodKSUID",(e,n)=>{b8.init(e,n),Ve.init(e,n)}),rE=j("ZodIPv4",(e,n)=>{B8.init(e,n),Ve.init(e,n)}),oE=j("ZodIPv6",(e,n)=>{P8.init(e,n),Ve.init(e,n)}),iE=j("ZodCIDRv4",(e,n)=>{N8.init(e,n),Ve.init(e,n)}),aE=j("ZodCIDRv6",(e,n)=>{A8.init(e,n),Ve.init(e,n)}),sE=j("ZodBase64",(e,n)=>{O8.init(e,n),Ve.init(e,n)}),lE=j("ZodBase64URL",(e,n)=>{$8.init(e,n),Ve.init(e,n)}),uE=j("ZodE164",(e,n)=>{L8.init(e,n),Ve.init(e,n)}),cE=j("ZodJWT",(e,n)=>{M8.init(e,n),Ve.init(e,n)}),Dv=j("ZodNumber",(e,n)=>{kv.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(s,u,p)=>s5(e,s,u),li(e,"ZodNumber",{gt(s,u){return this.check(Oa(s,u))},gte(s,u){return this.check(Un(s,u))},min(s,u){return this.check(Un(s,u))},lt(s,u){return this.check(Aa(s,u))},lte(s,u){return this.check(Hr(s,u))},max(s,u){return this.check(Hr(s,u))},int(s){return this.check(Be(s))},safe(s){return this.check(Be(s))},positive(s){return this.check(Oa(0,s))},nonnegative(s){return this.check(Un(0,s))},negative(s){return this.check(Aa(0,s))},nonpositive(s){return this.check(Hr(0,s))},multipleOf(s,u){return this.check(uu(s,u))},step(s,u){return this.check(uu(s,u))},finite(){return this}});const o=e._zod.bag;e.minValue=Math.max(o.minimum??Number.NEGATIVE_INFINITY,o.exclusiveMinimum??Number.NEGATIVE_INFINITY)??null,e.maxValue=Math.min(o.maximum??Number.POSITIVE_INFINITY,o.exclusiveMaximum??Number.POSITIVE_INFINITY)??null,e.isInt=(o.format??"").includes("int")||Number.isSafeInteger(o.multipleOf??.5),e.isFinite=!0,e.format=o.format??null});function pr(e){return $w(Dv,e)}const dE=j("ZodNumberFormat",(e,n)=>{F8.init(e,n),Dv.init(e,n)});function Be(e){return Lw(dE,e)}const pE=j("ZodBoolean",(e,n)=>{U8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>l5(e,o,s)});function Z(e){return Dw(pE,e)}const fE=j("ZodBigInt",(e,n)=>{Z8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(s,u,p)=>u5(e,s),e.gte=(s,u)=>e.check(Un(s,u)),e.min=(s,u)=>e.check(Un(s,u)),e.gt=(s,u)=>e.check(Oa(s,u)),e.gte=(s,u)=>e.check(Un(s,u)),e.min=(s,u)=>e.check(Un(s,u)),e.lt=(s,u)=>e.check(Aa(s,u)),e.lte=(s,u)=>e.check(Hr(s,u)),e.max=(s,u)=>e.check(Hr(s,u)),e.positive=s=>e.check(Oa(BigInt(0),s)),e.negative=s=>e.check(Aa(BigInt(0),s)),e.nonpositive=s=>e.check(Hr(BigInt(0),s)),e.nonnegative=s=>e.check(Un(BigInt(0),s)),e.multipleOf=(s,u)=>e.check(uu(s,u));const o=e._zod.bag;e.minValue=o.minimum??null,e.maxValue=o.maximum??null,e.format=o.format??null}),mE=j("ZodUnknown",(e,n)=>{q8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>d5()});function Kn(){return Fw(mE)}const vE=j("ZodNever",(e,n)=>{V8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>c5(e,o,s)});function Ka(e){return Uw(vE,e)}const hE=j("ZodArray",(e,n)=>{W8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>h5(e,o,s,u),e.element=n.element,li(e,"ZodArray",{min(o,s){return this.check(ja(o,s))},nonempty(o){return this.check(ja(1,o))},max(o,s){return this.check(Bv(o,s))},length(o,s){return this.check(Pv(o,s))},unwrap(){return this.element}})});function N(e,n){return e5(hE,e,n)}const gE=j("ZodObject",(e,n)=>{K8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>g5(e,o,s,u),ze(e,"shape",()=>n.shape),li(e,"ZodObject",{keyof(){return Jt(Object.keys(this._zod.def.shape))},catchall(o){return this.clone({...this._zod.def,catchall:o})},passthrough(){return this.clone({...this._zod.def,catchall:Kn()})},loose(){return this.clone({...this._zod.def,catchall:Kn()})},strict(){return this.clone({...this._zod.def,catchall:Ka()})},strip(){return this.clone({...this._zod.def,catchall:void 0})},extend(o){return d_(this,o)},safeExtend(o){return p_(this,o)},merge(o){return f_(this,o)},pick(o){return u_(this,o)},omit(o){return c_(this,o)},partial(...o){return m_(Uv,this,o[0])},required(...o){return v_(Zv,this,o[0])}})});function g(e,n){const o={type:"object",shape:e??{},...ie(n)};return new gE(o)}const Mv=j("ZodUnion",(e,n)=>{Tv.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>y5(e,o,s,u),e.options=n.options});function Qn(e,n){return new Mv({type:"union",options:e,...ie(n)})}const yE=j("ZodDiscriminatedUnion",(e,n)=>{Mv.init(e,n),G8.init(e,n)});function Fv(e,n,o){return new yE({type:"union",options:n,discriminator:e,...ie(o)})}const _E=j("ZodIntersection",(e,n)=>{J8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>_5(e,o,s,u)});function wE(e,n){return new _E({type:"intersection",left:e,right:n})}const cm=j("ZodRecord",(e,n)=>{Q8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>w5(e,o,s,u),e.keyType=n.keyType,e.valueType=n.valueType});function fe(e,n,o){return!n||!n._zod?new cm({type:"record",keyType:i(),valueType:e,...ie(n)}):new cm({type:"record",keyType:e,valueType:n,...ie(o)})}const cu=j("ZodEnum",(e,n)=>{Y8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(s,u,p)=>p5(e,s,u),e.enum=n.entries,e.options=Object.values(n.entries);const o=new Set(Object.keys(n.entries));e.extract=(s,u)=>{const p={};for(const d of s)if(o.has(d))p[d]=n.entries[d];else throw new Error(`Key ${d} not found in enum`);return new cu({...n,checks:[],...ie(u),entries:p})},e.exclude=(s,u)=>{const p={...n.entries};for(const d of s)if(o.has(d))delete p[d];else throw new Error(`Key ${d} not found in enum`);return new cu({...n,checks:[],...ie(u),entries:p})}});function Jt(e,n){const o=Array.isArray(e)?Object.fromEntries(e.map(s=>[s,s])):e;return new cu({type:"enum",entries:o,...ie(n)})}const EE=j("ZodLiteral",(e,n)=>{X8.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>f5(e,o,s),e.values=new Set(n.values),Object.defineProperty(e,"value",{get(){if(n.values.length>1)throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");return n.values[0]}})});function x(e,n){return new EE({type:"literal",values:Array.isArray(e)?e:[e],...ie(n)})}const xE=j("ZodTransform",(e,n)=>{ew.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>v5(e,o),e._zod.parse=(o,s)=>{if(s.direction==="backward")throw new cv(e.constructor.name);o.addIssue=p=>{if(typeof p=="string")o.issues.push(ai(p,o.value,n));else{const d=p;d.fatal&&(d.continue=!1),d.code??(d.code="custom"),d.input??(d.input=o.value),d.inst??(d.inst=e),o.issues.push(ai(d))}};const u=n.transform(o.value,o);return u instanceof Promise?u.then(p=>(o.value=p,o.fallback=!0,o)):(o.value=u,o.fallback=!0,o)}});function IE(e){return new xE({type:"transform",transform:e})}const Uv=j("ZodOptional",(e,n)=>{Cv.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>jv(e,o,s,u),e.unwrap=()=>e._zod.def.innerType});function dm(e){return new Uv({type:"optional",innerType:e})}const SE=j("ZodExactOptional",(e,n)=>{tw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>jv(e,o,s,u),e.unwrap=()=>e._zod.def.innerType});function kE(e){return new SE({type:"optional",innerType:e})}const bE=j("ZodNullable",(e,n)=>{nw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>E5(e,o,s,u),e.unwrap=()=>e._zod.def.innerType});function pm(e){return new bE({type:"nullable",innerType:e})}const zE=j("ZodDefault",(e,n)=>{rw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>I5(e,o,s,u),e.unwrap=()=>e._zod.def.innerType,e.removeDefault=e.unwrap});function TE(e,n){return new zE({type:"default",innerType:e,get defaultValue(){return typeof n=="function"?n():fv(n)}})}const CE=j("ZodPrefault",(e,n)=>{ow.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>S5(e,o,s,u),e.unwrap=()=>e._zod.def.innerType});function RE(e,n){return new CE({type:"prefault",innerType:e,get defaultValue(){return typeof n=="function"?n():fv(n)}})}const Zv=j("ZodNonOptional",(e,n)=>{iw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>x5(e,o,s,u),e.unwrap=()=>e._zod.def.innerType});function BE(e,n){return new Zv({type:"nonoptional",innerType:e,...ie(n)})}const PE=j("ZodCatch",(e,n)=>{aw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>k5(e,o,s,u),e.unwrap=()=>e._zod.def.innerType,e.removeCatch=e.unwrap});function NE(e,n){return new PE({type:"catch",innerType:e,catchValue:typeof n=="function"?n:()=>n})}const AE=j("ZodPipe",(e,n)=>{sw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>b5(e,o,s,u),e.in=n.in,e.out=n.out});function fm(e,n){return new AE({type:"pipe",in:e,out:n})}const OE=j("ZodReadonly",(e,n)=>{lw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>z5(e,o,s,u),e.unwrap=()=>e._zod.def.innerType});function jE(e){return new OE({type:"readonly",innerType:e})}const $E=j("ZodCustom",(e,n)=>{uw.init(e,n),Fe.init(e,n),e._zod.processJSONSchema=(o,s,u)=>m5(e,o)});function LE(e,n={}){return t5($E,e,n)}function DE(e,n){return n5(e,n)}function E(e){return Mw(fE,e)}const ME=g({MaxMessageLength:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SupportsAttachments:Z(),SupportsChildConversations:Z()}),ui=g({account_id:i(),provider:i()});g({dir:i().optional(),name:i().min(1),provider:i().min(1),scope:i().optional()});g({agent:i(),status:i()});const FE=g({agent_id:i(),parent_tool_use_id:i()});g({dir:i().optional(),env:fe(i(),i()).optional(),name:i().optional(),scope:i().optional(),suspended:Z().optional(),tmux_alias:i().optional(),work_dir:i().optional()});g({agent:i(),bytes:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),prompt:i()});g({provider:i().optional(),scope:i().optional(),suspended:Z().optional()});g({provider:i().optional(),scope:i().optional(),suspended:Z().optional()});const UE=g({dir:i().optional(),is_pool:Z().optional(),name:i(),origin:i(),provider:i().optional(),scope:i().optional(),suspended:Z()}),ZE=g({acp_args:N(i()).optional(),acp_command:i().optional(),args:N(i()).nullish(),command:i().optional(),display_name:i().optional(),env:fe(i(),i()).optional(),origin:i(),prompt_flag:i().optional(),prompt_mode:i().optional(),ready_delay_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});g({event_cursor:i(),request_id:i(),status:i()});g({event_cursor:i(),request_id:i()});g({assignee:i().optional()});g({reason:i().max(1024).optional()});g({assignee:i().optional(),description:i().optional(),labels:N(i()).nullish(),metadata:fe(i(),i()).optional(),parent:i().optional(),priority:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),rig:i().optional(),title:i().min(1),type:i().optional()});g({assignee:i().optional(),description:i().optional(),labels:N(i()).nullish(),metadata:fe(i(),i()).optional(),parent:i().nullish(),priority:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),remove_labels:N(i()).nullish(),status:i().optional(),title:i().optional(),type:i().optional()});const qE=Jt(["active","ended"]),Mu=g({conversation_id:i(),provider:i(),session_id:i()});g({bootstrap_profile:Jt(["k8s-cell","kubernetes","kubernetes-cell","single-host-compat"]).optional(),dir:i().min(1),provider:i().min(1).optional(),start_command:i().optional()});const Fu=g({name:i(),path:i(),request_id:i()});g({agent_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),name:i(),path:i(),provider:i().optional(),rig_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_template:i().optional(),suspended:Z(),uptime_sec:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:i().optional()});const VE=g({error:i().optional(),name:i(),path:i(),phases_completed:N(i()).nullish(),running:Z(),status:i().optional()}),ci=g({name:i(),path:i()});g({suspended:Z().optional()});const Uu=g({name:i(),path:i(),request_id:i()}),WE=g({dir:i().optional(),is_pool:Z().optional(),name:i(),provider:i().optional(),scope:i().optional(),suspended:Z()}),HE=g({agents:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),providers:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),rigs:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({agents:N(UE).nullable(),patches:HE,providers:fe(i(),ZE)});const KE=g({agent_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),provider_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),rig_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),GE=g({name:i(),path:i(),prefix:i().optional(),suspended:Z()});g({errors:N(i()).nullable(),valid:Z(),warnings:N(i()).nullable()});g({GroupID:i(),Handle:i(),ID:i(),Metadata:fe(i(),i()),Public:Z(),SessionID:i()});const JE=Jt(["dm","room","thread"]),Qt=g({account_id:i(),conversation_id:i(),kind:JE,parent_conversation_id:i().optional(),provider:i(),scope_id:i()});g({items:N(i()).nullish()});g({closed:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),complete:Z(),convoy_id:i(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({items:N(i()).nullish(),rig:i().optional(),title:i().min(1)});const QE=g({closed:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({items:N(i()).nullish()});const YE=g({BindingGeneration:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),Conversation:Qt,ID:i(),LastMessageID:i(),LastPublishedAt:O({offset:!0}),Metadata:fe(i(),i()),SchemaVersion:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SessionID:i(),SourceSessionID:i()}),XE=g({depends_on_id:i(),issue_id:i(),type:i()}),fr=g({assignee:i().optional(),created_at:O({offset:!0}),dependencies:N(XE).nullish(),description:i().optional(),ephemeral:Z().optional(),from:i().optional(),id:i(),issue_type:i(),labels:N(i()).nullish(),metadata:fe(i(),i()).optional(),needs:N(i()).nullish(),parent:i().optional(),priority:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullish(),ref:i().optional(),status:i(),title:i(),updated_at:O({offset:!0}).optional()});g({children:N(fr).nullable()});const hr=g({bead:fr});g({children:N(fr).nullish(),convoy:fr.optional(),progress:QE.optional()});const ex=g({location:i().optional(),message:i().optional(),value:Kn().optional()});g({detail:i().optional(),errors:N(ex).nullish(),instance:um().optional(),status:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),title:i().optional(),type:um().optional().default("about:blank")});g({status:i()});g({actor:i().min(1),message:i().optional(),subject:i().optional(),type:i().min(1)});const tx=g({seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),ts:O({offset:!0}),type:i()}),nx=g({compression_status:Jt(["pending","complete"]),first_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),last_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),path:i()});g({anchor_event:tx.optional(),archive:nx.optional(),reason:i().optional(),rotated:Z()});g({account_id:i().min(1),callback_url:i().optional(),capabilities:ME.optional(),name:i().optional(),provider:i().min(1)});g({account_id:i(),name:i(),provider:i(),status:i()});g({account_id:i().min(1),provider:i().min(1)});g({conversation:Qt.optional(),metadata:fe(i(),i()).optional(),session_id:i().min(1)});g({default_handle:i().optional(),metadata:fe(i(),i()).optional(),mode:i().optional(),root_conversation:Qt.optional()});g({conversation:Qt.optional(),idempotency_key:i().optional(),reply_to_message_id:i().optional(),session_id:i().min(1),text:i().optional()});g({group_id:i().min(1),handle:i().min(1)});g({group_id:i().min(1),handle:i().min(1),metadata:fe(i(),i()).optional(),public:Z().optional(),session_id:i().min(1)});g({conversation:Qt.optional(),sequence:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),session_id:i().min(1)});g({conversation:Qt.optional(),session_id:i().min(1)});const qv=g({display_name:i(),id:i(),is_bot:Z()}),Vv=g({mime_type:i(),provider_id:i(),url:i()}),Wv=g({actor:qv,attachments:N(Vv).nullish(),conversation:Qt,dedup_key:i().optional(),explicit_target:i().optional(),provider_message_id:i(),received_at:O({offset:!0}),reply_to_message_id:i().optional(),text:i()});g({account_id:i().optional(),message:Wv.optional(),payload:i().optional(),provider:i().optional()});const rx=g({account_id:i(),name:i(),provider:i()}),ox=g({AllowUntargetedPublication:Z(),Enabled:Z(),MaxPeerTriggeredPublishes:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),MaxTotalPeerDeliveries:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({DefaultHandle:i(),FanoutPolicy:ox,ID:i(),LastAddressedHandle:i(),Metadata:fe(i(),i()),Mode:i(),RootConversation:Qt,SchemaVersion:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({scope_kind:i().optional(),scope_ref:i().optional(),target:i().min(1),vars:fe(i(),i()).optional()});const Hv=g({from:i(),kind:i().optional(),to:i()}),ix=g({id:i(),kind:i(),scope_ref:i().optional(),title:i()}),ax=g({edges:N(Hv).nullable(),nodes:N(ix).nullable()}),Kv=g({started_at:i(),status:i(),target:i(),updated_at:i(),workflow_id:i()});g({formula:i(),partial:Z(),partial_errors:N(i()).nullish(),recent_runs:N(Kv).nullable(),run_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const sx=g({assignee:i().optional(),id:i(),kind:i(),labels:N(i()).nullish(),metadata:fe(i(),i()).optional(),title:i(),type:i().optional()}),Gv=g({default:Kn().optional(),description:i().optional(),enum:N(i()).nullish(),name:i(),pattern:i().optional(),required:Z().optional(),type:i()});g({deps:N(Hv).nullable(),description:i(),name:i(),preview:ax,steps:N(sx).nullable(),var_defs:N(Gv).nullable(),version:i()});const lx=g({description:i(),name:i(),recent_runs:N(Kv).nullable(),run_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),var_defs:N(Gv).nullable(),version:i()});g({items:N(lx).nullable(),partial:Z(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const ux=g({ahead:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),behind:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),branch:i(),changed_files:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),clean:Z()}),Zu=g({conversation_id:i(),mode:i(),provider:i()}),cx=g({Match:i(),TargetSessionID:i(),UpdateCursor:Z()});g({city:i().optional(),status:i(),uptime_sec:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:i().optional()});const ao=g({timestamp:i()}),qu=g({actor:i(),conversation_id:i(),provider:i(),target_session:i()});g({items:N(fr).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({items:N(rx).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const dx=fe(i(),Ka());g({partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),unread:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({body:i().optional(),from:i().optional(),subject:i().optional()});g({body:i().optional(),from:i().optional(),rig:i().optional(),subject:i().min(1),to:i().min(1)});const Jv=g({body:i(),cc:N(i()).nullish(),created_at:O({offset:!0}),from:i(),id:i(),priority:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),read:Z(),reply_to:i().optional(),rig:i().optional(),subject:i(),thread_id:i().optional(),to:i()}),vt=g({message:Jv.optional(),rig:i()});g({items:N(Jv).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Qv=g({attached_bead_id:i().optional(),bead_id:i().optional(),detail_available:Z().optional(),id:i(),logical_bead_id:i().optional(),root_bead_id:i().optional(),root_store_ref:i().optional(),run_detail_available:Z().optional(),scope_kind:i(),scope_ref:i(),started_at:i(),status:i(),store_ref:i().optional(),target:i(),title:i(),type:i(),updated_at:i(),workflow_id:i().optional()});g({items:N(Qv).nullable(),partial:Z(),partial_errors:N(i()).nullish()});const ve=fe(i(),Ka());g({status:i()});g({id:i().optional(),status:i()});const px=g({label:i(),value:i()}),fx=g({due:Z(),last_run:i().optional(),last_run_outcome:i().optional(),name:i(),reason:i(),rig:i().optional(),scoped_name:i()});g({checks:N(fx).nullable()});g({bead_id:i(),created_at:i(),labels:N(i()).nullable(),output:i(),store_ref:i()});const mx=g({bead_id:i(),capture_output:Z(),created_at:i(),duration_ms:i().optional(),error:i().optional(),exit_code:i().optional(),has_output:Z(),labels:N(i()).nullable(),name:i(),rig:i().optional(),scoped_name:i(),signal:i().optional(),store_ref:i(),wisp_root_id:i().optional()});g({entries:N(mx).nullable()});const vx=g({capture_output:Z(),check:i().optional(),description:i().optional(),enabled:Z(),exec:i().optional(),formula:i().optional(),gate:i().optional(),interval:i().optional(),name:i(),on:i().optional(),pool:i().optional(),rig:i().optional(),schedule:i().optional(),scoped_name:i(),timeout:i().optional(),timeout_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),trigger:i().optional(),type:i()});g({orders:N(vx).nullable()});g({items:N(Qv).nullable(),partial:Z(),partial_errors:N(i()).nullish()});const Vu=g({conversation_id:i(),message_id:i(),provider:i(),session:i()}),Wu=g({role:i(),text:i(),timestamp:i().optional()}),hx=g({name:i(),path:i().optional(),ref:i().optional(),source:i().optional()});g({packs:N(hx).nullable()});const Ga=g({has_older_messages:Z(),returned_message_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total_compactions:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total_message_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),truncated_before_message:i().optional()}),Yv=g({agent:i(),format:i(),pagination:Ga.optional(),turns:N(Wu).nullable()});g({agent_patch:i().optional(),provider_patch:i().optional(),rig_patch:i().optional(),status:i()});g({agent_patch:i().optional(),provider_patch:i().optional(),rig_patch:i().optional(),status:i()});const Hu=g({kind:i(),metadata:fe(i(),i()).optional(),options:N(i()).nullish(),prompt:i().optional(),request_id:i()}),gx=g({Check:i().nullable(),DrainTimeout:i().nullable(),Max:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),Min:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),OnBoot:i().nullable(),OnDeath:i().nullable()}),yx=g({AppendFragments:N(i()).nullable(),Attach:Z().nullable(),DefaultSlingFormula:i().nullable(),DependsOn:N(i()).nullable(),Dir:i(),Env:fe(i(),i()),EnvRemove:N(i()).nullable(),HooksInstalled:Z().nullable(),IdleTimeout:i().nullable(),InjectAssignedSkills:Z().nullable(),InjectFragments:N(i()).nullable(),InjectFragmentsAppend:N(i()).nullable(),InstallAgentHooks:N(i()).nullable(),InstallAgentHooksAppend:N(i()).nullable(),Lifecycle:i().nullable(),MCP:N(i()).nullable(),MCPAppend:N(i()).nullable(),MaxActiveSessions:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),MaxSessionAge:i().nullable(),MaxSessionAgeJitter:i().nullable(),MinActiveSessions:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),MouseMode:i().nullable(),Name:i(),Nudge:i().nullable(),OptionDefaults:fe(i(),i()),OverlayDir:i().nullable(),Pool:gx,PreStart:N(i()).nullable(),PreStartAppend:N(i()).nullable(),PromptTemplate:i().nullable(),Provider:i().nullable(),ResumeCommand:i().nullable(),ScaleCheck:i().nullable(),Scope:i().nullable(),Session:i().nullable(),SessionLive:N(i()).nullable(),SessionLiveAppend:N(i()).nullable(),SessionSetup:N(i()).nullable(),SessionSetupAppend:N(i()).nullable(),SessionSetupScript:i().nullable(),Skills:N(i()).nullable(),SkillsAppend:N(i()).nullable(),SleepAfterIdle:i().nullable(),StartCommand:i().nullable(),Suspended:Z().nullable(),TmuxAlias:i().nullable(),WakeMode:i().nullable(),WorkDir:i().nullable()});g({items:N(yx).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Ku=g({host:i(),port:i(),scope_kind:i(),scope_name:i(),source:i(),user:i()}),Gu=g({layer:i(),new_id:i(),old_id:i().optional(),scope_root:i(),source:i()});g({acp_args:N(i()).nullish(),acp_command:i().optional(),args:N(i()).nullish(),args_append:N(i()).nullish(),base:i().optional(),command:i().optional(),display_name:i().optional(),env:fe(i(),i()).optional(),name:i().min(1),option_defaults:fe(i(),i()).optional(),options_schema_merge:i().optional(),prompt_flag:i().optional(),prompt_mode:i().optional(),ready_delay_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});g({provider:i(),status:i()});const _x=g({choices:N(px).nullable(),default:i(),key:i(),label:i(),type:i()}),wx=g({ACPArgs:N(i()).nullable(),ACPCommand:i().nullable(),AcceptStartupDialogs:Z().nullable(),Args:N(i()).nullable(),ArgsAppend:N(i()).nullable(),Base:i().nullable(),Command:i().nullable(),Env:fe(i(),i()),EnvRemove:N(i()).nullable(),Name:i(),OptionsSchemaMerge:i().nullable(),PromptFlag:i().nullable(),PromptMode:i().nullable(),ReadyDelayMs:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).nullable(),Replace:Z()});g({items:N(wx).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({accept_startup_dialogs:Z().optional(),acp_args:N(i()).nullish(),acp_command:i().optional(),args:N(i()).nullish(),command:i().optional(),env:fe(i(),i()).optional(),name:i().optional(),prompt_flag:i().optional(),prompt_mode:i().optional(),ready_delay_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});const Ex=g({builtin:Z(),city_level:Z(),display_name:i().optional(),effective_defaults:fe(i(),i()).optional(),name:i(),options_schema:N(_x).nullish()});g({items:N(Ex).nullable(),next_cursor:i().optional(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const xx=g({detail:i().optional(),display_name:i(),status:i()});g({providers:fe(i(),xx)});const Ix=g({acp_args:N(i()).optional(),acp_command:i().optional(),args:N(i()).nullish(),builtin:Z(),city_level:Z(),command:i().optional(),display_name:i().optional(),env:fe(i(),i()).optional(),name:i(),prompt_flag:i().optional(),prompt_mode:i().optional(),ready_delay_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});g({items:N(Ix).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Sx=g({acp_args:N(i()).optional(),acp_command:i().optional(),args:N(i()).nullish(),command:i().optional(),display_name:i().optional(),env:fe(i(),i()).optional(),prompt_flag:i().optional(),prompt_mode:i().optional(),ready_delay_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});g({acp_args:N(i()).nullish(),acp_command:i().optional(),args:N(i()).nullish(),args_append:N(i()).nullish(),base:i().optional(),command:i().optional(),display_name:i().optional(),env:fe(i(),i()).optional(),option_defaults:fe(i(),i()).optional(),options_schema_merge:i().optional(),prompt_flag:i().optional(),prompt_mode:i().optional(),ready_delay_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()});const kx=g({Conversation:Qt,Delivered:Z(),FailureKind:i(),MessageID:i(),Metadata:fe(i(),i()),RetryAfter:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),bx=g({detail:i().optional(),display_name:i(),kind:i(),name:i(),status:i()});g({items:fe(i(),bx)});const Ju=g({error_code:i(),error_message:i(),operation:Jt(["city.create","city.unregister","session.create","session.message","session.submit"]),request_id:i()});g({action:i(),failed:N(i()).nullish(),killed:N(i()).nullish(),rig:i(),status:i()});g({default_branch:i().optional(),name:i().min(1),path:i().min(1),prefix:i().optional()});g({rig:i(),status:i()});const zx=g({DefaultBranch:i().nullable(),FormulaVars:fe(i(),i()),Name:i(),Path:i().nullable(),Prefix:i().nullable(),Suspended:Z().nullable()});g({items:N(zx).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({default_branch:i().optional(),name:i().optional(),path:i().optional(),prefix:i().optional(),suspended:Z().optional()});const Tx=g({agent_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),default_branch:i().optional(),git:ux.optional(),last_activity:O({offset:!0}).optional(),name:i(),path:i(),prefix:i().optional(),running_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),suspended:Z()});g({items:N(Tx).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({default_branch:i().optional(),path:i().optional(),prefix:i().optional(),suspended:Z().optional()});const Qu=g({prior_archive:i(),prior_first_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),prior_last_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),Cx=fe(i(),Ka());g({action:i(),service:i(),status:i()});const Xv=g({activity:i()});g({messages:N(Kn()).nullable(),status:i().optional()});g({agents:N(FE).nullable()});const Yu=g({BindingGeneration:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),BoundAt:O({offset:!0}),Conversation:Qt,ExpiresAt:O({offset:!0}).nullable(),ID:i(),Metadata:fe(i(),i()),SchemaVersion:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SessionID:i(),Status:qE});g({unbound:N(Yu).nullable()});g({items:N(Yu).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({alias:i().optional(),async:Z().optional(),kind:i().optional(),message:i().optional(),name:i().optional(),options:fe(i(),i()).optional(),project_id:i().optional(),session_name:i().optional(),title:i().optional()});const Xu=g({bead_id:i(),bead_status:i().optional(),reason:i().optional(),session_id:i(),template:i().optional()}),Rx=g({attached:Z(),last_activity:O({offset:!0}).optional(),name:i()}),Bx=g({active_bead:i().optional(),activity:i().optional(),available:Z(),context_pct:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),context_window:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),description:i().optional(),display_name:i().optional(),last_output:i().optional(),model:i().optional(),name:i(),pool:i().optional(),provider:i().optional(),rig:i().optional(),running:Z(),session:Rx.optional(),state:i(),suspended:Z(),unavailable_reason:i().optional()});g({items:N(Bx).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const gr=g({reason:i().optional(),session_id:i(),template:i().optional()});g({message:i().min(1).regex(/\S/)});const ec=g({request_id:i(),session_id:i()});g({alias:i().optional(),title:i().min(1).optional()});g({pending:Hu.optional(),supported:Z()});g({permission_mode:i().min(1).regex(/\S/)});const eh=Kn();g({title:i().min(1)});g({action:i().min(1),metadata:fe(i(),i()).optional(),request_id:i().optional(),text:i().optional()});g({id:i(),status:i()});Qn([Xv,Hu,ao]);const Px=g({format:i(),id:i(),pagination:Ga.optional(),provider:i(),template:i(),turns:N(Wu).nullable()}),Nx=g({format:i(),id:i(),messages:N(eh).nullable(),pagination:Ga.optional(),provider:i(),template:i()}),tc=g({intent:i(),queued:Z(),request_id:i(),session_id:i()});g({format:i(),id:i(),messages:N(eh).nullish(),pagination:Ga.optional(),provider:i(),template:i(),turns:N(Wu).nullish()});g({attached_bead_id:i().optional(),bead:i().optional(),force:Z().optional(),formula:i().optional(),rig:i().optional(),scope_kind:i().optional(),scope_ref:i().optional(),target:i().min(1),title:i().optional(),vars:fe(i(),i()).optional()});g({attached_bead_id:i().optional(),bead:i().optional(),formula:i().optional(),mode:i().optional(),root_bead_id:i().optional(),status:i(),target:i(),warnings:N(i()).nullish(),workflow_id:i().optional()});const Ax=g({allow_websockets:Z().optional(),hostname:i().optional(),kind:i().optional(),local_state:i(),mount_path:i(),publication_state:i(),publish_mode:i(),reason:i().optional(),service_name:i(),state:i().optional(),state_root:i(),updated_at:O({offset:!0}),url:i().optional(),visibility:i().optional(),workflow_contract:i().optional()});g({items:N(Ax).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const Ox=g({quarantined:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),running:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),suspended:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),jx=g({draining:Z().optional(),expanded:Z().optional(),group_name:i().optional(),name:i(),qualified_name:i(),running:Z(),scale_label:i().optional(),scope:i(),session_name:i().optional(),suspended:Z()}),$x=g({total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),unread:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),Lx=g({identity:i(),mode:i(),status:i()}),Dx=g({suspended:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),Mx=g({name:i(),path:i(),suspended:Z()}),Fx=g({active:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),suspended:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})}),Ux=g({last_gc_at:i().optional(),last_gc_status:i().optional(),live_rows:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),path:i(),ratio_mb_per_row:pr(),size_bytes:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),threshold_mb_per_row:pr(),warning:Z()}),Zx=g({in_progress:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),open:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),ready:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({agent_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),agent_details:N(jx).nullish(),agents:Ox,mail:$x,name:i(),named_session_details:N(Lx).nullish(),partial:Z().optional(),partial_errors:N(i()).nullish(),path:i(),rig_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),rig_details:N(Mx).nullish(),rigs:Dx,running:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_counts_detail:Fx.optional(),store_health:Ux.optional(),suspended:Z(),uptime_sec:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:i().optional(),work:Zx});const nc=g({after_bytes:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),before_bytes:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),duration_s:pr(),snapshot_path:i()}),rc=g({duration_s:pr(),error_msg:i(),snapshot_path:i().optional(),stage:i()}),qx=g({supports_follow_up:Z(),supports_interrupt_now:Z()}),th=g({active_bead:i().optional(),activity:i().optional(),agent_kind:i().optional(),alias:i().optional(),attached:Z(),configured_named_session:Z().optional(),context_pct:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),context_window:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),created_at:i(),display_name:i().optional(),id:i(),kind:i().optional(),last_active:i().optional(),last_nudge_delivered_at:i().optional(),last_output:i().optional(),metadata:fe(i(),i()).optional(),model:i().optional(),options:fe(i(),i()).optional(),pool:i().optional(),provider:i(),reason:i().optional(),rig:i().optional(),running:Z(),session_name:i(),state:i(),submission_capabilities:qx.optional(),template:i(),title:i()});g({items:N(th).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const oc=g({request_id:i(),session:th}),Vx=Jt(["default","follow_up","interrupt_now"]);g({intent:Vx.optional(),message:i().min(1).regex(/\S/)});g({items:N(VE).nullable(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const ic=g({avg60:pr(),consecutive_skips:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),max_consecutive_skips:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),outcome:i(),threshold:pr(),trigger:i().optional()}),ac=g({client_addr:i().optional(),mode:Jt(["destructive","preserve_sessions","unknown"]),signal:i().optional(),source:Jt(["signal","socket_stop"])}),Wx=g({phase:i().optional(),phases_completed:N(i()).nullish(),ready:Z()});g({build_id:i().optional(),cities_running:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),cities_total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),startup:Wx.optional(),status:i(),uptime_sec:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),version:i()});const Hx=Jt(["inbound","outbound"]),Kx=Jt(["live","hydrated"]),sc=g({Actor:qv,Attachments:N(Vv).nullable(),Conversation:Qt,CreatedAt:O({offset:!0}),ExplicitTarget:i(),ID:i(),Kind:Hx,Metadata:fe(i(),i()),Provenance:Kx,ProviderMessageID:i(),ReplyToMessageID:i(),SchemaVersion:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),Sequence:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),SourceSessionID:i(),Text:i()});g({Binding:Yu,GroupRoute:cx,Message:Wv,TargetSessionID:i(),TranscriptEntry:sc});g({items:N(sc).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({DeliveryContext:YE,Receipt:kx,TranscriptEntry:sc});const lc=g({count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i()}),uc=g({agent_name:i().optional(),bead_id:i().optional(),cache_creation_tokens:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),cache_read_tokens:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),completion_tokens:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),cost_usd_estimate:pr().optional(),delivered:Z().optional(),duration_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),error:i().optional(),finished_at:O({offset:!0}),latency_ms:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),model:i().optional(),op_id:i(),operation:i(),prompt_sha:i().optional(),prompt_tokens:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),prompt_version:i().optional(),provider:i().optional(),queued:Z().optional(),result:i(),session_id:i().optional(),session_name:i().optional(),started_at:O({offset:!0}),template:i().optional(),transport:i().optional()}),nh=Qn([ui,hr,Mu,Fu,ci,Uu,Zu,qu,vt,ve,Vu,Ku,Gu,Ju,Qu,oc,Xu,gr,ec,tc,nc,rc,ic,ac,lc,uc]),Gx=g({active_attempt:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),attempt_count:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),max_attempts:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional()}),rh=g({assignee:i().optional(),attempt:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),id:i(),kind:i(),logical_bead_id:i().optional(),metadata:fe(i(),i()),scope_ref:i().optional(),status:i(),step_ref:i().optional(),title:i()});g({closed:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),deleted:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),partial:Z().optional(),partial_errors:N(i()).nullish(),workflow_id:i()});const du=g({from:i(),kind:i().optional(),to:i()});g({beads:N(fr).nullable(),deps:N(du).nullable(),root:fr});const L=g({attempt_summary:Gx.optional(),bead:rh,changed_fields:N(i()).nullable(),event_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),event_ts:i(),event_type:i(),logical_node_id:i(),requires_resync:Z().optional(),root_bead_id:i(),root_store_ref:i(),scope_kind:i(),scope_ref:i(),type:i(),watch_generation:i(),workflow_id:i(),workflow_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({actor:i(),message:i().optional(),payload:nh.optional(),run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:i(),workflow:L.optional()});g({actor:i(),city:i(),message:i().optional(),payload:nh.optional(),run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:i(),workflow:L.optional()});const Jx=g({actor:i(),message:i().optional(),payload:hr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("bead.closed"),workflow:L.optional()}),Qx=g({actor:i(),message:i().optional(),payload:hr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("bead.created"),workflow:L.optional()}),Yx=g({actor:i(),message:i().optional(),payload:hr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("bead.updated"),workflow:L.optional()}),Xx=g({actor:i(),message:i().optional(),payload:ci,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.created"),workflow:L.optional()}),e4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.resumed"),workflow:L.optional()}),t4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.suspended"),workflow:L.optional()}),n4=g({actor:i(),message:i().optional(),payload:ci,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.unregister_requested"),workflow:L.optional()}),r4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("controller.started"),workflow:L.optional()}),o4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("controller.stopped"),workflow:L.optional()}),i4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("convoy.closed"),workflow:L.optional()}),a4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("convoy.created"),workflow:L.optional()}),s4=g({actor:i(),message:i().optional(),payload:Kn(),run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:i(),workflow:L.optional()}),l4=g({actor:i(),message:i().optional(),payload:Qu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("events.rotated"),workflow:L.optional()}),u4=g({actor:i(),message:i().optional(),payload:ui,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.adapter_added"),workflow:L.optional()}),c4=g({actor:i(),message:i().optional(),payload:ui,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.adapter_removed"),workflow:L.optional()}),d4=g({actor:i(),message:i().optional(),payload:Mu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.bound"),workflow:L.optional()}),p4=g({actor:i(),message:i().optional(),payload:Zu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.group_created"),workflow:L.optional()}),f4=g({actor:i(),message:i().optional(),payload:qu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.inbound"),workflow:L.optional()}),m4=g({actor:i(),message:i().optional(),payload:Vu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.outbound"),workflow:L.optional()}),v4=g({actor:i(),message:i().optional(),payload:lc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.unbound"),workflow:L.optional()}),h4=g({actor:i(),message:i().optional(),payload:nc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("gc.store.maintenance.done"),workflow:L.optional()}),g4=g({actor:i(),message:i().optional(),payload:rc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("gc.store.maintenance.failed"),workflow:L.optional()}),y4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.archived"),workflow:L.optional()}),_4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.deleted"),workflow:L.optional()}),w4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.marked_read"),workflow:L.optional()}),E4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.marked_unread"),workflow:L.optional()}),x4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.read"),workflow:L.optional()}),I4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.replied"),workflow:L.optional()}),S4=g({actor:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.sent"),workflow:L.optional()}),k4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("order.completed"),workflow:L.optional()}),b4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("order.failed"),workflow:L.optional()}),z4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("order.fired"),workflow:L.optional()}),T4=g({actor:i(),message:i().optional(),payload:Ku,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("pg.credential_resolved"),workflow:L.optional()}),C4=g({actor:i(),message:i().optional(),payload:Gu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("project.identity.stamped"),workflow:L.optional()}),R4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("provider.swapped"),workflow:L.optional()}),B4=g({actor:i(),message:i().optional(),payload:Ju,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.failed"),workflow:L.optional()}),P4=g({actor:i(),message:i().optional(),payload:Fu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.city.create"),workflow:L.optional()}),N4=g({actor:i(),message:i().optional(),payload:Uu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.city.unregister"),workflow:L.optional()}),A4=g({actor:i(),message:i().optional(),payload:oc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.session.create"),workflow:L.optional()}),O4=g({actor:i(),message:i().optional(),payload:ec,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.session.message"),workflow:L.optional()}),j4=g({actor:i(),message:i().optional(),payload:tc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.session.submit"),workflow:L.optional()}),$4=g({actor:i(),message:i().optional(),payload:gr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.crashed"),workflow:L.optional()}),L4=g({actor:i(),message:i().optional(),payload:Xu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.drain_acked_with_assigned_work"),workflow:L.optional()}),D4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.draining"),workflow:L.optional()}),M4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.idle_killed"),workflow:L.optional()}),F4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.max_age_killed"),workflow:L.optional()}),U4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.quarantined"),workflow:L.optional()}),Z4=g({actor:i(),message:i().optional(),payload:gr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.stopped"),workflow:L.optional()}),q4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.stranded"),workflow:L.optional()}),V4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.suspended"),workflow:L.optional()}),W4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.undrained"),workflow:L.optional()}),H4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.updated"),workflow:L.optional()}),K4=g({actor:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.woke"),workflow:L.optional()}),G4=g({actor:i(),message:i().optional(),payload:gr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.work_query_failed"),workflow:L.optional()}),J4=g({actor:i(),message:i().optional(),payload:ic,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("supervisor.fs_pressure.skipped_tick"),workflow:L.optional()}),Q4=g({actor:i(),message:i().optional(),payload:ac,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("supervisor.shutdown_requested"),workflow:L.optional()}),Y4=g({actor:i(),message:i().optional(),payload:uc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("worker.operation"),workflow:L.optional()}),oh=Fv("type",[Jx.extend({type:x("bead.closed")}),Qx.extend({type:x("bead.created")}),Yx.extend({type:x("bead.updated")}),Xx.extend({type:x("city.created")}),e4.extend({type:x("city.resumed")}),t4.extend({type:x("city.suspended")}),n4.extend({type:x("city.unregister_requested")}),r4.extend({type:x("controller.started")}),o4.extend({type:x("controller.stopped")}),i4.extend({type:x("convoy.closed")}),a4.extend({type:x("convoy.created")}),l4.extend({type:x("events.rotated")}),u4.extend({type:x("extmsg.adapter_added")}),c4.extend({type:x("extmsg.adapter_removed")}),d4.extend({type:x("extmsg.bound")}),p4.extend({type:x("extmsg.group_created")}),f4.extend({type:x("extmsg.inbound")}),m4.extend({type:x("extmsg.outbound")}),v4.extend({type:x("extmsg.unbound")}),h4.extend({type:x("gc.store.maintenance.done")}),g4.extend({type:x("gc.store.maintenance.failed")}),y4.extend({type:x("mail.archived")}),_4.extend({type:x("mail.deleted")}),w4.extend({type:x("mail.marked_read")}),E4.extend({type:x("mail.marked_unread")}),x4.extend({type:x("mail.read")}),I4.extend({type:x("mail.replied")}),S4.extend({type:x("mail.sent")}),k4.extend({type:x("order.completed")}),b4.extend({type:x("order.failed")}),z4.extend({type:x("order.fired")}),T4.extend({type:x("pg.credential_resolved")}),C4.extend({type:x("project.identity.stamped")}),R4.extend({type:x("provider.swapped")}),B4.extend({type:x("request.failed")}),P4.extend({type:x("request.result.city.create")}),N4.extend({type:x("request.result.city.unregister")}),A4.extend({type:x("request.result.session.create")}),O4.extend({type:x("request.result.session.message")}),j4.extend({type:x("request.result.session.submit")}),$4.extend({type:x("session.crashed")}),L4.extend({type:x("session.drain_acked_with_assigned_work")}),D4.extend({type:x("session.draining")}),M4.extend({type:x("session.idle_killed")}),F4.extend({type:x("session.max_age_killed")}),U4.extend({type:x("session.quarantined")}),Z4.extend({type:x("session.stopped")}),q4.extend({type:x("session.stranded")}),V4.extend({type:x("session.suspended")}),W4.extend({type:x("session.undrained")}),H4.extend({type:x("session.updated")}),K4.extend({type:x("session.woke")}),G4.extend({type:x("session.work_query_failed")}),J4.extend({type:x("supervisor.fs_pressure.skipped_tick")}),Q4.extend({type:x("supervisor.shutdown_requested")}),Y4.extend({type:x("worker.operation")}),s4.extend({type:x("TypedEventStreamEnvelopeCustom")})]);g({items:N(oh).nullable(),next_cursor:i().optional(),partial:Z().optional(),partial_errors:N(i()).nullish(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});const X4=g({actor:i(),city:i(),message:i().optional(),payload:hr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("bead.closed"),workflow:L.optional()}),eI=g({actor:i(),city:i(),message:i().optional(),payload:hr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("bead.created"),workflow:L.optional()}),tI=g({actor:i(),city:i(),message:i().optional(),payload:hr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("bead.updated"),workflow:L.optional()}),nI=g({actor:i(),city:i(),message:i().optional(),payload:ci,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.created"),workflow:L.optional()}),rI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.resumed"),workflow:L.optional()}),oI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.suspended"),workflow:L.optional()}),iI=g({actor:i(),city:i(),message:i().optional(),payload:ci,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("city.unregister_requested"),workflow:L.optional()}),aI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("controller.started"),workflow:L.optional()}),sI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("controller.stopped"),workflow:L.optional()}),lI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("convoy.closed"),workflow:L.optional()}),uI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("convoy.created"),workflow:L.optional()}),cI=g({actor:i(),city:i(),message:i().optional(),payload:Kn(),run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:i(),workflow:L.optional()}),dI=g({actor:i(),city:i(),message:i().optional(),payload:Qu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("events.rotated"),workflow:L.optional()}),pI=g({actor:i(),city:i(),message:i().optional(),payload:ui,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.adapter_added"),workflow:L.optional()}),fI=g({actor:i(),city:i(),message:i().optional(),payload:ui,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.adapter_removed"),workflow:L.optional()}),mI=g({actor:i(),city:i(),message:i().optional(),payload:Mu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.bound"),workflow:L.optional()}),vI=g({actor:i(),city:i(),message:i().optional(),payload:Zu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.group_created"),workflow:L.optional()}),hI=g({actor:i(),city:i(),message:i().optional(),payload:qu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.inbound"),workflow:L.optional()}),gI=g({actor:i(),city:i(),message:i().optional(),payload:Vu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.outbound"),workflow:L.optional()}),yI=g({actor:i(),city:i(),message:i().optional(),payload:lc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("extmsg.unbound"),workflow:L.optional()}),_I=g({actor:i(),city:i(),message:i().optional(),payload:nc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("gc.store.maintenance.done"),workflow:L.optional()}),wI=g({actor:i(),city:i(),message:i().optional(),payload:rc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("gc.store.maintenance.failed"),workflow:L.optional()}),EI=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.archived"),workflow:L.optional()}),xI=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.deleted"),workflow:L.optional()}),II=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.marked_read"),workflow:L.optional()}),SI=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.marked_unread"),workflow:L.optional()}),kI=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.read"),workflow:L.optional()}),bI=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.replied"),workflow:L.optional()}),zI=g({actor:i(),city:i(),message:i().optional(),payload:vt,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("mail.sent"),workflow:L.optional()}),TI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("order.completed"),workflow:L.optional()}),CI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("order.failed"),workflow:L.optional()}),RI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("order.fired"),workflow:L.optional()}),BI=g({actor:i(),city:i(),message:i().optional(),payload:Ku,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("pg.credential_resolved"),workflow:L.optional()}),PI=g({actor:i(),city:i(),message:i().optional(),payload:Gu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("project.identity.stamped"),workflow:L.optional()}),NI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("provider.swapped"),workflow:L.optional()}),AI=g({actor:i(),city:i(),message:i().optional(),payload:Ju,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.failed"),workflow:L.optional()}),OI=g({actor:i(),city:i(),message:i().optional(),payload:Fu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.city.create"),workflow:L.optional()}),jI=g({actor:i(),city:i(),message:i().optional(),payload:Uu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.city.unregister"),workflow:L.optional()}),$I=g({actor:i(),city:i(),message:i().optional(),payload:oc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.session.create"),workflow:L.optional()}),LI=g({actor:i(),city:i(),message:i().optional(),payload:ec,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.session.message"),workflow:L.optional()}),DI=g({actor:i(),city:i(),message:i().optional(),payload:tc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("request.result.session.submit"),workflow:L.optional()}),MI=g({actor:i(),city:i(),message:i().optional(),payload:gr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.crashed"),workflow:L.optional()}),FI=g({actor:i(),city:i(),message:i().optional(),payload:Xu,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.drain_acked_with_assigned_work"),workflow:L.optional()}),UI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.draining"),workflow:L.optional()}),ZI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.idle_killed"),workflow:L.optional()}),qI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.max_age_killed"),workflow:L.optional()}),VI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.quarantined"),workflow:L.optional()}),WI=g({actor:i(),city:i(),message:i().optional(),payload:gr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.stopped"),workflow:L.optional()}),HI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.stranded"),workflow:L.optional()}),KI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.suspended"),workflow:L.optional()}),GI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.undrained"),workflow:L.optional()}),JI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.updated"),workflow:L.optional()}),QI=g({actor:i(),city:i(),message:i().optional(),payload:ve,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.woke"),workflow:L.optional()}),YI=g({actor:i(),city:i(),message:i().optional(),payload:gr,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("session.work_query_failed"),workflow:L.optional()}),XI=g({actor:i(),city:i(),message:i().optional(),payload:ic,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("supervisor.fs_pressure.skipped_tick"),workflow:L.optional()}),eS=g({actor:i(),city:i(),message:i().optional(),payload:ac,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("supervisor.shutdown_requested"),workflow:L.optional()}),tS=g({actor:i(),city:i(),message:i().optional(),payload:uc,run_id:i().optional(),seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),session_id:i().optional(),step_id:i().optional(),subject:i().optional(),ts:O({offset:!0}),type:x("worker.operation"),workflow:L.optional()}),ih=Fv("type",[X4.extend({type:x("bead.closed")}),eI.extend({type:x("bead.created")}),tI.extend({type:x("bead.updated")}),nI.extend({type:x("city.created")}),rI.extend({type:x("city.resumed")}),oI.extend({type:x("city.suspended")}),iI.extend({type:x("city.unregister_requested")}),aI.extend({type:x("controller.started")}),sI.extend({type:x("controller.stopped")}),lI.extend({type:x("convoy.closed")}),uI.extend({type:x("convoy.created")}),dI.extend({type:x("events.rotated")}),pI.extend({type:x("extmsg.adapter_added")}),fI.extend({type:x("extmsg.adapter_removed")}),mI.extend({type:x("extmsg.bound")}),vI.extend({type:x("extmsg.group_created")}),hI.extend({type:x("extmsg.inbound")}),gI.extend({type:x("extmsg.outbound")}),yI.extend({type:x("extmsg.unbound")}),_I.extend({type:x("gc.store.maintenance.done")}),wI.extend({type:x("gc.store.maintenance.failed")}),EI.extend({type:x("mail.archived")}),xI.extend({type:x("mail.deleted")}),II.extend({type:x("mail.marked_read")}),SI.extend({type:x("mail.marked_unread")}),kI.extend({type:x("mail.read")}),bI.extend({type:x("mail.replied")}),zI.extend({type:x("mail.sent")}),TI.extend({type:x("order.completed")}),CI.extend({type:x("order.failed")}),RI.extend({type:x("order.fired")}),BI.extend({type:x("pg.credential_resolved")}),PI.extend({type:x("project.identity.stamped")}),NI.extend({type:x("provider.swapped")}),AI.extend({type:x("request.failed")}),OI.extend({type:x("request.result.city.create")}),jI.extend({type:x("request.result.city.unregister")}),$I.extend({type:x("request.result.session.create")}),LI.extend({type:x("request.result.session.message")}),DI.extend({type:x("request.result.session.submit")}),MI.extend({type:x("session.crashed")}),FI.extend({type:x("session.drain_acked_with_assigned_work")}),UI.extend({type:x("session.draining")}),ZI.extend({type:x("session.idle_killed")}),qI.extend({type:x("session.max_age_killed")}),VI.extend({type:x("session.quarantined")}),WI.extend({type:x("session.stopped")}),HI.extend({type:x("session.stranded")}),KI.extend({type:x("session.suspended")}),GI.extend({type:x("session.undrained")}),JI.extend({type:x("session.updated")}),QI.extend({type:x("session.woke")}),YI.extend({type:x("session.work_query_failed")}),XI.extend({type:x("supervisor.fs_pressure.skipped_tick")}),eS.extend({type:x("supervisor.shutdown_requested")}),tS.extend({type:x("worker.operation")}),cI.extend({type:x("TypedTaggedEventStreamEnvelopeCustom")})]);g({event_cursor:i(),items:N(ih).nullable(),total:E().min(BigInt("-9223372036854775808"),{error:"Invalid value: Expected int64 to be >= -9223372036854775808"}).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"})});g({beads:N(rh).nullable(),deps:N(du).nullable(),logical_edges:N(du).nullable(),logical_nodes:N(dx).nullable(),partial:Z(),resolved_root_store:i(),root_bead_id:i(),root_store_ref:i(),scope_groups:N(Cx).nullable(),scope_kind:i(),scope_ref:i(),snapshot_event_seq:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}).optional(),snapshot_version:E().gte(BigInt(0)).max(BigInt("9223372036854775807"),{error:"Invalid value: Expected int64 to be <= 9223372036854775807"}),stores_scanned:N(i()).nullable(),workflow_id:i()});const nS=g({declared_name:i().optional(),declared_prefix:i().optional(),name:i(),prefix:i().optional(),provider:i().optional(),session_template:i().optional(),suspended:Z()});g({agents:N(WE).nullable(),patches:KE.optional(),providers:fe(i(),Sx).optional(),rigs:N(GE).nullable(),workspace:nS});N(Qn([g({data:ao,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()}),g({data:Yv,event:x("turn"),id:Be().optional(),retry:Be().optional()})]));N(Qn([g({data:ao,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()}),g({data:Yv,event:x("turn"),id:Be().optional(),retry:Be().optional()})]));fe(i(),i());N(Qn([g({data:oh,event:x("event"),id:Be().optional(),retry:Be().optional()}),g({data:ao,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()})]));N(Qn([g({data:Xv,event:x("activity"),id:Be().optional(),retry:Be().optional()}),g({data:ao,event:x("heartbeat"),id:Be().optional(),retry:Be().optional()}),g({data:Nx,event:x("message").optional(),id:Be().optional(),retry:Be().optional()}),g({data:Hu,event:x("pending"),id:Be().optional(),retry:Be().optional()}),g({data:Px,event:x("turn"),id:Be().optional(),retry:Be().optional()})]));N(Qn([g({data:ao,event:x("heartbeat"),id:i().optional(),retry:Be().optional()}),g({data:ih,event:x("tagged_event"),id:i().optional(),retry:Be().optional()})]));class vn extends Error{constructor(n,o,s){super(o),this.status=n,this.requestId=s}status;requestId;name="SupervisorApiError"}async function Ie(e,n){let o;try{o=await e}catch(p){throw rS(p)}const{response:s}=o;if(s===void 0)throw new vn(void 0,pu(o.error),void 0);if(!s.ok||o.error!==void 0)throw new vn(s.status,pu(o.error,s.statusText),s.headers.get("x-gc-request-id")??void 0);const u=o.data;if(u===void 0)throw new vn(s.status,n,s.headers.get("x-gc-request-id")??void 0);return u}function rS(e){return e instanceof vn?e:new vn(void 0,pu(e),void 0)}function pu(e,n="gc supervisor request failed"){if(typeof e=="string"&&e.trim().length>0)return e.trim();if(e instanceof Error&&e.message.trim().length>0)return e.message.trim();if(oS(e))for(const o of["error","message","detail"]){const s=e[o];if(typeof s=="string"&&s.trim().length>0)return s.trim()}return n}function oS(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}const iS="";function aS(){const e=globalThis.location?.origin;return typeof e=="string"&&e.length>0&&e!=="null"?e:iS}function sS(e){if(!e.startsWith("/"))return e;const n=globalThis.location?.origin;return typeof n!="string"||n.length===0||n==="null"?e:new URL(e,n).toString().replace(/\/$/,"")}function mm(e,n,o){const s=e.replace(/\/$/,""),u=new URLSearchParams(o).toString(),p=u.length>0?`${n}?${u}`:n;return s===""?p:s.startsWith("/")?`${s}${p}`:new URL(p,`${s}/`).toString()}const lS=6e4,Bt={"X-GC-Request":"dashboard"};let vm=null;const hm=new Map;function ah(e={}){const n=e.baseUrl??aS(),s={baseUrl:sS(n),headers:{Accept:"application/json"},responseStyle:"fields",throwOnError:!1},u=e.client??uv({...s,fetch:uS(e.fetch??globalThis.fetch,sh(e.timeoutMs))});return{baseUrl:n,health(){return Ie(b3({client:u}),"gc supervisor health response was empty")},cityHealth(p){return Ie(F3({client:u,path:{cityName:p}}),"gc supervisor city health response was empty")},cityStatus(p){return Ie(t_({client:u,path:{cityName:p}}),"gc supervisor status response was empty")},listCities(){return Ie(z3({client:u}),"gc supervisor cities response was empty")},listAgents(p){return Ie(P3({client:u,path:{cityName:p}}),"gc supervisor agents response was empty")},listRigs(p){return Ie(G3({client:u,path:{cityName:p}}),"gc supervisor rigs response was empty")},listBeads(p,d){return Ie(j3({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor beads response was empty")},listEvents(p,d){return Ie(L3({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor events response was empty")},getBead(p,d){return Ie(N3({client:u,path:{cityName:p,id:d}}),"gc supervisor bead response was empty")},createBead(p,d){return Ie($3({client:u,path:{cityName:p},headers:Bt,body:d}),"gc supervisor bead create response was empty")},updateBead(p,d,m){return Ie(A3({client:u,path:{cityName:p,id:d},headers:Bt,body:m}),"gc supervisor bead update response was empty")},closeBead(p,d,m){return Ie(O3({client:u,path:{cityName:p,id:d},headers:Bt,...m===void 0?{}:{body:m}}),"gc supervisor bead close response was empty")},nudgeAgent(p,d){const m=gm(d);return"dir"in m?Ie(B3({client:u,path:{cityName:p,dir:m.dir,base:m.base,action:"nudge"},headers:Bt}),"gc supervisor agent nudge response was empty"):Ie(C3({client:u,path:{cityName:p,base:m.base,action:"nudge"},headers:Bt}),"gc supervisor agent nudge response was empty")},agentPrime(p,d){const m=gm(d);return"dir"in m?Ie(R3({client:u,path:{cityName:p,dir:m.dir,base:m.base}}),"gc supervisor agent prime response was empty"):Ie(T3({client:u,path:{cityName:p,base:m.base}}),"gc supervisor agent prime response was empty")},sling(p,d){return Ie(e_({client:u,path:{cityName:p},headers:Bt,body:d}),"gc supervisor sling response was empty")},listMail(p,d){return Ie(U3({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor mail response was empty")},formulaFeed(p,d){return Ie(D3({client:u,path:{cityName:p},...d===void 0?{}:{query:d}}),"gc supervisor formula feed response was empty")},sendMail(p,d){return Ie(Z3({client:u,path:{cityName:p},headers:Bt,body:d}),"gc supervisor mail send response was empty")},mailThread(p,d){return Ie(q3({client:u,path:{cityName:p,id:d}}),"gc supervisor mail thread response was empty")},markMailRead(p,d,m){return Ie(H3({client:u,path:{cityName:p,id:d},headers:Bt,...m===void 0?{}:{query:m}}),"gc supervisor mail mark-read response was empty")},markMailUnread(p,d,m){return Ie(W3({client:u,path:{cityName:p,id:d},headers:Bt,...m===void 0?{}:{query:m}}),"gc supervisor mail mark-unread response was empty")},archiveMail(p,d,m){return Ie(V3({client:u,path:{cityName:p,id:d},headers:Bt,...m===void 0?{}:{query:m}}),"gc supervisor mail archive response was empty")},replyMail(p,d,m,h){return Ie(K3({client:u,path:{cityName:p,id:d},headers:Bt,body:m,...h===void 0?{}:{query:h}}),"gc supervisor mail reply response was empty")},cityEventStreamUrl(p,d){return mm(n,`/v0/city/${encodeURIComponent(p)}/events/stream`,d===void 0?void 0:{after_seq:d})},sessionStreamUrl(p,d,m){return mm(n,`/v0/city/${encodeURIComponent(p)}/session/${encodeURIComponent(d)}/stream`,m===void 0?void 0:{after:m})},listSessions(p){return Ie(X3({client:u,path:{cityName:p}}),"gc supervisor sessions response was empty")},sessionPending(p,d){return Ie(J3({client:u,path:{cityName:p,id:d}}),"gc supervisor session pending response was empty")},respondSession(p,d,m){return Ie(Q3({client:u,path:{cityName:p,id:d},headers:Bt,body:m}),"gc supervisor session respond response was empty")},sessionTranscript(p,d){return Ie(Y3({client:u,path:{cityName:p,id:d},query:{format:"conversation"}}),"gc supervisor transcript response was empty")},workflowRun(p,d,m){return Ie(n_({client:u,path:{cityName:p,workflow_id:d},...m===void 0?{}:{query:m}}),"gc supervisor workflow response was empty")},formulaDetail(p,d,m){return Ie(M3({client:u,path:{cityName:p,name:d},query:m}),"gc supervisor formula detail response was empty")},mutationHeaders(){return{...Bt}}}}function it(){return vm??=ah(),vm}function cc(e){const n=sh(e),o=hm.get(n);if(o!==void 0)return o;const s=ah({timeoutMs:n});return hm.set(n,s),s}function gm(e){const n=e.trim().split("/");if(n.length===1){const o=n[0];if(o!==void 0&&o!=="")return{base:o}}if(n.length===2){const o=n[0],s=n[1];if(o!==void 0&&o!==""&&s!==void 0&&s!=="")return{dir:o,base:s}}throw new Error(`invalid agent alias: ${e}`)}function sh(e){return typeof e=="number"&&Number.isFinite(e)&&e>0?e:lS}function uS(e,n){return async(o,s)=>{const u=new AbortController,p=new vn(void 0,`gc supervisor request timed out after ${n}ms`,void 0),d=cS(o,s);d?.aborted&&u.abort(d.reason);const m=()=>u.abort(d?.reason);d?.addEventListener("abort",m,{once:!0});let h;const y=new Promise((z,B)=>{h=setTimeout(()=>{u.abort(p),B(p)},n)}),w=new Request(o,{...s,signal:u.signal}),I=e(w);try{return await Promise.race([I,y])}finally{h!==void 0&&clearTimeout(h),d?.removeEventListener("abort",m)}}}function cS(e,n){return n?.signal!==void 0?n.signal:e instanceof Request?e.signal:null}async function dS(e,n){const o=Mt("list agent pending interactions"),s=pS(n),u=e.flatMap(d=>{const m=d.session?.name;if(m===void 0)return[];const h=s.get(m);return h===void 0?[]:[{agentName:d.name,sessionId:h,sessionName:m}]});return(await Promise.all(u.map(async d=>{const m=await it().sessionPending(o,d.sessionId);return m.pending===void 0?null:{...d,pending:m.pending}}))).filter(d=>d!==null)}async function Eb(e,n){const o=Mt("respond to agent pending interaction");return it().respondSession(o,e,n)}function xb(e){return`gc agent attach ${fS(e)}`}function pS(e){const n=new Map;for(const o of e)o.session_name!==void 0&&n.set(o.session_name,o.id);return n}function fS(e){return/^[A-Za-z0-9_./:-]+$/.test(e)?e:`'${e.replaceAll("'","'\\''")}'`}const mS=1e3,vS=200,hS=1e3,gS=new Set(["feature","bug","task","epic","chore","decision"]);async function yS(e={}){const n=Mt("list supervisor beads"),o=e.limit??mS,s=e.rigFilter?.trim()??"",u=e.includeClosed??!1,p=e.includeBookkeeping??!1,d={limit:o,...u?{all:!0}:{},...s.length===0?{}:{rig:s}},m=await it().listBeads(n,d),h=uh(m.items??[]),y=u?h:h.filter(z=>z.status!=="closed"),w=p?y:y.filter(_S),I=lh(m.total);return{items:w,total:w.length,...I===void 0?{}:{upstream_total:I},upstream_fetched:h.length,fetch_limit:o}}async function Ib(e,n={}){const o=Mt("list supervisor assigned beads"),s=ES(e),u=n.limit??vS,p=n.includeClosed??!1;if(s.length===0)return{items:[],total:0,upstream_fetched:0,fetch_limit:u};const d=await Promise.all(s.map(y=>it().listBeads(o,{assignee:y,limit:u,...p?{all:!0}:{}}))),m=uh(d.flatMap(y=>y.items??[])),h=wS(d);return{items:m,total:m.length,...h===void 0?{}:{upstream_total:h},upstream_fetched:m.length,fetch_limit:u}}async function Sb(e){const n=Mt("fetch supervisor bead");try{return await it().getBead(n,e)}catch(o){if(!(o instanceof vn)||o.status!==404)throw o;const u=((await it().listBeads(n,{limit:hS})).items??[]).find(p=>p.id===e);if(u!==void 0)return u;throw o}}function _S(e){return!(!gS.has(e.issue_type)||Array.isArray(e.labels)&&e.labels.some(n=>n.startsWith("gc:")))}function lh(e){if(typeof e=="number")return e;if(typeof e=="bigint")return Number(e)}function wS(e){let n=0;for(const o of e){const s=lh(o.total);if(s===void 0)return;n+=s}return n}function uh(e){const n=new Set,o=[];for(const s of e)n.has(s.id)||(n.add(s.id),o.push(s));return o}function ES(e){const n=new Set,o=[];for(const s of e){const u=s.trim();u.length===0||n.has(u)||(n.add(u),o.push(u))}return o}const kb=[100,500,1e3],dc=100,bb=["24h","7d","all"],xS="all",IS={"24h":1440*60*1e3,"7d":10080*60*1e3};async function pc(e,n,o,s=dc,u=xS,p=Date.now()){const d=Mt("list supervisor mail"),m=await it().listMail(d,{limit:s}),h=m.items??[],y=kS(SS(h,e,n,o),u,p);return y.sort(TS),{...m,items:y,total:y.length,upstream_total:h.length,upstream_fetched:h.length,fetch_limit:s}}async function zb(e,n,o,s=dc){const u=Mt("fetch supervisor mail thread");try{const p=await it().mailThread(u,e);return ym(p)}catch(p){if(!(p instanceof vn)||p.status!==404)throw p;const d=await pc("all",n,o,s),m=d.items.filter(h=>h.thread_id===e);return ym({...d,items:m,total:m.length})}}function ym(e){const n=zS(e.items??[]).sort(CS);return{...e,items:n,total:n.length}}function SS(e,n,o,s){const u=bS(o,s);return n==="all"?[...e]:n==="inbox"?e.filter(p=>p.to.toLowerCase()===u):e.filter(p=>p.from.toLowerCase()===u)}function kS(e,n,o){if(n==="all")return[...e];const s=o-IS[n];return e.filter(u=>{const p=Date.parse(u.created_at);return Number.isFinite(p)&&p>=s})}function bS(e,n){const o=e.toLowerCase();return o===n.operatorAlias.toLowerCase()?n.operatorWireAlias:o}function zS(e){const n=new Set,o=[];for(const s of e)n.has(s.id)||(n.add(s.id),o.push(s));return o}function TS(e,n){return n.created_at.localeCompare(e.created_at)}function CS(e,n){return e.created_at.localeCompare(n.created_at)}function ch(e,n){if(e===void 0||e.length===0)return null;const o=Date.parse(e);if(!Number.isFinite(o))return null;const s=n-o;return s>=0?s:null}function dh(e){const n=Math.max(1,Math.round(e/36e5));return n<48?`${n}h`:`${Math.round(n/24)}d`}const RS=1440*60*1e3,BS=4320*60*1e3;function PS(e,n){const o=[];for(const s of e.escalations){const u=NS(s);u!==null&&o.push(u)}for(const s of e.beads){const u=AS(s,n);u!==null&&o.push(u)}return o}function NS(e){return e.status==="closed"?null:{beadId:e.id,reason:"escalated",severity:"attention",summary:`${e.title} — escalation raised`,updatedAt:e.updated_at??e.created_at}}function AS(e,n){if(e.status!=="open"||OS(e))return null;const o=ch(e.created_at,n);if(o===null||o=BS;return{beadId:e.id,reason:"ready-unclaimed",severity:s?"attention":"watch",summary:`${e.title} opened ${dh(o)} ago`,updatedAt:e.created_at}}function OS(e){return e.assignee!==void 0&&e.assignee.trim().length>0}function _m(e,n){const o=`/runs/${encodeURIComponent(e)}`;if(n.status!=="available")return o;const s=new URLSearchParams;return s.set("scope_kind",n.kind),s.set("scope_ref",n.ref),`${o}?${s.toString()}`}const jS={"awaiting-input":"awaiting input",errored:"errored","rate-limited":"rate limited",stalled:"stalled"},$S={respond:"Respond to its prompt.",reset:"Reset the agent.",nudge:"Nudge it to resume."},LS={"awaiting-input":"stuck",errored:"stuck","rate-limited":"warn",stalled:"warn"};function DS(e){return jS[e]}function Tb(e){return $S[e]}function Cb(e){return LS[e]}const MS=new Set(["gc.store.maintenance.failed","order.failed","request.failed","session.crashed","session.stranded","session.work_query_failed","supervisor.shutdown_requested"]),FS=new Set(["events.rotated","session.quarantined","session.suspended","supervisor.fs_pressure.skipped_tick"]);function US(e){return MS.has(e.type)?"attention":FS.has(e.type)?"watch":"event"}function ZS(e){return e.message??e.subject??e.type}const qS=1440*60*1e3,VS=30,WS=2e9,HS=1e9,KS=1e9,GS=512e6,JS="gc:escalation",QS="decision.decide";function YS(e={}){return si.map(n=>XS(n,e))}function XS(e,n){switch(e){case"activity":return ik(n.activity);case"agents":return nk(n.agents);case"beads":return rk(n.beads);case"health":return ek(n.health);case"mail":return ok(n.mail);case"runs":return tk(n.runs)}}function ek(e){return{id:"health:derived",domain:"health",getItems:()=>gk(e)}}function tk(e){return{id:"runs:derived",domain:"runs",getItems:()=>ak(e)}}function nk(e){return{id:"agents:derived",domain:"agents",getItems:()=>sk(e)}}function rk(e){return{id:"beads:derived",domain:"beads",getItems:()=>lk(e)}}function ok(e){return{id:"mail:derived",domain:"mail",getItems:()=>pk(e)}}function ik(e){return{id:"activity:derived",domain:"activity",getItems:()=>mk(e)}}function ak(e){const n=[];if(e===void 0)return n;const o={provenance:e.provenance,fetchedAt:e.fetchedAt};if(e.error!==void 0&&e.error.length>0)return n.push(It("runs",{id:"runs:unavailable",title:"Run data unavailable",summary:e.error,href:"/runs"})),n;const s=e.summary;if(s===void 0)return n;s.lanesPartial===!0&&n.push(Yo("runs",{id:"runs:partial",title:"Run list incomplete",href:"/runs"},o));for(const u of[...s.lanes,...s.blockedLanes])u.health.status!=="available"&&n.push(Yo("runs",{id:`runs:${u.id}:health-unavailable`,title:`${u.title} health unavailable`,summary:u.health.error,href:_m(u.id,u.scope)},o));for(const u of m7(s.blockedLanes))n.push(It("runs",{id:`runs:${u.id}:blocked`,title:`${u.title} blocked`,summary:u.reason,href:_m(u.id,u.scope)}));return n}function sk(e){const n=[];if(e===void 0)return n;if(e.error!==void 0&&e.error.length>0)return n.push(Yo("agents",{id:"agents:unavailable",title:"Agent data unavailable",summary:e.error,href:"/agents"})),n;e.partial===!0&&n.push(Yo("agents",{id:"agents:partial",title:"Agent list incomplete",href:"/agents"})),e.pendingError!==void 0&&e.pendingError.length>0&&n.push(Yo("agents",{id:"agents:pending-unavailable",title:"Agent pending state unavailable",summary:e.pendingError,href:"/agents"}));const o=(e.pendingInteractions??[]).map(s=>({agentName:s.agentName,...s.pending.prompt===void 0?{}:{prompt:s.pending.prompt}}));for(const s of l7(e.items??[],o))n.push(It("agents",{id:`agents:${s.name}:needs-you`,title:`${s.name} ${DS(s.reason)}`,summary:s.detail,href:`/agents/${encodeURIComponent(s.name)}`}));return n}function lk(e){const n=[];if(e===void 0)return n;e.error!==void 0&&e.error.length>0&&n.push(It("beads",{id:"beads:unavailable",title:"Bead data unavailable",summary:e.error,href:"/beads"})),e.partial===!0&&n.push(qn("beads",{id:"beads:partial",title:"Bead list incomplete",href:"/beads"})),e.decisionsError!==void 0&&e.decisionsError.length>0&&n.push(It("beads",{id:"beads:decisions-unavailable",title:"Decision queue unavailable",summary:e.decisionsError,href:"/beads"})),e.escalationsError!==void 0&&e.escalationsError.length>0&&n.push(It("beads",{id:"beads:escalations-unavailable",title:"Escalation queue unavailable",summary:e.escalationsError,href:"/beads"}));for(const u of e.decisions??[])n.push(dk(u));const o=e.nowMs??Date.now(),s=(e.items??[]).filter(u=>!ck(u,e.decisionLabel));for(const u of PS({beads:s,escalations:e.escalations??[]},o)){const p=u.severity==="attention"?It:qn;n.push(p("beads",{id:`beads:${u.beadId}:${u.reason}`,title:`${u.beadId} ${uk(u.reason)}`,summary:u.summary,href:ph(u.beadId),updatedAt:u.updatedAt}))}return n}function uk(e){return e==="escalated"?"escalated":"unclaimed"}function ph(e){const n=new URLSearchParams;return n.set("bead",e),`/beads?${n.toString()}`}function ck(e,n){return(e.labels??[]).includes(n)}function dk(e){const n=e.metadata?.[QS];return It("beads",{id:`beads:${e.id}:mayor-decision`,title:e.title,href:ph(e.id),updatedAt:e.updated_at??e.created_at,...n!==void 0&&n.trim().length>0?{summary:n}:{}})}function pk(e){const n=[];if(e===void 0)return n;e.error!==void 0&&e.error.length>0&&n.push(It("mail",{id:"mail:unavailable",title:"Mail data unavailable",summary:e.error,href:"/mail"})),e.partial===!0&&n.push(qn("mail",{id:"mail:partial",title:"Mail list incomplete",href:"/mail"}));const o=e.nowMs??Date.now();for(const s of O2(e.items??[])){const u=ch(s.created_at,o),p=u!==null&&u>=qS;n.push(It("mail",{id:`mail:${s.id}:${p?"unread-stale":"unread"}`,title:s.subject,summary:p?`from ${s.from}, unread for ${dh(u)}`:`from ${s.from}`,href:fk(s.id),updatedAt:s.created_at}))}return n}function fk(e){const n=new URLSearchParams;return n.set("message",e),`/mail?${n.toString()}`}function mk(e){const n=[];if(e===void 0)return n;e.deploysError!==void 0&&e.deploysError.length>0&&n.push(It("activity",{id:"activity:deploys-unavailable",title:"Deploy data unavailable",summary:e.deploysError,href:"/activity"})),e.eventsDegraded!==void 0&&e.eventsDegraded.length>0&&n.push(qn("activity",{id:"activity:events-degraded",title:"Event stream degraded",summary:e.eventsDegraded,href:"/activity"})),e.eventsError!==void 0&&e.eventsError.length>0&&n.push(qn("activity",{id:"activity:events-unavailable",title:"Event history unavailable",summary:e.eventsError,href:"/activity"})),e.eventsPartial===!0&&n.push(qn("activity",{id:"activity:events-partial",title:"Event history incomplete",href:"/activity"})),vk(n,e.events??[]);const o=e.deploys;if(o===void 0)return n;o.failed_marker&&n.push(It("activity",{id:"activity:failed-marker",title:"Deploy failed marker present",href:"/activity"}));for(const s of o.items)s.status==="failed"?n.push(It("activity",{id:`activity:deploy:${s.at}:failed`,title:"Deploy failed",summary:s.detail,href:"/activity",updatedAt:s.at})):s.status==="in-progress"&&n.push(qn("activity",{id:`activity:deploy:${s.at}:in-progress`,title:"Deploy in progress",summary:s.detail,href:"/activity",updatedAt:s.at}));return n}function vk(e,n){for(const o of n){const s=US(o);if(s==="event")continue;const u=s==="attention"?It:qn;e.push(u("activity",{id:`activity:event:${String(o.seq)}:${o.type}`,title:o.type,summary:ZS(o),href:hk(o),updatedAt:o.ts}))}}function hk(e){return`/activity?${new URLSearchParams({mode:"events",type:e.type}).toString()}`}function gk(e){const n=[];return e===void 0||(e.dashboardError!==void 0&&e.dashboardError.length>0&&n.push(Wn({id:"health:dashboard-health-unavailable",title:"Dashboard health unavailable",summary:e.dashboardError})),e.supervisor!==void 0&&yk(n,e.supervisor),e.system!==void 0&&(_k(n,e.system),wk(n,e.system)),e.trend!==void 0&&!e.trend.available&&n.push(mr({id:"health:dolt-noms-unavailable",title:"Dolt-noms trend unavailable",summary:e.trend.reason}))),n}function yk(e,n){if(n.status==="unavailable"){e.push(Wn({id:"health:supervisor-unreachable",title:"Supervisor unreachable",summary:n.error}));return}const o=n.data;o.status!=="ok"&&e.push(Wn({id:"health:supervisor-not-ok",title:`Supervisor ${o.status}`})),o.city===void 0&&e.push(mr({id:"health:supervisor-city-missing",title:"Supervisor city missing",summary:"city was absent from generated supervisor health"})),o.version===void 0&&e.push(mr({id:"health:supervisor-version-missing",title:"Supervisor version missing",summary:"version was absent from generated supervisor health"}))}function _k(e,n){const o=n.admin;o.uptime_sec=WS?e.push(Wn({id:"health:dashboard-process-rss-high",title:"Dashboard RSS high",summary:Ta(o.rss_bytes)})):o.rss_bytes>=HS&&e.push(mr({id:"health:dashboard-process-rss-elevated",title:"Dashboard RSS elevated",summary:Ta(o.rss_bytes)})),o.heap_used_bytes>=KS?e.push(Wn({id:"health:dashboard-process-heap-high",title:"Dashboard heap high",summary:Ta(o.heap_used_bytes)})):o.heap_used_bytes>=GS&&e.push(mr({id:"health:dashboard-process-heap-elevated",title:"Dashboard heap elevated",summary:Ta(o.heap_used_bytes)}))}function wk(e,n){const o=wm(n.host.free_mem_bytes,n.host.total_mem_bytes);o!==null&&o<.05?e.push(Wn({id:"health:memory-critical",title:"Host memory critical",summary:`${Math.round(o*100)}% free`})):o!==null&&o<.1&&e.push(mr({id:"health:memory-low",title:"Host memory low",summary:`${Math.round(o*100)}% free`}));const s=wm(n.host.load_avg_1,n.host.cpu_count);s!==null&&s>1.5?e.push(Wn({id:"health:load-high",title:"Host load high",summary:`${n.host.load_avg_1.toFixed(2)} load across ${n.host.cpu_count} CPUs`})):s!==null&&s>1&&e.push(mr({id:"health:load-elevated",title:"Host load elevated",summary:`${n.host.load_avg_1.toFixed(2)} load across ${n.host.cpu_count} CPUs`}))}function Ta(e){return e>=1e9?`${(e/1e9).toFixed(1)} GB`:e>=1e6?`${Math.round(e/1e6)} MB`:e>=1e3?`${Math.round(e/1e3)} KB`:`${e} B`}function wm(e,n){return n<=0?null:e/n}function Wn(e){return{domain:"health",severity:"attention",href:"/health",current:!0,actionable:!0,...e}}function It(e,n){return{domain:e,severity:"attention",current:!0,actionable:!0,...n}}function qn(e,n){return{domain:e,severity:"watch",current:!0,actionable:!1,...n}}function Yo(e,n,o){return{domain:e,severity:"unavailable",current:!0,actionable:!1,...n,...o?.provenance===void 0?{}:{provenance:o.provenance},...o?.fetchedAt===void 0?{}:{fetchedAt:o.fetchedAt}}}function mr(e){return{domain:"health",severity:"watch",href:"/health",current:!0,actionable:!1,...e}}const Ek=1e3,xk=100,Ik="24h",Sk=2500;function kk(e,n){const o=Ua(),s=o??"no-city",{decisionLabel:u,operatorWireAlias:p}=e,d=b.useMemo(()=>bk(n),[n]),m=mn(`attention:agents:${s}`,()=>zk(o)),h=mn(`attention:beads:${s}:${u}`,()=>Tk(o,u)),y=mn(`attention:mail:${s}:${p}`,()=>Bk(o,e)),w=mn(`attention:activity:${s}`,()=>Pk(o)),I=mn(`attention:health:${s}`,()=>Nk(o));return b.useMemo(()=>YS(Ak({activity:w.data,agents:m.data,beads:h.data,health:I.data,mail:y.data,runs:d})),[w.data,m.data,h.data,I.data,y.data,d])}function bk(e){if(e!==void 0)return e.status==="error"?{error:e.error,provenance:"error"}:{summary:e.data,provenance:e.status,fetchedAt:e.fetchedAt}}async function zk(e){if(e===null)return{};try{const n=await it().listAgents(e),o={items:n.items??[],partial:n.partial===!0};try{const s=await it().listSessions(e);o.pendingInteractions=await dS(n.items??[],s.items??[])}catch(s){o.pendingError=Gt(s,"agent pending state unavailable")}return o}catch(n){return{error:Gt(n,"agent list unavailable")}}}async function Tk(e,n){if(e===null)return{decisionLabel:n};const[o,s,u]=await Promise.allSettled([yS({limit:Ek}),Ck(e,n),Rk(e)]),p={nowMs:Date.now(),decisionLabel:n};return o.status==="fulfilled"?(p.items=o.value.items,p.partial=o.value.partial===!0):p.error=Gt(o.reason,"bead list unavailable"),s.status==="fulfilled"?p.decisions=s.value.items??[]:p.decisionsError=Gt(s.reason,"decision queue unavailable"),u.status==="fulfilled"?p.escalations=u.value.items??[]:p.escalationsError=Gt(u.reason,"escalation queue unavailable"),p}async function Ck(e,n){return it().listBeads(e,{label:n,status:"open"})}async function Rk(e){return it().listBeads(e,{label:JS,status:"open"})}async function Bk(e,n){if(e===null)return{};try{const o=await pc("inbox",n.operatorAlias,n,dc);return{items:o.items??[],nowMs:Date.now(),partial:o.partial===!0}}catch(o){return{error:Gt(o,"mail list unavailable")}}}async function Pk(e){const[n,o]=await Promise.allSettled([oi.listBuilds(),e===null?Promise.resolve(null):it().listEvents(e,{limit:xk,since:Ik})]),s={};return n.status==="fulfilled"?s.deploys=n.value:s.deploysError=Gt(n.reason,"deploy activity unavailable"),o.status==="fulfilled"?o.value!==null&&(s.events=o.value.items??[],s.eventsPartial=o.value.partial===!0,o.value.partial_errors!==null&&o.value.partial_errors!==void 0&&(s.eventsDegraded=o.value.partial_errors.join("; "))):s.eventsError=Gt(o.reason,"event history unavailable"),s}async function Nk(e){if(e===null)return{};const[n,o,s]=await Promise.allSettled([oi.systemHealth(),cc(Sk).cityHealth(e),oi.doltTrend()]),u={},p=[];return n.status==="fulfilled"?u.system=n.value:p.push(Gt(n.reason,"dashboard health unavailable")),o.status==="fulfilled"?u.supervisor={status:"available",data:o.value}:u.supervisor={status:"unavailable",error:Gt(o.reason,"supervisor health unavailable")},s.status==="fulfilled"?u.trend=s.value:p.push(Gt(s.reason,"dolt-noms trend unavailable")),p.length>0&&(u.dashboardError=p.join("; ")),u}function Ak(e){const n={};for(const[o,s]of Object.entries(e))s!==void 0&&(n[o]=s);return n}async function Kr(e){const n={Accept:"application/json","Content-Type":"application/json","X-GC-Request":"dashboard"};try{const o=await fetch("/api/client-errors",{method:"POST",headers:n,credentials:"same-origin",keepalive:!0,body:JSON.stringify(e)});return o.ok?{status:"reported"}:{status:"failed",error:`client error report failed with ${o.status}`}}catch(o){return{status:"failed",error:Vr(o)}}}class fh extends b.Component{state={crashed:!1};static getDerivedStateFromError(){return{crashed:!0}}componentDidCatch(n,o){Kr({component:"ErrorBoundary",operation:"componentDidCatch",message:Vr(n)})}render(){return this.state.crashed?$.jsx("main",{className:"max-w-dashboard mx-auto px-4 sm:px-6 lg:px-8 py-12",children:$.jsxs("section",{className:"space-y-4",role:"alert",children:[$.jsx("h1",{className:"text-display font-semibold text-fg",children:"Dashboard view failed."}),$.jsx("p",{className:"text-body text-fg-muted",children:"The error was reported to the local dashboard log. Refresh to retry this view."})]})}):this.props.children}}function Ok({label:e,summary:n}){const o=n.attention+n.watch;if(o===0||n.severity===null)return null;const s=o===1?"item":"items";return $.jsx("span",{"aria-label":`${e}: ${o} ${n.severity} ${s}`,className:`ml-1 align-super text-[0.65rem] leading-none tnum ${jk(n.severity)}`,children:o})}function jk(e){return e==="attention"?"text-accent":"text-warn"}function mh(e,n,o){try{const s=fc(e).getItem(n);return s===null?{status:"missing"}:{status:"found",value:s}}catch(s){return mc(e,"getItem",n,o,s)}}function vh(e,n,o,s){try{return fc(e).setItem(n,o),{status:"stored"}}catch(u){return mc(e,"setItem",n,s,u)}}function hh(e,n,o){try{return fc(e).removeItem(n),{status:"stored"}}catch(s){return mc(e,"removeItem",n,o,s)}}function fc(e){return e==="localStorage"?window.localStorage:window.sessionStorage}function mc(e,n,o,s,u){const p=Vr(u);return Kr({component:s,operation:`${e}.${n}`,message:`${o}: ${p}`}),{status:"unavailable",error:p}}const fu="gascity:theme",mu="ThemeContext",gh=b.createContext(null);function $k(){const e=mh("localStorage",fu,mu);return e.status==="found"&&(e.value==="light"||e.value==="dark")?e.value:"system"}function Lk(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function Dk(e){const n=document.documentElement;e==="system"?n.removeAttribute("data-theme"):n.setAttribute("data-theme",e)}function Mk({children:e}){const[n,o]=b.useState($k),[s,u]=b.useState(Lk);b.useEffect(()=>{const y=window.matchMedia("(prefers-color-scheme: dark)"),w=()=>u(y.matches?"dark":"light");return y.addEventListener("change",w),()=>y.removeEventListener("change",w)},[]);const p=n==="system"?s:n,d=b.useCallback(y=>{o(y),y==="system"?hh("localStorage",fu,mu):vh("localStorage",fu,y,mu),Dk(y)},[]),m=b.useCallback(()=>{d(p==="dark"?"light":"dark")},[p,d]),h=b.useMemo(()=>({pref:n,resolved:p,set:d,toggle:m}),[n,p,d,m]);return $.jsx(gh.Provider,{value:h,children:e})}function Fk(){const e=b.useContext(gh);if(e===null)throw new Error("useTheme must be used inside ");return e}const yh={operatorAlias:"operator",operatorWireAlias:"human",decisionLabel:"needs/operator"},_h=b.createContext(yh);function Uk({operator:e,children:n}){return $.jsx(_h.Provider,{value:e,children:n})}function wh(){return b.useContext(_h)}function Zk(e){return e===void 0?yh:{operatorAlias:e.operatorAlias,operatorWireAlias:e.operatorWireAlias,decisionLabel:e.decisionLabel}}const qk={ok:"text-ok",warn:"text-warn",stuck:"text-accent",neutral:"text-fg-muted"},Vk={ok:"●",warn:"▲",stuck:"■",neutral:"·"};function Wk({tone:e,label:n,glyph:o,trailing:s,className:u="",title:p}){return $.jsxs("span",{className:`inline-flex items-baseline gap-1.5 text-body ${qk[e]} ${u}`,title:p,children:[$.jsx("span",{"aria-hidden":!0,className:"text-[0.85em] leading-none translate-y-[1px]",children:o??Vk[e]}),$.jsx("span",{children:n}),s&&$.jsx("span",{className:"text-fg-faint text-label uppercase tracking-wider",children:s})]})}function Rb(e){switch(e){case"closed":return"neutral";case"in_progress":return"ok";case"blocked":return"stuck";default:return"warn"}}function Bb(e){switch(e){case"active":case"running":return"ok";case"rate-limited":case"rate_limited":case"waiting":return"warn";case"failed":case"closed":case"errored":case"stuck":return"stuck";default:return"neutral"}}const Eh=b.createContext(!1);function Hk({readOnly:e,children:n}){return $.jsx(Eh.Provider,{value:e,children:n})}function Kk(){return b.useContext(Eh)}function Gk(e,n){return e?e.readOnly:n!==null}const xh="Read-only mode: mutations are disabled";function Pb(){return $.jsx(Wk,{tone:"warn",label:"Read-only",title:xh})}const Jk="mayor";function Qk(e){const{operator:n,sessionAliases:o,mailFromOrTo:s}=e,u=new Map;for(const B of o){const D=B.toLowerCase();u.has(D)||u.set(D,B)}for(const B of s){const D=B.toLowerCase();u.has(D)||u.set(D,B)}const p=n.toLowerCase(),d=new Set(s.map(B=>B.toLowerCase())),m=[n],h=[],y=[],w=[];for(const[B,D]of u)if(B!==p){if(B===Jk){h.push(D);continue}d.has(B)?y.push(D):w.push(D)}const I=(B,D)=>B.toLowerCase().localeCompare(D.toLowerCase());y.sort(I),w.sort(I);const z=[{tier:"you",aliases:m}];return h.length>0&&z.push({tier:"mayor",aliases:h}),y.length>0&&z.push({tier:"active",aliases:y}),w.length>0&&z.push({tier:"other",aliases:w}),z}function Yk(e,n){return e===n?"user":e}function Nb(e){switch(e){case"you":return"You";case"mayor":return"Mayor";case"active":return"Active";case"other":return"Other"}}async function Xk(){return it().listSessions(Mt("list supervisor sessions"))}async function Ab(e){const n=await it().sessionTranscript(Mt("fetch supervisor session transcript"),e);return n6(n)}function e6(e){return(e.items??[]).map(t6)}function t6(e){const n={id:e.id,template:e.template,session_name:e.session_name,title:e.title,state:e.state,created_at:e.created_at,attached:e.attached,running:e.running,provider:e.provider};return e.alias!==void 0&&(n.alias=e.alias),e.reason!==void 0&&(n.reason=e.reason),e.display_name!==void 0&&(n.display_name=e.display_name),e.last_active!==void 0&&(n.last_active=e.last_active),e.rig!==void 0&&(n.rig=e.rig),e.pool!==void 0&&(n.pool=e.pool),e.agent_kind!==void 0&&(n.agent_kind=e.agent_kind),e.model!==void 0&&(n.model=e.model),e.context_pct!==void 0&&(n.context_pct=e.context_pct),e.context_window!==void 0&&(n.context_window=e.context_window),e.activity!==void 0&&(n.activity=e.activity),n}function n6(e,n=new Date().toISOString()){const o=e.turns??[];return{...e,turns:o,total_chars:o.reduce((s,u)=>s+u.text.length,0),captured_at:n,truncated:!1}}const vu="gascity.dashboard.viewingAs",Gr="ViewingAsContext",Em=/^[a-z][a-z0-9_./-]{1,63}$/i,xm=[3e4,9e4,27e4];function r6(e){if(!Number.isInteger(e)||e<0||e>=xm.length)return null;const n=xm[e];return n===void 0?null:n}const Ih=b.createContext(null);function Im(e){const n=mh("sessionStorage",vu,Gr);if(n.status==="found"){const o=n.value;if(o.length>0&&o.length<=64)return o}return e}function Xl(e,n){e===n?hh("sessionStorage",vu,Gr):vh("sessionStorage",vu,e,Gr)}function o6({children:e}){const n=wh(),{operatorAlias:o}=n,[s,u]=b.useState(()=>Im(o)),p=b.useRef(o),[d,m]=b.useState([]),[h,y]=b.useState([]),[w,I]=b.useState(!1),[z,B]=b.useState(!1),D=b.useRef(!1),V=b.useRef(!0),A=b.useRef(null),H=b.useCallback(pe=>{u(pe),Xl(pe,o)},[o]),oe=b.useCallback(()=>{u(o),Xl(o,o)},[o]),Q=b.useCallback(async()=>{try{const pe=await Xk();if(!V.current)return!0;const Te=new Set,ye=[];for(const Ae of pe.items??[]){if(typeof Ae.alias!="string"||!Em.test(Ae.alias))continue;const Ke=Ae.alias.toLowerCase();Te.has(Ke)||(Te.add(Ke),ye.push(Ae.alias))}return m(ye),B(!1),!0}catch(pe){return Kr({component:Gr,operation:"loadAliases.sessions",message:Vr(pe)}),!1}},[]),K=b.useCallback(pe=>{if(!V.current)return;const Te=r6(pe);Te!==null&&(A.current=setTimeout(()=>{A.current=null,V.current&&Q().then(ye=>{V.current&&(ye||K(pe+1))}).catch(ye=>{Kr({component:Gr,operation:"loadAliases.sessionsRetry",message:Vr(ye)})})},Te))},[Q]),Y=b.useCallback(()=>{if(D.current)return;D.current=!0,I(!0);let pe=2;const Te=()=>{pe-=1,pe===0&&V.current&&I(!1)};Q().then(ye=>{V.current&&(ye||(B(!0),K(0)))}).finally(Te),pc("all",o,n).then(ye=>{if(!V.current)return;const Ae=new Set,Ke=[];for(const Oe of ye.items)for(const Xe of[Oe.from,Oe.to]){if(typeof Xe!="string"||Xe.length===0||!Em.test(Xe))continue;const kt=Xe.toLowerCase();Ae.has(kt)||(Ae.add(kt),Ke.push(Xe))}y(Ke)}).catch(ye=>{Kr({component:Gr,operation:"loadAliases.mail",message:Vr(ye)})}).finally(Te)},[Q,K,o,n]);b.useEffect(()=>(V.current=!0,()=>{V.current=!1,A.current!==null&&(clearTimeout(A.current),A.current=null)}),[]),b.useEffect(()=>{const pe=p.current;p.current=o,pe!==o&&s===pe&&u(Im(o))},[o,s]);const ue=b.useMemo(()=>Qk({operator:o,sessionAliases:d.includes(s)?d:[...d,s],mailFromOrTo:h}),[d,h,s,o]),ce=b.useMemo(()=>({viewingAs:{alias:s,isOperator:s===o},setAlias:H,resetToOperator:oe,aliasBuckets:ue,aliasesLoading:w,sessionsUnavailable:z,loadAliases:Y}),[s,o,H,oe,ue,w,z,Y]);return b.useEffect(()=>{const pe=()=>{document.hidden&&s!==o&&(u(o),Xl(o,o))};return document.addEventListener("visibilitychange",pe),()=>document.removeEventListener("visibilitychange",pe)},[s,o]),$.jsx(Ih.Provider,{value:ce,children:e})}function i6(){const e=b.useContext(Ih);if(e===null)throw new Error("useViewingAs must be inside ");return e}const a6={id:"activity",kind:"core",path:"/activity",nav:{label:"Activity",order:55},element:b.lazy(()=>En(()=>import("./Activity-Ca_fEMiY.js"),__vite__mapDeps([0,1,2,3,4])).then(e=>({default:e.ActivityPage})))},s6={id:"health",kind:"core",path:"/health",nav:{label:"Health",order:60},element:b.lazy(()=>En(()=>import("./Health-B0fm2qWB.js"),__vite__mapDeps([5,1,2,4,6,3])).then(e=>({default:e.HealthPage})))},Sh=[a6,s6],l6={views:"views"};function u6(e,n){console.warn(`[${e}] ${n}`)}function kh(e,n){const o=new Set(n??[]);return e.filter(s=>s.kind==="core"||o.has(s.id))}const c6={};function d6(e,n){const o=[];if(n!==null){const d=c6[n];if(d!==void 0){if(e.some(h=>h.id===d.target))return{view:null,redirectTo:d.redirectTo,source:"env",warnings:o};o.push(`DEFAULT_VIEW="${n}" alias targets the "${d.target}" view, which is not enabled in this deployment (known enabled ids: ${e.map(h=>h.id).join(", ")||"(none)"}); falling through to descriptor / ambient-home`)}else{const m=e.find(h=>h.id===n);if(m!==void 0)return{view:m,source:"env",warnings:o};o.push(`DEFAULT_VIEW="${n}" does not match any enabled view (known enabled ids: ${e.map(h=>h.id).join(", ")||"(none)"}); falling through to descriptor / ambient-home`)}}const s=e.filter(d=>d.defaultRoute===!0),[u,...p]=s;if(u!==void 0&&p.length===0)return{view:u,source:"descriptor",warnings:o};if(u!==void 0){const m=[...s].sort(f6)[0]??u;return o.push(`multiple views declare defaultRoute: true (${s.map(h=>h.id).join(", ")}); picking "${m.id}" by lowest nav.order`),{view:m,source:"descriptor",warnings:o}}return{view:null,source:"fallback",warnings:o}}function p6(e,n){const o=d6(e,n);for(const s of o.warnings)u6(l6.views,s);return o}function f6(e,n){const o=e.nav?.order??Number.POSITIVE_INFINITY,s=n.nav?.order??Number.POSITIVE_INFINITY;return o!==s?o-s:e.id.localeCompare(n.id)}const m6=[{to:"/",label:"Home",end:!0,order:10},{to:"/agents",label:"Agents",order:20},{to:"/beads",label:"Beads",order:30},{to:"/runs",label:"Runs",order:40},{to:"/mail",label:"Mail",order:50}],v6={"/agents":"agents","/beads":"beads","/runs":"runs","/mail":"mail","/activity":"activity","/health":"health"};function h6(){const{resolved:e,toggle:n}=Fk(),{viewingAs:o}=i6(),{operatorAlias:s}=wh(),u=Kk(),p=d3(),{data:d}=mn("config",()=>oi.config()),{data:m}=mn("cities",()=>it().listCities()),h=Ua(),y=m?.items??[],w=h??d?.cityName??"",I=w===""||y.some(H=>H.name===w),z=y.length>1||!I,B=H=>{H!==h&&window.location.assign(`/city/${encodeURIComponent(H)}/`)},D=b.useMemo(()=>{const oe=kh(Sh,d?.enabledModules??null).flatMap(Q=>Q.nav===null?[]:[{to:Q.path,label:Q.nav.label,end:Q.path==="/",order:Q.nav.order}]);return[...m6,...oe].sort((Q,K)=>Q.order-K.order)},[d?.enabledModules]),{pathname:V}=wn(),A=!o.isOperator&&V.startsWith("/mail");return $.jsx("header",{className:"border-b border-rule",children:$.jsxs("div",{className:"max-w-dashboard mx-auto px-4 sm:px-6 lg:px-8 py-5 flex items-baseline gap-x-6 lg:gap-x-8 gap-y-2 flex-wrap",children:[$.jsxs("div",{className:"flex items-baseline gap-3 min-w-0",children:[$.jsx("span",{className:"text-title font-semibold tracking-tight text-fg",children:"gas city"}),$.jsx("span",{className:"text-fg-muted","aria-hidden":"true",children:"·"}),z?$.jsx("label",{className:"sr-only",htmlFor:"city-switcher",children:"Switch city"}):null,z?$.jsxs("select",{id:"city-switcher",value:w,onChange:H=>B(H.target.value),className:"text-label uppercase tracking-wider text-fg-muted bg-transparent border-0 focus-mark cursor-pointer hover:text-fg transition-colors duration-150 ease-out-quart",children:[!I&&w!==""?$.jsxs("option",{value:w,disabled:!0,children:[w," (unknown)"]}):null,y.map(H=>$.jsxs("option",{value:H.name,children:[H.name,H.running?"":" (stopped)"]},H.name))]}):$.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:w||"city"}),A&&$.jsxs("span",{className:"text-label uppercase tracking-wider text-accent ml-3",children:["· reading as ",Yk(o.alias,s)]}),u&&$.jsx("span",{title:xh,className:"text-label uppercase tracking-wider text-warn ml-3",children:"· read-only"})]}),$.jsx("nav",{className:"flex-1",children:$.jsx("ul",{className:"flex items-baseline gap-x-5 lg:gap-x-7 gap-y-1 flex-wrap",children:D.map(H=>{const oe=v6[H.to];return $.jsx("li",{children:$.jsxs(Hy,{to:H.to,end:H.end??!1,className:({isActive:Q})=>["text-title transition-colors duration-150 ease-out-quart focus-mark",Q?"text-fg font-semibold":"text-fg-muted font-medium hover:text-fg"].join(" "),children:[H.label,oe!==void 0&&$.jsx(Ok,{label:H.label,summary:p.byDomain[oe]})]})},H.to)})})}),$.jsx("button",{type:"button",onClick:n,"aria-label":`Switch to ${e==="dark"?"light":"dark"} theme`,className:"text-label uppercase tracking-wider text-fg-muted hover:text-fg transition-colors duration-150 ease-out-quart focus-mark",children:e==="dark"?"Light":"Dark"})]})})}function g6({children:e}){return $.jsxs("div",{className:"min-h-screen bg-surface text-fg antialiased",children:[$.jsx(h6,{}),$.jsx("main",{className:"max-w-dashboard mx-auto px-4 sm:px-6 lg:px-8 py-12",children:e})]})}const bh=b.createContext(null);function y6({children:e,intervalMs:n=1e3}){const[o,s]=b.useState(()=>Date.now());return b.useEffect(()=>{const u=window.setInterval(()=>{s(Date.now())},n);return()=>{window.clearInterval(u)}},[n]),$.jsx(bh.Provider,{value:o,children:e})}function Ob(){const e=b.useContext(bh);if(e===null)throw new Error("useNow must be called inside a NowProvider.");return e}const _6=2e3,w6=2500;function E6(e,n,o={}){const[s,u]=b.useState("connecting"),p=b.useRef(n);p.current=n;const d=b.useRef(o.matches);d.current=o.matches;const m=b.useRef(o.coalesceMs);m.current=o.coalesceMs;const h=e.join(","),y=b.useRef(0),w=b.useRef(null);return b.useEffect(()=>{if(e.length===0){u("closed");return}let I=null,z=!1,B=null,D=null,V=1e3,A=!1;const H=()=>{D!==null&&(clearTimeout(D),D=null)},oe=ue=>{A||(A=!0,x6(ue))},Q=()=>{y.current=Date.now(),p.current()},K=()=>{const ue=m.current??w6,ce=Date.now()-y.current;ce>=ue?(w.current&&(clearTimeout(w.current),w.current=null),Q()):w.current===null&&(w.current=setTimeout(()=>{w.current=null,z||Q()},ue-ce))},Y=()=>{const ue=globalThis.EventSource;if(typeof ue!="function"){u("closed");return}const ce=Ua();if(ce===null){u("closed");return}const pe=new ue(it().cityEventStreamUrl(ce));I=pe,u("connecting"),D=setTimeout(()=>{z||I!==pe||pe.readyState===ue.CLOSED||u("open")},_6),I.onopen=()=>{z||(H(),u("open"),V=1e3)};const Te=ye=>{if(z)return;let Ae=null;try{Ae=JSON.parse(ye.data)}catch{u("degraded"),oe("invalid JSON");return}if(!I6(Ae)){u("degraded"),oe("missing string event type");return}const Ke=Ae.type;if(typeof Ke!="string"){u("degraded"),oe("missing string event type");return}u("open");for(const Oe of e)if(Ke.startsWith(Oe)){const Xe=Ae;(d.current?.(Xe)??!0)&&K();break}};I.onmessage=Te,I.addEventListener("event",Te),I.onerror=()=>{z||(H(),u("closed"),I?.close(),I=null,B=setTimeout(()=>{V=Math.min(V*2,3e4),Y()},V))}};return Y(),()=>{z=!0,B&&clearTimeout(B),H(),w.current&&(clearTimeout(w.current),w.current=null),I?.close()}},[h]),s}function x6(e){Kr({component:"gc-events",operation:"parse event",message:`Malformed gc event payload: ${e}.`})}function I6(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}const Sm=1,S6=250;async function k6(e){let n;for(let o=0;o<=Sm;o+=1)try{return await e()}catch(s){if(n=s,o===Sm||!b6(s))throw s;await z6(S6)}throw n}function b6(e){return e instanceof vn?e.status===void 0?/timed out after \d+ms/.test(e.message):e.status>=500:!1}function z6(e){return new Promise(n=>setTimeout(n,e))}function zh(e){return e.partial===!0||(e.partial_errors?.length??0)>0||(e.next_cursor?.length??0)>0}function Th(e,n){return zh(e)||typeof e.total=="number"&&e.total>n}const vc=500,km=500,hc=60*1e3,T6=15e3,gc=2500,C6=3e4,R6=3e3,bm=new Map;async function B6(){return Ch(C6)}async function jb(){return Ch(gc)}async function Ch(e){const n=Mt("load supervisor run summary"),o=new Date().toISOString();try{const[s,u]=await Promise.all([Bh(n,vc,e),Ph(n,e)]),p=Cu(s.beads.filter(Ru).map(bu),s.feedScopes,s.partial),d={source:"runs",status:"fresh",fetchedAt:o,staleAt:new Date(Date.parse(o)+hc).toISOString(),error:{kind:"none"},data:p};return{...d,data:Nh(n,d,u)}}catch(s){return{source:"runs",status:"error",error:Ec(s,"formula runs unavailable")}}}async function P6(){const e=Mt("load supervisor run summary active"),n=new Date().toISOString();try{const[o,s]=await Promise.all([N6(e,vc),Ph(e,gc)]),u=Cu(o.beads.filter(Ru).map(bu),o.feedScopes,o.partial),p={source:"runs",status:"fresh",fetchedAt:n,staleAt:new Date(Date.parse(n)+hc).toISOString(),error:{kind:"none"},data:u};return{...p,data:Nh(e,p,s)}}catch(o){return{source:"runs",status:"error",error:Ec(o,"formula runs unavailable")}}}async function N6(e,n){const o=await Rh(e,n),s=wc(o.items??[]);return{beads:s,feedScopes:new Map,partial:Th(o,s.length)}}async function A6(){const e=Mt("load supervisor run summary preview"),n=new Date().toISOString();try{const o=await Bh(e,vc,gc),s=Cu(o.beads.filter(Ru).map(bu),o.feedScopes,o.partial);return{source:"runs",status:"fresh",fetchedAt:n,staleAt:new Date(Date.parse(n)+hc).toISOString(),error:{kind:"none"},data:s}}catch(o){return{source:"runs",status:"error",error:Ec(o,"formula runs unavailable")}}}function O6(){return cc(T6)}async function Rh(e,n){return k6(()=>O6().listBeads(e,{limit:n}))}function yc(e){return cc(e)}async function Bh(e,n,o){const s=zm(e,{limit:km,type:"molecule",all:!0},Math.min(R6,o)),[u,p]=await Promise.all([Rh(e,n),j6(e,o)]),d=wc(u.items??[]),h=L6($6(d),p.rigNames).map(z=>zm(e,{limit:km,type:"task",rig:z,all:!0},o)),y=await Promise.all([s,...h]),w=[];let I=p.partial||Th(u,d.length);for(const z of y){if(z.ok){w.push(...z.items),I||=z.partial;continue}I=!0}return{beads:D6([...d,...w]),feedScopes:p.scopes,partial:I}}async function zm(e,n,o){try{const s=await _c(yc(o).listBeads(e,n),`recent ${n.type} beads`,o);return{ok:!0,items:wc(s.items??[]),partial:zh(s)}}catch{return{ok:!1}}}async function j6(e,n){try{const o=await _c(yc(n).formulaFeed(e,{scope_kind:"city",scope_ref:e}),"formula feed",n),s=new Set,u=new Map;for(const p of o.items??[]){if(p.type!=="formula")continue;const d=xu(p.root_store_ref??null);d?.scopeKind==="rig"&&s.add(d.scopeRef);const m=p.root_bead_id??p.workflow_id??null,h=d?.scopeKind==="rig"&&Ba.test(d.scopeRef)?d:e7(p);m!==null&&h!==null&&u.set(m,{scopeKind:h.scopeKind,scopeRef:h.scopeRef,rootStoreRef:p.root_store_ref??`${h.scopeKind}:${h.scopeRef}`})}return{rigNames:[...s],scopes:u,partial:F6(o)}}catch{return{rigNames:[],scopes:new Map,partial:!0}}}async function Ph(e,n){try{const o=await _c(yc(n).listSessions(e),"run sessions",n);return{kind:"available",sessions:e6(o)}}catch{return{kind:"unavailable",sessions:[]}}}function Nh(e,n,o){const s=[...n.data.lanes,...n.data.blockedLanes],u=bm.get(e),p=Date.parse(n.fetchedAt);let d=u?.marks??new Map;(u===void 0||p>Date.parse(u.fetchedAt))&&(d=J7(d,s),bm.set(e,{marks:d,fetchedAt:n.fetchedAt}));const m=o.kind==="available",{lanes:h}=Q7({lanes:s,sessions:o.sessions,sessionsAvailable:m,marks:d}),y=h.filter(B=>B.phase==="blocked"),I=h.filter(B=>B.phase!=="blocked").filter(B=>!o2(B,p,m)),z=Gm([...I,...y]);return{...n.data,totalActive:I.length,lanes:I,blockedLanes:y,runCounts:Jm(I,I.length,y.length),census:{status:"available",data:z}}}function $6(e){const n=new Set;for(const o of e){const s=xu(o.metadata?.["gc.root_store_ref"]);if(s?.scopeKind==="rig"){n.add(s.scopeRef);continue}const u=Zm(o.metadata);u?.scopeKind==="rig"&&n.add(u.scopeRef)}return Array.from(n).sort()}function L6(e,n){const o=new Set;for(const s of e)o.add(s);for(const s of n)o.add(s);return[...o]}function _c(e,n,o){let s=null;const u=new Promise((p,d)=>{s=setTimeout(()=>{d(new Error(`${n} timed out after ${o}ms`))},o)});return Promise.race([e.finally(()=>{s!==null&&clearTimeout(s)}),u])}function D6(e){const n=new Map;for(const o of e)n.has(o.id)||n.set(o.id,o);return Array.from(n.values())}function wc(e){return e.map(M6)}function M6(e){const n={id:e.id,title:e.title,status:e.status,issue_type:e.issue_type,priority:e.priority??null,created_at:e.created_at};return e.description!==void 0&&(n.description=e.description),e.assignee!==void 0&&(n.assignee=e.assignee),Array.isArray(e.labels)&&(n.labels=e.labels),e.metadata!==void 0&&(n.metadata=e.metadata),e.ref!==void 0&&(n.ref=e.ref),e.parent!==void 0&&(n.parent=e.parent),e.from!==void 0&&(n.from=e.from),e.ephemeral!==void 0&&(n.ephemeral=e.ephemeral),e.needs!==void 0&&(n.needs=e.needs),e.dependencies!==void 0&&(n.dependencies=e.dependencies),e.updated_at!==void 0&&(n.updated_at=e.updated_at),n}function F6(e){return e.partial===!0||(e.partial_errors?.length??0)>0}function Ec(e,n){return e instanceof Error&&e.message.trim().length>0?e.message:n}const Tm=1e4,U6=[2e3,5e3,1e4];function Z6(){const e=Ua(),n=b.useRef(null),o=b.useRef(!1),s=b.useCallback(async()=>{const Y=await B6().catch(ce=>({source:"runs",status:"error",error:ce instanceof Error?ce.message:"formula runs unavailable"}));if(Y.status!=="error")return o.current=!1,Y;const ue=n.current;return ue===null?Y:(o.current=!0,{...ue,status:"stale"})},[]),u=b.useCallback(async()=>{const Y=await P6().catch(ce=>({source:"runs",status:"error",error:ce instanceof Error?ce.message:"formula runs unavailable"}));if(Y.status!=="error"){const ce=n.current,pe=ce?.data.historicalLanes??[],Te=new Set([...Y.data.lanes.map(Oe=>Oe.id),...Y.data.blockedLanes.map(Oe=>Oe.id)]),ye=pe.filter(Oe=>!Te.has(Oe.id)),Ae=pe.length-ye.length,Ke=Math.max(0,(ce?.data.totalHistorical??0)-Ae);return{...Y,data:{...Y.data,historicalLanes:ye,totalHistorical:Ke}}}const ue=n.current;return ue===null?Y:(o.current=!0,{...ue,status:"stale"})},[]),{data:p,loading:d,error:m,refresh:h,cheapRefresh:y}=mn(`runs:summary:${e??"no-city"}`,A6,{refreshFetcher:s,sseRefreshFetcher:u});p!==void 0&&p.status!=="error"&&(n.current=p);const w=p??null,I=b.useRef(null);I.current=w?.status??null;const z=b.useRef(d);z.current=d;const B=b.useRef(0),D=b.useRef(null);b.useEffect(()=>{if(w===null||w.status==="error")return;const Y=e??"no-city";D.current!==Y&&(D.current=Y,h().catch(()=>{D.current=null}))},[e,h,w]);const V=b.useRef(0);b.useEffect(()=>{if(w===null)return;if(!(w.status==="error"?!0:o.current||w.data.lanesPartial===!0&&w.data.lanes.length===0&&w.data.blockedLanes.length===0)){V.current=0;return}const ue=U6[V.current];if(ue===void 0)return;V.current+=1;const ce=setTimeout(()=>{h()},ue);return()=>clearTimeout(ce)},[w,h]);const A=b.useRef(!1),H=b.useRef(null),oe=b.useCallback(()=>{H.current!==null&&(clearTimeout(H.current),H.current=null),B.current=Date.now(),y().catch(()=>{B.current=0})},[y]),Q=b.useCallback(()=>{if(I.current===null||I.current==="fixture")return;if(z.current){A.current=!0;return}Date.now()-B.current{if(d||!A.current)return;A.current=!1;const Y=Math.max(0,Tm-(Date.now()-B.current));return H.current=setTimeout(oe,Y),()=>{H.current!==null&&(clearTimeout(H.current),H.current=null)}},[d,oe]);const K=E6([B2.bead],Q);return{source:p,loading:d,error:m,refresh:h,sseState:K}}const Ah=b.createContext(null);function q6({children:e}){const n=Z6();return $.jsx(Ah.Provider,{value:n,children:e})}function V6(){const e=b.useContext(Ah);if(e===null)throw new Error("useRunSummary must be used within a RunSummaryProvider");return e}const W6=b.lazy(()=>En(()=>import("./Agents-40RuA321.js"),__vite__mapDeps([7,8,1,9,10,2,11,3,12,6,13,14])).then(e=>({default:e.AgentsPage}))),H6=b.lazy(()=>En(()=>import("./AgentDetail-B_PEx1iU.js"),__vite__mapDeps([15,16,6,17,12,3,10,2,8,14])).then(e=>({default:e.AgentDetailPage}))),K6=b.lazy(()=>En(()=>import("./AmbientHome-Cvjm2Kmk.js"),__vite__mapDeps([18,2])).then(e=>({default:e.AmbientHomePage}))),G6=b.lazy(()=>En(()=>import("./Beads-BVqefDvL.js"),__vite__mapDeps([19,1,16,6,17,12,3,10,20,9,2,14])).then(e=>({default:e.BeadsPage}))),J6=b.lazy(()=>En(()=>import("./Mail-um-BH4TD.js"),__vite__mapDeps([21,9,1,20,13,10,2,17,3])).then(e=>({default:e.MailPage}))),Q6=b.lazy(()=>En(()=>import("./FormulaRunDetail-BXhub1du.js"),__vite__mapDeps([22,2,16,6,17,12,3,10,23])).then(e=>({default:e.FormulaRunDetailPage}))),Y6=b.lazy(()=>En(()=>import("./Runs-CTNTp8Tf.js"),__vite__mapDeps([24,1,2,11,3,23])).then(e=>({default:e.RunsPage})));function X6(){const{data:e,error:n}=mn("config",()=>oi.config()),o=e?.enabledModules??null,s=e?.defaultView??null,u=Gk(e,n),p=Zk(e),d=b.useMemo(()=>kh(Sh,o),[o]),m=b.useMemo(()=>p6(d,s),[d,s]),h=m.view?.element??null,y=m.redirectTo??null;return $.jsx(Uk,{operator:p,children:$.jsx(o6,{children:$.jsx(y6,{children:$.jsx(Hk,{readOnly:u,children:$.jsx(q6,{children:$.jsx(eb,{operator:p,children:$.jsxs(g6,{children:[n!==null&&$.jsx(nb,{message:n}),$.jsx(tb,{defaultRedirectTo:y,DefaultViewElement:h,enabledViews:d})]})})})})})})})}function eb({operator:e,children:n}){const{source:o}=V6(),s=kk(e,o);return $.jsx(c3,{contributors:s,children:n})}function tb({defaultRedirectTo:e,DefaultViewElement:n,enabledViews:o}){const{pathname:s}=wn();return $.jsx(fh,{children:$.jsx(b.Suspense,{fallback:null,children:$.jsxs(Ay,{children:[$.jsx(on,{path:"/",element:e!==null?$.jsx(Py,{to:e,replace:!0}):n!==null?$.jsx(n,{}):$.jsx(K6,{})}),$.jsx(on,{path:"/agents",element:$.jsx(W6,{})}),$.jsx(on,{path:"/agents/:slug",element:$.jsx(H6,{})}),$.jsx(on,{path:"/beads",element:$.jsx(G6,{})}),$.jsx(on,{path:"/runs",element:$.jsx(Y6,{})}),$.jsx(on,{path:"/runs/:runId",element:$.jsx(Q6,{})}),$.jsx(on,{path:"/mail",element:$.jsx(J6,{})}),o.map(u=>{const p=u.element;return $.jsx(on,{path:u.path,element:$.jsx(p,{})},u.id)}),$.jsx(on,{path:"*",element:$.jsx(rb,{})})]})})},s)}function nb({message:e}){return $.jsxs("section",{role:"alert",className:"mb-8 border border-warn/40 rounded-sm px-4 py-3 text-body text-fg-muted",children:[$.jsx("span",{className:"text-warn",children:"config unavailable:"})," ",e," · some controls may be disabled until it loads."]})}function rb(){return $.jsxs("section",{"aria-labelledby":"not-found-title",className:"space-y-3",children:[$.jsx("h1",{id:"not-found-title",className:"text-5xl font-semibold tracking-tight text-fg",children:"Page not found"}),$.jsx("p",{className:"text-title text-fg-muted",children:"No dashboard route matches this path."})]})}const ob={default:"border border-rule text-fg-muted hover:text-fg hover:bg-surface-tint",accent:"border border-accent text-accent hover:bg-accent hover:text-surface",quiet:"border border-transparent text-fg-muted hover:text-fg"},ib={sm:"px-2.5 py-1 text-label uppercase tracking-wider",md:"px-3.5 py-1.5 text-body"};function ab({tone:e="default",size:n="sm",className:o="",children:s,...u}){return $.jsx("button",{...u,className:`inline-flex items-center gap-1.5 rounded-sm transition-colors duration-150 ease-out-quart focus-mark disabled:opacity-40 disabled:cursor-not-allowed ${ob[e]} ${ib[n]} ${o}`,children:s})}const sb="https://docs.gascity.com/getting-started/quickstart",lb=/^\/city\/([^/]+)(?:\/|$)/;function ub(e){const n=lb.exec(e);if(n===null)return null;const o=n[1];if(o===void 0)return null;let s;try{s=decodeURIComponent(o)}catch{return null}return Ym.test(s)?{cityName:s,basename:`/city/${o}`}:null}function cb(){const e=b.useMemo(()=>ub(window.location.pathname),[]),[n,o]=b.useState({phase:"loading"}),[s,u]=b.useState(0),p=b.useCallback(()=>{o({phase:"loading"}),u(d=>d+1)},[]);return b.useEffect(()=>{let d=!1;return o({phase:"loading"}),it().listCities().then(m=>{if(d)return;const h=m.items??[];if(e!==null){const w=h.some(I=>I.name===e.cityName);o(w?{phase:"mount"}:{phase:"unknown-city",cities:h});return}const y=h[0];if(y===void 0){o({phase:"empty"});return}window.location.replace(`/city/${encodeURIComponent(y.name)}/`)}).catch(m=>{if(!d){if(e!==null){o({phase:"mount"});return}o({phase:"error",message:m instanceof Error?m.message:"failed to load cities"})}}),()=>{d=!0}},[e,s]),e!==null&&n.phase==="mount"?(L2(e.cityName),$.jsx(Zy,{basename:e.basename,future:{v7_relativeSplatPath:!0,v7_startTransition:!0},children:$.jsx(X6,{})})):n.phase==="unknown-city"&&e!==null?$.jsx(db,{cityName:e.cityName,cities:n.cities}):n.phase==="empty"?$.jsx(pb,{}):n.phase==="error"?$.jsx(fb,{message:n.message,onRetry:p}):$.jsx(Ja,{children:$.jsx("div",{className:"text-label uppercase tracking-wider text-fg-muted",children:"Resolving city…"})})}function Ja({children:e}){return $.jsx("div",{className:"min-h-screen bg-surface text-fg antialiased flex items-center justify-center px-6",children:$.jsx("div",{className:"max-w-prose w-full space-y-4",children:e})})}function db({cityName:e,cities:n}){return $.jsx(Ja,{children:$.jsxs("section",{role:"alert",className:"space-y-4",children:[$.jsxs("h1",{className:"text-display font-semibold text-fg",children:["City “",e,"” is not registered on this supervisor."]}),n.length>0?$.jsxs("div",{className:"space-y-2",children:[$.jsx("p",{className:"text-body text-fg-muted",children:"Available cities:"}),$.jsx("ul",{className:"space-y-1",children:n.map(o=>$.jsxs("li",{children:[$.jsx("a",{href:`/city/${encodeURIComponent(o.name)}/`,className:"text-body text-accent hover:underline focus-mark",children:o.name}),o.running?null:$.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted ml-2",children:"· stopped"})]},o.name))})]}):$.jsx(Oh,{})]})})}function pb(){return $.jsx(Ja,{children:$.jsxs("section",{className:"space-y-4",children:[$.jsx("h1",{className:"text-display font-semibold text-fg",children:"No cities are registered on this supervisor."}),$.jsx(Oh,{})]})})}function Oh(){return $.jsxs("div",{className:"space-y-3",children:[$.jsx("p",{className:"text-body text-fg-muted",children:"Create one from a terminal:"}),$.jsx("pre",{className:"text-body bg-surface-tint rounded-sm px-3 py-2 overflow-x-auto",children:$.jsx("code",{children:"gc init ~/my-city"})}),$.jsxs("p",{className:"text-body text-fg-muted",children:[$.jsx("code",{children:"gc init"})," bootstraps the city directory, registers it with the supervisor, and starts the orchestrator. Then refresh this page. See the"," ",$.jsx("a",{href:sb,target:"_blank",rel:"noreferrer",className:"text-accent hover:underline focus-mark",children:"getting-started guide"})," ","for the full walkthrough."]})]})}function fb({message:e,onRetry:n}){return $.jsx(Ja,{children:$.jsxs("section",{role:"alert",className:"space-y-4",children:[$.jsx("h1",{className:"text-display font-semibold text-fg",children:"Could not load cities."}),$.jsx("p",{className:"text-body text-fg-muted",children:e}),$.jsx(ab,{onClick:n,children:"Retry"})]})})}const jh=document.getElementById("root");if(!jh)throw new Error("missing #root");Z0.createRoot(jh).render($.jsx(Rm.StrictMode,{children:$.jsx(Mk,{children:$.jsx(fh,{children:$.jsx(cb,{})})})}));export{hb as $,pc as A,ab as B,t3 as C,mh as D,vh as E,G7 as F,B2 as G,jb as H,Ua as I,it as J,Mt as K,Wy as L,vb as M,Yk as N,Nb as O,dc as P,xS as Q,Pb as R,Wk as S,zb as T,O2 as U,A2 as V,bb as W,kb as X,ro as Y,_b as Z,qm as _,d3 as a,gb as a0,yb as a1,f7 as a2,y7 as a3,O7 as a4,U7 as a5,Xy as a6,cc as a7,k6 as a8,e6 as a9,vn as aa,oi as ab,Ql as ac,Ba as ad,Sb as ae,Th as af,Rb as ag,Qy as ah,Ab as ai,n6 as aj,_m as ak,wb as al,m7 as am,V6 as an,US as ao,ZS as ap,mn as b,yS as c,dS as d,l7 as e,E6 as f,Kk as g,Eb as h,xh as i,$ as j,xb as k,Xk as l,DS as m,Cb as n,Tb as o,Vr as p,mb as q,b as r,Bb as s,wu as t,Ob as u,i6 as v,wh as w,Kr as x,Ib as y,Gt as z}; diff --git a/internal/api/dashboardspa/dist/assets/projectOf-nApq7eyo.js b/internal/api/dashboardspa/dist/assets/projectOf-CAOn7SI-.js similarity index 92% rename from internal/api/dashboardspa/dist/assets/projectOf-nApq7eyo.js rename to internal/api/dashboardspa/dist/assets/projectOf-CAOn7SI-.js index 467071540d..e25d500b2c 100644 --- a/internal/api/dashboardspa/dist/assets/projectOf-nApq7eyo.js +++ b/internal/api/dashboardspa/dist/assets/projectOf-CAOn7SI-.js @@ -1 +1 @@ -import{j as c,I as R}from"./index-zPatq59W.js";function k({value:e,onChange:t,placeholder:n="Search",matchCount:r,totalCount:i,ariaLabel:a="Search list"}){const d=e.length>0&&typeof r=="number"&&typeof i=="number";return c.jsxs("div",{className:"flex items-baseline gap-3 border-b border-rule pb-1",children:[c.jsx("input",{type:"search",value:e,onChange:m=>t(m.target.value),placeholder:n,"aria-label":a,className:"flex-1 bg-transparent border-0 text-body text-fg placeholder:text-fg-faint focus:outline-none focus:ring-0 px-0 py-0.5"}),d&&c.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:[r," / ",i]})]})}const b=/^(.+?)-[a-z0-9]+(?:\.\d+)?$/i;function C(e){return b.exec(e.id)?.[1]??e.id}const o="Orchestration";function u(){return R()??o}const s="(no rig)",l="Maintenance",E=new Set(["mayor","control-dispatcher","oversight-rig.chief-of-staff"]);function g(e){return e.rig&&e.rig.length>0?!1:!!e.template&&E.has(e.template)}const p=/\/control-dispatcher$/;function O(e){return!e.rig||e.rig.length===0?!1:p.test(e.alias??"")}const _=/(?:worker|polecat)(?:-\d+)?$/,f=/(?:\.project-lead|chief-of-staff)$/;function j(e){if(e.state!=="active"&&e.state!=="running"||g(e)||O(e))return!1;const t=e.template??"",n=e.alias??"";if(f.test(t)||f.test(n))return!1;const r=e.session_name;return[t,n,r].filter(a=>a.length>0).map(a=>I(a)).some(a=>_.test(a))}function h(e){return e.toLowerCase().replace(/_/g,"-")}function L(e){if(g(e))return{key:o,label:u()};const t=e.rig??e.pool??e.template;if(!t)return{key:s,label:s};const n=t.split(/[\\/]/).filter(Boolean),r=n[n.length-1]??t;return{key:h(r),label:r}}function w(e){return e.rig&&e.rig.length>0?e.rig:s}const S=new Set(["mayor","control-dispatcher","oversight-rig.chief-of-staff"]),N=new Set(["dog"]);function y(e){return e.rig&&e.rig.length>0?!1:S.has(e.name)}function v(e){return!e.rig||e.rig.length===0?!1:p.test(e.name)}function A(e){if(y(e))return{key:o,label:u()};const t=e.rig&&e.rig.length>0?e.rig:void 0;if(!t&&e.pool&&N.has(e.pool))return{key:l,label:l};const n=t??e.pool;if(!n)return{key:s,label:s};const r=n.split(/[\\/]/).filter(Boolean),i=T(r[r.length-1]??n);return{key:h(i),label:i}}function T(e){return e.endsWith("-main")?e.slice(0,-5):e}const x=/-(?:gc|td|th|[a-z]{4})-[a-z0-9]*[0-9][a-z0-9]*$/;function I(e){const t=e.trim(),n=t.split(/[\\/]/).filter(Boolean),r=n[n.length-1]??t,i=r.replace(x,"");return i.length>0?i:r}function X(e){const{key:t}=A(e);return t===o||t===l||t===s}export{k as L,T as a,X as b,I as c,A as d,v as e,C as f,j as i,w as m,L as s}; +import{j as c,H as R}from"./index-QWRimsO3.js";function k({value:e,onChange:t,placeholder:n="Search",matchCount:r,totalCount:i,ariaLabel:a="Search list"}){const d=e.length>0&&typeof r=="number"&&typeof i=="number";return c.jsxs("div",{className:"flex items-baseline gap-3 border-b border-rule pb-1",children:[c.jsx("input",{type:"search",value:e,onChange:m=>t(m.target.value),placeholder:n,"aria-label":a,className:"flex-1 bg-transparent border-0 text-body text-fg placeholder:text-fg-faint focus:outline-none focus:ring-0 px-0 py-0.5"}),d&&c.jsxs("span",{className:"text-label uppercase tracking-wider text-fg-faint tnum",children:[r," / ",i]})]})}const b=/^(.+?)-[a-z0-9]+(?:\.\d+)?$/i;function C(e){return b.exec(e.id)?.[1]??e.id}const o="Orchestration";function u(){return R()??o}const s="(no rig)",l="Maintenance",E=new Set(["mayor","control-dispatcher","oversight-rig.chief-of-staff"]);function g(e){return e.rig&&e.rig.length>0?!1:!!e.template&&E.has(e.template)}const p=/\/control-dispatcher$/;function O(e){return!e.rig||e.rig.length===0?!1:p.test(e.alias??"")}const _=/(?:worker|polecat)(?:-\d+)?$/,f=/(?:\.project-lead|chief-of-staff)$/;function j(e){if(e.state!=="active"&&e.state!=="running"||g(e)||O(e))return!1;const t=e.template??"",n=e.alias??"";if(f.test(t)||f.test(n))return!1;const r=e.session_name;return[t,n,r].filter(a=>a.length>0).map(a=>I(a)).some(a=>_.test(a))}function h(e){return e.toLowerCase().replace(/_/g,"-")}function L(e){if(g(e))return{key:o,label:u()};const t=e.rig??e.pool??e.template;if(!t)return{key:s,label:s};const n=t.split(/[\\/]/).filter(Boolean),r=n[n.length-1]??t;return{key:h(r),label:r}}function w(e){return e.rig&&e.rig.length>0?e.rig:s}const S=new Set(["mayor","control-dispatcher","oversight-rig.chief-of-staff"]),N=new Set(["dog"]);function y(e){return e.rig&&e.rig.length>0?!1:S.has(e.name)}function v(e){return!e.rig||e.rig.length===0?!1:p.test(e.name)}function A(e){if(y(e))return{key:o,label:u()};const t=e.rig&&e.rig.length>0?e.rig:void 0;if(!t&&e.pool&&N.has(e.pool))return{key:l,label:l};const n=t??e.pool;if(!n)return{key:s,label:s};const r=n.split(/[\\/]/).filter(Boolean),i=T(r[r.length-1]??n);return{key:h(i),label:i}}function T(e){return e.endsWith("-main")?e.slice(0,-5):e}const x=/-(?:gc|td|th|[a-z]{4})-[a-z0-9]*[0-9][a-z0-9]*$/;function I(e){const t=e.trim(),n=t.split(/[\\/]/).filter(Boolean),r=n[n.length-1]??t,i=r.replace(x,"");return i.length>0?i:r}function H(e){const{key:t}=A(e);return t===o||t===l||t===s}export{k as L,T as a,H as b,I as c,A as d,v as e,C as f,j as i,w as m,L as s}; diff --git a/internal/api/dashboardspa/dist/assets/useListFilters-C9ZhD4ch.js b/internal/api/dashboardspa/dist/assets/useListFilters-D2HcBe10.js similarity index 98% rename from internal/api/dashboardspa/dist/assets/useListFilters-C9ZhD4ch.js rename to internal/api/dashboardspa/dist/assets/useListFilters-D2HcBe10.js index 4c45065dbe..a9b3c9b5b6 100644 --- a/internal/api/dashboardspa/dist/assets/useListFilters-C9ZhD4ch.js +++ b/internal/api/dashboardspa/dist/assets/useListFilters-D2HcBe10.js @@ -1 +1 @@ -import{j as C,r as g,D as Y,E as D,x as tt,p as et}from"./index-zPatq59W.js";function gt({chips:e,activeIds:n,onToggle:a,legend:p}){return e.length===0?null:C.jsxs("div",{className:"flex items-baseline gap-4 flex-wrap",children:[p&&C.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:p}),e.map(i=>{const d=n.has(i.id);return C.jsx("button",{type:"button",onClick:()=>a(i.id),"aria-pressed":d,className:`text-label uppercase tracking-wider transition-colors duration-150 ease-out-quart focus-mark rounded-sm ${d?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,children:i.label},i.id)})]})}const st="gcd:listFilters:collapsed:",rt="gcd:listFilters:expanded:",X="gcd:listFilters:sortMode:",m="useListFilters";function B(e,n){return(n?rt:st)+e}function R(e,n){const a=B(e,n),p=Y("localStorage",a,m);if(p.status!=="found")return new Set;try{const i=JSON.parse(p.value);if(Array.isArray(i))return new Set(i.filter(d=>typeof d=="string"))}catch(i){at(a,i)}return new Set}function nt(e,n,a){D("localStorage",B(e,n),JSON.stringify(Array.from(a)),m)}function T(e,n){const a=Y("localStorage",X+e,m);return a.status==="found"&&(a.value==="alpha"||a.value==="activity")?a.value:n}function ot(e,n){D("localStorage",X+e,n,m)}function at(e,n){tt({component:m,operation:"localStorage.parse",message:`${e}: ${et(n)}`})}const ct=[],it=new Set,lt=[];function pt({viewKey:e,rows:n,projectOf:a,searchOf:p,chips:i,initialActiveChipIds:d=lt,defaultCollapsed:f=!1,activityOf:x,defaultSortMode:M="alpha",pinnedProjects:k=ct,nonCollapsibleProjects:I=it}){const $=d.join(","),[N,L]=g.useState(""),[P,_]=g.useState(()=>new Set(d)),[w,A]=g.useState(()=>R(e,f)),[F,v]=g.useState(()=>T(e,M));g.useEffect(()=>{A(R(e,f)),v(T(e,M)),L(""),_(new Set(d))},[e,f,M,$]);const H=g.useCallback(r=>{_(S=>{const l=new Set(S);return l.has(r)?l.delete(r):l.add(r),l})},[]),J=g.useCallback(r=>{A(S=>{const l=new Set(S);return l.has(r)?l.delete(r):l.add(r),nt(e,f,l),l})},[e,f]),U=g.useCallback(r=>w.has(r)?!f:f,[w,f]),q=g.useCallback(r=>{v(r),ot(e,r)},[e]),y=g.useMemo(()=>{const r=N.trim().toLowerCase(),S=i.filter(t=>P.has(t.id)),l=t=>{if(r.length===0)return!0;for(const s of p(t))if(s&&s.toLowerCase().includes(r))return!0;return!1},Z=t=>{if(S.length===0)return!0;for(const s of S)if(s.match(t))return!0;return!1},b=new Map;for(const t of n){if(!l(t)||!Z(t))continue;const s=a(t),o=typeof s=="string"?s:s.key,c=typeof s=="string"?s:s.label,u=b.get(o);u?(u.rows.push(t),u.labelCounts.set(c,(u.labelCounts.get(c)??0)+1)):b.set(o,{rows:[t],labelCounts:new Map([[c,1]])})}const z=t=>{let s="",o=-1,c=!1;for(const[u,h]of t){const j=/[A-Z]/.test(u);(h>o||h===o&&j&&!c)&&(s=u,o=h,c=j)}return s},G=Array.from(b.keys()),O=k.filter(t=>b.has(t)),Q=new Set(O),E=G.filter(t=>!Q.has(t));if(F==="activity"&&x){const t=new Map;for(const s of E){const o=b.get(s);let c=-1/0;if(o)for(const u of o.rows){const h=x(u);typeof h=="number"&&Number.isFinite(h)&&h>c&&(c=h)}t.set(s,c)}E.sort((s,o)=>{const c=t.get(s)??-1/0,u=t.get(o)??-1/0;return c!==u?u-c:s.localeCompare(o)})}else E.sort();const V=[...O,...E],W=t=>I.has(t)?!1:w.has(t)?!f:f;return V.map(t=>{const s=b.get(t),o=s?.rows??[];return{project:s?z(s.labelCounts):t,projectKey:t,rows:o,totalInProject:o.length,collapsed:W(t),collapsible:!I.has(t)}})},[n,N,P,i,a,p,w,f,F,x,k,I]),K=g.useMemo(()=>y.reduce((r,S)=>r+S.totalInProject,0),[y]);return{search:N,setSearch:L,activeChipIds:P,toggleChip:H,isCollapsed:U,toggleProject:J,sortMode:F,setSortMode:q,groups:y,totalMatches:K}}export{gt as F,pt as u}; +import{j as C,r as g,D as Y,E as D,x as tt,p as et}from"./index-QWRimsO3.js";function gt({chips:e,activeIds:n,onToggle:a,legend:p}){return e.length===0?null:C.jsxs("div",{className:"flex items-baseline gap-4 flex-wrap",children:[p&&C.jsx("span",{className:"text-label uppercase tracking-wider text-fg-muted",children:p}),e.map(i=>{const d=n.has(i.id);return C.jsx("button",{type:"button",onClick:()=>a(i.id),"aria-pressed":d,className:`text-label uppercase tracking-wider transition-colors duration-150 ease-out-quart focus-mark rounded-sm ${d?"text-fg font-semibold underline decoration-fg underline-offset-4":"text-fg-muted hover:text-fg"}`,children:i.label},i.id)})]})}const st="gcd:listFilters:collapsed:",rt="gcd:listFilters:expanded:",X="gcd:listFilters:sortMode:",m="useListFilters";function B(e,n){return(n?rt:st)+e}function R(e,n){const a=B(e,n),p=Y("localStorage",a,m);if(p.status!=="found")return new Set;try{const i=JSON.parse(p.value);if(Array.isArray(i))return new Set(i.filter(d=>typeof d=="string"))}catch(i){at(a,i)}return new Set}function nt(e,n,a){D("localStorage",B(e,n),JSON.stringify(Array.from(a)),m)}function T(e,n){const a=Y("localStorage",X+e,m);return a.status==="found"&&(a.value==="alpha"||a.value==="activity")?a.value:n}function ot(e,n){D("localStorage",X+e,n,m)}function at(e,n){tt({component:m,operation:"localStorage.parse",message:`${e}: ${et(n)}`})}const ct=[],it=new Set,lt=[];function pt({viewKey:e,rows:n,projectOf:a,searchOf:p,chips:i,initialActiveChipIds:d=lt,defaultCollapsed:f=!1,activityOf:x,defaultSortMode:M="alpha",pinnedProjects:k=ct,nonCollapsibleProjects:I=it}){const $=d.join(","),[N,L]=g.useState(""),[P,_]=g.useState(()=>new Set(d)),[w,A]=g.useState(()=>R(e,f)),[F,v]=g.useState(()=>T(e,M));g.useEffect(()=>{A(R(e,f)),v(T(e,M)),L(""),_(new Set(d))},[e,f,M,$]);const H=g.useCallback(r=>{_(S=>{const l=new Set(S);return l.has(r)?l.delete(r):l.add(r),l})},[]),J=g.useCallback(r=>{A(S=>{const l=new Set(S);return l.has(r)?l.delete(r):l.add(r),nt(e,f,l),l})},[e,f]),U=g.useCallback(r=>w.has(r)?!f:f,[w,f]),q=g.useCallback(r=>{v(r),ot(e,r)},[e]),y=g.useMemo(()=>{const r=N.trim().toLowerCase(),S=i.filter(t=>P.has(t.id)),l=t=>{if(r.length===0)return!0;for(const s of p(t))if(s&&s.toLowerCase().includes(r))return!0;return!1},Z=t=>{if(S.length===0)return!0;for(const s of S)if(s.match(t))return!0;return!1},b=new Map;for(const t of n){if(!l(t)||!Z(t))continue;const s=a(t),o=typeof s=="string"?s:s.key,c=typeof s=="string"?s:s.label,u=b.get(o);u?(u.rows.push(t),u.labelCounts.set(c,(u.labelCounts.get(c)??0)+1)):b.set(o,{rows:[t],labelCounts:new Map([[c,1]])})}const z=t=>{let s="",o=-1,c=!1;for(const[u,h]of t){const j=/[A-Z]/.test(u);(h>o||h===o&&j&&!c)&&(s=u,o=h,c=j)}return s},G=Array.from(b.keys()),O=k.filter(t=>b.has(t)),Q=new Set(O),E=G.filter(t=>!Q.has(t));if(F==="activity"&&x){const t=new Map;for(const s of E){const o=b.get(s);let c=-1/0;if(o)for(const u of o.rows){const h=x(u);typeof h=="number"&&Number.isFinite(h)&&h>c&&(c=h)}t.set(s,c)}E.sort((s,o)=>{const c=t.get(s)??-1/0,u=t.get(o)??-1/0;return c!==u?u-c:s.localeCompare(o)})}else E.sort();const V=[...O,...E],W=t=>I.has(t)?!1:w.has(t)?!f:f;return V.map(t=>{const s=b.get(t),o=s?.rows??[];return{project:s?z(s.labelCounts):t,projectKey:t,rows:o,totalInProject:o.length,collapsed:W(t),collapsible:!I.has(t)}})},[n,N,P,i,a,p,w,f,F,x,k,I]),K=g.useMemo(()=>y.reduce((r,S)=>r+S.totalInProject,0),[y]);return{search:N,setSearch:L,activeChipIds:P,toggleChip:H,isCollapsed:U,toggleProject:J,sortMode:F,setSortMode:q,groups:y,totalMatches:K}}export{gt as F,pt as u}; diff --git a/internal/api/dashboardspa/dist/assets/useVisibleRefresh-DdfUjLcH.js b/internal/api/dashboardspa/dist/assets/useVisibleRefresh-Bm_cCiAg.js similarity index 92% rename from internal/api/dashboardspa/dist/assets/useVisibleRefresh-DdfUjLcH.js rename to internal/api/dashboardspa/dist/assets/useVisibleRefresh-Bm_cCiAg.js index a85244da81..dce98c19e3 100644 --- a/internal/api/dashboardspa/dist/assets/useVisibleRefresh-DdfUjLcH.js +++ b/internal/api/dashboardspa/dist/assets/useVisibleRefresh-Bm_cCiAg.js @@ -1 +1 @@ -import{r}from"./index-zPatq59W.js";const F=2e3,w=6e4;function h(e,u,s={}){const a=r.useRef(e);a.current=e;const n=r.useRef(l(s));n.current=l(s);const t=r.useRef(0),c=r.useRef(0),o=r.useRef(!1),{enabled:i,initialBackoffMs:d,maxBackoffMs:B}=n.current;r.useEffect(()=>{if(!i)return;const M=()=>{t.current=0,c.current=0},R=A=>{const f=n.current;f.onError?.(A);const E=Math.min(f.initialBackoffMs*2**t.current,f.maxBackoffMs);t.current+=1,c.current=Date.now()+E},k=()=>{document.hidden||o.current||Date.now(){o.current=!1}))},m=window.setInterval(k,u);return()=>window.clearInterval(m)},[i,u,d,B])}function l(e){return{enabled:e.enabled??!0,initialBackoffMs:e.initialBackoffMs??F,maxBackoffMs:e.maxBackoffMs??w,onError:e.onError??x}}function x(){}export{h as u}; +import{r}from"./index-QWRimsO3.js";const F=2e3,w=6e4;function h(e,u,s={}){const a=r.useRef(e);a.current=e;const n=r.useRef(l(s));n.current=l(s);const t=r.useRef(0),c=r.useRef(0),o=r.useRef(!1),{enabled:i,initialBackoffMs:d,maxBackoffMs:B}=n.current;r.useEffect(()=>{if(!i)return;const M=()=>{t.current=0,c.current=0},R=A=>{const f=n.current;f.onError?.(A);const E=Math.min(f.initialBackoffMs*2**t.current,f.maxBackoffMs);t.current+=1,c.current=Date.now()+E},k=()=>{document.hidden||o.current||Date.now(){o.current=!1}))},m=window.setInterval(k,u);return()=>window.clearInterval(m)},[i,u,d,B])}function l(e){return{enabled:e.enabled??!0,initialBackoffMs:e.initialBackoffMs??F,maxBackoffMs:e.maxBackoffMs??w,onError:e.onError??x}}function x(){}export{h as u}; diff --git a/internal/api/dashboardspa/dist/index.html b/internal/api/dashboardspa/dist/index.html index c712be5f3c..9a614dd130 100644 --- a/internal/api/dashboardspa/dist/index.html +++ b/internal/api/dashboardspa/dist/index.html @@ -20,7 +20,7 @@ } catch (_) {} })(); - + diff --git a/internal/api/dashboardspa/web/frontend/src/api/client.test.ts b/internal/api/dashboardspa/web/frontend/src/api/client.test.ts index 6047dac996..83a48ac7b6 100644 --- a/internal/api/dashboardspa/web/frontend/src/api/client.test.ts +++ b/internal/api/dashboardspa/web/frontend/src/api/client.test.ts @@ -257,3 +257,131 @@ describe('api client error handling', () => { }); }); }); + +describe('run projection endpoints', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + const emptyRunSummary = { + totalActive: 0, + totalHistorical: 0, + runCounts: { active: 0, blocked: 0, complete: 0 }, + lanes: [], + historicalLanes: [], + blockedLanes: [], + recentChanges: [], + census: { status: 'unavailable' }, + }; + + it('reads the run summary from the city-scoped BFF endpoint', async () => { + const fetchMock = vi.fn( + async () => + new Response(JSON.stringify(emptyRunSummary), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + await expect(api.runSummary()).resolves.toMatchObject({ totalActive: 0 }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/city/test-city/runs/summary', + expect.objectContaining({ method: 'GET' }), + ); + }); + + it('rejects a run-summary body missing its lane arrays at the edge', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ totalActive: 0, totalHistorical: 0 }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ), + ); + + await expect(api.runSummary()).rejects.toMatchObject({ + name: 'ApiResponseDecodeError', + message: expect.stringContaining('run summary.lanes must be an array'), + }); + }); + + it('reads the run detail from the city-scoped BFF endpoint, encoding the run id', async () => { + const detail = { + runId: 'mol:adopt-1', + rootBeadId: 'b-1', + rootStoreRef: 'rig:demo', + resolvedRootStore: 'rig:demo', + scopeKind: 'rig', + scopeRef: 'demo', + title: 'Adopt PR', + formula: { kind: 'unavailable', reason: 'missing_formula_metadata' }, + formulaDetail: { kind: 'unavailable', reason: 'missing_formula_metadata' }, + executionPath: { kind: 'unavailable', reason: 'missing_cwd_and_rig_root' }, + snapshotVersion: 1, + snapshotEventSeq: { kind: 'known', seq: 100 }, + completeness: { kind: 'complete' }, + progress: { statusCounts: {} }, + phase: 'intake', + stages: [], + nodes: [], + edges: [], + lanes: [], + }; + const fetchMock = vi.fn( + async () => + new Response(JSON.stringify(detail), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + await expect(api.runDetail('mol:adopt-1')).resolves.toMatchObject({ runId: 'mol:adopt-1' }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/city/test-city/runs/mol%3Aadopt-1/detail', + expect.objectContaining({ method: 'GET' }), + ); + }); + + it('surfaces the 422 run-detail reason on the thrown ApiClientError', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ error: 'run is not a graph.v2 run', reason: 'not_run_view' }), { + status: 422, + headers: { 'content-type': 'application/json' }, + }), + ), + ); + + await expect(api.runDetail('v1-run')).rejects.toMatchObject({ + name: 'ApiClientError', + status: 422, + reason: 'not_run_view', + message: 'run is not a graph.v2 run', + }); + }); + + it('surfaces a 404 run-detail as an ApiClientError without a reason', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ error: 'unknown run' }), { + status: 404, + headers: { 'content-type': 'application/json' }, + }), + ), + ); + + const err = await api.runDetail('ghost').catch((e: unknown) => e); + expect(err).toBeInstanceOf(ApiClientError); + expect(err).toMatchObject({ status: 404, message: 'unknown run' }); + expect((err as ApiClientError).reason).toBeUndefined(); + }); +}); diff --git a/internal/api/dashboardspa/web/frontend/src/api/client.ts b/internal/api/dashboardspa/web/frontend/src/api/client.ts index 3e8e848603..d3b967e036 100644 --- a/internal/api/dashboardspa/web/frontend/src/api/client.ts +++ b/internal/api/dashboardspa/web/frontend/src/api/client.ts @@ -12,6 +12,8 @@ import type { RunDiffRequest, RunDiffResponse, RunScopeKind, + RunSummary, + FormulaRunDetail, } from 'gas-city-dashboard-shared'; import { cityPath } from './cityBase'; @@ -55,7 +57,7 @@ async function performRequest( const bodyText = await res.text(); const payload = parseApiErrorBody(bodyText); const message = payload?.error ?? (bodyText.trim() || res.statusText || `HTTP ${res.status}`); - throw new ApiClientError(res.status, message, payload?.kind); + throw new ApiClientError(res.status, message, payload?.kind, payload?.reason); } let json: unknown; try { @@ -80,7 +82,8 @@ function isApiError(value: unknown): value is ApiError { if (typeof value !== 'object' || value === null) return false; const record = value as Record; if (typeof record.error !== 'string') return false; - return record.kind === undefined || typeof record.kind === 'string'; + if (record.kind !== undefined && typeof record.kind !== 'string') return false; + return record.reason === undefined || typeof record.reason === 'string'; } // The /api plane uses the same-origin custom-header CSRF model (X-GC-Request), @@ -103,6 +106,9 @@ export class ApiClientError extends Error { public readonly status: number, message: string, public readonly kind?: string, + // The BFF run-detail discriminator (see ApiError.reason): 'not_run_view' + // vs 'invalid_snapshot' on a 422. Absent on every other endpoint. + public readonly reason?: string, ) { super(message); this.name = 'ApiClientError'; @@ -158,6 +164,10 @@ function requireBooleanField(record: JsonRecord, url: string, label: string, fie if (typeof record[field] !== 'boolean') failDecode(url, `${label}.${field} must be a boolean`); } +function requireNumberField(record: JsonRecord, url: string, label: string, field: string): void { + if (typeof record[field] !== 'number') failDecode(url, `${label}.${field} must be a number`); +} + function requireArrayField(record: JsonRecord, url: string, label: string, field: string): void { if (!Array.isArray(record[field])) failDecode(url, `${label}.${field} must be an array`); } @@ -293,6 +303,46 @@ const decodeRunDiff = objectDecoder('run diff', (record, url) = requireStringField(record, url, 'run diff', 'patch'); requireBooleanField(record, url, 'run diff', 'truncated'); }); +// The run summary/detail DTOs are produced by the Go run projection +// (internal/runproj), which is golden-gated byte-for-byte against these exact +// shapes. Validate the structural arrays/objects the renderers iterate at the +// edge (matching decodeRunDiff's depth) so a wire-shape regression fails here +// rather than mis-rendering deep in a lane or diagram component. +const decodeRunSummary = objectDecoder('run summary', (record, url) => { + // Validate every field a renderer dereferences: RunMap reads the counts, + // the lane arrays, and totalActive/totalHistorical. The DTO is golden-gated + // against the Go projection, so this edge check is defensive — but it is now + // the ONLY backstop (the client-side fold that used to rebuild this is gone). + requireNumberField(record, url, 'run summary', 'totalActive'); + requireNumberField(record, url, 'run summary', 'totalHistorical'); + requireArrayField(record, url, 'run summary', 'lanes'); + requireArrayField(record, url, 'run summary', 'historicalLanes'); + requireArrayField(record, url, 'run summary', 'blockedLanes'); + requireArrayField(record, url, 'run summary', 'recentChanges'); + requireObjectField(record, url, 'run summary', 'runCounts'); + requireObjectField(record, url, 'run summary', 'census'); +}); +const decodeFormulaRunDetail = objectDecoder( + 'formula run detail', + (record, url) => { + requireStringField(record, url, 'formula run detail', 'runId'); + requireObjectField(record, url, 'formula run detail', 'formula'); + requireObjectField(record, url, 'formula run detail', 'formulaDetail'); + requireObjectField(record, url, 'formula run detail', 'executionPath'); + // The detail renderer hard-derefs these union/nested fields (snapshotLabel + // reads snapshotEventSeq.kind, the partial notice reads completeness.kind, + // the status summary reads progress.statusCounts[...]), so validate them at + // the edge rather than let a malformed wire value throw deep in the diagram. + requireObjectField(record, url, 'formula run detail', 'snapshotEventSeq'); + requireObjectField(record, url, 'formula run detail', 'completeness'); + const progress = requireRecord(record['progress'], url, 'formula run detail.progress'); + requireObjectField(progress, url, 'formula run detail.progress', 'statusCounts'); + requireArrayField(record, url, 'formula run detail', 'stages'); + requireArrayField(record, url, 'formula run detail', 'nodes'); + requireArrayField(record, url, 'formula run detail', 'edges'); + requireArrayField(record, url, 'formula run detail', 'lanes'); + }, +); export interface ApiErrorParts { message: string; status?: number; @@ -358,6 +408,20 @@ export const api = { body, ); }, + // The run view reads its summary and per-run detail from the BFF run + // projection (internal/api/dashboardbff/runtailer.go), a sub-second warm + // fold of the city event log that already layers session health/census. + // Both DTOs are the same shapes the SPA used to reconstruct client-side. + runSummary(): Promise { + return request('GET', cityPath('/runs/summary'), decodeRunSummary); + }, + // 200 → FormulaRunDetail. The endpoint rejects a non-graph.v2 run with + // 422 + reason 'not_run_view' (list-only) or 'invalid_snapshot' (load + // failure), an unknown run with 404, and a still-warming projection with + // 503 — surfaced to callers as ApiClientError (status + reason). + runDetail(runId: string): Promise { + return request('GET', cityPath(`/runs/${encodeURIComponent(runId)}/detail`), decodeFormulaRunDetail); + }, }; function runQuery(params?: { scopeKind?: RunScopeKind; scopeRef?: string }): string { diff --git a/internal/api/dashboardspa/web/frontend/src/components/run/RunMap.test.tsx b/internal/api/dashboardspa/web/frontend/src/components/run/RunMap.test.tsx index 93fa043b39..af8ce68f91 100644 --- a/internal/api/dashboardspa/web/frontend/src/components/run/RunMap.test.tsx +++ b/internal/api/dashboardspa/web/frontend/src/components/run/RunMap.test.tsx @@ -1,12 +1,35 @@ import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { afterEach, describe, expect, it } from 'vitest'; -import { emptyRunSummary, MAX_VISIBLE_ACTIVE_LANES } from 'gas-city-dashboard-shared'; +import { MAX_VISIBLE_ACTIVE_LANES } from 'gas-city-dashboard-shared'; import type { RunLane, RunSummary, SourceState } from 'gas-city-dashboard-shared'; import { RunMap } from './RunMap'; afterEach(() => cleanup()); +// A blank server-shaped RunSummary for fixtures (the old shared emptyRunSummary +// helper retired with the client-side fold; the wire now produces this shape). +function emptyRunSummary(): RunSummary { + return { + totalActive: 0, + totalHistorical: 0, + runCounts: { + total: 0, + visible: 0, + prReview: 0, + designReview: 0, + bugfix: 0, + blocked: 0, + other: 0, + }, + lanes: [], + historicalLanes: [], + blockedLanes: [], + recentChanges: [], + census: { status: 'unavailable', error: 'run health has not been derived' }, + }; +} + function historicalLane(id: string): RunLane { return { id, diff --git a/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.test.tsx b/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.test.tsx index 8ba139357c..d24c3af929 100644 --- a/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.test.tsx +++ b/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.test.tsx @@ -1,10 +1,10 @@ import { cleanup, renderHook, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; +import type { FormulaRunDetail } from 'gas-city-dashboard-shared'; import { invalidate } from '../api/cache'; +import { ApiClientError } from '../api/client'; import { reportClientError } from '../lib/clientErrorReporting'; -import { supervisorApi, supervisorApiForRequestBudget } from '../supervisor/client'; -import type * as SupervisorClient from '../supervisor/client'; -import { SupervisorApiError } from '../supervisor/errors'; +import { loadSupervisorFormulaRunDetail } from '../supervisor/runDetail'; import { formulaRunDetailCacheKey, useFormulaRunDetail } from './useFormulaRunDetail'; vi.mock('../api/cityBase', () => ({ @@ -16,44 +16,63 @@ vi.mock('../lib/clientErrorReporting', () => ({ reportClientError: vi.fn(() => Promise.resolve({ status: 'reported' })), })); -vi.mock('../supervisor/client', async (importOriginal) => { - const actual = await importOriginal(); - return { - // Keep the real SupervisorApiError so runDetail's `instanceof` checks work, - // and route the request-budget client to the same mock as the default one. - ...actual, - supervisorApi: vi.fn(), - supervisorApiForRequestBudget: vi.fn(), - }; -}); +// The run-detail loader is now a thin BFF GET (covered by runDetail.test.ts); +// the hook's job is purely mapping its result/errors onto the view states, so +// mock the loader directly and drive each state from what it resolves/throws. +vi.mock('../supervisor/runDetail', () => ({ + loadSupervisorFormulaRunDetail: vi.fn(), +})); const mockReportClientError = reportClientError as Mock; -const mockSupervisorApi = supervisorApi as Mock; -const mockSupervisorApiForRequestBudget = supervisorApiForRequestBudget as Mock; -const supervisor = { - workflowRun: vi.fn(), - listSessions: vi.fn(), - formulaDetail: vi.fn(), -}; +const mockLoadDetail = loadSupervisorFormulaRunDetail as Mock; + +function runDetail(overrides: Partial = {}): FormulaRunDetail { + return { + runId: 'wf-1', + rootBeadId: 'wf-1', + rootStoreRef: 'city:test-city', + resolvedRootStore: 'city:test-city', + scopeKind: 'city', + scopeRef: 'test-city', + title: 'Direct supervisor run', + formula: { kind: 'known', name: 'mol-test', source: 'metadata' }, + formulaDetail: { kind: 'available', name: 'mol-test', target: 'test-city/codex' }, + executionPath: { kind: 'unavailable', reason: 'missing_cwd_and_rig_root' }, + snapshotVersion: 1, + snapshotEventSeq: { kind: 'known', seq: 100 }, + completeness: { kind: 'complete' }, + progress: { + snapshotVersion: 1, + snapshotEventSeq: { kind: 'known', seq: 100 }, + snapshotPartial: false, + totalNodeCount: 0, + visibleNodeCount: 0, + edgeCount: 0, + executionInstanceCount: 0, + sessionLinkCount: 0, + streamableSessionCount: 0, + streamableSessionIds: [], + statusCounts: {}, + allStatusCounts: {}, + }, + phase: 'intake', + stages: [], + nodes: [], + edges: [], + lanes: [], + ...overrides, + }; +} afterEach(() => { cleanup(); invalidate(''); vi.clearAllMocks(); - supervisor.workflowRun.mockReset(); - supervisor.listSessions.mockReset(); - supervisor.formulaDetail.mockReset(); - mockSupervisorApi.mockReturnValue(supervisor); - mockSupervisorApiForRequestBudget.mockReturnValue(supervisor); }); describe('useFormulaRunDetail', () => { beforeEach(() => { - mockSupervisorApi.mockReturnValue(supervisor); - mockSupervisorApiForRequestBudget.mockReturnValue(supervisor); - supervisor.workflowRun.mockResolvedValue(workflowSnapshot()); - supervisor.listSessions.mockResolvedValue({ items: [], total: 0 }); - supervisor.formulaDetail.mockResolvedValue(formulaDetail()); + mockLoadDetail.mockResolvedValue(runDetail()); }); it('does not fetch or report when no run id is available', async () => { @@ -61,20 +80,17 @@ describe('useFormulaRunDetail', () => { await waitFor(() => expect(result.current.kind).toBe('idle')); - expect(supervisor.workflowRun).not.toHaveBeenCalled(); + expect(mockLoadDetail).not.toHaveBeenCalled(); expect(mockReportClientError).not.toHaveBeenCalled(); }); it('reports run detail load failures to the centralized client log', async () => { - supervisor.workflowRun.mockRejectedValue(new Error('detail unavailable')); + mockLoadDetail.mockRejectedValue(new Error('detail unavailable')); const { result } = renderHook(() => useFormulaRunDetail('wf-1')); await waitFor(() => - expect(result.current).toMatchObject({ - kind: 'failed', - error: 'detail unavailable', - }), + expect(result.current).toMatchObject({ kind: 'failed', error: 'detail unavailable' }), ); expect(mockReportClientError).toHaveBeenCalledWith({ @@ -82,10 +98,9 @@ describe('useFormulaRunDetail', () => { operation: 'load detail', message: 'wf-1: detail unavailable', }); - expect(supervisor.formulaDetail).not.toHaveBeenCalled(); }); - it('loads formula run detail from the direct supervisor workflow endpoint', async () => { + it('loads formula run detail from the BFF projection endpoint', async () => { const { result } = renderHook(() => useFormulaRunDetail('wf-1', 'city', 'test-city')); await waitFor(() => expect(result.current.kind).toBe('ready')); @@ -93,34 +108,19 @@ describe('useFormulaRunDetail', () => { if (result.current.kind !== 'ready') throw new Error('run detail did not load'); expect(result.current.detail.runId).toBe('wf-1'); expect(result.current.detail.title).toBe('Direct supervisor run'); - expect(result.current.detail.formulaDetail).toEqual({ - kind: 'available', - name: 'mol-test', - target: 'test-city/codex', - }); expect(result.current.refreshState).toEqual({ kind: 'idle' }); expect('diff' in result.current).toBe(false); - expect(supervisor.workflowRun).toHaveBeenCalledWith('test-city', 'wf-1', { - scope_kind: 'city', - scope_ref: 'test-city', - }); - expect(supervisor.formulaDetail).toHaveBeenCalledWith('test-city', 'mol-test', { - target: 'test-city/codex', - scope_kind: 'city', - scope_ref: 'test-city', - }); + // The loader is scope-independent now (the projection derives scope from the + // run's own root bead); the route's scope still drives only the cache key. + expect(mockLoadDetail).toHaveBeenCalledWith('wf-1'); expect(mockReportClientError).not.toHaveBeenCalled(); }); - it('does not stay loading for completed runs that lack formula metadata', async () => { - supervisor.workflowRun.mockResolvedValue( - workflowSnapshot({ - status: 'completed', - metadata: { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.run_target': 'test-city/codex', - }, + it('reaches ready for a run that lacks formula metadata (no hang)', async () => { + mockLoadDetail.mockResolvedValue( + runDetail({ + formula: { kind: 'unavailable', reason: 'missing_formula_metadata' }, + formulaDetail: { kind: 'unavailable', reason: 'missing_formula_metadata' }, }), ); @@ -133,23 +133,15 @@ describe('useFormulaRunDetail', () => { kind: 'unavailable', reason: 'missing_formula_metadata', }); - expect(result.current.detail.formulaDetail).toEqual({ - kind: 'unavailable', - reason: 'missing_formula_metadata', - }); - expect(supervisor.formulaDetail).not.toHaveBeenCalled(); expect(mockReportClientError).not.toHaveBeenCalled(); }); - it('surfaces a v1 / non-graph.v2 run as unsupported, not a generic failure', async () => { - // A v1 / wisp run: the root bead carries no gc.formula_contract=graph.v2, - // so enrichFormulaRun throws UnsupportedRunError('not_run_view'). The hook - // must map ONLY that case to {kind:'unsupported'} (the detail view then - // shows a list-only message) and NOT route it through the error path. - // The generic-failure branch is locked separately by the load-failure test - // above (a plain Error -> kind 'failed'). - supervisor.workflowRun.mockResolvedValue( - workflowSnapshot({ metadata: { 'gc.kind': 'workflow' } }), + it('surfaces a 422 not_run_view as unsupported, not a generic failure', async () => { + // A v1 / wisp run loads server-side but is not a graph.v2 run, so the BFF + // returns 422 + reason 'not_run_view'. The hook maps ONLY that case to + // {kind:'unsupported'} (list-only message), never the error path. + mockLoadDetail.mockRejectedValue( + new ApiClientError(422, 'run is not a graph.v2 run', undefined, 'not_run_view'), ); const { result } = renderHook(() => useFormulaRunDetail('wf-1', 'city', 'test-city')); @@ -158,17 +150,38 @@ describe('useFormulaRunDetail', () => { expect(mockReportClientError).not.toHaveBeenCalled(); }); - it('surfaces a raw 404 from the workflow endpoint as not_found, not v1-unsupported', async () => { - // gascity-dashboard (Major 2): a raw SupervisorApiError 404 (no snapshot at - // all) is AMBIGUOUS — a v1/wisp id the workflow endpoint never knew, a - // completed run whose snapshot wasn't retained, a pruned/deleted run, or a - // stale/wrong derived scope. It must NOT be mislabeled as the definitive v1 - // 'unsupported' state, and it must NOT collapse into the generic 'failed' - // transport state — it gets its own honest 'not_found' state. - supervisor.workflowRun.mockRejectedValue( - new SupervisorApiError(404, 'workflow gc-p7yf1m not found', undefined), + it('surfaces a 422 invalid_snapshot as a generic load failure, not unsupported', async () => { + // A malformed graph.v2 snapshot is a genuine load failure: it must propagate + // to the generic 'failed' state, distinct from the honest v1 'unsupported'. + mockLoadDetail.mockRejectedValue( + new ApiClientError(422, 'run snapshot is invalid', undefined, 'invalid_snapshot'), ); + const { result } = renderHook(() => useFormulaRunDetail('wf-1', 'city', 'test-city')); + + await waitFor(() => expect(result.current.kind).toBe('failed')); + }); + + it('surfaces an exhausted 503 warming budget as a generic failure, never not_found/unsupported', async () => { + // The loader retries 503 internally and, once the budget is spent, re-throws + // the ApiClientError(503). The hook must route that to the generic 'failed' + // state — a 503 is neither an honest list-only run nor a missing one. + mockLoadDetail.mockRejectedValue(new ApiClientError(503, 'run view is warming')); + + const { result } = renderHook(() => useFormulaRunDetail('wf-1', 'city', 'test-city')); + + await waitFor(() => expect(result.current.kind).toBe('failed')); + expect(result.current.kind).not.toBe('not_found'); + expect(result.current.kind).not.toBe('unsupported'); + }); + + it('surfaces a 404 as not_found, not v1-unsupported', async () => { + // gascity-dashboard (Major 2): a 404 (no run root in the projection) is + // AMBIGUOUS — a v1/wisp id, a completed run whose events rotated out, a + // pruned run, or a wrong derived scope. It gets its own honest 'not_found' + // state, never mislabeled as the definitive v1 'unsupported'. + mockLoadDetail.mockRejectedValue(new ApiClientError(404, 'unknown run')); + const { result } = renderHook(() => useFormulaRunDetail('gc-p7yf1m', 'city', 'test-city')); await waitFor(() => expect(result.current.kind).toBe('not_found')); @@ -197,56 +210,3 @@ describe('formulaRunDetailCacheKey (bvu4)', () => { ); }); }); - -function workflowSnapshot( - overrides: { - status?: string; - metadata?: Record; - } = {}, -) { - return { - workflow_id: 'wf-1', - root_bead_id: 'wf-1', - root_store_ref: 'city:test-city', - resolved_root_store: 'city:test-city', - scope_kind: 'city', - scope_ref: 'test-city', - snapshot_version: 1, - snapshot_event_seq: 1, - partial: false, - stores_scanned: ['city:test-city'], - beads: [ - { - id: 'wf-1', - title: 'Direct supervisor run', - status: overrides.status ?? 'in_progress', - kind: 'workflow', - metadata: overrides.metadata ?? { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.formula': 'mol-test', - 'gc.run_target': 'test-city/codex', - }, - }, - ], - deps: [], - logical_nodes: [], - logical_edges: [], - scope_groups: [], - }; -} - -function formulaDetail() { - return { - name: 'mol-test', - description: 'formula detail', - version: 'v1', - preview: { - nodes: [{ id: 'wf-1', title: 'Direct supervisor run', kind: 'workflow' }], - edges: [], - }, - steps: [], - deps: [], - var_defs: [], - }; -} diff --git a/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.ts b/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.ts index 4e301ff6c9..9ffd1b2d89 100644 --- a/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.ts +++ b/internal/api/dashboardspa/web/frontend/src/hooks/useFormulaRunDetail.ts @@ -1,8 +1,8 @@ import type { FormulaRunDetail, RunScopeKind } from 'gas-city-dashboard-shared'; -import { errorMessage, UnsupportedRunError } from 'gas-city-dashboard-shared'; +import { errorMessage } from 'gas-city-dashboard-shared'; import { reportClientError } from '../lib/clientErrorReporting'; import { loadSupervisorFormulaRunDetail } from '../supervisor/runDetail'; -import { SupervisorApiError } from '../supervisor/errors'; +import { ApiClientError } from '../api/client'; import { useCachedData } from './useCachedData'; interface FormulaRunDetailState { @@ -16,19 +16,19 @@ type FormulaRunRefreshState = | { kind: 'failed'; error: string }; // gascity-dashboard-9w3k: a v1 / wisp run (not graph.v2) is surfaced in the run -// list but has no graph.v2 step-detail view. When its snapshot LOADS but isn't a -// run view, enrichFormulaRun throws UnsupportedRunError('not_run_view') — the -// RELIABLE v1 signal. We carry that as a DISTINCT 'unsupported' payload (not a -// thrown error → not the generic failed state) so the page can render an honest -// "list-only" message instead of the opaque "Formula run unavailable." dead-end. +// list but has no graph.v2 step-detail view. The BFF detail endpoint rejects it +// with 422 + reason 'not_run_view' — the RELIABLE v1 signal. We carry that as a +// DISTINCT 'unsupported' payload (not a thrown error → not the generic failed +// state) so the page can render an honest "list-only" message instead of the +// opaque "Formula run unavailable." dead-end. // -// gascity-dashboard (Major 2): a raw SupervisorApiError 404 (no snapshot at all) -// is AMBIGUOUS — it can be a v1/wisp id the workflow endpoint never knew, a -// completed run whose snapshot wasn't retained, a pruned/deleted run, or a -// stale/wrong derived scope. We must NOT assert it is definitively v1. It maps -// to a distinct 'not_found' payload with honest copy that lists the -// possibilities, kept separate from both 'unsupported' (which over-claims v1) -// and the generic transport 'failed' state. No shared wire-shape field is added. +// gascity-dashboard (Major 2): a 404 (no run root in the projection) is +// AMBIGUOUS — it can be a v1/wisp id, a completed run whose events rotated out, +// a pruned/deleted run, or a stale/wrong derived scope. We must NOT assert it is +// definitively v1. It maps to a distinct 'not_found' payload with honest copy +// that lists the possibilities, kept separate from both 'unsupported' (which +// over-claims v1) and the generic transport 'failed' state. A malformed graph.v2 +// snapshot (422 + 'invalid_snapshot') stays in that generic 'failed' state. type FormulaRunDetailPayload = | { kind: 'unrequested' } | { kind: 'unsupported' } @@ -58,7 +58,7 @@ export function useFormulaRunDetail( const key = formulaRunDetailCacheKey(runId, scopeKind, scopeRef); const { data, loading, error, refresh } = useCachedData( key, - () => loadFormulaRunDetail(runId, scopeKind, scopeRef), + () => loadFormulaRunDetail(runId), { onError: (err) => { if (runId !== undefined) reportRunDetailError('load detail', runId, err); @@ -81,34 +81,30 @@ export function useFormulaRunDetail( return { kind: 'loading', refresh }; } -async function loadFormulaRunDetail( - runId: string | undefined, - scopeKind?: RunScopeKind, - scopeRef?: string, -): Promise { +async function loadFormulaRunDetail(runId: string | undefined): Promise { if (!runId) return { kind: 'unrequested' }; - const params: { scopeKind?: RunScopeKind; scopeRef?: string } = {}; - if (scopeKind !== undefined) params.scopeKind = scopeKind; - if (scopeRef !== undefined) params.scopeRef = scopeRef; try { - const detail = await loadSupervisorFormulaRunDetail(runId, params.scopeKind, params.scopeRef); + const detail = await loadSupervisorFormulaRunDetail(runId); return { kind: 'loaded', detail }; } catch (err) { - // gascity-dashboard-9w3k: a snapshot that LOADS but isn't a graph.v2 run - // view throws UnsupportedRunError('not_run_view'). That is the RELIABLE v1 / - // wisp signal, so it maps to the 'unsupported' payload and the page renders - // the honest list-only message instead of a raw error. - if (err instanceof UnsupportedRunError && err.reason === 'not_run_view') { + // gascity-dashboard-9w3k: a v1 / wisp run (not graph.v2) loads but has no + // graph.v2 step-detail view. The BFF rejects it with 422 + reason + // 'not_run_view' — the RELIABLE list-only signal — which maps to the + // distinct 'unsupported' payload so the page renders an honest list-only + // message instead of a raw error. A malformed graph.v2 snapshot + // (422 + 'invalid_snapshot') and any other failure propagate as a generic + // load error. + if (err instanceof ApiClientError && err.status === 422 && err.reason === 'not_run_view') { return { kind: 'unsupported' }; } - // gascity-dashboard (Major 2): a raw SupervisorApiError 404 (no snapshot at - // all) is AMBIGUOUS — v1/wisp id the workflow endpoint never knew, a - // completed run whose snapshot wasn't retained, a pruned/deleted run, or a - // stale/wrong derived scope. We do NOT claim it is definitively v1; it maps - // to the distinct 'not_found' payload whose copy lists the possibilities - // without over-claiming. A malformed graph.v2 snapshot ('invalid_snapshot') - // and any other transport failure still propagate as a generic load error. - if (err instanceof SupervisorApiError && err.status === 404) { + // gascity-dashboard (Major 2): a 404 (no run root in the projection) is + // AMBIGUOUS — a v1/wisp id the projection never saw, a completed run whose + // events rotated out, a pruned/deleted run, or a stale/wrong derived scope. + // We do NOT claim it is definitively v1; it maps to the distinct 'not_found' + // payload whose copy lists the possibilities without over-claiming, kept + // separate from 'unsupported' (which over-claims v1) and the generic + // transport 'failed' state. + if (err instanceof ApiClientError && err.status === 404) { return { kind: 'not_found' }; } throw err; diff --git a/internal/api/dashboardspa/web/frontend/src/routes/FormulaRunDetail.test.tsx b/internal/api/dashboardspa/web/frontend/src/routes/FormulaRunDetail.test.tsx index afbc39c50f..4f14f65c35 100644 --- a/internal/api/dashboardspa/web/frontend/src/routes/FormulaRunDetail.test.tsx +++ b/internal/api/dashboardspa/web/frontend/src/routes/FormulaRunDetail.test.tsx @@ -16,9 +16,8 @@ import { type RunLane, type RunSummary, type SourceState, - UnsupportedRunError, } from 'gas-city-dashboard-shared'; -import { SupervisorApiError } from '../supervisor/errors'; +import { ApiClientError } from '../api/client'; import rawFormulaRunDetailFixture from '../test/fixtures/formula-run-detail.json'; const loadSupervisorFormulaRunDetail = vi.hoisted(() => vi.fn()); @@ -480,11 +479,10 @@ describe('FormulaRunDetailPage', () => { await screen.findByRole('heading', { name: /adopt pr #42/i }); const runUrls = fetchUrls.filter((url) => url.startsWith('/api/city/test-city/runs/')); - expect(loadSupervisorFormulaRunDetail).toHaveBeenCalledWith( - 'gc-adopt-pr-active', - 'city', - 'racoon-city', - ); + // The detail loader is scope-independent now (the BFF projection derives the + // run's scope from its own root bead); the route's scope still drives the + // separate run-diff fetch below. + expect(loadSupervisorFormulaRunDetail).toHaveBeenCalledWith('gc-adopt-pr-active'); expect(runUrls).toContain( '/api/city/test-city/runs/gc-adopt-pr-active/diff?scope_kind=city&scope_ref=racoon-city', ); @@ -661,10 +659,10 @@ describe('FormulaRunDetailPage', () => { it('renders an informative list-only message for a v1 / wisp (unsupported) run, not the generic failure (gascity-dashboard-9w3k)', async () => { // A v1 / wisp run is clickable in the run list but has no graph.v2 detail - // view: enrichFormulaRun throws UnsupportedRunError('not_run_view'). The page - // must explain that clearly rather than show the opaque generic fallback. + // view: the BFF rejects it with 422 + reason 'not_run_view'. The page must + // explain that clearly rather than show the opaque generic fallback. loadSupervisorFormulaRunDetail.mockImplementation(async () => { - throw new UnsupportedRunError('run is not a graph.v2 run', 'not_run_view'); + throw new ApiClientError(422, 'run is not a graph.v2 run', undefined, 'not_run_view'); }); renderPage(); @@ -679,13 +677,13 @@ describe('FormulaRunDetailPage', () => { }); it('renders an honest not-found message for a raw 404 (ambiguous), not the v1 over-claim or the generic failure (Major 2)', async () => { - // gascity-dashboard (Major 2): a raw SupervisorApiError 404 (no snapshot at - // all) is ambiguous — it can be a v1/wisp id, a completed run whose snapshot - // wasn't retained, a pruned run, or a stale/wrong derived scope. The page - // must NOT assert it is definitively v1 (the 'unsupported' copy) and must NOT - // fall to the generic "Formula run unavailable." dead-end either. + // gascity-dashboard (Major 2): a 404 (no run root in the projection) is + // ambiguous — it can be a v1/wisp id, a completed run whose events rotated + // out, a pruned run, or a stale/wrong derived scope. The page must NOT assert + // it is definitively v1 (the 'unsupported' copy) and must NOT fall to the + // generic "Formula run unavailable." dead-end either. loadSupervisorFormulaRunDetail.mockImplementation(async () => { - throw new SupervisorApiError(404, 'workflow gc-p7yf1m not found', undefined); + throw new ApiClientError(404, 'unknown run'); }); renderPage(); @@ -699,10 +697,10 @@ describe('FormulaRunDetailPage', () => { }); it('still shows the generic failure for a malformed graph.v2 snapshot (invalid_snapshot)', async () => { - // A genuine load failure (malformed graph.v2 snapshot, or any other - // UnsupportedRunError reason) must NOT be mistaken for a v1 list-only run. + // A genuine load failure (malformed graph.v2 snapshot: 422 + + // 'invalid_snapshot') must NOT be mistaken for a v1 list-only run. loadSupervisorFormulaRunDetail.mockImplementation(async () => { - throw new UnsupportedRunError('run snapshot identity is missing or invalid'); + throw new ApiClientError(422, 'run snapshot is invalid', undefined, 'invalid_snapshot'); }); renderPage(); diff --git a/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.test.tsx b/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.test.tsx index 9c2d06fa99..91a3d09880 100644 --- a/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.test.tsx +++ b/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.test.tsx @@ -402,11 +402,13 @@ describe('useRunSummarySubscription / RunSummaryProvider (gascity-dashboard-2j8e await waitFor(() => expect(screen.getByTestId('page').textContent).toBe('error:-1')); }); - it('reconciles merged history against the fresh active set on a cheap refresh (no double-display)', async () => { - // MAJOR 1: the cheap refresh borrows historicalLanes from the last wide - // snapshot. If a run that WAS historical is now active/blocked again, it must - // appear ONLY in the live set, not in both. The wide upgrade seeds history - // with gc-1 (historical); the cheap refresh then reports gc-1 as active again. + it('publishes the active source snapshot verbatim on a cheap refresh (server owns the active/historical split)', async () => { + // The cheap SSE refresh now hits the same complete BFF read as the wide one, + // so the active source returns the authoritative active+historical split and + // the subscription publishes it directly — NO borrowing history from the last + // wide snapshot, no client-side reconciliation. The discriminating assertion: + // an active read with empty history publishes empty history (a borrow would + // have re-surfaced gc-2 from the seeded wide snapshot). mockFull.mockResolvedValue(buildLaneSource({ active: [], historical: ['gc-1', 'gc-2'] })); render( @@ -420,16 +422,15 @@ describe('useRunSummarySubscription / RunSummaryProvider (gascity-dashboard-2j8e expect(screen.getByTestId('page').textContent).toBe('live=[] hist=[gc-1,gc-2] totalHist=2'), ); - // A bead event lands and the cheap active read now reports gc-1 active again. + // A bead event lands; the cheap active read returns its OWN complete snapshot + // (gc-1 active, no historical). Published verbatim — gc-2 is NOT borrowed back. mockActive.mockResolvedValue(buildLaneSource({ active: ['gc-1'], historical: [] })); await act(async () => { lastHookCall.onMatch?.(); }); - // gc-1 appears ONLY in the live set; the borrowed history drops it and the - // count is recomputed. gc-2 (still only historical) stays in history. await waitFor(() => - expect(screen.getByTestId('page').textContent).toBe('live=[gc-1] hist=[gc-2] totalHist=1'), + expect(screen.getByTestId('page').textContent).toBe('live=[gc-1] hist=[] totalHist=0'), ); }); diff --git a/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.tsx b/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.tsx index 03289d7177..06d639cdd2 100644 --- a/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.tsx +++ b/internal/api/dashboardspa/web/frontend/src/runs/runSummarySubscription.tsx @@ -114,33 +114,19 @@ export function useRunSummarySubscription(): RunSummarySubscription { }), ); if (result.status !== 'error') { - // A cheap (active-only) success does NOT prove the WIDE failure recovered: - // the wide retry loop must keep driving off staleDueToFailureRef until a - // wide refresh actually lands (refreshWithLastGoodRetention clears it). So - // we deliberately do NOT clear the flag here. - const lastGood = lastGoodRef.current; - const borrowedHistorical = lastGood?.data.historicalLanes ?? []; - // Reconcile the borrowed history against the CURRENT active/blocked set: a - // run that was historical in last-good but is active or blocked again in - // this fresh snapshot would otherwise appear in BOTH sets (double-display). - // Drop any historical lane whose run is now live; recompute the count to - // match. (A run that completes between wide refreshes still lags into - // History on the next wide scan — accepted; this only kills the overlap.) - const liveIds = new Set([ - ...result.data.lanes.map((lane) => lane.id), - ...result.data.blockedLanes.map((lane) => lane.id), - ]); - const historicalLanes = borrowedHistorical.filter((lane) => !liveIds.has(lane.id)); - const dropped = borrowedHistorical.length - historicalLanes.length; - const totalHistorical = Math.max(0, (lastGood?.data.totalHistorical ?? 0) - dropped); - return { - ...result, - data: { - ...result.data, - historicalLanes, - totalHistorical, - }, - }; + // The active source now returns the COMPLETE server-enriched snapshot + // (full historical lanes included), so an SSE refresh publishes it as-is. + // The BFF folds the whole event log atomically (ColdLoad → BuildRunSummary + // produces active + historical + blocked buckets together, never + // staggered), so there is no intermediate active-but-empty-history state + // the old client-side history-merge had to paper over. + // There is no longer a cheap/wide split whose partial history must be + // reconciled against last-good. We still do NOT clear staleDueToFailureRef + // here: a fast SSE refresh succeeding does not retroactively prove a prior + // wide-refresh failure recovered — refreshWithLastGoodRetention owns + // clearing the flag — so the degraded-retry loop keeps driving until an + // explicit refresh lands. + return result; } const lastGood = lastGoodRef.current; if (lastGood === null) return result; diff --git a/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.test.ts b/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.test.ts index 35d2cdec65..1ded1a09ee 100644 --- a/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.test.ts +++ b/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.test.ts @@ -1,310 +1,124 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - resetSupervisorApiForTests, - setSupervisorApiForTests, - SupervisorApiError, - type SupervisorApi, -} from './client'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ApiClientError } from '../api/client'; import { loadSupervisorFormulaRunDetail } from './runDetail'; -vi.mock('../api/cityBase', () => ({ - getActiveCity: () => 'test-city', - activeCityOrThrow: () => 'test-city', -})); - -const baseApi: SupervisorApi = { - baseUrl: '/gc-supervisor', - health: vi.fn(), - cityHealth: vi.fn(), - cityStatus: vi.fn(), - listCities: vi.fn(), - listAgents: vi.fn(), - listRigs: vi.fn(), - listBeads: vi.fn(), - listEvents: vi.fn(), - getBead: vi.fn(), - createBead: vi.fn(), - updateBead: vi.fn(), - closeBead: vi.fn(), - nudgeAgent: vi.fn(), - agentPrime: vi.fn(), - sling: vi.fn(), - formulaFeed: vi.fn(), - listMail: vi.fn(), - markMailRead: vi.fn(), - markMailUnread: vi.fn(), - archiveMail: vi.fn(), - replyMail: vi.fn(), - sendMail: vi.fn(), - mailThread: vi.fn(), - cityEventStreamUrl: vi.fn(), - sessionStreamUrl: vi.fn(), - listSessions: vi.fn(), - sessionPending: vi.fn(), - respondSession: vi.fn(), - sessionTranscript: vi.fn(), - workflowRun: vi.fn(), - formulaDetail: vi.fn(), - mutationHeaders: () => ({ 'X-GC-Request': 'dashboard' }), +// The detail pipeline (snapshot synthesis, grouping, phase/stage, edges, lanes, +// formula identity, completeness) moved to Go (internal/runproj.BuildRunDetail) +// and is golden-gated byte-for-byte. The TS loader is now one GET to the BFF +// run-projection endpoint with a bounded retry while the projection is still +// cold-replaying (HTTP 503). This file covers that thin read: the warm path, the +// warming retry, and the error surface the hook maps. + +const detailBody = { + runId: 'mol-adopt-1', + rootBeadId: 'b-1', + rootStoreRef: 'rig:demo', + resolvedRootStore: 'rig:demo', + scopeKind: 'rig', + scopeRef: 'demo', + title: 'Adopt PR', + formula: { kind: 'unavailable', reason: 'missing_formula_metadata' }, + formulaDetail: { kind: 'unavailable', reason: 'missing_formula_metadata' }, + executionPath: { kind: 'unavailable', reason: 'missing_cwd_and_rig_root' }, + snapshotVersion: 1, + snapshotEventSeq: { kind: 'known', seq: 100 }, + completeness: { kind: 'complete' }, + progress: { statusCounts: {} }, + phase: 'intake', + stages: [], + nodes: [], + edges: [], + lanes: [], }; -describe('loadSupervisorFormulaRunDetail', () => { - const workflowRun = vi.fn(); - const formulaDetail = vi.fn(); - const listSessions = vi.fn(); - - beforeEach(() => { - workflowRun.mockResolvedValue(workflowSnapshot()); - formulaDetail.mockResolvedValue(formulaDetailResponse()); - listSessions.mockResolvedValue({ items: [], total: 0 }); - setSupervisorApiForTests({ - ...baseApi, - workflowRun, - formulaDetail, - listSessions, - }); +function jsonResponse(body: unknown, status: number): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, }); +} +describe('loadSupervisorFormulaRunDetail', () => { afterEach(() => { - resetSupervisorApiForTests(); - vi.clearAllMocks(); + vi.unstubAllGlobals(); + vi.useRealTimers(); }); - it('fetches formula detail when the root exposes formula metadata and a run target', async () => { - const detail = await loadSupervisorFormulaRunDetail('wf-1', 'city', 'test-city'); + it('reads the run detail from the city-scoped BFF projection endpoint', async () => { + const fetchMock = vi.fn(async () => jsonResponse(detailBody, 200)); + vi.stubGlobal('fetch', fetchMock); - expect(detail.formula).toEqual({ - kind: 'known', - name: 'mol-test', - source: 'metadata', + await expect(loadSupervisorFormulaRunDetail('mol-adopt-1')).resolves.toMatchObject({ + runId: 'mol-adopt-1', }); - expect(detail.formulaDetail).toEqual({ - kind: 'available', - name: 'mol-test', - target: 'test-city/codex', - }); - expect(detail.completeness).toEqual({ kind: 'complete' }); - expect(formulaDetail).toHaveBeenCalledWith('test-city', 'mol-test', { - target: 'test-city/codex', - scope_kind: 'city', - scope_ref: 'test-city', - }); - }); - - it('resolves formula detail when the supervisor omits the version field (3eo8, mol-focus-review)', async () => { - formulaDetail.mockResolvedValue(versionlessFormulaDetailResponse()); - - const detail = await loadSupervisorFormulaRunDetail('wf-1', 'city', 'test-city'); - - expect(detail.formulaDetail).toEqual({ - kind: 'available', - name: 'mol-test', - target: 'test-city/codex', - }); - expect(detail.completeness).toEqual({ kind: 'complete' }); - }); - - it('reports missing formula metadata without calling the formula endpoint', async () => { - workflowRun.mockResolvedValue( - workflowSnapshot({ - status: 'closed', - metadata: { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.run_target': 'test-city/codex', - }, - }), + expect(fetchMock).toHaveBeenCalledWith( + '/api/city/test-city/runs/mol-adopt-1/detail', + expect.objectContaining({ method: 'GET' }), ); - - const detail = await loadSupervisorFormulaRunDetail('wf-1'); - - expect(detail.formula).toEqual({ - kind: 'unavailable', - reason: 'missing_formula_metadata', - }); - expect(detail.formulaDetail).toEqual({ - kind: 'unavailable', - reason: 'missing_formula_metadata', - }); - expect(detail.completeness).toEqual({ - kind: 'partial', - reasons: ['formula_detail_missing_formula_metadata'], - }); - expect(formulaDetail).not.toHaveBeenCalled(); }); - it('does not title-fallback into formula detail for completed supervisor runs', async () => { - workflowRun.mockResolvedValue( - workflowSnapshot({ - status: 'completed', - metadata: { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.run_target': 'test-city/codex', - }, - }), - ); + it('retries while the projection is warming (503) and resolves once it is ready', async () => { + vi.useFakeTimers(); + const fetchMock = vi + .fn() + .mockResolvedValueOnce(jsonResponse({ error: 'run view is warming' }, 503)) + .mockResolvedValueOnce(jsonResponse(detailBody, 200)); + vi.stubGlobal('fetch', fetchMock); - const detail = await loadSupervisorFormulaRunDetail('wf-1'); + const pending = loadSupervisorFormulaRunDetail('mol-adopt-1'); + await vi.advanceTimersByTimeAsync(600); - expect(detail.formula).toEqual({ - kind: 'unavailable', - reason: 'missing_formula_metadata', - }); - expect(detail.formulaDetail).toEqual({ - kind: 'unavailable', - reason: 'missing_formula_metadata', - }); - expect(detail.completeness).toEqual({ - kind: 'partial', - reasons: ['formula_detail_missing_formula_metadata'], - }); - expect(formulaDetail).not.toHaveBeenCalled(); + await expect(pending).resolves.toMatchObject({ runId: 'mol-adopt-1' }); + expect(fetchMock).toHaveBeenCalledTimes(2); }); - it('reports missing run target without calling the formula endpoint', async () => { - workflowRun.mockResolvedValue( - workflowSnapshot({ - metadata: { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.formula': 'mol-test', - }, - }), - ); + it('gives up after the warming budget is spent and surfaces the 503', async () => { + vi.useFakeTimers(); + const fetchMock = vi.fn(async () => jsonResponse({ error: 'run view is warming' }, 503)); + vi.stubGlobal('fetch', fetchMock); - const detail = await loadSupervisorFormulaRunDetail('wf-1'); + const pending = loadSupervisorFormulaRunDetail('mol-adopt-1'); + const assertion = expect(pending).rejects.toMatchObject({ status: 503 }); + await vi.advanceTimersByTimeAsync(600 + 1_200 + 2_400); + await assertion; - expect(detail.formula).toEqual({ - kind: 'known', - name: 'mol-test', - source: 'metadata', - }); - expect(detail.formulaDetail).toEqual({ - kind: 'unavailable', - reason: 'missing_run_target', - name: 'mol-test', - }); - expect(detail.completeness).toEqual({ - kind: 'partial', - reasons: ['formula_detail_missing_run_target'], - }); - expect(formulaDetail).not.toHaveBeenCalled(); + // The initial attempt plus three bounded retries. + expect(fetchMock).toHaveBeenCalledTimes(4); }); - it('preserves supervisor formula endpoint failures as partial formula detail', async () => { - formulaDetail.mockRejectedValue(new SupervisorApiError(404, 'not found', undefined)); - - const detail = await loadSupervisorFormulaRunDetail('wf-1'); + it('propagates a 422 unsupported run with its reason for the hook to map', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => + jsonResponse({ error: 'run is not a graph.v2 run', reason: 'not_run_view' }, 422), + ), + ); - expect(detail.formulaDetail).toEqual({ - kind: 'unavailable', - reason: 'fetch_failed', - name: 'mol-test', - target: 'test-city/codex', - failure: 'not_found', - }); - expect(detail.completeness).toEqual({ - kind: 'partial', - reasons: ['formula_detail_fetch_failed'], - }); + const err = await loadSupervisorFormulaRunDetail('v1-run').catch((e: unknown) => e); + expect(err).toBeInstanceOf(ApiClientError); + expect(err).toMatchObject({ status: 422, reason: 'not_run_view' }); }); - // Fix A: the workflow snapshot is the run-detail core read; a transient - // timeout/5xx is retried once before it blanks the view, mirroring the runs - // list core read. A 4xx is the caller's fault and is never retried. - it('retries the workflow core read once on a transient timeout', async () => { - let attempts = 0; - workflowRun.mockImplementation(async () => { - attempts += 1; - if (attempts === 1) { - throw new SupervisorApiError( - undefined, - 'gc supervisor request timed out after 60000ms', - undefined, - ); - } - return workflowSnapshot(); - }); + it('retries a transient 5xx (not just 503) and resolves', async () => { + vi.useFakeTimers(); + const fetchMock = vi + .fn() + .mockResolvedValueOnce(jsonResponse({ error: 'bad gateway' }, 502)) + .mockResolvedValueOnce(jsonResponse(detailBody, 200)); + vi.stubGlobal('fetch', fetchMock); - const detail = await loadSupervisorFormulaRunDetail('wf-1', 'rig', 'app'); + const pending = loadSupervisorFormulaRunDetail('mol-adopt-1'); + await vi.advanceTimersByTimeAsync(600); - expect(attempts).toBe(2); - expect(detail.completeness).toEqual({ kind: 'complete' }); + await expect(pending).resolves.toMatchObject({ runId: 'mol-adopt-1' }); + expect(fetchMock).toHaveBeenCalledTimes(2); }); - it('does not retry the workflow core read on a non-transient (4xx) failure', async () => { - let attempts = 0; - workflowRun.mockImplementation(async () => { - attempts += 1; - throw new SupervisorApiError(400, 'bad request', undefined); - }); + it('does not retry a 404', async () => { + const fetchMock = vi.fn(async () => jsonResponse({ error: 'unknown run' }, 404)); + vi.stubGlobal('fetch', fetchMock); - await expect(loadSupervisorFormulaRunDetail('wf-1', 'rig', 'app')).rejects.toThrow( - 'bad request', - ); - expect(attempts).toBe(1); + await expect(loadSupervisorFormulaRunDetail('ghost')).rejects.toMatchObject({ status: 404 }); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); - -function workflowSnapshot( - overrides: { - status?: string; - metadata?: Record; - } = {}, -) { - return { - workflow_id: 'wf-1', - root_bead_id: 'wf-1', - root_store_ref: 'city:test-city', - resolved_root_store: 'city:test-city', - scope_kind: 'city', - scope_ref: 'test-city', - snapshot_version: 1, - snapshot_event_seq: 1, - partial: false, - stores_scanned: ['city:test-city'], - beads: [ - { - id: 'wf-1', - title: 'Direct supervisor run', - status: overrides.status ?? 'in_progress', - kind: 'workflow', - metadata: overrides.metadata ?? { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.formula': 'mol-test', - 'gc.run_target': 'test-city/codex', - }, - }, - ], - deps: [], - logical_nodes: [], - logical_edges: [], - scope_groups: [], - }; -} - -function formulaDetailResponse() { - return { - name: 'mol-test', - description: 'formula detail', - version: 'v1', - preview: { - nodes: [{ id: 'wf-1', title: 'Direct supervisor run', kind: 'workflow' }], - edges: [], - }, - steps: [], - deps: [], - var_defs: [], - }; -} - -// Inferred/title-based formulas (e.g. mol-focus-review) come back from the -// supervisor with no `version` key — `{ name, description, var_defs, steps, -// deps, preview }`. The dashboard must resolve these to `available`, not -// degrade the Formula Detail panel (3eo8). -function versionlessFormulaDetailResponse() { - const { version: _version, ...rest } = formulaDetailResponse(); - return rest; -} diff --git a/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.ts b/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.ts index a6dd74f5f3..4b5af0d599 100644 --- a/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.ts +++ b/internal/api/dashboardspa/web/frontend/src/supervisor/runDetail.ts @@ -1,213 +1,48 @@ -import type { - FormulaRunDetail, - FormulaRunPartialReason, - FormulaDetail, - RunSnapshot, - DashboardSession, - RunFormulaDetailFetchFailure, - RunFormulaDetailState, - RunScopeKind, -} from 'gas-city-dashboard-shared'; -import { - enrichFormulaRun, - formulaRunCompleteness, - resolveRunFormulaIdentity, -} from 'gas-city-dashboard-shared'; -import { activeCityOrThrow } from '../api/cityBase'; -import type { - FormulaDetailResponse, - WorkflowSnapshotResponse, -} from 'gas-city-dashboard-shared/gc-supervisor'; -import { SupervisorApiError, supervisorApi, supervisorApiForRequestBudget } from './client'; -import type { SupervisorApi } from './client'; -import { fetchCoreRead } from './coreRead'; -import { normalizeSessions } from './sessionReads'; - -// The workflow snapshot is the run-detail core read — the one fetch whose -// failure blanks the whole detail view (sessions and formula detail degrade to -// 'partial'). It gets the same treatment as the runs-list core read -// (runSummary.ts): a burst-tolerant budget so a CPU spike doesn't time it out, -// and one retry on a transient timeout/5xx. A city-scoped (no-rig) fetch hits -// the supervisor's full-store scan (~12-14s, upstream gascity-dashboard#88), so -// the wider budget is what lets that path complete instead of timing out; the -// rig-scoped fetch (the common case once scope is passed) is sub-second. -const RUN_DETAIL_CORE_TIMEOUT_MS = 60_000; - -export async function loadSupervisorFormulaRunDetail( - runId: string, - scopeKind?: RunScopeKind, - scopeRef?: string, -): Promise { - const cityName = activeCityOrThrow('load supervisor formula run detail'); - const query = runScopeQuery(scopeKind, scopeRef); - const coreApi = supervisorApiForRequestBudget(RUN_DETAIL_CORE_TIMEOUT_MS); - const api = supervisorApi(); - const [raw, sessionsLookup] = await Promise.all([ - fetchCoreRead(() => coreApi.workflowRun(cityName, runId, query)), - loadRunSessions(cityName), - ]); - const snapshot = toRunSnapshot(raw); - const formulaDetailLookup = await loadRunFormulaDetail(api, cityName, snapshot, query); - const detail = enrichFormulaRun(snapshot, { - sessions: sessionsLookup.sessions, - formulaDetailState: formulaDetailLookup.state, - ...(formulaDetailLookup.kind === 'available' - ? { formulaDetail: formulaDetailLookup.detail } - : {}), - }); - const reasons: FormulaRunPartialReason[] = [ - ...(detail.completeness.kind === 'partial' ? detail.completeness.reasons : []), - ...(sessionsLookup.kind === 'unavailable' ? ['session_list_failed' as const] : []), - ...(formulaDetailLookup.kind === 'unavailable' - ? [formulaDetailPartialReason(formulaDetailLookup.state.reason)] - : []), - ]; - return { - ...detail, - completeness: formulaRunCompleteness(reasons), - }; -} - -type RunSessionsLookup = - | { kind: 'available'; sessions: readonly DashboardSession[] } - | { kind: 'unavailable'; sessions: readonly DashboardSession[] }; - -type RunFormulaDetailLookup = - | { kind: 'available'; detail: FormulaDetail; state: RunFormulaDetailState } - | { - kind: 'unavailable'; - state: Extract; - }; - -async function loadRunSessions(cityName: string): Promise { - try { - const list = await supervisorApi().listSessions(cityName); - return { - kind: 'available', - sessions: normalizeSessions(list), - }; - } catch { - return { kind: 'unavailable', sessions: [] }; - } -} - -async function loadRunFormulaDetail( - api: SupervisorApi, - cityName: string, - snapshot: RunSnapshot, - scopeQuery: { scope_kind?: string; scope_ref?: string } | undefined, -): Promise { - const root = snapshot.beads?.find((bead) => bead.id === snapshot.root_bead_id); - const resolved = resolveRunFormulaIdentity('route', { root }); - const name = resolved.name ?? undefined; - const target = resolved.target ?? undefined; - if (name === undefined) { - return { - kind: 'unavailable', - state: { kind: 'unavailable', reason: 'missing_formula_metadata' }, - }; - } - if (target === undefined) { - return { - kind: 'unavailable', - state: { kind: 'unavailable', reason: 'missing_run_target', name }, - }; - } - try { - const detail = toFormulaDetail( - await api.formulaDetail(cityName, name, { - target, - ...(scopeQuery ?? {}), - }), - ); - return { - kind: 'available', - detail, - state: { kind: 'available', name, target }, - }; - } catch (err) { - return { - kind: 'unavailable', - state: { - kind: 'unavailable', - reason: 'fetch_failed', - name, - target, - failure: formulaDetailFetchFailure(err), - }, - }; - } -} - -function toRunSnapshot(raw: WorkflowSnapshotResponse): RunSnapshot { - const snapshot: RunSnapshot = { - run_id: raw.workflow_id, - root_bead_id: raw.root_bead_id, - root_store_ref: raw.root_store_ref, - resolved_root_store: raw.resolved_root_store, - scope_kind: raw.scope_kind, - scope_ref: raw.scope_ref, - snapshot_version: raw.snapshot_version, - partial: raw.partial, - stores_scanned: raw.stores_scanned, - beads: raw.beads, - deps: raw.deps, - logical_nodes: raw.logical_nodes as RunSnapshot['logical_nodes'], - logical_edges: raw.logical_edges, - scope_groups: raw.scope_groups as RunSnapshot['scope_groups'], - }; - if (raw.snapshot_event_seq !== undefined) { - snapshot.snapshot_event_seq = raw.snapshot_event_seq; - } - return snapshot; -} - -function toFormulaDetail(raw: FormulaDetailResponse): FormulaDetail { - const detail: FormulaDetail = { name: raw.name }; - const preview: NonNullable = {}; - if (Array.isArray(raw.preview.nodes)) { - preview.nodes = raw.preview.nodes; - } - if (Array.isArray(raw.preview.edges)) { - preview.edges = raw.preview.edges; - } - if (preview.nodes !== undefined || preview.edges !== undefined) { - detail.preview = preview; - } - if (Array.isArray(raw.steps)) { - detail.steps = raw.steps; - } - if (Array.isArray(raw.deps)) { - detail.deps = raw.deps; - } - return detail; -} - -function formulaDetailPartialReason( - reason: Extract['reason'], -): FormulaRunPartialReason { - switch (reason) { - case 'missing_formula_metadata': - return 'formula_detail_missing_formula_metadata'; - case 'missing_run_target': - return 'formula_detail_missing_run_target'; - case 'fetch_failed': - return 'formula_detail_fetch_failed'; - } -} - -function formulaDetailFetchFailure(err: unknown): RunFormulaDetailFetchFailure { - if (err instanceof SupervisorApiError && err.status === 404) return 'not_found'; - return 'upstream_error'; -} - -function runScopeQuery( - scopeKind?: RunScopeKind, - scopeRef?: string, -): { scope_kind?: string; scope_ref?: string } | undefined { - if (scopeKind === undefined && scopeRef === undefined) return undefined; - const query: { scope_kind?: string; scope_ref?: string } = {}; - if (scopeKind !== undefined) query.scope_kind = scopeKind; - if (scopeRef !== undefined) query.scope_ref = scopeRef; - return query; +import type { FormulaRunDetail } from 'gas-city-dashboard-shared'; +import { api, ApiClientError } from '../api/client'; + +// The run-detail view reads from the BFF run-projection endpoint +// (GET /api/city/{city}/runs/{runId}/detail): one warm read of the same fold +// the summary uses, so detail stages == summary stages by construction. The +// whole client-side detail pipeline (the workflowRun snapshot + formulaDetail +// fetch + enrichFormulaRun) moved to Go (internal/runproj.BuildRunDetail) and +// is golden-gated byte-for-byte. Scope is no longer threaded into the read — +// the projection derives a run's scope from its own root bead — though the +// route still parses scope for the separate run-diff endpoint. + +// Retry transient failures a few times before surfacing one: the BFF's 503 +// warming signal while a city's projection cold-replays (bounded server-side to +// ~5s), a 5xx upstream-proxy blip, or a network-level fetch reject. This +// restores the single-transient-retry resilience the pre-cutover supervisor +// read had (fetchCoreRead). A 4xx (404 unknown run, 422 unsupported) is +// definitive and surfaces immediately; SSE refresh and the manual Refresh +// button recover anything past the budget. +const WARMING_RETRY_DELAYS_MS = [600, 1_200, 2_400]; + +export async function loadSupervisorFormulaRunDetail(runId: string): Promise { + for (let attempt = 0; ; attempt += 1) { + try { + return await api.runDetail(runId); + } catch (err) { + const delayMs = WARMING_RETRY_DELAYS_MS[attempt]; + if (delayMs !== undefined && isTransientDetailError(err)) { + await delay(delayMs); + continue; + } + throw err; + } + } +} + +// A 4xx (404/422) is a definitive answer about the run — never retry it. The +// BFF's 503 warming signal and any 5xx are transient, as is a network-level +// fetch reject (a TypeError, e.g. "Failed to fetch"); a malformed-body decode +// error (ApiResponseDecodeError) is NOT transient and surfaces immediately. +function isTransientDetailError(err: unknown): boolean { + if (err instanceof ApiClientError) return err.status >= 500; + return err instanceof TypeError; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.test.ts b/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.test.ts index 317dd05ff0..2f10ead850 100644 --- a/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.test.ts +++ b/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.test.ts @@ -1,958 +1,87 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { setActiveCity } from '../api/cityBase'; -import type { - Bead, - FormulaFeedBody, - ListBodyBead, - ListBodySessionResponse, - MonitorFeedItemResponse, -} from 'gas-city-dashboard-shared/gc-supervisor'; -import { resetSupervisorApiForTests, setSupervisorApiForTests, type SupervisorApi } from './client'; -import { SupervisorApiError } from './errors'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { - CORE_RUN_SUMMARY_TIMEOUT_MS, loadSupervisorRunSummaryActiveSource, loadSupervisorRunSummaryMountSource, loadSupervisorRunSummaryPreviewSource, loadSupervisorRunSummarySource, - resetSupervisorRunSummaryStateForTests, } from './runSummary'; -const baseApi: SupervisorApi = { - baseUrl: '/gc-supervisor', - health: vi.fn(), - cityHealth: vi.fn(), - cityStatus: vi.fn(), - listCities: vi.fn(), - listAgents: vi.fn(), - listRigs: vi.fn(), - listBeads: vi.fn(), - listEvents: vi.fn(), - getBead: vi.fn(), - createBead: vi.fn(), - updateBead: vi.fn(), - closeBead: vi.fn(), - nudgeAgent: vi.fn(), - agentPrime: vi.fn(), - sling: vi.fn(), - formulaFeed: vi.fn(), - listMail: vi.fn(), - markMailRead: vi.fn(), - markMailUnread: vi.fn(), - archiveMail: vi.fn(), - replyMail: vi.fn(), - sendMail: vi.fn(), - mailThread: vi.fn(), - cityEventStreamUrl: vi.fn(), - sessionStreamUrl: vi.fn(), - listSessions: vi.fn(), - sessionPending: vi.fn(), - respondSession: vi.fn(), - sessionTranscript: vi.fn(), - workflowRun: vi.fn(), - formulaDetail: vi.fn(), - mutationHeaders: () => ({ 'X-GC-Request': 'dashboard' }), +// The four source loaders now all delegate to one warm GET against the BFF +// run-projection endpoint, which folds the city event log and layers +// session health/census + thrash marks server-side. Run semantics — grouping, +// phase/stage classification, health/census derivation, stale-latch demotion, +// the cheap/wide read split — moved to Go (internal/runproj) and are covered +// byte-for-byte by the goldens there. What remains in TS is purely the +// SourceState wrapping (fresh on success, error on failure), tested here. + +const sampleSummary = { + totalActive: 1, + totalHistorical: 0, + runCounts: { active: 1, blocked: 0, complete: 0 }, + lanes: [], + historicalLanes: [], + blockedLanes: [], + recentChanges: [], + census: { status: 'unavailable' }, }; -describe('loadSupervisorRunSummaryPreviewSource', () => { - beforeEach(() => { - setActiveCity('test-city'); - resetSupervisorRunSummaryStateForTests(); - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-06-01T12:00:00.000Z')); - }); - - afterEach(() => { - resetSupervisorApiForTests(); - resetSupervisorRunSummaryStateForTests(); - vi.unstubAllGlobals(); - vi.useRealTimers(); - vi.clearAllMocks(); - }); - - it('builds a first-paint summary from bounded active and recent run reads', async () => { - const listBeads = vi.fn(async () => beadList([runRoot()])); - const formulaFeed = vi.fn(async () => feed([feedRun()])); - const listSessions = vi.fn(async () => sessionList()); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed, - listSessions, - }); - - const source = await loadSupervisorRunSummaryPreviewSource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes[0]?.id).toBe('run-1'); - expect(source.data.lanes[0]?.health.status).toBe('unavailable'); - expect(listBeads).toHaveBeenCalledTimes(3); - expect(listBeads).toHaveBeenCalledWith('test-city', { limit: 500 }); - expect(listBeads).toHaveBeenCalledWith('test-city', { - limit: 500, - type: 'molecule', - all: true, - }); - expect(listBeads).toHaveBeenCalledWith('test-city', { - limit: 500, - type: 'task', - rig: 'rig-a', - all: true, - }); - expect(formulaFeed).toHaveBeenCalledWith('test-city', { - scope_kind: 'city', - scope_ref: 'test-city', - }); - expect(listSessions).not.toHaveBeenCalled(); - }); - - it('marks the summary partial when a slow enrichment read exceeds the tight first-paint budget (gascity-dashboard-4bol)', async () => { - // First paint blocks on the preview load, so it keeps a tight 2.5s budget: a - // rig read that takes 10s on a slow supervisor degrades to partial rather - // than holding the tab blank. The wider refresh budget then clears it. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') { - return new Promise((resolve) => { - setTimeout(() => resolve(beadList([])), 10_000); - }); - } - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummaryPreviewSource(); - await vi.advanceTimersByTimeAsync(2_500); - const source = await pending; - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanesPartial).toBe(true); - }); -}); - -describe('loadSupervisorRunSummarySource', () => { - beforeEach(() => { - setActiveCity('test-city'); - resetSupervisorRunSummaryStateForTests(); - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-06-01T12:00:00.000Z')); - }); +const loaders = { + source: loadSupervisorRunSummarySource, + mount: loadSupervisorRunSummaryMountSource, + active: loadSupervisorRunSummaryActiveSource, + preview: loadSupervisorRunSummaryPreviewSource, +}; +describe('run summary source loaders', () => { afterEach(() => { - resetSupervisorApiForTests(); - resetSupervisorRunSummaryStateForTests(); vi.unstubAllGlobals(); - vi.useRealTimers(); - vi.clearAllMocks(); }); - it('builds the run summary from direct supervisor beads, feed, and sessions', async () => { - const fetchSpy = vi.fn(); - vi.stubGlobal('fetch', fetchSpy); - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') { - return beadList([ - bead({ - id: 'run-1-step-1', - title: 'Implementation patch', - status: 'in_progress', - metadata: { - 'gc.kind': 'step', - 'gc.root_bead_id': 'run-1', - 'gc.parent_bead_id': 'run-1', - 'gc.step_id': 'implementation.patch', - }, + for (const [name, load] of Object.entries(loaders)) { + it(`${name}: wraps the warm GET as a fresh runs source`, async () => { + const fetchMock = vi.fn( + async () => + new Response(JSON.stringify(sampleSummary), { + status: 200, + headers: { 'content-type': 'application/json' }, }), - ]); - } - return beadList([runRoot()]); - }); - const formulaFeed = vi.fn(async () => feed([feedRun()])); - const listSessions = vi.fn(async () => sessionList()); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed, - listSessions, - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.fetchedAt).toBe('2026-06-01T12:00:00.000Z'); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes).toHaveLength(1); - expect(source.data.lanes[0]).toMatchObject({ - id: 'run-1', - title: 'Adopt PR #42', - phase: 'implementation', - scope: { - status: 'available', - kind: 'rig', - ref: 'rig-a', - rootStoreRef: 'rig:rig-a', - }, - statusCounts: { open: 1, in_progress: 1 }, - }); - expect(source.data.census.status).toBe('available'); - expect(listBeads).toHaveBeenCalledWith('test-city', { limit: 500 }); - expect(listBeads).toHaveBeenCalledWith('test-city', { - limit: 500, - type: 'molecule', - all: true, - }); - expect(listBeads).toHaveBeenCalledWith('test-city', { - limit: 500, - type: 'task', - rig: 'rig-a', - all: true, - }); - expect(formulaFeed).toHaveBeenCalledWith('test-city', { - scope_kind: 'city', - scope_ref: 'test-city', - }); - expect(listSessions).toHaveBeenCalledWith('test-city'); - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it('derives a rig lane scope from the feed root_store_ref even when the feed scope_kind is city (q89b detail scope leak)', async () => { - // The formula feed's top-level scope_kind is always 'city'; the rig identity - // lives only in root_store_ref. When the root bead carries no scope metadata - // (scope must come from the feed map), the lane must still resolve to its rig - // scope so the detail href carries it and the workflow fetch hits the fast - // single-store path — not the city-wide full-store scan. - const rootNoScope = runRoot({ - id: 'run-2', - metadata: { - 'gc.kind': 'run', - 'gc.formula': 'mol-adopt-pr-v2', - 'gc.formula_contract': 'graph.v2', - }, - }); - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig !== undefined) return beadList([]); - return beadList([rootNoScope]); - }); - const cityScopedFeedRun = feedRun({ - id: 'run-2', - root_bead_id: 'run-2', - workflow_id: 'run-2', - root_store_ref: 'rig:rig-b', - scope_kind: 'city', - scope_ref: 'test-city', - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([cityScopedFeedRun])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - const lane = source.data.lanes.find((l) => l.id === 'run-2'); - expect(lane?.scope).toEqual({ - status: 'available', - kind: 'rig', - ref: 'rig-b', - rootStoreRef: 'rig:rig-b', - }); - }); - - it('does not emit a malformed feed root_store_ref as the lane scope (validates against SCOPE_REF_RE)', async () => { - // The store-ref-first branch in discoverFromFeed must validate the parsed - // ref against SCOPE_REF_RE before using it — fromStoreRef does not validate, - // so a malformed root_store_ref like 'rig:bad ref@!' would otherwise become a - // scope_ref the detail route rejects. It must fall back to the feed scope. - const rootNoScope = runRoot({ - id: 'run-3', - metadata: { - 'gc.kind': 'run', - 'gc.formula': 'mol-adopt-pr-v2', - 'gc.formula_contract': 'graph.v2', - }, - }); - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig !== undefined) return beadList([]); - return beadList([rootNoScope]); - }); - const malformedFeedRun = feedRun({ - id: 'run-3', - root_bead_id: 'run-3', - workflow_id: 'run-3', - root_store_ref: 'rig:bad ref@!', - scope_kind: 'city', - scope_ref: 'test-city', - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([malformedFeedRun])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - const lane = source.data.lanes.find((l) => l.id === 'run-3'); - expect(lane?.scope).toMatchObject({ - status: 'available', - kind: 'city', - ref: 'test-city', - }); - if (lane?.scope.status === 'available') { - expect(lane.scope.ref).not.toBe('bad ref@!'); - } - }); - - it('enriches blocked lanes with health and keeps them out of the active set (gascity-dashboard-4xcv)', async () => { - // gc-1920 repro: a stale blocked formula latch must land in - // blockedLanes (with derived health, so attention still sees it), - // never in lanes/totalActive. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') return beadList([]); - return beadList([ - runRoot(), - runRoot({ - id: 'gc-1920', - title: 'mol-focus-review', - status: 'blocked', - metadata: { - 'gc.kind': 'workflow', - 'gc.formula_contract': 'graph.v2', - 'gc.scope_kind': 'city', - 'gc.scope_ref': 'test-city', - 'gc.root_store_ref': 'city:test-city', - }, - }), - ]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['run-1']); - expect(source.data.blockedLanes.map((lane) => lane.id)).toEqual(['gc-1920']); - expect(source.data.blockedLanes[0]?.health.status).toBe('available'); - expect(source.data.runCounts.blocked).toBe(1); - }); - - // gascity-dashboard-s4rp: the gc-1920 phantom now surfaces NOT as blocked but - // as a stale session-less approval latch — counted Active:1 despite no live - // session, no in_progress step, and ~4d since its last write. Enrichment must - // demote it out of the Active set and count once sessions resolve. - function staleLatch(): Bead { - return runRoot({ - id: 'gc-1920', - title: 'mol-focus-review', - description: 'Focus + in-session review formula. Approval gate, review.', - status: 'open', - updated_at: '2026-05-28T00:00:00.000Z', - metadata: { - 'gc.kind': 'run', - 'gc.formula_contract': 'graph.v2', - 'gc.scope_kind': 'city', - 'gc.scope_ref': 'test-city', - 'gc.root_store_ref': 'city:test-city', - }, - }); - } - - it('demotes a stale session-less approval latch out of the Active set (gascity-dashboard-s4rp)', async () => { - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') { - return beadList([ - bead({ - id: 'run-1-step-1', - title: 'Implementation patch', - status: 'in_progress', - updated_at: '2026-06-01T11:55:00.000Z', - metadata: { - 'gc.kind': 'step', - 'gc.root_bead_id': 'run-1', - 'gc.step_id': 'implementation.patch', - }, - }), - ]); - } - return beadList([runRoot(), staleLatch()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - // run-1 has an in_progress step and stays Active; gc-1920 is demoted out of - // the Active set, count, AND the blocked/historical buckets (dropped). - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['run-1']); - expect(source.data.blockedLanes).toEqual([]); - expect(source.data.historicalLanes.map((lane) => lane.id)).not.toContain('gc-1920'); - expect(source.data.runCounts.total).toBe(1); - }); - - it('keeps a session-less recent latch in the Active set (gascity-dashboard-s4rp)', async () => { - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') return beadList([]); - // Same shape as the stale latch but written 30 minutes ago — a recent - // session-less latch must still appear as Active (not demoted by liveness). - return beadList([{ ...staleLatch(), updated_at: '2026-06-01T11:30:00.000Z' }]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['gc-1920']); - // gascity-dashboard-q3p1: this latch is a bare root with NO step beads, so - // phase comes from the tightened keyword fallback. Its title - // ('mol-focus-review') carries a 'review' signal → 'review'. The old - // assertion was 'approval', which was the bug itself: the OLD scan read - // 'approval'/'gate' out of the run's DESCRIPTION ("Approval gate, review.") - // even though no approval-gate step exists. The lane staying Active — the - // actual subject of this test — is unchanged. - expect(source.data.lanes[0]?.phase).toBe('review'); - }); - - it('demotes a phantom exactly even when active runs exceed the visible cap (gascity-dashboard-s4rp)', async () => { - // Demotion runs on the FULL active set, not the capped window, so totalActive - // is exact: a phantom is removed from the count even with >8 active runs. - const liveRuns = Array.from({ length: 9 }, (_, i) => - runRoot({ - id: `live-${i}`, - title: `Live run ${i}`, - status: 'open', - updated_at: '2026-06-01T11:55:00.000Z', - metadata: { - 'gc.kind': 'run', - 'gc.formula_contract': 'graph.v2', - 'gc.scope_kind': 'city', - 'gc.scope_ref': 'test-city', - 'gc.root_store_ref': 'city:test-city', - }, - }), - ); - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') return beadList([]); - return beadList([...liveRuns, staleLatch()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(9); - // gascity-dashboard: `lanes` carries the FULL active set now — the rendered - // 8-lane collapse is applied by RunMap, not the wire. - expect(source.data.lanes).toHaveLength(9); - expect(source.data.lanes.map((lane) => lane.id)).not.toContain('gc-1920'); - // The phantom (the 10th, session-less latch) is demoted; all 9 live runs survive. - expect(source.data.lanes.map((lane) => lane.id)).toEqual(liveRuns.map((_, i) => `live-${i}`)); - expect(source.data.runCounts.total).toBe(9); - expect(source.data.runCounts.visible).toBe(9); - }); - - it('keeps available lanes while marking the summary partial when a recent rig read fails', async () => { - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') throw new Error('rig unavailable'); - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanesPartial).toBe(true); - }); - - it('marks the summary partial when the active bead list is cursor-truncated', async () => { - // gascity-dashboard-4xcv: the supervisor truncates at the fetch limit and - // reports the rest via next_cursor WITHOUT setting partial. Treat a present - // cursor as partial so saturation surfaces the notice + retry instead of - // silently dropping lanes at 501+ beads. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - return beadList([runRoot()], false, 'next-page-token'); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanesPartial).toBe(true); - }); - - it('marks the summary partial when the active bead list is truncated at the fetch limit', async () => { - // gascity-dashboard-q89b: the primary fetch is bounded; when the upstream - // total exceeds what one page returned, active runs may be missing and the - // lanes must read as partial rather than complete. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') return beadList([]); - return { ...beadList([runRoot()]), total: 501 }; - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.lanesPartial).toBe(true); - }); - - it('does not let optional enrichment reads hold the summary refresh indefinitely', async () => { - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return new Promise(() => {}); - if (query?.rig === 'rig-a') return beadList([]); - return beadList([runRoot()]); - }); - const formulaFeed = vi.fn(async () => new Promise(() => {})); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed, - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummarySource(); - // The full source is Runs.tsx's background refresh, so its enrichment runs on - // the wider REFRESH budget (gascity-dashboard-4bol); the cap still fires. - await vi.advanceTimersByTimeAsync(30_000); - const source = await pending; - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanesPartial).toBe(true); - }); - - it('clears the spurious partial when a slow-but-available enrichment read lands within the wider refresh budget (gascity-dashboard-4bol)', async () => { - // The background refresh tolerates a slow supervisor: a rig read that takes - // 10s — past the 2.5s first-paint budget but inside the 30s refresh budget — - // lands, so the lanes are NOT latched partial (upstream gascity-dashboard#88). - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') { - return new Promise((resolve) => { - setTimeout(() => resolve(beadList([])), 10_000); - }); - } - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummarySource(); - await vi.advanceTimersByTimeAsync(10_000); - const source = await pending; - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanesPartial).toBeUndefined(); - }); - - it('keeps the tight first-paint budget on the mount source so Home/Formula Run Detail never block on a slow read (gascity-dashboard-4bol)', async () => { - // Same 10s rig read as the refresh test above, but the mount source (Home, - // Formula Run Detail first paint) runs on the 2.5s budget — the read does NOT - // land, so the lanes are latched partial rather than blocking ~30s on a cold - // navigation. This is the regression guard for the refresh-budget leak. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') { - return new Promise((resolve) => { - setTimeout(() => resolve(beadList([])), 10_000); - }); - } - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummaryMountSource(); - await vi.advanceTimersByTimeAsync(2_500); - const source = await pending; - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanesPartial).toBe(true); - }); - - it('keeps active lanes and marks partial when the molecule-history read rejects (gascity-dashboard-9rk2)', async () => { - // The molecule(all=true) scan surfaces historical run roots only; it is - // best-effort. A rejection must fold to `partial` with the active lanes still - // present — never escalate to a whole-view error/"Run data unavailable". - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') throw new Error('molecule history unavailable'); - if (query?.rig === 'rig-a') return beadList([]); - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['run-1']); - expect(source.data.lanesPartial).toBe(true); - }); - - it('bounds a slow molecule-history read to its own timeout and folds it to partial (gascity-dashboard-9rk2)', async () => { - // Live the molecule scan runs ~6.8s — past the 5s required-fetch budget. On - // the wide 30s refresh it would otherwise stall the whole refresh; its own - // 3s bound caps it so the active set still paints and only the historical - // lanes degrade to partial. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') { - return new Promise((resolve) => { - setTimeout(() => resolve(beadList([])), 6_800); - }); - } - if (query?.rig === 'rig-a') return beadList([]); - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummarySource(); - // Advance past the molecule's own 3s bound but well short of its 6.8s - // completion: the bound fires, the active set still resolves. - await vi.advanceTimersByTimeAsync(3_000); - const source = await pending; - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['run-1']); - expect(source.data.lanesPartial).toBe(true); - }); - - it('returns an error source when the active bead list fails', async () => { - setSupervisorApiForTests({ - ...baseApi, - listBeads: vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - throw new Error('beads unavailable'); - }), - formulaFeed: vi.fn(async () => feed([])), - listSessions: vi.fn(async () => sessionList()), - }); - - const source = await loadSupervisorRunSummarySource(); - - expect(source).toEqual({ - source: 'runs', - status: 'error', - error: 'beads unavailable', - }); - }); - - // gascity-dashboard fix/runs-fetch-resilience: a transient core-read timeout - // under a CPU burst must not blank the view on a first load (no last-good - // snapshot yet). The core active-bead read retries once before giving up. - it('retries the core active-bead read once on a transient timeout and resolves to data', async () => { - let coreAttempts = 0; - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - if (query?.rig === 'rig-a') return beadList([]); - coreAttempts += 1; - if (coreAttempts === 1) { - throw new SupervisorApiError( - undefined, - 'gc supervisor request timed out after 15000ms', - undefined, - ); + ); + vi.stubGlobal('fetch', fetchMock); + + const state = await load(); + + expect(state).toMatchObject({ + source: 'runs', + status: 'fresh', + error: { kind: 'none' }, + data: { totalActive: 1 }, + }); + if (state.status !== 'error') { + expect(typeof state.fetchedAt).toBe('string'); + expect(Date.parse(state.staleAt)).toBeGreaterThan(Date.parse(state.fetchedAt)); } - return beadList([runRoot()]); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([feedRun()])), - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummarySource(); - // Drain the short retry backoff so the second attempt fires. - await vi.advanceTimersByTimeAsync(250); - const source = await pending; - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - expect(coreAttempts).toBe(2); - expect(source.data.totalActive).toBe(1); - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['run-1']); - }); - - it('surfaces an error when the core active-bead read times out on every attempt', async () => { - let coreAttempts = 0; - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - coreAttempts += 1; - throw new SupervisorApiError( - undefined, - 'gc supervisor request timed out after 15000ms', - undefined, + expect(fetchMock).toHaveBeenCalledWith( + '/api/city/test-city/runs/summary', + expect.objectContaining({ method: 'GET' }), ); }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([])), - listSessions: vi.fn(async () => sessionList()), - }); - - const pending = loadSupervisorRunSummarySource(); - await vi.advanceTimersByTimeAsync(250); - const source = await pending; - - // A sustained failure is not hidden: after the retries are spent the view - // still surfaces the error so a real outage isn't masked forever. - expect(coreAttempts).toBe(2); - expect(source.status).toBe('error'); - if (source.status !== 'error') throw new Error('expected error source'); - expect(source.error).toContain('timed out after 15000ms'); - }); - - it('does not retry the core read on a non-transient (4xx) failure', async () => { - let coreAttempts = 0; - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query?.type === 'molecule') return beadList([]); - coreAttempts += 1; - throw new SupervisorApiError(400, 'bad request', undefined); - }); - setSupervisorApiForTests({ - ...baseApi, - listBeads, - formulaFeed: vi.fn(async () => feed([])), - listSessions: vi.fn(async () => sessionList()), - }); - const source = await loadSupervisorRunSummarySource(); - - expect(coreAttempts).toBe(1); - expect(source.status).toBe('error'); - }); + it(`${name}: returns an error source when the GET fails`, async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ error: 'run view is warming' }), { + status: 503, + headers: { 'content-type': 'application/json' }, + }), + ), + ); - it('reads the core active-bead fetch on the raised burst-tolerant budget', () => { - // The path is ~0.02s normally, so the higher ceiling only matters during a - // spike; it must stay well above the old 5s budget to absorb a burst. - expect(CORE_RUN_SUMMARY_TIMEOUT_MS).toBe(15_000); - expect(CORE_RUN_SUMMARY_TIMEOUT_MS).toBeGreaterThan(5_000); - }); -}); + const state = await load(); -describe('loadSupervisorRunSummaryActiveSource — cheap SSE-burst path', () => { - beforeEach(() => { - setActiveCity('test-city'); - resetSupervisorRunSummaryStateForTests(); - }); - afterEach(() => { - resetSupervisorApiForTests(); - vi.restoreAllMocks(); - }); - - it('skips the molecule history scan, city feed, and per-rig task reads', async () => { - // The core active read returns ONLY the no-query (active) listBeads call — - // any molecule/per-rig listBeads call or formulaFeed call would mean the - // cheap path is still firing the expensive reads. The run carries an - // in-progress step in the SAME core read (no per-rig fetch on the cheap - // path), so the lane stays active without session enrichment. - const listBeads = vi.fn(async (_cityName: string, query?: Record) => { - if (query && (query.type !== undefined || query.rig !== undefined)) { - throw new Error(`cheap path must not call listBeads with ${JSON.stringify(query)}`); - } - return beadList([ - runRoot(), - bead({ - id: 'run-1-step-1', - title: 'Implementation patch', - status: 'in_progress', - metadata: { - 'gc.kind': 'step', - 'gc.root_bead_id': 'run-1', - 'gc.parent_bead_id': 'run-1', - 'gc.step_id': 'implementation.patch', - }, - }), - ]); + expect(state).toMatchObject({ source: 'runs', status: 'error' }); + if (state.status === 'error') expect(state.error).toContain('warming'); }); - const formulaFeed = vi.fn(async () => feed([])); - const listSessions = vi.fn(async () => sessionList()); - setSupervisorApiForTests({ ...baseApi, listBeads, formulaFeed, listSessions }); - - const source = await loadSupervisorRunSummaryActiveSource(); - - expect(source.status).toBe('fresh'); - if (source.status === 'error') throw new Error(source.error); - // The active lane is present, and its rig scope resolves from the bead's own - // gc.root_store_ref metadata — no feed needed. - expect(source.data.lanes.map((lane) => lane.id)).toEqual(['run-1']); - expect(source.data.lanes[0]?.scope).toMatchObject({ status: 'available', kind: 'rig' }); - // No history reads on the cheap path. - expect(source.data.historicalLanes).toEqual([]); - expect(source.data.totalHistorical).toBe(0); - // The expensive reads were never issued. - expect(formulaFeed).not.toHaveBeenCalled(); - expect( - listBeads.mock.calls.every(([, query]) => query === undefined || query.limit !== undefined), - ).toBe(true); - expect(listBeads.mock.calls.some(([, query]) => query?.type === 'molecule')).toBe(false); - expect(listBeads.mock.calls.some(([, query]) => query?.rig !== undefined)).toBe(false); - }); + } }); - -function runRoot(overrides: Partial = {}): Bead { - return bead({ - id: 'run-1', - title: 'Adopt PR #42', - issue_type: 'molecule', - metadata: { - 'gc.kind': 'run', - 'gc.formula': 'mol-adopt-pr-v2', - 'gc.formula_contract': 'graph.v2', - 'gc.scope_kind': 'rig', - 'gc.scope_ref': 'rig-a', - 'gc.root_store_ref': 'rig:rig-a', - }, - ...overrides, - }); -} - -function bead(overrides: Partial = {}): Bead { - return { - id: 'bead-1', - title: 'Bead', - issue_type: 'task', - status: 'open', - created_at: '2026-06-01T11:00:00.000Z', - ...overrides, - }; -} - -function beadList(items: Bead[], partial = false, nextCursor?: string): ListBodyBead { - return { - items, - partial, - total: items.length, - ...(nextCursor !== undefined ? { next_cursor: nextCursor } : {}), - }; -} - -function feed(items: MonitorFeedItemResponse[], partial = false): FormulaFeedBody { - return { - items, - partial, - }; -} - -function feedRun(overrides: Partial = {}): MonitorFeedItemResponse { - return { - id: 'run-1', - root_bead_id: 'run-1', - root_store_ref: 'rig:rig-a', - workflow_id: 'run-1', - scope_kind: 'rig', - scope_ref: 'rig-a', - started_at: '2026-06-01T11:00:00.000Z', - status: 'running', - target: 'rig-a/codex', - title: 'Adopt PR #42', - type: 'formula', - updated_at: '2026-06-01T11:05:00.000Z', - ...overrides, - }; -} - -function sessionList(): ListBodySessionResponse { - return { - items: [], - total: 0, - }; -} diff --git a/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.ts b/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.ts index 783f26d365..310beec2ae 100644 --- a/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.ts +++ b/internal/api/dashboardspa/web/frontend/src/supervisor/runSummary.ts @@ -1,143 +1,21 @@ -import type { - DashboardBead, - DashboardSession, - RunFeedScope, - RunFeedScopeMap, - RunSummary, - SourceAvailableState, - SourceState, -} from 'gas-city-dashboard-shared'; -import { - advanceProgressMarks, - buildCensus, - buildRunSummary, - deriveRunHealth, - fromFeedScope, - fromDashboardBead, - fromRootMetadataScope, - fromStoreRef, - isStaleSessionlessLatch, - runBeadFilter, - runCounts, - SCOPE_REF_RE, - type LaneProgressMark, -} from 'gas-city-dashboard-shared'; -import { activeCityOrThrow } from '../api/cityBase'; -import type { Bead, FormulaFeedBody, ListBodyBead } from 'gas-city-dashboard-shared/gc-supervisor'; -import { supervisorApiForRequestBudget } from './client'; -import { fetchCoreRead } from './coreRead'; -import { listIsIncomplete, listIsPartial } from './listPartial'; -import { normalizeSessions } from './sessionReads'; - -// Pre-exposure load bound (gascity-dashboard-q89b): the primary in-flight -// bead fetch refreshes on SSE bursts (10s debounce floor in the shared -// run-summary subscription) per client. listIsIncomplete keeps truncation -// visible in the lanes-partial signal. -const RUNS_FETCH_LIMIT = 500; -// gascity-dashboard-4xcv: the supervisor's bead list has no sort/recency -// guarantee, so a small `all=true` window can drop run roots and step beads -// arbitrarily — observed live as an empty first paint and a history list -// missing most completed runs (a busy rig store holds hundreds of closed -// task beads). 500 covers the largest observed store with headroom; if a -// store outgrows it the symptom returns as silently missing lanes, so a -// real fix beyond raising the cap means cursor pagination. -const RECENT_RUN_FETCH_LIMIT = 500; +import type { RunSummary, SourceAvailableState, SourceState } from 'gas-city-dashboard-shared'; +import { api } from '../api/client'; + +// The run summary is served by one warm GET to the BFF run-projection endpoint +// (internal/api/dashboardbff/runtailer.go): a sub-second fold of the per-city +// event log that already layers session health/census and the monotonic +// thrash/progress marks server-side. The four exported source loaders — +// historically a tight "mount" read, a wide refresh, a cheap active-only SSE +// read, and a first-paint preview — collapse to that single complete read. The +// old client-side fold (buildRunSummary + enrichRunSummary over molecule/feed/ +// per-rig/session fan-out) is gone, and with it the cheap/wide split that only +// existed because those scans were slow. const RUNS_STALE_AFTER_MS = 60 * 1000; -// The CORE active-bead read is the one fetch whose failure blanks the whole runs -// view (everything else degrades to `partial`). The proxy path is ~0.02s -// normally, but the box runs at load avg ~30 under a slung-pipeline burst (the -// supervisor reconciler alone hits ~274% CPU), and during a spike a single core -// read occasionally crosses the old 5s budget → a first load with no last-good -// snapshot blanks to "Run data unavailable". Raise this read's ceiling to absorb -// a burst (the higher bound only costs time when actually slow), and retry once -// on a transient timeout/5xx before giving up. A real outage still surfaces an -// error after the retries are spent — the resilience only hides a brief spike, -// never a sustained failure (upstream gascity-dashboard#88). -const REQUIRED_RUN_SUMMARY_TIMEOUT_MS = 15_000; -// Optional run-summary enrichment (recent-bead / formula-feed / session reads) -// is best-effort: a miss degrades the lanes to "partial" rather than failing the -// load. The budget is split by call site (gascity-dashboard-4bol). The preview -// runs on first paint and blocks it, so it keeps a tight bound and the tab paints -// fast even when the supervisor is slow. The full source runs only as the shared -// run-summary subscription's background refreshFetcher, so it can afford a far -// wider budget matching the 30s -// background status samplers: on the bloated store the supervisor's list/feed -// reads run ~10-38s (upstream gascity-dashboard#88), so a 2.5s interactive bound -// always times out and latches a spurious "runs partial" badge. The wider refresh -// budget lets those slow-but-available reads land and clear it, without the -// first-paint spinner a single global raise would cause. -const PREVIEW_ENRICHMENT_TIMEOUT_MS = 2_500; -const REFRESH_ENRICHMENT_TIMEOUT_MS = 30_000; -// gascity-dashboard-9rk2: the molecule(all=true) read scans the full (large, -// ~340k-row) molecule history purely to surface HISTORICAL run roots, which the -// view already caps at MAX_HISTORICAL_LANES (50). Live it runs ~6.8s — past the -// 5s required-fetch budget — and the supervisor exposes no recency-ordered or -// bounded molecule query, so it cannot be made cheaper server-side without a new -// endpoint. It is therefore the FIRST optional read to dominate the wider refresh -// budget. Bound it well under REQUIRED_RUN_SUMMARY_TIMEOUT_MS so a slow scan -// degrades the historical lanes to "partial" fast instead of holding the refresh -// (and so it can never out-wait the active set, which paints from the fast -// open/active read). It still rides the surrounding enrichment budget too via the -// min() at the call site, so the tight first-paint path is never loosened. -const MOLECULE_HISTORY_TIMEOUT_MS = 3_000; - -interface LoadedRunBeads { - beads: DashboardBead[]; - feedScopes: RunFeedScopeMap; - partial: boolean; -} - -type RecentFetchOutcome = { ok: true; items: DashboardBead[]; partial: boolean } | { ok: false }; - -type RunSessionsLookup = - | { kind: 'available'; sessions: DashboardSession[] } - | { kind: 'unavailable'; sessions: DashboardSession[] }; - -interface ProgressState { - marks: Map; - fetchedAt: string; -} -const progressStateByCity = new Map(); - -// The wide-budget run-summary source (gascity-dashboard-4bol): the wide -// enrichment budget lets slow-but-available list/feed reads land and clear the -// spurious "runs partial" badge (upstream gascity-dashboard#88). It is the -// authoritative refresh snapshot for the shared run-summary subscription -// (runs/runSummarySubscription), which both the /runs page and the nav attention -// badge read — so the badge counts the same genuinely-blocked runs the page -// shows, by construction, off a single fan-out (gascity-dashboard-2j8e.7). -// -// Do NOT use this as a ROUTE first-paint fetcher — it can block a route view for -// up to REFRESH_ENRICHMENT_TIMEOUT_MS; route mount consumers (Home, Formula Run -// Detail) use loadSupervisorRunSummaryMountSource on the tight budget instead. -// The shared subscription is exempt: it is cache-backed and the always-mounted -// header reads its result, so its latency never blocks a route view. -export async function loadSupervisorRunSummarySource(): Promise> { - return loadRunSummarySource(REFRESH_ENRICHMENT_TIMEOUT_MS); -} - -// Mount / first-paint full source for Home and Formula Run Detail: the same data -// as the refresh source (lanes + sessions) but on the tight first-paint budget, -// so a cold navigation to those routes never blocks on slow optional enrichment -// reads (gascity-dashboard-4bol). -export async function loadSupervisorRunSummaryMountSource(): Promise> { - return loadRunSummarySource(PREVIEW_ENRICHMENT_TIMEOUT_MS); -} - -async function loadRunSummarySource(enrichmentBudgetMs: number): Promise> { - const cityName = activeCityOrThrow('load supervisor run summary'); +async function loadRunSummarySource(): Promise> { const fetchedAt = new Date().toISOString(); try { - const [loaded, sessions] = await Promise.all([ - loadRunBeads(cityName, RUNS_FETCH_LIMIT, enrichmentBudgetMs), - loadRunSessions(cityName, enrichmentBudgetMs), - ]); - const summary = buildRunSummary( - loaded.beads.filter(runBeadFilter).map(fromDashboardBead), - loaded.feedScopes, - loaded.partial, - ); + const summary = await api.runSummary(); const source: SourceAvailableState = { source: 'runs', status: 'fresh', @@ -146,99 +24,7 @@ async function loadRunSummarySource(enrichmentBudgetMs: number): Promise> { - const cityName = activeCityOrThrow('load supervisor run summary active'); - const fetchedAt = new Date().toISOString(); - try { - const [loaded, sessions] = await Promise.all([ - loadActiveRunBeads(cityName, RUNS_FETCH_LIMIT), - loadRunSessions(cityName, PREVIEW_ENRICHMENT_TIMEOUT_MS), - ]); - const summary = buildRunSummary( - loaded.beads.filter(runBeadFilter).map(fromDashboardBead), - loaded.feedScopes, - loaded.partial, - ); - const source: SourceAvailableState = { - source: 'runs', - status: 'fresh', - fetchedAt, - staleAt: new Date(Date.parse(fetchedAt) + RUNS_STALE_AFTER_MS).toISOString(), - error: { kind: 'none' }, - data: summary, - }; - return { - ...source, - data: enrichRunSummary(cityName, source, sessions), - }; - } catch (err) { - return { - source: 'runs', - status: 'error', - error: errorMessage(err, 'formula runs unavailable'), - }; - } -} - -// Core active read only — no molecule history scan, no city formula feed, no -// per-rig task reads. Truncation still reads as partial lanes. feedScopes is -// empty: a run root whose OWN bead carries no scope metadata falls back to -// 'unavailable' scope until the next wide refresh repairs it (minority path — -// runScope prefers per-bead metadata). -async function loadActiveRunBeads(cityName: string, limit: number): Promise { - const activeList = await fetchCoreActiveBeads(cityName, limit); - const active = normalizeBeads(activeList.items ?? []); - return { - beads: active, - feedScopes: new Map(), - partial: listIsIncomplete(activeList, active.length), - }; -} - -export async function loadSupervisorRunSummaryPreviewSource(): Promise> { - const cityName = activeCityOrThrow('load supervisor run summary preview'); - const fetchedAt = new Date().toISOString(); - try { - const loaded = await loadRunBeads(cityName, RUNS_FETCH_LIMIT, PREVIEW_ENRICHMENT_TIMEOUT_MS); - const summary = buildRunSummary( - loaded.beads.filter(runBeadFilter).map(fromDashboardBead), - loaded.feedScopes, - loaded.partial, - ); - return { - source: 'runs', - status: 'fresh', - fetchedAt, - staleAt: new Date(Date.parse(fetchedAt) + RUNS_STALE_AFTER_MS).toISOString(), - error: { kind: 'none' }, - // First paint has no sessions yet, so no latch demotion. `lanes` carries - // the full active set; RunMap applies the collapsed window. - data: summary, - }; + return source; } catch (err) { return { source: 'runs', @@ -248,317 +34,28 @@ export async function loadSupervisorRunSummaryPreviewSource(): Promise { - return fetchCoreRead(() => requiredRunSummaryApi().listBeads(cityName, { limit })); -} - -function optionalRunSummaryApi(budgetMs: number) { - return supervisorApiForRequestBudget(budgetMs); -} - -async function loadRunBeads( - cityName: string, - limit: number, - enrichmentBudgetMs: number, -): Promise { - // The molecule-history read gets its own tight bound (gascity-dashboard-9rk2), - // capped to the surrounding enrichment budget so the first-paint path is never - // loosened: a slow scan folds to `partial` instead of dominating the refresh. - const moleculeFetch = settledRecentFetch( - cityName, - { - limit: RECENT_RUN_FETCH_LIMIT, - type: 'molecule', - all: true, - }, - Math.min(MOLECULE_HISTORY_TIMEOUT_MS, enrichmentBudgetMs), - ); - const [activeList, feedDiscovery] = await Promise.all([ - fetchCoreActiveBeads(cityName, limit), - discoverFromFeed(cityName, enrichmentBudgetMs), - ]); - const active = normalizeBeads(activeList.items ?? []); - const rigNames = unionRigNames(runRigNames(active), feedDiscovery.rigNames); - - const rigFetches = rigNames.map((rig) => - settledRecentFetch( - cityName, - { - limit: RECENT_RUN_FETCH_LIMIT, - type: 'task', - rig, - all: true, - }, - enrichmentBudgetMs, - ), - ); - - const settled = await Promise.all([moleculeFetch, ...rigFetches]); - const recentItems: DashboardBead[] = []; - // Truncation at the bounded fetch reads as partial lanes, not complete - // (gascity-dashboard-q89b). - let partial = feedDiscovery.partial || listIsIncomplete(activeList, active.length); - - for (const outcome of settled) { - if (outcome.ok) { - recentItems.push(...outcome.items); - partial ||= outcome.partial; - continue; - } - partial = true; - } - - return { - beads: uniqueBeads([...active, ...recentItems]), - feedScopes: feedDiscovery.scopes, - partial, - }; -} - -async function settledRecentFetch( - cityName: string, - query: { limit: number; type: string; all: true; rig?: string }, - budgetMs: number, -): Promise { - try { - const list = await withOptionalReadBudget( - optionalRunSummaryApi(budgetMs).listBeads(cityName, query), - `recent ${query.type} beads`, - budgetMs, - ); - return { - ok: true, - items: normalizeBeads(list.items ?? []), - partial: listIsPartial(list), - }; - } catch { - return { ok: false }; - } -} - -interface FeedDiscovery { - rigNames: string[]; - scopes: RunFeedScopeMap; - partial: boolean; -} - -async function discoverFromFeed(cityName: string, budgetMs: number): Promise { - try { - const runs = await withOptionalReadBudget( - optionalRunSummaryApi(budgetMs).formulaFeed(cityName, { - scope_kind: 'city', - scope_ref: cityName, - }), - 'formula feed', - budgetMs, - ); - const rigNames = new Set(); - const scopes = new Map(); - for (const run of runs.items ?? []) { - if (run.type !== 'formula') continue; - const storeScope = fromStoreRef(run.root_store_ref ?? null); - if (storeScope?.scopeKind === 'rig') { - rigNames.add(storeScope.scopeRef); - } - const rootId = run.root_bead_id ?? run.workflow_id ?? null; - // gascity-dashboard-q89b (detail scope leak): the feed's top-level - // scope_kind is always 'city', so fromFeedScope alone makes a rig lane's - // detail href drop to city scope — and a city-scoped workflow fetch hits - // the supervisor's full-store scan (~12-14s, upstream #88) instead of the - // sub-second single-store rig fetch. Recover the rig from root_store_ref - // first (store-ref-first — deliberately the inverse of - // fromRootMetadataScope's pair-first rule: the metadata edge trusts its - // explicit pair, but the feed's pair is unreliable here, always 'city'). - // Fall back to the feed (city) scope only when the store ref names no rig. - // The store ref is validated against SCOPE_REF_RE before it is emitted as - // a lane scope: unlike fromFeedScope/fromRootMetadataScope, fromStoreRef - // does not validate, so a malformed root_store_ref must fall back rather - // than emit a scope_ref the detail route would reject. - const scope = - storeScope?.scopeKind === 'rig' && SCOPE_REF_RE.test(storeScope.scopeRef) - ? storeScope - : fromFeedScope(run); - if (rootId !== null && scope !== null) { - scopes.set(rootId, { - scopeKind: scope.scopeKind, - scopeRef: scope.scopeRef, - rootStoreRef: run.root_store_ref ?? `${scope.scopeKind}:${scope.scopeRef}`, - }); - } - } - return { rigNames: [...rigNames], scopes, partial: feedIsPartial(runs) }; - } catch { - return { rigNames: [], scopes: new Map(), partial: true }; - } -} - -async function loadRunSessions(cityName: string, budgetMs: number): Promise { - try { - const list = await withOptionalReadBudget( - optionalRunSummaryApi(budgetMs).listSessions(cityName), - 'run sessions', - budgetMs, - ); - return { - kind: 'available', - sessions: normalizeSessions(list), - }; - } catch { - return { kind: 'unavailable', sessions: [] }; - } -} - -function enrichRunSummary( - cityName: string, - source: SourceAvailableState, - sessionsLookup: RunSessionsLookup, -): RunSummary { - // gascity-dashboard-4xcv: blocked lanes are enriched alongside active - // ones so they carry derived health (needsOperator) for the attention - // layer, then split back out — they are not part of the Active set. - const inFlight = [...source.data.lanes, ...source.data.blockedLanes]; - const state = progressStateByCity.get(cityName); - const generationMs = Date.parse(source.fetchedAt); - let marks = state?.marks ?? new Map(); - if (state === undefined || generationMs > Date.parse(state.fetchedAt)) { - marks = advanceProgressMarks(marks, inFlight); - progressStateByCity.set(cityName, { marks, fetchedAt: source.fetchedAt }); - } - - const sessionsAvailable = sessionsLookup.kind === 'available'; - const { lanes } = deriveRunHealth({ - lanes: inFlight, - sessions: sessionsLookup.sessions, - sessionsAvailable, - marks, - }); - - const blockedLanes = lanes.filter((lane) => lane.phase === 'blocked'); - const activeEnriched = lanes.filter((lane) => lane.phase !== 'blocked'); - - // gascity-dashboard-s4rp: sessions only resolve here at enrichment, so this is - // the earliest seam with enough information to demote stale session-less - // latches (the gc-1920 phantom: no live session, no in_progress step, days - // stale) out of the Active set. buildRunSummary hands us the FULL active set - // (not the capped window), so totalActive is recomputed exactly from the - // surviving lanes — a phantom past the 8th slot is demoted too. Staleness is - // judged against the snapshot generation time, not a live clock, so the result - // is stable for a snapshot. - const liveActive = activeEnriched.filter( - (lane) => !isStaleSessionlessLatch(lane, generationMs, sessionsAvailable), - ); - const census = buildCensus([...liveActive, ...blockedLanes]); - - // gascity-dashboard: `lanes` carries the FULL active set; RunMap owns the - // collapsed window (MAX_VISIBLE_ACTIVE_LANES) and its "Show N more runs" - // expander, mirroring the historical section. - return { - ...source.data, - totalActive: liveActive.length, - lanes: liveActive, - blockedLanes, - runCounts: runCounts(liveActive, liveActive.length, blockedLanes.length), - census: { status: 'available', data: census }, - }; -} - -function runRigNames(beads: readonly DashboardBead[]): string[] { - const names = new Set(); - for (const bead of beads) { - const storeScope = fromStoreRef(bead.metadata?.['gc.root_store_ref']); - if (storeScope?.scopeKind === 'rig') { - names.add(storeScope.scopeRef); - continue; - } - - const metadataScope = fromRootMetadataScope(bead.metadata); - if (metadataScope?.scopeKind === 'rig') { - names.add(metadataScope.scopeRef); - } - } - return Array.from(names).sort(); -} - -function unionRigNames(a: readonly string[], b: readonly string[]): string[] { - const all = new Set(); - for (const name of a) all.add(name); - for (const name of b) all.add(name); - return [...all]; -} - -function withOptionalReadBudget( - promise: Promise, - label: string, - budgetMs: number, -): Promise { - let timeout: ReturnType | null = null; - const budget = new Promise((_, reject) => { - timeout = setTimeout(() => { - reject(new Error(`${label} timed out after ${budgetMs}ms`)); - }, budgetMs); - }); - return Promise.race([ - promise.finally(() => { - if (timeout !== null) clearTimeout(timeout); - }), - budget, - ]); -} - -function uniqueBeads(beads: readonly DashboardBead[]): DashboardBead[] { - const byId = new Map(); - for (const bead of beads) { - if (!byId.has(bead.id)) byId.set(bead.id, bead); - } - return Array.from(byId.values()); +/** + * The authoritative run-summary source for the shared subscription + * (runs/runSummarySubscription) — the snapshot the /runs page renders and the + * nav attention badge counts off one fetch. + */ +export function loadSupervisorRunSummarySource(): Promise> { + return loadRunSummarySource(); } -function normalizeBeads(beads: readonly Bead[]): DashboardBead[] { - return beads.map(normalizeBead); +/** Mount / first-paint source for Home and Formula Run Detail. */ +export function loadSupervisorRunSummaryMountSource(): Promise> { + return loadRunSummarySource(); } -function normalizeBead(bead: Bead): DashboardBead { - const normalized: DashboardBead = { - id: bead.id, - title: bead.title, - status: bead.status, - issue_type: bead.issue_type, - priority: bead.priority ?? null, - created_at: bead.created_at, - }; - if (bead.description !== undefined) normalized.description = bead.description; - if (bead.assignee !== undefined) normalized.assignee = bead.assignee; - if (Array.isArray(bead.labels)) normalized.labels = bead.labels; - if (bead.metadata !== undefined) normalized.metadata = bead.metadata; - if (bead.ref !== undefined) normalized.ref = bead.ref; - if (bead.parent !== undefined) normalized.parent = bead.parent; - if (bead.from !== undefined) normalized.from = bead.from; - if (bead.ephemeral !== undefined) normalized.ephemeral = bead.ephemeral; - if (bead.needs !== undefined) normalized.needs = bead.needs; - if (bead.dependencies !== undefined) normalized.dependencies = bead.dependencies; - if (bead.updated_at !== undefined) normalized.updated_at = bead.updated_at; - return normalized; +/** Cheap SSE-burst refresh source for the shared subscription. */ +export function loadSupervisorRunSummaryActiveSource(): Promise> { + return loadRunSummarySource(); } -function feedIsPartial(feed: FormulaFeedBody): boolean { - return feed.partial === true || (feed.partial_errors?.length ?? 0) > 0; +/** First-paint preview source for the shared subscription. */ +export function loadSupervisorRunSummaryPreviewSource(): Promise> { + return loadRunSummarySource(); } function errorMessage(err: unknown, fallback: string): string { diff --git a/internal/api/dashboardspa/web/shared/src/api-error.ts b/internal/api/dashboardspa/web/shared/src/api-error.ts index 1b82a75ba9..d076eb8b5b 100644 --- a/internal/api/dashboardspa/web/shared/src/api-error.ts +++ b/internal/api/dashboardspa/web/shared/src/api-error.ts @@ -5,4 +5,11 @@ export interface ApiError { kind?: string; /** Optional details object — never leaks raw stderr to the browser. */ details?: Record; + /** + * Optional run-detail discriminator. The BFF `/runs/{id}/detail` endpoint + * sets this on a 422 to `'not_run_view'` (an honest list-only v1/wisp run) + * or `'invalid_snapshot'` (a genuine load failure) so the SPA can render the + * two unprocessable cases distinctly. Other endpoints leave it absent. + */ + reason?: string; } diff --git a/internal/api/dashboardspa/web/shared/src/index.ts b/internal/api/dashboardspa/web/shared/src/index.ts index e4bc77e83a..3a3c744a54 100644 --- a/internal/api/dashboardspa/web/shared/src/index.ts +++ b/internal/api/dashboardspa/web/shared/src/index.ts @@ -12,26 +12,13 @@ export * from './session-id.js'; export * from './work-in-flight.js'; export type * from './viewing-as.js'; export * from './agents/needsYou.js'; -export * from './runs/bead-fields.js'; +// The run-fold/graph-layout pipeline moved to Go (internal/runproj); the SPA is +// a pure renderer of the RunSummary / FormulaRunDetail DTOs. Only these run +// presentation helpers stay client-side: the blocked-runs selector, the +// needs-operator accessor, and the active-lane window size. export * from './runs/blocked.js'; -export * from './runs/display-state.js'; -export * from './runs/edges.js'; -export * from './runs/enrich.js'; -export * from './runs/execution-instances.js'; -export * from './runs/execution-path.js'; -export * from './runs/formula-name.js'; -export * from './runs/formula-order.js'; -export * from './runs/formula-run.js'; -export * from './runs/groups.js'; export * from './runs/health.js'; -export * from './runs/lanes.js'; -export * from './runs/liveness.js'; -export * from './runs/node-shape.js'; -export * from './runs/phaseMapping.js'; -export * from './runs/runtime-state.js'; -export * from './runs/session-link.js'; export * from './runs/summary.js'; -export * from './runs/status.js'; export * from './bead-id.js'; export * from './links.js'; export * from './links/build-link-view.js'; diff --git a/internal/api/dashboardspa/web/shared/src/runs/bead-fields.ts b/internal/api/dashboardspa/web/shared/src/runs/bead-fields.ts deleted file mode 100644 index 20f21bc990..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/bead-fields.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; - -export function meta(bead: RunSnapshotBead | undefined, key: string): string | undefined { - const value = bead?.metadata?.[key]; - if (typeof value === 'string') return nonEmpty(value); - return undefined; -} - -export function nonEmpty(value: unknown): string | undefined { - return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; -} - -export function normalizedStepRef(bead: RunSnapshotBead): string | null { - const ref = meta(bead, 'gc.step_ref') ?? nonEmpty(bead.step_ref); - return ref ?? null; -} - -export function iterationFor(bead: RunSnapshotBead): number | undefined { - return ( - numericMeta(bead, 'gc.iteration') ?? - numericRefSegment(bead, 'iteration') ?? - numericRefSegment(bead, 'run') - ); -} - -export function attemptFor(bead: RunSnapshotBead): number | undefined { - return ( - numericMeta(bead, 'gc.attempt') ?? - numericField(bead.attempt) ?? - numericRefSegment(bead, 'attempt') - ); -} - -export function positiveIntegerMeta(bead: RunSnapshotBead, key: string): number | undefined { - return numericMeta(bead, key); -} - -export function externalizeId(id: string): string { - return id.replace(/(^|[^A-Za-z0-9])ralph(?=$|[^A-Za-z0-9])/gi, '$1check-loop'); -} - -function numericRefSegment(bead: RunSnapshotBead, marker: string): number | undefined { - const ref = normalizedStepRef(bead); - if (!ref) return undefined; - const parts = ref.split('.'); - for (let i = 0; i < parts.length - 1; i += 1) { - if (parts[i] !== marker) continue; - const parsed = numericField(parts[i + 1]); - if (parsed !== undefined) return parsed; - } - return undefined; -} - -function numericMeta(bead: RunSnapshotBead, key: string): number | undefined { - return numericField(meta(bead, key)); -} - -function numericField(value: unknown): number | undefined { - if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value; - if (typeof value !== 'string') return undefined; - if (!/^[1-9]\d*$/.test(value)) return undefined; - const parsed = Number.parseInt(value, 10); - return Number.isSafeInteger(parsed) ? parsed : undefined; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/display-state.ts b/internal/api/dashboardspa/web/shared/src/runs/display-state.ts deleted file mode 100644 index a52541fdd9..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/display-state.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { RunDisplayEdge, RunDisplayNode, RunNodeStatus } from '../run-detail.js'; - -const TERMINAL_STATUSES = new Set(['completed', 'done', 'failed', 'skipped']); - -/** - * Convert raw bead state into graph presentation state. The supervisor exposes - * durable bead status, while the dashboard needs to show whether a waiting node - * is actually ready to be claimed or still blocked by upstream work. - */ -export function applyDisplayNodeStates( - nodes: readonly RunDisplayNode[], - edges: readonly RunDisplayEdge[], -): RunDisplayNode[] { - const byId = new Map(nodes.map((node) => [node.id, node])); - const inbound = buildInboundEdges(edges, byId); - const statusById = new Map(); - - for (const node of nodes) { - statusById.set(node.id, displayStatusFor(node, inbound.get(node.id) ?? [], byId)); - } - - return nodes.map((node) => { - const status = statusById.get(node.id) ?? node.status; - if (status === node.status) return node; - return { - ...node, - status, - executionInstances: node.executionInstances.map((instance) => - instance.currentIteration !== false && instance.status === 'pending' - ? { ...instance, status } - : instance, - ), - }; - }); -} - -function displayStatusFor( - node: RunDisplayNode, - blockers: readonly string[], - byId: Map, -): RunNodeStatus { - if (node.status !== 'pending') return node.status; - if (blockers.length === 0) return 'ready'; - const allDone = blockers.every((blockerId) => { - const blocker = byId.get(blockerId); - return blocker ? TERMINAL_STATUSES.has(blocker.status) : false; - }); - return allDone ? 'ready' : 'blocked'; -} - -function buildInboundEdges( - edges: readonly RunDisplayEdge[], - byId: Map, -): Map { - const inbound = new Map(); - for (const edge of edges) { - if (!byId.has(edge.from) || !byId.has(edge.to)) continue; - inbound.set(edge.to, [...(inbound.get(edge.to) ?? []), edge.from]); - } - return inbound; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/edges.ts b/internal/api/dashboardspa/web/shared/src/runs/edges.ts deleted file mode 100644 index aa80f49463..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/edges.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { RunSnapshotDep, RunSnapshot } from '../run-snapshot.js'; -import type { RunDisplayEdge, RunDisplayNode } from '../run-detail.js'; -import { externalizeId, nonEmpty } from './bead-fields.js'; - -export function buildRunDisplayEdges( - raw: RunSnapshot, - physicalToSemantic: Map, - nodes: RunDisplayNode[], -): RunDisplayEdge[] { - const logicalEdges = projectEdges(raw.logical_edges ?? [], physicalToSemantic, nodes); - if (logicalEdges.length > 0) return logicalEdges; - return projectEdges(raw.deps ?? [], physicalToSemantic, nodes, bridgeableScopeCheckIds(raw)); -} - -function projectEdges( - deps: RunSnapshotDep[], - physicalToSemantic: Map, - nodes: RunDisplayNode[], - bridgeableHiddenIds = new Set(), -): RunDisplayEdge[] { - const visible = new Set( - nodes.filter((node) => node.visibleInGraph !== false).map((node) => node.id), - ); - const outgoing = outgoingDeps(deps); - const seen = new Set(); - const edges: RunDisplayEdge[] = []; - for (const dep of deps) { - const rawFrom = nonEmpty(dep.from); - const rawTo = nonEmpty(dep.to); - if (!rawFrom || !rawTo) continue; - if (nonEmpty(dep.kind) === 'tracks') continue; - const from = physicalToSemantic.get(rawFrom) ?? externalizeId(rawFrom); - const to = physicalToSemantic.get(rawTo) ?? externalizeId(rawTo); - const kind = nonEmpty(dep.kind); - if (visible.has(from) && visible.has(to)) { - pushEdge(edges, seen, from, to, kind); - continue; - } - if (visible.has(from) && bridgeableHiddenIds.has(rawTo)) { - bridgeHiddenEdges({ - edges, - seen, - source: from, - currentRawId: rawTo, - outgoing, - visible, - bridgeableHiddenIds, - physicalToSemantic, - ...(kind !== undefined ? { inheritedKind: kind } : {}), - }); - } - } - return edges; -} - -function bridgeHiddenEdges({ - edges, - seen, - source, - currentRawId, - outgoing, - visible, - bridgeableHiddenIds, - physicalToSemantic, - inheritedKind, - visited = new Set(), -}: { - edges: RunDisplayEdge[]; - seen: Set; - source: string; - currentRawId: string; - outgoing: Map; - visible: Set; - bridgeableHiddenIds: Set; - physicalToSemantic: Map; - inheritedKind?: string; - visited?: Set; -}): void { - if (visited.has(currentRawId)) return; - visited.add(currentRawId); - for (const dep of outgoing.get(currentRawId) ?? []) { - const rawTo = nonEmpty(dep.to); - if (!rawTo) continue; - const kind = nonEmpty(dep.kind); - if (kind === 'tracks') continue; - const target = physicalToSemantic.get(rawTo) ?? externalizeId(rawTo); - const edgeKind = kind ?? inheritedKind; - if (visible.has(target)) { - pushEdge(edges, seen, source, target, edgeKind); - } else if (bridgeableHiddenIds.has(rawTo)) { - bridgeHiddenEdges({ - edges, - seen, - source, - currentRawId: rawTo, - outgoing, - visible, - bridgeableHiddenIds, - physicalToSemantic, - ...(edgeKind !== undefined ? { inheritedKind: edgeKind } : {}), - visited, - }); - } - } -} - -function pushEdge( - edges: RunDisplayEdge[], - seen: Set, - from: string, - to: string, - kind?: string, -): void { - if (from === to) return; - const edgeKind = kind ?? 'dependency'; - const key = `${from}->${to}:${edgeKind}`; - if (seen.has(key)) return; - seen.add(key); - edges.push({ from, to, kind: edgeKind }); -} - -function outgoingDeps(deps: RunSnapshotDep[]): Map { - const out = new Map(); - for (const dep of deps) { - const from = nonEmpty(dep.from); - const to = nonEmpty(dep.to); - if (!from || !to) continue; - out.set(from, [...(out.get(from) ?? []), dep]); - } - return out; -} - -function bridgeableScopeCheckIds(raw: RunSnapshot): Set { - const ids = new Set(); - for (const bead of raw.beads ?? []) { - const id = nonEmpty(bead.id); - if (!id) continue; - const kind = nonEmpty(bead.metadata?.['gc.kind']) ?? nonEmpty(bead.kind); - if (kind === 'scope-check') ids.add(id); - } - return ids; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/enrich.ts b/internal/api/dashboardspa/web/shared/src/runs/enrich.ts deleted file mode 100644 index bacc51e718..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/enrich.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { FormulaDetail, RunSnapshotBead, RunSnapshot } from '../run-snapshot.js'; -import type { DashboardSession } from '../dashboard-sessions.js'; -import type { - RunFormulaDetailState, - FormulaRunCompleteness, - FormulaRunDetail, - FormulaRunPartialReason, -} from '../run-detail.js'; -import { fromSnapshotScope } from '../run-scope.js'; -import { meta, nonEmpty } from './bead-fields.js'; -import type { RunningFormulaRunInput } from './formula-run.js'; -import { buildRunningFormulaRun } from './formula-run.js'; - -interface EnrichOptions { - rigRoot?: string; - sessions?: readonly DashboardSession[]; - formulaDetail?: FormulaDetail; - formulaDetailState?: RunFormulaDetailState; -} - -/** - * Why a run snapshot cannot be enriched into a detail view. - * gascity-dashboard-9w3k: distinguishes the expected v1 / wisp case - * ('not_run_view' — the run shows in the list but has no graph.v2 detail - * view) from a malformed graph.v2 snapshot ('invalid_snapshot' — a genuine - * load failure). The frontend renders these differently: the former is an - * honest "list-only" message, the latter a generic load error. - */ -export type UnsupportedRunReason = 'not_run_view' | 'invalid_snapshot'; - -export class UnsupportedRunError extends Error { - readonly reason: UnsupportedRunReason; - - constructor(message: string, reason: UnsupportedRunReason = 'invalid_snapshot') { - super(message); - this.name = 'UnsupportedRunError'; - this.reason = reason; - } -} - -export function enrichFormulaRun(raw: RunSnapshot, opts: EnrichOptions): FormulaRunDetail { - if (!isGraphV2(raw)) { - throw new UnsupportedRunError('run is not a graph.v2 run', 'not_run_view'); - } - - const rootBeadId = nonEmpty(raw.root_bead_id) ?? ''; - const runId = nonEmpty(raw.run_id); - const rootStoreRef = nonEmpty(raw.root_store_ref); - const resolvedRootStore = nonEmpty(raw.resolved_root_store); - const beads = dedupeBeads(Array.isArray(raw.beads) ? raw.beads : []); - const root = rootBead(beads, rootBeadId); - const scope = fromSnapshotScope(raw); - if (!runId || !rootStoreRef || !resolvedRootStore) { - throw new UnsupportedRunError('run snapshot identity is missing or invalid'); - } - if (scope === null) { - throw new UnsupportedRunError('run scope is missing or invalid'); - } - if (!Number.isFinite(raw.snapshot_version)) { - throw new UnsupportedRunError('run snapshot version is missing or invalid'); - } - if (typeof raw.partial !== 'boolean') { - throw new UnsupportedRunError('run partial flag is missing or invalid'); - } - - const runInput: RunningFormulaRunInput = { - raw, - runId, - rootBeadId, - rootStoreRef, - resolvedRootStore, - scopeKind: scope.scopeKind, - scopeRef: scope.scopeRef, - beads, - }; - if (root !== undefined) runInput.root = root; - if (opts.rigRoot !== undefined) runInput.rigRoot = opts.rigRoot; - if (opts.sessions !== undefined) runInput.sessions = opts.sessions; - if (opts.formulaDetail !== undefined) runInput.formulaDetail = opts.formulaDetail; - if (opts.formulaDetailState !== undefined) runInput.formulaDetailState = opts.formulaDetailState; - - const formulaRun = buildRunningFormulaRun(runInput); - const partialReasons: FormulaRunPartialReason[] = raw.partial - ? ['supervisor_snapshot_partial'] - : []; - - return { - runId, - rootBeadId, - rootStoreRef, - resolvedRootStore, - scopeKind: scope.scopeKind, - scopeRef: scope.scopeRef, - title: formulaRun.title, - formula: formulaRun.formula, - formulaDetail: formulaRun.formulaDetail, - executionPath: formulaRun.executionPath, - snapshotVersion: raw.snapshot_version, - snapshotEventSeq: formulaRun.progress.snapshotEventSeq, - completeness: formulaRunCompleteness(partialReasons), - progress: formulaRun.progress, - phase: formulaRun.phase, - stages: formulaRun.stages, - nodes: formulaRun.nodes, - edges: formulaRun.edges, - lanes: formulaRun.lanes, - }; -} - -export function formulaRunCompleteness( - reasons: readonly FormulaRunPartialReason[], -): FormulaRunCompleteness { - const uniqueReasons = [...new Set(reasons)]; - return uniqueReasons.length === 0 - ? { kind: 'complete' } - : { kind: 'partial', reasons: uniqueReasons }; -} - -function isGraphV2(raw: RunSnapshot): boolean { - const root = rootBead(Array.isArray(raw.beads) ? raw.beads : [], raw.root_bead_id); - return meta(root, 'gc.formula_contract') === 'graph.v2'; -} - -function rootBead( - beads: RunSnapshotBead[], - rootBeadId: string | undefined, -): RunSnapshotBead | undefined { - const rootId = nonEmpty(rootBeadId); - if (!rootId) return undefined; - return beads.find((bead) => nonEmpty(bead.id) === rootId); -} - -function dedupeBeads(beads: RunSnapshotBead[]): RunSnapshotBead[] { - const seen = new Set(); - const out: RunSnapshotBead[] = []; - for (const bead of beads) { - const id = nonEmpty(bead.id); - if (id) { - if (seen.has(id)) continue; - seen.add(id); - } - out.push(bead); - } - return out; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/execution-instances.ts b/internal/api/dashboardspa/web/shared/src/runs/execution-instances.ts deleted file mode 100644 index 57f43a655d..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/execution-instances.ts +++ /dev/null @@ -1,264 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; -import type { - RunAttempt, - RunAttemptSummary, - RunConstructKind, - RunControlBadge, - RunDisplayNode, - RunExecutionInstance, - RunIteration, - RunIterationSummary, - RunNodeScope, - RunSessionAttachment, -} from '../run-detail.js'; -import { attemptFor, iterationFor, nonEmpty, positiveIntegerMeta } from './bead-fields.js'; -import type { RunSessionLinkContext } from './session-link.js'; -import { runSessionLinkFor } from './session-link.js'; -import { aggregateStatus, isRunningStatus, presentationStatus } from './status.js'; - -export interface RunNodeGroup { - semanticNodeId: string; - title: string; - kind: string; - constructKind: RunConstructKind; - scopeRef?: string; - loopControlNodeId?: string; - beads: RunSnapshotBead[]; -} - -export function buildRunDisplayNode( - group: RunNodeGroup, - controlBadges: RunControlBadge[], - latestLoopIteration: number | undefined, - sessionContext: RunSessionLinkContext = {}, -): RunDisplayNode { - const instances = group.beads - .map((bead, index) => buildExecutionInstance(group.semanticNodeId, bead, index, sessionContext)) - .sort(compareExecutionInstances); - const visibleInstance = preferredExecutionInstance(instances); - const iterations = new Set( - instances.map((instance) => iterationValue(instance.iteration)).filter(isNumber), - ); - const visibleIteration = - (visibleInstance ? iterationValue(visibleInstance.iteration) : undefined) ?? - (iterations.size > 0 ? Math.max(...iterations) : undefined); - const historicalOnly = - group.loopControlNodeId !== undefined && - visibleIteration !== undefined && - latestLoopIteration !== undefined && - visibleIteration < latestLoopIteration; - - for (const instance of instances) { - const currentIteration = - !historicalOnly && - (visibleIteration === undefined || iterationValue(instance.iteration) === visibleIteration); - instance.currentIteration = currentIteration; - instance.historical = !currentIteration; - instance.session = - instance.session.kind === 'attached' - ? { - ...instance.session, - streamable: currentIteration && isRunningStatus(instance.status), - } - : instance.session; - } - - if (visibleInstance === undefined) { - throw new Error(`run node ${group.semanticNodeId} has no execution instances`); - } - - const node: RunDisplayNode = { - id: group.semanticNodeId, - semanticNodeId: group.semanticNodeId, - title: group.title, - kind: group.kind, - constructKind: group.constructKind, - status: aggregateStatus(instances, visibleInstance), - currentBeadId: visibleInstance.beadId, - scope: runNodeScope(group.scopeRef), - visibleInGraph: !historicalOnly, - historicalOnly, - iterationSummary: iterationSummaryFor( - visibleIteration, - iterations.size, - group.loopControlNodeId, - ), - attemptSummary: attemptSummaryFor(instances, group.beads), - visibleExecutionInstanceId: visibleInstance.id, - executionInstances: instances, - controlBadges, - }; - return node; -} - -export function latestIterationsByLoop(groups: RunNodeGroup[]): Map { - const latest = new Map(); - for (const group of groups) { - if (!group.loopControlNodeId) continue; - for (const bead of group.beads) { - const iteration = iterationFor(bead); - if (!isNumber(iteration)) continue; - const current = latest.get(group.loopControlNodeId); - if (current === undefined || iteration > current) { - latest.set(group.loopControlNodeId, iteration); - } - } - } - return latest; -} - -function buildExecutionInstance( - semanticNodeId: string, - bead: RunSnapshotBead, - index: number, - sessionContext: RunSessionLinkContext, -): RunExecutionInstance { - const beadId = nonEmpty(bead.id); - if (beadId === undefined) { - throw new Error(`run node ${semanticNodeId} has a bead with an empty id`); - } - const iteration = iterationFor(bead); - const attempt = attemptFor(bead); - const status = presentationStatus(bead); - const sessionLink = runSessionLinkFor(bead, status, sessionContext); - const instance: RunExecutionInstance = { - id: beadId || `${semanticNodeId}:iteration-${iteration ?? 0}:attempt-${attempt ?? index}`, - semanticNodeId, - beadId, - iteration: iterationState(iteration), - attempt: attemptState(attempt), - label: instanceLabel(iteration, attempt), - status, - session: sessionState(status, sessionLink), - currentIteration: true, - historical: false, - }; - return instance; -} - -function preferredExecutionInstance( - instances: RunExecutionInstance[], -): RunExecutionInstance | undefined { - return [...instances].sort(compareExecutionInstances).at(-1); -} - -function compareExecutionInstances( - left: RunExecutionInstance, - right: RunExecutionInstance, -): number { - return ( - iterationOrder(left.iteration) - iterationOrder(right.iteration) || - attemptOrder(left.attempt) - attemptOrder(right.attempt) || - left.beadId.localeCompare(right.beadId) - ); -} - -function attemptSummaryFor( - instances: RunExecutionInstance[], - beads: RunSnapshotBead[], -): RunAttemptSummary { - const attemptCount = attemptCountFor(instances); - const activeAttempt = activeAttemptFor(instances); - const badgeLabel = attemptBadgeFor(beads); - if (attemptCount === 0 && badgeLabel === undefined) return { kind: 'none' }; - return { - kind: 'tracked', - count: Math.max(attemptCount, 1), - badge: - badgeLabel === undefined ? { kind: 'count-only' } : { kind: 'bounded', label: badgeLabel }, - active: - activeAttempt === undefined ? { kind: 'idle' } : { kind: 'running', value: activeAttempt }, - }; -} - -function attemptBadgeFor(beads: RunSnapshotBead[]): string | undefined { - const max = beads - .map((bead) => positiveIntegerMeta(bead, 'gc.max_attempts')) - .find((value) => value !== undefined); - if (max === undefined) return undefined; - const attempts = new Set(beads.map(attemptFor).filter(isNumber)); - return `${Math.max(attempts.size, 1)}/${max}`; -} - -function attemptCountFor(instances: RunExecutionInstance[]): number { - const attempts = new Set( - instances.map((instance) => attemptValue(instance.attempt)).filter(isNumber), - ); - return attempts.size; -} - -function activeAttemptFor(instances: RunExecutionInstance[]): number | undefined { - const active = instances.find((instance) => isRunningStatus(instance.status)); - return active ? attemptValue(active.attempt) : undefined; -} - -function instanceLabel(iteration: number | undefined, attempt: number | undefined): string { - if (iteration !== undefined && attempt !== undefined) { - return `iteration ${iteration}, attempt ${attempt}`; - } - if (iteration !== undefined) return `iteration ${iteration}`; - if (attempt !== undefined) return `attempt ${attempt}`; - return 'base'; -} - -function runNodeScope(scopeRef: string | undefined): RunNodeScope { - return scopeRef === undefined ? { kind: 'run' } : { kind: 'scoped', ref: scopeRef }; -} - -function iterationSummaryFor( - visibleIteration: number | undefined, - iterationCount: number, - loopControlNodeId: string | undefined, -): RunIterationSummary { - if (visibleIteration === undefined || iterationCount === 0) return { kind: 'single' }; - return { - kind: 'stacked', - visibleIteration, - iterationCount, - control: - loopControlNodeId === undefined - ? { kind: 'unknown' } - : { kind: 'known', id: loopControlNodeId }, - }; -} - -function iterationState(value: number | undefined): RunIteration { - return value === undefined ? { kind: 'base' } : { kind: 'loop', value }; -} - -function attemptState(value: number | undefined): RunAttempt { - return value === undefined ? { kind: 'untracked' } : { kind: 'attempt', value }; -} - -function sessionState( - status: RunExecutionInstance['status'], - link: ReturnType, -): RunSessionAttachment { - if (link !== undefined) { - return { kind: 'attached', link, streamable: false }; - } - return { - kind: 'none', - reason: status === 'pending' || status === 'ready' ? 'not_started' : 'session_unresolved', - }; -} - -function iterationValue(iteration: RunIteration): number | undefined { - return iteration.kind === 'loop' ? iteration.value : undefined; -} - -function attemptValue(attempt: RunAttempt): number | undefined { - return attempt.kind === 'attempt' ? attempt.value : undefined; -} - -function iterationOrder(iteration: RunIteration): number { - return iterationValue(iteration) ?? 0; -} - -function attemptOrder(attempt: RunAttempt): number { - return attemptValue(attempt) ?? 0; -} - -function isNumber(value: number | undefined): value is number { - return typeof value === 'number' && Number.isFinite(value); -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/execution-path.ts b/internal/api/dashboardspa/web/shared/src/runs/execution-path.ts deleted file mode 100644 index a30ff57d04..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/execution-path.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; -import type { RunExecutionPath } from '../run-detail.js'; -import { meta, nonEmpty } from './bead-fields.js'; - -export function resolveRunExecutionPath( - root: RunSnapshotBead | undefined, - beads: RunSnapshotBead[], - rigRoot?: string, -): RunExecutionPath { - const candidates = [ - ...executionWorkDirs(root), - ...beads.flatMap((bead) => executionWorkDirs(bead)), - ...rigRoots(root), - ...beads.flatMap((bead) => rigRoots(bead)), - nonEmpty(rigRoot), - ]; - const path = candidates.find((candidate) => candidate !== undefined); - return path === undefined - ? { kind: 'unavailable', reason: 'missing_cwd_and_rig_root' } - : { kind: 'known', path }; -} - -function executionWorkDirs(bead: RunSnapshotBead | undefined): Array { - return [ - meta(bead, 'gc.cwd'), - meta(bead, 'cwd'), - meta(bead, 'gc.work_dir'), - meta(bead, 'work_dir'), - ]; -} - -function rigRoots(bead: RunSnapshotBead | undefined): Array { - return [meta(bead, 'gc.rig_root'), meta(bead, 'rig_root')]; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/formula-name.ts b/internal/api/dashboardspa/web/shared/src/runs/formula-name.ts deleted file mode 100644 index 5e10d0dfeb..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/formula-name.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type { FormulaDetail, RunSnapshotBead } from '../run-snapshot.js'; -import type { RunFormulaSource } from '../run-detail.js'; -import { meta, nonEmpty } from './bead-fields.js'; - -export type RunFormulaIdentityMode = 'detail' | 'lane' | 'route' | 'state'; -export type RunFormulaIdentitySource = RunFormulaSource | 'formula_detail'; - -interface RunFormulaRootLike { - title: string; - status: string; - assignee?: string; - metadata?: Record; -} - -export interface ResolvedRunFormulaIdentity { - name: string | null; - source: RunFormulaIdentitySource | null; - target: string | null; -} - -export interface ResolveRunFormulaIdentityInput { - root?: RunFormulaRootLike | undefined; - formulaDetail?: Pick | undefined; - issues?: readonly RunFormulaRootLike[]; -} - -/** - * Resolved workflow formula name plus the provenance of that name. - * - * `source` carries which resolution path produced `name`: - * - `'metadata'` — the workflow root carried an explicit `gc.formula` key. - * - `'title_fallback'` — gc.formula was absent and the resolver derived - * the name from the bead title under the graph.v2 + `gc.run_target` - * gate. The dashboard surfaces this provenance to the operator in a - * warn tone instead of letting it pass as canonical metadata. See - * gascity-dashboard-e7hj for the precedent. - */ -export interface ResolvedRunFormulaName { - name: string; - source: RunFormulaSource; -} - -/** - * Resolve a workflow root bead to its formula name and the provenance of - * that name. - * - * gascity-dashboard-sadp: the live supervisor does NOT set `gc.formula` on - * graph.v2 workflow roots — the formula name lives in the bead title by - * convention (verified against live city data: title equals the - * registered formula name for every observed graph.v2 root, including - * `mol-focus-review`, `mol-dashboard-graphv2-smoke`). Without this - * fallback, both the formula-detail fetch in routes/runs.ts and the - * presentation-enrichment in formula-run.ts would treat graph.v2 lanes as - * missing a formula, collapsing every graph.v2 run-detail page to an - * empty `formula_detail_unavailable` state. - * - * The gate on `gc.run_target` is what keeps the fallback honest: only - * fully-instantiated runnable roots set both `gc.formula_contract` and - * `gc.run_target`. Operator-edited descriptive titles on closed roots - * without a target won't be mis-surfaced as formula names. - * - * gascity-dashboard-xfb7 (sadp follow-up): terminal graph.v2 roots are - * additionally excluded from the title fallback even when they retain - * `gc.run_target`. After a run completes operators sometimes retitle the - * root to a descriptive summary (e.g. 'investigation: foo bug'); a terminal - * run cannot be re-fetched against the supervisor's formula registry to - * refute a bad name, so the safer behavior is to defer — return null and - * let the consumer render 'unavailable' rather than a false attribution. - * Operators can override by setting `gc.formula` in metadata; the metadata - * path remains canonical regardless of run state. - * - * Returns `{name, source}`, or `null` if neither the explicit key nor the - * gated title fallback yields one. Callers may layer additional fallbacks - * (e.g. `formulaDetail?.name` for the rare case where the detail fetch - * succeeds despite missing root metadata) — those fallback paths attach - * their own `source` value. - * - * Returning a single object (rather than `name()` + `source()`) keeps the - * call atomic so a caller cannot accidentally surface a name from one - * resolution path with the source label of another. - */ -export function resolveRunFormulaName( - root: RunSnapshotBead | undefined, -): ResolvedRunFormulaName | null { - if (!root) return null; - const explicit = meta(root, 'gc.formula'); - if (explicit !== undefined) return { name: explicit, source: 'metadata' }; - if ( - meta(root, 'gc.formula_contract') === 'graph.v2' && - meta(root, 'gc.run_target') !== undefined && - !isTerminalRunRootStatus(root.status) - ) { - const title = root.title.trim(); - if (title.length > 0) return { name: title, source: 'title_fallback' }; - } - return null; -} - -export function resolveRunFormulaIdentity( - mode: RunFormulaIdentityMode, - { root, formulaDetail, issues = [] }: ResolveRunFormulaIdentityInput, -): ResolvedRunFormulaIdentity { - const target = runFormulaTarget(root); - const metadata = runFormulaMetadataName(mode, root, issues); - if (metadata !== null) return { name: metadata, source: 'metadata', target }; - - if (mode === 'detail' || mode === 'state') { - const detailName = nonEmpty(formulaDetail?.name); - if (detailName !== undefined) { - return { name: detailName, source: 'formula_detail', target }; - } - } - - const title = runFormulaTitleFallback(mode, root); - if (title !== null) return { name: title, source: 'title_fallback', target }; - - return { name: null, source: null, target }; -} - -function runFormulaMetadataName( - mode: RunFormulaIdentityMode, - root: RunFormulaRootLike | undefined, - issues: readonly RunFormulaRootLike[], -): string | null { - if (mode === 'lane') { - return ( - metadataString(issues, 'pr_review.workflow_formula') ?? - metadataString(issues, 'gc.formula') ?? - null - ); - } - return rootMeta(root, 'gc.formula') ?? rootMeta(root, 'gc.formula_name') ?? null; -} - -function runFormulaTitleFallback( - mode: RunFormulaIdentityMode, - root: RunFormulaRootLike | undefined, -): string | null { - if (root === undefined) return null; - if ( - rootMeta(root, 'gc.formula_contract') !== 'graph.v2' || - rootMeta(root, 'gc.run_target') === undefined || - isTerminalRunRootStatus(root.status) - ) { - return null; - } - const title = nonEmpty(root.title); - if (title === undefined) return null; - return mode === 'lane' && !title.startsWith('mol-') ? null : title; -} - -function isTerminalRunRootStatus(status: string): boolean { - switch (status.trim().toLowerCase()) { - case 'closed': - case 'completed': - case 'done': - case 'failed': - case 'skipped': - return true; - default: - return false; - } -} - -function runFormulaTarget(root: RunFormulaRootLike | undefined): string | null { - return ( - rootMeta(root, 'gc.run_target') ?? - rootMeta(root, 'gc.routed_to') ?? - nonEmpty(root?.assignee) ?? - null - ); -} - -function rootMeta(root: RunFormulaRootLike | undefined, key: string): string | undefined { - return nonEmpty(root?.metadata?.[key]); -} - -function metadataString(issues: readonly RunFormulaRootLike[], key: string): string | undefined { - for (const issue of issues) { - const value = rootMeta(issue, key); - if (value !== undefined) return value; - } - return undefined; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/formula-order.ts b/internal/api/dashboardspa/web/shared/src/runs/formula-order.ts deleted file mode 100644 index 262b4e38c9..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/formula-order.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { FormulaDetail, RunSnapshotBead } from '../run-snapshot.js'; -import { externalizeId, meta, nonEmpty, normalizedStepRef } from './bead-fields.js'; -import type { RunNodeGroup } from './execution-instances.js'; - -export function orderRunNodeGroups( - groups: readonly RunNodeGroup[], - formulaDetail: FormulaDetail | undefined, - rootBeadId: string, -): RunNodeGroup[] { - const rankByAlias = formulaRankByAlias(formulaDetail); - if (rankByAlias.size === 0) return [...groups]; - - return groups - .map((group, index) => ({ - group, - index, - rank: - group.semanticNodeId === rootBeadId - ? -1 - : rankForGroup(group, rankByAlias, formulaDetail?.name), - })) - .sort((left, right) => left.rank - right.rank || left.index - right.index) - .map((entry) => entry.group); -} - -function formulaRankByAlias(formulaDetail: FormulaDetail | undefined): Map { - const steps = formulaDetail?.preview?.nodes ?? formulaDetail?.steps ?? []; - const ranks = new Map(); - steps.forEach((step, index) => { - for (const alias of formulaStepAliases(step.id, formulaDetail?.name)) { - if (!ranks.has(alias)) ranks.set(alias, index); - } - }); - return ranks; -} - -function rankForGroup( - group: RunNodeGroup, - ranks: ReadonlyMap, - formulaName: string | undefined, -): number { - let rank = Number.POSITIVE_INFINITY; - for (const alias of groupAliases(group, formulaName)) { - const candidate = ranks.get(alias); - if (candidate !== undefined && candidate < rank) rank = candidate; - } - return rank; -} - -function groupAliases(group: RunNodeGroup, formulaName: string | undefined): string[] { - return [ - group.semanticNodeId, - ...group.beads.flatMap((bead) => beadAliases(bead, formulaName)), - ].flatMap((alias) => aliasVariants(alias)); -} - -function beadAliases(bead: RunSnapshotBead, formulaName: string | undefined): string[] { - return [ - nonEmpty(bead.id), - meta(bead, 'gc.logical_bead_id') ?? nonEmpty(bead.logical_bead_id), - meta(bead, 'gc.step_id'), - normalizedStepRef(bead), - ] - .filter((value): value is string => value !== undefined) - .flatMap((value) => aliasVariants(value, formulaName)); -} - -function formulaStepAliases(id: string, formulaName: string | undefined): string[] { - return aliasVariants(id, formulaName); -} - -function aliasVariants(value: string, formulaName?: string): string[] { - const clean = nonEmpty(value); - if (!clean) return []; - const stripped = stripFormulaPrefix(clean, formulaName); - return unique( - [clean, stripped, stripControlSuffix(clean), stripControlSuffix(stripped)].map((candidate) => - externalizeId(candidate), - ), - ); -} - -function stripFormulaPrefix(value: string, formulaName: string | undefined): string { - if (!formulaName) return value; - const prefix = `${formulaName}.`; - return value.startsWith(prefix) ? value.slice(prefix.length) : value; -} - -function stripControlSuffix(value: string): string { - return value.replace(/-scope-check$/, '').replace(/\.scope-check$/, ''); -} - -function unique(values: string[]): string[] { - return [...new Set(values)]; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/formula-run.ts b/internal/api/dashboardspa/web/shared/src/runs/formula-run.ts deleted file mode 100644 index 924dabf9f3..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/formula-run.ts +++ /dev/null @@ -1,289 +0,0 @@ -import type { FormulaDetail, RunSnapshotBead, RunSnapshot } from '../run-snapshot.js'; -import type { DashboardSession } from '../dashboard-sessions.js'; -import type { - RunControlBadge, - RunDisplayEdge, - RunDisplayLane, - RunDisplayNode, - RunExecutionPath, - RunFormula, - RunFormulaDetailState, - RunNodeStatus, - FormulaRunProgress, - RunScopeKind, - RunSnapshotSequence, -} from '../run-detail.js'; -import type { RunPhase, RunStage } from '../snapshot/types.js'; -import { mapRunPhase, stageProgress, type RunIssue } from './phaseMapping.js'; -import { meta } from './bead-fields.js'; -import { resolveRunFormulaIdentity } from './formula-name.js'; -import { applyDisplayNodeStates } from './display-state.js'; -import { buildRunDisplayEdges } from './edges.js'; -import { - buildRunDisplayNode, - latestIterationsByLoop, - type RunNodeGroup, -} from './execution-instances.js'; -import { resolveRunExecutionPath } from './execution-path.js'; -import { orderRunNodeGroups } from './formula-order.js'; -import { groupRunBeads } from './groups.js'; -import { buildRunDisplayLanes } from './lanes.js'; -import { - buildRunSessionIndex, - type RunSessionIndex, - type RunSessionLinkContext, -} from './session-link.js'; - -export interface RunningFormulaRunInput { - raw: RunSnapshot; - runId: string; - rootBeadId: string; - rootStoreRef: string; - resolvedRootStore: string; - scopeKind: RunScopeKind; - scopeRef: string; - root?: RunSnapshotBead; - beads: RunSnapshotBead[]; - rigRoot?: string; - sessions?: readonly DashboardSession[]; - formulaDetail?: FormulaDetail; - formulaDetailState?: RunFormulaDetailState; -} - -/** - * Backend-owned projection of a running graph.v2 formula. - * - * The React detail page should render this projection, not infer runtime - * state from raw run beads or the global sessions list. This is the - * single aggregation point for supervisor run shape, live bead state, - * session summaries, loop instances, and display graph state. - */ -export interface RunningFormulaRun { - raw: RunSnapshot; - runId: string; - rootBeadId: string; - rootStoreRef: string; - resolvedRootStore: string; - scopeKind: RunScopeKind; - scopeRef: string; - title: string; - formula: RunFormula; - formulaDetail: RunFormulaDetailState; - executionPath: RunExecutionPath; - root?: RunSnapshotBead; - beads: RunSnapshotBead[]; - nodeGroups: RunNodeGroup[]; - physicalToSemantic: Map; - badgesByTarget: Map; - latestIterationByLoop: Map; - sessionIndex: RunSessionIndex; - sessionContext: RunSessionLinkContext; - nodes: RunDisplayNode[]; - edges: RunDisplayEdge[]; - lanes: RunDisplayLane[]; - progress: FormulaRunProgress; - phase: RunPhase; - stages: RunStage[]; -} - -export function buildRunningFormulaRun(input: RunningFormulaRunInput): RunningFormulaRun { - const { - groups: unorderedGroups, - physicalToSemantic, - badgesByTarget, - } = groupRunBeads(input.beads, input.rootBeadId); - const groups = orderRunNodeGroups(unorderedGroups, input.formulaDetail, input.rootBeadId); - // Prefer supervisor-owned compiled formula order when available. If a run - // does not expose a formula name yet, preserve snapshot order rather than - // reading formula files locally. - const latestIterationByLoop = latestIterationsByLoop(groups); - const sessionIndex = buildRunSessionIndex(input.sessions ?? []); - const sessionContext = { - sessionIndex, - scopeRef: input.scopeRef, - }; - const rawNodes = groups.map((group) => - buildRunDisplayNode( - group, - badgesByTarget.get(group.semanticNodeId) ?? [], - latestIterationByLoop.get(group.loopControlNodeId ?? ''), - sessionContext, - ), - ); - const edges = buildRunDisplayEdges(input.raw, physicalToSemantic, rawNodes); - const nodes = applyDisplayNodeStates(rawNodes, edges); - const progress = buildFormulaRunProgress(input.raw, nodes, edges); - const formula = runFormulaState(input.root, input.formulaDetail); - const formulaDetail = - input.formulaDetailState ?? runFormulaDetailState(input.root, input.formulaDetail); - const executionPath = resolveRunExecutionPath(input.root, input.beads, input.rigRoot); - - // gascity-dashboard-ud6j: compute the dashboard phase ladder from this - // run's OWN beads through the SAME fromDashboardBead → mapRunPhase → stageProgress - // pipeline the snapshot lane uses, so the run-detail ladder cannot drift - // from the lane's. mapRunPhase keys off bead status + title, which run - // beads carry. The resolved formula name (when known) selects the - // formula-specific stage set; otherwise the generic 5-stage ladder applies. - const issues = input.beads.map(fromRunSnapshotBead); - const phaseMapping = mapRunPhase(issues); - const formulaName = formula.kind === 'known' ? formula.name : null; - const stages = stageProgress(phaseMapping, formulaName, issues); - - const run: RunningFormulaRun = { - raw: input.raw, - runId: input.runId, - rootBeadId: input.rootBeadId, - rootStoreRef: input.rootStoreRef, - resolvedRootStore: input.resolvedRootStore, - scopeKind: input.scopeKind, - scopeRef: input.scopeRef, - title: input.root?.title.trim() || input.runId, - formula, - formulaDetail, - executionPath, - beads: input.beads, - nodeGroups: groups, - physicalToSemantic, - badgesByTarget, - latestIterationByLoop, - sessionIndex, - sessionContext, - nodes, - edges, - lanes: buildRunDisplayLanes(nodes), - progress, - phase: phaseMapping.phase, - stages, - }; - if (input.root !== undefined) run.root = input.root; - return run; -} - -/** - * Adapt a supervisor run-snapshot bead to the phase classifier's - * RunIssue input. The phase pipeline's own fromDashboardBead adapter consumes the - * city-wide DashboardBead shape (issue_type, created_at); the run-snapshot wire row - * is a different shape (kind, no created_at). mapRunPhase only reads - * status / title / metadata / issue_type / parent, so this maps the run-bead - * `kind` onto `issue_type` and leaves updated_at empty (the snapshot carries - * no per-bead timestamp — latestStepId's ordering degrades gracefully to - * input order, which the phase classifier does not depend on). - */ -function fromRunSnapshotBead(bead: RunSnapshotBead): RunIssue { - const parent = meta(bead, 'gc.parent_bead_id'); - const issue: RunIssue = { - id: bead.id, - title: bead.title, - status: bead.status, - issue_type: bead.kind, - updated_at: '', - metadata: bead.metadata, - }; - if (bead.assignee !== undefined) issue.assignee = bead.assignee; - if (parent !== undefined) issue.parent = parent; - return issue; -} - -function runFormulaState( - root: RunSnapshotBead | undefined, - formulaDetail: FormulaDetail | undefined, -): RunFormula { - // Provenance precedence (gascity-dashboard-e7hj + sadp). The supervisor's - // canonical signals win over the graph.v2 bead-title heuristic; the title - // fallback only fires when none of them are present: - // 1. `gc.formula` / `gc.formula_name` metadata → source: 'metadata' - // 2. supervisor formula detail name → source: 'metadata' - // (canonical even when the root metadata key is absent) - // 3. graph.v2 title fallback (resolveRunFormulaIdentity) → 'title_fallback' - // The title fallback shares the mode-aware identity resolver with the - // route-side formula-detail fetch (routes/runs.ts) so both agree on which - // graph.v2 roots get a title-derived name. See gascity-dashboard-sadp. - const resolved = resolveRunFormulaIdentity('state', { root, formulaDetail }); - if (resolved.name !== null) { - const source = resolved.source === 'title_fallback' ? 'title_fallback' : 'metadata'; - return { - kind: 'known', - name: resolved.name, - source, - }; - } - return { - kind: 'unavailable', - reason: 'missing_formula_metadata', - }; -} - -function runFormulaDetailState( - root: RunSnapshotBead | undefined, - formulaDetail: FormulaDetail | undefined, -): RunFormulaDetailState { - const resolved = resolveRunFormulaIdentity('detail', { root, formulaDetail }); - const name = resolved.name; - if (name === null) return { kind: 'unavailable', reason: 'missing_formula_metadata' }; - const target = resolved.target; - if (!target) return { kind: 'unavailable', reason: 'missing_run_target', name }; - if (formulaDetail !== undefined) return { kind: 'available', name, target }; - return { - kind: 'unavailable', - reason: 'fetch_failed', - name, - target, - failure: 'upstream_error', - }; -} - -function buildFormulaRunProgress( - raw: RunSnapshot, - nodes: readonly RunDisplayNode[], - edges: readonly RunDisplayEdge[], -): FormulaRunProgress { - const visibleNodes = nodes.filter((node) => node.visibleInGraph); - const streamableSessionIds = new Set(); - let executionInstanceCount = 0; - let sessionLinkCount = 0; - let streamableSessionCount = 0; - - for (const node of nodes) { - for (const instance of node.executionInstances) { - executionInstanceCount += 1; - if (instance.session.kind === 'attached') { - sessionLinkCount += 1; - } - if (instance.session.kind === 'attached' && instance.session.streamable) { - streamableSessionCount += 1; - streamableSessionIds.add(instance.session.link.sessionId); - } - } - } - - return { - snapshotVersion: raw.snapshot_version, - snapshotEventSeq: runSnapshotSequence(raw.snapshot_event_seq), - snapshotPartial: raw.partial, - totalNodeCount: nodes.length, - visibleNodeCount: visibleNodes.length, - edgeCount: edges.length, - executionInstanceCount, - sessionLinkCount, - streamableSessionCount, - streamableSessionIds: [...streamableSessionIds], - statusCounts: countNodeStatuses(visibleNodes), - allStatusCounts: countNodeStatuses(nodes), - }; -} - -function runSnapshotSequence(raw: number | null | undefined): RunSnapshotSequence { - return typeof raw === 'number' - ? { kind: 'known', seq: raw } - : { kind: 'unavailable', reason: 'supervisor_omitted' }; -} - -function countNodeStatuses( - nodes: readonly RunDisplayNode[], -): Partial> { - const counts: Partial> = {}; - for (const node of nodes) { - counts[node.status] = (counts[node.status] ?? 0) + 1; - } - return counts; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/groups.ts b/internal/api/dashboardspa/web/shared/src/runs/groups.ts deleted file mode 100644 index 8b2857b825..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/groups.ts +++ /dev/null @@ -1,359 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; -import type { RunControlBadge } from '../run-detail.js'; -import { externalizeId, meta, nonEmpty, normalizedStepRef } from './bead-fields.js'; -import type { RunNodeGroup } from './execution-instances.js'; -import { - badgeLabelFor, - constructKindFor, - displayTitleFor, - externalKindFor, - hiddenBadgeTargetFor, - isHiddenConstruct, - loopControlNodeIdFor, - semanticNodeIdFor, -} from './node-shape.js'; -import { presentationStatus } from './status.js'; - -export interface RunBeadGroups { - groups: RunNodeGroup[]; - physicalToSemantic: Map; - badgesByTarget: Map; -} - -interface BeadIdentity { - base: string; - disambiguator: string | undefined; - semanticNodeId: string; -} - -export function groupRunBeads(beads: RunSnapshotBead[], rootBeadId: string): RunBeadGroups { - const groupedBeads = new Map(); - const physicalToSemantic = new Map(); - const badgesByTarget = new Map(); - const physicalLogicalTargets = referencedPhysicalLogicalTargets(beads); - const identities = resolveBeadIdentities(beads, rootBeadId, physicalLogicalTargets); - const badgeTargetAliases = buildBadgeTargetAliases( - beads, - rootBeadId, - identities, - physicalLogicalTargets, - ); - - for (const bead of beads) { - const beadId = nonEmpty(bead.id) ?? ''; - const constructKind = constructKindFor(bead, rootBeadId); - const semanticNodeId = - identities.get(bead)?.semanticNodeId ?? semanticNodeIdFor(bead, rootBeadId); - physicalToSemantic.set(beadId, semanticNodeId); - - if (isHiddenConstruct(constructKind)) { - const target = hiddenBadgeTargetFor(bead, rootBeadId); - const resolvedTarget = resolveBadgeTarget(bead, rootBeadId, badgeTargetAliases, target); - if (resolvedTarget) { - const badges = badgesByTarget.get(resolvedTarget) ?? []; - badges.push({ - id: beadId || `${resolvedTarget}-${constructKind}`, - label: badgeLabelFor(constructKind), - status: presentationStatus(bead), - }); - badgesByTarget.set(resolvedTarget, badges); - } - continue; - } - - const group = groupedBeads.get(semanticNodeId) ?? []; - group.push(bead); - groupedBeads.set(semanticNodeId, group); - } - - return { - groups: [...groupedBeads].map(([semanticNodeId, groupBeads]) => - buildRunNodeGroup(semanticNodeId, groupBeads, rootBeadId), - ), - physicalToSemantic, - badgesByTarget, - }; -} - -function buildRunNodeGroup( - semanticNodeId: string, - beads: RunSnapshotBead[], - rootBeadId: string, -): RunNodeGroup { - const shapeBead = preferredShapeBead(beads, rootBeadId); - const constructKind = constructKindFor(shapeBead, rootBeadId); - const scopeRef = groupOptional( - beads, - shapeBead, - (bead) => meta(bead, 'gc.scope_ref') ?? nonEmpty(bead.scope_ref), - ); - const loopControlNodeId = groupOptional(beads, shapeBead, loopControlNodeIdFor); - - return { - semanticNodeId, - title: displayTitleFor(shapeBead, semanticNodeId), - kind: externalKindFor(shapeBead, constructKind), - constructKind, - beads, - ...(scopeRef !== undefined ? { scopeRef } : {}), - ...(loopControlNodeId !== undefined ? { loopControlNodeId } : {}), - }; -} - -function preferredShapeBead( - beads: readonly RunSnapshotBead[], - rootBeadId: string, -): RunSnapshotBead { - const [first] = [...beads].sort((left, right) => { - const priorityDiff = - constructPriority(constructKindFor(right, rootBeadId)) - - constructPriority(constructKindFor(left, rootBeadId)); - if (priorityDiff !== 0) return priorityDiff; - return beadSortKey(left).localeCompare(beadSortKey(right)); - }); - if (!first) throw new Error('cannot build run node group from zero beads'); - return first; -} - -function groupOptional( - beads: readonly RunSnapshotBead[], - shapeBead: RunSnapshotBead, - resolve: (bead: RunSnapshotBead) => string | undefined, -): string | undefined { - return resolve(shapeBead) ?? sortedBeads(beads).map(resolve).find(isDefined); -} - -function constructPriority(kind: RunNodeGroup['constructKind']): number { - switch (kind) { - case 'run-root': - return 100; - case 'check-loop': - return 90; - case 'retry': - return 80; - case 'condition': - case 'fanout': - case 'scope': - case 'expansion': - return 70; - case 'step': - return 10; - case 'control': - case 'run-finalize': - case 'scope-check': - case 'spec': - case 'unknown': - return 0; - } -} - -function sortedBeads(beads: readonly RunSnapshotBead[]): RunSnapshotBead[] { - return [...beads].sort((left, right) => beadSortKey(left).localeCompare(beadSortKey(right))); -} - -function beadSortKey(bead: RunSnapshotBead): string { - return [nonEmpty(bead.id), normalizedStepRef(bead), nonEmpty(bead.title)] - .filter(isDefined) - .join('\u0000'); -} - -function resolveBeadIdentities( - beads: readonly RunSnapshotBead[], - rootBeadId: string, - physicalLogicalTargets: ReadonlySet, -): Map { - const partialIdentities = new Map>(); - const identitiesByBase = new Map>(); - - for (const bead of beads) { - const constructKind = constructKindFor(bead, rootBeadId); - if (isHiddenConstruct(constructKind)) continue; - const base = groupingBaseSemanticId(bead, rootBeadId, physicalLogicalTargets); - const disambiguator = duplicateResolutionIdentity( - bead, - rootBeadId, - base, - physicalLogicalTargets, - ); - partialIdentities.set(bead, { base, disambiguator }); - const identity = disambiguator ?? base; - const identities = identitiesByBase.get(base) ?? new Set(); - identities.add(identity); - identitiesByBase.set(base, identities); - } - - const resolved = new Map(); - for (const bead of beads) { - const partial = partialIdentities.get(bead) ?? { - base: semanticNodeIdFor(bead, rootBeadId), - disambiguator: undefined, - }; - const identities = identitiesByBase.get(partial.base); - const semanticNodeId = - identities && identities.size > 1 && partial.disambiguator - ? partial.disambiguator - : partial.base; - resolved.set(bead, { ...partial, semanticNodeId }); - } - return resolved; -} - -function groupingBaseSemanticId( - bead: RunSnapshotBead, - rootBeadId: string, - physicalLogicalTargets: ReadonlySet, -): string { - const beadId = nonEmpty(bead.id); - if (beadId && beadId === rootBeadId) return rootBeadId; - const explicit = meta(bead, 'gc.logical_bead_id') ?? nonEmpty(bead.logical_bead_id); - if (explicit) return externalizeId(explicit); - const constructKind = constructKindFor(bead, rootBeadId); - if ( - (constructKind === 'check-loop' || constructKind === 'retry') && - beadId && - physicalLogicalTargets.has(beadId) - ) { - return externalizeId(beadId); - } - return semanticNodeIdFor(bead, rootBeadId); -} - -function duplicateResolutionIdentity( - bead: RunSnapshotBead, - rootBeadId: string, - base: string, - physicalLogicalTargets: ReadonlySet, -): string | undefined { - const beadId = nonEmpty(bead.id); - if (beadId && physicalLogicalTargets.has(beadId) && externalizeId(beadId) === base) { - return base; - } - return stableSemanticIdentity(bead, rootBeadId); -} - -function buildBadgeTargetAliases( - beads: readonly RunSnapshotBead[], - rootBeadId: string, - identities: Map, - physicalLogicalTargets: ReadonlySet, -): Map { - const candidates = new Map>(); - - for (const bead of beads) { - const constructKind = constructKindFor(bead, rootBeadId); - if (isHiddenConstruct(constructKind)) continue; - const identity = identities.get(bead); - const resolved = identity?.semanticNodeId ?? semanticNodeIdFor(bead, rootBeadId); - for (const alias of visibleNodeAliases( - bead, - rootBeadId, - resolved, - identity, - physicalLogicalTargets, - )) { - const existing = candidates.get(alias) ?? new Set(); - existing.add(resolved); - candidates.set(alias, existing); - } - } - - const aliases = new Map(); - for (const [alias, targets] of candidates) { - if (targets.size === 1) { - const [target] = [...targets]; - if (target) aliases.set(alias, target); - } - } - return aliases; -} - -function visibleNodeAliases( - bead: RunSnapshotBead, - rootBeadId: string, - resolved: string, - identity: BeadIdentity | undefined, - physicalLogicalTargets: ReadonlySet, -): string[] { - return [ - resolved, - semanticNodeIdFor(bead, rootBeadId), - identity?.base ?? groupingBaseSemanticId(bead, rootBeadId, physicalLogicalTargets), - identity?.disambiguator, - stableSemanticIdentity(bead, rootBeadId), - meta(bead, 'gc.step_id'), - fullStepRefIdentity(normalizedStepRef(bead)), - nonEmpty(bead.id), - ] - .filter((value): value is string => value !== undefined) - .map((value) => externalizeId(value)); -} - -function isDefined(value: T | undefined): value is T { - return value !== undefined; -} - -function referencedPhysicalLogicalTargets(beads: readonly RunSnapshotBead[]): Set { - const beadIds = new Set( - beads.map((bead) => nonEmpty(bead.id)).filter((value): value is string => value !== undefined), - ); - const targets = new Set(); - for (const bead of beads) { - const logical = meta(bead, 'gc.logical_bead_id') ?? nonEmpty(bead.logical_bead_id); - if (logical && beadIds.has(logical)) targets.add(logical); - } - return targets; -} - -function resolveBadgeTarget( - bead: RunSnapshotBead, - rootBeadId: string, - aliases: Map, - fallback: string | null, -): string | null { - if (constructKindFor(bead, rootBeadId) === 'run-finalize') return rootBeadId; - for (const candidate of hiddenBadgeTargetCandidates(bead, fallback)) { - const target = aliases.get(candidate); - if (target) return target; - } - return fallback; -} - -function hiddenBadgeTargetCandidates(bead: RunSnapshotBead, fallback: string | null): string[] { - return [meta(bead, 'gc.control_for'), hiddenBadgeFullTargetFor(bead), fallback ?? undefined] - .filter((value): value is string => value !== undefined) - .flatMap((value) => { - const stripped = stripControlSuffix(value); - return [value, stripped, externalizeId(stripped)]; - }) - .map((value) => externalizeId(value)); -} - -function stableSemanticIdentity(bead: RunSnapshotBead, rootBeadId: string): string | undefined { - const beadId = nonEmpty(bead.id); - if (beadId && beadId === rootBeadId) return rootBeadId; - const explicit = meta(bead, 'gc.logical_bead_id') ?? nonEmpty(bead.logical_bead_id); - if (explicit) return externalizeId(explicit); - const stepId = meta(bead, 'gc.step_id'); - if (stepId) return externalizeId(stepId); - return fullStepRefIdentity(normalizedStepRef(bead)); -} - -function hiddenBadgeFullTargetFor(bead: RunSnapshotBead): string | undefined { - const controlFor = meta(bead, 'gc.control_for'); - if (controlFor) return externalizeId(stripControlSuffix(controlFor)); - return fullStepRefIdentity(stripControlSuffix(normalizedStepRef(bead) ?? '')); -} - -function fullStepRefIdentity(ref: string | null | undefined): string | undefined { - const clean = nonEmpty(ref); - if (!clean) return undefined; - const stripped = stripControlSuffix(clean); - const parts = stripped.split('.').filter(Boolean); - if (parts.length === 0) return undefined; - if (parts.length === 1) return externalizeId(parts[0] ?? stripped); - return externalizeId(parts.slice(1).join('.')); -} - -function stripControlSuffix(ref: string): string { - return ref.replace(/-scope-check$/, '').replace(/\.scope-check$/, ''); -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/health.test.ts b/internal/api/dashboardspa/web/shared/src/runs/health.test.ts deleted file mode 100644 index 76737bea01..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/health.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { test, describe } from 'node:test'; -import assert from 'node:assert/strict'; - -import { deriveRunHealth } from './health.js'; -import type { RunLane } from '../snapshot/types.js'; - -// gascity-dashboard (0gww): the run-lane health-unavailable attention emitter -// guards on `lane.health.status === 'available'`. deriveRunHealth used to wrap -// EVERY lane in status:'available' — even when the session list was unavailable -// and health could not actually be derived — so the guard always continued and -// the emitter was structurally dead. These tests pin the contract that drives -// the emitter: no session list ⇒ health.status === 'unavailable'. - -function lane(overrides: Partial = {}): RunLane { - return { - id: 'run-1', - title: 'a run', - formula: { status: 'known', name: 'mol-test' }, - scope: { status: 'available', kind: 'rig', ref: 'app', rootStoreRef: 'rig:app' }, - external: { status: 'unavailable', error: 'external reference unavailable' }, - phase: 'implementation', - phaseLabel: 'implementation', - statusCounts: { in_progress: 1 }, - activeAssignees: ['app/codex'], - updatedAt: { status: 'available', at: '2026-06-08T00:00:00.000Z' }, - stages: [], - progress: { status: 'unavailable', error: 'run progress unavailable' }, - formulaStageResolved: false, - health: { status: 'unavailable', error: 'run health has not been derived' }, - ...overrides, - }; -} - -describe('deriveRunHealth — session-list unavailability (0gww)', () => { - test('reports health.status unavailable for every lane when the session list is unavailable', () => { - const { lanes } = deriveRunHealth({ - lanes: [lane({ id: 'run-a' }), lane({ id: 'run-b' })], - sessions: [], - sessionsAvailable: false, - marks: new Map(), - }); - - for (const enriched of lanes) { - assert.equal(enriched.health.status, 'unavailable'); - if (enriched.health.status !== 'unavailable') continue; - assert.equal(enriched.health.error, 'run session list unavailable'); - } - }); - - test('still derives available health when the session list is available', () => { - const { lanes } = deriveRunHealth({ - lanes: [lane()], - sessions: [], - sessionsAvailable: true, - marks: new Map(), - }); - - assert.equal(lanes[0]?.health.status, 'available'); - }); -}); diff --git a/internal/api/dashboardspa/web/shared/src/runs/health.ts b/internal/api/dashboardspa/web/shared/src/runs/health.ts index f7de20a327..32f777abd0 100644 --- a/internal/api/dashboardspa/web/shared/src/runs/health.ts +++ b/internal/api/dashboardspa/web/shared/src/runs/health.ts @@ -1,49 +1,10 @@ -import { resolveSessionForTarget } from '../session-resolve.js'; -import type { DashboardSession } from '../dashboard-sessions.js'; -import type { RunCensus, RunLane, RunLaneHealth, RunPhase } from '../snapshot/types.js'; +import type { RunLane } from '../snapshot/types.js'; -const DEFAULT_ATTEMPT_CLIMB_MIN = 1; -const DEFAULT_THRASH_DETECTED_STREAK = 2; - -export interface HealthThresholds { - attemptClimbMin: number; - thrashDetectedStreak: number; -} - -const DEFAULT_THRESHOLDS: HealthThresholds = { - attemptClimbMin: DEFAULT_ATTEMPT_CLIMB_MIN, - thrashDetectedStreak: DEFAULT_THRASH_DETECTED_STREAK, -}; - -export interface LaneProgressMark { - progress: LaneProgressComparison; - thrashStreak: number; -} - -export type LaneProgressComparison = - | { - status: 'comparable'; - stepId: string; - stageIndex: number; - attempt: number; - } - | { - status: 'not_comparable'; - error: string; - }; - -export interface DeriveRunHealthInput { - lanes: readonly RunLane[]; - sessions: readonly DashboardSession[]; - sessionsAvailable: boolean; - marks: ReadonlyMap; - thresholds?: Partial; -} - -export interface DeriveRunHealthResult { - lanes: RunLane[]; - census: RunCensus; -} +// The run-health derivation (deriveRunHealth / buildCensus / advanceProgressMarks) +// moved to Go (internal/runproj); the dashboard reads health and census off the +// server-computed RunSummary DTO. This pure accessor is the one piece that stays +// client-side, because the attention layer (AmbientHome, StatusSentence) reads +// it during a session-list outage when per-lane health degrades to unavailable. /** * Structural needs-operator signal for a lane: true when the lane's phase is a @@ -58,167 +19,3 @@ export interface DeriveRunHealthResult { export function laneNeedsOperator(lane: RunLane): boolean { return lane.phase === 'approval' || lane.phase === 'blocked'; } - -export function advanceProgressMarks( - previous: ReadonlyMap, - lanes: readonly RunLane[], - thresholds: Partial = {}, -): Map { - const { attemptClimbMin } = { ...DEFAULT_THRESHOLDS, ...thresholds }; - const next = new Map(); - - for (const lane of lanes) { - const progress = comparableProgress(lane); - const prior = previous.get(lane.id); - - const positionFlat = - prior !== undefined && - prior.progress.status === 'comparable' && - progress.status === 'comparable' && - prior.progress.stepId === progress.stepId && - prior.progress.stageIndex === progress.stageIndex; - const climbed = - prior !== undefined && - prior.progress.status === 'comparable' && - progress.status === 'comparable' && - progress.attempt - prior.progress.attempt >= attemptClimbMin; - - const thrashStreak = positionFlat && climbed ? prior.thrashStreak + 1 : 0; - - next.set(lane.id, { - progress, - thrashStreak, - }); - } - - return next; -} - -export function deriveRunHealth(input: DeriveRunHealthInput): DeriveRunHealthResult { - const { thrashDetectedStreak } = { ...DEFAULT_THRESHOLDS, ...input.thresholds }; - - const lanes = input.lanes.map((lane): RunLane => { - // gascity-dashboard (0gww): without the session list, health cannot be - // derived — phaseConfidence collapses to 'inferred' and the session-trust - // signals (needsOperator/thrash) lose their grounding. Report the lane's - // health as genuinely 'unavailable' rather than wrapping a degraded shell in - // status:'available'. That degraded-but-'available' shell is what made the - // attention emitter's `health.status === 'available'` guard - // (attention/registry.ts) skip every lane, so the per-lane - // health-unavailable signal never fired. Consumers already gate every - // .health.data read on status === 'available', so they degrade cleanly here. - if (!input.sessionsAvailable) { - return { ...lane, health: { status: 'unavailable', error: 'run session list unavailable' } }; - } - - const session = resolveLaneSession(lane, input.sessions); - const sessionResolved = session.status === 'resolved'; - - const phaseConfidence: RunLaneHealth['phaseConfidence'] = - lane.formulaStageResolved === true && sessionResolved ? 'known' : 'inferred'; - - const thrashStreak = input.marks.get(lane.id)?.thrashStreak ?? 0; - - const health: RunLaneHealth = { - phaseConfidence, - needsOperator: laneNeedsOperator(lane), - stuckNode: stuckNode(lane), - thrashingDetected: thrashStreak >= thrashDetectedStreak, - session: - session.status === 'resolved' - ? sessionFacts(session.session) - : { status: 'unresolved', error: session.error }, - }; - - return { ...lane, health: { status: 'available' as const, data: health } }; - }); - - return { lanes, census: buildCensus(lanes) }; -} - -function resolveLaneSession( - lane: RunLane, - sessions: readonly DashboardSession[], -): { status: 'resolved'; session: DashboardSession } | { status: 'unresolved'; error: string } { - for (const assignee of lane.activeAssignees) { - const session = resolveSessionForTarget(assignee, sessions); - if (session !== null) return { status: 'resolved', session }; - } - return { status: 'unresolved', error: 'run session unresolved' }; -} - -function comparableProgress(lane: RunLane): LaneProgressComparison { - if (lane.progress.status !== 'active_step') { - return { status: 'not_comparable', error: 'run has no active step' }; - } - if (lane.progress.stage.status !== 'available') { - return { status: 'not_comparable', error: lane.progress.stage.error }; - } - if (lane.progress.attempt.status !== 'available') { - return { status: 'not_comparable', error: lane.progress.attempt.error }; - } - return { - status: 'comparable', - stepId: lane.progress.stepId, - stageIndex: lane.progress.stage.index, - attempt: lane.progress.attempt.value, - }; -} - -function stuckNode(lane: RunLane): RunLaneHealth['stuckNode'] { - return lane.progress.status === 'active_step' - ? { status: 'available', id: lane.progress.stepId } - : { status: 'unavailable', error: 'active run step unavailable' }; -} - -function sessionFacts(session: DashboardSession): RunLaneHealth['session'] { - return { - status: 'resolved', - lastActive: - session.last_active === undefined - ? { status: 'unavailable', error: 'session last_active unavailable' } - : { status: 'available', at: session.last_active }, - running: { status: 'available', value: session.running }, - activity: - session.activity === undefined - ? { status: 'unavailable', error: 'session activity unavailable' } - : { status: 'available', value: session.activity }, - }; -} - -function zeroByPhase(): Record { - return { - intake: 0, - implementation: 0, - review: 0, - approval: 0, - finalization: 0, - blocked: 0, - complete: 0, - active: 0, - }; -} - -export function buildCensus(lanes: readonly RunLane[]): RunCensus { - const byPhase = zeroByPhase(); - - let totalInFlight = 0; - let unverifiable = 0; - let knownDenominator = 0; - let thrashing = 0; - - for (const lane of lanes) { - byPhase[lane.phase] += 1; - if (lane.phase === 'complete') continue; - - totalInFlight += 1; - if (lane.health.status === 'available' && lane.health.data.phaseConfidence === 'known') { - knownDenominator += 1; - if (lane.health.data.thrashingDetected === true) thrashing += 1; - } else { - unverifiable += 1; - } - } - - return { byPhase, totalInFlight, unverifiable, knownDenominator, thrashing }; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/lanes.ts b/internal/api/dashboardspa/web/shared/src/runs/lanes.ts deleted file mode 100644 index f72121d514..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/lanes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RunDisplayLane, RunDisplayNode } from '../run-detail.js'; - -const RUN_SCOPE = '__run'; - -export function buildRunDisplayLanes(nodes: RunDisplayNode[]): RunDisplayLane[] { - const byScope = new Map(); - for (const node of nodes) { - const scope = node.scope.kind === 'scoped' ? node.scope.ref : RUN_SCOPE; - const existing = byScope.get(scope) ?? { - id: scope, - label: scope === RUN_SCOPE ? 'Run' : scope, - nodeIds: [], - }; - existing.nodeIds.push(node.id); - byScope.set(scope, existing); - } - return [...byScope.values()]; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/liveness.test.ts b/internal/api/dashboardspa/web/shared/src/runs/liveness.test.ts deleted file mode 100644 index 8391114b16..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/liveness.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { test, describe } from 'node:test'; -import assert from 'node:assert/strict'; - -import { isDanglingRootGroup, isStaleSessionlessLatch, STALE_LATCH_AFTER_MS } from './liveness.js'; -import type { RunIssue } from './phaseMapping.js'; -import type { RunLane, RunLaneHealth } from '../snapshot/types.js'; - -// gascity-dashboard-s4rp: the sharp session-less demotion predicate. Operator -// repro gc-1920 — an ancient approval-gate latch with no live session, no -// in_progress step, ~4d stale — inflated Active:1 and flickered in/out. It must -// be demoted, while a freshly-queued run and an approval gate genuinely waiting -// on a human (both legitimately session-less) must NOT be. - -const NOW_MS = Date.parse('2026-06-07T00:00:00.000Z'); - -function health(sessionStatus: 'resolved' | 'unresolved'): RunLane['health'] { - const session: RunLaneHealth['session'] = - sessionStatus === 'resolved' - ? { - status: 'resolved', - lastActive: { status: 'available', at: new Date(NOW_MS).toISOString() }, - running: { status: 'available', value: true }, - activity: { status: 'available', value: 'working' }, - } - : { status: 'unresolved', error: 'run session unresolved' }; - - return { - status: 'available', - data: { - phaseConfidence: 'inferred', - needsOperator: false, - stuckNode: { status: 'unavailable', error: 'active run step unavailable' }, - thrashingDetected: false, - session, - }, - }; -} - -function lane(overrides: Partial = {}): RunLane { - return { - id: 'gc-1920', - title: 'mol-focus-review', - formula: { status: 'known', name: 'mol-focus-review' }, - scope: { status: 'unavailable', error: 'run scope metadata unavailable' }, - external: { status: 'unavailable', error: 'external reference unavailable' }, - phase: 'approval', - phaseLabel: 'approval', - statusCounts: { open: 1 }, - activeAssignees: [], - updatedAt: { - status: 'available', - at: new Date(NOW_MS - 4 * 24 * 60 * 60 * 1000).toISOString(), - }, - stages: [], - progress: { status: 'unavailable', error: 'run progress unavailable' }, - formulaStageResolved: false, - health: health('unresolved'), - ...overrides, - }; -} - -describe('isStaleSessionlessLatch — gascity-dashboard-s4rp', () => { - test('demotes a session-less, step-less, stale approval latch (gc-1920)', () => { - assert.equal(isStaleSessionlessLatch(lane(), NOW_MS, true), true); - }); - - test('keeps a freshly-queued session-less run (recent updatedAt)', () => { - const queued = lane({ - phase: 'intake', - updatedAt: { status: 'available', at: new Date(NOW_MS - 60_000).toISOString() }, - }); - assert.equal(isStaleSessionlessLatch(queued, NOW_MS, true), false); - }); - - test('keeps an approval gate waiting on a human while still recent', () => { - const waiting = lane({ - updatedAt: { status: 'available', at: new Date(NOW_MS - 30 * 60_000).toISOString() }, - }); - assert.equal(isStaleSessionlessLatch(waiting, NOW_MS, true), false); - }); - - test('keeps a stale run that still has a resolved live session', () => { - assert.equal( - isStaleSessionlessLatch(lane({ health: health('resolved') }), NOW_MS, true), - false, - ); - }); - - test('keeps a stale run that still has an in_progress primary step', () => { - const active = lane({ - progress: { - status: 'active_step', - stepId: 'implementation.patch', - stage: { status: 'unavailable', error: 'active run stage unavailable' }, - attempt: { status: 'unavailable', error: 'run step attempt unavailable' }, - }, - }); - assert.equal(isStaleSessionlessLatch(active, NOW_MS, true), false); - }); - - test('does not demote when the session list is unavailable', () => { - assert.equal(isStaleSessionlessLatch(lane(), NOW_MS, false), false); - }); - - test('never demotes complete or blocked lanes (already partitioned upstream)', () => { - assert.equal(isStaleSessionlessLatch(lane({ phase: 'complete' }), NOW_MS, true), false); - assert.equal(isStaleSessionlessLatch(lane({ phase: 'blocked' }), NOW_MS, true), false); - }); - - test('does not demote without a known age', () => { - const noAge = lane({ - updatedAt: { status: 'unavailable', error: 'run update time unavailable' }, - }); - assert.equal(isStaleSessionlessLatch(noAge, NOW_MS, true), false); - }); - - test('the staleness boundary is exclusive below the floor', () => { - const justUnder = lane({ - updatedAt: { - status: 'available', - at: new Date(NOW_MS - (STALE_LATCH_AFTER_MS - 1_000)).toISOString(), - }, - }); - assert.equal(isStaleSessionlessLatch(justUnder, NOW_MS, true), false); - const atFloor = lane({ - updatedAt: { status: 'available', at: new Date(NOW_MS - STALE_LATCH_AFTER_MS).toISOString() }, - }); - assert.equal(isStaleSessionlessLatch(atFloor, NOW_MS, true), true); - }); -}); - -describe('isDanglingRootGroup — gascity-dashboard-s4rp', () => { - function issue(id: string): RunIssue { - return { - id, - title: id, - status: 'open', - issue_type: 'task', - updated_at: '2026-06-01T00:00:00Z', - }; - } - - test('flags a group whose root bead is absent from its issues', () => { - assert.equal(isDanglingRootGroup('gc-1920', [issue('gc-1920-step-1')]), true); - }); - - test('does not flag a group whose root bead is present', () => { - assert.equal( - isDanglingRootGroup('gc-1920', [issue('gc-1920'), issue('gc-1920-step-1')]), - false, - ); - }); -}); diff --git a/internal/api/dashboardspa/web/shared/src/runs/liveness.ts b/internal/api/dashboardspa/web/shared/src/runs/liveness.ts deleted file mode 100644 index 1a9114ce5f..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/liveness.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { RunIssue } from './phaseMapping.js'; -import type { RunLane } from '../snapshot/types.js'; - -// gascity-dashboard-s4rp: a run can be echoed by the supervisor long after it -// has stopped progressing. The operator repro is gc-1920 — an ancient -// (id ~1920 vs a current store at ~346k) mol-focus-review approval-gate latch -// with NO live session, ~4 days since its last bead write, and no in_progress -// step. It was counted as Active:1 and flickered in and out of the lane set on -// every refresh. Half (b) of gascity-dashboard-4xcv asked for these -// session-less latches to be demoted out of Active; the blocked half (a) was -// already handled by the blockedLanes split. -// -// The sharp predicate has to demote the dead latch WITHOUT demoting a run that -// is legitimately session-less for a benign reason — a freshly queued run that -// has not yet been picked up, or an approval gate genuinely waiting on a human. -// Those are RECENT; the dead latch is days old. Staleness is therefore the -// distinguishing axis, with a floor well clear of any human-in-the-loop -// turnaround so a real approval gate is never demoted while it is still being -// waited on. - -/** - * A run is treated as a stale session-less latch once its most-recent bead - * write is older than this. Deliberately far above the live-attention staleness - * tiers (frontend `STALENESS_TIER_MS.stalled` = 30m) — that tier flags a run - * the operator should look at NOW; this floor decides a run is abandoned and - * should leave the Active set entirely, so it must clear normal queue and - * approval-gate dwell times (hours) with room to spare. - */ -export const STALE_LATCH_AFTER_MS = 24 * 60 * 60 * 1000; - -/** - * True when `lane` is an open run that is no longer progressing and should be - * demoted out of the Active set: the session list resolved, no session maps to - * the lane, no primary step is in_progress, and the lane's last write is older - * than {@link STALE_LATCH_AFTER_MS}. complete/blocked lanes are already - * partitioned out upstream and are never considered here. - * - * `nowMs` is the snapshot generation time (the caller's fetch timestamp), not a - * live clock read — staleness is judged against the data's own generation so - * the result is deterministic for a given snapshot (no wall-clock test flake). - */ -export function isStaleSessionlessLatch( - lane: RunLane, - nowMs: number, - sessionsAvailable: boolean, -): boolean { - // Without a session list we cannot trust the "no session" signal — a failed - // session read must not demote every lane. - if (!sessionsAvailable) return false; - if (lane.phase === 'complete' || lane.phase === 'blocked') return false; - // A live in_progress primary step means the run IS progressing. - if (lane.progress.status === 'active_step') return false; - // A resolved session means a worker is on it (even if parked at a gate). - if (laneSessionResolved(lane)) return false; - // Needs a known age to judge staleness; absent that we do not demote. - if (lane.updatedAt.status !== 'available') return false; - const ageMs = nowMs - Date.parse(lane.updatedAt.at); - return Number.isFinite(ageMs) && ageMs >= STALE_LATCH_AFTER_MS; -} - -function laneSessionResolved(lane: RunLane): boolean { - return lane.health.status === 'available' && lane.health.data.session.status === 'resolved'; -} - -/** - * True when a run group's root bead is absent from the group's issues — the run - * is rooted at a bead that no longer exists in the store (gascity-dashboard-s4rp - * dangling root). Such a group has no authoritative root metadata; its title is - * inferred from a child and its scope is unresolvable, so it must not be - * surfaced as a live run. - */ -export function isDanglingRootGroup(rootId: string, issues: readonly RunIssue[]): boolean { - return !issues.some((issue) => issue.id === rootId); -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/node-shape.ts b/internal/api/dashboardspa/web/shared/src/runs/node-shape.ts deleted file mode 100644 index 47cd89a22c..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/node-shape.ts +++ /dev/null @@ -1,190 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; -import type { RunConstructKind } from '../run-detail.js'; -import { externalizeId, meta, nonEmpty, normalizedStepRef } from './bead-fields.js'; - -const HIDDEN_CONSTRUCTS = new Set(['scope-check', 'run-finalize', 'spec']); - -export function isHiddenConstruct(kind: RunConstructKind): boolean { - return HIDDEN_CONSTRUCTS.has(kind) || kind === 'control'; -} - -export function semanticNodeIdFor(bead: RunSnapshotBead, rootBeadId: string): string { - const beadId = nonEmpty(bead.id); - if (beadId && beadId === rootBeadId) return rootBeadId; - const explicit = meta(bead, 'gc.logical_bead_id') ?? nonEmpty(bead.logical_bead_id); - if (explicit) return externalizeId(explicit); - const stepId = meta(bead, 'gc.step_id'); - if (stepId) return externalizeId(stepId); - const ref = normalizedStepRef(bead); - if (ref) { - const semanticId = semanticIdFromStepRef(ref); - if (semanticId) return externalizeId(semanticId); - } - return externalizeId(beadId ?? 'run-node'); -} - -export function hiddenBadgeTargetFor(bead: RunSnapshotBead, rootBeadId: string): string | null { - const kind = constructKindFor(bead, rootBeadId); - if (kind === 'run-finalize') return rootBeadId; - const controlRef = meta(bead, 'gc.control_for'); - if (controlRef) { - const controlTarget = semanticIdFromControlRef(controlRef); - if (controlTarget) return externalizeId(controlTarget); - } - const ref = normalizedStepRef(bead); - if (!ref) return null; - const target = semanticIdFromControlRef(ref); - return target ? externalizeId(target) : null; -} - -export function constructKindFor(bead: RunSnapshotBead, rootBeadId: string): RunConstructKind { - const beadId = nonEmpty(bead.id); - if (beadId && beadId === rootBeadId) return 'run-root'; - const kind = rawKind(bead); - switch (kind) { - case 'ralph': - return 'check-loop'; - case 'retry': - return 'retry'; - case 'scope': - case 'epic': - case 'body': - return 'scope'; - case 'fanout': - return 'fanout'; - case 'condition': - return 'condition'; - case 'expand': - case 'expansion': - return 'expansion'; - case 'scope-check': - return 'scope-check'; - case 'run-finalize': - return 'run-finalize'; - case 'spec': - return 'spec'; - case 'cleanup': - return 'control'; - default: - return 'step'; - } -} - -export function externalKindFor(bead: RunSnapshotBead, constructKind: RunConstructKind): string { - if (constructKind === 'check-loop') return 'check-loop'; - const kind = rawKind(bead); - return kind === 'ralph' ? 'check-loop' : kind || constructKind; -} - -export function displayTitleFor(bead: RunSnapshotBead, fallback: string): string { - return externalizeDisplayText(nonEmpty(bead.title) ?? fallback.replace(/[-_]/g, ' ')); -} - -export function badgeLabelFor(kind: RunConstructKind): string { - switch (kind) { - case 'scope-check': - return 'scope check'; - case 'run-finalize': - return 'finalize'; - case 'check-loop': - case 'condition': - case 'control': - case 'expansion': - case 'fanout': - case 'retry': - case 'scope': - case 'spec': - case 'step': - case 'unknown': - case 'run-root': - return kind.replace(/-/g, ' '); - } -} - -export function loopControlNodeIdFor(bead: RunSnapshotBead): string | undefined { - const scopeRef = meta(bead, 'gc.scope_ref') ?? nonEmpty(bead.scope_ref); - const scopeControlId = scopeRef - ? loopControlIdFromRuntimeRef(scopeRef, ['iteration', 'run']) - : undefined; - if (scopeControlId) return scopeControlId; - - const ref = normalizedStepRef(bead); - if (!ref) return undefined; - return loopControlIdFromRuntimeRef(ref, ['iteration']); -} - -function rawKind(bead: RunSnapshotBead): string { - return meta(bead, 'gc.kind') ?? meta(bead, 'gc.original_kind') ?? nonEmpty(bead.kind) ?? ''; -} - -function semanticIdFromStepRef(ref: string): string | undefined { - const parts = ref.split('.').filter(Boolean); - if (parts.length === 0) return undefined; - - const semanticParts = stripRuntimeSuffix(parts); - const iterationIndex = semanticParts.lastIndexOf('iteration'); - if ( - iterationIndex >= 0 && - iterationIndex < semanticParts.length - 2 && - isPositiveInteger(semanticParts[iterationIndex + 1]) - ) { - return semanticParts.at(-1); - } - if ( - iterationIndex === semanticParts.length - 2 && - isPositiveInteger(semanticParts[iterationIndex + 1]) - ) { - return semanticParts[iterationIndex - 1]; - } - return semanticParts.at(-1); -} - -function semanticIdFromControlRef(ref: string): string | undefined { - return semanticIdFromStepRef(stripScopeCheckSuffix(ref)); -} - -function stripScopeCheckSuffix(ref: string): string { - return ref.replace(/-scope-check$/, '').replace(/\.scope-check$/, ''); -} - -function stripRuntimeSuffix(parts: string[]): string[] { - const marker = parts.at(-2); - const value = parts.at(-1); - if ( - value && - marker && - isPositiveInteger(value) && - (marker === 'attempt' || marker === 'run' || marker === 'check' || marker === 'eval') - ) { - return parts.slice(0, -2); - } - return parts; -} - -function loopControlIdFromRuntimeRef(ref: string, markers: readonly string[]): string | undefined { - const parts = ref.split('.').filter(Boolean); - for (const marker of markers) { - const markerIndex = parts.findIndex( - (part, index) => part === marker && isPositiveInteger(parts[index + 1]), - ); - if (markerIndex <= 0) continue; - const controlId = parts[markerIndex - 1]; - return controlId ? externalizeId(controlId) : undefined; - } - return undefined; -} - -function isPositiveInteger(value: string | undefined): boolean { - if (!value) return false; - const parsed = Number.parseInt(value, 10); - return String(parsed) === value && parsed > 0; -} - -function externalizeDisplayText(value: string): string { - if (!/(^|[^A-Za-z0-9])ralph(?=$|[^A-Za-z0-9])/i.test(value)) return value; - return value - .replace(/[-_]+/g, ' ') - .replace(/(^|[^A-Za-z0-9])ralph(?=$|[^A-Za-z0-9])/gi, '$1check loop') - .replace(/\s+/g, ' ') - .trim(); -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/phaseMapping.test.ts b/internal/api/dashboardspa/web/shared/src/runs/phaseMapping.test.ts deleted file mode 100644 index ddd3a4fdd3..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/phaseMapping.test.ts +++ /dev/null @@ -1,411 +0,0 @@ -import { test, describe } from 'node:test'; -import assert from 'node:assert/strict'; - -import { mapRunPhase, stepIdPhase, type RunIssue } from './phaseMapping.js'; - -// gascity-dashboard-q3p1: phase is derived from the run's CURRENT step -// (structured-first), not from a keyword scan over title+description+metadata. -// The old scan collapsed almost every real formula run onto 'approval' because -// broad needles like 'gate' (order:gate-sweep, "ship gate") and 'human' (the -// ubiquitous summary_for_human metadata key) matched incidental text in some -// bead of the group. - -function root(overrides: Partial & Pick): RunIssue { - return { - title: 'run root', - status: 'open', - issue_type: 'molecule', - updated_at: '2026-06-06T00:00:00.000Z', - metadata: { 'gc.formula_contract': 'graph.v2', 'gc.kind': 'run' }, - ...overrides, - }; -} - -function step( - id: string, - stepId: string, - status: string, - overrides: Partial = {}, -): RunIssue { - return { - id, - title: 'step', - status, - issue_type: 'task', - updated_at: '2026-06-06T00:01:00.000Z', - metadata: { 'gc.kind': 'step', 'gc.step_id': stepId }, - ...overrides, - }; -} - -describe('mapRunPhase — structured derivation from the current step (gascity-dashboard-q3p1)', () => { - test('an in_progress implementation step → implementation', () => { - const phase = mapRunPhase([ - root({ id: 'r1' }), - step('r1-s1', 'implement-change', 'in_progress'), - ]); - assert.equal(phase.phase, 'implementation'); - }); - - test('an in_progress review step → review (with a review round)', () => { - const phase = mapRunPhase([ - root({ id: 'r2' }), - step('r2-s1', 'code-review-loop', 'in_progress'), - ]); - assert.equal(phase.phase, 'review'); - assert.ok(phase.reviewRound !== null && phase.reviewRound >= 1); - }); - - test('an in_progress approval/gate step → approval (true positive still works)', () => { - const phase = mapRunPhase([root({ id: 'r3' }), step('r3-s1', 'human-approval', 'in_progress')]); - assert.equal(phase.phase, 'approval'); - }); - - test('an in_progress finalize step → finalization', () => { - const phase = mapRunPhase([ - root({ id: 'r4' }), - step('r4-s1', 'merge-and-finalize', 'in_progress'), - ]); - assert.equal(phase.phase, 'finalization'); - }); - - test('phase advances as the active step advances (graph.v2 progression)', () => { - const base = [root({ id: 'rp' })]; - // 1. implementation in progress, later steps not yet started. - const atImpl = mapRunPhase([ - ...base, - step('rp-s1', 'implement-change', 'in_progress'), - step('rp-s2', 'code-review-loop', 'open'), - step('rp-s3', 'human-approval', 'open'), - ]); - assert.equal(atImpl.phase, 'implementation'); - - // 2. implementation closed, review now in progress. - const atReview = mapRunPhase([ - ...base, - step('rp-s1', 'implement-change', 'closed'), - step('rp-s2', 'code-review-loop', 'in_progress', { - updated_at: '2026-06-06T01:00:00.000Z', - }), - step('rp-s3', 'human-approval', 'open'), - ]); - assert.equal(atReview.phase, 'review'); - - // 3. review closed, approval now in progress. - const atApproval = mapRunPhase([ - ...base, - step('rp-s1', 'implement-change', 'closed'), - step('rp-s2', 'code-review-loop', 'closed'), - step('rp-s3', 'human-approval', 'in_progress', { - updated_at: '2026-06-06T02:00:00.000Z', - }), - ]); - assert.equal(atApproval.phase, 'approval'); - }); - - test('falls back to latest advanced step when nothing is in_progress', () => { - const phase = mapRunPhase([ - root({ id: 'rl' }), - step('rl-s1', 'implement-change', 'closed', { updated_at: '2026-06-06T00:01:00.000Z' }), - step('rl-s2', 'code-review-loop', 'closed', { updated_at: '2026-06-06T02:00:00.000Z' }), - ]); - // Latest advanced step is the review-loop, so the run reads as review. - assert.equal(phase.phase, 'review'); - }); -}); - -describe('mapRunPhase — incidental text never forces approval (the core regression)', () => { - test('a summary_for_human metadata key does NOT classify the run as approval', () => { - const phase = mapRunPhase([ - root({ - id: 'h1', - metadata: { - 'gc.formula_contract': 'graph.v2', - 'gc.kind': 'run', - summary_for_human: 'the run summary that operators read', - }, - }), - step('h1-s1', 'implement-change', 'in_progress', { - metadata: { - 'gc.kind': 'step', - 'gc.step_id': 'implement-change', - summary_for_human: 'implementing the change', - }, - }), - ]); - assert.notEqual(phase.phase, 'approval'); - assert.equal(phase.phase, 'implementation'); - }); - - test('a "gate" reference (order:gate-sweep dep / "Run BLOCKING gates" desc) does NOT classify as approval', () => { - const phase = mapRunPhase([ - root({ - id: 'g1', - description: 'Run BLOCKING gates before merge; order:gate-sweep dependency', - }), - step('g1-s1', 'implement-change', 'in_progress', { - title: 'Run BLOCKING gates', - description: 'order:gate-sweep — ship gate', - }), - ]); - assert.notEqual(phase.phase, 'approval'); - assert.equal(phase.phase, 'implementation'); - }); - - test('a run with summary_for_human but NO step beads stays conservative, not approval', () => { - const phase = mapRunPhase([ - root({ - id: 'n1', - title: 'A generic run', - metadata: { - 'gc.kind': 'run', - summary_for_human: 'a human-readable summary that mentions a gate', - }, - }), - ]); - // No step identity, and the summary_for_human value (with its 'gate'/'human' - // text) is no longer scanned — so the fallback stays conservative. - assert.notEqual(phase.phase, 'approval'); - assert.equal(phase.phase, 'active'); - }); -}); - -describe('mapRunPhase — deterministic current-step pick without updated_at (gascity-dashboard Major 3)', () => { - // The run-detail snapshot adapter (formula-run.ts fromRunSnapshotBead) sets - // every bead updated_at=''. With no in_progress step the old timestamp-based - // pick was input-ORDER-dependent (Date.parse('') === NaN). The fallback must - // be deterministic and pick the furthest-advanced stage, and a summary-context - // version of the same run (real timestamps) must agree with the detail context. - - function snapshotStep(stepId: string, status: string): RunIssue { - // Detail-snapshot shape: no per-bead timestamp. - return { - id: `step-${stepId}`, - title: 'step', - status, - issue_type: 'task', - updated_at: '', - metadata: { 'gc.kind': 'step', 'gc.step_id': stepId }, - }; - } - - test('no in_progress step + empty updated_at → deterministic furthest stage, order-independent', () => { - const forward = mapRunPhase([ - root({ id: 'd1', updated_at: '' }), - snapshotStep('implement-change', 'closed'), - snapshotStep('code-review-loop', 'closed'), - ]); - const reversed = mapRunPhase([ - root({ id: 'd1', updated_at: '' }), - snapshotStep('code-review-loop', 'closed'), - snapshotStep('implement-change', 'closed'), - ]); - // Furthest advanced of the two closed steps is the review loop. - assert.equal(forward.phase, 'review'); - // Input order must not change the result. - assert.equal(reversed.phase, forward.phase); - }); - - test('summary-context (real timestamps) and detail-context (empty) of the same run agree', () => { - const detail = mapRunPhase([ - root({ id: 'd2', updated_at: '' }), - snapshotStep('implement-change', 'closed'), - snapshotStep('code-review-loop', 'closed'), - ]); - const summary = mapRunPhase([ - root({ id: 'd2' }), - step('d2-s1', 'implement-change', 'closed', { updated_at: '2026-06-06T00:01:00.000Z' }), - step('d2-s2', 'code-review-loop', 'closed', { updated_at: '2026-06-06T02:00:00.000Z' }), - ]); - assert.equal(detail.phase, summary.phase); - }); -}); - -describe('mapRunPhase — status branches remain authoritative', () => { - test('any blocked bead → blocked, regardless of step identity', () => { - const phase = mapRunPhase([root({ id: 'b1' }), step('b1-s1', 'implement-change', 'blocked')]); - assert.equal(phase.phase, 'blocked'); - }); - - test('all closed → complete, regardless of step identity', () => { - const phase = mapRunPhase([ - root({ id: 'c1', status: 'closed' }), - step('c1-s1', 'implement-change', 'closed'), - ]); - assert.equal(phase.phase, 'complete'); - }); -}); - -describe('mapRunPhase — tightened keyword fallback (no structured steps)', () => { - test('a do-work title (no gc.step_id) still reads as implementation', () => { - const phase = mapRunPhase([ - root({ id: 'f1', title: 'mol-do-work' }), - { - id: 'f1-c1', - title: 'Do the work', - status: 'in_progress', - issue_type: 'task', - updated_at: '2026-06-06T00:02:00.000Z', - metadata: { molecule_id: 'f1' }, - }, - ]); - assert.equal(phase.phase, 'implementation'); - }); - - test('a description-only "gate"/"human"/"merge" mention does NOT reach approval/finalization in the fallback', () => { - const phase = mapRunPhase([ - root({ - id: 'f2', - title: 'A generic run', - description: 'review the gate, ask a human, then merge and report', - }), - ]); - // Description is no longer scanned and the title carries no step signal → - // conservative active. The incidental 'gate'/'human'/'merge' words in the - // description never force a late phase. - assert.notEqual(phase.phase, 'approval'); - assert.notEqual(phase.phase, 'finalization'); - assert.equal(phase.phase, 'active'); - }); -}); - -describe('stepIdPhase — step-identity classification', () => { - test('classifies representative declared step ids', () => { - assert.equal(stepIdPhase('bootstrap-run'), 'intake'); - assert.equal(stepIdPhase('preflight'), 'intake'); - assert.equal(stepIdPhase('implement-change'), 'implementation'); - assert.equal(stepIdPhase('implementation.patch'), 'implementation'); - assert.equal(stepIdPhase('do-work'), 'implementation'); - assert.equal(stepIdPhase('review-pipeline.review-claude'), 'review'); - assert.equal(stepIdPhase('code-review-loop'), 'review'); - assert.equal(stepIdPhase('human-approval'), 'approval'); - assert.equal(stepIdPhase('approve-merge'), 'approval'); - assert.equal(stepIdPhase('merge-and-finalize'), 'finalization'); - assert.equal(stepIdPhase('cleanup-worktree'), 'finalization'); - }); - - test('unknown step id is conservative (active), never invents a late phase', () => { - assert.equal(stepIdPhase('totally-unknown-step'), 'active'); - }); - - // gascity-dashboard (Major 1): tokenized whole-token matching, not raw - // substring includes(). A leading/CI step that merely CONTAINS a late-stage - // word as a substring (or as a token behind a negating prefix) must not be - // misbucketed onto the late stage — that falsely surfaces a CI step as - // "waiting on human" through needsOperator (health.ts keys on phase==='approval'). - test('pre-approval-ci is NOT approval (negating `pre` prefix on the gate token)', () => { - assert.notEqual(stepIdPhase('pre-approval-ci'), 'approval'); - }); - - test('dispatch-implementation is implementation, not finalization', () => { - assert.equal(stepIdPhase('dispatch-implementation'), 'implementation'); - }); - - test('prepare-review-context is review, never approval or finalization', () => { - const phase = stepIdPhase('prepare-review-context'); - assert.notEqual(phase, 'approval'); - assert.notEqual(phase, 'finalization'); - assert.equal(phase, 'review'); - }); - - test('whole real step ids still classify correctly after tokenization', () => { - assert.equal(stepIdPhase('review'), 'review'); - assert.equal(stepIdPhase('approval'), 'approval'); - assert.equal(stepIdPhase('approve'), 'approval'); - assert.equal(stepIdPhase('implementation'), 'implementation'); - assert.equal(stepIdPhase('do-work'), 'implementation'); - assert.equal(stepIdPhase('load-context'), 'intake'); - assert.equal(stepIdPhase('finalize'), 'finalization'); - }); - - test('a substring that is not a whole token does not match (e.g. `approval` inside a longer token)', () => { - // `disapproval-note` tokenizes to [disapproval, note]; neither token is a - // recognized stage word, so the old includes('approval') false positive - // is gone. - assert.equal(stepIdPhase('disapproval-note'), 'active'); - }); - - // gascity-dashboard (Residual A): the gate stages (approval, finalization) - // reject the stage token when ANY lead-up qualifier token appears anywhere in - // the step-id tokens — not only when the qualifier immediately precedes the - // stage token. `wait-for-approval` ([wait,for,approval]) and `prepare-for-merge` - // ([prepare,for,merge]) are steps that LEAD UP TO the gate, so they must not - // classify as the gate even though the token before the stage token is `for`. - describe('gate stages reject any lead-up qualifier token anywhere (Residual A)', () => { - test('pre-approval-ci is NOT approval (qualifier `pre` anywhere)', () => { - assert.notEqual(stepIdPhase('pre-approval-ci'), 'approval'); - }); - - test('wait-for-approval is NOT approval (qualifier `wait`/`for`, not adjacent)', () => { - assert.notEqual(stepIdPhase('wait-for-approval'), 'approval'); - }); - - test('prepare-for-merge is NOT finalization (qualifier `prepare`/`for`, not adjacent)', () => { - assert.notEqual(stepIdPhase('prepare-for-merge'), 'finalization'); - }); - - test('true gates still classify: approval/approve → approval', () => { - assert.equal(stepIdPhase('approval'), 'approval'); - assert.equal(stepIdPhase('approve'), 'approval'); - }); - - test('true gates still classify: finalize/finalization → finalization', () => { - assert.equal(stepIdPhase('finalize'), 'finalization'); - assert.equal(stepIdPhase('finalization'), 'finalization'); - }); - - test('approve-merge is approval — it IS the approval step, no lead-up qualifier', () => { - assert.equal(stepIdPhase('approve-merge'), 'approval'); - }); - - test('non-gate stages keep classifying on a whole stage token (no qualifier rejection)', () => { - assert.equal(stepIdPhase('review'), 'review'); - assert.equal(stepIdPhase('do-work'), 'implementation'); - assert.equal(stepIdPhase('implementation'), 'implementation'); - assert.equal(stepIdPhase('load-context'), 'intake'); - }); - }); -}); - -// gascity-dashboard (Residual B): furthestStageStepId / latestStepId stage -// tiebreak rank the lifecycle order intake → implementation → review → approval -// → finalization, so finalization is the FURTHEST stage. A no-in_progress run -// whose furthest closed step is finalization must read as finalization, not -// approval. The mapRunPhase CURRENT-phase precedence (approval before -// finalization) is a separate concern and stays as-is. -describe('mapRunPhase — finalization is the furthest lifecycle stage (Residual B)', () => { - function snapshotStep(stepId: string, status: string): RunIssue { - return { - id: `step-${stepId}`, - title: 'step', - status, - issue_type: 'task', - updated_at: '', - metadata: { 'gc.kind': 'step', 'gc.step_id': stepId }, - }; - } - - test('no in_progress, closed approve-merge + closed merge-and-finalize → finalization (NOT approval)', () => { - const forward = mapRunPhase([ - root({ id: 'fb1', updated_at: '' }), - snapshotStep('approve-merge', 'closed'), - snapshotStep('merge-and-finalize', 'closed'), - ]); - const reversed = mapRunPhase([ - root({ id: 'fb1', updated_at: '' }), - snapshotStep('merge-and-finalize', 'closed'), - snapshotStep('approve-merge', 'closed'), - ]); - assert.equal(forward.phase, 'finalization'); - assert.equal(reversed.phase, forward.phase); - }); - - test('no in_progress, closed review + closed approval → approval (approval furthest of the two)', () => { - const phase = mapRunPhase([ - root({ id: 'fb2', updated_at: '' }), - snapshotStep('code-review-loop', 'closed'), - snapshotStep('human-approval', 'closed'), - ]); - assert.equal(phase.phase, 'approval'); - }); -}); diff --git a/internal/api/dashboardspa/web/shared/src/runs/phaseMapping.ts b/internal/api/dashboardspa/web/shared/src/runs/phaseMapping.ts deleted file mode 100644 index 6091244b46..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/phaseMapping.ts +++ /dev/null @@ -1,708 +0,0 @@ -// Pure phase-classification rules for the runs collector -// (gascity-dashboard-0t6). Kept in their own module so the rules can be -// tested independently of the lane builder and the transport layer. -// -// All exports here are deterministic functions over RunIssue values; -// no IO, no global state. The companion test file pins the upstream -// classifier behavior so the React translation of RunMap inherits a -// consistent phase grammar. -// -// Phase is derived structurally — from the run's CURRENT step (gc.step_id) -// classified into a generic RunPhase (see structuredPhase / stepIdPhase). Only -// when no step carries a gc.step_id does it fall back to a tightened keyword -// scan scoped to step-identity signals (titles + gc.step_id), never the full -// description+metadata dump (see fallbackPhase). Parent keyword scans read -// DashboardBead.parent first and fall back to the older -// metadata['gc.parent_bead_id'] marker when present. - -import type { DashboardBead } from '../dashboard-beads.js'; -import type { RunPhase as SharedRunPhase, RunStage } from '../snapshot/types.js'; - -export interface RunIssue { - id: string; - title: string; - description?: string; - status: string; - issue_type: string; - assignee?: string; - updated_at: string; - /** Populated from DashboardBead.parent or metadata['gc.parent_bead_id']. */ - parent?: string; - metadata?: Record; -} - -export interface PhaseMapping { - phase: SharedRunPhase; - label: string; - reviewRound: number | null; -} - -export function mapRunPhase(issues: RunIssue[]): PhaseMapping { - // Status-based branches first — these are authoritative and correct. - if (issues.some((i) => i.status === 'blocked' || textForIssue(i).includes('blocked'))) { - return { phase: 'blocked', label: 'blocked', reviewRound: null }; - } - - if (issues.length > 0 && issues.every((i) => i.status === 'closed')) { - return { phase: 'complete', label: 'complete', reviewRound: null }; - } - - // gascity-dashboard-q3p1: structured-first phase derivation. When the run has - // step beads carrying gc.step_id, the run's phase is the phase of its CURRENT - // step (the in_progress primary step, else the latest-advanced primary step) — - // a real progress signal that tracks the run forward as steps advance. The old - // keyword scan over title+description+metadata collapsed almost every formula - // run to 'approval' because needles like 'gate' (order:gate-sweep, "ship gate") - // and 'human' (the ubiquitous summary_for_human metadata key) matched incidental - // text in some bead of the group. Step-id is a structural identifier, not free - // text, so it does not suffer that false-positive class. - const structured = structuredPhase(issues); - if (structured !== null) { - return structured; - } - - // Fallback (no resolvable step identity): a tightened keyword scan scoped to - // step-identity signals only (titles + gc.step_id), with the incidental-match - // needles removed. Conservative — returns 'active' when genuinely ambiguous. - return fallbackPhase(issues); -} - -/** - * gascity-dashboard-q3p1: derive the phase from the run's current step. - * - * The active step is the latest in_progress primary step; if none is in_progress - * (e.g. between steps) it is the latest primary step that carries a gc.step_id. - * The step-id is classified into a generic RunPhase by stepIdPhase. Returns null - * when no primary step carries a gc.step_id (no structured signal to use). - */ -function structuredPhase(issues: RunIssue[]): PhaseMapping | null { - const primary = issues.filter(isPrimaryStepIssue); - const inProgressStep = latestStepId(primary.filter((i) => i.status === 'in_progress')); - // gascity-dashboard (Major 3): when no step is in_progress, the current step - // is the FURTHEST-ADVANCED step by stage rank — a deterministic, order- and - // timestamp-independent signal. The run-detail snapshot adapter sets every - // bead updated_at='' (the snapshot carries no per-bead timestamp), so a - // timestamp-based pick there is input-order-dependent and can drift from the - // summary lane. Stage rank is derived from gc.step_id alone, so the summary - // context (real timestamps) and the detail context (empty timestamps) resolve - // the same run to the same phase. - const activeStepId = inProgressStep ?? furthestStageStepId(primary); - if (activeStepId === null) { - return null; - } - - const phase = stepIdPhase(activeStepId); - if (phase === 'review') { - const resolved = reviewRoundForIssues(issues) ?? fallbackReviewRound(issues); - return { phase: 'review', label: `review round ${resolved}`, reviewRound: resolved }; - } - return { phase, label: phase, reviewRound: null }; -} - -/** - * Classify a single gc.step_id into a generic RunPhase. The step id is split on - * its structural delimiters (`-`, `.`, `_`, `:`, `/`) into TOKENS and matched - * WHOLE-TOKEN against per-stage keyword sets — never as raw substrings. Whole- - * token matching is what stops a CI/leading step from being misbucketed onto a - * late stage: `pre-approval-ci` is not approval, `dispatch-implementation` is - * implementation (not finalization), `prepare-review-context` is review (not - * approval/finalization). - * - * Precedence is latest-stage-first (approval > finalization > review > - * implementation > intake), preserving the approval-before-finalization rule so - * an approval gate that names its successor (`approve-merge`, - * `verify-merge-approval`) resolves to approval, not finalization, while a pure - * finalization step (`merge-and-finalize`) still resolves to finalization. - * - * The gate stages (approval, finalization) additionally reject the stage token - * when ANY lead-up qualifier token (`pre`, `prepare`, `wait`, `await`, - * `pending`, `before`, `for`, `to`) appears anywhere in the step-id tokens — - * not only immediately before the stage token. Those are steps that LEAD UP TO - * the gate, not the gate itself (`pre-approval-ci`, `wait-for-approval`, - * `prepare-for-merge`). Implementation/review/intake do not apply the qualifier - * rule: a real stage token there is a reliable signal regardless of qualifier. - * - * Returns 'active' when no token names a recognizable stage — deliberately - * conservative, so an unknown step never invents a specific late phase. - */ -export function stepIdPhase(stepId: string): SharedRunPhase { - const tokens = tokenizeStepId(stepId); - if (hasStageToken(tokens, APPROVAL_STAGE_TOKENS, { rejectWithLeadUpQualifier: true })) { - return 'approval'; - } - if (hasStageToken(tokens, FINALIZATION_STAGE_TOKENS, { rejectWithLeadUpQualifier: true })) { - return 'finalization'; - } - if (hasStageToken(tokens, REVIEW_STAGE_TOKENS)) return 'review'; - if (hasStageToken(tokens, IMPLEMENTATION_STAGE_TOKENS)) return 'implementation'; - if (hasStageToken(tokens, INTAKE_STAGE_TOKENS)) return 'intake'; - return 'active'; -} - -const STEP_ID_DELIMITERS = /[-._:/]+/; - -function tokenizeStepId(stepId: string): string[] { - return stepId.toLowerCase().split(STEP_ID_DELIMITERS).filter(Boolean); -} - -// Qualifier tokens that mark a step as LEADING UP TO a gate rather than being -// the gate itself, wherever they appear in the step id: `pre-approval-ci`, -// `wait-for-approval`, `prepare-for-merge`, `before-merge`, `pending-approval`. -const LEAD_UP_QUALIFIER_TOKENS: ReadonlySet = new Set([ - 'pre', - 'prepare', - 'wait', - 'await', - 'pending', - 'before', - 'for', - 'to', -]); - -function hasStageToken( - tokens: readonly string[], - stageTokens: ReadonlySet, - options: { rejectWithLeadUpQualifier?: boolean } = {}, -): boolean { - if (!tokens.some((token) => stageTokens.has(token))) return false; - // Gate stages: a stage token is the gate only when no lead-up qualifier token - // appears anywhere in the step id (a step that LEADS UP TO the gate is not it). - if (options.rejectWithLeadUpQualifier) { - return !tokens.some((token) => LEAD_UP_QUALIFIER_TOKENS.has(token)); - } - return true; -} - -// Whole-token stage vocabularies, matched against tokenized gc.step_id values. -// Drawn from the declared step ids in stagesForFormula plus the v1/wisp step -// names (do-work, load-context, …). Multi-word step ids contribute each of -// their tokens (e.g. `merge-and-finalize` → merge, finalize). -const APPROVAL_STAGE_TOKENS: ReadonlySet = new Set([ - 'approval', - 'approve', - 'approved', - 'gate', -]); -const FINALIZATION_STAGE_TOKENS: ReadonlySet = new Set([ - 'finalize', - 'finalization', - 'merge', - 'cleanup', - 'publish', -]); -const REVIEW_STAGE_TOKENS: ReadonlySet = new Set([ - 'review', - 'reviewer', - 'scorecard', - 'persona', - 'personas', - 'audit', - 'repro', - 'baseline', - 'investigation', - 'classify', - 'classification', -]); -const IMPLEMENTATION_STAGE_TOKENS: ReadonlySet = new Set([ - 'implement', - 'implementation', - 'patch', - 'fixes', - 'work', - 'design', -]); -const INTAKE_STAGE_TOKENS: ReadonlySet = new Set([ - 'intake', - 'bootstrap', - 'context', - 'router', - 'request', - 'preflight', - 'setup', - 'rebase', -]); - -/** - * Keyword fallback used only when no step carries a gc.step_id. Scans - * step-identity signals (issue titles + any gc.step_id present) rather than the - * full description+metadata dump, and drops the incidental-matching needles - * ('gate', 'human', 'merge', 'close', 'report', 'work', 'fix', 'code') that - * collapsed real runs onto late phases. Conservative: 'active' when ambiguous. - */ -function fallbackPhase(issues: RunIssue[]): PhaseMapping { - if (stepSignalContainsAny(issues, ['approval', 'approved', 'finalize-scope'])) { - return { phase: 'approval', label: 'approval', reviewRound: null }; - } - - if (stepSignalContainsAny(issues, ['post-merge', 'finalization', 'finalize'])) { - return { phase: 'finalization', label: 'finalization', reviewRound: null }; - } - - const round = reviewRoundForIssues(issues); - if (round !== null || stepSignalContainsAny(issues, ['review', 'reviewer', 'scorecard'])) { - const resolved = round ?? fallbackReviewRound(issues); - return { phase: 'review', label: `review round ${resolved}`, reviewRound: resolved }; - } - - if (stepSignalContainsAny(issues, ['implementation', 'patch', 'do-work'])) { - return { phase: 'implementation', label: 'implementation', reviewRound: null }; - } - - if (stepSignalContainsAny(issues, ['intake', 'load-context', 'router', 'request'])) { - return { phase: 'intake', label: 'intake', reviewRound: null }; - } - - return { phase: 'active', label: 'active', reviewRound: null }; -} - -/** - * Step-identity text for fallback scanning: the issue title plus any gc.step_id. - * Excludes description and the metadata dump so incidental words (e.g. a - * summary_for_human value, a "ship gate" mention) never drive the phase. - */ -function stepSignalText(issue: RunIssue): string { - const stepId = stringValue(issue.metadata?.['gc.step_id']); - return [issue.title, stepId].filter(Boolean).join(' ').toLowerCase(); -} - -function stepSignalContainsAny(issues: RunIssue[], needles: string[]): boolean { - return issues.some((i) => { - const text = stepSignalText(i); - return needles.some((n) => text.includes(n)); - }); -} - -/** - * Returns the per-issue review round when one is encoded in metadata. - * Three supported shapes (matching demo-dash): - * 1. key like `*iteration.N` or `*attempt.N` → N (from key). - * 2. key like `*iteration` or `*attempt` with numeric value → value. - * 3. value matching `*iteration.N` or `*attempt.N` → N (from value). - */ -export function reviewRoundForIssue(issue: RunIssue): number | null { - const metadata = issue.metadata ?? {}; - - // Capture group 1 in both ROUND_IN_KEY and ROUND_IN_VALUE is the digit - // sequence. noUncheckedIndexedAccess makes match[1] type as - // `string | undefined`, so each branch checks `!== undefined` before - // numeric coercion. - for (const [key, value] of Object.entries(metadata)) { - const keyMatch = key.match(ROUND_IN_KEY); - if (keyMatch && keyMatch[1] !== undefined) { - return Number(keyMatch[1]); - } - - if (ROUND_KEY_NO_DIGITS.test(key)) { - const attempt = parsePositiveInteger(value); - if (attempt !== null) { - return attempt; - } - } - - const valueMatch = String(value).match(ROUND_IN_VALUE); - if (valueMatch && valueMatch[1] !== undefined) { - return Number(valueMatch[1]); - } - } - - return null; -} - -const ROUND_IN_KEY = /(?:^|\.)(?:iteration|attempt)\.(\d+)$/; -const ROUND_IN_VALUE = /(?:^|\.)(?:iteration|attempt)\.(\d+)$/; -const ROUND_KEY_NO_DIGITS = /(?:^|\.)(?:iteration|attempt)$/; - -export function reviewRoundForIssues(issues: RunIssue[]): number | null { - const rounds = issues.map(reviewRoundForIssue).filter((r): r is number => r !== null); - if (rounds.length === 0) return null; - return Math.max(...rounds); -} - -export function fallbackReviewRound(issues: RunIssue[]): number { - const reviewIssueCount = issues.filter((i) => textForIssue(i).includes('review')).length; - return Math.max(reviewIssueCount, 1); -} - -export function textForIssue(issue: RunIssue): string { - // gascity-dashboard-9w3k: skip `gc.var.*` keys. v1 / wisp runs carry operator - // free-text template inputs there (e.g. gc.var.prompt = "review the blocked PR - // and merge it") which would otherwise feed phase needles ('review', 'blocked', - // 'merge', 'approval', ...) and mis-bucket the run's phase. These are inputs, - // not structural progress signals, so they are excluded from classification. - // graph.v2 roots derive phase from gc.step_id / status, not gc.var.*, so this - // does not affect graph.v2 classification. - const metadataText = Object.entries(issue.metadata ?? {}) - .filter(([key]) => !key.startsWith('gc.var.')) - .map(([key, value]) => `${key} ${String(value)}`) - .join(' '); - - return [ - issue.title, - issue.description, - issue.status, - issue.issue_type, - issue.assignee, - issue.parent, - metadataText, - ] - .filter(Boolean) - .join(' ') - .toLowerCase(); -} - -export function stringValue(value: unknown): string { - return typeof value === 'string' ? value.trim() : ''; -} - -function parsePositiveInteger(value: unknown): number | null { - const parsed = Number(value); - return Number.isInteger(parsed) && parsed > 0 ? parsed : null; -} - -// ── DashboardBead adapter ───────────────────────────────────────────────────────── - -/** - * Adapt the dashboard bead projection to the phase classifier's input. The - * metadata fallback survives because formula scaffolding can still write the - * older `gc.parent_bead_id` key on synthetic beads. - */ -export function fromDashboardBead(bead: DashboardBead): RunIssue { - const parent = bead.parent ?? stringValue(bead.metadata?.['gc.parent_bead_id']); - const issue: RunIssue = { - id: bead.id, - title: bead.title, - status: bead.status, - issue_type: bead.issue_type, - updated_at: bead.updated_at ?? bead.created_at, - }; - if (bead.description !== undefined) issue.description = bead.description; - if (bead.assignee !== undefined) issue.assignee = bead.assignee; - if (parent) issue.parent = parent; - if (bead.metadata !== undefined) issue.metadata = bead.metadata; - return issue; -} - -// ── Stage progression ────────────────────────────────────────────────────── - -const runStages = [ - ['intake', 'Intake'], - ['implementation', 'Implementation'], - ['review', 'Review'], - ['approval', 'Approval'], - ['finalization', 'Finalization'], -] as const; - -export function stageProgress( - phase: PhaseMapping, - formula: string | null, - issues: RunIssue[], -): RunStage[] { - const formulaStages = stagesForFormula(formula); - if (formulaStages.length > 0) { - return formulaStageProgress(formulaStages, issues); - } - - if (phase.phase === 'blocked') { - return [{ key: 'blocked', label: 'Blocked', status: 'blocked' }]; - } - - if (phase.phase === 'complete') { - return runStages.map(([key, label]) => ({ - key, - label, - status: 'complete' as const, - })); - } - - const activeIndex = runStages.findIndex(([key]) => key === phase.phase); - - if (activeIndex < 0) { - return runStages.map(([key, label]) => ({ - key, - label, - status: (key === 'implementation' ? 'active' : 'pending') as RunStage['status'], - })); - } - - return runStages.map(([key, label], idx) => ({ - key, - label: - key === 'review' && phase.reviewRound !== null ? `Review round ${phase.reviewRound}` : label, - status: (idx < activeIndex - ? 'complete' - : idx === activeIndex - ? 'active' - : 'pending') as RunStage['status'], - })); -} - -export function stagesForFormula( - formula: string | null, -): Array<{ key: string; label: string; steps: string[] }> { - if (formula === 'mol-adopt-pr-v2') { - return [ - { key: 'preflight', label: 'Preflight', steps: ['preflight'] }, - { key: 'rebase', label: 'Worktree / rebase', steps: ['rebase-check'] }, - { - key: 'review', - label: 'Review loop', - steps: [ - 'review-loop', - 'review-pipeline.review-claude', - 'review-pipeline.review-codex', - 'review-pipeline.review-gemini', - 'review-pipeline.synthesize', - 'review-pipeline.quality-scorecard', - 'apply-fixes', - ], - }, - { - key: 'ci', - label: 'Pre-approval CI', - steps: ['pre-approval-ci', 'repair-ci-failures'], - }, - { key: 'approval', label: 'Human approval', steps: ['human-approval'] }, - { key: 'finalize', label: 'Merge-ready', steps: ['finalize'] }, - { key: 'cleanup', label: 'Cleanup', steps: ['cleanup-worktree'] }, - ]; - } - - if (formula === 'mol-design-review-v2') { - return [ - { key: 'setup', label: 'Setup', steps: ['design-review.setup'] }, - { - key: 'personas', - label: 'Personas', - steps: [ - 'design-review.persona-gen-claude', - 'design-review.persona-gen-codex', - 'design-review.persona-gen-gemini', - 'design-review.persona-synthesis', - ], - }, - { - key: 'fanout', - label: 'Persona fanout', - steps: ['design-review.prepare-review-items', 'design-review.persona-review-fanout'], - }, - { - key: 'synthesis', - label: 'Synthesis', - steps: ['design-review.global-synthesis'], - }, - { - key: 'apply', - label: 'Apply findings', - steps: ['design-review.apply-design-changes'], - }, - { key: 'finalize', label: 'Finalize', steps: ['finalize'] }, - ]; - } - - if (formula === 'mol-bug-report-flow-v2') { - return [ - { - key: 'intake', - label: 'Intake', - steps: ['bootstrap-run', 'refresh-intake'], - }, - { - key: 'repro', - label: 'Reproduction', - steps: ['historical-baseline', 'reported-build-repro', 'main-repro'], - }, - { - key: 'audit', - label: 'Audit', - steps: ['code-path-audit', 'coverage-audit', 'related-refs-audit'], - }, - { - key: 'classify', - label: 'Classify', - steps: ['investigation-synthesis', 'followup-evidence', 'normalize-outcome'], - }, - { - key: 'approval', - label: 'Human approval', - steps: ['approve-classification', 'verify-classification-approval'], - }, - { key: 'publish', label: 'Publish', steps: ['publish-classification'] }, - { - key: 'dispatch', - label: 'Dispatch fix', - steps: ['dispatch-implementation'], - }, - ]; - } - - if (formula === 'mol-bug-report-implementation-v2') { - return [ - { - key: 'plan', - label: 'Plan approval', - steps: ['approve-fix-plan', 'approve-test-hardening-plan', 'verify-selected-plan-approval'], - }, - { - key: 'design', - label: 'Design review', - steps: ['prepare-design-review-doc', 'design-review'], - }, - { - key: 'implement', - label: 'Implement', - steps: ['implement-change', 'prepare-review-context'], - }, - { - key: 'review', - label: 'Code review', - steps: ['code-review-loop', 'apply-code-fixes'], - }, - { - key: 'pr', - label: 'Open PR', - steps: ['approve-pr-open', 'verify-pr-open-approval', 'open-or-update-pr'], - }, - { key: 'ci', label: 'CI', steps: ['wait-for-ci'] }, - { - key: 'merge', - label: 'Merge', - steps: ['approve-merge', 'verify-merge-approval', 'merge-and-finalize'], - }, - ]; - } - - return []; -} - -function formulaStageProgress( - stages: Array<{ key: string; label: string; steps: string[] }>, - issues: RunIssue[], -): RunStage[] { - const primary = issues.filter(isPrimaryStepIssue); - const activeStepId = latestStepId(primary.filter((i) => i.status === 'in_progress')); - const activeIndex = activeStepId - ? stages.findIndex((s) => s.steps.includes(activeStepId)) - : firstOpenStageIndex(stages, primary); - const furthestClosedIndex = furthestClosedStageIndex(stages, primary); - - const stageHasClosed = (stage: { steps: string[] }): boolean => - stage.steps.some((step) => stepIssues(primary, step).some((i) => i.status === 'closed')); - - return stages.map((stage, idx) => { - let status: RunStage['status']; - if (activeIndex >= 0) { - status = idx < activeIndex ? 'complete' : idx === activeIndex ? 'active' : 'pending'; - } else if (stageHasClosed(stage) || idx < furthestClosedIndex) { - status = 'complete'; - } else { - status = 'pending'; - } - return { key: stage.key, label: stage.label, status }; - }); -} - -function firstOpenStageIndex(stages: Array<{ steps: string[] }>, issues: RunIssue[]): number { - return stages.findIndex((s) => - s.steps.some((step) => stepIssues(issues, step).some((i) => i.status !== 'closed')), - ); -} - -function furthestClosedStageIndex(stages: Array<{ steps: string[] }>, issues: RunIssue[]): number { - let furthest = -1; - stages.forEach((s, idx) => { - if (s.steps.some((step) => stepIssues(issues, step).some((i) => i.status === 'closed'))) { - furthest = idx; - } - }); - return furthest; -} - -export function latestStepId(issues: RunIssue[]): string | null { - return ( - [...issues] - .sort(byMostRecentThenStage) - .map((i) => stringValue(i.metadata?.['gc.step_id'])) - .find(Boolean) ?? null - ); -} - -/** - * gascity-dashboard (Major 3): the deterministic current-step pick for the - * "no step in_progress" fallback. Among the steps that carry a gc.step_id, - * select the one whose stage is furthest along the ladder (approval > - * finalization > review > implementation > intake > active). This depends only - * on gc.step_id — never on updated_at — so it is stable regardless of input - * order and identical whether the beads come from the summary projection (real - * timestamps) or the run-detail snapshot adapter (updated_at=''). Ties on stage - * rank break on the step id string for total determinism. - */ -function furthestStageStepId(issues: RunIssue[]): string | null { - const stepIds = issues - .map((i) => stringValue(i.metadata?.['gc.step_id'])) - .filter((id): id is string => id.length > 0); - if (stepIds.length === 0) return null; - return [...stepIds].sort((a, b) => { - const rankDelta = stageRank(stepIdPhase(b)) - stageRank(stepIdPhase(a)); - if (rankDelta !== 0) return rankDelta; - return a < b ? -1 : a > b ? 1 : 0; - })[0]!; -} - -// Lifecycle rank (higher = further along the run lifecycle), used ONLY to pick -// the furthest-reached stage among steps (furthestStageStepId, and the latest- -// step stage tiebreak in byMostRecentThenStage). The lifecycle order is -// intake → implementation → review → approval → finalization, so finalization -// is the FURTHEST stage. 'active' is the conservative floor. -// -// NOTE: this is deliberately decoupled from the CURRENT-phase precedence in -// stepIdPhase, which checks approval BEFORE finalization so an approval gate -// that names its successor (`approve-merge`) resolves to approval. That -// precedence is encoded in the if-order of stepIdPhase, not here — the two -// concerns must not be conflated (a closed approval + closed finalization run -// has its furthest stage = finalization, while a single `approve-merge` step -// is classified as the approval phase). -const LIFECYCLE_RANK: Record = { - active: 0, - intake: 1, - implementation: 2, - review: 3, - approval: 4, - finalization: 5, - blocked: 6, - complete: 7, -}; - -function stageRank(phase: SharedRunPhase): number { - return LIFECYCLE_RANK[phase]; -} - -/** - * Deterministic step ordering: most-recently-updated first, then furthest stage, - * then step id. Date.parse('') is NaN, so when the run-detail snapshot adapter - * leaves every updated_at empty the timestamp comparison collapses to 0 and the - * stage / step-id tiebreakers make the pick deterministic instead of leaving it - * to the engine's NaN-comparator behavior (input-order-dependent). - */ -function byMostRecentThenStage(a: RunIssue, b: RunIssue): number { - const timeDelta = parseTimestamp(b.updated_at) - parseTimestamp(a.updated_at); - if (timeDelta !== 0) return timeDelta; - const aStep = stringValue(a.metadata?.['gc.step_id']); - const bStep = stringValue(b.metadata?.['gc.step_id']); - const rankDelta = stageRank(stepIdPhase(bStep)) - stageRank(stepIdPhase(aStep)); - if (rankDelta !== 0) return rankDelta; - return aStep < bStep ? -1 : aStep > bStep ? 1 : 0; -} - -function parseTimestamp(value: string): number { - const parsed = Date.parse(value); - return Number.isNaN(parsed) ? 0 : parsed; -} - -export function stepIssues(issues: RunIssue[], step: string): RunIssue[] { - return issues.filter((i) => stringValue(i.metadata?.['gc.step_id']) === step); -} - -export function isPrimaryStepIssue(issue: RunIssue): boolean { - const kind = stringValue(issue.metadata?.['gc.kind']); - return kind !== 'spec' && kind !== 'scope-check' && kind !== 'workflow-finalize'; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/runtime-state.ts b/internal/api/dashboardspa/web/shared/src/runs/runtime-state.ts deleted file mode 100644 index b28a00bd9e..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/runtime-state.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { DashboardBead } from '../dashboard-beads.js'; -import type { RunSnapshotBead, RunSnapshot } from '../run-snapshot.js'; -import { nonEmpty } from './bead-fields.js'; - -const PRESENTATION_METADATA_KEYS = [ - 'gc.outcome', - 'gc.cwd', - 'cwd', - 'gc.work_dir', - 'work_dir', - 'gc.rig_root', - 'rig_root', - 'session_id', - 'session_name', - 'gc.session_id', - 'gc.session_name', - 'gc.sessionId', - 'gc.sessionName', - 'rig_id', -] as const; - -/** - * The supervisor run snapshot carries compiled graph shape, but today its - * embedded bead rows can lag the canonical /beads runtime state. Merge only - * fields that affect presentation state so graph topology still comes from - * /run while run status comes from exact live supervisor bead reads. - */ -export function mergeRunRuntimeState( - raw: RunSnapshot, - runtimeBeads: readonly DashboardBead[], -): RunSnapshot { - if (!Array.isArray(raw.beads) || runtimeBeads.length === 0) return raw; - const runtimeById = new Map( - runtimeBeads - .map((bead) => [nonEmpty(bead.id), bead] as const) - .filter((entry): entry is readonly [string, DashboardBead] => entry[0] !== undefined), - ); - - return { - ...raw, - beads: raw.beads.map((bead) => mergeRunBead(bead, runtimeById.get(bead.id))), - }; -} - -function mergeRunBead(bead: RunSnapshotBead, runtime: DashboardBead | undefined): RunSnapshotBead { - if (!runtime) return bead; - const status = nonEmpty(runtime.status) ?? bead.status; - const assignee = nonEmpty(runtime.assignee) ?? bead.assignee; - const merged: RunSnapshotBead = { - ...bead, - status, - metadata: { - ...bead.metadata, - ...presentationMetadata(runtime.metadata), - }, - }; - if (assignee !== undefined) merged.assignee = assignee; - return merged; -} - -function presentationMetadata(metadata: DashboardBead['metadata']): Record { - if (!metadata) return {}; - const out: Record = {}; - for (const key of PRESENTATION_METADATA_KEYS) { - const value = metadata[key]; - const text = nonEmpty(value); - if (text) out[key] = text; - } - return out; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/session-link.test.ts b/internal/api/dashboardspa/web/shared/src/runs/session-link.test.ts deleted file mode 100644 index 39f1b50987..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/session-link.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { test, describe } from 'node:test'; -import assert from 'node:assert/strict'; - -import { runSessionLinkFor } from './session-link.js'; - -// Regression coverage for the rig-store / polecat run "invalid session id" -// bug. A polecat run records its session as the pool-qualified NAME -// (`polecat-gc-333573`), whose real supervisor session id is the gc-suffix -// (`gc-333573`). When the session has completed it is absent from the live -// session index, so it can't be resolved by name — the link must normalize -// the recorded value to the supervisor id itself, or the Session tab feeds a -// name where an id is expected and the route rejects it as "invalid session id". -describe('runSessionLinkFor — session id normalization', () => { - test('normalizes a pool-qualified session name in metadata to the supervisor id', () => { - const bead = { - assignee: 'polecat-gc-333573', - metadata: { session_id: 'polecat-gc-333573' }, - } as never; - const link = runSessionLinkFor(bead, 'done'); - assert.equal(link?.sessionId, 'gc-333573'); - }); - - test('leaves a clean gc-prefixed session id unchanged', () => { - const bead = { metadata: { session_id: 'gc-333573' } } as never; - const link = runSessionLinkFor(bead, 'done'); - assert.equal(link?.sessionId, 'gc-333573'); - }); - - test('derives the id from a pool-qualified assignee when no metadata id is present', () => { - const bead = { assignee: 'polecat-gc-333573' } as never; - const link = runSessionLinkFor(bead, 'done'); - assert.equal(link?.sessionId, 'gc-333573'); - }); - - test('degrades to no link when an unresolvable value carries no supervisor id', () => { - // A completed run whose recorded handle carries no extractable supervisor - // id and is absent from the live session index must NOT leak that handle - // into link.sessionId — the session route would reject it as "invalid - // session id". The link degrades so the Session tab shows a clean - // "session not available" empty state instead. - const bead = { metadata: { session_id: 'mystery-handle' } } as never; - assert.equal(runSessionLinkFor(bead, 'done'), undefined); - }); - - test('degrades a runtime-derived bare assignee that cannot yield a supervisor id', () => { - // The runtime-derived path: a completed pool/rig-store run records only an - // assignee (no session_id metadata), and that assignee is a bare worker - // name with no embedded supervisor id. With no live index match there is - // no usable id, so the link degrades rather than feeding "polecat" to the - // route as an "invalid session id". - const bead = { assignee: 'polecat' } as never; - assert.equal(runSessionLinkFor(bead, 'done'), undefined); - }); - - test('returns undefined for pending/ready nodes (no session yet)', () => { - const bead = { assignee: 'polecat-gc-333573' } as never; - assert.equal(runSessionLinkFor(bead, 'pending'), undefined); - assert.equal(runSessionLinkFor(bead, 'ready'), undefined); - }); -}); diff --git a/internal/api/dashboardspa/web/shared/src/runs/session-link.ts b/internal/api/dashboardspa/web/shared/src/runs/session-link.ts deleted file mode 100644 index 588092c030..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/session-link.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; -import type { DashboardSession } from '../dashboard-sessions.js'; -import type { RunNodeStatus, RunSessionLink } from '../run-detail.js'; -import { SESSION_ID_RE } from '../session-id.js'; -import { meta, nonEmpty } from './bead-fields.js'; - -export interface RunSessionIndex { - byId: Map; - byName: Map; - byTemplate: Map; -} - -export interface RunSessionLinkContext { - sessionIndex?: RunSessionIndex; - scopeRef?: string; -} - -export function buildRunSessionIndex(sessions: readonly DashboardSession[]): RunSessionIndex { - const byId = new Map(); - const byName = new Map(); - const byTemplate = new Map(); - - for (const session of sessions) { - remember(byId, session.id, session); - remember(byName, session.alias, session); - remember(byName, session.title, session); - remember(byName, session.session_name, session); - const template = nonEmpty(session.template); - if (template) byTemplate.set(template, [...(byTemplate.get(template) ?? []), session]); - } - - return { byId, byName, byTemplate }; -} - -export function runSessionLinkFor( - bead: RunSnapshotBead, - status: RunNodeStatus, - context: RunSessionLinkContext = {}, -): RunSessionLink | undefined { - if (status === 'pending' || status === 'ready') return undefined; - const assignee = nonEmpty(bead.assignee); - const sessionId = sessionIdFromBead(bead, assignee); - const sessionName = sessionNameFromBead(bead, assignee, sessionId); - if (!sessionId && !sessionName) return undefined; - const rawLink = rawLinkFrom(sessionId, sessionName, assignee); - const link = resolveRunSessionLink(rawLink, context.sessionIndex); - // Final gate: link.sessionId is fed straight to the supervisor session - // routes, which reject anything outside SESSION_ID_RE as "invalid session - // id". When the index could not resolve the run to a real session (a - // completed pool/rig-store run has dropped out of the live index) and the - // recorded handle carries no extractable supervisor id, drop the link so the - // Session tab degrades to a clean "session not available" state instead of - // leaking an unvalidated handle into the route. - if (!SESSION_ID_RE.test(link.sessionId)) return undefined; - return link; -} - -// The bead's recorded session id can be a pool-qualified session NAME -// (e.g. a polecat run records `polecat-gc-333573`, whose real supervisor -// session id is `gc-333573`). Normalize every candidate through -// supervisorSessionIdFrom so the link carries the id the session routes -// expect — otherwise a completed session (absent from the live index, so -// unresolvable by name) leaks its name into the id slot and the Session -// tab rejects it as "invalid session id". -function sessionIdFromBead( - bead: RunSnapshotBead, - assignee: string | undefined, -): string | undefined { - const rawSessionId = - meta(bead, 'session_id') ?? - meta(bead, 'gc.session_id') ?? - meta(bead, 'gc.sessionId') ?? - assignee; - return supervisorSessionIdFrom(rawSessionId) ?? rawSessionId; -} - -function sessionNameFromBead( - bead: RunSnapshotBead, - assignee: string | undefined, - sessionId: string | undefined, -): string | undefined { - return ( - meta(bead, 'session_name') ?? - meta(bead, 'gc.session_name') ?? - meta(bead, 'gc.sessionName') ?? - assignee ?? - sessionId - ); -} - -function rawLinkFrom( - sessionId: string | undefined, - sessionName: string | undefined, - assignee: string | undefined, -): RunSessionLink { - const name = sessionName ?? sessionId ?? ''; - return { - sessionId: sessionId ?? sessionName ?? '', - sessionName: name, - assignee: assignee ?? name, - }; -} - -function supervisorSessionIdFrom(value: string | undefined): string | undefined { - const clean = nonEmpty(value); - if (!clean) return undefined; - if (SESSION_ID_RE.test(clean)) return clean; - const suffix = clean.match(/(?:^|[-_/])((?:gc|td|th|[a-z]{4})-[a-z0-9-]{1,32})$/)?.[1]; - if (!suffix || !SESSION_ID_RE.test(suffix)) return undefined; - return suffix; -} - -function resolveRunSessionLink( - rawLink: RunSessionLink, - sessionIndex: RunSessionIndex | undefined, -): RunSessionLink { - if (!sessionIndex) return rawLink; - const session = resolveRunSessionSummary(rawLink, sessionIndex); - if (!session) return rawLink; - return linkForSession(session, rawLink); -} - -function resolveRunSessionSummary( - link: RunSessionLink, - sessionIndex: RunSessionIndex, -): DashboardSession | null { - for (const candidate of [link.sessionId, link.sessionName, link.assignee]) { - const key = nonEmpty(candidate); - if (!key) continue; - const exact = - sessionIndex.byId.get(key) ?? - sessionIndex.byName.get(key) ?? - uniquePreferredSession(sessionIndex.byTemplate.get(key) ?? []); - if (exact) return exact; - } - return null; -} - -function linkForSession(session: DashboardSession, rawLink: RunSessionLink): RunSessionLink { - return { - sessionId: session.id, - sessionName: - nonEmpty(session.alias) ?? - nonEmpty(session.title) ?? - nonEmpty(session.session_name) ?? - nonEmpty(session.template) ?? - rawLink.sessionName, - assignee: - rawLink.assignee || - nonEmpty(session.template) || - nonEmpty(session.alias) || - nonEmpty(session.title) || - nonEmpty(session.session_name) || - session.id, - }; -} - -function uniquePreferredSession(sessions: readonly DashboardSession[]): DashboardSession | null { - if (sessions.length === 0) return null; - const active = sessions.filter( - (session) => session.state === 'active' || session.running === true, - ); - if (active.length === 1) return active[0] ?? null; - if (sessions.length === 1) return sessions[0] ?? null; - return null; -} - -function remember( - store: Map, - key: string | undefined, - session: DashboardSession, -): void { - const clean = nonEmpty(key); - if (!clean || store.has(clean)) return; - store.set(clean, session); -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/status.ts b/internal/api/dashboardspa/web/shared/src/runs/status.ts deleted file mode 100644 index 0db61f16f3..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/status.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { RunSnapshotBead } from '../run-snapshot.js'; -import type { RunExecutionInstance, RunNodeStatus } from '../run-detail.js'; -import { meta, nonEmpty } from './bead-fields.js'; - -export function presentationStatus(bead: RunSnapshotBead): RunNodeStatus { - const raw = (nonEmpty(bead.status) ?? '').toLowerCase(); - const outcome = meta(bead, 'gc.outcome')?.toLowerCase(); - if (raw === 'closed' || raw === 'completed' || raw === 'done') { - if (outcome === 'fail' || outcome === 'failed') return 'failed'; - if (outcome === 'skipped') return 'skipped'; - return 'completed'; - } - if (raw === 'in_progress' || raw === 'active' || raw === 'running') { - return 'active'; - } - if (raw === 'blocked') return 'blocked'; - if (raw === 'ready') return 'ready'; - if (raw === 'failed') return 'failed'; - if (raw === 'skipped') return 'skipped'; - return 'pending'; -} - -export function aggregateStatus( - instances: RunExecutionInstance[], - visibleInstance: RunExecutionInstance | undefined, -): RunNodeStatus { - if (instances.some((instance) => isRunningStatus(instance.status))) { - return 'active'; - } - if (visibleInstance?.status) return visibleInstance.status; - return 'pending'; -} - -export function isRunningStatus(status: RunNodeStatus | undefined): boolean { - return status === 'active' || status === 'running'; -} diff --git a/internal/api/dashboardspa/web/shared/src/runs/summary.test.ts b/internal/api/dashboardspa/web/shared/src/runs/summary.test.ts deleted file mode 100644 index 78287954bc..0000000000 --- a/internal/api/dashboardspa/web/shared/src/runs/summary.test.ts +++ /dev/null @@ -1,519 +0,0 @@ -import { test, describe } from 'node:test'; -import assert from 'node:assert/strict'; - -import { - buildRunSummary, - emptyRunSummary, - runLane, - MAX_HISTORICAL_LANES, - MAX_VISIBLE_ACTIVE_LANES, -} from './summary.js'; -import type { RunFeedScope } from './summary.js'; -import type { RunIssue } from './phaseMapping.js'; - -// Active-classification regression coverage (gascity-dashboard-4xcv §2). -// Operator repro gc-1920: a long-stale formula latch with status=blocked -// (city-level, no live session) was counted and shown in the ACTIVE lane. -// A blocked run still needs operator attention, but it is not progressing: -// it belongs in its own blocked bucket, never in the Active set or count. - -function latch(overrides: Partial = {}): RunIssue { - // Shape mirrors a real mol-focus-review latch root (gc-1920-class bead): - // a single graph.v2 root task with no step children. - return { - id: 'gc-1920', - title: 'mol-focus-review', - description: 'Focus + in-session review formula. Approval gate, review.', - status: 'blocked', - issue_type: 'task', - updated_at: '2026-04-01T00:00:00Z', - metadata: { - 'gc.formula_contract': 'graph.v2', - 'gc.kind': 'workflow', - }, - ...overrides, - }; -} - -function activeRun(id: string): RunIssue[] { - return [ - latch({ - id, - title: 'Adopt PR #42', - description: 'implementation work', - status: 'open', - updated_at: '2026-06-01T00:00:00Z', - metadata: { 'gc.formula_contract': 'graph.v2', 'gc.kind': 'run' }, - }), - { - id: `${id}-step-1`, - title: 'Implementation patch', - status: 'in_progress', - issue_type: 'task', - updated_at: '2026-06-01T00:05:00Z', - metadata: { - 'gc.kind': 'step', - 'gc.root_bead_id': id, - 'gc.step_id': 'implementation.patch', - }, - }, - ]; -} - -function completedRun(id: string): RunIssue[] { - return [ - latch({ - id, - title: 'Done run', - description: 'merge finalize', - status: 'closed', - updated_at: '2026-05-01T00:00:00Z', - metadata: { 'gc.formula_contract': 'graph.v2', 'gc.kind': 'run' }, - }), - ]; -} - -describe('buildRunSummary — blocked lanes are not Active (gascity-dashboard-4xcv)', () => { - test('a blocked formula latch lands in blockedLanes, not lanes or totalActive', () => { - const summary = buildRunSummary([latch(), ...activeRun('run-1')]); - - assert.equal(summary.totalActive, 1); - assert.deepEqual( - summary.lanes.map((lane) => lane.id), - ['run-1'], - ); - assert.deepEqual( - summary.blockedLanes.map((lane) => lane.id), - ['gc-1920'], - ); - assert.equal(summary.runCounts.total, 1); - assert.equal(summary.runCounts.blocked, 1); - }); - - test('blocked lanes are not historical either', () => { - const summary = buildRunSummary([latch(), ...completedRun('run-done')]); - - assert.equal(summary.totalActive, 0); - assert.equal(summary.totalHistorical, 1); - assert.deepEqual( - summary.historicalLanes.map((lane) => lane.id), - ['run-done'], - ); - assert.deepEqual( - summary.blockedLanes.map((lane) => lane.id), - ['gc-1920'], - ); - }); - - test('with no blocked lanes, blockedLanes is empty and blocked count is zero', () => { - const summary = buildRunSummary(activeRun('run-1')); - - assert.deepEqual(summary.blockedLanes, []); - assert.equal(summary.runCounts.blocked, 0); - }); - - test('emptyRunSummary carries an empty blockedLanes array', () => { - assert.deepEqual(emptyRunSummary().blockedLanes, []); - }); -}); - -// gascity-dashboard-s4rp: a run rooted at a bead missing from the store -// (dangling root, gc-1920-class — id ~1920 vs a current store at ~346k) has no -// authoritative root metadata. Its only members are orphan step beads pointing -// at the absent root, so it must never be surfaced as a live lane. -describe('buildRunSummary — dangling-root groups are not surfaced (gascity-dashboard-s4rp)', () => { - function orphanStep(rootId: string): RunIssue { - return { - id: `${rootId}-step-1`, - title: 'Implementation patch', - status: 'in_progress', - issue_type: 'task', - updated_at: '2026-06-01T00:05:00Z', - metadata: { - 'gc.kind': 'step', - 'gc.formula_contract': 'graph.v2', - 'gc.root_bead_id': rootId, - 'gc.step_id': 'implementation.patch', - }, - }; - } - - test('a group whose root bead is absent does not appear anywhere', () => { - const summary = buildRunSummary([orphanStep('gc-1920'), ...activeRun('run-1')]); - - assert.equal(summary.totalActive, 1); - assert.deepEqual( - summary.lanes.map((lane) => lane.id), - ['run-1'], - ); - assert.deepEqual(summary.blockedLanes, []); - assert.deepEqual(summary.historicalLanes, []); - assert.equal(summary.runCounts.total, 1); - }); - - // gascity-dashboard-2j8e.2: the Runs badge counts selectBlockedRuns over - // blockedLanes, so a dangling-root group whose orphan step is BLOCKED must - // not reach blockedLanes — else the phantom inflates the badge. Pins the - // #87/#89 intent (suppress phantom roots with no backing bead) for the - // badge's source, not just the Active set. - function blockedOrphanStep(rootId: string): RunIssue { - return { - id: `${rootId}-step-1`, - title: 'Blocked patch', - status: 'blocked', - issue_type: 'task', - updated_at: '2026-06-01T00:05:00Z', - metadata: { - 'gc.kind': 'step', - 'gc.formula_contract': 'graph.v2', - 'gc.root_bead_id': rootId, - 'gc.step_id': 'implementation.patch', - }, - }; - } - - test('a dangling-root group with a blocked step never reaches blockedLanes', () => { - const summary = buildRunSummary([blockedOrphanStep('gc-1920'), ...activeRun('run-1')]); - - assert.deepEqual(summary.blockedLanes, []); - assert.equal(summary.runCounts.blocked, 0); - }); -}); - -// gascity-dashboard-5e5v: supervisor-controlled rig/scope refs are rendered -// verbatim by run-summary consumers (the web frontend, and any terminal client -// that emits DTO strings into the operator's terminal — Ink tokenises ANSI but -// does not strip escape/OSC). A hostile or malformed `gc.root_store_ref` or a -// feed-scope ref carrying ANSI/OSC/bidi must be stripped at the DTO edge. - -function runIssue(overrides: Partial & Pick): RunIssue { - return { - title: 'run', - status: 'in_progress', - issue_type: 'task', - updated_at: '2026-06-06T00:00:00.000Z', - ...overrides, - }; -} - -// gascity-dashboard-9w3k: v1 / wisp (non-graph.v2) runs are surfaced as lanes -// using the same lane/stage primitives as graph.v2. The drop used to be at the -// graph.v2-only keep-filter, which silently swallowed the entire v1 history. -// These tests pin (a) a genuine v1 wisp run becomes exactly one lane, (b) a -// lone engineering bead is NOT promoted into a lane (flood guard), and that -// graph.v2 lanes are unchanged. -describe('buildRunSummary — v1 / wisp runs surface as lanes (gascity-dashboard-9w3k)', () => { - // A real wisp run: a `molecule` root carrying gc.var.* template inputs but no - // gc.formula_contract, plus a child `task` whose metadata.molecule_id points - // back at the root. runRootId groups the child under the molecule root. - function wispRun(id: string): RunIssue[] { - return [ - runIssue({ - id, - title: 'mol-do-work', - status: 'open', - issue_type: 'molecule', - updated_at: '2026-06-02T00:00:00Z', - metadata: { 'gc.var.target': 'demo-app', 'gc.var.prompt': 'fix the thing' }, - }), - runIssue({ - id: `${id}-child-1`, - title: 'Implementation work', - updated_at: '2026-06-02T00:05:00Z', - metadata: { molecule_id: id, 'gc.step_id': 'do-work' }, - }), - ]; - } - - test('a v1 wisp molecule run emits exactly one lane with a phase', () => { - const summary = buildRunSummary(wispRun('wisp-1')); - - assert.deepEqual( - summary.lanes.map((lane) => lane.id), - ['wisp-1'], - ); - assert.equal(summary.totalActive, 1); - const lane = summary.lanes[0]; - assert.ok(lane !== undefined); - assert.equal(lane.id, 'wisp-1'); - // mapRunPhase is generic: 'work'/'implementation' text yields a real phase. - assert.equal(lane.phase, 'implementation'); - // No graph.v2 formula metadata, so the formula resolves to unavailable but - // the generic run stages still render. - assert.equal(lane.formula.status, 'unavailable'); - assert.ok(lane.stages.length > 0); - }); - - test('graph.v2 lanes still render unchanged (regression)', () => { - const summary = buildRunSummary(activeRun('run-1')); - - assert.deepEqual( - summary.lanes.map((lane) => lane.id), - ['run-1'], - ); - assert.equal(summary.totalActive, 1); - }); - - // gascity-dashboard-9w3k: a wisp run's gc.var.* free-text template inputs must - // NOT drive phase classification. Here gc.var.prompt contains 'review', - // 'blocked', and 'merge' — phase needles that would mis-bucket the run as - // blocked/approval/finalization — but the run's actual work (a 'do-work' step) - // keeps it in implementation/active. - test('gc.var.* free-text does not mis-classify a wisp run as blocked/approval', () => { - const summary = buildRunSummary([ - runIssue({ - id: 'wisp-var', - title: 'mol-do-work', - status: 'open', - issue_type: 'molecule', - updated_at: '2026-06-02T00:00:00Z', - metadata: { - 'gc.var.prompt': 'review the blocked PR and merge it after approval, then finalize', - }, - }), - runIssue({ - id: 'wisp-var-child-1', - title: 'Do the work', - updated_at: '2026-06-02T00:05:00Z', - metadata: { molecule_id: 'wisp-var', 'gc.step_id': 'do-work' }, - }), - ]); - - const lane = summary.lanes[0]; - assert.ok(lane !== undefined); - assert.equal(lane.id, 'wisp-var'); - assert.notEqual(lane.phase, 'blocked'); - assert.notEqual(lane.phase, 'approval'); - assert.equal(summary.blockedLanes.length, 0); - // The 'do-work' step keeps it in the implementation register. - assert.equal(lane.phase, 'implementation'); - }); - - // Flood guard: a lone task/bug/feature bead (root = itself, no molecule / - // gc.kind=run / gc.formula) must NOT become a run lane — otherwise every - // engineering bead in the store would render as a phantom run. - test('a lone engineering bead does not become a lane', () => { - const loneTask: RunIssue = { - id: 'task-99', - title: 'Fix a typo', - status: 'open', - issue_type: 'task', - updated_at: '2026-06-02T00:00:00Z', - }; - const summary = buildRunSummary([loneTask]); - - assert.deepEqual(summary.lanes, []); - assert.deepEqual(summary.historicalLanes, []); - assert.deepEqual(summary.blockedLanes, []); - assert.equal(summary.totalActive, 0); - assert.equal(summary.totalHistorical, 0); - }); -}); - -// gascity-dashboard: the builder carries the FULL active set on the wire — the -// rendered 8-lane collapse is applied by the consumer (RunMap), mirroring the -// historical section. The builder must NOT pre-cap `lanes`. -describe('buildRunSummary — active lanes carry the full set (component-collapsed)', () => { - test('lanes carries every active run when there are more than MAX_VISIBLE_ACTIVE_LANES', () => { - const count = MAX_VISIBLE_ACTIVE_LANES + 3; - const issues = Array.from({ length: count }, (_, i) => activeRun(`run-${i}`)).flat(); - const summary = buildRunSummary(issues); - - assert.equal(summary.totalActive, count); - assert.equal(summary.lanes.length, count); - assert.equal(summary.runCounts.total, count); - assert.equal(summary.runCounts.visible, count); - }); -}); - -// gascity-dashboard-9w3k (part b): the now-much-larger v1 history can bury -// active runs in the historical set on the wire. Cap the historical lanes the -// builder emits to the most-recent MAX_HISTORICAL_LANES, while totalHistorical -// keeps reporting the true full count. -describe('buildRunSummary — historical lanes are recency-bounded (gascity-dashboard-9w3k)', () => { - test('caps historicalLanes at MAX_HISTORICAL_LANES, keeps the most-recent ones, reports true total', () => { - const total = MAX_HISTORICAL_LANES + 10; - const issues: RunIssue[] = []; - // Distinct, monotonically increasing updated_at so newest-first order is - // unambiguous. id-N has updated_at = base + N minutes; higher N = newer. - // A closed molecule root is a complete v1 run (one lane each). - const base = Date.parse('2026-01-01T00:00:00Z'); - for (let n = 0; n < total; n += 1) { - const at = new Date(base + n * 60_000).toISOString(); - issues.push( - runIssue({ - id: `hist-${String(n).padStart(3, '0')}`, - issue_type: 'molecule', - status: 'closed', - updated_at: at, - }), - ); - } - - const summary = buildRunSummary(issues); - - assert.equal(summary.historicalLanes.length, MAX_HISTORICAL_LANES); - assert.equal(summary.totalHistorical, total); - - // The retained lanes must be the newest N (highest n), newest first. - const expected = Array.from({ length: MAX_HISTORICAL_LANES }, (_, i) => { - const n = total - 1 - i; - return `hist-${String(n).padStart(3, '0')}`; - }); - assert.deepEqual( - summary.historicalLanes.map((lane) => lane.id), - expected, - ); - }); -}); - -// gascity-dashboard-km0w: live run-root beads carry gc.root_store_ref -// ("rig:gascity-packs") plus gc.var.rig_name, but NOT gc.scope_kind / -// gc.scope_ref. Before this fix the scope derived to unavailable for EVERY -// run, so runDetailHref omitted the scope query and the detail fetch hit the -// supervisor at the default city scope (12-14s full-store scan then 404). -// The scope must be recovered from gc.root_store_ref when the explicit -// gc.scope_ref pair is absent, while gc.scope_ref stays primary when present. -describe('runLane scope derives from gc.root_store_ref fallback — gascity-dashboard-km0w', () => { - function rootOnly(id: string, metadata: Record): RunIssue[] { - return [ - runIssue({ - id, - title: 'mol-focus-review', - issue_type: 'molecule', - metadata, - }), - ]; - } - - test('derives rig scope from gc.root_store_ref when gc.scope_ref is absent', () => { - const lane = runLane( - 'gpk-4fyo6', - rootOnly('gpk-4fyo6', { 'gc.root_store_ref': 'rig:gascity-packs' }), - new Map(), - ); - - assert.equal(lane.scope.status, 'available'); - if (lane.scope.status !== 'available') return; - assert.equal(lane.scope.kind, 'rig'); - assert.equal(lane.scope.ref, 'gascity-packs'); - assert.equal(lane.scope.rootStoreRef, 'rig:gascity-packs'); - }); - - test('derives city scope from a city: gc.root_store_ref', () => { - const lane = runLane( - 'city-run', - rootOnly('city-run', { 'gc.root_store_ref': 'city:ds-research' }), - new Map(), - ); - - assert.equal(lane.scope.status, 'available'); - if (lane.scope.status !== 'available') return; - assert.equal(lane.scope.kind, 'city'); - assert.equal(lane.scope.ref, 'ds-research'); - }); - - test('explicit gc.scope_ref still wins over gc.root_store_ref', () => { - const lane = runLane( - 'mixed', - rootOnly('mixed', { - 'gc.scope_kind': 'rig', - 'gc.scope_ref': 'gascity-dashboard', - 'gc.root_store_ref': 'rig:gascity-packs', - }), - new Map(), - ); - - assert.equal(lane.scope.status, 'available'); - if (lane.scope.status !== 'available') return; - assert.equal(lane.scope.kind, 'rig'); - assert.equal(lane.scope.ref, 'gascity-dashboard'); - // rootStoreRef is still carried through verbatim. - assert.equal(lane.scope.rootStoreRef, 'rig:gascity-packs'); - }); - - test('an unknown-prefix gc.root_store_ref is not guessed — scope unavailable', () => { - const lane = runLane( - 'bad-prefix', - rootOnly('bad-prefix', { 'gc.root_store_ref': 'workspace:ds-research' }), - new Map(), - ); - - assert.equal(lane.scope.status, 'unavailable'); - }); - - test('a colon-less gc.root_store_ref is not guessed — scope unavailable', () => { - const lane = runLane( - 'no-colon', - rootOnly('no-colon', { 'gc.root_store_ref': 'gascity-packs' }), - new Map(), - ); - - assert.equal(lane.scope.status, 'unavailable'); - }); - - test('a malformed parsed ref (fails SCOPE_REF_RE) is rejected — scope unavailable', () => { - const lane = runLane( - 'malformed', - // leading '-' violates SCOPE_REF_RE (must start alnum). - rootOnly('malformed', { 'gc.root_store_ref': 'rig:-bad ref' }), - new Map(), - ); - - assert.equal(lane.scope.status, 'unavailable'); - }); - - test('a control sequence in the derived ref fails SCOPE_REF_RE — scope unavailable', () => { - // The fallback validates the parsed ref against SCOPE_REF_RE BEFORE the DTO - // edge, so an injected ESC byte (which the pattern rejects) yields - // unavailable rather than a sanitised-but-spoofed ref. - const lane = runLane( - 'sanitise', - rootOnly('sanitise', { 'gc.root_store_ref': 'rig:gascity-packs\x1b[31m' }), - new Map(), - ); - - assert.equal(lane.scope.status, 'unavailable'); - }); -}); - -describe('runLane scope sanitisation — gascity-dashboard-5e5v', () => { - test('strips ANSI/OSC from rootStoreRef on the metadata path', () => { - const lane = runLane( - 'root-1', - [ - runIssue({ - id: 'root-1', - metadata: { - 'gc.scope_kind': 'rig', - 'gc.scope_ref': 'demo-app', - 'gc.root_store_ref': 'rig:demo-app\x1b]0;evil-title\x07\x1b[31m', - }, - }), - ], - new Map(), - ); - - assert.equal(lane.scope.status, 'available'); - if (lane.scope.status !== 'available') return; - assert.equal(lane.scope.rootStoreRef, 'rig:demo-app'); - assert.ok(!lane.scope.rootStoreRef.includes('\x1b'), 'no ESC byte may survive'); - assert.equal(lane.scope.ref, 'demo-app'); - }); - - test('strips ANSI/bidi from both ref and rootStoreRef on the feed-scope path', () => { - const feedScope: RunFeedScope = { - scopeKind: 'rig', - scopeRef: 'demo\x1b[1m‮app', - rootStoreRef: 'rig:demo\x1b[1mapp', - }; - const lane = runLane('root-2', [runIssue({ id: 'root-2' })], new Map([['root-2', feedScope]])); - - assert.equal(lane.scope.status, 'available'); - if (lane.scope.status !== 'available') return; - assert.equal(lane.scope.ref, 'demoapp'); - assert.equal(lane.scope.rootStoreRef, 'rig:demoapp'); - assert.ok(!lane.scope.ref.includes('\x1b'), 'no ESC byte may survive in ref'); - assert.ok(!lane.scope.rootStoreRef.includes('\x1b'), 'no ESC byte may survive in rootStoreRef'); - }); -}); diff --git a/internal/api/dashboardspa/web/shared/src/runs/summary.ts b/internal/api/dashboardspa/web/shared/src/runs/summary.ts index 614133cd74..de01561969 100644 --- a/internal/api/dashboardspa/web/shared/src/runs/summary.ts +++ b/internal/api/dashboardspa/web/shared/src/runs/summary.ts @@ -1,510 +1,6 @@ -import type { RunChange, RunCounts, RunLane, RunSummary, RunStage } from '../snapshot/types.js'; -import { fromRootMetadataScope } from '../run-scope.js'; -import { stripNonPrintable } from '../strip-non-printable.js'; -import { resolveRunFormulaIdentity } from './formula-name.js'; -import { isDanglingRootGroup } from './liveness.js'; -import { - isPrimaryStepIssue, - latestStepId, - mapRunPhase, - reviewRoundForIssues, - stageProgress, - stagesForFormula, - stepIssues, - stringValue, - type RunIssue, -} from './phaseMapping.js'; - -// Default collapsed active-lane count (component-controlled). The wire carries -// the FULL active set in `lanes`; RunMap renders this many by default and offers -// a "Show N more runs" expander (mirroring historicalLanes/MAX_HISTORICAL_LANES). +// The run-summary FOLD (buildRunSummary, counts, lane/scope/phase derivation, +// enrichment) moved to Go (internal/runproj); the dashboard renders the +// server-computed RunSummary DTO. This window size is the one presentation knob +// the renderer keeps: the wire carries the FULL active set in `lanes`, and +// RunMap renders this many by default behind a "Show N more runs" expander. export const MAX_VISIBLE_ACTIVE_LANES = 8; -export const RECENT_CHANGES_CAP = 12; -// gascity-dashboard-9w3k: once v1 history is surfaced the completed set can grow -// into the thousands. Cap the historical lanes carried on the wire to the most- -// recent N (sortedLanes is already newest-first) so a long tail of old runs -// cannot bury or out-pay the active set. totalHistorical still reports the full -// count so the operator sees the true number behind the window. -export const MAX_HISTORICAL_LANES = 50; -const ENGINEERING_TYPES = new Set([ - 'feature', - 'bug', - 'task', - 'epic', - 'chore', - 'decision', - 'molecule', -]); - -export interface RunFeedScope { - scopeKind: 'city' | 'rig'; - scopeRef: string; - rootStoreRef: string; -} - -export type RunFeedScopeMap = ReadonlyMap; - -export function buildRunSummary( - issues: RunIssue[], - feedScopes: RunFeedScopeMap = new Map(), - partial = false, -): RunSummary { - const groups = new Map(); - - for (const issue of issues) { - const rootId = runRootId(issue); - const group = groups.get(rootId) ?? []; - group.push(issue); - groups.set(rootId, group); - } - - const runGroups = Array.from(groups.entries()).filter( - ([rootId, groupIssues]) => - // gascity-dashboard-s4rp: a run rooted at a bead missing from the store - // (dangling root, gc-1920-class) has no authoritative root metadata — its - // title is inferred from a child and its scope is unresolvable. Drop it - // explicitly rather than rely on the run-marker check incidentally failing. - !isDanglingRootGroup(rootId, groupIssues) && isRunGroup(rootId, groupIssues), - ); - const laneIssues = runGroups.flatMap(([, groupIssues]) => groupIssues); - const sortedLanes = runGroups - .map(([rootId, groupIssues]) => runLane(rootId, groupIssues, feedScopes)) - .sort(compareLanes); - - // gascity-dashboard-4xcv: blocked lanes are split out of Active. A stale - // blocked formula latch (gc-1920 repro) is not progressing; it surfaces in - // its own section instead of inflating the Active set. - const activeLanes = sortedLanes.filter( - (lane) => lane.phase !== 'complete' && lane.phase !== 'blocked', - ); - const completedLanes = sortedLanes.filter((lane) => lane.phase === 'complete'); - // gascity-dashboard-9w3k: cap on the wire, but keep the FULL count for the DTO. - const totalHistorical = completedLanes.length; - const historicalLanes = completedLanes.slice(0, MAX_HISTORICAL_LANES); - const blockedLanes = sortedLanes.filter((lane) => lane.phase === 'blocked'); - - // gascity-dashboard-s4rp: `lanes` carries the FULL active set, not a capped - // window. Session-less-latch demotion (enrichRunSummary) is session-aware and - // can only run downstream of this builder, so it must see every active lane to - // recompute totalActive exactly — capping here would hide phantoms beyond the - // 8th slot from demotion and leave them in the count. RunMap owns the rendered - // collapse (default MAX_VISIBLE_ACTIVE_LANES) and its "Show N more" expander, - // mirroring the historical section — so the wire is never pre-capped. - const summary: RunSummary = { - totalActive: activeLanes.length, - totalHistorical, - runCounts: runCounts(activeLanes, activeLanes.length, blockedLanes.length), - lanes: activeLanes, - historicalLanes, - blockedLanes, - recentChanges: recentChanges(laneIssues), - census: runCensusUnavailable(), - }; - return partial ? { ...summary, lanesPartial: true } : summary; -} - -// gascity-dashboard-9w3k: a group is a run when its root bead carries a run -// marker — the graph.v2 `gc.formula_contract`, or a v1 / wisp signal (a -// molecule bead, an explicit `gc.kind=run` marker, or a `gc.formula` -// attribution). The v1 arms are the flood guard: a lone engineering bead -// (root = itself with none of these markers) must NOT be promoted to a lane, -// or every task/bug/feature in the store would render as a phantom run. -// Convoys are already excluded upstream by runBeadFilter (ENGINEERING_TYPES -// has no 'convoy'); dangling roots are dropped separately by the caller. -function isRunGroup(rootId: string, issues: RunIssue[]): boolean { - const root = issues.find((issue) => issue.id === rootId); - if (!root) return false; - const metadata = root.metadata; - return ( - stringValue(metadata?.['gc.formula_contract']) === 'graph.v2' || - root.issue_type === 'molecule' || - stringValue(metadata?.['gc.kind']) === 'run' || - stringValue(metadata?.['gc.formula']) !== '' - ); -} - -export function runCounts(lanes: RunLane[], visible: number, blocked: number): RunCounts { - const counts: RunCounts = { - total: lanes.length, - visible, - prReview: 0, - designReview: 0, - bugfix: 0, - blocked, - other: 0, - }; - - for (const lane of lanes) { - switch (runKind(lane.formula)) { - case 'prReview': - counts.prReview += 1; - break; - case 'designReview': - counts.designReview += 1; - break; - case 'bugfix': - counts.bugfix += 1; - break; - case 'other': - counts.other += 1; - break; - } - } - - return counts; -} - -export function runKind( - formula: RunLane['formula'], -): 'prReview' | 'designReview' | 'bugfix' | 'other' { - const formulaName = runFormulaName(formula); - if (formulaName === 'mol-adopt-pr-v2') return 'prReview'; - if (formulaName === 'mol-design-review-v2') return 'designReview'; - if ( - formulaName === 'mol-bug-report-flow-v2' || - formulaName === 'mol-bug-report-implementation-v2' - ) { - return 'bugfix'; - } - return 'other'; -} - -export function runLane(rootId: string, issues: RunIssue[], feedScopes: RunFeedScopeMap): RunLane { - const phase = mapRunPhase(issues); - const updatedAt = latestUpdatedAt(issues); - const formula = runFormula(rootId, issues); - const formulaName = runFormulaName(formula); - const stages = stageProgress(phase, formulaName, issues); - const foundStageIndex = stages.findIndex((stage) => stage.status === 'active'); - const activeStage = foundStageIndex >= 0 ? stages[foundStageIndex] : undefined; - - const primaryInProgress = issues.filter( - (issue) => isPrimaryStepIssue(issue) && issue.status === 'in_progress', - ); - const activeStepId = latestStepId(primaryInProgress); - const progress = runProgress(stages, foundStageIndex, activeStepId, issues); - - const formulaStages = stagesForFormula(formulaName); - const formulaStageResolved = - formulaStages.length > 0 && - progress.status === 'active_step' && - formulaStages.some((stage) => stage.steps.includes(progress.stepId)); - const scope = runScope(rootId, issues, feedScopes); - - return { - id: rootId, - title: displayTitle(rootId, issues), - formula, - scope, - external: externalReference(issues), - phase: phase.phase, - phaseLabel: formula.status === 'known' ? (activeStage?.label ?? phase.label) : phase.label, - statusCounts: statusCounts(issues), - activeAssignees: activeAssignees(issues), - updatedAt, - stages, - progress, - formulaStageResolved, - health: runHealthUnavailable(), - }; -} - -export function runRootId(issue: RunIssue): string { - const sourceRoot = sourceRunRootId(issue); - if (sourceRoot) return sourceRoot; - - const metadata = issue.metadata ?? {}; - const explicitRoot = stringValue(metadata['gc.root_bead_id']); - if (explicitRoot) return explicitRoot; - - if (stringValue(metadata['gc.kind']) === 'run' || issue.issue_type === 'molecule') { - return issue.id; - } - - const moleculeId = stringValue(metadata.molecule_id); - if (moleculeId) return moleculeId; - - return issue.id; -} - -function runScope( - rootId: string, - issues: RunIssue[], - feedScopes: RunFeedScopeMap, -): RunLane['scope'] { - const root = issues.find((issue) => issue.id === rootId); - const ordered = root ? [root, ...issues.filter((issue) => issue !== root)] : issues; - const rootStoreRef = metadataString(ordered, 'gc.root_store_ref'); - const metadataScope = fromRootMetadataScope({ - ...(root?.metadata ?? {}), - ...(rootStoreRef ? { 'gc.root_store_ref': rootStoreRef } : {}), - 'gc.scope_ref': - stringValue(root?.metadata?.['gc.scope_ref']) || metadataString(ordered, 'gc.scope_ref'), - }); - - if (metadataScope !== null) { - return availableScope( - metadataScope.scopeKind, - metadataScope.scopeRef, - metadataScope.rootStoreRef, - ); - } - - const feedScope = feedScopes.get(rootId); - if (feedScope !== undefined) { - return availableScope( - feedScope.scopeKind, - feedScope.scopeRef, - rootStoreRef || feedScope.rootStoreRef, - ); - } - - return { - status: 'unavailable', - error: 'run scope metadata unavailable', - }; -} - -// gascity-dashboard-5e5v: the single edge where supervisor-controlled rig/scope -// refs enter the DTO. Strip ANSI/OSC/control/bidi here so every consumer of the -// run summary (web frontend, any terminal client that renders DTO strings -// verbatim) is covered at one choke point — a hostile or malformed -// `gc.root_store_ref` reaches here unvalidated (scopeRef itself is already -// constrained by SCOPE_REF_RE upstream, but the feed-scope map is not -// re-validated, so both fields are sanitised for a uniform guarantee). -function availableScope(kind: 'city' | 'rig', ref: string, rootStoreRef: string): RunLane['scope'] { - return { - status: 'available', - kind, - ref: stripNonPrintable(ref), - rootStoreRef: stripNonPrintable(rootStoreRef), - }; -} - -function sourceRunRootId(issue: RunIssue): string { - return ( - stringValue(issue.metadata?.['pr_review.run_root_id']) || - stringValue(issue.metadata?.['pr_review.workflow_root_id']) || - stringValue(issue.metadata?.['bugflow.active_run_id']) || - stringValue(issue.metadata?.['bugflow.implementation_run_id']) || - stringValue(issue.metadata?.['bugflow.implementation_workflow_id']) || - stringValue(issue.metadata?.['design_review.run_root_id']) || - stringValue(issue.metadata?.['design_review.workflow_root_id']) - ); -} - -function runFormula(rootId: string, issues: RunIssue[]): RunLane['formula'] { - const root = issues.find((issue) => issue.id === rootId); - const resolved = resolveRunFormulaIdentity('lane', { root, issues }); - if (resolved.name !== null) return { status: 'known', name: resolved.name }; - - return { status: 'unavailable', error: 'run formula unavailable' }; -} - -function runFormulaName(formula: RunLane['formula']): string | null { - return formula.status === 'known' ? formula.name : null; -} - -export function displayTitle(rootId: string, issues: RunIssue[]): string { - const prTitle = metadataString(issues, 'pr_review.github_title'); - const prNumber = metadataString(issues, 'pr_review.pr_number'); - if (prTitle && prNumber) { - return `PR #${prNumber}: ${prTitle}`; - } - - const issueUrl = metadataString(issues, 'bugflow.github_issue_url'); - const issueNumber = metadataString(issues, 'bugflow.github_issue_number'); - if (issueUrl && issueNumber) { - return `Issue #${issueNumber}: ${issues[0]?.title ?? rootId}`; - } - - const root = issues.find((i) => i.id === rootId); - return root?.title ?? issues[0]?.title ?? rootId; -} - -export function statusCounts(issues: RunIssue[]): Record { - return issues.reduce>((counts, i) => { - counts[i.status] = (counts[i.status] ?? 0) + 1; - return counts; - }, {}); -} - -export function activeAssignees(issues: RunIssue[]): string[] { - return Array.from( - new Set( - issues - .filter((i) => i.status !== 'closed') - .map((i) => i.assignee?.trim()) - .filter((a): a is string => Boolean(a)), - ), - ).sort(); -} - -export function latestUpdatedAt(issues: RunIssue[]): RunLane['updatedAt'] { - const at = issues - .map((i) => i.updated_at) - .filter(Boolean) - .sort((a, b) => Date.parse(b) - Date.parse(a))[0]; - - return at === undefined - ? { status: 'unavailable', error: 'run update time unavailable' } - : { status: 'available', at }; -} - -export function recentChanges(issues: RunIssue[]): RunChange[] { - return [...issues] - .filter((i) => i.updated_at) - .sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)) - .slice(0, RECENT_CHANGES_CAP) - .map((i) => ({ - id: i.id, - title: i.title, - status: i.status, - updatedAt: i.updated_at, - })); -} - -export function compareLanes(a: RunLane, b: RunLane): number { - const aTime = a.updatedAt.status === 'available' ? Date.parse(a.updatedAt.at) : 0; - const bTime = b.updatedAt.status === 'available' ? Date.parse(b.updatedAt.at) : 0; - return bTime - aTime || a.id.localeCompare(b.id); -} - -export function externalReference(issues: RunIssue[]): RunLane['external'] { - const label = externalLabel(issues); - const url = externalUrl(issues); - if (label !== null && url !== null) { - return { status: 'available', label, url }; - } - if (label !== null) { - return { status: 'label_only', label }; - } - return { status: 'unavailable', error: 'external reference unavailable' }; -} - -export function externalUrl(issues: RunIssue[]): string | null { - const raw = - metadataString(issues, 'pr_review.pr_url') || - metadataString(issues, 'bugflow.github_issue_url'); - return raw && /^https?:\/\//i.test(raw) ? raw : null; -} - -export function externalLabel(issues: RunIssue[]): string | null { - const prNumber = metadataString(issues, 'pr_review.pr_number'); - if (prNumber) return `PR #${prNumber}`; - const issueNumber = metadataString(issues, 'bugflow.github_issue_number'); - if (issueNumber) return `Issue #${issueNumber}`; - return ( - metadataString(issues, 'pr_review.external_ref') || - metadataString(issues, 'bugflow.external_ref') || - null - ); -} - -export function metadataString(issues: RunIssue[], key: string): string { - return issues.map((i) => stringValue(i.metadata?.[key])).find(Boolean) ?? ''; -} - -export function emptyRunSummary(): RunSummary { - return { - totalActive: 0, - totalHistorical: 0, - runCounts: { - total: 0, - visible: 0, - prReview: 0, - designReview: 0, - bugfix: 0, - blocked: 0, - other: 0, - }, - lanes: [], - historicalLanes: [], - blockedLanes: [], - recentChanges: [], - census: runCensusUnavailable(), - }; -} - -export function runCensusUnavailable(): RunSummary['census'] { - return { - status: 'unavailable', - error: 'run health has not been derived', - }; -} - -export function runHealthUnavailable(): RunLane['health'] { - return { - status: 'unavailable', - error: 'run health has not been derived', - }; -} - -export function runBeadFilter(bead: { - issue_type: string; - labels?: string[]; - metadata?: Record; -}): boolean { - if (Array.isArray(bead.labels) && bead.labels.some((l) => l.startsWith('gc:'))) { - return false; - } - if (ENGINEERING_TYPES.has(bead.issue_type)) { - return true; - } - if (stringValue(bead.metadata?.['gc.kind']) === 'run') { - return true; - } - return false; -} - -export function runProgress( - stages: RunStage[], - activeStageIndex: number, - activeStepId: string | null, - issues: RunIssue[], -): RunLane['progress'] { - const stage = runStagePosition(stages, activeStageIndex); - if (activeStepId !== null) { - return { - status: 'active_step', - stepId: activeStepId, - stage, - attempt: runStepAttempt(issues, activeStepId), - }; - } - - if (stage.status === 'available') { - return { - status: 'stage_only', - stage, - error: 'active run step unavailable', - }; - } - - return { status: 'unavailable', error: 'run progress unavailable' }; -} - -export function runStagePosition( - stages: RunStage[], - activeStageIndex: number, -): Extract['stage'] { - const stage = stages[activeStageIndex]; - return stage === undefined - ? { status: 'unavailable', error: 'active run stage unavailable' } - : { - status: 'available', - index: activeStageIndex, - key: stage.key, - label: stage.label, - }; -} - -export function runStepAttempt( - issues: RunIssue[], - stepId: string, -): Extract['attempt'] { - const value = reviewRoundForIssues(stepIssues(issues, stepId)); - return value === null - ? { status: 'unavailable', error: 'run step attempt unavailable' } - : { status: 'available', value }; -} diff --git a/internal/beadmeta/keys.go b/internal/beadmeta/keys.go index 1912e4418b..25158ae7c6 100644 --- a/internal/beadmeta/keys.go +++ b/internal/beadmeta/keys.go @@ -60,6 +60,7 @@ const ( ControllerErrorMetadataKey = "gc.controller_error" ControllerRetryableMetadataKey = "gc.controller_retryable" CurrentRunIDMetadataKey = "gc.current_run_id" + CwdMetadataKey = "gc.cwd" // ActiveWorkBeadMetadataKey is the session bead's current-pointer to the STEP it // is executing — the work bead's bare gc.step_id (NOT its namespaced bead id), // stamped at the claim hook and read at the usage record site to populate @@ -102,6 +103,7 @@ const ( FanoutStateMetadataKey = "gc.fanout_state" FinalDispositionMetadataKey = "gc.final_disposition" ForEachMetadataKey = "gc.for_each" + FormulaMetadataKey = "gc.formula" FormulaContractMetadataKey = "gc.formula_contract" FormulaHashMetadataKey = "gc.formula_hash" FormulaNameMetadataKey = "gc.formula_name" @@ -111,6 +113,7 @@ const ( IdempotencyKeyMetadataKey = "gc.idempotency_key" InputConvoyIDMetadataKey = "gc.input_convoy_id" InstantiatingMetadataKey = "gc.instantiating" + IterationMetadataKey = "gc.iteration" ItemRootKeyMetadataKey = "gc.item_root_key" KindMetadataKey = "gc.kind" LastFailureClassMetadataKey = "gc.last_failure_class" @@ -128,6 +131,7 @@ const ( OutcomeMetadataKey = "gc.outcome" OutputJSONMetadataKey = "gc.output_json" OutputJSONRequiredMetadataKey = "gc.output_json_required" + ParentBeadIDMetadataKey = "gc.parent_bead_id" ParentConvoyIDMetadataKey = "gc.parent_convoy_id" PartialFragmentMetadataKey = "gc.partial_fragment" PartialRetryMetadataKey = "gc.partial_retry" @@ -143,6 +147,7 @@ const ( RetryFromMetadataKey = "gc.retry_from" RetrySessionRecycledMetadataKey = "gc.retry_session_recycled" RetryStateMetadataKey = "gc.retry_state" + RigRootMetadataKey = "gc.rig_root" RootBeadIDMetadataKey = "gc.root_bead_id" RootStoreRefMetadataKey = "gc.root_store_ref" RoutedToMetadataKey = "gc.routed_to" @@ -154,31 +159,38 @@ const ( ScopeRoleMetadataKey = "gc.scope_role" SessionAffinityMetadataKey = "gc.session_affinity" SessionIDMetadataKey = "gc.session_id" - SessionNameMetadataKey = "gc.session_name" - SourceBeadIDMetadataKey = "gc.source_bead_id" - SourceStepSpecMetadataKey = "gc.source_step_spec" - SourceStoreRefMetadataKey = "gc.source_store_ref" - SpawnedCountMetadataKey = "gc.spawned_count" - SpecForMetadataKey = "gc.spec_for" - SpecForRefMetadataKey = "gc.spec_for_ref" - StderrMetadataKey = "gc.stderr" - StdoutMetadataKey = "gc.stdout" - StepIDMetadataKey = "gc.step_id" - StepRefMetadataKey = "gc.step_ref" - StepTimeoutMetadataKey = "gc.step_timeout" - SyntheticKindMetadataKey = "gc.synthetic_kind" - SyntheticMetadataKey = "gc.synthetic" - TemplateMetadataKey = "gc.template" - TerminalMetadataKey = "gc.terminal" - TriggerBeadIDMetadataKey = "gc.trigger_bead_id" - TriggerBeadStoreRefMetadataKey = "gc.trigger_bead_store_ref" - TruncatedMetadataKey = "gc.truncated" - WorkBranchMetadataKey = "gc.work_branch" - WorkCommitMetadataKey = "gc.work_commit" - WorkDirMetadataKey = "gc.work_dir" - WorkOutcomeMetadataKey = "gc.work_outcome" - WorkVerificationMetadataKey = "gc.work_verification" - WorkflowIDMetadataKey = "gc.workflow_id" + // SessionIDCamelMetadataKey is the camelCase variant some bead writers stamp + // alongside the snake_case SessionIDMetadataKey; both are read when resolving a + // bead's session link. + SessionIDCamelMetadataKey = "gc.sessionId" + SessionNameMetadataKey = "gc.session_name" + // SessionNameCamelMetadataKey is the camelCase variant of SessionNameMetadataKey, + // mirroring SessionIDCamelMetadataKey. + SessionNameCamelMetadataKey = "gc.sessionName" + SourceBeadIDMetadataKey = "gc.source_bead_id" + SourceStepSpecMetadataKey = "gc.source_step_spec" + SourceStoreRefMetadataKey = "gc.source_store_ref" + SpawnedCountMetadataKey = "gc.spawned_count" + SpecForMetadataKey = "gc.spec_for" + SpecForRefMetadataKey = "gc.spec_for_ref" + StderrMetadataKey = "gc.stderr" + StdoutMetadataKey = "gc.stdout" + StepIDMetadataKey = "gc.step_id" + StepRefMetadataKey = "gc.step_ref" + StepTimeoutMetadataKey = "gc.step_timeout" + SyntheticKindMetadataKey = "gc.synthetic_kind" + SyntheticMetadataKey = "gc.synthetic" + TemplateMetadataKey = "gc.template" + TerminalMetadataKey = "gc.terminal" + TriggerBeadIDMetadataKey = "gc.trigger_bead_id" + TriggerBeadStoreRefMetadataKey = "gc.trigger_bead_store_ref" + TruncatedMetadataKey = "gc.truncated" + WorkBranchMetadataKey = "gc.work_branch" + WorkCommitMetadataKey = "gc.work_commit" + WorkDirMetadataKey = "gc.work_dir" + WorkOutcomeMetadataKey = "gc.work_outcome" + WorkVerificationMetadataKey = "gc.work_verification" + WorkflowIDMetadataKey = "gc.workflow_id" ) // Work-record metadata keys (ADR-0009). These bind a work bead to its claim @@ -263,6 +275,7 @@ var KnownMetadataKeys = []string{ ControllerRetryableMetadataKey, CurrentRunIDMetadataKey, ActiveWorkBeadMetadataKey, + CwdMetadataKey, DeferredAssigneeMetadataKey, DeferredExecutionRoutedToMetadataKey, DeferredRoutedToMetadataKey, @@ -299,6 +312,7 @@ var KnownMetadataKeys = []string{ FanoutStateMetadataKey, FinalDispositionMetadataKey, ForEachMetadataKey, + FormulaMetadataKey, FormulaContractMetadataKey, FormulaHashMetadataKey, FormulaNameMetadataKey, @@ -308,6 +322,7 @@ var KnownMetadataKeys = []string{ IdempotencyKeyMetadataKey, InputConvoyIDMetadataKey, InstantiatingMetadataKey, + IterationMetadataKey, ItemRootKeyMetadataKey, KindMetadataKey, LastFailureClassMetadataKey, @@ -325,6 +340,7 @@ var KnownMetadataKeys = []string{ OutcomeMetadataKey, OutputJSONMetadataKey, OutputJSONRequiredMetadataKey, + ParentBeadIDMetadataKey, ParentConvoyIDMetadataKey, PartialFragmentMetadataKey, PartialRetryMetadataKey, @@ -340,6 +356,7 @@ var KnownMetadataKeys = []string{ RetryFromMetadataKey, RetrySessionRecycledMetadataKey, RetryStateMetadataKey, + RigRootMetadataKey, RootBeadIDMetadataKey, RootStoreRefMetadataKey, RoutedToMetadataKey, @@ -351,7 +368,9 @@ var KnownMetadataKeys = []string{ ScopeRoleMetadataKey, SessionAffinityMetadataKey, SessionIDMetadataKey, + SessionIDCamelMetadataKey, SessionNameMetadataKey, + SessionNameCamelMetadataKey, SourceBeadIDMetadataKey, SourceStepSpecMetadataKey, SourceStoreRefMetadataKey, diff --git a/internal/events/reader.go b/internal/events/reader.go index 258ff2bd54..a5c28c8520 100644 --- a/internal/events/reader.go +++ b/internal/events/reader.go @@ -147,6 +147,153 @@ func ReadFiltered(path string, filter Filter) ([]Event, error) { return result, nil } +// ReadFilteredWithInFlight is ReadFiltered plus events still stranded in +// in-flight rotation files. When rotateLocked renames the active log to +// events.jsonl.rotating--seq--, a background goroutine gzips it into +// the canonical .gz archive and only then removes the rotating file. In that +// window the just-rotated events live ONLY in the plain-JSONL rotating file, +// which ReadFiltered (it lists only .gz archives) cannot see. The live run +// tailer folds these in when it detects a rotation, before resetting its +// active-file cursor, so events written to the old active log in the poll window +// before the rename are not lost during the asynchronous compression. +// +// Callers must be seq-idempotent: during the brief window when a canonical .gz +// and its source rotating file coexist, an event can appear in both. The result +// is de-duplicated by seq and returned in seq order. Intended for the AfterSeq +// catch-up path; a positive Filter.Limit bounds only ReadFiltered's own scan, +// not the merged in-flight events. +func ReadFilteredWithInFlight(path string, filter Filter) ([]Event, error) { + base, baseErr := ReadFiltered(path, filter) + inflight, inErr := readInFlightRotating(path, filter) + if len(inflight) == 0 { + if baseErr == nil { + return base, inErr + } + return base, baseErr + } + merged := mergeEventsBySeq(base, inflight) + if baseErr != nil { + return merged, baseErr + } + return merged, inErr +} + +// readInFlightRotating reads events matching filter from any in-flight rotation +// files (events.jsonl.rotating--seq--) beside path — the plain-JSONL +// renames of a just-rotated active log the background gzip has not yet promoted +// to a canonical .gz archive. Files whose seq window is fully excluded by +// filter.AfterSeq are skipped without opening. Results are in seq order across +// rotating files (sorted by FirstSeq; each file is internally seq ordered). +// Returns (nil, nil) when nothing is rotating — the overwhelmingly common case. +func readInFlightRotating(path string, filter Filter) ([]Event, error) { + dir := filepath.Dir(path) + entries, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + type rotatingFile struct { + name string + firstSeq uint64 + } + var files []rotatingFile + for _, e := range entries { + if e.IsDir() || !hasRotatingPrefix(e.Name()) { + continue + } + _, first, last, ok := parseRotatingBasename(e.Name()) + if !ok { + // Legacy rotating file without a seq window; the startup orphan + // reaper promotes it — a live reader skips it rather than guess. + continue + } + if filter.AfterSeq > 0 && last <= filter.AfterSeq { + continue + } + files = append(files, rotatingFile{name: e.Name(), firstSeq: first}) + } + if len(files) == 0 { + return nil, nil + } + sort.Slice(files, func(i, j int) bool { return files[i].firstSeq < files[j].firstSeq }) + + var result []Event + for _, rf := range files { + evts, err := readPlainJSONLFiltered(filepath.Join(dir, rf.name), filter) + if err != nil { + return result, fmt.Errorf("reading in-flight rotation %q: %w", rf.name, err) + } + result = append(result, evts...) + } + return result, nil +} + +// readPlainJSONLFiltered reads every filter-matching event from a plain-JSONL +// events file, scanning the whole file from the start. Unlike ReadFrom it keeps +// no byte offset; unlike the active-file scan in ReadFiltered it does not honor +// Filter.Limit (its only caller merges the result under an AfterSeq filter). +func readPlainJSONLFiltered(path string, filter Filter) ([]Event, error) { + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + defer f.Close() //nolint:errcheck // read-only file + + var result []Event + scanner := bufio.NewScanner(f) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + var e Event + if err := json.Unmarshal(scanner.Bytes(), &e); err != nil { + continue // skip malformed lines (partial write mid-rename) + } + if !matchesFilter(e, filter) { + continue + } + result = append(result, e) + } + if err := scanner.Err(); err != nil { + return result, fmt.Errorf("scanning: %w", err) + } + return result, nil +} + +// mergeEventsBySeq merges two seq-ascending event slices into one seq-ascending +// slice, dropping exact seq duplicates — an event present in both a canonical +// archive and its not-yet-removed source rotating file. Event seqs are globally +// monotonic and unique, so equal seq means the same event. +func mergeEventsBySeq(a, b []Event) []Event { + out := make([]Event, 0, len(a)+len(b)) + appendUnique := func(e Event) { + if n := len(out); n > 0 && out[n-1].Seq == e.Seq { + return + } + out = append(out, e) + } + i, j := 0, 0 + for i < len(a) && j < len(b) { + if a[i].Seq <= b[j].Seq { + appendUnique(a[i]) + i++ + } else { + appendUnique(b[j]) + j++ + } + } + for ; i < len(a); i++ { + appendUnique(a[i]) + } + for ; j < len(b); j++ { + appendUnique(b[j]) + } + return out +} + // archiveFilesIn lists canonical events archives in dir, sorted by // FirstSeq ascending so callers can read them in chronological order. // Files that don't match the canonical name pattern (legacy archives, diff --git a/internal/events/rotation_reader_test.go b/internal/events/rotation_reader_test.go index 5872cb27ab..e447e22471 100644 --- a/internal/events/rotation_reader_test.go +++ b/internal/events/rotation_reader_test.go @@ -5,11 +5,118 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "testing" "time" ) +// writeJSONLEvents writes seq-stamped bead.created events to path as plain JSONL, +// one per line — the on-disk shape of an active log or an in-flight rotating file. +func writeJSONLEvents(t *testing.T, path string, seqs ...uint64) { + t.Helper() + var b strings.Builder + for _, s := range seqs { + fmt.Fprintf(&b, `{"seq":%d,"type":%q,"subject":"s%d"}`+"\n", s, string(BeadCreated), s) + } + if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +func seqsOf(evts []Event) []uint64 { + out := make([]uint64, 0, len(evts)) + for _, e := range evts { + out = append(out, e.Seq) + } + return out +} + +// TestReadFilteredWithInFlightIncludesRotatingFiles is the reader-level guard for +// the async-compression drop: a just-rotated active log is renamed to a plain +// events.jsonl.rotating-* file and gzipped in the background, so between the +// rename and the canonical .gz those events are visible to neither the archive +// walker nor the active-file scan. ReadFiltered misses them; ReadFilteredWithInFlight +// folds them back in, in seq order. +func TestReadFilteredWithInFlightIncludesRotatingFiles(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "events.jsonl") + + // A fully-compressed earlier rotation: seq 1 lives only in a canonical .gz. + gzSrc := filepath.Join(dir, "events.jsonl.rotating-20260507T120000Z-seq-1-1") + writeJSONLEvents(t, gzSrc, 1) + gz := filepath.Join(dir, formatArchiveBasename(time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC), 1, 1)) + var stderr bytes.Buffer + if err := gzipAndArchive(gzSrc, gz, &stderr); err != nil { + t.Fatalf("gzipAndArchive: %v", err) + } + + // An in-flight rotation whose gzip has NOT finished: seq 2,3 live only in the + // plain-JSONL rotating-* file (no .gz yet). + writeJSONLEvents(t, filepath.Join(dir, "events.jsonl.rotating-20260507T120500Z-seq-2-3"), 2, 3) + + // The fresh active file opened after that rotation. + writeJSONLEvents(t, path, 4, 5) + + // Baseline: ReadFiltered lists only .gz archives + active, so it MISSES the + // in-flight window (seq 2,3) — the exact drop this guards. + base, err := ReadFiltered(path, Filter{}) + if err != nil { + t.Fatalf("ReadFiltered: %v", err) + } + if got := seqsOf(base); !reflect.DeepEqual(got, []uint64{1, 4, 5}) { + t.Fatalf("ReadFiltered seqs = %v, want [1 4 5] (the in-flight gap)", got) + } + + // ReadFilteredWithInFlight folds the rotating window back in, in seq order. + all, err := ReadFilteredWithInFlight(path, Filter{}) + if err != nil { + t.Fatalf("ReadFilteredWithInFlight: %v", err) + } + if got := seqsOf(all); !reflect.DeepEqual(got, []uint64{1, 2, 3, 4, 5}) { + t.Fatalf("ReadFilteredWithInFlight seqs = %v, want [1 2 3 4 5]", got) + } + + // AfterSeq fully excludes the rotating window (last seq 3 <= 3) without opening it. + after, err := ReadFilteredWithInFlight(path, Filter{AfterSeq: 3}) + if err != nil { + t.Fatalf("ReadFilteredWithInFlight(AfterSeq=3): %v", err) + } + if got := seqsOf(after); !reflect.DeepEqual(got, []uint64{4, 5}) { + t.Fatalf("ReadFilteredWithInFlight(AfterSeq=3) seqs = %v, want [4 5]", got) + } +} + +// TestReadFilteredWithInFlightDedupsArchiveRotatingOverlap covers the instant +// after gzipAndArchive renames the .gz into place but before it removes the +// source rotating file: both cover the same seq window and the merged read must +// emit each event once, not twice. +func TestReadFilteredWithInFlightDedupsArchiveRotatingOverlap(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "events.jsonl") + + // The in-flight rotating file (stays on disk). + writeJSONLEvents(t, filepath.Join(dir, "events.jsonl.rotating-20260507T120500Z-seq-2-3"), 2, 3) + // A canonical .gz for the SAME window, gzipped from a throwaway source so the + // rotating file above is left in place — modeling the coexistence window. + gzSrc := filepath.Join(dir, "gz-source.jsonl") + writeJSONLEvents(t, gzSrc, 2, 3) + gz := filepath.Join(dir, formatArchiveBasename(time.Date(2026, 5, 7, 12, 5, 0, 0, time.UTC), 2, 3)) + var stderr bytes.Buffer + if err := gzipAndArchive(gzSrc, gz, &stderr); err != nil { + t.Fatalf("gzipAndArchive: %v", err) + } + writeJSONLEvents(t, path, 4) + + all, err := ReadFilteredWithInFlight(path, Filter{}) + if err != nil { + t.Fatalf("ReadFilteredWithInFlight: %v", err) + } + if got := seqsOf(all); !reflect.DeepEqual(got, []uint64{2, 3, 4}) { + t.Fatalf("overlap seqs = %v, want [2 3 4] (deduped)", got) + } +} + // seedRecorderWithRotation creates a fresh recorder, writes recordsBefore // events, force-rotates, then writes recordsAfter events. Returns the // directory holding the active log + archives; the recorder is diff --git a/internal/runproj/detail.go b/internal/runproj/detail.go new file mode 100644 index 0000000000..de9eded5a2 --- /dev/null +++ b/internal/runproj/detail.go @@ -0,0 +1,627 @@ +package runproj + +import ( + "fmt" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" +) + +// UnsupportedRunReason distinguishes the expected v1/wisp case ("not_run_view" — +// the run lists but has no graph.v2 detail) from a malformed graph.v2 snapshot +// ("invalid_snapshot" — a genuine load failure). Port of TS UnsupportedRunReason +// (gascity-dashboard-9w3k). +type UnsupportedRunReason string + +const ( + // ReasonNotRunView marks a run that has no graph.v2 detail view. + ReasonNotRunView UnsupportedRunReason = "not_run_view" + // ReasonInvalidSnapshot marks a malformed graph.v2 snapshot. + ReasonInvalidSnapshot UnsupportedRunReason = "invalid_snapshot" +) + +// UnsupportedRunError is returned when a run cannot be projected into a detail +// view. Port of TS UnsupportedRunError. +type UnsupportedRunError struct { + Message string + Reason UnsupportedRunReason +} + +func (e *UnsupportedRunError) Error() string { return e.Message } + +func unsupportedRun(message string, reason UnsupportedRunReason) error { + return &UnsupportedRunError{Message: message, Reason: reason} +} + +// BuildRunDetail projects one run's folded beads into the dashboard run-detail +// DTO. It is the bead-derived entry point that reproduces the supervisor's +// /workflow/{id} projection client-side: it synthesizes a run snapshot for runID +// from beadList (member selection + dep synthesis, mirroring the golden +// generator's snapshotForRun), then runs the shared detail pipeline. No sessions +// or compiled formula are layered here — that enrichment is request-time on the +// endpoint. snapshotVersion and snapshotEventSeq parameterize the snapshot +// identity (the golden passes 1/100; the live tailer passes a real version and +// its LastSeq cursor). +// +// It returns an *UnsupportedRunError when the run is not a graph.v2 run or its +// snapshot identity/scope is missing — the same cases the TS enrichFormulaRun +// throws on. +func BuildRunDetail(beadList []beads.Bead, runID string, snapshotVersion int, snapshotEventSeq int64) (FormulaRunDetail, error) { + return BuildRunDetailWithSessions(beadList, runID, snapshotVersion, snapshotEventSeq, nil) +} + +// BuildRunDetailWithSessions is BuildRunDetail with a request-time session list +// layered in, so the detail's execution-instance session links and the +// streamable-session progress resolve against live sessions. The golden path +// passes nil (BuildRunDetail); the live endpoint passes the loopback /v0 sessions +// read. Session enrichment is NOT golden-gated. +func BuildRunDetailWithSessions(beadList []beads.Bead, runID string, snapshotVersion int, snapshotEventSeq int64, sessions []DashboardSession) (FormulaRunDetail, error) { + return BuildRunDetailWithSessionsAndFormula(beadList, runID, snapshotVersion, snapshotEventSeq, sessions, nil, FormulaDetailUpstreamError) +} + +// BuildRunDetailWithSessionsAndFormula is BuildRunDetailWithSessions with the +// supervisor's compiled formula detail layered in as well. Like sessions, this +// is request-time endpoint enrichment (NOT golden-gated): the live endpoint +// fetches the compiled formula for a run's name+target and passes it here so the +// run's nodes honor the authored step order and the formula-detail state resolves +// to "available". A nil formulaDetail keeps the un-enriched projection — the +// detail state then resolves from run metadata alone (a missing_* arm, or, once a +// name+target are known but no detail was supplied, the fetch_failed arm that +// honestly reports a run whose compiled formula could not be layered in). +// fetchFailure is the reason the live fetch failed when formulaDetail is nil +// (FormulaDetailNotFound for a supervisor 404, else FormulaDetailUpstreamError); +// callers with no live fetch pass FormulaDetailUpstreamError. +func BuildRunDetailWithSessionsAndFormula(beadList []beads.Bead, runID string, snapshotVersion int, snapshotEventSeq int64, sessions []DashboardSession, formulaDetail *FormulaOrderingDetail, fetchFailure RunFormulaDetailFetchFailure) (FormulaRunDetail, error) { + snap, err := snapshotForRun(beadList, runID, snapshotVersion, snapshotEventSeq) + if err != nil { + return FormulaRunDetail{}, err + } + return enrichFormulaRun(snap, sessions, formulaDetail.toInput(), fetchFailure) +} + +// RunFormulaTargetForRun resolves the compiled-formula name, preview target, and +// run scope for the run rooted at runID, reusing the exact identity and scope +// resolution the detail build applies. ok is false when the run is not a +// fetchable graph.v2 run, lacks a formula name+target, or has no valid scope. +// +// The dashboard BFF calls this to decide whether to fetch the supervisor's +// compiled formula detail and to target the correct formula layer. The +// formula-detail endpoint resolves the compiled formula against scope-derived +// search paths, so a rig-scoped run must send its scope_kind/scope_ref or the +// lookup resolves the wrong layer (or is rejected for missing required scope). +func RunFormulaTargetForRun(beadList []beads.Bead, runID string) (name, target, scopeKind, scopeRef string, ok bool) { + snap, err := snapshotForRun(beadList, runID, 0, 0) + if err != nil { + return "", "", "", "", false + } + if !isGraphV2(snap) { + return "", "", "", "", false + } + root := rootBead(dedupeBeads(snap.beads), nonEmpty(snap.rootBeadID)) + name, _, target, hasName := resolveRunFormulaIdentityDetailState(root, "", false) + if !hasName || target == "" { + return "", "", "", "", false + } + scopeKind, scopeRef, scopeOK := fromSnapshotScope(snap) + if !scopeOK { + return "", "", "", "", false + } + return name, target, scopeKind, scopeRef, true +} + +// snapshotForRun synthesizes a run snapshot for one root from the folded beads: +// member selection, the issue_type→kind / ref→step_ref projection (port of the +// golden generator's snapshotForRun + toRunSnapshotBead), snapshot identity from +// root metadata, and the dependency edges via snapshotDeps — the real bead +// dependencies plus the root→member parent edges, so the detail graph renders the +// actual step→step DAG the supervisor snapshot carried, not just parent links. +func snapshotForRun(beadList []beads.Bead, rootID string, version int, eventSeq int64) (runSnapshot, error) { + rootIdx := -1 + for i := range beadList { + if beadList[i].ID == rootID { + rootIdx = i + break + } + } + if rootIdx < 0 { + return runSnapshot{}, fmt.Errorf("runproj: detail run root %q not found", rootID) + } + root := beadList[rootIdx] + + var members []beads.Bead + for i := range beadList { + b := beadList[i] + if b.ID == rootID || + b.ParentID == rootID || + b.Metadata[beadmeta.RootBeadIDMetadataKey] == rootID || + strings.HasPrefix(b.ID, rootID+".") { + members = append(members, b) + } + } + + snapBeads := make([]runSnapshotBead, 0, len(members)) + for i := range members { + snapBeads = append(snapBeads, toRunSnapshotBead(members[i])) + } + + seq := eventSeq + rootStoreRef := root.Metadata[beadmeta.RootStoreRefMetadataKey] + return runSnapshot{ + runID: rootID, + rootBeadID: rootID, + rootStoreRef: rootStoreRef, + resolvedRootStore: rootStoreRef, + scopeKind: root.Metadata[beadmeta.ScopeKindMetadataKey], + scopeRef: root.Metadata[beadmeta.ScopeRefMetadataKey], + snapshotVersion: version, + snapshotEventSeq: &seq, + partial: false, + storesScanned: []string{rootStoreRef}, + beads: snapBeads, + deps: snapshotDeps(members), + // logicalEdges stays nil on the bead-derived path: the supervisor's + // precomputed logical edges are not available here, so buildRunDisplayEdges + // derives the display graph from deps (bridging hidden scope-check nodes), + // which reproduces the supervisor's logical-edge behavior. + logicalEdges: nil, + }, nil +} + +// toRunSnapshotBead projects a folded bead into the supervisor run-snapshot row. +// Port of the golden generator's toRunSnapshotBead (issue_type→kind unless +// gc.original_kind overrides; ref→step_ref; scope_ref / logical_bead_id mirrored +// from metadata). +func toRunSnapshotBead(b beads.Bead) runSnapshotBead { + kind := b.Type + if v, ok := b.Metadata[beadmeta.OriginalKindMetadataKey]; ok { + kind = v + } + return runSnapshotBead{ + id: b.ID, + title: b.Title, + status: b.Status, + kind: kind, + stepRef: b.Ref, + assignee: b.Assignee, + scopeRef: b.Metadata[beadmeta.ScopeRefMetadataKey], + logicalBeadID: b.Metadata[beadmeta.LogicalBeadIDMetadataKey], + metadata: b.Metadata, + } +} + +// snapshotDeps synthesizes a run snapshot's dependency edges from the folded +// members. It reproduces the supervisor RunSnapshot's dep set on the OSS-local +// path: the real bead dependency edges each member carries (its Dependencies and +// Needs) merged with the root→member parent edges, using the same edge direction +// and dedup semantics as the supervisor graph API's collectWorkflowDeps +// (internal/api/handler_convoy_dispatch.go) — the prerequisite (DependsOnID) is +// the edge source and the dependent (IssueID) the target, carrying the dep type. +// Edges to beads outside the run are dropped, since the detail graph can only +// render members it holds. Without the real edges, buildRunDisplayEdges would +// project only root→member parent edges and every genuine step→step dependency +// (and the logical graph derived from it) would be lost. +func snapshotDeps(members []beads.Bead) []runSnapshotDep { + if len(members) == 0 { + return nil + } + memberIDs := make(map[string]bool, len(members)) + for i := range members { + memberIDs[members[i].ID] = true + } + + deps := make([]runSnapshotDep, 0, len(members)) + seen := make(map[string]bool) + add := func(from, to, kind string) { + if from == "" || to == "" || from == to { + return + } + if !memberIDs[from] || !memberIDs[to] { + return + } + key := from + "\x00" + to + "\x00" + kind + if seen[key] { + return + } + seen[key] = true + deps = append(deps, runSnapshotDep{from: from, to: to, kind: kind}) + } + + // Real dependency edges the fold preserved on each member: the structured + // Dependencies (issue depends-on a prerequisite, carrying the dep type) and + // the simpler Needs prerequisite list. The prerequisite is the source and the + // dependent the target, matching collectWorkflowDeps. + for i := range members { + b := members[i] + for _, d := range b.Dependencies { + add(d.DependsOnID, d.IssueID, d.Type) + } + for _, need := range b.Needs { + add(need, b.ID, "") + } + } + + // Root→member parent edges (the first-seen member is the root in fold order). + rootID := members[0].ID + for i := range members { + if members[i].ID == rootID { + continue + } + add(rootID, members[i].ID, "parent") + } + return deps +} + +// runningFormulaRunInput mirrors the TS RunningFormulaRunInput. formulaDetail and +// sessions are nil on the bead-derived path; the live endpoint layers sessions in +// at request time. formulaDetailFailure records why the live fetch failed when +// formulaDetail is nil (not_found for a 404, else upstream_error); it is empty on +// the bead-derived path and defaults to upstream_error. +type runningFormulaRunInput struct { + raw runSnapshot + runID string + rootBeadID string + rootStoreRef string + resolvedRootStore string + scopeKind string + scopeRef string + root *runSnapshotBead + beads []runSnapshotBead + rigRoot string + sessions []DashboardSession + formulaDetail *formulaDetailInput + formulaDetailFailure RunFormulaDetailFetchFailure +} + +// runningFormulaRun carries the orchestrated detail outputs enrichFormulaRun +// assembles into the DTO. Port of the consumed subset of TS RunningFormulaRun. +type runningFormulaRun struct { + title string + formula RunFormula + formulaDetail RunFormulaDetailState + executionPath RunExecutionPath + progress FormulaRunProgress + phase string + stages []RunStage + nodes []RunDisplayNode + edges []RunDisplayEdge + lanes []RunDisplayLane +} + +// enrichFormulaRun is the bead-derived detail pipeline entry. Port of TS +// enrichFormulaRun (enrich.ts). sessions and formulaDetail carry the optional +// request-time enrichment (both nil on the golden path). fetchFailure records why +// a live formula-detail fetch failed when formulaDetail is nil (empty on the +// golden path, defaulting to upstream_error). +func enrichFormulaRun(raw runSnapshot, sessions []DashboardSession, formulaDetail *formulaDetailInput, fetchFailure RunFormulaDetailFetchFailure) (FormulaRunDetail, error) { + if !isGraphV2(raw) { + return FormulaRunDetail{}, unsupportedRun("run is not a graph.v2 run", ReasonNotRunView) + } + + rootBeadID := nonEmpty(raw.rootBeadID) + runID := nonEmpty(raw.runID) + rootStoreRef := nonEmpty(raw.rootStoreRef) + resolvedRootStore := nonEmpty(raw.resolvedRootStore) + deduped := dedupeBeads(raw.beads) + root := rootBead(deduped, rootBeadID) + scopeKind, scopeRef, scopeOK := fromSnapshotScope(raw) + + if runID == "" || rootStoreRef == "" || resolvedRootStore == "" { + return FormulaRunDetail{}, unsupportedRun("run snapshot identity is missing or invalid", ReasonInvalidSnapshot) + } + if !scopeOK { + return FormulaRunDetail{}, unsupportedRun("run scope is missing or invalid", ReasonInvalidSnapshot) + } + + formulaRun := buildRunningFormulaRun(runningFormulaRunInput{ + raw: raw, + runID: runID, + rootBeadID: rootBeadID, + rootStoreRef: rootStoreRef, + resolvedRootStore: resolvedRootStore, + scopeKind: scopeKind, + scopeRef: scopeRef, + root: root, + beads: deduped, + sessions: sessions, + formulaDetail: formulaDetail, + formulaDetailFailure: fetchFailure, + }) + + var partialReasons []string + if raw.partial { + partialReasons = []string{"supervisor_snapshot_partial"} + } + + return FormulaRunDetail{ + RunID: runID, + RootBeadID: rootBeadID, + RootStoreRef: rootStoreRef, + ResolvedRootStore: resolvedRootStore, + ScopeKind: scopeKind, + ScopeRef: scopeRef, + Title: formulaRun.title, + Formula: formulaRun.formula, + FormulaDetail: formulaRun.formulaDetail, + ExecutionPath: formulaRun.executionPath, + SnapshotVersion: raw.snapshotVersion, + SnapshotEventSeq: formulaRun.progress.SnapshotEventSeq, + Completeness: formulaRunCompleteness(partialReasons), + Progress: formulaRun.progress, + Phase: formulaRun.phase, + Stages: formulaRun.stages, + Nodes: formulaRun.nodes, + Edges: formulaRun.edges, + Lanes: formulaRun.lanes, + }, nil +} + +// buildRunningFormulaRun is the central detail aggregation. Port of TS +// buildRunningFormulaRun (formula-run.ts). +func buildRunningFormulaRun(input runningFormulaRunInput) runningFormulaRun { + bg := groupRunBeads(input.beads, input.rootBeadID) + groups := orderRunNodeGroups(bg.groups, input.formulaDetail, input.rootBeadID) + latestIterationByLoop := latestIterationsByLoop(groups) + sessionIndex := buildRunSessionIndex(input.sessions) + sessionContext := runSessionLinkContext{sessionIndex: &sessionIndex, scopeRef: input.scopeRef} + + rawNodes := make([]RunDisplayNode, 0, len(groups)) + for _, group := range groups { + latest, hasLatest := latestIterationByLoop[group.loopControlNodeID] + rawNodes = append(rawNodes, buildRunDisplayNode( + group, + bg.badgesByTarget[group.semanticNodeID], + latest, + hasLatest, + sessionContext, + )) + } + edges := buildRunDisplayEdges(input.raw, bg.physicalToSemantic, rawNodes) + nodes := applyDisplayNodeStates(rawNodes, edges) + progress := buildFormulaRunProgress(input.raw, nodes, edges) + + hasFormulaDetail := input.formulaDetail != nil + formula := runFormulaState(input.root, hasFormulaDetail) + formulaDetail := runFormulaDetailState(input.root, hasFormulaDetail, input.formulaDetailFailure) + executionPath := resolveRunExecutionPath(input.root, input.beads, input.rigRoot) + + issues := make([]runIssue, 0, len(input.beads)) + for i := range input.beads { + issues = append(issues, fromRunSnapshotBead(input.beads[i])) + } + phase := mapRunPhase(issues) + formulaName, hasFormulaName := "", false + if formula.Kind == "known" { + formulaName, hasFormulaName = formula.Name, true + } + stages := stageProgress(phase, formulaName, hasFormulaName, issues) + + title := input.runID + if input.root != nil { + if t := nonEmpty(input.root.title); t != "" { + title = t + } + } + + return runningFormulaRun{ + title: title, + formula: formula, + formulaDetail: formulaDetail, + executionPath: executionPath, + progress: progress, + phase: phase.phase, + stages: stages, + nodes: nodes, + edges: edges, + lanes: buildRunDisplayLanes(nodes), + } +} + +// fromRunSnapshotBead adapts a run-snapshot bead to the phase classifier's +// runIssue. Port of TS fromRunSnapshotBead (formula-run.ts): kind→issue_type, +// empty updated_at, gc.parent_bead_id → parent. +func fromRunSnapshotBead(bead runSnapshotBead) runIssue { + issue := runIssue{ + id: bead.id, + title: bead.title, + status: bead.status, + issueType: bead.kind, + updatedAt: "", + metadata: bead.metadata, + } + if bead.assignee != "" { + issue.assignee = bead.assignee + } + if parent := beadMeta(bead, beadmeta.ParentBeadIDMetadataKey); parent != "" { + issue.parent = parent + } + return issue +} + +// runFormulaState resolves the run's formula identity union. Port of TS +// runFormulaState. +func runFormulaState(root *runSnapshotBead, hasFormulaDetail bool) RunFormula { + name, source, _, hasName := resolveRunFormulaIdentityDetailState(root, "", hasFormulaDetail) + if hasName { + resolvedSource := "metadata" + if source == "title_fallback" { + resolvedSource = "title_fallback" + } + return RunFormula{Kind: "known", Name: name, Source: resolvedSource} + } + return RunFormula{Kind: "unavailable", Reason: "missing_formula_metadata"} +} + +// runFormulaDetailState resolves the compiled-formula-detail union. Port of TS +// runFormulaDetailState. When a name+target are known but no compiled detail was +// layered in, it reports the fetch_failed arm with fetchFailure as the reason: +// the live BFF passes not_found for a supervisor 404 and upstream_error for every +// other failure, matching the TS formulaDetailFetchFailure mapping. The +// bead-derived path (no live fetch) leaves fetchFailure empty and defaults to +// upstream_error. +func runFormulaDetailState(root *runSnapshotBead, hasFormulaDetail bool, fetchFailure RunFormulaDetailFetchFailure) RunFormulaDetailState { + name, _, target, hasName := resolveRunFormulaIdentityDetailState(root, "", hasFormulaDetail) + if !hasName { + return RunFormulaDetailState{Kind: "unavailable", Reason: "missing_formula_metadata"} + } + if target == "" { + return RunFormulaDetailState{Kind: "unavailable", Reason: "missing_run_target", Name: name} + } + if hasFormulaDetail { + return RunFormulaDetailState{Kind: "available", Name: name, Target: target} + } + failure := fetchFailure + if failure == "" { + failure = FormulaDetailUpstreamError + } + return RunFormulaDetailState{ + Kind: "unavailable", + Reason: "fetch_failed", + Name: name, + Target: target, + Failure: string(failure), + } +} + +// buildFormulaRunProgress computes the run progress/census. Port of TS +// buildFormulaRunProgress. +func buildFormulaRunProgress(raw runSnapshot, nodes []RunDisplayNode, edges []RunDisplayEdge) FormulaRunProgress { + visibleCount := 0 + for _, node := range nodes { + if node.VisibleInGraph { + visibleCount++ + } + } + + streamableSessionIDs := []string{} + seenStreamable := map[string]bool{} + executionInstanceCount, sessionLinkCount, streamableSessionCount := 0, 0, 0 + + for _, node := range nodes { + for _, instance := range node.ExecutionInstances { + executionInstanceCount++ + if instance.Session.Kind == "attached" { + sessionLinkCount++ + if instance.Session.Streamable { + streamableSessionCount++ + id := instance.Session.Link.SessionID + if !seenStreamable[id] { + seenStreamable[id] = true + streamableSessionIDs = append(streamableSessionIDs, id) + } + } + } + } + } + + var visibleStatuses, allStatuses nodeStatusCounts + for _, node := range nodes { + allStatuses.inc(node.Status) + if node.VisibleInGraph { + visibleStatuses.inc(node.Status) + } + } + + return FormulaRunProgress{ + SnapshotVersion: raw.snapshotVersion, + SnapshotEventSeq: runSnapshotSequenceOf(raw.snapshotEventSeq), + SnapshotPartial: raw.partial, + TotalNodeCount: len(nodes), + VisibleNodeCount: visibleCount, + EdgeCount: len(edges), + ExecutionInstanceCount: executionInstanceCount, + SessionLinkCount: sessionLinkCount, + StreamableSessionCount: streamableSessionCount, + StreamableSessionIDs: streamableSessionIDs, + StatusCounts: visibleStatuses, + AllStatusCounts: allStatuses, + } +} + +// runSnapshotSequenceOf renders the snapshot-sequence union. Port of TS +// runSnapshotSequence (nil seq → supervisor_omitted). +func runSnapshotSequenceOf(seq *int64) RunSnapshotSequence { + if seq != nil { + return RunSnapshotSequence{Kind: "known", Seq: *seq} + } + return RunSnapshotSequence{Kind: "unavailable", Reason: "supervisor_omitted"} +} + +// formulaRunCompleteness collapses partial reasons into the completeness union. +// Port of TS formulaRunCompleteness (dedupes reasons, preserving first-seen order). +func formulaRunCompleteness(reasons []string) FormulaRunCompleteness { + seen := map[string]bool{} + var unique []string + for _, r := range reasons { + if !seen[r] { + seen[r] = true + unique = append(unique, r) + } + } + if len(unique) == 0 { + return FormulaRunCompleteness{Kind: "complete"} + } + return FormulaRunCompleteness{Kind: "partial", Reasons: unique} +} + +// isGraphV2 reports whether the snapshot's root carries gc.formula_contract = +// graph.v2. Port of TS isGraphV2. +func isGraphV2(raw runSnapshot) bool { + root := rootBead(raw.beads, raw.rootBeadID) + return rootMetaPtr(root, beadmeta.FormulaContractMetadataKey) == "graph.v2" +} + +// rootBead finds the root bead by id. Port of TS rootBead (nil mirrors undefined). +func rootBead(beads []runSnapshotBead, rootBeadID string) *runSnapshotBead { + rootID := nonEmpty(rootBeadID) + if rootID == "" { + return nil + } + for i := range beads { + if nonEmpty(beads[i].id) == rootID { + return &beads[i] + } + } + return nil +} + +// dedupeBeads drops beads whose id repeats, keeping the first. Port of TS +// dedupeBeads (empty-id beads are kept). +func dedupeBeads(in []runSnapshotBead) []runSnapshotBead { + seen := map[string]bool{} + out := make([]runSnapshotBead, 0, len(in)) + for _, bead := range in { + id := nonEmpty(bead.id) + if id != "" { + if seen[id] { + continue + } + seen[id] = true + } + out = append(out, bead) + } + return out +} + +// fromSnapshotScope resolves the run scope from the snapshot identity. Port of TS +// fromSnapshotScope (bool mirrors null), extended with the same gc.root_store_ref +// fallback summary already applies via fromRootMetadataScope: the explicit +// gc.scope_kind/gc.scope_ref pair wins, but a root carrying only gc.root_store_ref +// recovers its scope from that store ref. Without the fallback a run that lists +// successfully in /runs/summary (which uses fromRootMetadataScope) would 422 with +// invalid_snapshot when opened in /runs/{id}/detail. +func fromSnapshotScope(raw runSnapshot) (kind, ref string, ok bool) { + scopeKind, kindOK := parseRunScopeKind(raw.scopeKind) + scopeRef := stringValueOrEmpty(raw.scopeRef) + if kindOK && scopeRef != "" { + return scopeKind, scopeRef, true + } + // Fallback (gascity-dashboard-km0w): recover the scope from the snapshot's + // gc.root_store_ref, matching summary's fromRootMetadataScope so a run scoped + // only by its root store ref opens in detail instead of failing invalid. + parsedKind, parsedRef, storeOK := fromStoreRef(raw.rootStoreRef) + if storeOK && scopeRefRe.MatchString(parsedRef) { + return parsedKind, parsedRef, true + } + return "", "", false +} diff --git a/internal/runproj/detail_consistency_test.go b/internal/runproj/detail_consistency_test.go new file mode 100644 index 0000000000..53e0e483e9 --- /dev/null +++ b/internal/runproj/detail_consistency_test.go @@ -0,0 +1,60 @@ +package runproj + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/gastownhall/gascity/internal/beads" +) + +// TestSummaryDetailPhaseStageConsistency proves the ADR invariant structurally: +// the same run resolves to the same phase and the same stage ladder through both +// BuildRunSummary and BuildRunDetail, because both call the shared +// mapRunPhase/stageProgress classifier (detail stages == summary stages by +// construction, not by two call sites that happen to agree). +func TestSummaryDetailPhaseStageConsistency(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "beads_fixture.json")) + if err != nil { + t.Fatalf("read fixture: %v", err) + } + var beadList []beads.Bead + if err := json.Unmarshal(raw, &beadList); err != nil { + t.Fatalf("unmarshal fixture: %v", err) + } + + const runID = "dt-adopt1" + + summary := BuildRunSummary(beadList) + lane, ok := findLane(summary, runID) + if !ok { + t.Fatalf("run %q not found in summary lanes", runID) + } + + detail, err := BuildRunDetail(beadList, runID, detailGoldenSnapshotVersion, detailGoldenSnapshotEventSeq) + if err != nil { + t.Fatalf("BuildRunDetail: %v", err) + } + + if lane.Phase != detail.Phase { + t.Errorf("phase mismatch: summary=%q detail=%q", lane.Phase, detail.Phase) + } + if !reflect.DeepEqual(lane.Stages, detail.Stages) { + t.Errorf("stage ladder mismatch:\nsummary=%+v\ndetail =%+v", lane.Stages, detail.Stages) + } +} + +// findLane locates a lane by id across the summary's active/blocked/historical +// partitions. +func findLane(summary RunSummary, id string) (RunLane, bool) { + for _, group := range [][]RunLane{summary.Lanes, summary.BlockedLanes, summary.HistoricalLanes} { + for _, lane := range group { + if lane.ID == id { + return lane, true + } + } + } + return RunLane{}, false +} diff --git a/internal/runproj/detail_deps_test.go b/internal/runproj/detail_deps_test.go new file mode 100644 index 0000000000..8c860ef365 --- /dev/null +++ b/internal/runproj/detail_deps_test.go @@ -0,0 +1,122 @@ +package runproj + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/gastownhall/gascity/internal/beads" +) + +// TestBuildRunDetailPreservesStepDependencies proves the bead-derived detail +// projection preserves real step→step dependency edges (the supervisor snapshot's +// deps), not just the synthesized root→member parent edges. A regression here is +// what dropped the workflow dependency graph from the run-detail view. +func TestBuildRunDetailPreservesStepDependencies(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "beads_fixture.json")) + if err != nil { + t.Fatalf("read fixture: %v", err) + } + var beadList []beads.Bead + if err := json.Unmarshal(raw, &beadList); err != nil { + t.Fatalf("unmarshal fixture: %v", err) + } + + // Give rebase-check (dt-adopt1.2) a real dependency on preflight (dt-adopt1.1): + // the kind of edge the supervisor RunSnapshot carried and the bead-derived + // projection previously discarded. + found := false + for i := range beadList { + if beadList[i].ID == "dt-adopt1.2" { + beadList[i].Dependencies = []beads.Dep{ + {IssueID: "dt-adopt1.2", DependsOnID: "dt-adopt1.1", Type: "blocks"}, + } + found = true + } + } + if !found { + t.Fatal("fixture missing dt-adopt1.2; test needs updating") + } + + detail, err := BuildRunDetail(beadList, "dt-adopt1", 1, 100) + if err != nil { + t.Fatalf("BuildRunDetail: %v", err) + } + + // The dependency must surface as a display edge between the two steps' semantic + // nodes (preflight → rebase-check), carrying the dep type. + want := RunDisplayEdge{From: "preflight", To: "rebase-check", Kind: "blocks"} + if !hasEdge(detail.Edges, want) { + t.Errorf("step→step dependency edge missing: want %+v, got edges %+v", want, detail.Edges) + } + // The additive fix must not drop the existing root→member parent edges. + parent := RunDisplayEdge{From: "dt-adopt1", To: "preflight", Kind: "parent"} + if !hasEdge(detail.Edges, parent) { + t.Errorf("parent edge dropped: want %+v, got edges %+v", parent, detail.Edges) + } +} + +// TestSnapshotDepsMergesRealAndParentEdges unit-tests the dep synthesis helper: +// real Dependencies and Needs edges (prerequisite → dependent) are merged with +// root→member parent edges, edges to non-members are dropped, and duplicate +// edges (same from|to|kind) collapse. +func TestSnapshotDepsMergesRealAndParentEdges(t *testing.T) { + members := []beads.Bead{ + {ID: "root"}, + {ID: "a", Dependencies: []beads.Dep{{IssueID: "a", DependsOnID: "root", Type: "blocks"}}}, + {ID: "b", Needs: []string{"a"}}, + {ID: "c", Dependencies: []beads.Dep{ + {IssueID: "c", DependsOnID: "a", Type: "blocks"}, + {IssueID: "c", DependsOnID: "outsider", Type: "blocks"}, // dropped: not a member + {IssueID: "c", DependsOnID: "root", Type: "parent"}, // dupes the synthesized parent edge + }}, + } + + deps := snapshotDeps(members) + + // Real dependency edges: prerequisite → dependent, carrying the dep type + // (Needs carries no type, so it projects as the default dependency kind). + assertHasDep(t, deps, runSnapshotDep{from: "root", to: "a", kind: "blocks"}) + assertHasDep(t, deps, runSnapshotDep{from: "a", to: "b", kind: ""}) + assertHasDep(t, deps, runSnapshotDep{from: "a", to: "c", kind: "blocks"}) + // Parent edges: root → each non-root member. + assertHasDep(t, deps, runSnapshotDep{from: "root", to: "a", kind: "parent"}) + assertHasDep(t, deps, runSnapshotDep{from: "root", to: "b", kind: "parent"}) + assertHasDep(t, deps, runSnapshotDep{from: "root", to: "c", kind: "parent"}) + + for _, d := range deps { + if d.from == "outsider" || d.to == "outsider" { + t.Errorf("edge to non-member leaked: %+v", d) + } + } + + rootCParent := 0 + for _, d := range deps { + if d.from == "root" && d.to == "c" && d.kind == "parent" { + rootCParent++ + } + } + if rootCParent != 1 { + t.Errorf("root→c parent edge not deduped: count=%d, deps=%+v", rootCParent, deps) + } +} + +func hasEdge(edges []RunDisplayEdge, want RunDisplayEdge) bool { + for _, e := range edges { + if e == want { + return true + } + } + return false +} + +func assertHasDep(t *testing.T, deps []runSnapshotDep, want runSnapshotDep) { + t.Helper() + for _, d := range deps { + if d == want { + return + } + } + t.Errorf("missing dep %+v in %+v", want, deps) +} diff --git a/internal/runproj/detail_displaystate.go b/internal/runproj/detail_displaystate.go new file mode 100644 index 0000000000..4708eaf67e --- /dev/null +++ b/internal/runproj/detail_displaystate.go @@ -0,0 +1,81 @@ +package runproj + +// terminalStatuses are the node statuses that satisfy an upstream blocker. +// Port of TS TERMINAL_STATUSES (display-state.ts). +var terminalStatuses = map[string]bool{ + "completed": true, + "done": true, + "failed": true, + "skipped": true, +} + +// applyDisplayNodeStates promotes pending nodes to ready or blocked based on +// their upstream edges. Port of TS applyDisplayNodeStates. The returned slice is +// a fresh copy with the pending → ready/blocked transitions applied, mirroring +// the TS immutable update (the field order of each node is preserved). +func applyDisplayNodeStates(nodes []RunDisplayNode, edges []RunDisplayEdge) []RunDisplayNode { + byID := make(map[string]RunDisplayNode, len(nodes)) + for _, node := range nodes { + byID[node.ID] = node + } + inbound := buildInboundEdges(edges, byID) + + statusByID := make(map[string]string, len(nodes)) + for _, node := range nodes { + statusByID[node.ID] = displayStatusFor(node, inbound[node.ID], byID) + } + + out := make([]RunDisplayNode, len(nodes)) + for i, node := range nodes { + status := node.Status + if s, ok := statusByID[node.ID]; ok { + status = s + } + if status == node.Status { + out[i] = node + continue + } + updated := node + updated.Status = status + instances := make([]RunExecutionInstance, len(node.ExecutionInstances)) + for j, inst := range node.ExecutionInstances { + if inst.CurrentIteration && inst.Status == "pending" { + inst.Status = status + } + instances[j] = inst + } + updated.ExecutionInstances = instances + out[i] = updated + } + return out +} + +func displayStatusFor(node RunDisplayNode, blockers []string, byID map[string]RunDisplayNode) string { + if node.Status != "pending" { + return node.Status + } + if len(blockers) == 0 { + return "ready" + } + for _, blockerID := range blockers { + blocker, ok := byID[blockerID] + if !ok || !terminalStatuses[blocker.Status] { + return "blocked" + } + } + return "ready" +} + +func buildInboundEdges(edges []RunDisplayEdge, byID map[string]RunDisplayNode) map[string][]string { + inbound := make(map[string][]string) + for _, edge := range edges { + if _, ok := byID[edge.From]; !ok { + continue + } + if _, ok := byID[edge.To]; !ok { + continue + } + inbound[edge.To] = append(inbound[edge.To], edge.From) + } + return inbound +} diff --git a/internal/runproj/detail_edges.go b/internal/runproj/detail_edges.go new file mode 100644 index 0000000000..d55e705868 --- /dev/null +++ b/internal/runproj/detail_edges.go @@ -0,0 +1,135 @@ +package runproj + +import "github.com/gastownhall/gascity/internal/beadmeta" + +// buildRunDisplayEdges projects a run snapshot's dependency edges into the +// display graph, preferring logical edges and bridging across hidden scope-check +// nodes. Port of TS buildRunDisplayEdges (edges.ts). +func buildRunDisplayEdges(raw runSnapshot, physicalToSemantic map[string]string, nodes []RunDisplayNode) []RunDisplayEdge { + logicalEdges := projectEdges(raw.logicalEdges, physicalToSemantic, nodes, nil) + if len(logicalEdges) > 0 { + return logicalEdges + } + return projectEdges(raw.deps, physicalToSemantic, nodes, bridgeableScopeCheckIDs(raw)) +} + +func projectEdges(deps []runSnapshotDep, physicalToSemantic map[string]string, nodes []RunDisplayNode, bridgeableHiddenIDs map[string]bool) []RunDisplayEdge { + visible := make(map[string]bool) + for _, node := range nodes { + if node.VisibleInGraph { + visible[node.ID] = true + } + } + outgoing := outgoingDeps(deps) + seen := make(map[string]bool) + edges := []RunDisplayEdge{} + + for _, dep := range deps { + rawFrom := nonEmpty(dep.from) + rawTo := nonEmpty(dep.to) + if rawFrom == "" || rawTo == "" { + continue + } + if nonEmpty(dep.kind) == "tracks" { + continue + } + from := semanticOf(physicalToSemantic, rawFrom) + to := semanticOf(physicalToSemantic, rawTo) + kind := nonEmpty(dep.kind) + hasKind := kind != "" + if visible[from] && visible[to] { + edges = pushEdge(edges, seen, from, to, kind, hasKind) + continue + } + if visible[from] && bridgeableHiddenIDs[rawTo] { + edges = bridgeHiddenEdges(edges, seen, from, rawTo, outgoing, visible, bridgeableHiddenIDs, physicalToSemantic, kind, hasKind, make(map[string]bool)) + } + } + return edges +} + +func bridgeHiddenEdges(edges []RunDisplayEdge, seen map[string]bool, source, currentRawID string, outgoing map[string][]runSnapshotDep, visible, bridgeableHiddenIDs map[string]bool, physicalToSemantic map[string]string, inheritedKind string, hasInheritedKind bool, visited map[string]bool) []RunDisplayEdge { + if visited[currentRawID] { + return edges + } + visited[currentRawID] = true + for _, dep := range outgoing[currentRawID] { + rawTo := nonEmpty(dep.to) + if rawTo == "" { + continue + } + kind := nonEmpty(dep.kind) + if kind == "tracks" { + continue + } + target := semanticOf(physicalToSemantic, rawTo) + edgeKind, hasEdgeKind := kind, kind != "" + if !hasEdgeKind { + edgeKind, hasEdgeKind = inheritedKind, hasInheritedKind + } + if visible[target] { + edges = pushEdge(edges, seen, source, target, edgeKind, hasEdgeKind) + } else if bridgeableHiddenIDs[rawTo] { + edges = bridgeHiddenEdges(edges, seen, source, rawTo, outgoing, visible, bridgeableHiddenIDs, physicalToSemantic, edgeKind, hasEdgeKind, visited) + } + } + return edges +} + +func pushEdge(edges []RunDisplayEdge, seen map[string]bool, from, to, kind string, hasKind bool) []RunDisplayEdge { + if from == to { + return edges + } + edgeKind := "dependency" + if hasKind { + edgeKind = kind + } + key := from + "->" + to + ":" + edgeKind + if seen[key] { + return edges + } + seen[key] = true + return append(edges, RunDisplayEdge{From: from, To: to, Kind: edgeKind}) +} + +func outgoingDeps(deps []runSnapshotDep) map[string][]runSnapshotDep { + out := make(map[string][]runSnapshotDep) + for _, dep := range deps { + from := nonEmpty(dep.from) + to := nonEmpty(dep.to) + if from == "" || to == "" { + continue + } + out[from] = append(out[from], dep) + } + return out +} + +// bridgeableScopeCheckIDs collects the bead ids of scope-check constructs, which +// edges may bridge across. Port of TS bridgeableScopeCheckIds. +func bridgeableScopeCheckIDs(raw runSnapshot) map[string]bool { + ids := make(map[string]bool) + for _, bead := range raw.beads { + id := nonEmpty(bead.id) + if id == "" { + continue + } + kind := nonEmpty(bead.metadata[beadmeta.KindMetadataKey]) + if kind == "" { + kind = nonEmpty(bead.kind) + } + if kind == "scope-check" { + ids[id] = true + } + } + return ids +} + +// semanticOf maps a physical id to its semantic id, falling back to the +// externalized raw id (TS `physicalToSemantic.get(raw) ?? externalizeId(raw)`). +func semanticOf(physicalToSemantic map[string]string, rawID string) string { + if semantic, ok := physicalToSemantic[rawID]; ok { + return semantic + } + return externalizeID(rawID) +} diff --git a/internal/runproj/detail_formulaname.go b/internal/runproj/detail_formulaname.go new file mode 100644 index 0000000000..d6a66e66e9 --- /dev/null +++ b/internal/runproj/detail_formulaname.go @@ -0,0 +1,135 @@ +package runproj + +import "github.com/gastownhall/gascity/internal/beadmeta" + +// resolveRunFormulaIdentityDetailState resolves a run root's formula name, +// provenance, and target for the 'detail'/'state' modes. Port of the +// non-lane path through TS resolveRunFormulaIdentity (formula-name.ts): the +// 'detail' and 'state' modes share every branch (the only mode-specific logic in +// resolveRunFormulaIdentity is the lane handling), so one function serves both. +// The bool mirrors `name: string | null`; target is "" when absent. +func resolveRunFormulaIdentityDetailState(root *runSnapshotBead, formulaDetailName string, hasFormulaDetail bool) (name, source, target string, hasName bool) { + target = runFormulaTarget(root) + + if metadata := runFormulaMetadataNameRoot(root); metadata != "" { + return metadata, "metadata", target, true + } + if hasFormulaDetail { + if detailName := nonEmpty(formulaDetailName); detailName != "" { + return detailName, "formula_detail", target, true + } + } + if title, ok := runFormulaTitleFallbackDetail(root); ok { + return title, "title_fallback", target, true + } + return "", "", target, false +} + +// runFormulaMetadataNameRoot resolves the explicit formula name from root +// metadata. Port of the non-lane runFormulaMetadataName. +func runFormulaMetadataNameRoot(root *runSnapshotBead) string { + if v := rootMetaPtr(root, beadmeta.FormulaMetadataKey); v != "" { + return v + } + return rootMetaPtr(root, beadmeta.FormulaNameMetadataKey) +} + +// runFormulaTitleFallbackDetail is the graph.v2 title fallback for the +// detail/state modes (no 'mol-' prefix gate, unlike lane mode). Port of +// runFormulaTitleFallback for non-lane modes. +func runFormulaTitleFallbackDetail(root *runSnapshotBead) (string, bool) { + if root == nil { + return "", false + } + if rootMetaPtr(root, beadmeta.FormulaContractMetadataKey) != "graph.v2" || + rootMetaPtr(root, beadmeta.RunTargetMetadataKey) == "" || + isTerminalRunRootStatus(root.status) { + return "", false + } + title := nonEmpty(root.title) + if title == "" { + return "", false + } + return title, true +} + +// runFormulaTarget resolves the run's routing target. Port of TS runFormulaTarget +// ("" mirrors null). +func runFormulaTarget(root *runSnapshotBead) string { + if v := rootMetaPtr(root, beadmeta.RunTargetMetadataKey); v != "" { + return v + } + if v := rootMetaPtr(root, beadmeta.RoutedToMetadataKey); v != "" { + return v + } + if root != nil { + return nonEmpty(root.assignee) + } + return "" +} + +func rootMetaPtr(root *runSnapshotBead, key string) string { + if root == nil { + return "" + } + return nonEmpty(root.metadata[key]) +} + +// ── execution-path.ts ─────────────────────────────────────────────────────── + +// resolveRunExecutionPath resolves the run's execution path from cwd/work_dir/ +// rig_root metadata, then the rig root argument. Port of TS resolveRunExecutionPath. +func resolveRunExecutionPath(root *runSnapshotBead, beads []runSnapshotBead, rigRoot string) RunExecutionPath { + if path, ok := executionWorkDirsPtr(root); ok { + return RunExecutionPath{Kind: "known", Path: path} + } + for i := range beads { + if path, ok := executionWorkDirs(beads[i]); ok { + return RunExecutionPath{Kind: "known", Path: path} + } + } + if path, ok := rigRootsPtr(root); ok { + return RunExecutionPath{Kind: "known", Path: path} + } + for i := range beads { + if path, ok := rigRoots(beads[i]); ok { + return RunExecutionPath{Kind: "known", Path: path} + } + } + if path := nonEmpty(rigRoot); path != "" { + return RunExecutionPath{Kind: "known", Path: path} + } + return RunExecutionPath{Kind: "unavailable", Reason: "missing_cwd_and_rig_root"} +} + +func executionWorkDirs(bead runSnapshotBead) (string, bool) { + for _, key := range []string{beadmeta.CwdMetadataKey, "cwd", beadmeta.WorkDirMetadataKey, "work_dir"} { + if v := beadMeta(bead, key); v != "" { + return v, true + } + } + return "", false +} + +func executionWorkDirsPtr(bead *runSnapshotBead) (string, bool) { + if bead == nil { + return "", false + } + return executionWorkDirs(*bead) +} + +func rigRoots(bead runSnapshotBead) (string, bool) { + for _, key := range []string{beadmeta.RigRootMetadataKey, "rig_root"} { + if v := beadMeta(bead, key); v != "" { + return v, true + } + } + return "", false +} + +func rigRootsPtr(bead *runSnapshotBead) (string, bool) { + if bead == nil { + return "", false + } + return rigRoots(*bead) +} diff --git a/internal/runproj/detail_golden_test.go b/internal/runproj/detail_golden_test.go new file mode 100644 index 0000000000..63ad534199 --- /dev/null +++ b/internal/runproj/detail_golden_test.go @@ -0,0 +1,60 @@ +package runproj + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/gastownhall/gascity/internal/beads" +) + +// detailGoldenRunID is the graph.v2 run captured as the FormulaRunDetail golden +// (rundetail_golden.json), the Go-owned reference fixture. +const detailGoldenRunID = "dt-adopt1" + +// detailGoldenSnapshotVersion / detailGoldenSnapshotEventSeq are the +// snapshot_version=1 / snapshot_event_seq=100 constants that appear verbatim in +// the golden's snapshotVersion/snapshotEventSeq. +const ( + detailGoldenSnapshotVersion = 1 + detailGoldenSnapshotEventSeq = 100 +) + +// TestBuildRunDetailGolden pins the Go port of the detail pipeline to the +// TypeScript oracle: it loads the shared bead fixture, builds the detail for the +// captured run, and asserts the canonical JSON matches rundetail_golden.json +// byte-for-byte (same JSON.stringify(value, null, 2)+newline canonicalization the +// generator used). +func TestBuildRunDetailGolden(t *testing.T) { + fixturePath := filepath.Join("testdata", "beads_fixture.json") + goldenPath := filepath.Join("testdata", "rundetail_golden.json") + + raw, err := os.ReadFile(fixturePath) + if err != nil { + t.Fatalf("read fixture: %v", err) + } + var beadList []beads.Bead + if err := json.Unmarshal(raw, &beadList); err != nil { + t.Fatalf("unmarshal fixture: %v", err) + } + + detail, err := BuildRunDetail(beadList, detailGoldenRunID, detailGoldenSnapshotVersion, detailGoldenSnapshotEventSeq) + if err != nil { + t.Fatalf("BuildRunDetail: %v", err) + } + got, err := canonicalJSON(detail) + if err != nil { + t.Fatalf("marshal detail: %v", err) + } + + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("read golden: %v", err) + } + + if !bytes.Equal(got, want) { + t.Errorf("run detail does not match golden:\n%s", unifiedDiff(string(want), string(got))) + } +} diff --git a/internal/runproj/detail_groups.go b/internal/runproj/detail_groups.go new file mode 100644 index 0000000000..a3ca812752 --- /dev/null +++ b/internal/runproj/detail_groups.go @@ -0,0 +1,453 @@ +package runproj + +import ( + "sort" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// runNodeGroup is one semantic node and the physical beads grouped under it. +// Port of TS RunNodeGroup (execution-instances.ts). scopeRef and +// loopControlNodeID use "" to mean undefined (groupOptional only ever yields a +// non-empty string or undefined). +type runNodeGroup struct { + semanticNodeID string + title string + kind string + constructKind string + scopeRef string + loopControlNodeID string + beads []runSnapshotBead +} + +// runBeadGroups is the output of groupRunBeads. Port of TS RunBeadGroups, with +// groups carried in first-seen semantic-id order. +type runBeadGroups struct { + groups []runNodeGroup + physicalToSemantic map[string]string + badgesByTarget map[string][]RunControlBadge +} + +// beadIdentity is a bead's resolved semantic identity. Port of TS BeadIdentity +// (hasDisambiguator mirrors `disambiguator: string | undefined`). +type beadIdentity struct { + base string + disambiguator string + hasDisambiguator bool + semanticNodeID string +} + +// groupRunBeads partitions a run's beads into semantic node groups, mapping +// physical bead ids to semantic ids and collecting hidden-construct badges. +// Port of TS groupRunBeads (groups.ts). Beads are addressed by index to mirror +// the TS Map keyed by object identity. +func groupRunBeads(beads []runSnapshotBead, rootBeadID string) runBeadGroups { + physicalToSemantic := make(map[string]string) + badgesByTarget := make(map[string][]RunControlBadge) + physicalLogicalTargets := referencedPhysicalLogicalTargets(beads) + identities := resolveBeadIdentities(beads, rootBeadID, physicalLogicalTargets) + badgeTargetAliases := buildBadgeTargetAliases(beads, rootBeadID, identities, physicalLogicalTargets) + + grouped := make(map[string][]runSnapshotBead) + var groupOrder []string + + for i := range beads { + bead := beads[i] + beadID := nonEmpty(bead.id) + constructKind := constructKindFor(bead, rootBeadID) + semanticNodeID := semanticNodeIDFor(bead, rootBeadID) + if id, ok := identities[i]; ok { + semanticNodeID = id.semanticNodeID + } + physicalToSemantic[beadID] = semanticNodeID + + if isHiddenConstruct(constructKind) { + target := hiddenBadgeTargetFor(bead, rootBeadID) + resolvedTarget, ok := resolveBadgeTarget(bead, rootBeadID, badgeTargetAliases, target) + if ok { + id := beadID + if id == "" { + id = resolvedTarget + "-" + constructKind + } + badgesByTarget[resolvedTarget] = append(badgesByTarget[resolvedTarget], RunControlBadge{ + ID: id, + Label: badgeLabelFor(constructKind), + Status: presentationStatus(bead), + }) + } + continue + } + + if _, ok := grouped[semanticNodeID]; !ok { + groupOrder = append(groupOrder, semanticNodeID) + } + grouped[semanticNodeID] = append(grouped[semanticNodeID], bead) + } + + groups := make([]runNodeGroup, 0, len(groupOrder)) + for _, semanticNodeID := range groupOrder { + groups = append(groups, buildRunNodeGroup(semanticNodeID, grouped[semanticNodeID], rootBeadID)) + } + + return runBeadGroups{ + groups: groups, + physicalToSemantic: physicalToSemantic, + badgesByTarget: badgesByTarget, + } +} + +// buildRunNodeGroup assembles one semantic node group. Port of TS +// buildRunNodeGroup. +func buildRunNodeGroup(semanticNodeID string, beads []runSnapshotBead, rootBeadID string) runNodeGroup { + shapeBead := preferredShapeBead(beads, rootBeadID) + constructKind := constructKindFor(shapeBead, rootBeadID) + scopeRef := groupOptional(beads, shapeBead, func(b runSnapshotBead) string { + if v := beadMeta(b, beadmeta.ScopeRefMetadataKey); v != "" { + return v + } + return nonEmpty(b.scopeRef) + }) + loopControlNodeID := groupOptional(beads, shapeBead, loopControlNodeIDFor) + + return runNodeGroup{ + semanticNodeID: semanticNodeID, + title: displayTitleFor(shapeBead, semanticNodeID), + kind: externalKindFor(shapeBead, constructKind), + constructKind: constructKind, + scopeRef: scopeRef, + loopControlNodeID: loopControlNodeID, + beads: beads, + } +} + +// preferredShapeBead picks the bead that drives a group's shape: highest +// construct priority, then lowest sort key. Port of TS preferredShapeBead (a +// stable sort, mirrored with sort.SliceStable). localeCompare is approximated by +// byte comparison, matching the P1 convention. +func preferredShapeBead(beads []runSnapshotBead, rootBeadID string) runSnapshotBead { + if len(beads) == 0 { + // buildRunNodeGroup is only ever called with a non-empty group; mirror the + // TS throw defensively. + panic("runproj: cannot build run node group from zero beads") + } + sorted := make([]runSnapshotBead, len(beads)) + copy(sorted, beads) + sort.SliceStable(sorted, func(i, j int) bool { + priorityDiff := constructPriority(constructKindFor(sorted[j], rootBeadID)) - + constructPriority(constructKindFor(sorted[i], rootBeadID)) + if priorityDiff != 0 { + return priorityDiff < 0 + } + return strings.Compare(beadSortKey(sorted[i]), beadSortKey(sorted[j])) < 0 + }) + return sorted[0] +} + +// groupOptional resolves an optional group field: the shape bead's value, else +// the first defined value across the sorted beads. Port of TS groupOptional ("" = +// undefined). +func groupOptional(beads []runSnapshotBead, shapeBead runSnapshotBead, resolve func(runSnapshotBead) string) string { + if v := resolve(shapeBead); v != "" { + return v + } + for _, b := range sortedBeads(beads) { + if v := resolve(b); v != "" { + return v + } + } + return "" +} + +// constructPriority ranks construct kinds for shape-bead selection. Port of TS +// constructPriority. +func constructPriority(kind string) int { + switch kind { + case "run-root": + return 100 + case "check-loop": + return 90 + case "retry": + return 80 + case "condition", "fanout", "scope", "expansion": + return 70 + case "step": + return 10 + default: // control, run-finalize, scope-check, spec, unknown + return 0 + } +} + +// sortedBeads returns beads sorted by sort key. Port of TS sortedBeads. +func sortedBeads(beads []runSnapshotBead) []runSnapshotBead { + sorted := make([]runSnapshotBead, len(beads)) + copy(sorted, beads) + sort.SliceStable(sorted, func(i, j int) bool { + return strings.Compare(beadSortKey(sorted[i]), beadSortKey(sorted[j])) < 0 + }) + return sorted +} + +// beadSortKey builds a bead's deterministic sort key. Port of TS beadSortKey. +func beadSortKey(b runSnapshotBead) string { + parts := make([]string, 0, 3) + if v := nonEmpty(b.id); v != "" { + parts = append(parts, v) + } + if v := normalizedStepRef(b); v != "" { + parts = append(parts, v) + } + if v := nonEmpty(b.title); v != "" { + parts = append(parts, v) + } + return strings.Join(parts, "\x00") +} + +// resolveBeadIdentities computes each non-hidden bead's semantic identity, +// disambiguating only when a base id is shared. Port of TS resolveBeadIdentities. +func resolveBeadIdentities(beads []runSnapshotBead, rootBeadID string, physicalLogicalTargets map[string]bool) map[int]beadIdentity { + partial := make(map[int]beadIdentity) + identitiesByBase := make(map[string]map[string]bool) + + for i := range beads { + bead := beads[i] + if isHiddenConstruct(constructKindFor(bead, rootBeadID)) { + continue + } + base := groupingBaseSemanticID(bead, rootBeadID, physicalLogicalTargets) + disambiguator, hasDisambiguator := duplicateResolutionIdentity(bead, rootBeadID, base, physicalLogicalTargets) + partial[i] = beadIdentity{base: base, disambiguator: disambiguator, hasDisambiguator: hasDisambiguator} + + identity := base + if hasDisambiguator { + identity = disambiguator + } + if identitiesByBase[base] == nil { + identitiesByBase[base] = make(map[string]bool) + } + identitiesByBase[base][identity] = true + } + + resolved := make(map[int]beadIdentity, len(beads)) + for i := range beads { + id, ok := partial[i] + if !ok { + id = beadIdentity{base: semanticNodeIDFor(beads[i], rootBeadID)} + } + semanticNodeID := id.base + if set := identitiesByBase[id.base]; len(set) > 1 && id.hasDisambiguator { + semanticNodeID = id.disambiguator + } + id.semanticNodeID = semanticNodeID + resolved[i] = id + } + return resolved +} + +// groupingBaseSemanticID resolves a bead's grouping base id. Port of TS +// groupingBaseSemanticId. +func groupingBaseSemanticID(b runSnapshotBead, rootBeadID string, physicalLogicalTargets map[string]bool) string { + beadID := nonEmpty(b.id) + if beadID != "" && beadID == rootBeadID { + return rootBeadID + } + if explicit := explicitLogicalBeadID(b); explicit != "" { + return externalizeID(explicit) + } + constructKind := constructKindFor(b, rootBeadID) + if (constructKind == "check-loop" || constructKind == "retry") && beadID != "" && physicalLogicalTargets[beadID] { + return externalizeID(beadID) + } + return semanticNodeIDFor(b, rootBeadID) +} + +// duplicateResolutionIdentity resolves the disambiguator candidate for a bead. +// Port of TS duplicateResolutionIdentity (bool mirrors undefined). +func duplicateResolutionIdentity(b runSnapshotBead, rootBeadID, base string, physicalLogicalTargets map[string]bool) (string, bool) { + beadID := nonEmpty(b.id) + if beadID != "" && physicalLogicalTargets[beadID] && externalizeID(beadID) == base { + return base, true + } + return stableSemanticIdentity(b, rootBeadID) +} + +// buildBadgeTargetAliases maps every alias that uniquely identifies one visible +// node to that node. Port of TS buildBadgeTargetAliases. +func buildBadgeTargetAliases(beads []runSnapshotBead, rootBeadID string, identities map[int]beadIdentity, physicalLogicalTargets map[string]bool) map[string]string { + candidates := make(map[string]map[string]bool) + + for i := range beads { + bead := beads[i] + if isHiddenConstruct(constructKindFor(bead, rootBeadID)) { + continue + } + id, hasIdentity := identities[i] + resolvedTarget := semanticNodeIDFor(bead, rootBeadID) + if hasIdentity { + resolvedTarget = id.semanticNodeID + } + var identityPtr *beadIdentity + if hasIdentity { + identityPtr = &id + } + for _, alias := range visibleNodeAliases(bead, rootBeadID, identityPtr, physicalLogicalTargets) { + if candidates[alias] == nil { + candidates[alias] = make(map[string]bool) + } + candidates[alias][resolvedTarget] = true + } + } + + aliases := make(map[string]string) + for alias, targets := range candidates { + if len(targets) == 1 { + for target := range targets { + aliases[alias] = target + } + } + } + return aliases +} + +// visibleNodeAliases lists the alias strings under which a visible node can be +// referenced. Port of TS visibleNodeAliases (undefined entries dropped, each +// externalized). +func visibleNodeAliases(b runSnapshotBead, rootBeadID string, identity *beadIdentity, physicalLogicalTargets map[string]bool) []string { + resolvedTarget := semanticNodeIDFor(b, rootBeadID) + base := groupingBaseSemanticID(b, rootBeadID, physicalLogicalTargets) + if identity != nil { + resolvedTarget = identity.semanticNodeID + base = identity.base + } + + var raw []string + raw = append(raw, resolvedTarget) + raw = append(raw, semanticNodeIDFor(b, rootBeadID)) + raw = append(raw, base) + if identity != nil && identity.hasDisambiguator { + raw = append(raw, identity.disambiguator) + } + if v, ok := stableSemanticIdentity(b, rootBeadID); ok { + raw = append(raw, v) + } + if v := beadMeta(b, beadmeta.StepIDMetadataKey); v != "" { + raw = append(raw, v) + } + if v, ok := fullStepRefIdentity(normalizedStepRef(b)); ok { + raw = append(raw, v) + } + if v := nonEmpty(b.id); v != "" { + raw = append(raw, v) + } + + out := make([]string, 0, len(raw)) + for _, v := range raw { + out = append(out, externalizeID(v)) + } + return out +} + +// referencedPhysicalLogicalTargets collects bead ids that another bead references +// as its logical bead id. Port of TS referencedPhysicalLogicalTargets. +func referencedPhysicalLogicalTargets(beads []runSnapshotBead) map[string]bool { + beadIDs := make(map[string]bool) + for i := range beads { + if id := nonEmpty(beads[i].id); id != "" { + beadIDs[id] = true + } + } + targets := make(map[string]bool) + for i := range beads { + logical := explicitLogicalBeadID(beads[i]) + if logical != "" && beadIDs[logical] { + targets[logical] = true + } + } + return targets +} + +// resolveBadgeTarget resolves the visible node a hidden bead's badge attaches to. +// Port of TS resolveBadgeTarget (bool mirrors a non-null result). +func resolveBadgeTarget(b runSnapshotBead, rootBeadID string, aliases map[string]string, fallback string) (string, bool) { + if constructKindFor(b, rootBeadID) == "run-finalize" { + return rootBeadID, true + } + for _, candidate := range hiddenBadgeTargetCandidates(b, fallback) { + if target, ok := aliases[candidate]; ok && target != "" { + return target, true + } + } + if fallback != "" { + return fallback, true + } + return "", false +} + +// hiddenBadgeTargetCandidates lists the alias candidates a hidden badge resolves +// against. Port of TS hiddenBadgeTargetCandidates. +func hiddenBadgeTargetCandidates(b runSnapshotBead, fallback string) []string { + var sources []string + if v := beadMeta(b, beadmeta.ControlForMetadataKey); v != "" { + sources = append(sources, v) + } + if v, ok := hiddenBadgeFullTargetFor(b); ok { + sources = append(sources, v) + } + if fallback != "" { + sources = append(sources, fallback) + } + + var out []string + for _, value := range sources { + stripped := stripScopeCheckSuffix(value) + out = append(out, externalizeID(value), externalizeID(stripped), externalizeID(externalizeID(stripped))) + } + return out +} + +// stableSemanticIdentity resolves a bead's stable semantic identity. Port of TS +// stableSemanticIdentity (bool mirrors undefined). +func stableSemanticIdentity(b runSnapshotBead, rootBeadID string) (string, bool) { + beadID := nonEmpty(b.id) + if beadID != "" && beadID == rootBeadID { + return rootBeadID, true + } + if explicit := explicitLogicalBeadID(b); explicit != "" { + return externalizeID(explicit), true + } + if stepID := beadMeta(b, beadmeta.StepIDMetadataKey); stepID != "" { + return externalizeID(stepID), true + } + return fullStepRefIdentity(normalizedStepRef(b)) +} + +// hiddenBadgeFullTargetFor resolves the full-ref target of a hidden badge. Port +// of TS hiddenBadgeFullTargetFor (bool mirrors undefined). +func hiddenBadgeFullTargetFor(b runSnapshotBead) (string, bool) { + if controlFor := beadMeta(b, beadmeta.ControlForMetadataKey); controlFor != "" { + return externalizeID(stripScopeCheckSuffix(controlFor)), true + } + return fullStepRefIdentity(stripScopeCheckSuffix(normalizedStepRef(b))) +} + +// fullStepRefIdentity resolves the full (non-root-prefixed) step ref identity. +// Port of TS fullStepRefIdentity (bool mirrors undefined). +func fullStepRefIdentity(ref string) (string, bool) { + clean := nonEmpty(ref) + if clean == "" { + return "", false + } + stripped := stripScopeCheckSuffix(clean) + parts := splitNonEmpty(stripped, ".") + if len(parts) == 0 { + return "", false + } + if len(parts) == 1 { + first := parts[0] + if first == "" { + first = stripped + } + return externalizeID(first), true + } + return externalizeID(strings.Join(parts[1:], ".")), true +} diff --git a/internal/runproj/detail_instances.go b/internal/runproj/detail_instances.go new file mode 100644 index 0000000000..f022dc6323 --- /dev/null +++ b/internal/runproj/detail_instances.go @@ -0,0 +1,341 @@ +package runproj + +import ( + "sort" + "strconv" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// buildRunDisplayNode projects one semantic group into a display node, including +// its execution instances, iteration/attempt summaries, and control badges. +// Port of TS buildRunDisplayNode (execution-instances.ts). latestLoopIteration's +// bool mirrors `number | undefined`. +func buildRunDisplayNode(group runNodeGroup, controlBadges []RunControlBadge, latestLoopIteration int, hasLatestLoopIteration bool, ctx runSessionLinkContext) RunDisplayNode { + instances := make([]RunExecutionInstance, 0, len(group.beads)) + for index := range group.beads { + instances = append(instances, buildExecutionInstance(group.semanticNodeID, group.beads[index], index, ctx)) + } + sortExecutionInstances(instances) + + // preferredExecutionInstance re-sorts (a no-op on the already-sorted slice) + // and takes the last element. + hasVisible := len(instances) > 0 + + iterationSet := map[int]bool{} + for _, inst := range instances { + if v, ok := iterationValue(inst.Iteration); ok { + iterationSet[v] = true + } + } + + visibleIteration, hasVisibleIteration := 0, false + if hasVisible { + if v, ok := iterationValue(instances[len(instances)-1].Iteration); ok { + visibleIteration, hasVisibleIteration = v, true + } + } + if !hasVisibleIteration && len(iterationSet) > 0 { + visibleIteration, hasVisibleIteration = maxKey(iterationSet), true + } + + historicalOnly := group.loopControlNodeID != "" && + hasVisibleIteration && + hasLatestLoopIteration && + visibleIteration < latestLoopIteration + + for k := range instances { + // currentIteration = !historicalOnly && (visibleIteration is undefined || + // instance's loop value === visibleIteration). A base (non-loop) instance + // under a known visible iteration compares undefined === number → false. + currentIteration := !historicalOnly + if currentIteration && hasVisibleIteration { + v, ok := iterationValue(instances[k].Iteration) + currentIteration = ok && v == visibleIteration + } + instances[k].CurrentIteration = currentIteration + instances[k].Historical = !currentIteration + if instances[k].Session.Kind == "attached" { + instances[k].Session.Streamable = currentIteration && isRunningStatus(instances[k].Status) + } + } + + if !hasVisible { + panic("runproj: run node " + group.semanticNodeID + " has no execution instances") + } + visible := &instances[len(instances)-1] + + if controlBadges == nil { + controlBadges = []RunControlBadge{} + } + + return RunDisplayNode{ + ID: group.semanticNodeID, + SemanticNodeID: group.semanticNodeID, + Title: group.title, + Kind: group.kind, + ConstructKind: group.constructKind, + Status: aggregateStatus(instances, visible), + CurrentBeadID: visible.BeadID, + Scope: runNodeScope(group.scopeRef), + VisibleInGraph: !historicalOnly, + HistoricalOnly: historicalOnly, + IterationSummary: iterationSummaryFor(visibleIteration, hasVisibleIteration, len(iterationSet), group.loopControlNodeID), + AttemptSummary: attemptSummaryFor(instances, group.beads), + VisibleExecutionInstanceID: visible.ID, + ExecutionInstances: instances, + ControlBadges: controlBadges, + } +} + +// latestIterationsByLoop computes the furthest iteration reached per loop-control +// node. Port of TS latestIterationsByLoop. +func latestIterationsByLoop(groups []runNodeGroup) map[string]int { + latest := make(map[string]int) + for _, group := range groups { + if group.loopControlNodeID == "" { + continue + } + for _, bead := range group.beads { + iteration, ok := iterationFor(bead) + if !ok { + continue + } + if cur, exists := latest[group.loopControlNodeID]; !exists || iteration > cur { + latest[group.loopControlNodeID] = iteration + } + } + } + return latest +} + +// buildExecutionInstance projects one physical bead into an execution instance. +// Port of TS buildExecutionInstance. +func buildExecutionInstance(semanticNodeID string, bead runSnapshotBead, index int, ctx runSessionLinkContext) RunExecutionInstance { + beadID := nonEmpty(bead.id) + if beadID == "" { + panic("runproj: run node " + semanticNodeID + " has a bead with an empty id") + } + iteration, hasIteration := iterationFor(bead) + attempt, hasAttempt := attemptFor(bead) + status := presentationStatus(bead) + sessionLink, hasLink := runSessionLinkFor(bead, status, ctx) + + id := beadID + if id == "" { + iterPart := 0 + if hasIteration { + iterPart = iteration + } + attemptPart := index + if hasAttempt { + attemptPart = attempt + } + id = semanticNodeID + ":iteration-" + strconv.Itoa(iterPart) + ":attempt-" + strconv.Itoa(attemptPart) + } + + return RunExecutionInstance{ + ID: id, + SemanticNodeID: semanticNodeID, + BeadID: beadID, + Iteration: iterationState(iteration, hasIteration), + Attempt: attemptState(attempt, hasAttempt), + Label: instanceLabel(iteration, hasIteration, attempt, hasAttempt), + Status: status, + Session: sessionState(status, sessionLink, hasLink), + CurrentIteration: true, + Historical: false, + } +} + +// sortExecutionInstances stably sorts instances by iteration, then attempt, then +// bead id. Port of TS compareExecutionInstances (localeCompare approximated by +// byte comparison). +func sortExecutionInstances(instances []RunExecutionInstance) { + sort.SliceStable(instances, func(i, j int) bool { + if d := iterationOrder(instances[i].Iteration) - iterationOrder(instances[j].Iteration); d != 0 { + return d < 0 + } + if d := attemptOrder(instances[i].Attempt) - attemptOrder(instances[j].Attempt); d != 0 { + return d < 0 + } + return strings.Compare(instances[i].BeadID, instances[j].BeadID) < 0 + }) +} + +// attemptSummaryFor derives a node's attempt summary. Port of TS attemptSummaryFor. +func attemptSummaryFor(instances []RunExecutionInstance, beads []runSnapshotBead) RunAttemptSummary { + attemptCount := attemptCountFor(instances) + activeAttempt, hasActive := activeAttemptFor(instances) + badgeLabel, hasBadge := attemptBadgeFor(beads) + if attemptCount == 0 && !hasBadge { + return RunAttemptSummary{Kind: "none"} + } + count := attemptCount + if count < 1 { + count = 1 + } + badge := RunAttemptBadge{Kind: "count-only"} + if hasBadge { + badge = RunAttemptBadge{Kind: "bounded", Label: badgeLabel} + } + active := RunAttemptActive{Kind: "idle"} + if hasActive { + active = RunAttemptActive{Kind: "running", Value: activeAttempt} + } + return RunAttemptSummary{Kind: "tracked", Count: count, Badge: badge, Active: active} +} + +// attemptBadgeFor derives the bounded attempt badge from gc.max_attempts. Port of +// TS attemptBadgeFor. +func attemptBadgeFor(beads []runSnapshotBead) (string, bool) { + maxAttempts, hasMax := 0, false + for _, bead := range beads { + if v, ok := positiveIntegerMeta(bead, beadmeta.MaxAttemptsMetadataKey); ok { + maxAttempts, hasMax = v, true + break + } + } + if !hasMax { + return "", false + } + attempts := map[int]bool{} + for _, bead := range beads { + if v, ok := attemptFor(bead); ok { + attempts[v] = true + } + } + size := len(attempts) + if size < 1 { + size = 1 + } + return strconv.Itoa(size) + "/" + strconv.Itoa(maxAttempts), true +} + +func attemptCountFor(instances []RunExecutionInstance) int { + attempts := map[int]bool{} + for _, inst := range instances { + if v, ok := attemptValue(inst.Attempt); ok { + attempts[v] = true + } + } + return len(attempts) +} + +func activeAttemptFor(instances []RunExecutionInstance) (int, bool) { + for _, inst := range instances { + if isRunningStatus(inst.Status) { + return attemptValue(inst.Attempt) + } + } + return 0, false +} + +// instanceLabel renders an instance's iteration/attempt label. Port of TS +// instanceLabel. +func instanceLabel(iteration int, hasIteration bool, attempt int, hasAttempt bool) string { + if hasIteration && hasAttempt { + return "iteration " + strconv.Itoa(iteration) + ", attempt " + strconv.Itoa(attempt) + } + if hasIteration { + return "iteration " + strconv.Itoa(iteration) + } + if hasAttempt { + return "attempt " + strconv.Itoa(attempt) + } + return "base" +} + +// runNodeScope renders a group's node scope. Port of TS runNodeScope ("" = +// undefined → run). +func runNodeScope(scopeRef string) RunNodeScope { + if scopeRef == "" { + return RunNodeScope{Kind: "run"} + } + return RunNodeScope{Kind: "scoped", Ref: scopeRef} +} + +// iterationSummaryFor renders a node's iteration summary. Port of TS +// iterationSummaryFor. +func iterationSummaryFor(visibleIteration int, hasVisibleIteration bool, iterationCount int, loopControlNodeID string) RunIterationSummary { + if !hasVisibleIteration || iterationCount == 0 { + return RunIterationSummary{Kind: "single"} + } + control := RunIterationControl{Kind: "unknown"} + if loopControlNodeID != "" { + control = RunIterationControl{Kind: "known", ID: loopControlNodeID} + } + return RunIterationSummary{ + Kind: "stacked", + VisibleIteration: visibleIteration, + IterationCount: iterationCount, + Control: control, + } +} + +func iterationState(value int, has bool) RunIteration { + if !has { + return RunIteration{Kind: "base"} + } + return RunIteration{Kind: "loop", Value: value} +} + +func attemptState(value int, has bool) RunAttempt { + if !has { + return RunAttempt{Kind: "untracked"} + } + return RunAttempt{Kind: "attempt", Value: value} +} + +// sessionState renders an instance's session attachment. Port of TS sessionState. +func sessionState(status string, link RunSessionLink, hasLink bool) RunSessionAttachment { + if hasLink { + return RunSessionAttachment{Kind: "attached", Link: link, Streamable: false} + } + reason := "session_unresolved" + if status == "pending" || status == "ready" { + reason = "not_started" + } + return RunSessionAttachment{Kind: "none", Reason: reason} +} + +func iterationValue(iteration RunIteration) (int, bool) { + if iteration.Kind == "loop" { + return iteration.Value, true + } + return 0, false +} + +func attemptValue(attempt RunAttempt) (int, bool) { + if attempt.Kind == "attempt" { + return attempt.Value, true + } + return 0, false +} + +func iterationOrder(iteration RunIteration) int { + if v, ok := iterationValue(iteration); ok { + return v + } + return 0 +} + +func attemptOrder(attempt RunAttempt) int { + if v, ok := attemptValue(attempt); ok { + return v + } + return 0 +} + +func maxKey(set map[int]bool) int { + first := true + best := 0 + for k := range set { + if first || k > best { + best = k + first = false + } + } + return best +} diff --git a/internal/runproj/detail_marshal.go b/internal/runproj/detail_marshal.go new file mode 100644 index 0000000000..daa40a948f --- /dev/null +++ b/internal/runproj/detail_marshal.go @@ -0,0 +1,245 @@ +package runproj + +import "fmt" + +// The detail DTO unions below mirror the summary unions in marshal.go: each +// carries every arm's fields and a custom MarshalJSON that emits exactly the +// active arm's keys, in the TS object-literal order. Key order is load-bearing +// for byte-for-byte golden parity. + +// nodeStatusCounts is a per-node-status tally that preserves first-seen status +// order, matching the TS Partial> insertion order +// (a Go map would sort keys and break parity). +type nodeStatusCounts struct { + keys []string + counts map[string]int +} + +func (c *nodeStatusCounts) inc(status string) { + if c.counts == nil { + c.counts = map[string]int{} + } + if _, ok := c.counts[status]; !ok { + c.keys = append(c.keys, status) + } + c.counts[status]++ +} + +// MarshalJSON renders the counts in first-seen order. +func (c nodeStatusCounts) MarshalJSON() ([]byte, error) { + pairs := make([]kv, 0, len(c.keys)) + for _, k := range c.keys { + pairs = append(pairs, kv{k, c.counts[k]}) + } + return marshalObject(pairs) +} + +// MarshalJSON renders the active formula arm. TS: {kind:'known', name, source} | +// {kind:'unavailable', reason}. +func (f RunFormula) MarshalJSON() ([]byte, error) { + switch f.Kind { + case "known": + return marshalObject([]kv{{"kind", "known"}, {"name", f.Name}, {"source", f.Source}}) + case "unavailable": + return marshalObject([]kv{{"kind", "unavailable"}, {"reason", f.Reason}}) + default: + return nil, fmt.Errorf("runproj: invalid RunFormula kind %q", f.Kind) + } +} + +// MarshalJSON renders the active formula-detail arm. TS: {kind:'available', name, +// target} | {kind:'unavailable', reason} (+ name / +name,target,failure variants). +func (s RunFormulaDetailState) MarshalJSON() ([]byte, error) { + switch { + case s.Kind == "available": + return marshalObject([]kv{{"kind", "available"}, {"name", s.Name}, {"target", s.Target}}) + case s.Kind == "unavailable" && s.Reason == "missing_formula_metadata": + return marshalObject([]kv{{"kind", "unavailable"}, {"reason", s.Reason}}) + case s.Kind == "unavailable" && s.Reason == "missing_run_target": + return marshalObject([]kv{{"kind", "unavailable"}, {"reason", s.Reason}, {"name", s.Name}}) + case s.Kind == "unavailable" && s.Reason == "fetch_failed": + return marshalObject([]kv{ + {"kind", "unavailable"}, + {"reason", s.Reason}, + {"name", s.Name}, + {"target", s.Target}, + {"failure", s.Failure}, + }) + default: + return nil, fmt.Errorf("runproj: invalid RunFormulaDetailState kind=%q reason=%q", s.Kind, s.Reason) + } +} + +// MarshalJSON renders the active execution-path arm. TS: {kind:'known', path} | +// {kind:'unavailable', reason}. +func (p RunExecutionPath) MarshalJSON() ([]byte, error) { + switch p.Kind { + case "known": + return marshalObject([]kv{{"kind", "known"}, {"path", p.Path}}) + case "unavailable": + return marshalObject([]kv{{"kind", "unavailable"}, {"reason", p.Reason}}) + default: + return nil, fmt.Errorf("runproj: invalid RunExecutionPath kind %q", p.Kind) + } +} + +// MarshalJSON renders the active snapshot-sequence arm. TS: {kind:'known', seq} | +// {kind:'unavailable', reason}. +func (s RunSnapshotSequence) MarshalJSON() ([]byte, error) { + switch s.Kind { + case "known": + return marshalObject([]kv{{"kind", "known"}, {"seq", s.Seq}}) + case "unavailable": + return marshalObject([]kv{{"kind", "unavailable"}, {"reason", s.Reason}}) + default: + return nil, fmt.Errorf("runproj: invalid RunSnapshotSequence kind %q", s.Kind) + } +} + +// MarshalJSON renders the active completeness arm. TS: {kind:'complete'} | +// {kind:'partial', reasons}. +func (c FormulaRunCompleteness) MarshalJSON() ([]byte, error) { + switch c.Kind { + case "complete": + return marshalObject([]kv{{"kind", "complete"}}) + case "partial": + reasons := c.Reasons + if reasons == nil { + reasons = []string{} + } + return marshalObject([]kv{{"kind", "partial"}, {"reasons", reasons}}) + default: + return nil, fmt.Errorf("runproj: invalid FormulaRunCompleteness kind %q", c.Kind) + } +} + +// MarshalJSON renders the active node-scope arm. TS: {kind:'run'} | +// {kind:'scoped', ref}. +func (s RunNodeScope) MarshalJSON() ([]byte, error) { + switch s.Kind { + case "run": + return marshalObject([]kv{{"kind", "run"}}) + case "scoped": + return marshalObject([]kv{{"kind", "scoped"}, {"ref", s.Ref}}) + default: + return nil, fmt.Errorf("runproj: invalid RunNodeScope kind %q", s.Kind) + } +} + +// MarshalJSON renders the active iteration arm. TS: {kind:'base'} | +// {kind:'loop', value}. +func (i RunIteration) MarshalJSON() ([]byte, error) { + switch i.Kind { + case "base": + return marshalObject([]kv{{"kind", "base"}}) + case "loop": + return marshalObject([]kv{{"kind", "loop"}, {"value", i.Value}}) + default: + return nil, fmt.Errorf("runproj: invalid RunIteration kind %q", i.Kind) + } +} + +// MarshalJSON renders the active attempt arm. TS: {kind:'untracked'} | +// {kind:'attempt', value}. +func (a RunAttempt) MarshalJSON() ([]byte, error) { + switch a.Kind { + case "untracked": + return marshalObject([]kv{{"kind", "untracked"}}) + case "attempt": + return marshalObject([]kv{{"kind", "attempt"}, {"value", a.Value}}) + default: + return nil, fmt.Errorf("runproj: invalid RunAttempt kind %q", a.Kind) + } +} + +// MarshalJSON renders the active session-attachment arm. TS: {kind:'attached', +// link, streamable} | {kind:'none', reason}. +func (s RunSessionAttachment) MarshalJSON() ([]byte, error) { + switch s.Kind { + case "attached": + return marshalObject([]kv{ + {"kind", "attached"}, + {"link", s.Link}, + {"streamable", s.Streamable}, + }) + case "none": + return marshalObject([]kv{{"kind", "none"}, {"reason", s.Reason}}) + default: + return nil, fmt.Errorf("runproj: invalid RunSessionAttachment kind %q", s.Kind) + } +} + +// MarshalJSON renders the active iteration-summary arm. TS: {kind:'single'} | +// {kind:'stacked', visibleIteration, iterationCount, control}. +func (s RunIterationSummary) MarshalJSON() ([]byte, error) { + switch s.Kind { + case "single": + return marshalObject([]kv{{"kind", "single"}}) + case "stacked": + return marshalObject([]kv{ + {"kind", "stacked"}, + {"visibleIteration", s.VisibleIteration}, + {"iterationCount", s.IterationCount}, + {"control", s.Control}, + }) + default: + return nil, fmt.Errorf("runproj: invalid RunIterationSummary kind %q", s.Kind) + } +} + +// MarshalJSON renders the active iteration-control arm. TS: {kind:'known', id} | +// {kind:'unknown'}. +func (c RunIterationControl) MarshalJSON() ([]byte, error) { + switch c.Kind { + case "known": + return marshalObject([]kv{{"kind", "known"}, {"id", c.ID}}) + case "unknown": + return marshalObject([]kv{{"kind", "unknown"}}) + default: + return nil, fmt.Errorf("runproj: invalid RunIterationControl kind %q", c.Kind) + } +} + +// MarshalJSON renders the active attempt-summary arm. TS: {kind:'none'} | +// {kind:'tracked', count, badge, active}. +func (s RunAttemptSummary) MarshalJSON() ([]byte, error) { + switch s.Kind { + case "none": + return marshalObject([]kv{{"kind", "none"}}) + case "tracked": + return marshalObject([]kv{ + {"kind", "tracked"}, + {"count", s.Count}, + {"badge", s.Badge}, + {"active", s.Active}, + }) + default: + return nil, fmt.Errorf("runproj: invalid RunAttemptSummary kind %q", s.Kind) + } +} + +// MarshalJSON renders the active attempt-badge arm. TS: {kind:'bounded', label} | +// {kind:'count-only'}. +func (b RunAttemptBadge) MarshalJSON() ([]byte, error) { + switch b.Kind { + case "bounded": + return marshalObject([]kv{{"kind", "bounded"}, {"label", b.Label}}) + case "count-only": + return marshalObject([]kv{{"kind", "count-only"}}) + default: + return nil, fmt.Errorf("runproj: invalid RunAttemptBadge kind %q", b.Kind) + } +} + +// MarshalJSON renders the active attempt-active arm. TS: {kind:'running', value} +// | {kind:'idle'}. +func (a RunAttemptActive) MarshalJSON() ([]byte, error) { + switch a.Kind { + case "running": + return marshalObject([]kv{{"kind", "running"}, {"value", a.Value}}) + case "idle": + return marshalObject([]kv{{"kind", "idle"}}) + default: + return nil, fmt.Errorf("runproj: invalid RunAttemptActive kind %q", a.Kind) + } +} diff --git a/internal/runproj/detail_nodeshape.go b/internal/runproj/detail_nodeshape.go new file mode 100644 index 0000000000..b99007b6df --- /dev/null +++ b/internal/runproj/detail_nodeshape.go @@ -0,0 +1,546 @@ +package runproj + +import ( + "regexp" + "strconv" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// ── bead-fields.ts ────────────────────────────────────────────────────────── + +// beadMeta returns the trimmed string metadata value for key, or "" when absent +// or whitespace. Port of TS meta() (the metadata is always string-typed in Go, +// so the typeof guard is unconditional). "" stands in for TS's undefined; callers +// test `!= ""`. +func beadMeta(b runSnapshotBead, key string) string { + return nonEmpty(b.metadata[key]) +} + +// normalizedStepRef resolves a bead's step ref: gc.step_ref metadata, else the +// step_ref column. Port of TS normalizedStepRef ("" mirrors null). +func normalizedStepRef(b runSnapshotBead) string { + if ref := beadMeta(b, beadmeta.StepRefMetadataKey); ref != "" { + return ref + } + return nonEmpty(b.stepRef) +} + +// iterationFor resolves a bead's loop iteration. Port of TS iterationFor. The +// bool mirrors TS's `number | undefined`. +func iterationFor(b runSnapshotBead) (int, bool) { + if v, ok := numericMeta(b, beadmeta.IterationMetadataKey); ok { + return v, true + } + if v, ok := numericRefSegment(b, "iteration"); ok { + return v, true + } + return numericRefSegment(b, "run") +} + +// attemptFor resolves a bead's attempt number. Port of TS attemptFor. +func attemptFor(b runSnapshotBead) (int, bool) { + if v, ok := numericMeta(b, beadmeta.AttemptMetadataKey); ok { + return v, true + } + if b.attempt != nil { + if v, ok := numericFieldInt(*b.attempt); ok { + return v, true + } + } + return numericRefSegment(b, "attempt") +} + +// positiveIntegerMeta returns a strictly-positive integer metadata value. +// Port of TS positiveIntegerMeta. +func positiveIntegerMeta(b runSnapshotBead, key string) (int, bool) { + return numericMeta(b, key) +} + +var numericFieldRe = regexp.MustCompile(`^[1-9]\d*$`) + +// externalizeID rewrites whole-word "ralph" (case-insensitive) to "check-loop". +// Port of TS externalizeId. Go RE2 lacks the lookahead the TS pattern uses, so +// the word boundaries are matched manually (the leading boundary char is +// preserved, the trailing boundary is not consumed — overlapping boundaries +// like "ralph.ralph" both rewrite). +func externalizeID(s string) string { + return rewriteRalph(s, "check-loop") +} + +// rewriteRalph replaces each whole-word "ralph" (delimited by string edges or +// non-alphanumeric chars, case-insensitive) with replacement. +func rewriteRalph(s, replacement string) string { + const word = "ralph" + if !containsRalphWord(s) { + return s + } + lower := strings.ToLower(s) + var b strings.Builder + i := 0 + for i < len(s) { + if lower[i] == 'r' && strings.HasPrefix(lower[i:], word) { + beforeOK := i == 0 || !isASCIIAlnum(s[i-1]) + after := i + len(word) + afterOK := after >= len(s) || !isASCIIAlnum(s[after]) + if beforeOK && afterOK { + b.WriteString(replacement) + i = after + continue + } + } + b.WriteByte(s[i]) + i++ + } + return b.String() +} + +// containsRalphWord reports whether s contains a whole-word "ralph". Port of the +// boolean test in TS externalizeDisplayText. +func containsRalphWord(s string) bool { + const word = "ralph" + lower := strings.ToLower(s) + for i := 0; i+len(word) <= len(lower); i++ { + if !strings.HasPrefix(lower[i:], word) { + continue + } + beforeOK := i == 0 || !isASCIIAlnum(s[i-1]) + after := i + len(word) + afterOK := after >= len(s) || !isASCIIAlnum(s[after]) + if beforeOK && afterOK { + return true + } + } + return false +} + +func isASCIIAlnum(c byte) bool { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +func numericRefSegment(b runSnapshotBead, marker string) (int, bool) { + ref := normalizedStepRef(b) + if ref == "" { + return 0, false + } + parts := strings.Split(ref, ".") + for i := 0; i < len(parts)-1; i++ { + if parts[i] != marker { + continue + } + if v, ok := numericFieldString(parts[i+1]); ok { + return v, true + } + } + return 0, false +} + +func numericMeta(b runSnapshotBead, key string) (int, bool) { + return numericFieldString(beadMeta(b, key)) +} + +// numericFieldString parses a strictly-positive decimal integer with no leading +// zero. Port of TS numericField (string arm). +func numericFieldString(value string) (int, bool) { + if !numericFieldRe.MatchString(value) { + return 0, false + } + parsed, err := strconv.Atoi(value) + if err != nil { + return 0, false + } + // Number.isSafeInteger guard (< 2^53). int is 64-bit here, so any value that + // parsed cleanly and matched the regex is well within range; the explicit + // bound keeps parity with the TS check. + if int64(parsed) > 1<<53-1 { + return 0, false + } + return parsed, true +} + +// numericFieldInt mirrors TS numericField for an already-numeric value: a +// positive integer is accepted as-is. +func numericFieldInt(value int) (int, bool) { + if value > 0 { + return value, true + } + return 0, false +} + +// ── status.ts ─────────────────────────────────────────────────────────────── + +// presentationStatus maps a bead's raw status (+ gc.outcome) to a node status. +// Port of TS presentationStatus. +func presentationStatus(b runSnapshotBead) string { + raw := strings.ToLower(nonEmpty(b.status)) + outcome := strings.ToLower(beadMeta(b, beadmeta.OutcomeMetadataKey)) + if raw == "closed" || raw == "completed" || raw == "done" { + if outcome == "fail" || outcome == "failed" { + return "failed" + } + if outcome == "skipped" { + return "skipped" + } + return "completed" + } + if raw == "in_progress" || raw == "active" || raw == "running" { + return "active" + } + switch raw { + case "blocked": + return "blocked" + case "ready": + return "ready" + case "failed": + return "failed" + case "skipped": + return "skipped" + } + return "pending" +} + +// aggregateStatus folds a node's instances into one status. Port of TS +// aggregateStatus. +func aggregateStatus(instances []RunExecutionInstance, visible *RunExecutionInstance) string { + for _, inst := range instances { + if isRunningStatus(inst.Status) { + return "active" + } + } + if visible != nil && visible.Status != "" { + return visible.Status + } + return "pending" +} + +// isRunningStatus reports whether status is a running state. Port of TS +// isRunningStatus. +func isRunningStatus(status string) bool { + return status == "active" || status == "running" +} + +// ── node-shape.ts ─────────────────────────────────────────────────────────── + +// hiddenConstructs are construct kinds rendered as badges, not graph nodes. +// Port of TS HIDDEN_CONSTRUCTS (plus the 'control' case in isHiddenConstruct). +var hiddenConstructs = map[string]bool{ + "scope-check": true, + "run-finalize": true, + "spec": true, +} + +// isHiddenConstruct reports whether a construct kind is hidden from the graph. +// Port of TS isHiddenConstruct. +func isHiddenConstruct(kind string) bool { + return hiddenConstructs[kind] || kind == "control" +} + +// semanticNodeIDFor resolves a bead's semantic node id. Port of TS +// semanticNodeIdFor. +func semanticNodeIDFor(b runSnapshotBead, rootBeadID string) string { + beadID := nonEmpty(b.id) + if beadID != "" && beadID == rootBeadID { + return rootBeadID + } + if explicit := explicitLogicalBeadID(b); explicit != "" { + return externalizeID(explicit) + } + if stepID := beadMeta(b, beadmeta.StepIDMetadataKey); stepID != "" { + return externalizeID(stepID) + } + if ref := normalizedStepRef(b); ref != "" { + if semanticID, ok := semanticIDFromStepRef(ref); ok { + return externalizeID(semanticID) + } + } + if beadID != "" { + return externalizeID(beadID) + } + return externalizeID("run-node") +} + +// hiddenBadgeTargetFor resolves the visible node a hidden bead's badge attaches +// to. Port of TS hiddenBadgeTargetFor ("" mirrors null). +func hiddenBadgeTargetFor(b runSnapshotBead, rootBeadID string) string { + if constructKindFor(b, rootBeadID) == "run-finalize" { + return rootBeadID + } + if controlRef := beadMeta(b, beadmeta.ControlForMetadataKey); controlRef != "" { + if target, ok := semanticIDFromControlRef(controlRef); ok { + return externalizeID(target) + } + } + ref := normalizedStepRef(b) + if ref == "" { + return "" + } + if target, ok := semanticIDFromControlRef(ref); ok { + return externalizeID(target) + } + return "" +} + +// constructKindFor maps a bead's raw kind to a RunConstructKind. Port of TS +// constructKindFor. +func constructKindFor(b runSnapshotBead, rootBeadID string) string { + beadID := nonEmpty(b.id) + if beadID != "" && beadID == rootBeadID { + return "run-root" + } + switch rawKind(b) { + case "ralph": + return "check-loop" + case "retry": + return "retry" + case "scope", "epic", "body": + return "scope" + case "fanout": + return "fanout" + case "condition": + return "condition" + case "expand", "expansion": + return "expansion" + case "scope-check": + return "scope-check" + case "run-finalize": + return "run-finalize" + case "spec": + return "spec" + case "cleanup": + return "control" + default: + return "step" + } +} + +// externalKindFor resolves the display kind. Port of TS externalKindFor. +func externalKindFor(b runSnapshotBead, constructKind string) string { + if constructKind == "check-loop" { + return "check-loop" + } + kind := rawKind(b) + if kind == "ralph" { + return "check-loop" + } + if kind != "" { + return kind + } + return constructKind +} + +// displayTitleFor resolves a node's display title. Port of TS displayTitleFor. +func displayTitleFor(b runSnapshotBead, fallback string) string { + title := nonEmpty(b.title) + if title == "" { + title = strings.NewReplacer("-", " ", "_", " ").Replace(fallback) + } + return externalizeDisplayText(title) +} + +// badgeLabelFor maps a construct kind to a badge label. Port of TS badgeLabelFor. +func badgeLabelFor(kind string) string { + switch kind { + case "scope-check": + return "scope check" + case "run-finalize": + return "finalize" + default: + return strings.ReplaceAll(kind, "-", " ") + } +} + +// loopControlNodeIDFor resolves the loop-control node id for a bead. Port of TS +// loopControlNodeIdFor ("" mirrors undefined). +func loopControlNodeIDFor(b runSnapshotBead) string { + scopeRef := beadMeta(b, beadmeta.ScopeRefMetadataKey) + if scopeRef == "" { + scopeRef = nonEmpty(b.scopeRef) + } + if scopeRef != "" { + if id := loopControlIDFromRuntimeRef(scopeRef, []string{"iteration", "run"}); id != "" { + return id + } + } + ref := normalizedStepRef(b) + if ref == "" { + return "" + } + return loopControlIDFromRuntimeRef(ref, []string{"iteration"}) +} + +// rawKind resolves a bead's raw kind. Port of TS rawKind. +func rawKind(b runSnapshotBead) string { + if k := beadMeta(b, beadmeta.KindMetadataKey); k != "" { + return k + } + if k := beadMeta(b, beadmeta.OriginalKindMetadataKey); k != "" { + return k + } + return nonEmpty(b.kind) +} + +// explicitLogicalBeadID resolves the explicit logical bead id. Mirrors the TS +// `meta(bead, 'gc.logical_bead_id') ?? nonEmpty(bead.logical_bead_id)` idiom. +func explicitLogicalBeadID(b runSnapshotBead) string { + if v := beadMeta(b, beadmeta.LogicalBeadIDMetadataKey); v != "" { + return v + } + return nonEmpty(b.logicalBeadID) +} + +// semanticIDFromStepRef extracts a semantic id from a step ref. Port of TS +// semanticIdFromStepRef. The bool mirrors TS's undefined. +func semanticIDFromStepRef(ref string) (string, bool) { + parts := splitNonEmpty(ref, ".") + if len(parts) == 0 { + return "", false + } + semanticParts := stripRuntimeSuffix(parts) + iterationIndex := lastIndexOf(semanticParts, "iteration") + if iterationIndex >= 0 && + iterationIndex < len(semanticParts)-2 && + isPositiveIntegerStr(at(semanticParts, iterationIndex+1)) { + return at(semanticParts, len(semanticParts)-1), true + } + if iterationIndex == len(semanticParts)-2 && + isPositiveIntegerStr(at(semanticParts, iterationIndex+1)) { + // TS reads semanticParts[iterationIndex-1] with PLAIN bracket indexing, so + // iterationIndex==0 yields semanticParts[-1] === undefined (NOT .at(-1)). + // A "" here means out of range → undefined, so the caller falls through to + // the bead-id/run-node id instead of grouping on an empty semantic id. + prev := at(semanticParts, iterationIndex-1) + if prev == "" { + return "", false + } + return prev, true + } + last := at(semanticParts, len(semanticParts)-1) + if last == "" { + return "", false + } + return last, true +} + +// semanticIDFromControlRef strips the scope-check suffix, then resolves the +// semantic id. Port of TS semanticIdFromControlRef. +func semanticIDFromControlRef(ref string) (string, bool) { + return semanticIDFromStepRef(stripScopeCheckSuffix(ref)) +} + +func stripScopeCheckSuffix(ref string) string { + ref = strings.TrimSuffix(ref, "-scope-check") + ref = strings.TrimSuffix(ref, ".scope-check") + return ref +} + +// stripRuntimeSuffix drops a trailing "." runtime segment. Port of TS +// stripRuntimeSuffix. +func stripRuntimeSuffix(parts []string) []string { + if len(parts) < 2 { + return parts + } + marker := parts[len(parts)-2] + value := parts[len(parts)-1] + if value != "" && marker != "" && isPositiveIntegerStr(value) && + (marker == "attempt" || marker == "run" || marker == "check" || marker == "eval") { + return parts[:len(parts)-2] + } + return parts +} + +// loopControlIDFromRuntimeRef finds the control id preceding a "." +// segment. Port of TS loopControlIdFromRuntimeRef ("" mirrors undefined). +func loopControlIDFromRuntimeRef(ref string, markers []string) string { + parts := splitNonEmpty(ref, ".") + for _, marker := range markers { + markerIndex := -1 + for index, part := range parts { + if part == marker && index+1 < len(parts) && isPositiveIntegerStr(parts[index+1]) { + markerIndex = index + break + } + } + if markerIndex <= 0 { + continue + } + controlID := parts[markerIndex-1] + if controlID != "" { + return externalizeID(controlID) + } + return "" + } + return "" +} + +// isPositiveIntegerStr reports whether value is a canonical positive decimal +// integer. Port of TS isPositiveInteger (`String(Number.parseInt(value,10)) === +// value && parsed > 0`). parseInt yields a float64, so beyond float64's exact +// integer range the String() round-trip no longer equals value and TS rejects; +// the exact-representability gate mirrors that (an Atoi overflow → undecodable → +// rejected, which TS also does for such huge values). +func isPositiveIntegerStr(value string) bool { + if value == "" { + return false + } + parsed, err := strconv.Atoi(value) + if err != nil { + return false + } + if strconv.Itoa(parsed) != value || parsed <= 0 { + return false + } + return int64(float64(parsed)) == int64(parsed) +} + +var ( + dashUnderscoreRunRe = regexp.MustCompile(`[-_]+`) + // jsWhitespaceRunRe matches one-or-more characters of the ECMAScript `\s` + // whitespace set (Go RE2 `\s` is ASCII-only), so the collapse mirrors the TS + // `.replace(/\s+/g, ' ')`. + jsWhitespaceRunRe = regexp.MustCompile(`[\t\n\v\f\r \x{00a0}\x{1680}\x{2000}-\x{200a}\x{2028}\x{2029}\x{202f}\x{205f}\x{3000}\x{feff}]+`) +) + +// externalizeDisplayText rewrites whole-word "ralph" to "check loop" in display +// text. Port of TS externalizeDisplayText. +func externalizeDisplayText(value string) string { + if !containsRalphWord(value) { + return value + } + value = dashUnderscoreRunRe.ReplaceAllString(value, " ") + value = rewriteRalph(value, "check loop") + value = jsWhitespaceRunRe.ReplaceAllString(value, " ") + return nonEmpty(value) +} + +// ── small slice helpers (TS array idioms) ─────────────────────────────────── + +// splitNonEmpty splits s on sep and drops empty segments (TS `.filter(Boolean)`). +func splitNonEmpty(s, sep string) []string { + parts := strings.Split(s, sep) + out := make([]string, 0, len(parts)) + for _, p := range parts { + if p != "" { + out = append(out, p) + } + } + return out +} + +func lastIndexOf(parts []string, want string) int { + for i := len(parts) - 1; i >= 0; i-- { + if parts[i] == want { + return i + } + } + return -1 +} + +// at returns parts[i] or "" when out of range (TS Array.prototype.at semantics +// for the indices we use, where a miss feeds an undefined the callers guard). +func at(parts []string, i int) string { + if i < 0 || i >= len(parts) { + return "" + } + return parts[i] +} diff --git a/internal/runproj/detail_nodeshape_test.go b/internal/runproj/detail_nodeshape_test.go new file mode 100644 index 0000000000..a1606bc015 --- /dev/null +++ b/internal/runproj/detail_nodeshape_test.go @@ -0,0 +1,88 @@ +package runproj + +import "testing" + +// TestSemanticNodeIDForIterationStepRef is the regression test for the +// iteration-at-index-0 bug: a step ref that reduces to ["iteration", ] makes +// semanticIdFromStepRef return undefined in TS (plain semanticParts[-1]), so +// semanticNodeIdFor falls through to the bead id. The Go port previously returned +// a present-but-empty "" semantic id, corrupting grouping for that node. +func TestSemanticNodeIDForIterationStepRef(t *testing.T) { + const root = "run-root" + cases := []struct { + name string + bead runSnapshotBead + want string + }{ + { + name: "iteration.N step ref falls through to bead id", + bead: runSnapshotBead{id: "bead-x", stepRef: "iteration.5"}, + want: "bead-x", + }, + { + name: "iteration.N with runtime suffix still falls through to bead id", + bead: runSnapshotBead{id: "bead-y", stepRef: "iteration.5.run.2"}, + want: "bead-y", + }, + { + name: "iteration.N with no bead id falls through to run-node", + bead: runSnapshotBead{stepRef: "iteration.5"}, + want: "run-node", + }, + { + name: "trailing iteration keeps the preceding segment", + bead: runSnapshotBead{id: "bead-z", stepRef: "preflight.iteration.3"}, + want: "preflight", + }, + { + name: "mid iteration keeps the last segment", + bead: runSnapshotBead{id: "bead-w", stepRef: "review.iteration.3.apply"}, + want: "apply", + }, + { + name: "plain step ref uses the last segment", + bead: runSnapshotBead{id: "bead-v", stepRef: "mol-adopt-pr-v2.preflight"}, + want: "preflight", + }, + { + name: "explicit gc.step_id wins over the ref", + bead: runSnapshotBead{id: "bead-u", stepRef: "iteration.5", metadata: map[string]string{"gc.step_id": "real-step"}}, + want: "real-step", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := semanticNodeIDFor(tc.bead, root); got != tc.want { + t.Errorf("semanticNodeIDFor = %q, want %q", got, tc.want) + } + }) + } +} + +// TestIsPositiveIntegerStr pins the JS isPositiveInteger semantics, including the +// float64 exact-representability boundary (parseInt yields a float64, so values +// beyond 2^53 only pass when they happen to be exactly representable). +func TestIsPositiveIntegerStr(t *testing.T) { + cases := []struct { + value string + want bool + }{ + {"1", true}, + {"12", true}, + {"0", false}, + {"012", false}, + {"-3", false}, + {"3.0", false}, + {"12abc", false}, + {"", false}, + {"9007199254740992", true}, // 2^53, exactly representable + {"9007199254740993", false}, // 2^53+1, parseInt rounds → String != value + {"9007199254740994", true}, // 2^53+2, exactly representable (even) + {"99999999999999999999", false}, // overflows int64 → rejected (TS also rejects) + } + for _, tc := range cases { + if got := isPositiveIntegerStr(tc.value); got != tc.want { + t.Errorf("isPositiveIntegerStr(%q) = %v, want %v", tc.value, got, tc.want) + } + } +} diff --git a/internal/runproj/detail_order.go b/internal/runproj/detail_order.go new file mode 100644 index 0000000000..48fe4bf615 --- /dev/null +++ b/internal/runproj/detail_order.go @@ -0,0 +1,264 @@ +package runproj + +import ( + "math" + "sort" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// formulaPreviewNode mirrors a compiled-formula preview/step node. Port of TS +// FormulaPreviewNode (run-snapshot.ts) — only the id drives ordering. +type formulaPreviewNode struct { + id string +} + +// formulaDetailInput is the compiled-formula detail used to order run nodes. Port +// of the ordering-relevant slice of TS FormulaDetail. The bead-derived +// BuildRunDetail passes nil (no compiled formula), so orderRunNodeGroups is a +// no-op there; a live caller that fetches the supervisor's compiled formula can +// supply it to honor the authored step order. +type formulaDetailInput struct { + name string + // previewNodes mirrors preview.nodes: nil means absent (so formulaRankByAlias + // falls back to steps, matching TS `??`); a non-nil empty slice means present + // but empty (no fallback). + previewNodes []formulaPreviewNode + steps []formulaPreviewNode +} + +// FormulaOrderingDetail is the ordering-relevant slice of the supervisor's +// compiled formula detail. A live caller (the dashboard BFF) fetches the +// supervisor formula endpoint and passes it to BuildRunDetailWithSessionsAndFormula +// so the run's nodes honor the authored step order and the formula-detail state +// resolves to "available" instead of a synthetic fetch failure. PreviewNodeIDs +// mirrors preview.nodes: nil means the field was absent (ordering falls back to +// StepIDs, matching the dashboard's `preview?.nodes ?? steps`); a non-nil empty +// slice means present-but-empty (no fallback to steps). +type FormulaOrderingDetail struct { + Name string + PreviewNodeIDs []string + StepIDs []string +} + +// RunFormulaDetailFetchFailure enumerates why a live compiled-formula-detail +// fetch did not yield a usable payload, mirroring the shared dashboard +// RunFormulaDetailFetchFailure union. The live BFF distinguishes a genuine HTTP +// 404 (the compiled formula is absent) from every other failure so the detail +// page reports the right operator diagnostic instead of collapsing a missing +// formula into a generic upstream error. Port of the TS formulaDetailFetchFailure +// mapping (runDetail.ts): a 404 is not_found, everything else is upstream_error. +type RunFormulaDetailFetchFailure string + +const ( + // FormulaDetailUpstreamError is the default fetch-failure reason: the + // compiled formula could not be layered in for any reason other than a + // definite 404 (network error, timeout, non-404 status, unparseable body). + // It is also what the bead-derived projection reports when a name+target are + // known but no live detail was supplied. + FormulaDetailUpstreamError RunFormulaDetailFetchFailure = "upstream_error" + // FormulaDetailNotFound marks a fetch whose supervisor response was HTTP 404: + // the compiled formula is genuinely missing, distinct from a transient or + // upstream error. + FormulaDetailNotFound RunFormulaDetailFetchFailure = "not_found" +) + +// toInput converts the exported ordering detail into the internal +// formulaDetailInput, preserving the nil-vs-empty distinction on PreviewNodeIDs. +// A nil receiver yields a nil input (the un-enriched path). +func (d *FormulaOrderingDetail) toInput() *formulaDetailInput { + if d == nil { + return nil + } + return &formulaDetailInput{ + name: d.Name, + previewNodes: previewNodesFromIDs(d.PreviewNodeIDs), + steps: previewNodesFromIDs(d.StepIDs), + } +} + +// previewNodesFromIDs lifts node ids into formulaPreviewNode values, preserving +// nil (absent) versus non-nil empty (present-but-empty) so the `??` fallback in +// formulaRankByAlias behaves exactly as the dashboard's TS did. +func previewNodesFromIDs(ids []string) []formulaPreviewNode { + if ids == nil { + return nil + } + nodes := make([]formulaPreviewNode, 0, len(ids)) + for _, id := range ids { + nodes = append(nodes, formulaPreviewNode{id: id}) + } + return nodes +} + +// orderRunNodeGroups orders groups by the compiled formula's authored step order, +// preserving snapshot order when no formula detail is available. Port of TS +// orderRunNodeGroups (formula-order.ts). +func orderRunNodeGroups(groups []runNodeGroup, formulaDetail *formulaDetailInput, rootBeadID string) []runNodeGroup { + rankByAlias := formulaRankByAlias(formulaDetail) + out := make([]runNodeGroup, len(groups)) + copy(out, groups) + if len(rankByAlias) == 0 { + return out + } + + var formulaName string + if formulaDetail != nil { + formulaName = formulaDetail.name + } + + type ranked struct { + group runNodeGroup + index int + rank float64 + } + entries := make([]ranked, len(out)) + for i, group := range out { + var rank float64 + if group.semanticNodeID == rootBeadID { + rank = -1 + } else { + rank = rankForGroup(group, rankByAlias, formulaName) + } + entries[i] = ranked{group: group, index: i, rank: rank} + } + sort.SliceStable(entries, func(i, j int) bool { + if entries[i].rank != entries[j].rank { + return entries[i].rank < entries[j].rank + } + return entries[i].index < entries[j].index + }) + for i, entry := range entries { + out[i] = entry.group + } + return out +} + +func formulaRankByAlias(formulaDetail *formulaDetailInput) map[string]int { + ranks := make(map[string]int) + if formulaDetail == nil { + return ranks + } + // TS: `formulaDetail?.preview?.nodes ?? formulaDetail?.steps ?? []`. The + // nullish `??` falls back to steps only when preview.nodes is ABSENT, so a + // present-but-empty preview.nodes (non-nil, len 0) must NOT fall through. + steps := formulaDetail.previewNodes + if steps == nil { + steps = formulaDetail.steps + } + for index, step := range steps { + for _, alias := range aliasVariants(step.id, formulaDetail.name) { + if _, ok := ranks[alias]; !ok { + ranks[alias] = index + } + } + } + return ranks +} + +func rankForGroup(group runNodeGroup, ranks map[string]int, formulaName string) float64 { + rank := math.Inf(1) + for _, alias := range groupAliases(group, formulaName) { + if candidate, ok := ranks[alias]; ok && float64(candidate) < rank { + rank = float64(candidate) + } + } + return rank +} + +func groupAliases(group runNodeGroup, formulaName string) []string { + var base []string + base = append(base, group.semanticNodeID) + for _, bead := range group.beads { + base = append(base, beadAliases(bead, formulaName)...) + } + var out []string + for _, alias := range base { + out = append(out, aliasVariants(alias, "")...) + } + return out +} + +func beadAliases(bead runSnapshotBead, formulaName string) []string { + var sources []string + if v := nonEmpty(bead.id); v != "" { + sources = append(sources, v) + } + if v := explicitLogicalBeadID(bead); v != "" { + sources = append(sources, v) + } + if v := beadMeta(bead, beadmeta.StepIDMetadataKey); v != "" { + sources = append(sources, v) + } + if v := normalizedStepRef(bead); v != "" { + sources = append(sources, v) + } + var out []string + for _, value := range sources { + out = append(out, aliasVariants(value, formulaName)...) + } + return out +} + +// aliasVariants expands a value into its externalized alias variants. Port of TS +// aliasVariants (formulaName "" means no prefix to strip). +func aliasVariants(value, formulaName string) []string { + clean := nonEmpty(value) + if clean == "" { + return nil + } + stripped := stripFormulaPrefix(clean, formulaName) + candidates := []string{clean, stripped, stripScopeCheckSuffix(clean), stripScopeCheckSuffix(stripped)} + seen := make(map[string]bool) + var out []string + for _, candidate := range candidates { + ext := externalizeID(candidate) + if !seen[ext] { + seen[ext] = true + out = append(out, ext) + } + } + return out +} + +func stripFormulaPrefix(value, formulaName string) string { + if formulaName == "" { + return value + } + prefix := formulaName + "." + return strings.TrimPrefix(value, prefix) +} + +// ── lanes.ts ──────────────────────────────────────────────────────────────── + +const runLaneScope = "__run" + +// buildRunDisplayLanes groups nodes into scope lanes. Port of TS +// buildRunDisplayLanes (lanes.ts), preserving first-seen scope order. +func buildRunDisplayLanes(nodes []RunDisplayNode) []RunDisplayLane { + byScope := make(map[string]*RunDisplayLane) + var order []string + for _, node := range nodes { + scope := runLaneScope + if node.Scope.Kind == "scoped" { + scope = node.Scope.Ref + } + lane, ok := byScope[scope] + if !ok { + label := scope + if scope == runLaneScope { + label = "Run" + } + lane = &RunDisplayLane{ID: scope, Label: label, NodeIDs: []string{}} + byScope[scope] = lane + order = append(order, scope) + } + lane.NodeIDs = append(lane.NodeIDs, node.ID) + } + lanes := make([]RunDisplayLane, 0, len(order)) + for _, scope := range order { + lanes = append(lanes, *byScope[scope]) + } + return lanes +} diff --git a/internal/runproj/detail_parity_test.go b/internal/runproj/detail_parity_test.go new file mode 100644 index 0000000000..e0ec4dbbea --- /dev/null +++ b/internal/runproj/detail_parity_test.go @@ -0,0 +1,164 @@ +package runproj + +import ( + "errors" + "testing" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" +) + +// The golden tests pin one graph.v2 adopt-pr run byte-for-byte. These parity +// tests broaden the oracle across the shapes the golden fixture does not cover — +// every supported formula's stage ladder, multi-iteration loop instancing, and +// the unsupported-run classification — so a porting regression in that +// most-bug-fixed logic fails CI instead of silently diverging from the deleted TS. + +// TestStagesForFormulaCoversEverySupportedFormula pins each supported formula's +// stagesForFormula ladder (keys and order). A dropped, reordered, or renamed +// stage — the kind of error the narrow golden cannot see — fails here. +func TestStagesForFormulaCoversEverySupportedFormula(t *testing.T) { + want := map[string][]string{ + "mol-adopt-pr-v2": {"preflight", "rebase", "review", "ci", "approval", "finalize", "cleanup"}, + "mol-design-review-v2": {"setup", "personas", "fanout", "synthesis", "apply", "finalize"}, + "mol-bug-report-flow-v2": {"intake", "repro", "audit", "classify", "approval", "publish", "dispatch"}, + "mol-bug-report-implementation-v2": {"plan", "design", "implement", "review", "pr", "ci", "merge"}, + } + for formula, keys := range want { + stages := stagesForFormula(formula, true) + if len(stages) != len(keys) { + t.Errorf("%s: got %d stages, want %d", formula, len(stages), len(keys)) + continue + } + for i, stage := range stages { + if stage.key != keys[i] { + t.Errorf("%s: stage[%d].key = %q, want %q", formula, i, stage.key, keys[i]) + } + if stage.label == "" { + t.Errorf("%s: stage %q has an empty label", formula, stage.key) + } + if len(stage.steps) == 0 { + t.Errorf("%s: stage %q has no steps", formula, stage.key) + } + } + } + if got := stagesForFormula("mol-adopt-pr-v2", false); got != nil { + t.Errorf("hasFormula=false must yield nil stages, got %v", got) + } + if got := stagesForFormula("mol-not-a-real-formula", true); got != nil { + t.Errorf("unknown formula must yield nil stages, got %v", got) + } +} + +// TestFormulaStageProgressMarksCompleteActivePending drives the stage-status +// classifier for a mid-flight adopt-pr run: two closed early steps, one +// in-progress step. Stages before the active one are complete, the owning stage +// active, and the rest pending. +func TestFormulaStageProgressMarksCompleteActivePending(t *testing.T) { + stages := stagesForFormula("mol-adopt-pr-v2", true) + issues := []runIssue{ + stepIssue("preflight", "closed"), + stepIssue("rebase-check", "closed"), + stepIssue("review-loop", "in_progress"), + } + + got := formulaStageProgress(stages, issues) + want := []string{"complete", "complete", "active", "pending", "pending", "pending", "pending"} + if len(got) != len(want) { + t.Fatalf("got %d stages, want %d", len(got), len(want)) + } + for i := range want { + if got[i].Status != want[i] { + t.Errorf("stage %q status = %q, want %q", got[i].Key, got[i].Status, want[i]) + } + } +} + +// TestReviewRoundDetectionAcrossIterations exercises loop instancing: the review +// round is read from the highest iteration/attempt marker across a run's beads, +// in each of the encodings reviewRoundForIssue accepts (digit-suffixed key, +// digit-suffixed value, and a bare iteration/attempt key whose value holds the +// number). +func TestReviewRoundDetectionAcrossIterations(t *testing.T) { + cases := []struct { + name string + issue runIssue + want int + }{ + {"digit-suffixed key", metaIssue(map[string]string{"review.iteration.2": "in_progress"}), 2}, + {"digit-suffixed value", metaIssue(map[string]string{beadmeta.ScopeRefMetadataKey: "review-loop.iteration.3"}), 3}, + {"bare attempt key holds the number", metaIssue(map[string]string{"attempt": "4"}), 4}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, ok := reviewRoundForIssue(tc.issue) + if !ok || got != tc.want { + t.Fatalf("reviewRoundForIssue = (%d,%v), want (%d,true)", got, ok, tc.want) + } + }) + } + + // Across a multi-iteration convoy the run resolves to the furthest round. + issues := []runIssue{ + metaIssue(map[string]string{"review.iteration.1": "closed"}), + metaIssue(map[string]string{"review.iteration.3": "in_progress"}), + metaIssue(map[string]string{beadmeta.StepIDMetadataKey: "preflight"}), + } + round, ok := reviewRoundForIssues(issues) + if !ok || round != 3 { + t.Fatalf("reviewRoundForIssues = (%d,%v), want (3,true)", round, ok) + } +} + +// TestBuildRunDetailUnsupportedRunReasons pins the run-classification bug fix +// (gascity-dashboard-9w3k): a non-graph.v2 run reports not_run_view (an honest +// list-only run), while a graph.v2 run missing its snapshot identity reports +// invalid_snapshot (a genuine load failure) — the SPA renders these differently. +func TestBuildRunDetailUnsupportedRunReasons(t *testing.T) { + notRunView := beads.Bead{ + ID: "run-legacy", Type: "molecule", Status: "open", + Metadata: map[string]string{beadmeta.KindMetadataKey: "run"}, + } + assertUnsupported(t, []beads.Bead{notRunView}, "run-legacy", ReasonNotRunView) + + invalidSnapshot := beads.Bead{ + ID: "run-x", Type: "molecule", Status: "open", + Metadata: map[string]string{ + beadmeta.FormulaContractMetadataKey: "graph.v2", + beadmeta.KindMetadataKey: "run", + beadmeta.ScopeKindMetadataKey: "rig", + beadmeta.ScopeRefMetadataKey: "demo", + // No gc.root_store_ref: the snapshot identity is incomplete. + }, + } + assertUnsupported(t, []beads.Bead{invalidSnapshot}, "run-x", ReasonInvalidSnapshot) +} + +func stepIssue(stepID, status string) runIssue { + return runIssue{ + status: status, + metadata: map[string]string{ + beadmeta.StepIDMetadataKey: stepID, + beadmeta.KindMetadataKey: "step", + }, + } +} + +func metaIssue(metadata map[string]string) runIssue { + return runIssue{metadata: metadata} +} + +func assertUnsupported(t *testing.T, beadList []beads.Bead, runID string, wantReason UnsupportedRunReason) { + t.Helper() + _, err := BuildRunDetail(beadList, runID, 1, 1) + if err == nil { + t.Fatalf("run %q: expected UnsupportedRunError %q, got nil", runID, wantReason) + } + var unsupported *UnsupportedRunError + if !errors.As(err, &unsupported) { + t.Fatalf("run %q: error %v is not an UnsupportedRunError", runID, err) + } + if unsupported.Reason != wantReason { + t.Errorf("run %q: reason = %q, want %q", runID, unsupported.Reason, wantReason) + } +} diff --git a/internal/runproj/detail_scope_test.go b/internal/runproj/detail_scope_test.go new file mode 100644 index 0000000000..1b55ae7737 --- /dev/null +++ b/internal/runproj/detail_scope_test.go @@ -0,0 +1,70 @@ +package runproj + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" +) + +// TestBuildRunDetailResolvesScopeFromRootStoreRef proves the detail path applies +// the same gc.root_store_ref scope fallback that summary uses. A run root that +// carries only gc.root_store_ref (no explicit gc.scope_kind/gc.scope_ref pair) +// lists in /runs/summary via fromRootMetadataScope, and must also open in +// /runs/{id}/detail instead of failing with invalid_snapshot. Regression for the +// summary/detail scope-fallback divergence. +func TestBuildRunDetailResolvesScopeFromRootStoreRef(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "beads_fixture.json")) + if err != nil { + t.Fatalf("read fixture: %v", err) + } + var beadList []beads.Bead + if err := json.Unmarshal(raw, &beadList); err != nil { + t.Fatalf("unmarshal fixture: %v", err) + } + + // Strip the explicit scope pair from the run root, leaving only + // gc.root_store_ref=rig:gascity-packs — the scopeless shape that summary + // accepts but detail previously rejected with 422 invalid_snapshot. + found := false + for i := range beadList { + if beadList[i].ID == "dt-adopt1" { + delete(beadList[i].Metadata, beadmeta.ScopeKindMetadataKey) + delete(beadList[i].Metadata, beadmeta.ScopeRefMetadataKey) + found = true + } + } + if !found { + t.Fatal("fixture missing dt-adopt1 root; test needs updating") + } + + detail, err := BuildRunDetail(beadList, "dt-adopt1", 1, 100) + if err != nil { + t.Fatalf("BuildRunDetail on root_store_ref-only root: %v", err) + } + if detail.ScopeKind != "rig" || detail.ScopeRef != "gascity-packs" { + t.Errorf("scope not recovered from root_store_ref: got kind=%q ref=%q, want rig/gascity-packs", + detail.ScopeKind, detail.ScopeRef) + } +} + +// TestFromSnapshotScopeFallback unit-tests the scope resolver directly: the +// explicit pair wins (root_store_ref ignored), the store ref recovers scope when +// the pair is absent, and neither an empty nor a malformed store ref resolves. +func TestFromSnapshotScopeFallback(t *testing.T) { + if k, r, ok := fromSnapshotScope(runSnapshot{scopeKind: "city", scopeRef: "main", rootStoreRef: "rig:other"}); !ok || k != "city" || r != "main" { + t.Errorf("explicit pair: got (%q,%q,%v), want (city,main,true)", k, r, ok) + } + if k, r, ok := fromSnapshotScope(runSnapshot{rootStoreRef: "rig:gascity-packs"}); !ok || k != "rig" || r != "gascity-packs" { + t.Errorf("store-ref fallback: got (%q,%q,%v), want (rig,gascity-packs,true)", k, r, ok) + } + if _, _, ok := fromSnapshotScope(runSnapshot{}); ok { + t.Error("empty snapshot: got ok=true, want false") + } + if _, _, ok := fromSnapshotScope(runSnapshot{rootStoreRef: "not-a-store-ref"}); ok { + t.Error("malformed store ref: got ok=true, want false") + } +} diff --git a/internal/runproj/detail_sessionlink.go b/internal/runproj/detail_sessionlink.go new file mode 100644 index 0000000000..1886dd1b0f --- /dev/null +++ b/internal/runproj/detail_sessionlink.go @@ -0,0 +1,243 @@ +package runproj + +import ( + "regexp" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// sessionIDRe gates a value before it is fed to the supervisor session routes. +// Port of TS SESSION_ID_RE (session-id.ts) — lowercase-only, case-sensitive. +var sessionIDRe = regexp.MustCompile(`^(gc|td|th|[a-z]{4})-[a-z0-9-]{1,32}$`) + +// supervisorSessionIDSuffixRe extracts a trailing supervisor id from a +// pool-qualified handle. Port of the TS suffix match in supervisorSessionIdFrom. +var supervisorSessionIDSuffixRe = regexp.MustCompile(`(?:^|[-_/])((?:gc|td|th|[a-z]{4})-[a-z0-9-]{1,32})$`) + +// runSessionIndex indexes sessions by id, name, and template for run-link +// resolution. Port of TS RunSessionIndex. +type runSessionIndex struct { + byID map[string]DashboardSession + byName map[string]DashboardSession + byTemplate map[string][]DashboardSession +} + +// runSessionLinkContext carries the session index (and scope) for link +// resolution. Port of TS RunSessionLinkContext. A nil index mirrors undefined. +type runSessionLinkContext struct { + sessionIndex *runSessionIndex + scopeRef string +} + +// buildRunSessionIndex indexes a session list for link resolution. Port of TS +// buildRunSessionIndex (first-write-wins for the id/name maps). +func buildRunSessionIndex(sessions []DashboardSession) runSessionIndex { + idx := runSessionIndex{ + byID: make(map[string]DashboardSession), + byName: make(map[string]DashboardSession), + byTemplate: make(map[string][]DashboardSession), + } + for _, session := range sessions { + rememberSession(idx.byID, session.ID, session) + rememberSession(idx.byName, derefString(session.Alias), session) + rememberSession(idx.byName, session.Title, session) + rememberSession(idx.byName, session.SessionName, session) + if template := nonEmpty(session.Template); template != "" { + idx.byTemplate[template] = append(idx.byTemplate[template], session) + } + } + return idx +} + +// runSessionLinkFor resolves a bead to a streamable session link, or (zero, +// false) when none is usable. Port of TS runSessionLinkFor. +func runSessionLinkFor(bead runSnapshotBead, status string, ctx runSessionLinkContext) (RunSessionLink, bool) { + if status == "pending" || status == "ready" { + return RunSessionLink{}, false + } + assignee := nonEmpty(bead.assignee) + sessionID := sessionIDFromBead(bead, assignee) + sessionName := sessionNameFromBead(bead, assignee, sessionID) + if sessionID == "" && sessionName == "" { + return RunSessionLink{}, false + } + rawLink := rawLinkFrom(sessionID, sessionName, assignee) + link := resolveRunSessionLink(rawLink, ctx.sessionIndex) + if !sessionIDRe.MatchString(link.SessionID) { + return RunSessionLink{}, false + } + return link, true +} + +// sessionIDFromBead resolves the supervisor session id from a bead. Port of TS +// sessionIdFromBead ("" mirrors undefined). +func sessionIDFromBead(bead runSnapshotBead, assignee string) string { + rawSessionID := beadMeta(bead, "session_id") + if rawSessionID == "" { + rawSessionID = beadMeta(bead, beadmeta.SessionIDMetadataKey) + } + if rawSessionID == "" { + rawSessionID = beadMeta(bead, beadmeta.SessionIDCamelMetadataKey) + } + if rawSessionID == "" { + rawSessionID = assignee + } + if supervisor := supervisorSessionIDFrom(rawSessionID); supervisor != "" { + return supervisor + } + return rawSessionID +} + +// sessionNameFromBead resolves the session display name from a bead. Port of TS +// sessionNameFromBead. +func sessionNameFromBead(bead runSnapshotBead, assignee, sessionID string) string { + if v := beadMeta(bead, "session_name"); v != "" { + return v + } + if v := beadMeta(bead, beadmeta.SessionNameMetadataKey); v != "" { + return v + } + if v := beadMeta(bead, beadmeta.SessionNameCamelMetadataKey); v != "" { + return v + } + if assignee != "" { + return assignee + } + return sessionID +} + +func rawLinkFrom(sessionID, sessionName, assignee string) RunSessionLink { + name := sessionName + if name == "" { + name = sessionID + } + id := sessionID + if id == "" { + id = sessionName + } + resolvedAssignee := assignee + if resolvedAssignee == "" { + resolvedAssignee = name + } + return RunSessionLink{SessionID: id, SessionName: name, Assignee: resolvedAssignee} +} + +// supervisorSessionIDFrom extracts a supervisor session id from a raw handle. +// Port of TS supervisorSessionIdFrom ("" mirrors undefined). +func supervisorSessionIDFrom(value string) string { + clean := nonEmpty(value) + if clean == "" { + return "" + } + if sessionIDRe.MatchString(clean) { + return clean + } + m := supervisorSessionIDSuffixRe.FindStringSubmatch(clean) + if m == nil { + return "" + } + suffix := m[1] + if suffix == "" || !sessionIDRe.MatchString(suffix) { + return "" + } + return suffix +} + +func resolveRunSessionLink(rawLink RunSessionLink, sessionIndex *runSessionIndex) RunSessionLink { + if sessionIndex == nil { + return rawLink + } + session, ok := resolveRunSessionSummary(rawLink, *sessionIndex) + if !ok { + return rawLink + } + return linkForSession(session, rawLink) +} + +func resolveRunSessionSummary(link RunSessionLink, sessionIndex runSessionIndex) (DashboardSession, bool) { + for _, candidate := range []string{link.SessionID, link.SessionName, link.Assignee} { + key := nonEmpty(candidate) + if key == "" { + continue + } + if session, ok := sessionIndex.byID[key]; ok { + return session, true + } + if session, ok := sessionIndex.byName[key]; ok { + return session, true + } + if session, ok := uniquePreferredSession(sessionIndex.byTemplate[key]); ok { + return session, true + } + } + return DashboardSession{}, false +} + +func linkForSession(session DashboardSession, rawLink RunSessionLink) RunSessionLink { + // sessionName: nonEmpty(alias) ?? nonEmpty(title) ?? nonEmpty(session_name) ?? + // nonEmpty(template) ?? rawLink.sessionName. The `??` chain returns the first + // trimmed-non-empty value, else rawLink.sessionName verbatim (not trimmed). + sessionName := rawLink.SessionName + for _, v := range []string{derefString(session.Alias), session.Title, session.SessionName, session.Template} { + if t := nonEmpty(v); t != "" { + sessionName = t + break + } + } + + // assignee: rawLink.assignee || nonEmpty(template) || nonEmpty(alias) || + // nonEmpty(title) || nonEmpty(session_name) || session.id. The `||` chain + // takes rawLink.assignee verbatim when non-empty (JS-truthy), then the first + // trimmed-non-empty value, else session.id verbatim. + assignee := session.ID + switch { + case rawLink.Assignee != "": + assignee = rawLink.Assignee + default: + for _, v := range []string{session.Template, derefString(session.Alias), session.Title, session.SessionName} { + if t := nonEmpty(v); t != "" { + assignee = t + break + } + } + } + + return RunSessionLink{SessionID: session.ID, SessionName: sessionName, Assignee: assignee} +} + +func uniquePreferredSession(sessions []DashboardSession) (DashboardSession, bool) { + if len(sessions) == 0 { + return DashboardSession{}, false + } + var active []DashboardSession + for _, s := range sessions { + if s.State == "active" || s.Running { + active = append(active, s) + } + } + if len(active) == 1 { + return active[0], true + } + if len(sessions) == 1 { + return sessions[0], true + } + return DashboardSession{}, false +} + +func rememberSession(store map[string]DashboardSession, key string, session DashboardSession) { + clean := nonEmpty(key) + if clean == "" { + return + } + if _, ok := store[clean]; ok { + return + } + store[clean] = session +} + +func derefString(p *string) string { + if p == nil { + return "" + } + return *p +} diff --git a/internal/runproj/detail_sessionlink_test.go b/internal/runproj/detail_sessionlink_test.go new file mode 100644 index 0000000000..198be7fba4 --- /dev/null +++ b/internal/runproj/detail_sessionlink_test.go @@ -0,0 +1,72 @@ +package runproj + +import "testing" + +// TestRunSessionLinkForNormalization ports session-link.test.ts: regression +// coverage for the rig-store / polecat "invalid session id" bug. A run records +// its session as a pool-qualified NAME (polecat-gc-333573) whose real supervisor +// id is the gc-suffix; the link must normalize to the supervisor id or degrade, +// never leak an unvalidated handle into the session route. +func TestRunSessionLinkForNormalization(t *testing.T) { + var emptyCtx runSessionLinkContext + + t.Run("normalizes a pool-qualified session name in metadata to the supervisor id", func(t *testing.T) { + bead := runSnapshotBead{ + assignee: "polecat-gc-333573", + metadata: map[string]string{"session_id": "polecat-gc-333573"}, + } + link, ok := runSessionLinkFor(bead, "done", emptyCtx) + if !ok { + t.Fatalf("expected a link") + } + if link.SessionID != "gc-333573" { + t.Errorf("sessionID = %q, want %q", link.SessionID, "gc-333573") + } + }) + + t.Run("leaves a clean gc-prefixed session id unchanged", func(t *testing.T) { + bead := runSnapshotBead{metadata: map[string]string{"session_id": "gc-333573"}} + link, ok := runSessionLinkFor(bead, "done", emptyCtx) + if !ok { + t.Fatalf("expected a link") + } + if link.SessionID != "gc-333573" { + t.Errorf("sessionID = %q, want %q", link.SessionID, "gc-333573") + } + }) + + t.Run("derives the id from a pool-qualified assignee when no metadata id is present", func(t *testing.T) { + bead := runSnapshotBead{assignee: "polecat-gc-333573"} + link, ok := runSessionLinkFor(bead, "done", emptyCtx) + if !ok { + t.Fatalf("expected a link") + } + if link.SessionID != "gc-333573" { + t.Errorf("sessionID = %q, want %q", link.SessionID, "gc-333573") + } + }) + + t.Run("degrades to no link when an unresolvable value carries no supervisor id", func(t *testing.T) { + bead := runSnapshotBead{metadata: map[string]string{"session_id": "mystery-handle"}} + if _, ok := runSessionLinkFor(bead, "done", emptyCtx); ok { + t.Errorf("expected no link for an unresolvable handle") + } + }) + + t.Run("degrades a runtime-derived bare assignee that cannot yield a supervisor id", func(t *testing.T) { + bead := runSnapshotBead{assignee: "polecat"} + if _, ok := runSessionLinkFor(bead, "done", emptyCtx); ok { + t.Errorf("expected no link for a bare worker name") + } + }) + + t.Run("returns no link for pending/ready nodes (no session yet)", func(t *testing.T) { + bead := runSnapshotBead{assignee: "polecat-gc-333573"} + if _, ok := runSessionLinkFor(bead, "pending", emptyCtx); ok { + t.Errorf("expected no link for a pending node") + } + if _, ok := runSessionLinkFor(bead, "ready", emptyCtx); ok { + t.Errorf("expected no link for a ready node") + } + }) +} diff --git a/internal/runproj/detail_types.go b/internal/runproj/detail_types.go new file mode 100644 index 0000000000..697fd00ef0 --- /dev/null +++ b/internal/runproj/detail_types.go @@ -0,0 +1,275 @@ +package runproj + +// The run-detail DTO is a faithful Go port of the TypeScript FormulaRunDetail +// (internal/api/dashboardspa/web/shared/src/run-detail.ts) and the run-snapshot +// input shape (run-snapshot.ts). BuildRunDetail folds a city's beads into one +// run's detail graph, the SAME projection the supervisor's /workflow/{id} route +// produced client-side. Field order in these structs is load-bearing: the +// golden-parity test marshals them with the same canonical JSON the TS generator +// used (JSON.stringify(..., 2)), so the JSON key order must match the TS +// object-literal key order the detail pipeline emits. + +// runSnapshotDep is a raw dependency edge from a run snapshot. Port of TS +// RunSnapshotDep. Kept package-internal: BuildRunDetail synthesizes deps from the +// folded beads (the supervisor's RunSnapshot is not on the OSS-local path). +type runSnapshotDep struct { + from string + to string + kind string +} + +// runSnapshotBead is one bead row inside a run snapshot — supervisor wire shape, +// not the dashboard display node. Port of TS RunSnapshotBead. Package-internal: +// it is projected from beads.Bead by toRunSnapshotBead. +type runSnapshotBead struct { + id string + title string + status string + kind string + stepRef string + attempt *int + logicalBeadID string + scopeRef string + assignee string + metadata map[string]string +} + +// runSnapshot is the dashboard-normalized supervisor snapshot. Port of TS +// RunSnapshot. BuildRunDetail synthesizes it from the folded beads, mirroring the +// golden generator's snapshotForRun. +type runSnapshot struct { + runID string + rootBeadID string + rootStoreRef string + resolvedRootStore string + scopeKind string + scopeRef string + snapshotVersion int + snapshotEventSeq *int64 + partial bool + storesScanned []string + beads []runSnapshotBead + deps []runSnapshotDep + logicalEdges []runSnapshotDep +} + +// RunFormula is the run's formula-identity union. TS RunFormula: +// {kind:'known', name, source} | {kind:'unavailable', reason}. +type RunFormula struct { + Kind string // "known" | "unavailable" + Name string + Source string // "metadata" | "title_fallback" + Reason string // "missing_formula_metadata" +} + +// RunFormulaDetailState is the compiled-formula-detail union. TS +// RunFormulaDetailState (four arms). +type RunFormulaDetailState struct { + Kind string // "available" | "unavailable" + Name string + Target string + Reason string // "missing_formula_metadata" | "missing_run_target" | "fetch_failed" + Failure string // RunFormulaDetailFetchFailure (fetch_failed arm only) +} + +// RunExecutionPath is the run-diff execution-path union. TS RunExecutionPath: +// {kind:'known', path} | {kind:'unavailable', reason}. +type RunExecutionPath struct { + Kind string // "known" | "unavailable" + Path string + Reason string // "missing_cwd_and_rig_root" +} + +// RunSnapshotSequence is the snapshot-event-seq union. TS RunSnapshotSequence: +// {kind:'known', seq} | {kind:'unavailable', reason:'supervisor_omitted'}. +type RunSnapshotSequence struct { + Kind string // "known" | "unavailable" + Seq int64 + Reason string // "supervisor_omitted" +} + +// FormulaRunCompleteness is the completeness union. TS FormulaRunCompleteness: +// {kind:'complete'} | {kind:'partial', reasons}. +type FormulaRunCompleteness struct { + Kind string // "complete" | "partial" + Reasons []string +} + +// RunNodeScope is the per-node scope union. TS RunNodeScope: +// {kind:'run'} | {kind:'scoped', ref}. +type RunNodeScope struct { + Kind string // "run" | "scoped" + Ref string +} + +// RunIteration is the loop-iteration union. TS RunIteration: +// {kind:'base'} | {kind:'loop', value}. +type RunIteration struct { + Kind string // "base" | "loop" + Value int +} + +// RunAttempt is the attempt union. TS RunAttempt: +// {kind:'untracked'} | {kind:'attempt', value}. +type RunAttempt struct { + Kind string // "untracked" | "attempt" + Value int +} + +// RunSessionLink is a resolved session reference. Port of TS RunSessionLink. +type RunSessionLink struct { + SessionID string `json:"sessionId"` + SessionName string `json:"sessionName"` + Assignee string `json:"assignee"` +} + +// RunSessionAttachment is the per-instance session union. TS +// RunSessionAttachment: +// {kind:'attached', link, streamable} | {kind:'none', reason}. +type RunSessionAttachment struct { + Kind string // "attached" | "none" + Link RunSessionLink + Streamable bool + Reason string // "not_started" | "session_unresolved" +} + +// RunIterationSummary is the node-level iteration summary union. TS +// RunIterationSummary: {kind:'single'} | {kind:'stacked', visibleIteration, +// iterationCount, control}. +type RunIterationSummary struct { + Kind string // "single" | "stacked" + VisibleIteration int + IterationCount int + Control RunIterationControl +} + +// RunIterationControl is the stacked-summary control union. TS: +// {kind:'known', id} | {kind:'unknown'}. +type RunIterationControl struct { + Kind string // "known" | "unknown" + ID string +} + +// RunAttemptSummary is the node-level attempt summary union. TS +// RunAttemptSummary: {kind:'none'} | {kind:'tracked', count, badge, active}. +type RunAttemptSummary struct { + Kind string // "none" | "tracked" + Count int + Badge RunAttemptBadge + Active RunAttemptActive +} + +// RunAttemptBadge is the tracked-summary badge union. TS: +// {kind:'bounded', label} | {kind:'count-only'}. +type RunAttemptBadge struct { + Kind string // "bounded" | "count-only" + Label string +} + +// RunAttemptActive is the tracked-summary active union. TS: +// {kind:'running', value} | {kind:'idle'}. +type RunAttemptActive struct { + Kind string // "running" | "idle" + Value int +} + +// RunExecutionInstance is one physical bead execution behind a semantic node. +// Port of TS RunExecutionInstance. Field order matches the object literal in +// buildExecutionInstance (execution-instances.ts). +type RunExecutionInstance struct { + ID string `json:"id"` + SemanticNodeID string `json:"semanticNodeId"` + BeadID string `json:"beadId"` + Iteration RunIteration `json:"iteration"` + Attempt RunAttempt `json:"attempt"` + Label string `json:"label"` + Status string `json:"status"` + Session RunSessionAttachment `json:"session"` + CurrentIteration bool `json:"currentIteration"` + Historical bool `json:"historical"` +} + +// RunControlBadge is a hidden-construct badge attached to a visible node. Port of +// TS RunControlBadge. +type RunControlBadge struct { + ID string `json:"id"` + Label string `json:"label"` + Status string `json:"status"` +} + +// RunDisplayNode is one semantic node in the run graph. Port of TS +// RunDisplayNode. Field order matches the run-detail.ts interface (the object +// literal in buildRunDisplayNode emits the same order). +type RunDisplayNode struct { + ID string `json:"id"` + SemanticNodeID string `json:"semanticNodeId"` + Title string `json:"title"` + Kind string `json:"kind"` + ConstructKind string `json:"constructKind"` + Status string `json:"status"` + CurrentBeadID string `json:"currentBeadId"` + Scope RunNodeScope `json:"scope"` + VisibleInGraph bool `json:"visibleInGraph"` + HistoricalOnly bool `json:"historicalOnly"` + IterationSummary RunIterationSummary `json:"iterationSummary"` + AttemptSummary RunAttemptSummary `json:"attemptSummary"` + VisibleExecutionInstanceID string `json:"visibleExecutionInstanceId"` + ExecutionInstances []RunExecutionInstance `json:"executionInstances"` + ControlBadges []RunControlBadge `json:"controlBadges"` +} + +// RunDisplayEdge is a directed edge between semantic nodes. Port of TS +// RunDisplayEdge. +type RunDisplayEdge struct { + From string `json:"from"` + To string `json:"to"` + Kind string `json:"kind"` +} + +// RunDisplayLane groups nodes by scope. Port of TS RunDisplayLane. +type RunDisplayLane struct { + ID string `json:"id"` + Label string `json:"label"` + NodeIDs []string `json:"nodeIds"` +} + +// FormulaRunProgress is the run progress/census summary. Port of TS +// FormulaRunProgress. Field order matches buildFormulaRunProgress (formula-run.ts). +type FormulaRunProgress struct { + SnapshotVersion int `json:"snapshotVersion"` + SnapshotEventSeq RunSnapshotSequence `json:"snapshotEventSeq"` + SnapshotPartial bool `json:"snapshotPartial"` + TotalNodeCount int `json:"totalNodeCount"` + VisibleNodeCount int `json:"visibleNodeCount"` + EdgeCount int `json:"edgeCount"` + ExecutionInstanceCount int `json:"executionInstanceCount"` + SessionLinkCount int `json:"sessionLinkCount"` + StreamableSessionCount int `json:"streamableSessionCount"` + StreamableSessionIDs []string `json:"streamableSessionIds"` + StatusCounts nodeStatusCounts `json:"statusCounts"` + AllStatusCounts nodeStatusCounts `json:"allStatusCounts"` +} + +// FormulaRunDetail is the run-detail DTO. Port of TS FormulaRunDetail. Field +// order matches the object literal enrichFormulaRun returns (enrich.ts). +type FormulaRunDetail struct { + RunID string `json:"runId"` + RootBeadID string `json:"rootBeadId"` + RootStoreRef string `json:"rootStoreRef"` + ResolvedRootStore string `json:"resolvedRootStore"` + ScopeKind string `json:"scopeKind"` + ScopeRef string `json:"scopeRef"` + Title string `json:"title"` + Formula RunFormula `json:"formula"` + FormulaDetail RunFormulaDetailState `json:"formulaDetail"` + ExecutionPath RunExecutionPath `json:"executionPath"` + SnapshotVersion int `json:"snapshotVersion"` + SnapshotEventSeq RunSnapshotSequence `json:"snapshotEventSeq"` + Completeness FormulaRunCompleteness `json:"completeness"` + Progress FormulaRunProgress `json:"progress"` + Phase string `json:"phase"` + Stages []RunStage `json:"stages"` + Nodes []RunDisplayNode `json:"nodes"` + Edges []RunDisplayEdge `json:"edges"` + Lanes []RunDisplayLane `json:"lanes"` +} diff --git a/internal/runproj/enrich.go b/internal/runproj/enrich.go new file mode 100644 index 0000000000..8f39978fdd --- /dev/null +++ b/internal/runproj/enrich.go @@ -0,0 +1,332 @@ +package runproj + +import "time" + +// Run-health enrichment: a faithful Go port of the TypeScript run-summary enrich +// composition (shared/src/runs/health.ts + liveness.ts, driven by the frontend +// enrichRunSummary in supervisor/runSummary.ts). BuildRunSummary produces the +// bead-derived summary with health/census in the unavailable shell; +// EnrichRunSummary layers session-derived health + census on top. +// +// Per the run-view ADR the monotonic progress/thrash marks live in the per-city +// tailer, not in this function: the caller advances them with +// AdvanceProgressMarks once per fold generation and passes the result in, so a +// request-time enrich never double-advances them. + +// attemptClimbMin and thrashDetectedStreak are the default health thresholds. +// Port of TS DEFAULT_ATTEMPT_CLIMB_MIN / DEFAULT_THRASH_DETECTED_STREAK. +const ( + attemptClimbMin = 1 + thrashDetectedStreak = 2 +) + +// staleLatchAfterMs is the age past which a session-less, non-progressing open +// run is demoted out of Active. Port of TS STALE_LATCH_AFTER_MS (24h). +const staleLatchAfterMs = 24 * 60 * 60 * 1000 + +// LaneProgressMark is the per-lane monotonic progress record the tailer carries +// across fold generations to detect thrashing. Port of TS LaneProgressMark. +type LaneProgressMark struct { + Progress laneProgressComparison + ThrashStreak int +} + +// laneProgressComparison is the comparable-progress union. Port of TS +// LaneProgressComparison: {status:'comparable', stepId, stageIndex, attempt} | +// {status:'not_comparable', error}. +type laneProgressComparison struct { + Status string // "comparable" | "not_comparable" + StepID string + StageIndex int + Attempt int + Error string +} + +// AdvanceProgressMarks folds the previous per-lane marks forward against the +// current lanes, incrementing a lane's thrash streak when its graph position +// stayed flat while the active step's attempt climbed. Port of TS +// advanceProgressMarks. previous may be nil (cold start). +func AdvanceProgressMarks(previous map[string]LaneProgressMark, lanes []RunLane) map[string]LaneProgressMark { + next := make(map[string]LaneProgressMark, len(lanes)) + for _, lane := range lanes { + progress := comparableProgress(lane) + prior, hasPrior := previous[lane.ID] + + positionFlat := hasPrior && + prior.Progress.Status == "comparable" && + progress.Status == "comparable" && + prior.Progress.StepID == progress.StepID && + prior.Progress.StageIndex == progress.StageIndex + climbed := hasPrior && + prior.Progress.Status == "comparable" && + progress.Status == "comparable" && + progress.Attempt-prior.Progress.Attempt >= attemptClimbMin + + thrashStreak := 0 + if positionFlat && climbed { + thrashStreak = prior.ThrashStreak + 1 + } + + next[lane.ID] = LaneProgressMark{Progress: progress, ThrashStreak: thrashStreak} + } + return next +} + +// EnrichRunSummary layers session-derived health and the city census onto a +// bead-derived RunSummary. Port of the frontend enrichRunSummary: derive per-lane +// health from the session list, split blocked lanes back out, demote stale +// session-less latches out of Active, recompute counts and census. marks are the +// tailer's advanced per-city marks (pass nil for a cold first generation); nowMs +// is the snapshot generation time used for the stale-latch demotion. +func EnrichRunSummary(s RunSummary, sessions []DashboardSession, sessionsAvailable bool, nowMs int64, marks map[string]LaneProgressMark) RunSummary { + inFlight := make([]RunLane, 0, len(s.Lanes)+len(s.BlockedLanes)) + inFlight = append(inFlight, s.Lanes...) + inFlight = append(inFlight, s.BlockedLanes...) + + lanes := deriveRunHealthLanes(inFlight, sessions, sessionsAvailable, marks) + + blockedLanes := make([]RunLane, 0) + activeEnriched := make([]RunLane, 0) + for _, lane := range lanes { + if lane.Phase == "blocked" { + blockedLanes = append(blockedLanes, lane) + } else { + activeEnriched = append(activeEnriched, lane) + } + } + + liveActive := make([]RunLane, 0, len(activeEnriched)) + for _, lane := range activeEnriched { + if !isStaleSessionlessLatch(lane, nowMs, sessionsAvailable) { + liveActive = append(liveActive, lane) + } + } + + censusInput := make([]RunLane, 0, len(liveActive)+len(blockedLanes)) + censusInput = append(censusInput, liveActive...) + censusInput = append(censusInput, blockedLanes...) + + out := s + out.TotalActive = len(liveActive) + out.Lanes = liveActive + out.BlockedLanes = blockedLanes + out.RunCounts = runCounts(liveActive, len(liveActive), len(blockedLanes)) + out.Census = RunCensusState{Status: "available", Data: buildCensus(censusInput)} + return out +} + +// deriveRunHealthLanes returns the lanes with engine-derived health. Port of the +// lane-mapping half of TS deriveRunHealth (the census it also returns is recomputed +// by the caller after demotion, so it is not returned here). +func deriveRunHealthLanes(lanes []RunLane, sessions []DashboardSession, sessionsAvailable bool, marks map[string]LaneProgressMark) []RunLane { + out := make([]RunLane, 0, len(lanes)) + for _, lane := range lanes { + enriched := lane + + // Without the session list, health cannot be derived (gascity-dashboard + // 0gww): report the lane's health as genuinely unavailable rather than a + // degraded-but-available shell. + if !sessionsAvailable { + enriched.Health = RunLaneHealthState{Status: "unavailable", Error: "run session list unavailable"} + out = append(out, enriched) + continue + } + + session, resolved := resolveLaneSession(lane, sessions) + + phaseConfidence := "inferred" + if lane.FormulaStageResolved && resolved { + phaseConfidence = "known" + } + + thrashStreak := marks[lane.ID].ThrashStreak + + sessionState := RunLaneSessionState{Status: "unresolved", Error: "run session unresolved"} + if resolved { + sessionState = sessionFacts(session) + } + + enriched.Health = RunLaneHealthState{ + Status: "available", + Data: RunLaneHealth{ + PhaseConfidence: phaseConfidence, + NeedsOperator: laneNeedsOperator(lane), + StuckNode: stuckNode(lane), + ThrashingDetected: thrashStreak >= thrashDetectedStreak, + Session: sessionState, + }, + } + out = append(out, enriched) + } + return out +} + +// laneNeedsOperator reports the structural human-gate signal. Port of TS +// laneNeedsOperator: phase 'approval' or 'blocked'. Derived from lane.phase alone +// so it stays valid during a session-list outage. +func laneNeedsOperator(lane RunLane) bool { + return lane.Phase == "approval" || lane.Phase == "blocked" +} + +// resolveLaneSession resolves the first of a lane's active assignees to a +// session. Port of TS resolveLaneSession. +func resolveLaneSession(lane RunLane, sessions []DashboardSession) (DashboardSession, bool) { + for _, assignee := range lane.ActiveAssignees { + if s, ok := resolveSessionForTarget(assignee, sessions); ok { + return s, true + } + } + return DashboardSession{}, false +} + +// comparableProgress projects a lane's progress to the comparable shape used for +// thrash detection. Port of TS comparableProgress. +func comparableProgress(lane RunLane) laneProgressComparison { + if lane.Progress.Status != "active_step" { + return laneProgressComparison{Status: "not_comparable", Error: "run has no active step"} + } + if lane.Progress.Stage.Status != "available" { + return laneProgressComparison{Status: "not_comparable", Error: lane.Progress.Stage.Error} + } + if lane.Progress.Attempt.Status != "available" { + return laneProgressComparison{Status: "not_comparable", Error: lane.Progress.Attempt.Error} + } + return laneProgressComparison{ + Status: "comparable", + StepID: lane.Progress.StepID, + StageIndex: lane.Progress.Stage.Index, + Attempt: lane.Progress.Attempt.Value, + } +} + +// stuckNode reports the semantic node a lane is parked on. Port of TS stuckNode. +func stuckNode(lane RunLane) RunLaneStuckNode { + if lane.Progress.Status == "active_step" { + return RunLaneStuckNode{Status: "available", ID: lane.Progress.StepID} + } + return RunLaneStuckNode{Status: "unavailable", Error: "active run step unavailable"} +} + +// sessionFacts projects a resolved session into the lane's session-fact union. +// Port of TS sessionFacts. +func sessionFacts(session DashboardSession) RunLaneSessionState { + lastActive := RunLaneSessionLastActive{Status: "unavailable", Error: "session last_active unavailable"} + if session.LastActive != nil { + lastActive = RunLaneSessionLastActive{Status: "available", At: *session.LastActive} + } + activity := RunLaneSessionActivity{Status: "unavailable", Error: "session activity unavailable"} + if session.Activity != nil { + activity = RunLaneSessionActivity{Status: "available", Value: *session.Activity} + } + return RunLaneSessionState{ + Status: "resolved", + LastActive: lastActive, + Running: RunLaneSessionRunning{Status: "available", Value: session.Running}, + Activity: activity, + } +} + +// buildCensus tallies a threshold-independent city census from the enriched +// lanes. Port of TS buildCensus. +func buildCensus(lanes []RunLane) RunCensus { + var byPhase RunCensusByPhase + totalInFlight := 0 + unverifiable := 0 + knownDenominator := 0 + thrashing := 0 + + for _, lane := range lanes { + incCensusPhase(&byPhase, lane.Phase) + if lane.Phase == "complete" { + continue + } + totalInFlight++ + if lane.Health.Status == "available" && lane.Health.Data.PhaseConfidence == "known" { + knownDenominator++ + if lane.Health.Data.ThrashingDetected { + thrashing++ + } + } else { + unverifiable++ + } + } + + return RunCensus{ + ByPhase: byPhase, + TotalInFlight: totalInFlight, + Unverifiable: unverifiable, + KnownDenominator: knownDenominator, + Thrashing: thrashing, + } +} + +func incCensusPhase(b *RunCensusByPhase, phase string) { + switch phase { + case "intake": + b.Intake++ + case "implementation": + b.Implementation++ + case "review": + b.Review++ + case "approval": + b.Approval++ + case "finalization": + b.Finalization++ + case "blocked": + b.Blocked++ + case "complete": + b.Complete++ + case "active": + b.Active++ + } +} + +// isStaleSessionlessLatch reports whether an open, non-progressing, session-less +// lane is old enough to demote out of Active. Port of TS isStaleSessionlessLatch. +// nowMs is the snapshot generation time, not a live clock. +func isStaleSessionlessLatch(lane RunLane, nowMs int64, sessionsAvailable bool) bool { + if !sessionsAvailable { + return false + } + if lane.Phase == "complete" || lane.Phase == "blocked" { + return false + } + if lane.Progress.Status == "active_step" { + return false + } + if laneSessionResolved(lane) { + return false + } + if lane.UpdatedAt.Status != "available" { + return false + } + // A parse failure means we cannot judge staleness — mirrors the TS + // Number.isFinite(ageMs) guard (Date.parse → NaN → not stale). + ms, ok := millisFromTimestamp(lane.UpdatedAt.At) + if !ok { + return false + } + return nowMs-ms >= staleLatchAfterMs +} + +// laneSessionResolved reports whether a lane's enriched health carries a resolved +// session. Port of TS laneSessionResolved. +func laneSessionResolved(lane RunLane) bool { + return lane.Health.Status == "available" && lane.Health.Data.Session.Status == "resolved" +} + +// millisFromTimestamp parses an RFC3339 timestamp to Unix milliseconds, reporting +// ok=false on an empty or unparseable value (the TS Number.isFinite guard). +func millisFromTimestamp(value string) (int64, bool) { + if value == "" { + return 0, false + } + t, err := time.Parse(time.RFC3339, value) + if err != nil { + t, err = time.Parse(time.RFC3339Nano, value) + if err != nil { + return 0, false + } + } + return t.UnixMilli(), true +} diff --git a/internal/runproj/enrich_test.go b/internal/runproj/enrich_test.go new file mode 100644 index 0000000000..8f605078a2 --- /dev/null +++ b/internal/runproj/enrich_test.go @@ -0,0 +1,265 @@ +package runproj + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gastownhall/gascity/internal/beads" +) + +// TestEnrichRunSummaryGolden pins the Go EnrichRunSummary port to the TypeScript +// oracle: it folds the shared bead fixture into a bead-derived summary, advances +// the cold marks, enriches it against the shared sessions fixture at the same +// fixed generation time the generator used, and asserts the canonical JSON +// matches runsummary_enriched_golden.json byte-for-byte. +func TestEnrichRunSummaryGolden(t *testing.T) { + beadList := loadFixtureBeads(t) + sessions := loadFixtureSessions(t) + + base := BuildRunSummary(beadList) + + inFlight := make([]RunLane, 0, len(base.Lanes)+len(base.BlockedLanes)) + inFlight = append(inFlight, base.Lanes...) + inFlight = append(inFlight, base.BlockedLanes...) + marks := AdvanceProgressMarks(nil, inFlight) + + // Must equal the generation time frozen into runsummary_enriched_golden.json + // (captured from the now-retired gen-run-goldens.mts; the golden is the + // Go-owned source of truth). + nowMs := mustMillis(t, "2026-06-09T00:00:00Z") + enriched := EnrichRunSummary(base, sessions, true, nowMs, marks) + + got, err := canonicalJSON(enriched) + if err != nil { + t.Fatalf("marshal enriched summary: %v", err) + } + want, err := os.ReadFile(filepath.Join("testdata", "runsummary_enriched_golden.json")) + if err != nil { + t.Fatalf("read golden: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("enriched run summary does not match golden:\n%s", unifiedDiff(string(want), string(got))) + } +} + +// TestDeriveRunHealthSessionUnavailability ports health.test.ts (gascity-dashboard +// 0gww): without the session list every lane's health collapses to unavailable; +// with it available, health derives. +func TestDeriveRunHealthSessionUnavailability(t *testing.T) { + mk := func(id string) RunLane { + return RunLane{ + ID: id, + Phase: "implementation", + ActiveAssignees: []string{"app/codex"}, + UpdatedAt: RunLaneUpdatedAt{Status: "available", At: "2026-06-08T00:00:00Z"}, + Progress: RunLaneProgress{Status: "unavailable", Error: "run progress unavailable"}, + FormulaStageResolved: false, + Health: runHealthUnavailable(), + } + } + + t.Run("unavailable session list ⇒ every lane health unavailable", func(t *testing.T) { + lanes := deriveRunHealthLanes([]RunLane{mk("run-a"), mk("run-b")}, nil, false, nil) + for _, l := range lanes { + if l.Health.Status != "unavailable" { + t.Errorf("lane %s: health.status = %q, want unavailable", l.ID, l.Health.Status) + } + if l.Health.Error != "run session list unavailable" { + t.Errorf("lane %s: health.error = %q", l.ID, l.Health.Error) + } + } + }) + + t.Run("available session list ⇒ health derives", func(t *testing.T) { + lanes := deriveRunHealthLanes([]RunLane{mk("run-a")}, nil, true, nil) + if lanes[0].Health.Status != "available" { + t.Errorf("health.status = %q, want available", lanes[0].Health.Status) + } + }) +} + +// TestIsStaleSessionlessLatch ports liveness.test.ts (gascity-dashboard-s4rp): +// the sharp session-less demotion predicate. +func TestIsStaleSessionlessLatch(t *testing.T) { + nowMs := mustMillis(t, "2026-06-07T00:00:00Z") + + at := func(deltaMs int64) RunLaneUpdatedAt { + return RunLaneUpdatedAt{Status: "available", At: time.UnixMilli(nowMs - deltaMs).UTC().Format(time.RFC3339)} + } + health := func(session string) RunLaneHealthState { + sess := RunLaneSessionState{Status: "unresolved", Error: "run session unresolved"} + if session == "resolved" { + sess = RunLaneSessionState{ + Status: "resolved", + LastActive: RunLaneSessionLastActive{Status: "available", At: "2026-06-07T00:00:00Z"}, + Running: RunLaneSessionRunning{Status: "available", Value: true}, + Activity: RunLaneSessionActivity{Status: "available", Value: "working"}, + } + } + return RunLaneHealthState{Status: "available", Data: RunLaneHealth{ + PhaseConfidence: "inferred", + StuckNode: RunLaneStuckNode{Status: "unavailable", Error: "active run step unavailable"}, + ThrashingDetected: false, + Session: sess, + }} + } + // The gc-1920 baseline: approval-gate latch, unresolved session, no active + // step, ~4 days stale. + base := RunLane{ + ID: "gc-1920", + Phase: "approval", + ActiveAssignees: []string{}, + UpdatedAt: at(4 * 24 * 60 * 60 * 1000), + Progress: RunLaneProgress{Status: "unavailable", Error: "run progress unavailable"}, + Health: health("unresolved"), + } + withProgress := base + withProgress.Progress = RunLaneProgress{ + Status: "active_step", + StepID: "implementation.patch", + Stage: RunLaneStagePosition{Status: "unavailable", Error: "active run stage unavailable"}, + Attempt: RunLaneStepAttempt{Status: "unavailable", Error: "run step attempt unavailable"}, + } + + clone := func(mut func(*RunLane)) RunLane { + l := base + mut(&l) + return l + } + + cases := []struct { + name string + lane RunLane + sessionsAvailable bool + want bool + }{ + {"demotes stale gc-1920 latch", base, true, true}, + {"keeps freshly-queued recent run", clone(func(l *RunLane) { l.Phase = "intake"; l.UpdatedAt = at(60_000) }), true, false}, + {"keeps recent approval gate", clone(func(l *RunLane) { l.UpdatedAt = at(30 * 60_000) }), true, false}, + {"keeps stale run with resolved session", clone(func(l *RunLane) { l.Health = health("resolved") }), true, false}, + {"keeps stale run with in_progress step", withProgress, true, false}, + {"no demotion when session list unavailable", base, false, false}, + {"never demotes complete", clone(func(l *RunLane) { l.Phase = "complete" }), true, false}, + {"never demotes blocked", clone(func(l *RunLane) { l.Phase = "blocked" }), true, false}, + {"no demotion without known age", clone(func(l *RunLane) { + l.UpdatedAt = RunLaneUpdatedAt{Status: "unavailable", Error: "run update time unavailable"} + }), true, false}, + {"boundary just under floor stays", clone(func(l *RunLane) { l.UpdatedAt = at(staleLatchAfterMs - 1_000) }), true, false}, + {"boundary at floor demotes", clone(func(l *RunLane) { l.UpdatedAt = at(staleLatchAfterMs) }), true, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := isStaleSessionlessLatch(tc.lane, nowMs, tc.sessionsAvailable); got != tc.want { + t.Errorf("isStaleSessionlessLatch = %v, want %v", got, tc.want) + } + }) + } +} + +// TestAdvanceProgressMarksThrash exercises the cross-generation monotonicity the +// golden cannot reach (one snapshot ⇒ all streaks 0): a lane whose graph position +// stays flat while the active step's attempt climbs accrues a thrash streak, and +// the census counts it as thrashing only once the streak crosses the threshold +// AND the lane is phaseConfidence 'known'. +func TestAdvanceProgressMarksThrash(t *testing.T) { + mkLane := func(attempt int) RunLane { + return RunLane{ + ID: "run-thrash", + Phase: "review", + ActiveAssignees: []string{"pool-x"}, + FormulaStageResolved: true, + UpdatedAt: RunLaneUpdatedAt{Status: "available", At: "2026-06-08T00:00:00Z"}, + Progress: RunLaneProgress{ + Status: "active_step", + StepID: "review-loop", + Stage: RunLaneStagePosition{Status: "available", Index: 1, Key: "review", Label: "Review"}, + Attempt: RunLaneStepAttempt{Status: "available", Value: attempt}, + }, + Health: runHealthUnavailable(), + } + } + session := DashboardSession{ID: "s1", SessionName: "x", State: "active", Provider: "claude", Running: true} + session.Alias = ptr("pool-x") + sessions := []DashboardSession{session} + + // Generation 1: attempt 1, no prior marks ⇒ streak 0. + g1 := []RunLane{mkLane(1)} + marks := AdvanceProgressMarks(nil, g1) + if marks["run-thrash"].ThrashStreak != 0 { + t.Fatalf("gen1 streak = %d, want 0", marks["run-thrash"].ThrashStreak) + } + + // Generation 2: same stage/step, attempt climbed to 2 ⇒ streak 1. + g2 := []RunLane{mkLane(2)} + marks = AdvanceProgressMarks(marks, g2) + if marks["run-thrash"].ThrashStreak != 1 { + t.Fatalf("gen2 streak = %d, want 1", marks["run-thrash"].ThrashStreak) + } + + // Generation 3: attempt climbed to 3 ⇒ streak 2 ⇒ thrashingDetected. + g3 := []RunLane{mkLane(3)} + marks = AdvanceProgressMarks(marks, g3) + if marks["run-thrash"].ThrashStreak != 2 { + t.Fatalf("gen3 streak = %d, want 2", marks["run-thrash"].ThrashStreak) + } + + lanes := deriveRunHealthLanes(g3, sessions, true, marks) + h := lanes[0].Health + if h.Status != "available" || !h.Data.ThrashingDetected { + t.Fatalf("expected thrashingDetected with known confidence, got %+v", h) + } + if h.Data.PhaseConfidence != "known" { + t.Fatalf("phaseConfidence = %q, want known", h.Data.PhaseConfidence) + } + census := buildCensus(lanes) + if census.Thrashing != 1 || census.KnownDenominator != 1 { + t.Fatalf("census thrashing=%d knownDenominator=%d, want 1/1", census.Thrashing, census.KnownDenominator) + } + + // A flat attempt resets the streak to 0 (progress stalled, not thrashing). + marks = AdvanceProgressMarks(marks, []RunLane{mkLane(3)}) + if marks["run-thrash"].ThrashStreak != 0 { + t.Fatalf("flat-attempt streak = %d, want 0", marks["run-thrash"].ThrashStreak) + } +} + +func loadFixtureBeads(t *testing.T) []beads.Bead { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "beads_fixture.json")) + if err != nil { + t.Fatalf("read bead fixture: %v", err) + } + var beadList []beads.Bead + if err := json.Unmarshal(raw, &beadList); err != nil { + t.Fatalf("unmarshal bead fixture: %v", err) + } + return beadList +} + +func loadFixtureSessions(t *testing.T) []DashboardSession { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "sessions_fixture.json")) + if err != nil { + t.Fatalf("read sessions fixture: %v", err) + } + var sessions []DashboardSession + if err := json.Unmarshal(raw, &sessions); err != nil { + t.Fatalf("unmarshal sessions fixture: %v", err) + } + return sessions +} + +func mustMillis(t *testing.T, value string) int64 { + t.Helper() + ms, ok := millisFromTimestamp(value) + if !ok { + t.Fatalf("parse %q failed", value) + } + return ms +} + +func ptr(s string) *string { return &s } diff --git a/internal/runproj/filter_test.go b/internal/runproj/filter_test.go new file mode 100644 index 0000000000..78f56cf2d0 --- /dev/null +++ b/internal/runproj/filter_test.go @@ -0,0 +1,46 @@ +package runproj + +import ( + "testing" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" +) + +// TestFilterRunBeads pins the projection-boundary filter policy: engineering +// types and gc.kind=run roots are kept, while gc:-labeled control beads and +// non-run bookkeeping types (message/session) are dropped. This is the analog of +// the frontend runBeadFilter (summary.ts) that the live tailer applies +// before the pure summary/detail builders. +func TestFilterRunBeads(t *testing.T) { + in := []beads.Bead{ + {ID: "root", Type: "molecule", Metadata: map[string]string{beadmeta.KindMetadataKey: "run"}}, + {ID: "task", Type: "task"}, + {ID: "bug", Type: "bug"}, + {ID: "ctl", Type: "task", Labels: []string{"gc:control"}}, + {ID: "msg", Type: "message", Metadata: map[string]string{beadmeta.RootBeadIDMetadataKey: "root"}}, + {ID: "sess", Type: "session"}, + {ID: "run-labeled", Type: "molecule", Labels: []string{"gc:workflow"}, Metadata: map[string]string{beadmeta.KindMetadataKey: "run"}}, + } + + got := FilterRunBeads(in) + + gotIDs := map[string]bool{} + for _, b := range got { + gotIDs[b.ID] = true + } + want := map[string]bool{"root": true, "task": true, "bug": true} + for id := range want { + if !gotIDs[id] { + t.Errorf("FilterRunBeads dropped %q; it should be kept", id) + } + } + for _, id := range []string{"ctl", "msg", "sess", "run-labeled"} { + if gotIDs[id] { + t.Errorf("FilterRunBeads kept %q; a gc:-labeled or non-run bead must be dropped", id) + } + } + if len(got) != len(want) { + t.Errorf("FilterRunBeads returned %d beads, want %d (%v)", len(got), len(want), gotIDs) + } +} diff --git a/internal/runproj/fold.go b/internal/runproj/fold.go new file mode 100644 index 0000000000..a61ff188d7 --- /dev/null +++ b/internal/runproj/fold.go @@ -0,0 +1,82 @@ +// Package runproj projects the dashboard run view from a city's append-only +// event log (.gc/events.jsonl) — the OSS-local analog of the hosted ClickHouse +// run projection. It folds bead lifecycle events into the latest bead snapshot +// per id and (in later phases) builds the RunSummary and run-detail off that +// fold, so the run view no longer scans the beads molecule history. +// +// Layering: this is object-model-layer code. It depends only on internal/beads +// and internal/events, never on the API or CLI layers, so the same projection +// can back any consumer. +package runproj + +import ( + "encoding/json" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" +) + +// beadEventTypes are the event types the fold consumes; everything else is +// ignored. Kept as a set so callers can pre-filter a read if they want. +var beadEventTypes = map[string]bool{ + events.BeadCreated: true, + events.BeadUpdated: true, + events.BeadClosed: true, + events.BeadDeleted: true, +} + +// Fold reduces a chronological (seq-ordered) event slice to the latest bead +// snapshot per id. bead.created/updated/closed upsert the snapshot; +// bead.deleted removes it. Non-bead events are ignored. The result is the input +// to buildRunSummary / buildRunDetail. +func Fold(evts []events.Event) map[string]beads.Bead { + out := make(map[string]beads.Bead) + Apply(out, evts) + return out +} + +// Apply folds evts into an existing bead map in place (the live-tail path: +// apply newly-watched events to the warm snapshot). Returns the highest seq +// applied, so the caller can advance its cursor. +func Apply(into map[string]beads.Bead, evts []events.Event) (lastSeq uint64) { + for i := range evts { + e := &evts[i] + if e.Seq > lastSeq { + lastSeq = e.Seq + } + if !beadEventTypes[e.Type] { + continue + } + b, ok := decodeBead(e.Payload) + if !ok { + continue + } + if e.Type == events.BeadDeleted { + delete(into, b.ID) + continue + } + into[b.ID] = b + } + return lastSeq +} + +// decodeBead extracts a beads.Bead from a bead.* event payload. The current +// payload shape is {"bead": }; older logs wrote the raw snapshot +// directly, so both are accepted. A payload without an id is treated as a +// decode miss. +func decodeBead(payload json.RawMessage) (beads.Bead, bool) { + if len(payload) == 0 { + return beads.Bead{}, false + } + var env struct { + Bead beads.Bead `json:"bead"` + } + if err := json.Unmarshal(payload, &env); err == nil && env.Bead.ID != "" { + return env.Bead, true + } + var raw beads.Bead + if err := json.Unmarshal(payload, &raw); err == nil && raw.ID != "" { + return raw, true + } + return beads.Bead{}, false +} diff --git a/internal/runproj/fold_test.go b/internal/runproj/fold_test.go new file mode 100644 index 0000000000..1ecd75b384 --- /dev/null +++ b/internal/runproj/fold_test.go @@ -0,0 +1,84 @@ +package runproj + +import ( + "encoding/json" + "testing" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" +) + +func beadEvent(seq uint64, typ, id, status string) events.Event { + payload, _ := json.Marshal(struct { + Bead beads.Bead `json:"bead"` + }{beads.Bead{ID: id, Status: status, Type: "task"}}) + return events.Event{Seq: seq, Type: typ, Payload: payload} +} + +func TestFoldKeepsLatestSnapshotPerID(t *testing.T) { + evts := []events.Event{ + beadEvent(1, events.BeadCreated, "a", "open"), + beadEvent(2, events.BeadCreated, "b", "open"), + beadEvent(3, events.BeadUpdated, "a", "in_progress"), + beadEvent(4, events.BeadClosed, "b", "closed"), + {Seq: 5, Type: events.SessionWoke, Subject: "worker-1"}, // ignored + beadEvent(6, events.BeadCreated, "c", "open"), + beadEvent(7, events.BeadDeleted, "c", "open"), + } + + got := Fold(evts) + + if len(got) != 2 { + t.Fatalf("fold size = %d, want 2 (a + b; c deleted, session ignored)", len(got)) + } + if got["a"].Status != "in_progress" { + t.Errorf("a.status = %q, want in_progress (latest snapshot wins)", got["a"].Status) + } + if got["b"].Status != "closed" { + t.Errorf("b.status = %q, want closed", got["b"].Status) + } + if _, ok := got["c"]; ok { + t.Error("c should be removed by bead.deleted") + } +} + +func TestApplyAdvancesCursorAndMutatesInPlace(t *testing.T) { + state := Fold([]events.Event{beadEvent(10, events.BeadCreated, "a", "open")}) + + last := Apply(state, []events.Event{ + beadEvent(11, events.BeadUpdated, "a", "closed"), + beadEvent(12, events.BeadCreated, "d", "open"), + }) + + if last != 12 { + t.Errorf("lastSeq = %d, want 12", last) + } + if state["a"].Status != "closed" { + t.Errorf("a.status = %q, want closed after live-tail apply", state["a"].Status) + } + if _, ok := state["d"]; !ok { + t.Error("d should be added by live-tail apply") + } +} + +func TestApplyCursorTracksMaxSeqEvenForIgnoredEvents(t *testing.T) { + // A non-bead event still advances the cursor so the tailer does not re-read + // it; only the fold map is unaffected. + state := map[string]beads.Bead{} + last := Apply(state, []events.Event{{Seq: 99, Type: events.SessionStopped, Subject: "w"}}) + if last != 99 { + t.Errorf("lastSeq = %d, want 99 (cursor advances past ignored events)", last) + } + if len(state) != 0 { + t.Errorf("fold size = %d, want 0", len(state)) + } +} + +func TestDecodeBeadAcceptsLegacyRawShape(t *testing.T) { + // Older logs wrote the raw bead snapshot with no {"bead": ...} envelope. + raw, _ := json.Marshal(beads.Bead{ID: "legacy", Status: "open", Type: "task"}) + b, ok := decodeBead(raw) + if !ok || b.ID != "legacy" { + t.Fatalf("legacy raw-shape decode failed: ok=%v bead=%+v", ok, b) + } +} diff --git a/internal/runproj/formulaname.go b/internal/runproj/formulaname.go new file mode 100644 index 0000000000..3692f0663a --- /dev/null +++ b/internal/runproj/formulaname.go @@ -0,0 +1,101 @@ +package runproj + +import ( + "strings" + "unicode" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// resolveRunFormulaIdentityLane resolves a run group's formula name for the +// "lane" mode used by the summary builder. Faithful port of the lane path +// through TS resolveRunFormulaIdentity (shared/src/runs/formula-name.ts): the +// summary uses only mode='lane' with no formulaDetail, so this collapses to the +// metadata-name path followed by the graph.v2 title fallback. The bool mirrors +// TS's `name: string | null`. +// +// Metadata path (runFormulaMetadataName, mode='lane'): first non-empty +// pr_review.workflow_formula across issues, else first non-empty gc.formula. +// Title fallback (runFormulaTitleFallback, mode='lane'): only for a root that +// carries gc.formula_contract='graph.v2' AND gc.run_target, is non-terminal, +// and whose trimmed title starts with "mol-". +func resolveRunFormulaIdentityLane(root *runIssue, issues []runIssue) (string, bool) { + if name := metadataNonEmptyAcrossIssues(issues, "pr_review.workflow_formula"); name != "" { + return name, true + } + if name := metadataNonEmptyAcrossIssues(issues, beadmeta.FormulaMetadataKey); name != "" { + return name, true + } + + if name, ok := runFormulaTitleFallbackLane(root); ok { + return name, true + } + return "", false +} + +// metadataNonEmptyAcrossIssues returns the first trimmed non-empty value for key +// across issues. Mirrors formula-name.ts metadataString (which uses rootMeta / +// nonEmpty — trimmed, empty-skipping). +func metadataNonEmptyAcrossIssues(issues []runIssue, key string) string { + for _, i := range issues { + if v := nonEmpty(i.metadata[key]); v != "" { + return v + } + } + return "" +} + +// runFormulaTitleFallbackLane is the lane-mode graph.v2 title fallback. +// Port of TS runFormulaTitleFallback for mode='lane'. +func runFormulaTitleFallbackLane(root *runIssue) (string, bool) { + if root == nil { + return "", false + } + if nonEmpty(root.metadata[beadmeta.FormulaContractMetadataKey]) != "graph.v2" || + nonEmpty(root.metadata[beadmeta.RunTargetMetadataKey]) == "" || + isTerminalRunRootStatus(root.status) { + return "", false + } + title := nonEmpty(root.title) + if title == "" { + return "", false + } + // mode === 'lane' && !title.startsWith('mol-') ? null : title + if !strings.HasPrefix(title, "mol-") { + return "", false + } + return title, true +} + +// isTerminalRunRootStatus reports whether a root status is terminal. +// Port of TS isTerminalRunRootStatus. +func isTerminalRunRootStatus(status string) bool { + switch strings.ToLower(strings.TrimSpace(status)) { + case "closed", "completed", "done", "failed", "skipped": + return true + default: + return false + } +} + +// nonEmpty trims a value and returns "" for empty/whitespace. Port of TS +// nonEmpty (bead-fields.ts), which trims the ECMAScript String.prototype.trim() +// whitespace set. That set differs from Go's unicode.IsSpace in exactly two +// codepoints with opposite membership: JS strips U+FEFF (ZWNBSP/BOM) but NOT +// U+0085 (NEL). jsTrimCut applies that delta so the trim is byte-faithful. +func nonEmpty(value string) string { + return strings.TrimFunc(value, jsTrimCut) +} + +// jsTrimCut reports whether r is in the ECMAScript String.prototype.trim() +// whitespace set: unicode.IsSpace, minus U+0085, plus U+FEFF. +func jsTrimCut(r rune) bool { + switch r { + case '\u0085': // NEL: Go's unicode.IsSpace trims it, JS does not. + return false + case '\ufeff': // ZWNBSP/BOM: JS trims it, Go's unicode.IsSpace does not. + return true + default: + return unicode.IsSpace(r) + } +} diff --git a/internal/runproj/marshal.go b/internal/runproj/marshal.go new file mode 100644 index 0000000000..69e6444a3d --- /dev/null +++ b/internal/runproj/marshal.go @@ -0,0 +1,297 @@ +package runproj + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// The union DTO types below are TypeScript discriminated unions. Go has no +// native tagged unions, so each carries every arm's fields and a custom +// MarshalJSON that emits exactly the active arm's keys, in the same order the +// TS object literals use. Key order is load-bearing for byte-for-byte golden +// parity (the generator used JSON.stringify, which preserves insertion order). +// +// We build each object with an ordered list of (key, value) pairs rather than a +// map, because encoding/json sorts map keys and would break parity. + +type kv struct { + key string + value any +} + +// marshalObject renders an ordered set of key/value pairs as a JSON object, +// preserving the given key order (unlike a Go map, which json sorts). +func marshalObject(pairs []kv) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('{') + for i, p := range pairs { + if i > 0 { + buf.WriteByte(',') + } + k, err := json.Marshal(p.key) + if err != nil { + return nil, err + } + buf.Write(k) + buf.WriteByte(':') + v, err := json.Marshal(p.value) + if err != nil { + return nil, err + } + buf.Write(v) + } + buf.WriteByte('}') + return buf.Bytes(), nil +} + +// MarshalJSON renders the active formula arm. TS: {status:'known', name} | +// {status:'unavailable', error}. +func (f RunLaneFormula) MarshalJSON() ([]byte, error) { + switch f.Status { + case "known": + return marshalObject([]kv{{"status", "known"}, {"name", f.Name}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", f.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneFormula status %q", f.Status) + } +} + +// MarshalJSON renders the active scope arm. TS: {status:'available', kind, ref, +// rootStoreRef} | {status:'unavailable', error}. +func (s RunLaneScope) MarshalJSON() ([]byte, error) { + switch s.Status { + case "available": + return marshalObject([]kv{ + {"status", "available"}, + {"kind", s.Kind}, + {"ref", s.Ref}, + {"rootStoreRef", s.RootStoreRef}, + }) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", s.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneScope status %q", s.Status) + } +} + +// MarshalJSON renders the active external-reference arm. TS: +// {status:'available', label, url} | {status:'label_only', label} | +// {status:'unavailable', error}. +func (e RunLaneExternalReference) MarshalJSON() ([]byte, error) { + switch e.Status { + case "available": + return marshalObject([]kv{ + {"status", "available"}, + {"label", e.Label}, + {"url", e.URL}, + }) + case "label_only": + return marshalObject([]kv{{"status", "label_only"}, {"label", e.Label}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", e.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneExternalReference status %q", e.Status) + } +} + +// MarshalJSON renders the active updated-at arm. TS: {status:'available', at} | +// {status:'unavailable', error}. +func (u RunLaneUpdatedAt) MarshalJSON() ([]byte, error) { + switch u.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"at", u.At}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", u.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneUpdatedAt status %q", u.Status) + } +} + +// MarshalJSON renders the active progress arm. TS: {status:'active_step', +// stepId, stage, attempt} | {status:'stage_only', stage, error} | +// {status:'unavailable', error}. +func (p RunLaneProgress) MarshalJSON() ([]byte, error) { + switch p.Status { + case "active_step": + return marshalObject([]kv{ + {"status", "active_step"}, + {"stepId", p.StepID}, + {"stage", p.Stage}, + {"attempt", p.Attempt}, + }) + case "stage_only": + return marshalObject([]kv{ + {"status", "stage_only"}, + {"stage", p.Stage}, + {"error", p.Error}, + }) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", p.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneProgress status %q", p.Status) + } +} + +// MarshalJSON renders the active stage-position arm. TS: {status:'available', +// index, key, label} | {status:'unavailable', error}. +func (s RunLaneStagePosition) MarshalJSON() ([]byte, error) { + switch s.Status { + case "available": + return marshalObject([]kv{ + {"status", "available"}, + {"index", s.Index}, + {"key", s.Key}, + {"label", s.Label}, + }) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", s.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneStagePosition status %q", s.Status) + } +} + +// MarshalJSON renders the active step-attempt arm. TS: {status:'available', +// value} | {status:'unavailable', error}. +func (a RunLaneStepAttempt) MarshalJSON() ([]byte, error) { + switch a.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"value", a.Value}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", a.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneStepAttempt status %q", a.Status) + } +} + +// MarshalJSON renders the active lane-health arm. BuildRunSummary emits the +// unavailable arm; EnrichRunSummary emits the available arm. TS: +// {status:'available', data} | {status:'unavailable', error}. +func (h RunLaneHealthState) MarshalJSON() ([]byte, error) { + switch h.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"data", h.Data}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", h.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneHealthState status %q", h.Status) + } +} + +// MarshalJSON renders RunLaneHealth in deriveRunHealth's object-literal order. +func (h RunLaneHealth) MarshalJSON() ([]byte, error) { + return marshalObject([]kv{ + {"phaseConfidence", h.PhaseConfidence}, + {"needsOperator", h.NeedsOperator}, + {"stuckNode", h.StuckNode}, + {"thrashingDetected", h.ThrashingDetected}, + {"session", h.Session}, + }) +} + +// MarshalJSON renders the active stuck-node arm. TS: {status:'available', id} | +// {status:'unavailable', error}. +func (n RunLaneStuckNode) MarshalJSON() ([]byte, error) { + switch n.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"id", n.ID}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", n.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneStuckNode status %q", n.Status) + } +} + +// MarshalJSON renders the active session arm. TS: {status:'resolved', lastActive, +// running, activity} | {status:'unresolved', error}. +func (s RunLaneSessionState) MarshalJSON() ([]byte, error) { + switch s.Status { + case "resolved": + return marshalObject([]kv{ + {"status", "resolved"}, + {"lastActive", s.LastActive}, + {"running", s.Running}, + {"activity", s.Activity}, + }) + case "unresolved": + return marshalObject([]kv{{"status", "unresolved"}, {"error", s.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneSessionState status %q", s.Status) + } +} + +// MarshalJSON renders the active last-active arm. TS Avail<{at}>. +func (l RunLaneSessionLastActive) MarshalJSON() ([]byte, error) { + switch l.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"at", l.At}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", l.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneSessionLastActive status %q", l.Status) + } +} + +// MarshalJSON renders the active running arm. TS Avail<{value}>. +func (r RunLaneSessionRunning) MarshalJSON() ([]byte, error) { + switch r.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"value", r.Value}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", r.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneSessionRunning status %q", r.Status) + } +} + +// MarshalJSON renders the active activity arm. TS Avail<{value}>. +func (a RunLaneSessionActivity) MarshalJSON() ([]byte, error) { + switch a.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"value", a.Value}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", a.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunLaneSessionActivity status %q", a.Status) + } +} + +// MarshalJSON renders the active census arm. BuildRunSummary emits the +// unavailable arm; EnrichRunSummary emits the available arm. TS: +// {status:'available', data} | {status:'unavailable', error}. +func (c RunCensusState) MarshalJSON() ([]byte, error) { + switch c.Status { + case "available": + return marshalObject([]kv{{"status", "available"}, {"data", c.Data}}) + case "unavailable": + return marshalObject([]kv{{"status", "unavailable"}, {"error", c.Error}}) + default: + return nil, fmt.Errorf("runproj: invalid RunCensusState status %q", c.Status) + } +} + +// MarshalJSON renders RunCensus in buildCensus's object-literal order. +func (c RunCensus) MarshalJSON() ([]byte, error) { + return marshalObject([]kv{ + {"byPhase", c.ByPhase}, + {"totalInFlight", c.TotalInFlight}, + {"unverifiable", c.Unverifiable}, + {"knownDenominator", c.KnownDenominator}, + {"thrashing", c.Thrashing}, + }) +} + +// MarshalJSON renders the per-phase tally in zeroByPhase()'s key order. +func (b RunCensusByPhase) MarshalJSON() ([]byte, error) { + return marshalObject([]kv{ + {"intake", b.Intake}, + {"implementation", b.Implementation}, + {"review", b.Review}, + {"approval", b.Approval}, + {"finalization", b.Finalization}, + {"blocked", b.Blocked}, + {"complete", b.Complete}, + {"active", b.Active}, + }) +} diff --git a/internal/runproj/phasemapping.go b/internal/runproj/phasemapping.go new file mode 100644 index 0000000000..f2b3b8dd9f --- /dev/null +++ b/internal/runproj/phasemapping.go @@ -0,0 +1,723 @@ +package runproj + +import ( + "regexp" + "sort" + "strconv" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// runIssue is the phase classifier's input shape, a faithful port of the TS +// RunIssue (shared/src/runs/phaseMapping.ts). updatedAt is the resolved ISO +// string (zero updated_at falls back to created_at, mirroring fromDashboardBead). +type runIssue struct { + id string + title string + desc string + status string + issueType string + assignee string + updatedAt string + parent string + metadata map[string]string +} + +// phaseMapping is the result of mapRunPhase. Port of TS PhaseMapping. +type phaseMapping struct { + phase string + label string + reviewRound int // valid only when hasReviewRound is true + hasRound bool // TS reviewRound: number | null +} + +// mapRunPhase classifies a run group into a phase. Port of TS mapRunPhase. +func mapRunPhase(issues []runIssue) phaseMapping { + // Status-based branches first — authoritative. + for _, i := range issues { + if i.status == "blocked" || strings.Contains(textForIssue(i), "blocked") { + return phaseMapping{phase: "blocked", label: "blocked"} + } + } + + if len(issues) > 0 { + allClosed := true + for _, i := range issues { + if i.status != "closed" { + allClosed = false + break + } + } + if allClosed { + return phaseMapping{phase: "complete", label: "complete"} + } + } + + // gascity-dashboard-q3p1: structured-first phase derivation. + if structured, ok := structuredPhase(issues); ok { + return structured + } + + return fallbackPhase(issues) +} + +// structuredPhase derives the phase from the run's current step. +// Port of TS structuredPhase. The bool return mirrors TS's `null`. +func structuredPhase(issues []runIssue) (phaseMapping, bool) { + var primary []runIssue + for _, i := range issues { + if isPrimaryStepIssue(i) { + primary = append(primary, i) + } + } + var inProgress []runIssue + for _, i := range primary { + if i.status == "in_progress" { + inProgress = append(inProgress, i) + } + } + activeStepID, hasActive := latestStepID(inProgress) + if !hasActive { + activeStepID, hasActive = furthestStageStepID(primary) + } + if !hasActive { + return phaseMapping{}, false + } + + phase := stepIDPhase(activeStepID) + if phase == "review" { + resolved, ok := reviewRoundForIssues(issues) + if !ok { + resolved = fallbackReviewRound(issues) + } + return phaseMapping{ + phase: "review", + label: "review round " + strconv.Itoa(resolved), + reviewRound: resolved, + hasRound: true, + }, true + } + return phaseMapping{phase: phase, label: phase}, true +} + +var stepIDDelimiters = regexp.MustCompile(`[-._:/]+`) + +func tokenizeStepID(stepID string) []string { + parts := stepIDDelimiters.Split(strings.ToLower(stepID), -1) + out := make([]string, 0, len(parts)) + for _, p := range parts { + if p != "" { + out = append(out, p) + } + } + return out +} + +// leadUpQualifierTokens mark a step as LEADING UP TO a gate rather than being +// the gate itself. Port of TS LEAD_UP_QUALIFIER_TOKENS. +var leadUpQualifierTokens = map[string]bool{ + "pre": true, "prepare": true, "wait": true, "await": true, + "pending": true, "before": true, "for": true, "to": true, +} + +var ( + approvalStageTokens = map[string]bool{"approval": true, "approve": true, "approved": true, "gate": true} + finalizationStageTokens = map[string]bool{"finalize": true, "finalization": true, "merge": true, "cleanup": true, "publish": true} + reviewStageTokens = map[string]bool{"review": true, "reviewer": true, "scorecard": true, "persona": true, "personas": true, "audit": true, "repro": true, "baseline": true, "investigation": true, "classify": true, "classification": true} + implementationStageTokens = map[string]bool{"implement": true, "implementation": true, "patch": true, "fixes": true, "work": true, "design": true} + intakeStageTokens = map[string]bool{"intake": true, "bootstrap": true, "context": true, "router": true, "request": true, "preflight": true, "setup": true, "rebase": true} +) + +// stepIDPhase classifies a single gc.step_id into a generic RunPhase. +// Port of TS stepIdPhase. +func stepIDPhase(stepID string) string { + tokens := tokenizeStepID(stepID) + if hasStageToken(tokens, approvalStageTokens, true) { + return "approval" + } + if hasStageToken(tokens, finalizationStageTokens, true) { + return "finalization" + } + if hasStageToken(tokens, reviewStageTokens, false) { + return "review" + } + if hasStageToken(tokens, implementationStageTokens, false) { + return "implementation" + } + if hasStageToken(tokens, intakeStageTokens, false) { + return "intake" + } + return "active" +} + +func hasStageToken(tokens []string, stageTokens map[string]bool, rejectWithLeadUpQualifier bool) bool { + found := false + for _, t := range tokens { + if stageTokens[t] { + found = true + break + } + } + if !found { + return false + } + if rejectWithLeadUpQualifier { + for _, t := range tokens { + if leadUpQualifierTokens[t] { + return false + } + } + } + return true +} + +// fallbackPhase is the keyword fallback used only when no step carries a +// gc.step_id. Port of TS fallbackPhase. +func fallbackPhase(issues []runIssue) phaseMapping { + if stepSignalContainsAny(issues, []string{"approval", "approved", "finalize-scope"}) { + return phaseMapping{phase: "approval", label: "approval"} + } + if stepSignalContainsAny(issues, []string{"post-merge", "finalization", "finalize"}) { + return phaseMapping{phase: "finalization", label: "finalization"} + } + + round, hasRound := reviewRoundForIssues(issues) + if hasRound || stepSignalContainsAny(issues, []string{"review", "reviewer", "scorecard"}) { + resolved := round + if !hasRound { + resolved = fallbackReviewRound(issues) + } + return phaseMapping{ + phase: "review", + label: "review round " + strconv.Itoa(resolved), + reviewRound: resolved, + hasRound: true, + } + } + + if stepSignalContainsAny(issues, []string{"implementation", "patch", "do-work"}) { + return phaseMapping{phase: "implementation", label: "implementation"} + } + if stepSignalContainsAny(issues, []string{"intake", "load-context", "router", "request"}) { + return phaseMapping{phase: "intake", label: "intake"} + } + return phaseMapping{phase: "active", label: "active"} +} + +// stepSignalText is the step-identity text for fallback scanning: title plus any +// gc.step_id. Port of TS stepSignalText. +func stepSignalText(issue runIssue) string { + stepID := stringValue(issue.metadata[beadmeta.StepIDMetadataKey]) + parts := make([]string, 0, 2) + if issue.title != "" { + parts = append(parts, issue.title) + } + if stepID != "" { + parts = append(parts, stepID) + } + return strings.ToLower(strings.Join(parts, " ")) +} + +func stepSignalContainsAny(issues []runIssue, needles []string) bool { + for _, i := range issues { + text := stepSignalText(i) + for _, n := range needles { + if strings.Contains(text, n) { + return true + } + } + } + return false +} + +var ( + roundInKey = regexp.MustCompile(`(?:^|\.)(?:iteration|attempt)\.(\d+)$`) + roundInValue = regexp.MustCompile(`(?:^|\.)(?:iteration|attempt)\.(\d+)$`) + roundKeyNoDigit = regexp.MustCompile(`(?:^|\.)(?:iteration|attempt)$`) +) + +// reviewRoundForIssue returns the per-issue review round when one is encoded in +// metadata. Port of TS reviewRoundForIssue (three supported shapes). The bool +// mirrors TS's `null`. Go map iteration is unordered, but the TS loop returns on +// the first match in Object.entries order; to stay deterministic we iterate keys +// in sorted order and return the first match (only one round shape exists per +// bead in practice, so order does not change the value). +func reviewRoundForIssue(issue runIssue) (int, bool) { + keys := make([]string, 0, len(issue.metadata)) + for k := range issue.metadata { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + value := issue.metadata[key] + if m := roundInKey.FindStringSubmatch(key); m != nil { + if n, err := strconv.Atoi(m[1]); err == nil { + return n, true + } + } + if roundKeyNoDigit.MatchString(key) { + if attempt, ok := parsePositiveInteger(value); ok { + return attempt, true + } + } + if m := roundInValue.FindStringSubmatch(value); m != nil { + if n, err := strconv.Atoi(m[1]); err == nil { + return n, true + } + } + } + return 0, false +} + +// reviewRoundForIssues returns the max per-issue review round. Port of TS +// reviewRoundForIssues. +func reviewRoundForIssues(issues []runIssue) (int, bool) { + best := 0 + found := false + for _, i := range issues { + if r, ok := reviewRoundForIssue(i); ok { + if !found || r > best { + best = r + } + found = true + } + } + return best, found +} + +// fallbackReviewRound counts issues whose text mentions "review", min 1. +// Port of TS fallbackReviewRound. +func fallbackReviewRound(issues []runIssue) int { + count := 0 + for _, i := range issues { + if strings.Contains(textForIssue(i), "review") { + count++ + } + } + if count < 1 { + return 1 + } + return count +} + +// textForIssue concatenates issue fields for keyword scanning, skipping gc.var.* +// keys. Port of TS textForIssue. +func textForIssue(issue runIssue) string { + metaKeys := make([]string, 0, len(issue.metadata)) + for k := range issue.metadata { + if strings.HasPrefix(k, "gc.var.") { + continue + } + metaKeys = append(metaKeys, k) + } + sort.Strings(metaKeys) + var metaParts []string + for _, k := range metaKeys { + metaParts = append(metaParts, k+" "+issue.metadata[k]) + } + metadataText := strings.Join(metaParts, " ") + + parts := []string{ + issue.title, + issue.desc, + issue.status, + issue.issueType, + issue.assignee, + issue.parent, + metadataText, + } + + out := make([]string, 0, len(parts)) + for _, p := range parts { + if p != "" { + out = append(out, p) + } + } + return strings.ToLower(strings.Join(out, " ")) +} + +// stringValue trims a string-typed metadata value; non-strings become "". +// Port of TS stringValue. (Go metadata is map[string]string, so a missing key +// yields "" naturally.) It delegates to nonEmpty so the JS-faithful trim +// (String.prototype.trim(): BOM stripped, NEL kept) is uniform across the +// package's metadata helpers rather than diverging on Go's unicode.IsSpace. +func stringValue(value string) string { + return nonEmpty(value) +} + +func parsePositiveInteger(value string) (int, bool) { + parsed, err := strconv.Atoi(strings.TrimSpace(value)) + if err != nil { + return 0, false + } + if parsed > 0 { + return parsed, true + } + return 0, false +} + +// runStages is the generic 5-stage ladder. Port of TS runStages. +var runStages = [][2]string{ + {"intake", "Intake"}, + {"implementation", "Implementation"}, + {"review", "Review"}, + {"approval", "Approval"}, + {"finalization", "Finalization"}, +} + +// stageProgress derives the lane stage ladder. Port of TS stageProgress. +func stageProgress(phase phaseMapping, formula string, hasFormula bool, issues []runIssue) []RunStage { + formulaStages := stagesForFormula(formula, hasFormula) + if len(formulaStages) > 0 { + return formulaStageProgress(formulaStages, issues) + } + + if phase.phase == "blocked" { + return []RunStage{{Key: "blocked", Label: "Blocked", Status: "blocked"}} + } + + if phase.phase == "complete" { + out := make([]RunStage, len(runStages)) + for i, s := range runStages { + out[i] = RunStage{Key: s[0], Label: s[1], Status: "complete"} + } + return out + } + + activeIndex := -1 + for i, s := range runStages { + if s[0] == phase.phase { + activeIndex = i + break + } + } + + if activeIndex < 0 { + out := make([]RunStage, len(runStages)) + for i, s := range runStages { + status := "pending" + if s[0] == "implementation" { + status = "active" + } + out[i] = RunStage{Key: s[0], Label: s[1], Status: status} + } + return out + } + + out := make([]RunStage, len(runStages)) + for idx, s := range runStages { + label := s[1] + if s[0] == "review" && phase.hasRound { + label = "Review round " + strconv.Itoa(phase.reviewRound) + } + var status string + switch { + case idx < activeIndex: + status = "complete" + case idx == activeIndex: + status = "active" + default: + status = "pending" + } + out[idx] = RunStage{Key: s[0], Label: label, Status: status} + } + return out +} + +// formulaStage is a per-formula stage with its constituent step ids. +type formulaStage struct { + key string + label string + steps []string +} + +// stagesForFormula returns the per-formula stage tables. Port of TS +// stagesForFormula. hasFormula mirrors TS's `formula: string | null`. +func stagesForFormula(formula string, hasFormula bool) []formulaStage { + if !hasFormula { + return nil + } + switch formula { + case "mol-adopt-pr-v2": + return []formulaStage{ + {"preflight", "Preflight", []string{"preflight"}}, + {"rebase", "Worktree / rebase", []string{"rebase-check"}}, + {"review", "Review loop", []string{ + "review-loop", + "review-pipeline.review-claude", + "review-pipeline.review-codex", + "review-pipeline.review-gemini", + "review-pipeline.synthesize", + "review-pipeline.quality-scorecard", + "apply-fixes", + }}, + {"ci", "Pre-approval CI", []string{"pre-approval-ci", "repair-ci-failures"}}, + {"approval", "Human approval", []string{"human-approval"}}, + {"finalize", "Merge-ready", []string{"finalize"}}, + {"cleanup", "Cleanup", []string{"cleanup-worktree"}}, + } + case "mol-design-review-v2": + return []formulaStage{ + {"setup", "Setup", []string{"design-review.setup"}}, + {"personas", "Personas", []string{ + "design-review.persona-gen-claude", + "design-review.persona-gen-codex", + "design-review.persona-gen-gemini", + "design-review.persona-synthesis", + }}, + {"fanout", "Persona fanout", []string{ + "design-review.prepare-review-items", + "design-review.persona-review-fanout", + }}, + {"synthesis", "Synthesis", []string{"design-review.global-synthesis"}}, + {"apply", "Apply findings", []string{"design-review.apply-design-changes"}}, + {"finalize", "Finalize", []string{"finalize"}}, + } + case "mol-bug-report-flow-v2": + return []formulaStage{ + {"intake", "Intake", []string{"bootstrap-run", "refresh-intake"}}, + {"repro", "Reproduction", []string{"historical-baseline", "reported-build-repro", "main-repro"}}, + {"audit", "Audit", []string{"code-path-audit", "coverage-audit", "related-refs-audit"}}, + {"classify", "Classify", []string{"investigation-synthesis", "followup-evidence", "normalize-outcome"}}, + {"approval", "Human approval", []string{"approve-classification", "verify-classification-approval"}}, + {"publish", "Publish", []string{"publish-classification"}}, + {"dispatch", "Dispatch fix", []string{"dispatch-implementation"}}, + } + case "mol-bug-report-implementation-v2": + return []formulaStage{ + {"plan", "Plan approval", []string{"approve-fix-plan", "approve-test-hardening-plan", "verify-selected-plan-approval"}}, + {"design", "Design review", []string{"prepare-design-review-doc", "design-review"}}, + {"implement", "Implement", []string{"implement-change", "prepare-review-context"}}, + {"review", "Code review", []string{"code-review-loop", "apply-code-fixes"}}, + {"pr", "Open PR", []string{"approve-pr-open", "verify-pr-open-approval", "open-or-update-pr"}}, + {"ci", "CI", []string{"wait-for-ci"}}, + {"merge", "Merge", []string{"approve-merge", "verify-merge-approval", "merge-and-finalize"}}, + } + } + return nil +} + +// formulaStageProgress maps formula stages to RunStage statuses. +// Port of TS formulaStageProgress. +func formulaStageProgress(stages []formulaStage, issues []runIssue) []RunStage { + primary := primaryStepIssues(issues) + activeIndex := formulaActiveStageIndex(stages, primary) + furthestClosedIndex := furthestClosedStageIndex(stages, primary) + + out := make([]RunStage, len(stages)) + for idx, stage := range stages { + out[idx] = RunStage{ + Key: stage.key, + Label: stage.label, + Status: formulaStageStatus(idx, activeIndex, furthestClosedIndex, stage, primary), + } + } + return out +} + +// primaryStepIssues keeps only the primary-step issues, mirroring the +// isPrimaryStepIssue filter formulaStageProgress applies before stage mapping. +func primaryStepIssues(issues []runIssue) []runIssue { + var primary []runIssue + for _, i := range issues { + if isPrimaryStepIssue(i) { + primary = append(primary, i) + } + } + return primary +} + +// formulaActiveStageIndex resolves the active stage index: the stage carrying +// the latest in-progress primary step, else the first open stage (-1 when none). +func formulaActiveStageIndex(stages []formulaStage, primary []runIssue) int { + var inProgress []runIssue + for _, i := range primary { + if i.status == "in_progress" { + inProgress = append(inProgress, i) + } + } + activeStepID, hasActiveStep := latestStepID(inProgress) + if !hasActiveStep { + return firstOpenStageIndex(stages, primary) + } + for idx, s := range stages { + if containsString(s.steps, activeStepID) { + return idx + } + } + return -1 +} + +// formulaStageStatus resolves one stage's status relative to the active and +// furthest-closed stage indices. Port of the TS status switch. +func formulaStageStatus(idx, activeIndex, furthestClosedIndex int, stage formulaStage, primary []runIssue) string { + switch { + case activeIndex >= 0 && idx < activeIndex: + return "complete" + case activeIndex >= 0 && idx == activeIndex: + return "active" + case activeIndex >= 0: + return "pending" + case stageHasClosedStep(stage, primary) || idx < furthestClosedIndex: + return "complete" + default: + return "pending" + } +} + +// stageHasClosedStep reports whether any of the stage's steps has a closed +// primary issue. +func stageHasClosedStep(stage formulaStage, primary []runIssue) bool { + for _, step := range stage.steps { + for _, i := range stepIssues(primary, step) { + if i.status == "closed" { + return true + } + } + } + return false +} + +func firstOpenStageIndex(stages []formulaStage, issues []runIssue) int { + for idx, s := range stages { + for _, step := range s.steps { + for _, i := range stepIssues(issues, step) { + if i.status != "closed" { + return idx + } + } + } + } + return -1 +} + +func furthestClosedStageIndex(stages []formulaStage, issues []runIssue) int { + furthest := -1 + for idx, s := range stages { + for _, step := range s.steps { + closed := false + for _, i := range stepIssues(issues, step) { + if i.status == "closed" { + closed = true + break + } + } + if closed { + furthest = idx + break + } + } + } + return furthest +} + +// latestStepID returns the gc.step_id of the most-recent-then-furthest issue. +// Port of TS latestStepId. The bool mirrors TS's `null`. +func latestStepID(issues []runIssue) (string, bool) { + sorted := make([]runIssue, len(issues)) + copy(sorted, issues) + sort.SliceStable(sorted, func(i, j int) bool { + return byMostRecentThenStage(sorted[i], sorted[j]) < 0 + }) + for _, i := range sorted { + if s := stringValue(i.metadata[beadmeta.StepIDMetadataKey]); s != "" { + return s, true + } + } + return "", false +} + +// furthestStageStepID picks the step whose stage is furthest along the ladder. +// Port of TS furthestStageStepId. The bool mirrors TS's `null`. +func furthestStageStepID(issues []runIssue) (string, bool) { + var stepIDs []string + for _, i := range issues { + if id := stringValue(i.metadata[beadmeta.StepIDMetadataKey]); id != "" { + stepIDs = append(stepIDs, id) + } + } + if len(stepIDs) == 0 { + return "", false + } + sorted := make([]string, len(stepIDs)) + copy(sorted, stepIDs) + sort.SliceStable(sorted, func(i, j int) bool { + rankDelta := stageRank(stepIDPhase(sorted[j])) - stageRank(stepIDPhase(sorted[i])) + if rankDelta != 0 { + return rankDelta < 0 + } + return sorted[i] < sorted[j] + }) + return sorted[0], true +} + +// lifecycleRank is the lifecycle rank (higher = further along). Port of TS +// LIFECYCLE_RANK. +var lifecycleRank = map[string]int{ + "active": 0, + "intake": 1, + "implementation": 2, + "review": 3, + "approval": 4, + "finalization": 5, + "blocked": 6, + "complete": 7, +} + +func stageRank(phase string) int { + return lifecycleRank[phase] +} + +// byMostRecentThenStage is the deterministic step ordering comparator. +// Port of TS byMostRecentThenStage. Returns <0 if a sorts before b. +func byMostRecentThenStage(a, b runIssue) int { + timeDelta := parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt) + if timeDelta != 0 { + if timeDelta < 0 { + return -1 + } + return 1 + } + aStep := stringValue(a.metadata[beadmeta.StepIDMetadataKey]) + bStep := stringValue(b.metadata[beadmeta.StepIDMetadataKey]) + rankDelta := stageRank(stepIDPhase(bStep)) - stageRank(stepIDPhase(aStep)) + if rankDelta != 0 { + return rankDelta + } + if aStep < bStep { + return -1 + } + if aStep > bStep { + return 1 + } + return 0 +} + +// stepIssues returns the issues whose gc.step_id equals step. Port of TS +// stepIssues. +func stepIssues(issues []runIssue, step string) []runIssue { + var out []runIssue + for _, i := range issues { + if stringValue(i.metadata[beadmeta.StepIDMetadataKey]) == step { + out = append(out, i) + } + } + return out +} + +// isPrimaryStepIssue excludes spec / scope-check / workflow-finalize kinds. +// Port of TS isPrimaryStepIssue. +func isPrimaryStepIssue(issue runIssue) bool { + kind := stringValue(issue.metadata[beadmeta.KindMetadataKey]) + return kind != "spec" && kind != "scope-check" && kind != "workflow-finalize" +} + +func containsString(haystack []string, needle string) bool { + for _, h := range haystack { + if h == needle { + return true + } + } + return false +} diff --git a/internal/runproj/projector.go b/internal/runproj/projector.go new file mode 100644 index 0000000000..9a9c86027f --- /dev/null +++ b/internal/runproj/projector.go @@ -0,0 +1,102 @@ +package runproj + +import ( + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" +) + +// Projector folds bead lifecycle events into the latest snapshot per id while +// preserving first-seen (creation) order. BuildRunSummary groups by first-seen +// order (mirroring the JS Map insertion order of the dashboard's listBeads +// read), so a plain Fold map — whose Go iteration order is random — would make a +// live run view flicker between requests. The per-city tailer drives a Projector +// instead: a cold ColdLoad over the full log, then incremental Apply of newly +// tailed events, and Beads() hands BuildRunSummary a deterministic slice. +// +// A Projector is not safe for concurrent use; the tailer mutates it from its +// single loop goroutine and publishes the built summary under its own lock. +type Projector struct { + beads map[string]beads.Bead + order []string + lastSeq uint64 +} + +// NewProjector returns an empty projector. +func NewProjector() *Projector { + return &Projector{beads: make(map[string]beads.Bead)} +} + +// ColdLoad folds the entire event log at path into the projector. It reads via +// events.ReadFilteredWithInFlight so the replay spans rotated .gz archives AND +// any in-flight events.jsonl.rotating-* file the recorder has not yet gzipped — +// otherwise a cold start that lands in a rotation's compression window would +// miss those pre-rotation events until the next rotation's catch-up. Safe to +// call once on a fresh projector before the incremental tail begins; Apply is +// seq-idempotent so the transient .gz/rotating overlap folds cleanly. +func (p *Projector) ColdLoad(path string) error { + evts, err := events.ReadFilteredWithInFlight(path, events.Filter{}) + if err != nil { + return err + } + p.Apply(evts) + return nil +} + +// Apply folds a chronological event slice, upserting bead.created/updated/closed +// snapshots and removing bead.deleted ones, preserving first-seen order for new +// ids. It advances the cursor past every event (bead or not) and reports whether +// any bead snapshot changed, so the caller can skip a rebuild on a no-op tick. +func (p *Projector) Apply(evts []events.Event) (changed bool) { + for i := range evts { + e := &evts[i] + if e.Seq > p.lastSeq { + p.lastSeq = e.Seq + } + if !beadEventTypes[e.Type] { + continue + } + b, ok := decodeBead(e.Payload) + if !ok { + continue + } + if e.Type == events.BeadDeleted { + if _, exists := p.beads[b.ID]; exists { + delete(p.beads, b.ID) + p.removeOrder(b.ID) + changed = true + } + continue + } + if _, exists := p.beads[b.ID]; !exists { + p.order = append(p.order, b.ID) + } + p.beads[b.ID] = b + changed = true + } + return changed +} + +// Beads returns the folded beads in first-seen order — the deterministic input +// BuildRunSummary expects. +func (p *Projector) Beads() []beads.Bead { + out := make([]beads.Bead, 0, len(p.order)) + for _, id := range p.order { + if b, ok := p.beads[id]; ok { + out = append(out, b) + } + } + return out +} + +// LastSeq returns the highest event seq applied — the cursor a live tail resumes +// from. +func (p *Projector) LastSeq() uint64 { return p.lastSeq } + +func (p *Projector) removeOrder(id string) { + for i, oid := range p.order { + if oid == id { + p.order = append(p.order[:i], p.order[i+1:]...) + return + } + } +} diff --git a/internal/runproj/projector_test.go b/internal/runproj/projector_test.go new file mode 100644 index 0000000000..42040435ba --- /dev/null +++ b/internal/runproj/projector_test.go @@ -0,0 +1,92 @@ +package runproj + +import ( + "testing" + + "github.com/gastownhall/gascity/internal/beads" + "github.com/gastownhall/gascity/internal/events" +) + +func TestProjectorPreservesFirstSeenOrder(t *testing.T) { + p := NewProjector() + changed := p.Apply([]events.Event{ + beadEvent(1, events.BeadCreated, "b", "open"), + beadEvent(2, events.BeadCreated, "a", "open"), + beadEvent(3, events.BeadCreated, "c", "open"), + // An update to an existing bead must NOT reorder it. + beadEvent(4, events.BeadUpdated, "b", "in_progress"), + }) + if !changed { + t.Fatal("Apply reported no change for bead events") + } + + got := idsOf(p.Beads()) + want := []string{"b", "a", "c"} + if !equalIDs(got, want) { + t.Errorf("order = %v, want %v (first-seen order, stable across updates)", got, want) + } + if p.LastSeq() != 4 { + t.Errorf("lastSeq = %d, want 4", p.LastSeq()) + } +} + +func TestProjectorIncrementalApplyAndDelete(t *testing.T) { + p := NewProjector() + p.Apply([]events.Event{ + beadEvent(1, events.BeadCreated, "a", "open"), + beadEvent(2, events.BeadCreated, "b", "open"), + }) + + // Incremental tail: a new bead appends at the end; a delete drops its slot. + changed := p.Apply([]events.Event{ + beadEvent(3, events.BeadCreated, "c", "open"), + beadEvent(4, events.BeadDeleted, "a", "open"), + }) + if !changed { + t.Fatal("Apply reported no change") + } + + got := idsOf(p.Beads()) + want := []string{"b", "c"} + if !equalIDs(got, want) { + t.Errorf("order after delete = %v, want %v", got, want) + } + if p.LastSeq() != 4 { + t.Errorf("lastSeq = %d, want 4", p.LastSeq()) + } +} + +func TestProjectorNoOpTickReportsUnchanged(t *testing.T) { + p := NewProjector() + p.Apply([]events.Event{beadEvent(1, events.BeadCreated, "a", "open")}) + + // A tick carrying only non-bead events advances the cursor but changes no + // bead, so the tailer can skip the rebuild. + changed := p.Apply([]events.Event{{Seq: 7, Type: events.SessionWoke, Subject: "w"}}) + if changed { + t.Error("non-bead event should not report a change") + } + if p.LastSeq() != 7 { + t.Errorf("lastSeq = %d, want 7 (cursor advances past ignored events)", p.LastSeq()) + } +} + +func idsOf(bl []beads.Bead) []string { + out := make([]string, len(bl)) + for i, b := range bl { + out[i] = b.ID + } + return out +} + +func equalIDs(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/internal/runproj/scope.go b/internal/runproj/scope.go new file mode 100644 index 0000000000..ce08cdf846 --- /dev/null +++ b/internal/runproj/scope.go @@ -0,0 +1,82 @@ +package runproj + +import ( + "regexp" + "strings" + + "github.com/gastownhall/gascity/internal/beadmeta" +) + +// scopeRefRe validates a scope ref. Port of TS SCOPE_REF_RE +// (shared/src/run-detail.ts). +var scopeRefRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$`) + +// runScopeWithStoreRef is the resolved scope. Port of TS RunScopeWithStoreRef. +type runScopeWithStoreRef struct { + scopeKind string + scopeRef string + rootStoreRef string +} + +// parseRunScopeKind accepts only "city" or "rig". Port of TS parseRunScopeKind. +func parseRunScopeKind(value string) (string, bool) { + if value == "city" || value == "rig" { + return value, true + } + return "", false +} + +// fromRootMetadataScope resolves the lane scope from root metadata, with the +// gc.root_store_ref fallback. Port of TS fromRootMetadataScope. The bool mirrors +// TS's `null`. +func fromRootMetadataScope(metadata map[string]string) (runScopeWithStoreRef, bool) { + rootStoreRef := stringValueOrEmpty(metadata[beadmeta.RootStoreRefMetadataKey]) + + // Primary: explicit gc.scope_kind / gc.scope_ref pair. + scopeKind, kindOK := parseRunScopeKind(metadata[beadmeta.ScopeKindMetadataKey]) + scopeRef := stringValueOrEmpty(metadata[beadmeta.ScopeRefMetadataKey]) + if kindOK && scopeRef != "" && scopeRefRe.MatchString(scopeRef) { + rsr := rootStoreRef + if rsr == "" { + rsr = scopeKind + ":" + scopeRef + } + return runScopeWithStoreRef{scopeKind: scopeKind, scopeRef: scopeRef, rootStoreRef: rsr}, true + } + + // Fallback (gascity-dashboard-km0w): recover scope from gc.root_store_ref. + if rootStoreRef == "" { + return runScopeWithStoreRef{}, false + } + parsedKind, parsedRef, ok := fromStoreRef(rootStoreRef) + if !ok || !scopeRefRe.MatchString(parsedRef) { + return runScopeWithStoreRef{}, false + } + return runScopeWithStoreRef{scopeKind: parsedKind, scopeRef: parsedRef, rootStoreRef: rootStoreRef}, true +} + +// fromStoreRef parses a ":" store ref. Port of TS fromStoreRef. +func fromStoreRef(rootStoreRef string) (kind, ref string, ok bool) { + value := stringValueOrEmpty(rootStoreRef) + if value == "" { + return "", "", false + } + colon := strings.IndexByte(value, ':') + if colon <= 0 || colon >= len(value)-1 { + return "", "", false + } + parsedKind, kindOK := parseRunScopeKind(value[:colon]) + parsedRef := stringValueOrEmpty(value[colon+1:]) + if !kindOK || parsedRef == "" { + return "", "", false + } + return parsedKind, parsedRef, true +} + +// stringValueOrEmpty trims a value; an all-whitespace or empty value becomes "". +// Mirrors the TS run-scope stringValue (which returns null for empty). It +// delegates to nonEmpty so the JS-faithful trim (String.prototype.trim(): BOM +// stripped, NEL kept) is uniform with the rest of the package rather than +// diverging on Go's unicode.IsSpace. +func stringValueOrEmpty(value string) string { + return nonEmpty(value) +} diff --git a/internal/runproj/session.go b/internal/runproj/session.go new file mode 100644 index 0000000000..9308a7e050 --- /dev/null +++ b/internal/runproj/session.go @@ -0,0 +1,101 @@ +package runproj + +import "strings" + +// DashboardSession is the dashboard-owned session projection that the run-health +// enrich layer joins lanes against. Port of the TypeScript DashboardSession in +// internal/api/dashboardspa/web/shared/src/dashboard-sessions.ts. Optional TS +// fields are modeled as pointers so an absent field (TS undefined) is +// distinguishable from an empty value — only Alias, Pool, LastActive, and +// Activity affect resolution/health, but the full shape is carried so the P2 +// endpoint can unmarshal a /v0 sessions read directly. +type DashboardSession struct { + ID string `json:"id"` + Template string `json:"template"` + SessionName string `json:"session_name"` + Title string `json:"title"` + Alias *string `json:"alias,omitempty"` + State string `json:"state"` + Reason *string `json:"reason,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + CreatedAt string `json:"created_at"` + LastActive *string `json:"last_active,omitempty"` + Attached bool `json:"attached"` + Rig *string `json:"rig,omitempty"` + Pool *string `json:"pool,omitempty"` + AgentKind *string `json:"agent_kind,omitempty"` + Running bool `json:"running"` + Model *string `json:"model,omitempty"` + ContextPct *float64 `json:"context_pct,omitempty"` + ContextWindow *int `json:"context_window,omitempty"` + Activity *string `json:"activity,omitempty"` + Provider string `json:"provider"` +} + +// resolveSessionForTarget resolves a role/assignee/target label to the concrete +// session that carries it, or (zero, false) when none match. Port of TS +// resolveSessionForTarget: active sessions outrank non-active; within a tier, +// first match wins (deterministic given gc's recency-sorted iteration order). +func resolveSessionForTarget(target string, sessions []DashboardSession) (DashboardSession, bool) { + if target == "" || len(sessions) == 0 { + return DashboardSession{}, false + } + active := make([]DashboardSession, 0, len(sessions)) + for _, s := range sessions { + if s.State == "active" { + active = append(active, s) + } + } + if s, ok := matchFirst(target, active); ok { + return s, true + } + return matchFirst(target, sessions) +} + +func matchFirst(target string, sessions []DashboardSession) (DashboardSession, bool) { + for _, s := range sessions { + if matchesSessionTarget(s, target) { + return s, true + } + } + return DashboardSession{}, false +} + +// matchesSessionTarget reports whether session carries target in any of the four +// documented positions: exact alias, exact pool, last-segment of alias (split on +// '/' '.'), or last-segment of session_name (split on '__' '--'). Port of TS +// matchesSessionTarget. +func matchesSessionTarget(session DashboardSession, target string) bool { + if session.Alias != nil && *session.Alias == target { + return true + } + if session.Pool != nil && *session.Pool == target { + return true + } + if session.Alias != nil && lastSegment(*session.Alias, []string{"/", "."}) == target { + return true + } + if lastSegment(session.SessionName, []string{"__", "--"}) == target { + return true + } + return false +} + +// lastSegment returns the substring after the last occurrence of any separator +// in seps (whole-token match for multi-char separators), or value unchanged when +// no separator is present. Port of TS lastSegment. +func lastSegment(value string, seps []string) string { + cut := -1 + sepLen := 0 + for _, sep := range seps { + idx := strings.LastIndex(value, sep) + if idx > cut { + cut = idx + sepLen = len(sep) + } + } + if cut < 0 { + return value + } + return value[cut+sepLen:] +} diff --git a/internal/runproj/strip.go b/internal/runproj/strip.go new file mode 100644 index 0000000000..667fc9fd62 --- /dev/null +++ b/internal/runproj/strip.go @@ -0,0 +1,28 @@ +package runproj + +import "regexp" + +// stripNonPrintable removes ANSI escapes (OSC + CSI), C0/DEL/C1 control bytes, +// and Unicode bidi/RTL controls from operator-influenced strings. Faithful port +// of TS stripNonPrintable (shared/src/strip-non-printable.ts). This is the +// single sanitisation choke point for scope refs that enter the DTO +// (gascity-dashboard-5e5v). +var ( + // OSC: ESC ] ... terminated by BEL or ESC \ ; the inner class excludes ESC. + oscRe = regexp.MustCompile("\x1b\\][^\x07\x1b]*(?:\x07|\x1b\\\\)") + // CSI: ESC [ params final-letter. + csiRe = regexp.MustCompile("\x1b\\[[?0-9;]*[a-zA-Z]") + // All control chars: C0 (<0x20, incl. tab/newline/CR), DEL, C1 (0x80-0x9f). + ctrlRe = regexp.MustCompile(`[\x{00}-\x{1f}\x{7f}-\x{9f}]`) + // The 12 Unicode bidi/RTL control codepoints (CVE-2021-42574): + // U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069. + bidiRe = regexp.MustCompile(`[\x{061c}\x{200e}\x{200f}\x{202a}-\x{202e}\x{2066}-\x{2069}]`) +) + +func stripNonPrintable(value string) string { + value = oscRe.ReplaceAllString(value, "") + value = csiRe.ReplaceAllString(value, "") + value = ctrlRe.ReplaceAllString(value, "") + value = bidiRe.ReplaceAllString(value, "") + return value +} diff --git a/internal/runproj/summary.go b/internal/runproj/summary.go new file mode 100644 index 0000000000..feaae75a69 --- /dev/null +++ b/internal/runproj/summary.go @@ -0,0 +1,736 @@ +package runproj + +import ( + "regexp" + "sort" + "strings" + "time" + + "github.com/gastownhall/gascity/internal/beadmeta" + "github.com/gastownhall/gascity/internal/beads" +) + +// recentChangesCap caps the recentChanges list. Port of TS RECENT_CHANGES_CAP. +const recentChangesCap = 12 + +// maxHistoricalLanes caps the historical lanes carried on the wire (the true +// count still surfaces via TotalHistorical). Port of TS MAX_HISTORICAL_LANES. +const maxHistoricalLanes = 50 + +// engineeringTypes is the run-bead-filter allowlist. Port of TS +// ENGINEERING_TYPES. (Used by RunBeadFilter; the summary builder itself does not +// pre-filter — it groups whatever it is given, exactly like buildRunSummary.) +var engineeringTypes = map[string]bool{ + "feature": true, "bug": true, "task": true, "epic": true, + "chore": true, "decision": true, "molecule": true, +} + +// BuildRunSummary projects a set of bead snapshots into the dashboard run-view +// RunSummary DTO. It is a faithful Go port of the TypeScript buildRunSummary +// (internal/api/dashboardspa/web/shared/src/runs/summary.ts) for the +// BEAD-DERIVED result: lane health and the city census are the builder's +// pre-enrich defaults (status "unavailable"); session enrichment is Phase 2. +// +// The input is the latest bead snapshot per id (e.g. the output of Fold). Each +// bead is mapped to the phase classifier's RunIssue shape with the same field +// mapping fromDashboardBead uses (Type→issue_type, ParentID→parent, a zero +// UpdatedAt falling back to CreatedAt). +// +// partial=false and an empty feed-scope map reproduce the golden fixture; the +// optional variadic params mirror the TS signature for downstream callers. +func BuildRunSummary(beadList []beads.Bead, opts ...BuildOption) RunSummary { + cfg := buildConfig{feedScopes: map[string]RunFeedScope{}} + for _, o := range opts { + o(&cfg) + } + + issues := make([]runIssue, len(beadList)) + for i, b := range beadList { + issues[i] = fromBead(b) + } + + // Group by run-root id, preserving first-seen order (mirrors JS Map order). + groups := map[string][]runIssue{} + var order []string + for _, issue := range issues { + rootID := runRootID(issue) + if _, ok := groups[rootID]; !ok { + order = append(order, rootID) + } + groups[rootID] = append(groups[rootID], issue) + } + + // Keep only real run groups (drop dangling roots and non-run groups). + var runRootIDs []string + var laneIssues []runIssue + for _, rootID := range order { + groupIssues := groups[rootID] + if isDanglingRootGroup(rootID, groupIssues) || !isRunGroup(rootID, groupIssues) { + continue + } + runRootIDs = append(runRootIDs, rootID) + laneIssues = append(laneIssues, groupIssues...) + } + + sortedLanes := make([]RunLane, 0, len(runRootIDs)) + for _, rootID := range runRootIDs { + sortedLanes = append(sortedLanes, runLane(rootID, groups[rootID], cfg.feedScopes)) + } + sort.SliceStable(sortedLanes, func(i, j int) bool { + return compareLanes(sortedLanes[i], sortedLanes[j]) < 0 + }) + + // gascity-dashboard-4xcv: blocked lanes are split out of Active. + activeLanes := make([]RunLane, 0) + completedLanes := make([]RunLane, 0) + blockedLanes := make([]RunLane, 0) + for _, lane := range sortedLanes { + switch lane.Phase { + case "complete": + completedLanes = append(completedLanes, lane) + case "blocked": + blockedLanes = append(blockedLanes, lane) + default: + activeLanes = append(activeLanes, lane) + } + } + + totalHistorical := len(completedLanes) + historicalLanes := completedLanes + if len(historicalLanes) > maxHistoricalLanes { + historicalLanes = historicalLanes[:maxHistoricalLanes] + } + + summary := RunSummary{ + TotalActive: len(activeLanes), + TotalHistorical: totalHistorical, + RunCounts: runCounts(activeLanes, len(activeLanes), len(blockedLanes)), + Lanes: activeLanes, + HistoricalLanes: historicalLanes, + BlockedLanes: blockedLanes, + RecentChanges: recentChanges(laneIssues), + Census: runCensusUnavailable(), + } + if cfg.partial { + summary.LanesPartial = true + } + return summary +} + +// RunFeedScope mirrors the TS RunFeedScope (feed-scope fallback entry). +type RunFeedScope struct { + ScopeKind string + ScopeRef string + RootStoreRef string +} + +type buildConfig struct { + feedScopes map[string]RunFeedScope + partial bool +} + +// BuildOption configures BuildRunSummary. The defaults (empty feed-scope map, +// partial=false) reproduce the golden fixture. +type BuildOption func(*buildConfig) + +// WithFeedScopes supplies the feed-scope fallback map (TS feedScopes arg). +func WithFeedScopes(m map[string]RunFeedScope) BuildOption { + return func(c *buildConfig) { + if m != nil { + c.feedScopes = m + } + } +} + +// WithPartial sets the partial flag (TS partial arg) — emits lanesPartial=true. +func WithPartial(partial bool) BuildOption { + return func(c *buildConfig) { c.partial = partial } +} + +// isRunGroup reports whether a group is a run (root carries a run marker). +// Port of TS isRunGroup. +func isRunGroup(rootID string, issues []runIssue) bool { + root, ok := findIssue(issues, rootID) + if !ok { + return false + } + md := root.metadata + return stringValue(md[beadmeta.FormulaContractMetadataKey]) == "graph.v2" || + root.issueType == "molecule" || + stringValue(md[beadmeta.KindMetadataKey]) == "run" || + stringValue(md[beadmeta.FormulaMetadataKey]) != "" +} + +// runCounts tallies lanes per kind. Port of TS runCounts. +func runCounts(lanes []RunLane, visible, blocked int) RunCounts { + counts := RunCounts{ + Total: len(lanes), + Visible: visible, + Blocked: blocked, + } + for _, lane := range lanes { + switch runKind(lane.Formula) { + case "prReview": + counts.PrReview++ + case "designReview": + counts.DesignReview++ + case "bugfix": + counts.Bugfix++ + case "other": + counts.Other++ + } + } + return counts +} + +// runKind classifies a lane's formula into a count bucket. Port of TS runKind. +func runKind(formula RunLaneFormula) string { + name, ok := runFormulaName(formula) + if !ok { + return "other" + } + switch name { + case "mol-adopt-pr-v2": + return "prReview" + case "mol-design-review-v2": + return "designReview" + case "mol-bug-report-flow-v2", "mol-bug-report-implementation-v2": + return "bugfix" + } + return "other" +} + +// runLane builds a single lane. Port of TS runLane. +func runLane(rootID string, issues []runIssue, feedScopes map[string]RunFeedScope) RunLane { + phase := mapRunPhase(issues) + updatedAt := latestUpdatedAt(issues) + formula := runFormula(rootID, issues) + formulaName, hasFormula := runFormulaName(formula) + stages := stageProgress(phase, formulaName, hasFormula, issues) + + foundStageIndex := -1 + for i, s := range stages { + if s.Status == "active" { + foundStageIndex = i + break + } + } + + var primaryInProgress []runIssue + for _, i := range issues { + if isPrimaryStepIssue(i) && i.status == "in_progress" { + primaryInProgress = append(primaryInProgress, i) + } + } + activeStepID, hasActiveStep := latestStepID(primaryInProgress) + progress := runProgress(stages, foundStageIndex, activeStepID, hasActiveStep, issues) + + formulaStages := stagesForFormula(formulaName, hasFormula) + formulaStageResolved := false + if len(formulaStages) > 0 && progress.Status == "active_step" { + for _, st := range formulaStages { + if containsString(st.steps, progress.StepID) { + formulaStageResolved = true + break + } + } + } + + phaseLabel := phase.label + if formula.Status == "known" && foundStageIndex >= 0 { + // activeStage?.label ?? phase.label + phaseLabel = stages[foundStageIndex].Label + } + + return RunLane{ + ID: rootID, + Title: displayTitle(rootID, issues), + Formula: formula, + Scope: runScope(rootID, issues, feedScopes), + External: externalReference(issues), + Phase: phase.phase, + PhaseLabel: phaseLabel, + StatusCounts: statusCounts(issues), + ActiveAssignees: activeAssignees(issues), + UpdatedAt: updatedAt, + Stages: stages, + Progress: progress, + FormulaStageResolved: formulaStageResolved, + Health: runHealthUnavailable(), + } +} + +// runRootID resolves the run-root id for a bead. Port of TS runRootId. +func runRootID(issue runIssue) string { + if sourceRoot := sourceRunRootID(issue); sourceRoot != "" { + return sourceRoot + } + md := issue.metadata + if explicit := stringValue(md[beadmeta.RootBeadIDMetadataKey]); explicit != "" { + return explicit + } + if stringValue(md[beadmeta.KindMetadataKey]) == "run" || issue.issueType == "molecule" { + return issue.id + } + if moleculeID := stringValue(md["molecule_id"]); moleculeID != "" { + return moleculeID + } + return issue.id +} + +func sourceRunRootID(issue runIssue) string { + md := issue.metadata + keys := []string{ + "pr_review.run_root_id", + "pr_review.workflow_root_id", + "bugflow.active_run_id", + "bugflow.implementation_run_id", + "bugflow.implementation_workflow_id", + "design_review.run_root_id", + "design_review.workflow_root_id", + } + for _, k := range keys { + if v := stringValue(md[k]); v != "" { + return v + } + } + return "" +} + +// runScope resolves a lane's scope from root metadata, then feed scopes. +// Port of TS runScope. +func runScope(rootID string, issues []runIssue, feedScopes map[string]RunFeedScope) RunLaneScope { + root, hasRoot := findIssue(issues, rootID) + + // ordered = root first, then the rest (root's metadata wins ties). TS + // excludes the root by identity (issue !== root); bead ids are unique within + // a group, so id inequality is the faithful predicate. + var ordered []runIssue + if hasRoot { + ordered = append(ordered, root) + for _, i := range issues { + if i.id != root.id { + ordered = append(ordered, i) + } + } + } else { + ordered = issues + } + + rootStoreRef := metadataString(ordered, beadmeta.RootStoreRefMetadataKey) + + // Build the metadata map fromRootMetadataScope consumes: root metadata, + // then overlay gc.root_store_ref and the resolved gc.scope_ref. + scopeMeta := map[string]string{} + if hasRoot { + for k, v := range root.metadata { + scopeMeta[k] = v + } + } + if rootStoreRef != "" { + scopeMeta[beadmeta.RootStoreRefMetadataKey] = rootStoreRef + } + scopeRef := "" + if hasRoot { + scopeRef = stringValue(root.metadata[beadmeta.ScopeRefMetadataKey]) + } + if scopeRef == "" { + scopeRef = metadataString(ordered, beadmeta.ScopeRefMetadataKey) + } + scopeMeta[beadmeta.ScopeRefMetadataKey] = scopeRef + + if ms, ok := fromRootMetadataScope(scopeMeta); ok { + return availableScope(ms.scopeKind, ms.scopeRef, ms.rootStoreRef) + } + + if feedScope, ok := feedScopes[rootID]; ok { + rsr := rootStoreRef + if rsr == "" { + rsr = feedScope.RootStoreRef + } + return availableScope(feedScope.ScopeKind, feedScope.ScopeRef, rsr) + } + + return RunLaneScope{Status: "unavailable", Error: "run scope metadata unavailable"} +} + +// availableScope is the single edge that sanitizes scope refs (gascity-dashboard-5e5v). +// Port of TS availableScope. +func availableScope(kind, ref, rootStoreRef string) RunLaneScope { + return RunLaneScope{ + Status: "available", + Kind: kind, + Ref: stripNonPrintable(ref), + RootStoreRef: stripNonPrintable(rootStoreRef), + } +} + +// runFormula resolves a lane's formula identity. Port of TS runFormula. +func runFormula(rootID string, issues []runIssue) RunLaneFormula { + root, hasRoot := findIssue(issues, rootID) + var rootPtr *runIssue + if hasRoot { + rootPtr = &root + } + name, ok := resolveRunFormulaIdentityLane(rootPtr, issues) + if ok { + return RunLaneFormula{Status: "known", Name: name} + } + return RunLaneFormula{Status: "unavailable", Error: "run formula unavailable"} +} + +func runFormulaName(formula RunLaneFormula) (string, bool) { + if formula.Status == "known" { + return formula.Name, true + } + return "", false +} + +// displayTitle resolves a lane's display title. Port of TS displayTitle. +func displayTitle(rootID string, issues []runIssue) string { + prTitle := metadataString(issues, "pr_review.github_title") + prNumber := metadataString(issues, "pr_review.pr_number") + if prTitle != "" && prNumber != "" { + return "PR #" + prNumber + ": " + prTitle + } + + issueURL := metadataString(issues, "bugflow.github_issue_url") + issueNumber := metadataString(issues, "bugflow.github_issue_number") + if issueURL != "" && issueNumber != "" { + first := "" + if len(issues) > 0 { + first = issues[0].title + } + if first == "" { + first = rootID + } + return "Issue #" + issueNumber + ": " + first + } + + if root, ok := findIssue(issues, rootID); ok && root.title != "" { + return root.title + } + if len(issues) > 0 && issues[0].title != "" { + return issues[0].title + } + return rootID +} + +// statusCounts tallies issue statuses, preserving first-seen status order to +// match the TS Record insertion order. Port of TS statusCounts. +func statusCounts(issues []runIssue) StatusCounts { + var counts StatusCounts + for _, i := range issues { + counts.inc(i.status) + } + return counts +} + +// activeAssignees returns the sorted unique non-closed assignees. +// Port of TS activeAssignees. +func activeAssignees(issues []runIssue) []string { + seen := map[string]bool{} + var out []string + for _, i := range issues { + if i.status == "closed" { + continue + } + a := nonEmpty(i.assignee) + if a == "" || seen[a] { + continue + } + seen[a] = true + out = append(out, a) + } + sort.Strings(out) + if out == nil { + return []string{} + } + return out +} + +// latestUpdatedAt returns the most-recent updated_at as the union value. +// Port of TS latestUpdatedAt. +func latestUpdatedAt(issues []runIssue) RunLaneUpdatedAt { + best := "" + bestMS := int64(0) + found := false + for _, i := range issues { + if i.updatedAt == "" { + continue + } + ms := parseTimestamp(i.updatedAt) + if !found || ms > bestMS { + best = i.updatedAt + bestMS = ms + found = true + } + } + if !found { + return RunLaneUpdatedAt{Status: "unavailable", Error: "run update time unavailable"} + } + return RunLaneUpdatedAt{Status: "available", At: best} +} + +// recentChanges returns the newest-first capped recent-change list. +// Port of TS recentChanges (stable sort preserves input order on ties). +func recentChanges(issues []runIssue) []RunChange { + var filtered []runIssue + for _, i := range issues { + if i.updatedAt != "" { + filtered = append(filtered, i) + } + } + sort.SliceStable(filtered, func(a, b int) bool { + return parseTimestamp(filtered[b].updatedAt) < parseTimestamp(filtered[a].updatedAt) + }) + if len(filtered) > recentChangesCap { + filtered = filtered[:recentChangesCap] + } + out := make([]RunChange, 0, len(filtered)) + for _, i := range filtered { + out = append(out, RunChange{ + ID: i.id, + Title: i.title, + Status: i.status, + UpdatedAt: i.updatedAt, + }) + } + return out +} + +// compareLanes orders lanes newest-first, then by id. Port of TS compareLanes. +// Returns <0 if a sorts before b. +func compareLanes(a, b RunLane) int { + aTime := int64(0) + if a.UpdatedAt.Status == "available" { + aTime = parseTimestamp(a.UpdatedAt.At) + } + bTime := int64(0) + if b.UpdatedAt.Status == "available" { + bTime = parseTimestamp(b.UpdatedAt.At) + } + if delta := bTime - aTime; delta != 0 { + if delta < 0 { + return -1 + } + return 1 + } + return strings.Compare(a.ID, b.ID) +} + +// externalReference resolves the external PR/issue reference. Port of TS +// externalReference. +func externalReference(issues []runIssue) RunLaneExternalReference { + label, hasLabel := externalLabel(issues) + url, hasURL := externalURL(issues) + if hasLabel && hasURL { + return RunLaneExternalReference{Status: "available", Label: label, URL: url} + } + if hasLabel { + return RunLaneExternalReference{Status: "label_only", Label: label} + } + return RunLaneExternalReference{Status: "unavailable", Error: "external reference unavailable"} +} + +var httpURLRe = regexp.MustCompile(`(?i)^https?://`) + +func externalURL(issues []runIssue) (string, bool) { + raw := metadataString(issues, "pr_review.pr_url") + if raw == "" { + raw = metadataString(issues, "bugflow.github_issue_url") + } + if raw != "" && httpURLRe.MatchString(raw) { + return raw, true + } + return "", false +} + +func externalLabel(issues []runIssue) (string, bool) { + if prNumber := metadataString(issues, "pr_review.pr_number"); prNumber != "" { + return "PR #" + prNumber, true + } + if issueNumber := metadataString(issues, "bugflow.github_issue_number"); issueNumber != "" { + return "Issue #" + issueNumber, true + } + if ref := metadataString(issues, "pr_review.external_ref"); ref != "" { + return ref, true + } + if ref := metadataString(issues, "bugflow.external_ref"); ref != "" { + return ref, true + } + return "", false +} + +// metadataString returns the first non-empty metadata value for key across +// issues. Port of TS metadataString. +func metadataString(issues []runIssue, key string) string { + for _, i := range issues { + if v := stringValue(i.metadata[key]); v != "" { + return v + } + } + return "" +} + +func runCensusUnavailable() RunCensusState { + return RunCensusState{Status: "unavailable", Error: "run health has not been derived"} +} + +func runHealthUnavailable() RunLaneHealthState { + return RunLaneHealthState{Status: "unavailable", Error: "run health has not been derived"} +} + +// RunBeadFilter reports whether a bead participates in run classification. +// Port of TS runBeadFilter. (Exposed for parity; the builder does not call it — +// live callers apply it at the projection boundary via FilterRunBeads.) +func RunBeadFilter(b beads.Bead) bool { + for _, l := range b.Labels { + if strings.HasPrefix(l, "gc:") { + return false + } + } + if engineeringTypes[b.Type] { + return true + } + return stringValue(b.Metadata[beadmeta.KindMetadataKey]) == "run" +} + +// FilterRunBeads returns the subset of beadList that participates in run +// classification, per RunBeadFilter. It is the projection-boundary analog of the +// frontend runBeadFilter (summary.ts): BuildRunSummary and BuildRunDetail +// are faithful ports of buildRunSummary/buildRunDetail, which receive +// already-filtered beads, so a live caller that folds the raw event log — which +// also carries message, session, and gc:-labeled control beads that can share a +// run root — must apply this before building. Dropping those unrelated beads +// keeps them from distorting lane status, counts, recent changes, and detail +// nodes. +func FilterRunBeads(beadList []beads.Bead) []beads.Bead { + out := make([]beads.Bead, 0, len(beadList)) + for _, b := range beadList { + if RunBeadFilter(b) { + out = append(out, b) + } + } + return out +} + +// runProgress resolves the lane's progress union. Port of TS runProgress. +func runProgress(stages []RunStage, activeStageIndex int, activeStepID string, hasActiveStep bool, issues []runIssue) RunLaneProgress { + stage := runStagePosition(stages, activeStageIndex) + if hasActiveStep { + return RunLaneProgress{ + Status: "active_step", + StepID: activeStepID, + Stage: stage, + Attempt: runStepAttempt(issues, activeStepID), + } + } + if stage.Status == "available" { + return RunLaneProgress{Status: "stage_only", Stage: stage, Error: "active run step unavailable"} + } + return RunLaneProgress{Status: "unavailable", Error: "run progress unavailable"} +} + +// runStagePosition resolves the active-stage position union. +// Port of TS runStagePosition. +func runStagePosition(stages []RunStage, activeStageIndex int) RunLaneStagePosition { + if activeStageIndex < 0 || activeStageIndex >= len(stages) { + return RunLaneStagePosition{Status: "unavailable", Error: "active run stage unavailable"} + } + stage := stages[activeStageIndex] + return RunLaneStagePosition{ + Status: "available", + Index: activeStageIndex, + Key: stage.Key, + Label: stage.Label, + } +} + +// runStepAttempt resolves the step-attempt union from the active step's review +// round. Port of TS runStepAttempt. +func runStepAttempt(issues []runIssue, stepID string) RunLaneStepAttempt { + value, ok := reviewRoundForIssues(stepIssues(issues, stepID)) + if !ok { + return RunLaneStepAttempt{Status: "unavailable", Error: "run step attempt unavailable"} + } + return RunLaneStepAttempt{Status: "available", Value: value} +} + +// fromBead maps a beads.Bead to the phase classifier's runIssue, mirroring the +// TS fromDashboardBead adapter (phaseMapping.ts:369). The verified field +// mapping: Type→issue_type, ParentID→parent (falling back to the legacy +// gc.parent_bead_id marker), a zero UpdatedAt falling back to CreatedAt. +func fromBead(b beads.Bead) runIssue { + parent := b.ParentID + if parent == "" { + parent = stringValue(b.Metadata[beadmeta.ParentBeadIDMetadataKey]) + } + + updatedAt := b.UpdatedAt + if updatedAt.IsZero() { + updatedAt = b.CreatedAt + } + + issue := runIssue{ + id: b.ID, + title: b.Title, + desc: b.Description, + status: b.Status, + issueType: b.Type, + assignee: b.Assignee, + updatedAt: formatTimestamp(updatedAt), + parent: parent, + } + if len(b.Metadata) > 0 { + issue.metadata = b.Metadata + } + return issue +} + +// formatTimestamp renders a time.Time the way the bead JSON wire carries it, so +// the projected updated_at string is byte-identical to the TS input. The fixture +// uses UTC RFC3339 with a "Z" suffix; time.Time.Format(time.RFC3339) on a UTC +// value produces exactly that. +func formatTimestamp(t time.Time) string { + if t.IsZero() { + return "" + } + return t.UTC().Format(time.RFC3339) +} + +// parseTimestamp parses an ISO timestamp to Unix milliseconds, returning 0 on +// failure (mirroring Date.parse → NaN → 0 in the TS comparators). +func parseTimestamp(value string) int64 { + if value == "" { + return 0 + } + t, err := time.Parse(time.RFC3339, value) + if err != nil { + t, err = time.Parse(time.RFC3339Nano, value) + if err != nil { + return 0 + } + } + return t.UnixMilli() +} + +func findIssue(issues []runIssue, id string) (runIssue, bool) { + for _, i := range issues { + if i.id == id { + return i, true + } + } + return runIssue{}, false +} + +// isDanglingRootGroup reports whether the group's root bead is absent. +// Port of TS isDanglingRootGroup. +func isDanglingRootGroup(rootID string, issues []runIssue) bool { + for _, i := range issues { + if i.id == rootID { + return false + } + } + return true +} diff --git a/internal/runproj/summary_golden_test.go b/internal/runproj/summary_golden_test.go new file mode 100644 index 0000000000..b971c3c994 --- /dev/null +++ b/internal/runproj/summary_golden_test.go @@ -0,0 +1,102 @@ +package runproj + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gastownhall/gascity/internal/beads" +) + +// TestBuildRunSummaryGolden pins the Go port of buildRunSummary to the +// TypeScript oracle: it loads the shared bead fixture, builds the summary, and +// asserts the canonical JSON matches runsummary_golden.json byte-for-byte. The +// golden was generated with JSON.stringify(value, null, 2) plus a trailing +// newline, so the canonicalization here mirrors that exactly (HTML escaping off, +// two-space indent, trailing newline). +func TestBuildRunSummaryGolden(t *testing.T) { + fixturePath := filepath.Join("testdata", "beads_fixture.json") + goldenPath := filepath.Join("testdata", "runsummary_golden.json") + + raw, err := os.ReadFile(fixturePath) + if err != nil { + t.Fatalf("read fixture: %v", err) + } + var beadList []beads.Bead + if err := json.Unmarshal(raw, &beadList); err != nil { + t.Fatalf("unmarshal fixture: %v", err) + } + + summary := BuildRunSummary(beadList) + got, err := canonicalJSON(summary) + if err != nil { + t.Fatalf("marshal summary: %v", err) + } + + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("read golden: %v", err) + } + + if !bytes.Equal(got, want) { + t.Errorf("run summary does not match golden:\n%s", unifiedDiff(string(want), string(got))) + } +} + +// canonicalJSON marshals v the way the TS golden generator did: JSON.stringify +// with two-space indent, HTML escaping disabled (JSON.stringify does not escape +// <, >, & or U+2028/U+2029 the way Go's default encoder does), and a single +// trailing newline. +func canonicalJSON(v any) ([]byte, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(v); err != nil { + return nil, err + } + // json.Encoder.Encode already appends a single trailing newline, matching the + // generator's `JSON.stringify(...) + "\n"`. + return buf.Bytes(), nil +} + +// unifiedDiff renders a minimal line-oriented diff of want vs got so a golden +// mismatch points straight at the divergent lines. +func unifiedDiff(want, got string) string { + wantLines := splitLines(want) + gotLines := splitLines(got) + var b bytes.Buffer + n := len(wantLines) + if len(gotLines) > n { + n = len(gotLines) + } + for i := 0; i < n; i++ { + var w, g string + if i < len(wantLines) { + w = wantLines[i] + } + if i < len(gotLines) { + g = gotLines[i] + } + if w == g { + continue + } + if i < len(wantLines) { + b.WriteString("- " + w + "\n") + } + if i < len(gotLines) { + b.WriteString("+ " + g + "\n") + } + } + if b.Len() == 0 { + return "(no line-level differences; check trailing bytes)" + } + return b.String() +} + +func splitLines(s string) []string { + return strings.Split(s, "\n") +} diff --git a/internal/runproj/testdata/beads_fixture.json b/internal/runproj/testdata/beads_fixture.json new file mode 100644 index 0000000000..4beed85167 --- /dev/null +++ b/internal/runproj/testdata/beads_fixture.json @@ -0,0 +1,248 @@ +[ + { + "id": "dt-adopt1", + "title": "mol-adopt-pr-v2", + "status": "open", + "issue_type": "molecule", + "priority": 2, + "created_at": "2026-06-01T10:00:00Z", + "updated_at": "2026-06-01T12:30:00Z", + "assignee": "polecat-1", + "ref": "mol-adopt-pr-v2", + "metadata": { + "gc.formula_contract": "graph.v2", + "gc.kind": "run", + "gc.formula": "mol-adopt-pr-v2", + "gc.run_target": "rig:gascity-packs", + "gc.root_store_ref": "rig:gascity-packs", + "gc.scope_kind": "rig", + "gc.scope_ref": "gascity-packs", + "pr_review.pr_number": "42", + "pr_review.pr_url": "https://github.com/gastownhall/gascity/pull/42", + "pr_review.github_title": "Add run-view Go projection" + } + }, + { + "id": "dt-adopt1.1", + "title": "Preflight checks", + "status": "closed", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-01T10:01:00Z", + "updated_at": "2026-06-01T10:05:00Z", + "parent": "dt-adopt1", + "ref": "mol-adopt-pr-v2.preflight", + "metadata": { + "gc.kind": "step", + "gc.root_bead_id": "dt-adopt1", + "gc.step_id": "preflight", + "gc.step_ref": "mol-adopt-pr-v2.preflight", + "gc.scope_ref": "gascity-packs" + } + }, + { + "id": "dt-adopt1.2", + "title": "Worktree rebase check", + "status": "closed", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-01T10:06:00Z", + "updated_at": "2026-06-01T10:20:00Z", + "parent": "dt-adopt1", + "ref": "mol-adopt-pr-v2.rebase-check", + "metadata": { + "gc.kind": "step", + "gc.root_bead_id": "dt-adopt1", + "gc.step_id": "rebase-check", + "gc.step_ref": "mol-adopt-pr-v2.rebase-check", + "gc.scope_ref": "gascity-packs" + } + }, + { + "id": "dt-adopt1.3", + "title": "Review loop", + "status": "in_progress", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-01T10:21:00Z", + "updated_at": "2026-06-01T12:30:00Z", + "assignee": "polecat-1", + "parent": "dt-adopt1", + "ref": "mol-adopt-pr-v2.review-loop", + "metadata": { + "gc.kind": "step", + "gc.root_bead_id": "dt-adopt1", + "gc.step_id": "review-loop", + "gc.step_ref": "mol-adopt-pr-v2.review-loop", + "gc.scope_ref": "gascity-packs", + "review.iteration.2": "in_progress" + } + }, + { + "id": "dt-adopt1.3a", + "title": "Claude review", + "status": "closed", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-01T10:22:00Z", + "updated_at": "2026-06-01T11:00:00Z", + "parent": "dt-adopt1", + "ref": "mol-adopt-pr-v2.review-pipeline.review-claude", + "metadata": { + "gc.kind": "step", + "gc.root_bead_id": "dt-adopt1", + "gc.step_id": "review-pipeline.review-claude", + "gc.step_ref": "mol-adopt-pr-v2.review-pipeline.review-claude", + "gc.logical_bead_id": "dt-adopt1.3a", + "gc.scope_ref": "gascity-packs" + } + }, + { + "id": "dt-adopt1.fin", + "title": "Merge and finalize", + "status": "open", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-01T10:00:30Z", + "updated_at": "2026-06-01T10:00:30Z", + "parent": "dt-adopt1", + "ref": "mol-adopt-pr-v2.finalize", + "metadata": { + "gc.kind": "run-finalize", + "gc.root_bead_id": "dt-adopt1", + "gc.step_id": "finalize", + "gc.step_ref": "mol-adopt-pr-v2.finalize", + "gc.scope_ref": "gascity-packs" + } + }, + { + "id": "dt-bugdone", + "title": "mol-bug-report-implementation-v2", + "status": "closed", + "issue_type": "molecule", + "priority": 2, + "created_at": "2026-05-20T09:00:00Z", + "updated_at": "2026-05-20T15:45:00Z", + "ref": "mol-bug-report-implementation-v2", + "metadata": { + "gc.formula_contract": "graph.v2", + "gc.kind": "run", + "gc.formula": "mol-bug-report-implementation-v2", + "gc.run_target": "rig:demo-app", + "gc.root_store_ref": "rig:demo-app", + "gc.scope_kind": "rig", + "gc.scope_ref": "demo-app", + "bugflow.github_issue_url": "https://github.com/gastownhall/gascity/issues/17", + "bugflow.github_issue_number": "17" + } + }, + { + "id": "dt-bugdone.1", + "title": "Implement change", + "status": "closed", + "issue_type": "task", + "priority": 2, + "created_at": "2026-05-20T09:05:00Z", + "updated_at": "2026-05-20T11:00:00Z", + "parent": "dt-bugdone", + "ref": "mol-bug-report-implementation-v2.implement-change", + "metadata": { + "gc.kind": "step", + "gc.root_bead_id": "dt-bugdone", + "gc.step_id": "implement-change", + "gc.step_ref": "mol-bug-report-implementation-v2.implement-change", + "gc.scope_ref": "demo-app" + } + }, + { + "id": "dt-bugdone.2", + "title": "Merge and finalize", + "status": "closed", + "issue_type": "task", + "priority": 2, + "created_at": "2026-05-20T15:30:00Z", + "updated_at": "2026-05-20T15:45:00Z", + "parent": "dt-bugdone", + "ref": "mol-bug-report-implementation-v2.merge-and-finalize", + "metadata": { + "gc.kind": "step", + "gc.root_bead_id": "dt-bugdone", + "gc.step_id": "merge-and-finalize", + "gc.step_ref": "mol-bug-report-implementation-v2.merge-and-finalize", + "gc.scope_ref": "demo-app" + } + }, + { + "id": "dt-latch1", + "title": "mol-focus-review", + "status": "blocked", + "issue_type": "molecule", + "priority": 1, + "created_at": "2026-04-01T00:00:00Z", + "updated_at": "2026-04-01T00:00:00Z", + "description": "Focus + in-session review formula. Approval gate, review.", + "ref": "mol-focus-review", + "metadata": { + "gc.formula_contract": "graph.v2", + "gc.kind": "workflow", + "gc.formula": "mol-focus-review", + "gc.root_store_ref": "rig:gascity-dashboard", + "gc.scope_kind": "rig", + "gc.scope_ref": "gascity-dashboard" + } + }, + { + "id": "dt-wisp1", + "title": "mol-do-work", + "status": "open", + "issue_type": "molecule", + "priority": 2, + "created_at": "2026-06-02T00:00:00Z", + "updated_at": "2026-06-02T00:00:00Z", + "ref": "mol-do-work", + "ephemeral": true, + "metadata": { + "gc.var.target": "demo-app", + "gc.var.prompt": "review the blocked PR and merge it after approval, then finalize" + } + }, + { + "id": "dt-wisp1.1", + "title": "Implementation work", + "status": "in_progress", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-02T00:05:00Z", + "updated_at": "2026-06-02T00:05:00Z", + "assignee": "ralph-3", + "ephemeral": true, + "metadata": { + "molecule_id": "dt-wisp1", + "gc.step_id": "do-work" + } + }, + { + "id": "dt-1920.step-1", + "title": "Orphan implementation patch", + "status": "in_progress", + "issue_type": "task", + "priority": 2, + "created_at": "2026-06-01T00:05:00Z", + "updated_at": "2026-06-01T00:05:00Z", + "metadata": { + "gc.kind": "step", + "gc.formula_contract": "graph.v2", + "gc.root_bead_id": "dt-1920", + "gc.step_id": "implementation.patch" + } + }, + { + "id": "dt-lone", + "title": "Fix a typo in the README", + "status": "open", + "issue_type": "task", + "priority": 3, + "created_at": "2026-06-03T00:00:00Z", + "updated_at": "2026-06-03T00:00:00Z" + } +] diff --git a/internal/runproj/testdata/rundetail_golden.json b/internal/runproj/testdata/rundetail_golden.json new file mode 100644 index 0000000000..ef9e61cc25 --- /dev/null +++ b/internal/runproj/testdata/rundetail_golden.json @@ -0,0 +1,359 @@ +{ + "runId": "dt-adopt1", + "rootBeadId": "dt-adopt1", + "rootStoreRef": "rig:gascity-packs", + "resolvedRootStore": "rig:gascity-packs", + "scopeKind": "rig", + "scopeRef": "gascity-packs", + "title": "mol-adopt-pr-v2", + "formula": { + "kind": "known", + "name": "mol-adopt-pr-v2", + "source": "metadata" + }, + "formulaDetail": { + "kind": "unavailable", + "reason": "fetch_failed", + "name": "mol-adopt-pr-v2", + "target": "rig:gascity-packs", + "failure": "upstream_error" + }, + "executionPath": { + "kind": "unavailable", + "reason": "missing_cwd_and_rig_root" + }, + "snapshotVersion": 1, + "snapshotEventSeq": { + "kind": "known", + "seq": 100 + }, + "completeness": { + "kind": "complete" + }, + "progress": { + "snapshotVersion": 1, + "snapshotEventSeq": { + "kind": "known", + "seq": 100 + }, + "snapshotPartial": false, + "totalNodeCount": 5, + "visibleNodeCount": 5, + "edgeCount": 4, + "executionInstanceCount": 5, + "sessionLinkCount": 0, + "streamableSessionCount": 0, + "streamableSessionIds": [], + "statusCounts": { + "ready": 1, + "completed": 3, + "active": 1 + }, + "allStatusCounts": { + "ready": 1, + "completed": 3, + "active": 1 + } + }, + "phase": "review", + "stages": [ + { + "key": "preflight", + "label": "Preflight", + "status": "complete" + }, + { + "key": "rebase", + "label": "Worktree / rebase", + "status": "complete" + }, + { + "key": "review", + "label": "Review loop", + "status": "active" + }, + { + "key": "ci", + "label": "Pre-approval CI", + "status": "pending" + }, + { + "key": "approval", + "label": "Human approval", + "status": "pending" + }, + { + "key": "finalize", + "label": "Merge-ready", + "status": "pending" + }, + { + "key": "cleanup", + "label": "Cleanup", + "status": "pending" + } + ], + "nodes": [ + { + "id": "dt-adopt1", + "semanticNodeId": "dt-adopt1", + "title": "mol-adopt-pr-v2", + "kind": "run", + "constructKind": "run-root", + "status": "ready", + "currentBeadId": "dt-adopt1", + "scope": { + "kind": "scoped", + "ref": "gascity-packs" + }, + "visibleInGraph": true, + "historicalOnly": false, + "iterationSummary": { + "kind": "single" + }, + "attemptSummary": { + "kind": "none" + }, + "visibleExecutionInstanceId": "dt-adopt1", + "executionInstances": [ + { + "id": "dt-adopt1", + "semanticNodeId": "dt-adopt1", + "beadId": "dt-adopt1", + "iteration": { + "kind": "base" + }, + "attempt": { + "kind": "untracked" + }, + "label": "base", + "status": "ready", + "session": { + "kind": "none", + "reason": "not_started" + }, + "currentIteration": true, + "historical": false + } + ], + "controlBadges": [ + { + "id": "dt-adopt1.fin", + "label": "finalize", + "status": "pending" + } + ] + }, + { + "id": "preflight", + "semanticNodeId": "preflight", + "title": "Preflight checks", + "kind": "step", + "constructKind": "step", + "status": "completed", + "currentBeadId": "dt-adopt1.1", + "scope": { + "kind": "scoped", + "ref": "gascity-packs" + }, + "visibleInGraph": true, + "historicalOnly": false, + "iterationSummary": { + "kind": "single" + }, + "attemptSummary": { + "kind": "none" + }, + "visibleExecutionInstanceId": "dt-adopt1.1", + "executionInstances": [ + { + "id": "dt-adopt1.1", + "semanticNodeId": "preflight", + "beadId": "dt-adopt1.1", + "iteration": { + "kind": "base" + }, + "attempt": { + "kind": "untracked" + }, + "label": "base", + "status": "completed", + "session": { + "kind": "none", + "reason": "session_unresolved" + }, + "currentIteration": true, + "historical": false + } + ], + "controlBadges": [] + }, + { + "id": "rebase-check", + "semanticNodeId": "rebase-check", + "title": "Worktree rebase check", + "kind": "step", + "constructKind": "step", + "status": "completed", + "currentBeadId": "dt-adopt1.2", + "scope": { + "kind": "scoped", + "ref": "gascity-packs" + }, + "visibleInGraph": true, + "historicalOnly": false, + "iterationSummary": { + "kind": "single" + }, + "attemptSummary": { + "kind": "none" + }, + "visibleExecutionInstanceId": "dt-adopt1.2", + "executionInstances": [ + { + "id": "dt-adopt1.2", + "semanticNodeId": "rebase-check", + "beadId": "dt-adopt1.2", + "iteration": { + "kind": "base" + }, + "attempt": { + "kind": "untracked" + }, + "label": "base", + "status": "completed", + "session": { + "kind": "none", + "reason": "session_unresolved" + }, + "currentIteration": true, + "historical": false + } + ], + "controlBadges": [] + }, + { + "id": "review-loop", + "semanticNodeId": "review-loop", + "title": "Review loop", + "kind": "step", + "constructKind": "step", + "status": "active", + "currentBeadId": "dt-adopt1.3", + "scope": { + "kind": "scoped", + "ref": "gascity-packs" + }, + "visibleInGraph": true, + "historicalOnly": false, + "iterationSummary": { + "kind": "single" + }, + "attemptSummary": { + "kind": "none" + }, + "visibleExecutionInstanceId": "dt-adopt1.3", + "executionInstances": [ + { + "id": "dt-adopt1.3", + "semanticNodeId": "review-loop", + "beadId": "dt-adopt1.3", + "iteration": { + "kind": "base" + }, + "attempt": { + "kind": "untracked" + }, + "label": "base", + "status": "active", + "session": { + "kind": "none", + "reason": "session_unresolved" + }, + "currentIteration": true, + "historical": false + } + ], + "controlBadges": [] + }, + { + "id": "dt-adopt1.3a", + "semanticNodeId": "dt-adopt1.3a", + "title": "Claude review", + "kind": "step", + "constructKind": "step", + "status": "completed", + "currentBeadId": "dt-adopt1.3a", + "scope": { + "kind": "scoped", + "ref": "gascity-packs" + }, + "visibleInGraph": true, + "historicalOnly": false, + "iterationSummary": { + "kind": "single" + }, + "attemptSummary": { + "kind": "none" + }, + "visibleExecutionInstanceId": "dt-adopt1.3a", + "executionInstances": [ + { + "id": "dt-adopt1.3a", + "semanticNodeId": "dt-adopt1.3a", + "beadId": "dt-adopt1.3a", + "iteration": { + "kind": "base" + }, + "attempt": { + "kind": "untracked" + }, + "label": "base", + "status": "completed", + "session": { + "kind": "none", + "reason": "session_unresolved" + }, + "currentIteration": true, + "historical": false + } + ], + "controlBadges": [] + } + ], + "edges": [ + { + "from": "dt-adopt1", + "to": "preflight", + "kind": "parent" + }, + { + "from": "dt-adopt1", + "to": "rebase-check", + "kind": "parent" + }, + { + "from": "dt-adopt1", + "to": "review-loop", + "kind": "parent" + }, + { + "from": "dt-adopt1", + "to": "dt-adopt1.3a", + "kind": "parent" + } + ], + "lanes": [ + { + "id": "gascity-packs", + "label": "gascity-packs", + "nodeIds": [ + "dt-adopt1", + "preflight", + "rebase-check", + "review-loop", + "dt-adopt1.3a" + ] + } + ] +} diff --git a/internal/runproj/testdata/runsummary_enriched_golden.json b/internal/runproj/testdata/runsummary_enriched_golden.json new file mode 100644 index 0000000000..3fd46add32 --- /dev/null +++ b/internal/runproj/testdata/runsummary_enriched_golden.json @@ -0,0 +1,453 @@ +{ + "totalActive": 2, + "totalHistorical": 1, + "runCounts": { + "total": 2, + "visible": 2, + "prReview": 1, + "designReview": 0, + "bugfix": 0, + "blocked": 1, + "other": 1 + }, + "lanes": [ + { + "id": "dt-wisp1", + "title": "mol-do-work", + "formula": { + "status": "unavailable", + "error": "run formula unavailable" + }, + "scope": { + "status": "unavailable", + "error": "run scope metadata unavailable" + }, + "external": { + "status": "unavailable", + "error": "external reference unavailable" + }, + "phase": "implementation", + "phaseLabel": "implementation", + "statusCounts": { + "open": 1, + "in_progress": 1 + }, + "activeAssignees": [ + "ralph-3" + ], + "updatedAt": { + "status": "available", + "at": "2026-06-02T00:05:00Z" + }, + "stages": [ + { + "key": "intake", + "label": "Intake", + "status": "complete" + }, + { + "key": "implementation", + "label": "Implementation", + "status": "active" + }, + { + "key": "review", + "label": "Review", + "status": "pending" + }, + { + "key": "approval", + "label": "Approval", + "status": "pending" + }, + { + "key": "finalization", + "label": "Finalization", + "status": "pending" + } + ], + "progress": { + "status": "active_step", + "stepId": "do-work", + "stage": { + "status": "available", + "index": 1, + "key": "implementation", + "label": "Implementation" + }, + "attempt": { + "status": "unavailable", + "error": "run step attempt unavailable" + } + }, + "formulaStageResolved": false, + "health": { + "status": "available", + "data": { + "phaseConfidence": "inferred", + "needsOperator": false, + "stuckNode": { + "status": "available", + "id": "do-work" + }, + "thrashingDetected": false, + "session": { + "status": "resolved", + "lastActive": { + "status": "available", + "at": "2026-06-02T00:04:45Z" + }, + "running": { + "status": "available", + "value": true + }, + "activity": { + "status": "available", + "value": "thinking" + } + } + } + } + }, + { + "id": "dt-adopt1", + "title": "PR #42: Add run-view Go projection", + "formula": { + "status": "known", + "name": "mol-adopt-pr-v2" + }, + "scope": { + "status": "available", + "kind": "rig", + "ref": "gascity-packs", + "rootStoreRef": "rig:gascity-packs" + }, + "external": { + "status": "available", + "label": "PR #42", + "url": "https://github.com/gastownhall/gascity/pull/42" + }, + "phase": "review", + "phaseLabel": "Review loop", + "statusCounts": { + "open": 2, + "closed": 3, + "in_progress": 1 + }, + "activeAssignees": [ + "polecat-1" + ], + "updatedAt": { + "status": "available", + "at": "2026-06-01T12:30:00Z" + }, + "stages": [ + { + "key": "preflight", + "label": "Preflight", + "status": "complete" + }, + { + "key": "rebase", + "label": "Worktree / rebase", + "status": "complete" + }, + { + "key": "review", + "label": "Review loop", + "status": "active" + }, + { + "key": "ci", + "label": "Pre-approval CI", + "status": "pending" + }, + { + "key": "approval", + "label": "Human approval", + "status": "pending" + }, + { + "key": "finalize", + "label": "Merge-ready", + "status": "pending" + }, + { + "key": "cleanup", + "label": "Cleanup", + "status": "pending" + } + ], + "progress": { + "status": "active_step", + "stepId": "review-loop", + "stage": { + "status": "available", + "index": 2, + "key": "review", + "label": "Review loop" + }, + "attempt": { + "status": "available", + "value": 2 + } + }, + "formulaStageResolved": true, + "health": { + "status": "available", + "data": { + "phaseConfidence": "known", + "needsOperator": false, + "stuckNode": { + "status": "available", + "id": "review-loop" + }, + "thrashingDetected": false, + "session": { + "status": "resolved", + "lastActive": { + "status": "available", + "at": "2026-06-01T12:29:30Z" + }, + "running": { + "status": "available", + "value": true + }, + "activity": { + "status": "available", + "value": "tool_use" + } + } + } + } + } + ], + "historicalLanes": [ + { + "id": "dt-bugdone", + "title": "Issue #17: mol-bug-report-implementation-v2", + "formula": { + "status": "known", + "name": "mol-bug-report-implementation-v2" + }, + "scope": { + "status": "available", + "kind": "rig", + "ref": "demo-app", + "rootStoreRef": "rig:demo-app" + }, + "external": { + "status": "available", + "label": "Issue #17", + "url": "https://github.com/gastownhall/gascity/issues/17" + }, + "phase": "complete", + "phaseLabel": "complete", + "statusCounts": { + "closed": 3 + }, + "activeAssignees": [], + "updatedAt": { + "status": "available", + "at": "2026-05-20T15:45:00Z" + }, + "stages": [ + { + "key": "plan", + "label": "Plan approval", + "status": "complete" + }, + { + "key": "design", + "label": "Design review", + "status": "complete" + }, + { + "key": "implement", + "label": "Implement", + "status": "complete" + }, + { + "key": "review", + "label": "Code review", + "status": "complete" + }, + { + "key": "pr", + "label": "Open PR", + "status": "complete" + }, + { + "key": "ci", + "label": "CI", + "status": "complete" + }, + { + "key": "merge", + "label": "Merge", + "status": "complete" + } + ], + "progress": { + "status": "unavailable", + "error": "run progress unavailable" + }, + "formulaStageResolved": false, + "health": { + "status": "unavailable", + "error": "run health has not been derived" + } + } + ], + "blockedLanes": [ + { + "id": "dt-latch1", + "title": "mol-focus-review", + "formula": { + "status": "known", + "name": "mol-focus-review" + }, + "scope": { + "status": "available", + "kind": "rig", + "ref": "gascity-dashboard", + "rootStoreRef": "rig:gascity-dashboard" + }, + "external": { + "status": "unavailable", + "error": "external reference unavailable" + }, + "phase": "blocked", + "phaseLabel": "blocked", + "statusCounts": { + "blocked": 1 + }, + "activeAssignees": [], + "updatedAt": { + "status": "available", + "at": "2026-04-01T00:00:00Z" + }, + "stages": [ + { + "key": "blocked", + "label": "Blocked", + "status": "blocked" + } + ], + "progress": { + "status": "unavailable", + "error": "run progress unavailable" + }, + "formulaStageResolved": false, + "health": { + "status": "available", + "data": { + "phaseConfidence": "inferred", + "needsOperator": true, + "stuckNode": { + "status": "unavailable", + "error": "active run step unavailable" + }, + "thrashingDetected": false, + "session": { + "status": "unresolved", + "error": "run session unresolved" + } + } + } + } + ], + "recentChanges": [ + { + "id": "dt-wisp1.1", + "title": "Implementation work", + "status": "in_progress", + "updatedAt": "2026-06-02T00:05:00Z" + }, + { + "id": "dt-wisp1", + "title": "mol-do-work", + "status": "open", + "updatedAt": "2026-06-02T00:00:00Z" + }, + { + "id": "dt-adopt1", + "title": "mol-adopt-pr-v2", + "status": "open", + "updatedAt": "2026-06-01T12:30:00Z" + }, + { + "id": "dt-adopt1.3", + "title": "Review loop", + "status": "in_progress", + "updatedAt": "2026-06-01T12:30:00Z" + }, + { + "id": "dt-adopt1.3a", + "title": "Claude review", + "status": "closed", + "updatedAt": "2026-06-01T11:00:00Z" + }, + { + "id": "dt-adopt1.2", + "title": "Worktree rebase check", + "status": "closed", + "updatedAt": "2026-06-01T10:20:00Z" + }, + { + "id": "dt-adopt1.1", + "title": "Preflight checks", + "status": "closed", + "updatedAt": "2026-06-01T10:05:00Z" + }, + { + "id": "dt-adopt1.fin", + "title": "Merge and finalize", + "status": "open", + "updatedAt": "2026-06-01T10:00:30Z" + }, + { + "id": "dt-bugdone", + "title": "mol-bug-report-implementation-v2", + "status": "closed", + "updatedAt": "2026-05-20T15:45:00Z" + }, + { + "id": "dt-bugdone.2", + "title": "Merge and finalize", + "status": "closed", + "updatedAt": "2026-05-20T15:45:00Z" + }, + { + "id": "dt-bugdone.1", + "title": "Implement change", + "status": "closed", + "updatedAt": "2026-05-20T11:00:00Z" + }, + { + "id": "dt-latch1", + "title": "mol-focus-review", + "status": "blocked", + "updatedAt": "2026-04-01T00:00:00Z" + } + ], + "census": { + "status": "available", + "data": { + "byPhase": { + "intake": 0, + "implementation": 1, + "review": 1, + "approval": 0, + "finalization": 0, + "blocked": 1, + "complete": 0, + "active": 0 + }, + "totalInFlight": 3, + "unverifiable": 2, + "knownDenominator": 1, + "thrashing": 0 + } + } +} diff --git a/internal/runproj/testdata/runsummary_golden.json b/internal/runproj/testdata/runsummary_golden.json new file mode 100644 index 0000000000..683312440b --- /dev/null +++ b/internal/runproj/testdata/runsummary_golden.json @@ -0,0 +1,380 @@ +{ + "totalActive": 2, + "totalHistorical": 1, + "runCounts": { + "total": 2, + "visible": 2, + "prReview": 1, + "designReview": 0, + "bugfix": 0, + "blocked": 1, + "other": 1 + }, + "lanes": [ + { + "id": "dt-wisp1", + "title": "mol-do-work", + "formula": { + "status": "unavailable", + "error": "run formula unavailable" + }, + "scope": { + "status": "unavailable", + "error": "run scope metadata unavailable" + }, + "external": { + "status": "unavailable", + "error": "external reference unavailable" + }, + "phase": "implementation", + "phaseLabel": "implementation", + "statusCounts": { + "open": 1, + "in_progress": 1 + }, + "activeAssignees": [ + "ralph-3" + ], + "updatedAt": { + "status": "available", + "at": "2026-06-02T00:05:00Z" + }, + "stages": [ + { + "key": "intake", + "label": "Intake", + "status": "complete" + }, + { + "key": "implementation", + "label": "Implementation", + "status": "active" + }, + { + "key": "review", + "label": "Review", + "status": "pending" + }, + { + "key": "approval", + "label": "Approval", + "status": "pending" + }, + { + "key": "finalization", + "label": "Finalization", + "status": "pending" + } + ], + "progress": { + "status": "active_step", + "stepId": "do-work", + "stage": { + "status": "available", + "index": 1, + "key": "implementation", + "label": "Implementation" + }, + "attempt": { + "status": "unavailable", + "error": "run step attempt unavailable" + } + }, + "formulaStageResolved": false, + "health": { + "status": "unavailable", + "error": "run health has not been derived" + } + }, + { + "id": "dt-adopt1", + "title": "PR #42: Add run-view Go projection", + "formula": { + "status": "known", + "name": "mol-adopt-pr-v2" + }, + "scope": { + "status": "available", + "kind": "rig", + "ref": "gascity-packs", + "rootStoreRef": "rig:gascity-packs" + }, + "external": { + "status": "available", + "label": "PR #42", + "url": "https://github.com/gastownhall/gascity/pull/42" + }, + "phase": "review", + "phaseLabel": "Review loop", + "statusCounts": { + "open": 2, + "closed": 3, + "in_progress": 1 + }, + "activeAssignees": [ + "polecat-1" + ], + "updatedAt": { + "status": "available", + "at": "2026-06-01T12:30:00Z" + }, + "stages": [ + { + "key": "preflight", + "label": "Preflight", + "status": "complete" + }, + { + "key": "rebase", + "label": "Worktree / rebase", + "status": "complete" + }, + { + "key": "review", + "label": "Review loop", + "status": "active" + }, + { + "key": "ci", + "label": "Pre-approval CI", + "status": "pending" + }, + { + "key": "approval", + "label": "Human approval", + "status": "pending" + }, + { + "key": "finalize", + "label": "Merge-ready", + "status": "pending" + }, + { + "key": "cleanup", + "label": "Cleanup", + "status": "pending" + } + ], + "progress": { + "status": "active_step", + "stepId": "review-loop", + "stage": { + "status": "available", + "index": 2, + "key": "review", + "label": "Review loop" + }, + "attempt": { + "status": "available", + "value": 2 + } + }, + "formulaStageResolved": true, + "health": { + "status": "unavailable", + "error": "run health has not been derived" + } + } + ], + "historicalLanes": [ + { + "id": "dt-bugdone", + "title": "Issue #17: mol-bug-report-implementation-v2", + "formula": { + "status": "known", + "name": "mol-bug-report-implementation-v2" + }, + "scope": { + "status": "available", + "kind": "rig", + "ref": "demo-app", + "rootStoreRef": "rig:demo-app" + }, + "external": { + "status": "available", + "label": "Issue #17", + "url": "https://github.com/gastownhall/gascity/issues/17" + }, + "phase": "complete", + "phaseLabel": "complete", + "statusCounts": { + "closed": 3 + }, + "activeAssignees": [], + "updatedAt": { + "status": "available", + "at": "2026-05-20T15:45:00Z" + }, + "stages": [ + { + "key": "plan", + "label": "Plan approval", + "status": "complete" + }, + { + "key": "design", + "label": "Design review", + "status": "complete" + }, + { + "key": "implement", + "label": "Implement", + "status": "complete" + }, + { + "key": "review", + "label": "Code review", + "status": "complete" + }, + { + "key": "pr", + "label": "Open PR", + "status": "complete" + }, + { + "key": "ci", + "label": "CI", + "status": "complete" + }, + { + "key": "merge", + "label": "Merge", + "status": "complete" + } + ], + "progress": { + "status": "unavailable", + "error": "run progress unavailable" + }, + "formulaStageResolved": false, + "health": { + "status": "unavailable", + "error": "run health has not been derived" + } + } + ], + "blockedLanes": [ + { + "id": "dt-latch1", + "title": "mol-focus-review", + "formula": { + "status": "known", + "name": "mol-focus-review" + }, + "scope": { + "status": "available", + "kind": "rig", + "ref": "gascity-dashboard", + "rootStoreRef": "rig:gascity-dashboard" + }, + "external": { + "status": "unavailable", + "error": "external reference unavailable" + }, + "phase": "blocked", + "phaseLabel": "blocked", + "statusCounts": { + "blocked": 1 + }, + "activeAssignees": [], + "updatedAt": { + "status": "available", + "at": "2026-04-01T00:00:00Z" + }, + "stages": [ + { + "key": "blocked", + "label": "Blocked", + "status": "blocked" + } + ], + "progress": { + "status": "unavailable", + "error": "run progress unavailable" + }, + "formulaStageResolved": false, + "health": { + "status": "unavailable", + "error": "run health has not been derived" + } + } + ], + "recentChanges": [ + { + "id": "dt-wisp1.1", + "title": "Implementation work", + "status": "in_progress", + "updatedAt": "2026-06-02T00:05:00Z" + }, + { + "id": "dt-wisp1", + "title": "mol-do-work", + "status": "open", + "updatedAt": "2026-06-02T00:00:00Z" + }, + { + "id": "dt-adopt1", + "title": "mol-adopt-pr-v2", + "status": "open", + "updatedAt": "2026-06-01T12:30:00Z" + }, + { + "id": "dt-adopt1.3", + "title": "Review loop", + "status": "in_progress", + "updatedAt": "2026-06-01T12:30:00Z" + }, + { + "id": "dt-adopt1.3a", + "title": "Claude review", + "status": "closed", + "updatedAt": "2026-06-01T11:00:00Z" + }, + { + "id": "dt-adopt1.2", + "title": "Worktree rebase check", + "status": "closed", + "updatedAt": "2026-06-01T10:20:00Z" + }, + { + "id": "dt-adopt1.1", + "title": "Preflight checks", + "status": "closed", + "updatedAt": "2026-06-01T10:05:00Z" + }, + { + "id": "dt-adopt1.fin", + "title": "Merge and finalize", + "status": "open", + "updatedAt": "2026-06-01T10:00:30Z" + }, + { + "id": "dt-bugdone", + "title": "mol-bug-report-implementation-v2", + "status": "closed", + "updatedAt": "2026-05-20T15:45:00Z" + }, + { + "id": "dt-bugdone.2", + "title": "Merge and finalize", + "status": "closed", + "updatedAt": "2026-05-20T15:45:00Z" + }, + { + "id": "dt-bugdone.1", + "title": "Implement change", + "status": "closed", + "updatedAt": "2026-05-20T11:00:00Z" + }, + { + "id": "dt-latch1", + "title": "mol-focus-review", + "status": "blocked", + "updatedAt": "2026-04-01T00:00:00Z" + } + ], + "census": { + "status": "unavailable", + "error": "run health has not been derived" + } +} diff --git a/internal/runproj/testdata/sessions_fixture.json b/internal/runproj/testdata/sessions_fixture.json new file mode 100644 index 0000000000..e8b1858a24 --- /dev/null +++ b/internal/runproj/testdata/sessions_fixture.json @@ -0,0 +1,40 @@ +[ + { + "id": "sess-polecat-1", + "template": "polecat", + "session_name": "gascity-packs__polecat-1", + "title": "Polecat 1", + "alias": "polecat-1", + "state": "active", + "display_name": "Claude Code", + "created_at": "2026-06-01T10:00:00Z", + "last_active": "2026-06-01T12:29:30Z", + "attached": false, + "rig": "gascity-packs", + "pool": "polecats", + "agent_kind": "pool", + "running": true, + "model": "claude-opus-4-8", + "activity": "tool_use", + "provider": "claude" + }, + { + "id": "sess-ralph-3", + "template": "ralph", + "session_name": "demo-app__ralph-3", + "title": "Ralph 3", + "alias": "ralph-3", + "state": "active", + "display_name": "Codex", + "created_at": "2026-06-02T00:00:00Z", + "last_active": "2026-06-02T00:04:45Z", + "attached": false, + "rig": "demo-app", + "pool": "ralphs", + "agent_kind": "pool", + "running": true, + "model": "gpt-5-codex", + "activity": "thinking", + "provider": "codex" + } +] diff --git a/internal/runproj/testenv_import_test.go b/internal/runproj/testenv_import_test.go new file mode 100644 index 0000000000..a94e4c44ff --- /dev/null +++ b/internal/runproj/testenv_import_test.go @@ -0,0 +1,5 @@ +// Code generated by go run scripts/add-testenv-import.go; DO NOT EDIT. + +package runproj + +import _ "github.com/gastownhall/gascity/internal/testenv" diff --git a/internal/runproj/types.go b/internal/runproj/types.go new file mode 100644 index 0000000000..6995a465fe --- /dev/null +++ b/internal/runproj/types.go @@ -0,0 +1,253 @@ +package runproj + +// RunSummary is the dashboard run-view DTO, a faithful Go port of the +// TypeScript RunSummary in +// internal/api/dashboardspa/web/shared/src/snapshot/types.ts. Field order here +// is load-bearing: the golden-parity test marshals this struct with the same +// canonical JSON the TS generator used (JSON.stringify(..., 2)), so the JSON +// key order must match the TS object-literal key order in +// shared/src/runs/summary.ts (buildRunSummary). +type RunSummary struct { + // TotalActive counts ACTIVE lanes (phase neither "complete" nor "blocked"). + TotalActive int `json:"totalActive"` + // TotalHistorical is the TRUE count of completed lanes (may exceed + // len(HistoricalLanes) once the cap applies). + TotalHistorical int `json:"totalHistorical"` + RunCounts RunCounts `json:"runCounts"` + Lanes []RunLane `json:"lanes"` + HistoricalLanes []RunLane `json:"historicalLanes"` + BlockedLanes []RunLane `json:"blockedLanes"` + RecentChanges []RunChange `json:"recentChanges"` + Census RunCensusState `json:"census"` + // LanesPartial is set only when the builder ran in partial mode; omitted + // otherwise (TS optional literal `true`). + LanesPartial bool `json:"lanesPartial,omitempty"` +} + +// RunCounts is the per-kind lane tally. Port of TS RunCounts. +type RunCounts struct { + Total int `json:"total"` + // Visible equals Total (RunMap owns the rendered collapse; deprecated in TS). + Visible int `json:"visible"` + PrReview int `json:"prReview"` + DesignReview int `json:"designReview"` + Bugfix int `json:"bugfix"` + Blocked int `json:"blocked"` + Other int `json:"other"` +} + +// RunLane is a single run lane. Port of TS RunLane. Field order matches the +// object literal returned by runLane() in summary.ts. +type RunLane struct { + ID string `json:"id"` + Title string `json:"title"` + Formula RunLaneFormula `json:"formula"` + Scope RunLaneScope `json:"scope"` + External RunLaneExternalReference `json:"external"` + Phase string `json:"phase"` + PhaseLabel string `json:"phaseLabel"` + StatusCounts StatusCounts `json:"statusCounts"` + ActiveAssignees []string `json:"activeAssignees"` + UpdatedAt RunLaneUpdatedAt `json:"updatedAt"` + Stages []RunStage `json:"stages"` + Progress RunLaneProgress `json:"progress"` + // FormulaStageResolved is true when stages came from a recognized formula + // AND the active gc.step_id mapped into one of those formula stages. + FormulaStageResolved bool `json:"formulaStageResolved"` + Health RunLaneHealthState `json:"health"` +} + +// RunLaneFormula is the discriminated formula-identity union. TS RunLaneFormula: +// {status:'known', name} | {status:'unavailable', error}. Marshaled via a custom +// MarshalJSON so only the active arm's fields appear. +type RunLaneFormula struct { + Status string // "known" | "unavailable" + Name string + Error string +} + +// RunLaneScope is the lane scope union. TS Avail<{kind, ref, rootStoreRef}>: +// {status:'available', kind, ref, rootStoreRef} | {status:'unavailable', error}. +type RunLaneScope struct { + Status string // "available" | "unavailable" + Kind string + Ref string + RootStoreRef string + Error string +} + +// RunLaneExternalReference is the external-reference union. TS: +// {status:'available', label, url} | {status:'label_only', label} | +// {status:'unavailable', error}. +type RunLaneExternalReference struct { + Status string // "available" | "label_only" | "unavailable" + Label string + URL string + Error string +} + +// RunLaneUpdatedAt is the updated-at union. TS Avail<{at}>. +type RunLaneUpdatedAt struct { + Status string // "available" | "unavailable" + At string + Error string +} + +// StatusCounts is a per-status tally that preserves first-seen status order, so +// it serializes in the same key order the TS `Record` does (JS +// objects keep insertion order; a Go map would sort keys and break parity). +type StatusCounts struct { + keys []string + counts map[string]int +} + +// inc records one occurrence of status, tracking first-seen order. +func (s *StatusCounts) inc(status string) { + if s.counts == nil { + s.counts = map[string]int{} + } + if _, ok := s.counts[status]; !ok { + s.keys = append(s.keys, status) + } + s.counts[status]++ +} + +// MarshalJSON renders the counts in first-seen order. +func (s StatusCounts) MarshalJSON() ([]byte, error) { + pairs := make([]kv, 0, len(s.keys)) + for _, k := range s.keys { + pairs = append(pairs, kv{k, s.counts[k]}) + } + return marshalObject(pairs) +} + +// RunStage is one stage in a lane's ladder. Port of TS RunStage. +type RunStage struct { + Key string `json:"key"` + Label string `json:"label"` + Status string `json:"status"` // "pending" | "active" | "complete" | "blocked" +} + +// RunLaneProgress is the lane-progress union. TS RunLaneProgress: +// {status:'active_step', stepId, stage, attempt} | +// {status:'stage_only', stage, error} | {status:'unavailable', error}. +type RunLaneProgress struct { + Status string // "active_step" | "stage_only" | "unavailable" + StepID string + Stage RunLaneStagePosition + Attempt RunLaneStepAttempt + Error string +} + +// RunLaneStagePosition is the active-stage union. TS Avail<{index, key, label}>. +type RunLaneStagePosition struct { + Status string // "available" | "unavailable" + Index int + Key string + Label string + Error string +} + +// RunLaneStepAttempt is the step-attempt union. TS Avail<{value}>. +type RunLaneStepAttempt struct { + Status string // "available" | "unavailable" + Value int + Error string +} + +// RunLaneHealthState is the lane-health union. TS Avail<{data}>. BuildRunSummary +// emits the unavailable arm; EnrichRunSummary replaces it with the available arm +// carrying the derived RunLaneHealth. +type RunLaneHealthState struct { + Status string // "available" | "unavailable" + Data RunLaneHealth + Error string +} + +// RunLaneHealth is the engine-derived per-lane health. Port of TS RunLaneHealth. +// Field order matches the object literal built in deriveRunHealth (health.ts). +type RunLaneHealth struct { + PhaseConfidence string // "known" | "inferred" + NeedsOperator bool + StuckNode RunLaneStuckNode + ThrashingDetected bool + Session RunLaneSessionState +} + +// RunLaneStuckNode is the stuck-node union. TS Avail<{id}>. +type RunLaneStuckNode struct { + Status string // "available" | "unavailable" + ID string + Error string +} + +// RunLaneSessionState is the resolved-session union. TS RunLaneSessionState: +// {status:'resolved', lastActive, running, activity} | {status:'unresolved', error}. +type RunLaneSessionState struct { + Status string // "resolved" | "unresolved" + LastActive RunLaneSessionLastActive + Running RunLaneSessionRunning + Activity RunLaneSessionActivity + Error string +} + +// RunLaneSessionLastActive is the last-active union. TS Avail<{at}>. +type RunLaneSessionLastActive struct { + Status string // "available" | "unavailable" + At string + Error string +} + +// RunLaneSessionRunning is the running-flag union. TS Avail<{value}>. +type RunLaneSessionRunning struct { + Status string // "available" | "unavailable" + Value bool + Error string +} + +// RunLaneSessionActivity is the activity-hint union. TS Avail<{value}>. +type RunLaneSessionActivity struct { + Status string // "available" | "unavailable" + Value string + Error string +} + +// RunCensusState is the city-census union. TS Avail<{data}>. BuildRunSummary +// emits the unavailable arm; EnrichRunSummary replaces it with the available arm +// carrying the derived RunCensus. +type RunCensusState struct { + Status string // "available" | "unavailable" + Data RunCensus + Error string +} + +// RunCensus is the threshold-independent city census. Port of TS RunCensus. +// Field order matches the object literal built in buildCensus (health.ts). +type RunCensus struct { + ByPhase RunCensusByPhase + TotalInFlight int + Unverifiable int + KnownDenominator int + Thrashing int +} + +// RunCensusByPhase is the per-phase lane tally. Port of TS Record. +// Key order matches zeroByPhase() in health.ts (NOT alphabetical). +type RunCensusByPhase struct { + Intake int + Implementation int + Review int + Approval int + Finalization int + Blocked int + Complete int + Active int +} + +// RunChange is one recent-change row. Port of TS RunChange. +type RunChange struct { + ID string `json:"id"` + Title string `json:"title"` + Status string `json:"status"` + UpdatedAt string `json:"updatedAt"` +} diff --git a/plans/runs-view-HANDOFF.md b/plans/runs-view-HANDOFF.md new file mode 100644 index 0000000000..3e37c55e3f --- /dev/null +++ b/plans/runs-view-HANDOFF.md @@ -0,0 +1,125 @@ +# Runs-view event-sourcing — Handoff + +Last updated: 2026-06-28. Owner of record: hand-off from the session that landed P0+P1. + +## Goal (one line) + +Replace the dashboard **Runs view**'s slow `bd`/`gc` molecule scans with a Go projection that folds the per-city `.gc/events.jsonl` (the OSS-local analog of the hosted ClickHouse run projection), so the view is fast and has one source of truth. + +## Decided architecture (do not re-litigate) + +**SingleSource-RunProjection** (effort-agnostic architect-panel decision). Full ADR: [`plans/runs-view-architecture-adr.md`]; design + feasibility evidence: [`plans/runs-view-event-sourcing.md`]. + +- Run **semantics** (fold, grouping, phase/stage classification, health/census) live **once, in Go** (`internal/runproj`), object-model layer (deps: `internal/beads` + `internal/events` only). +- Run **knowledge** that is editable data (per-formula stage tables, phase-token vocab, `gc.kind`→construct map, lifecycle rank) lifts into a versioned `runspec/*.toml`, code-generated into both Go and TS under a regenerate-and-diff CI gate (P5). +- The SPA becomes a **pure renderer** of the existing `RunSummary` / `FormulaRunDetail` DTOs (P4 deletes ~5,000 LOC of TS run logic). +- Summary + detail share the **same fold** so they cannot disagree on a lane/phase/stage. + +Locked product decisions: pure event-sourcing (no beads backfill); full server-side incl. session enrich; direct cutover gated by golden parity; cold-replay each supervisor start (no persisted checkpoint in v1). + +## Current state + +- **PR #3804** — branch `feat/runs-event-projection`, **draft → `main`** (rebased off the dashboard branch on 2026-06-29; see Base below). **Supersedes #3793**, which GitHub auto-closed when its base `feat/dashboard-supervisor-hosting` was deleted on the #3727 squash-merge (a closed PR with a deleted base cannot be reopened or retargeted, so a fresh PR was opened). Draft until ready to merge; **live deploy is gated by #3288** (see Deploy status). +- **Commits on the PR:** P0 foundation (`13e93eb22`) + P1 buildRunSummary port (`2c8be44f6`) + P2a enrich port (`006c5c32d`) + P2b tailer/endpoint (`62a599efa`) + P3 detail interpreter/endpoint (`1bc9e3dea`), all golden-gated and lint-clean. +- **Base: rebased onto `origin/main` on 2026-06-29** (after #3727 squash-merged to main as `677ce243f`; the dashboard branch no longer exists on origin). Command: `git rebase --onto origin/main 05b3a8ca6 feat/runs-event-projection` — replayed the **17 run-projection commits CLEANLY (no conflicts)**; the inherited 9 dashboard commits were dropped (their content is in main via the squash). Rebased tip `bb53a14e4` on `origin/main 8cacec199`; force-pushed-with-lease. Pre-rebase tip backed up on branch `backup/runs-event-projection-pre-main-rebase`. Re-verified green on main: `make dashboard-check`, dist **byte-identical**, `go build ./cmd/gc` (246 MB), `go test ./internal/runproj` goldens + `-race ./internal/api/dashboardbff`. **Commit hashes elsewhere in this doc are pre-rebase (historical); commits live under new hashes.** +- **Re-rebased onto current `origin/main` `c91105e2b` on 2026-06-30.** main had advanced 16 commits (incl. the #3288 fix #3811 + #3822 which rebuilt the dashboard dist), turning #3804 CONFLICTING. Re-rebased with `git rebase origin/main`; the **only** conflict was the generated `dist/` (source files disjoint) — resolved per-commit then one `make dashboard-build` regenerated dist for the merged source (committed `chore(dashboard): rebuild dist after rebase`, tip `48df87653`). Backup `backup/runs-event-projection-pre-main-rebase2`. #3804 is now **MERGEABLE**; re-verified green (`make dashboard-check`, `go build ./cmd/gc` 246 MB, lint, vitest 759, runproj goldens). +- **Deploy status — #3288 blocker CLEARED on main.** Window 3 (tmux `recovered-interactive-20260613:3`, deploy owner) fixed the boot-hang: **#3811 MERGED to main 2026-06-29** ("defer pool sweep + orphan/failed-create session closes on the boot reconcile"; cause was `792ad0337`'s boot-time per-session bd write-wave under Dolt contention). Live maintainer-city still runs `dev-d12ecdf81` (pre-#3288); redeploy mechanism: `gc-promote-dev --ref ` (clean clone → `make check` → install → flip `current` → session-preserving restart); `gc-rollback dev-d12ecdf81` to recover. **Remaining to ship the run-views live:** mark #3804 ready → merge to main → `gc-promote-dev --ref ` (main now boots clean) → run-views live with real data; coordinate the restart with window 3. To preview sooner without touching maintainer-city: a fresh/small test city renders the views (empty — no real run data). +- **Done:** + - **P0** — `internal/runproj/fold.go` (events→bead `Fold`/`Apply`/`FoldFile`) + golden corpus in `internal/runproj/testdata/` (14-bead fixture + `runsummary_golden.json` + `rundetail_golden.json`), generated by `internal/api/dashboardspa/web/scripts/gen-run-goldens.mts` with npm scripts `gen:run-goldens` / `gen:run-goldens:check` (CI drift guard). + - **P1** — `BuildRunSummary([]beads.Bead, ...BuildOption)` in `internal/runproj/{summary,types,marshal,phasemapping,scope,strip,formulaname}.go`, **byte-for-byte** equal to the TS `buildRunSummary` output (`summary_golden_test.go`, GREEN). Health/census left at the builder default (status "unavailable"). + - **P2a** — session-enrich port (`006c5c32d`): `EnrichRunSummary` + `AdvanceProgressMarks` + `DashboardSession`/session-resolve in `internal/runproj/{enrich,session}.go`, available-arm marshalers (`types.go`/`marshal.go`). New `runsummary_enriched_golden.json` (fixture beads + `sessions_fixture.json`, fixed generation time) emitted by the generator; `TestEnrichRunSummaryGolden` is byte-for-byte GREEN. Ported `health.test.ts` + `liveness.test.ts` to Go, plus a cross-generation thrash test. + - **P2b** — per-city tailer + endpoint (`62a599efa`): `runproj.Projector` (order-preserving fold) + `internal/api/dashboardbff/runtailer.go` (cold replay `ColdLoad`, read-only `events.ReadFrom` byte-offset tail, race-free resume, server-side marks) + `GET /api/city/{cityName}/runs/summary` (warm fold + request-time loopback `/v0 sessions` enrich, degrade-to-unavailable). Wired into `plane.go` Start/Stop. Race-clean tailer/endpoint/Projector tests; `TestOpenAPISpecInSync` stays green (non-Huma plane). + - **P3** — detail interpreter + endpoint (`1bc9e3dea`): `BuildRunDetail([]beads.Bead, runID, version, eventSeq)` + `BuildRunDetailWithSessions` in `internal/runproj/detail*.go` (13 new files). The whole TS detail pipeline ported single-homed: snapshot synthesis (`snapshotForRun`/`toRunSnapshotBead`/`depsForMembers`), `groupRunBeads` (semantic-id disambiguation, badge aliasing), `buildRunDisplayNode`/`latestIterationsByLoop`, `buildRunDisplayEdges` (hidden scope-check bridging), `applyDisplayNodeStates`, `orderRunNodeGroups`, `buildRunDisplayLanes`, `runSessionLinkFor`, `resolveRunFormulaIdentity`/`resolveRunExecutionPath`, union marshalers. **`rundetail_golden.json` byte-for-byte GREEN** (`TestBuildRunDetailGolden`); summary↔detail phase/stage consistency test; ported `session-link.test.ts`. New `GET /api/city/{cityName}/runs/{runId}/detail` on the BFF plane (warm `Projector.Beads()`+`LastSeq`, request-time `/v0` sessions enrich, `UnsupportedRunError`→422+reason, missing run→404, warming→503). `TestOpenAPISpecInSync` stays green. + - **Adversarial review (9-pair Go↔TS diff workflow) landed 5 fixes** in the same commit: (1) **`semanticIdFromStepRef`** for an `"iteration.N"` step ref returned a present-but-empty `""` semantic id (TS reads `semanticParts[-1]`=undefined via plain bracket, not `.at(-1)`) — corrupted grouping; now falls through to bead-id/`run-node` (`TestSemanticNodeIDForIterationStepRef`). (2) `isPositiveInteger` float64 exact-representability gate. (3) `externalizeDisplayText` `\s` collapse uses the JS Unicode whitespace set. (4) `nonEmpty` trims the ECMAScript `String.prototype.trim()` set (strips U+FEFF, not U+0085 — inverse of `unicode.IsSpace`) at the single chokepoint; all P0–P3 goldens stayed byte-identical. (5) `formulaRankByAlias` nil-vs-empty `preview.nodes` to mirror `??`. + - **Two divergences left as accepted/known (NOT bugs to fix here):** (a) `localeCompare` tie-breaks are byte-compared — the uniformly-applied P1 convention across summary+enrich+detail (an exact-ICU fix would need a collation dep and must touch every tie-break); (b) P1 `parsePositiveInteger` uses `strconv.Atoi` vs TS `Number()` (rejects `"1e2"`/`"0x10"` review-round values) — pre-existing P1 code, unreachable with real round values; fix in a P1-scoped change if ever needed. + - **P4a — SPA cutover (this session, `30dbb7214` + hardening `ceaeac078`).** The two SPA loaders now read the BFF endpoints; the SPA is a pure renderer of the unchanged DTOs. `api/client.ts` adds `runSummary()`/`runDetail(runId)` GETs (edge decoders validate every field a renderer hard-derefs) + threads the BFF 422 `reason` through `ApiError`/`ApiClientError`. `supervisor/runSummary.ts` → 1 warm GET (4 loaders collapsed; server owns enrich/health/census/marks/history). `runs/runSummarySubscription.tsx` drops the now-incorrect history-merge (BFF folds active+historical atomically), keeps last-good retention + SSE 10s-debounce + degraded-retry. `supervisor/runDetail.ts` → 1 GET with a bounded transient retry (503/5xx/network, not 4xx). `hooks/useFormulaRunDetail.ts` maps `ApiClientError` (422+not_run_view→unsupported, 422+invalid_snapshot→failed, 404→not_found). 6 test files rewritten to the thin read surface (deleted fold coverage now lives in the Go goldens). **Additive + revertable: `shared/src/runs/*` still exists, now unused by the loaders.** All gates green (dashboard-check, typecheck/typecheck:test/lint, vitest 759, golden-drift, `-race` dashboardbff). + +## Worktree map — IMPORTANT + +- **Work here:** `/data/projects/gascity/.claude/worktrees/runs-proj` (branch `feat/runs-event-projection`). `node_modules` is installed under `internal/api/dashboardspa/web` for the golden generator. +- **DO NOT touch:** `/data/projects/gascity/.claude/worktrees/new-dashboard` — it has ~300 uncommitted non-mine changes (the in-flight beads-attribution / core refactor). The operator said leave it untouched. Never `git add -A`/`stash`/`checkout -- .` there. +- The harness resets shell cwd each Bash call → prefix commands with `cd /data/projects/gascity/.claude/worktrees/runs-proj && …`. + +## How to verify (run before/after every change) + +```bash +cd /data/projects/gascity/.claude/worktrees/runs-proj +go build ./internal/runproj/ && go vet ./internal/runproj/ +go test ./internal/runproj/ -count=1 # golden parity must stay GREEN +gofmt -l internal/runproj/ # must be empty +golangci-lint run ./internal/runproj/ # 0 issues +# regenerate goldens (after intentional TS-shape changes only) + drift guard: +cd internal/api/dashboardspa/web && npm run gen:run-goldens:check +``` + +## Remaining phases (each = a golden-gated commit on #3793) + +### P2 — per-city tailer + `/runs/summary` endpoint + server-side enrich — ✅ DONE +Landed as P2a (`006c5c32d`) + P2b (`62a599efa`). Notes for later phases: +- The live tail uses a self-contained read-only `events.ReadFrom` byte-offset loop in `runtailer.go` (NOT a `transientCityEventProvider` — no such symbol exists; this keeps the tailer fork-owned with no supervisor wiring). Resume is race-free: offset captured before cold replay + seq dedupe. +- `EnrichRunSummary(s, sessions, sessionsAvailable, nowMs, marks)` — marks are tailer-owned (`AdvanceProgressMarks`), passed in read-only. Sessions are unmarshaled straight from the `/v0` list `items` into `[]DashboardSession` (equivalent to `normalizeSessions`). +- `enrich.ts` in the original P2 note was a red herring — it's the *detail* enrich (`enrichFormulaRun`), which is P3. + +### P3 — detail interpreter + `/runs/{id}/detail` — ✅ DONE (`1bc9e3dea`) + +Landed (see "Current state" above for the commit summary + the 5 adversarial-review fixes + 2 accepted divergences). **Next phase is P4.** Notes below are retained for context. +Port the detail pipeline: `shared/src/runs/{enrich(enrichFormulaRun),formula-run,groups,edges,node-shape,execution-instances,formula-order,session-link,display-state}.ts` into `internal/runproj` (`BuildRunDetail`). Sizes: `groups.ts` 359, `formula-run.ts` 289, `execution-instances.ts` 264, `node-shape.ts` 190, others smaller. The graph-layout (groups semantic-id disambiguation, alias maps, loop instancing; node-shape) is the hard, correctness-sensitive core — port as Go code, single-homed (the ADR says it's genuinely not data). + +**Design facts (verified this session — read before porting):** +- **The golden path is BEAD-DERIVED ONLY.** The generator builds `rundetail_golden.json` via `snapshotForRun(beads, "dt-adopt1")` → `enrichFormulaRun(snapshot, {})` — **no sessions, no formulaDetail** (`gen-run-goldens.mts:buildDetailGolden`). So the golden-gated Go port is the bead-derived detail; session/compiled-formula enrichment is OUT of golden scope. +- **Input shape:** `enrichFormulaRun` consumes a `RunSnapshot` of `RunSnapshotBead` (`shared/src/run-snapshot.ts`: id, title, status, **kind**, step_ref, scope_ref, logical_bead_id, metadata), NOT `beads.Bead`. The Go port must reproduce the generator's projection from the fold: + - `snapshotForRun(beads, rootId)` member selection: `id==rootId || parent==rootId || metadata['gc.root_bead_id']==rootId || id.startsWith(rootId+'.')`. + - `toRunSnapshotBead`: `kind = metadata['gc.original_kind'] ?? issue_type`; `step_ref = ref`; `scope_ref = metadata['gc.scope_ref']`; `logical_bead_id = metadata['gc.logical_bead_id']`. + - `depsForMembers`: each non-root member → `{from: rootId, to: member.id, kind:'parent'}`. + - snapshot identity from root metadata; **`snapshot_version=1`, `snapshot_event_seq=100`** (generator constants — they appear verbatim in the golden's `snapshotVersion`/`snapshotEventSeq`). Parameterize these (e.g. `BuildRunDetail(beadList, runID, version, eventSeq)`): golden test passes 1/100; the live endpoint passes a real version + the tailer's `LastSeq()`. +- **Shared classifier (ADR invariant):** `formula-run.ts` calls the SAME `mapRunPhase`/`stageProgress`/`stagesForFormula` already ported in P1 `phasemapping.go`. Reuse them so detail stages == summary stages by construction. Add a summary↔detail consistency test (same fixture run → same phase/stage through both). +- **session-link caveat:** `session-link.ts` (lane→session links in the detail view) only fires when `opts.sessions` is passed, which the golden path does NOT — so its effect is ABSENT from the golden. Port it for the live endpoint's request-time session enrich (mirror P2's summary enrich), but it is not golden-gated. `session-link.test.ts` exists — port it as a unit test. (`session-link.ts` differs in the `new-dashboard` dirty tree; the committed golden was generated on the clean `runs-proj` base and is valid — don't regenerate against the dirty tree.) +- **Detail golden shape (19 keys):** runId, rootBeadId, rootStoreRef, resolvedRootStore, scopeKind, scopeRef, title, formula, formulaDetail, executionPath, snapshotVersion, snapshotEventSeq, completeness, progress, phase, stages, nodes, edges, lanes. Fixture `dt-adopt1` run → 5 nodes, 4 edges, 7 stages, 1 lane. Same union-marshaling discipline as P1/P2a (per-arm keys in TS object-literal order). + +**Endpoint:** `GET /api/city/{cityName}/runs/{runId}/detail` on the BFF plane (non-Huma) returning the unchanged `FormulaRunDetail` DTO. The tailer already holds the warm `Projector`; the handler gets `Projector.Beads()` (all) + the `runId`, calls `BuildRunDetail` (which does member selection), then optionally request-time session enrich. Mirror `registerRunSummary`/`cityRunTailer` in `runtailer.go`. + +### P4 — SPA cutover (P4a ✅ DONE) + delete dead TS run logic (P4b ✅ DONE) + +**P4 is COMPLETE.** P4a (cutover) = `30dbb7214` + `ceaeac078`; P4b (deletion) = `c2aa9ac53` (30 files, +47/−5162 — 17 fold/graph modules + 5 tests deleted, the barrel pruned, the golden generator retired, `health.ts`/`summary.ts` slimmed to the kept selectors). The shipped dist is byte-identical (the deleted TS was already tree-shaken). All gates green. **Only P5 (`runspec` codegen) remains — deferrable; the system rests stably at single-home-in-Go.** The P4b scope below CORRECTED the original plan (a 24-agent value-import audit, `wf_dcc89776-4d0`, found "delete all of `shared/src/runs/*`" was wrong): + +- **KEEP (live presentation selectors over the server-computed DTO — NOT dead fold):** `runs/blocked.ts` (`selectBlockedRuns` — `attention/registry.ts` + `RunMap.tsx`), `runs/health.ts`'s `laneNeedsOperator` (`AmbientHome.tsx` + `StatusSentence.tsx`), `runs/summary.ts`'s `MAX_VISIBLE_ACTIVE_LANES` (`RunMap.tsx`). All three are DTO-only (blocked.ts imports only `RunLane`/`RunLaneScope` types; the others need only DTO types + `session-resolve`), so they do NOT pull in the fold. `health.ts`/`summary.ts` should be **slimmed** to just the kept symbol (drop the dead `deriveRunHealth`/`buildCensus`/`advanceProgressMarks`/`buildRunSummary`/`runCounts`/`runBeadFilter`), which then frees `phaseMapping.ts`/`formula-name.ts`/`liveness.ts`/`bead-fields.ts`. +- **DELETE (the dead fold + graph-layout pipeline, ~3–4k LOC):** `enrich, formula-run, groups, edges, node-shape, execution-instances, execution-path, display-state, formula-order, lanes, runtime-state, status, session-link, bead-fields` (+ `phaseMapping, formula-name, liveness` once `summary.ts` is slimmed) + their `*.test.ts`, plus the matching `runs/*` re-exports in `shared/src/index.ts`. Top-down (enrich → formula-run → leaves); `tsc` is the safety net (an orphaned value import fails immediately). +- **RETIRE the golden generator:** `scripts/gen-run-goldens.mts` value-imports `buildRunSummary`/`fromDashboardBead`/`enrichFormulaRun`/health/liveness, so it breaks on deletion. The goldens become **frozen Go-owned fixtures** (the Go golden tests already read the committed `internal/runproj/testdata/*.json` directly; Go is the single source now). Delete the generator + the `gen:run-goldens`/`gen:run-goldens:check` npm scripts. **⚠️ This removes the `gen:run-goldens:check` gate the original plan listed as "keep green" — an intentional consequence of single-homing in Go, flagged to the operator.** (Future Go-side golden changes would want a small `go test -update` flag — a P5-adjacent follow-up.) +- The 3 adversarial cutover-review findings were fixed in `ceaeac078` (detail retry resilience, decoder edge-depth, stale prose). + +The original frontend-only cutover plan (steps 1–4 below) is retained as history; it is DONE. + +> The file/symbol map below was gathered by exploration on the rebased base; **the exact RunPhase/RunStage/RunConstructKind enum members and DTO field sets must be re-read from the actual `.ts` files** (`shared/src/snapshot/types.ts`, `shared/src/run-detail.ts`, `shared/src/run-snapshot.ts`) before relying on them — an explore pass mis-listed `RunPhase`. Treat the paths as solid, the type enumerations as "verify first". + +**Cutover (≤5 files per phase; keep DTOs byte-stable — they are now the Go↔TS contract):** +1. **API client** (`frontend/src/api/client.ts`) — add two GET methods following the existing `request('GET', cityPath(...), decoder)` pattern (same one `api.runDiff` uses; `cityPath()` → `/api/city/{activeCity}/*`, `parseApiErrorBody` → `ApiClientError(status, message, kind)`, `X-GC-Request` only on mutations). Decoders return the existing `RunSummary` / `FormulaRunDetail` types (`objectDecoder`). +2. **Summary loader** (`frontend/src/supervisor/runSummary.ts`) — replace the multi-read fold (`listBeads` core + `formulaFeed` + per-rig `task` + `molecule(all=true)` + `listSessions`, then `buildRunSummary`→`enrichRunSummary`) with one warm GET to `/runs/summary`. Preserve the export surface (`loadSupervisorRunSummary{,Mount,Active,Preview}Source`) so `runs/runSummarySubscription.tsx` and `Runs.tsx`/`AmbientHome.tsx`/the attention badge keep working; the mount/active/preview variants collapse to the same cheap warm read (the server already did the enrich + thrash marks). **Keep the SSE nudge + debounce in `runs/runSummarySubscription.tsx` (`REFRESH_DEBOUNCE_MS=10_000`, `useGcEventRefresh([bead])`) — it now guards a sub-second warm read instead of the slow scan.** +3. **Detail loader** (`frontend/src/supervisor/runDetail.ts`) — replace `workflowRun`+`formulaDetail`+`listSessions`+`enrichFormulaRun` with one GET to `/runs/{runId}/detail`. **Map the BFF error contract to the existing `useFormulaRunDetail` states** (`frontend/src/hooks/useFormulaRunDetail.ts`): HTTP **422 + `{reason:'not_run_view'}`** → the hook's `'unsupported'` state; **422 + `{reason:'invalid_snapshot'}`** → load error; **404** → `'not_found'`; **503** (warming) → transient retry. The detail route `frontend/src/routes/FormulaRunDetail.tsx` + its `useGcEventRefresh` wiring stay. +4. **Shadow-compare** both paths over a soak window (log-diff old fold vs new endpoint) before deleting — the goldens prove byte-parity on the fixture, but live data exercises the uncovered paths (loops/retries/scope-check bridging) the golden does not. +5. **Delete the TS logic** — ⚠️ **SUPERSEDED by the "P4b deletion scope CORRECTED" block above.** The original "delete all of `shared/src/runs/*`" is wrong: `blocked.ts`, `laneNeedsOperator` (health.ts), and `MAX_VISIBLE_ACTIVE_LANES` (summary.ts) are live DTO selectors that STAY, and the golden generator must be retired. Use the corrected scope. + +**Gates:** `make dashboard-check` (= `dashboard-build` → `npm run typecheck` → `go test ./internal/api/dashboardspa/... ./internal/api/dashboardbff/...`) green; rebuild + re-embed `dist` (`//go:embed all:dist` in `internal/api/dashboardspa/embed.go`; `make dashboard-ci` checks the `internal/api/dashboardspa/dist/` git diff); start the SPA locally (`npm run preview -- --host 127.0.0.1 --port ` from `…/web`) and verify both views render against a live city. **Strengthen `wire_contract_test.go`** (ADR Phase 0 follow-up: today it only checks Go-side JSON shape) to run emitted Go JSON through the real TS decoders in Vitest, so the now-contract DTOs cannot drift in shape — do this BEFORE deleting the TS, while the decoders still exist. + +**Fallback:** if a live path reveals a Go-vs-TS divergence under shadow-compare, fix it in `internal/runproj` (add a fixture case + regenerate the golden on the clean base) rather than reviving TS logic. P4 can land incrementally: cutover-behind-shadow first (additive, revertable), deletion second. + +### P5 — `runspec` codegen +- Author `runspec/*.toml` (stage tables, phase vocab, lifecycle rank, `gc.kind`→construct sourced from `internal/beadmeta` + `internal/dispatch`); add `go:generate` + npm prebuild emitting `internal/runspec/spec_gen.go` + `shared/src/runs/spec/spec.gen.ts`; refactor P1/P3 literals to read the generated constants; add a regenerate-and-diff CI test mirroring `TestOpenAPISpecInSync`. If P5 is deferred, the system rests stably at P4 = single-home-in-Go. + +## Gotchas (read these) + +- **Pre-commit hook is broken** (stale `docs/schema/` path on the machine's shared `core.hooksPath`). Run its checks manually (gofmt, `make lint-changed LINT_CHANGED_SCOPE=staged`, `go vet`) then `git commit --no-verify`; `git push --no-verify`. Note it in PR bodies. +- **Golden discriminated unions:** Go maps reorder keys; `marshal.go` emits per-arm keys in TS object-literal order. `statusCounts` uses JS insertion order (not alphabetical). Use `sort.SliceStable` (JS sort is stable). +- **Timestamps:** `beads.Bead.UpdatedAt` zero → fall back to `CreatedAt`; re-render RFC3339 UTC (`…Z`) to match the fixture. The TS `Number.isFinite(Date.parse(x))` guard → a parse failure means "not stale"; the Go port mirrors this via `millisFromTimestamp` returning `ok=false` (`enrich.go`). +- **Live-view determinism (P2b):** never feed the live tailer a plain `Fold` map — Go map iteration is random and `BuildRunSummary` groups by first-seen order, so the view would flicker between requests. Use `runproj.Projector` (order-preserving). P3's detail endpoint must do the same. +- **Race-free tail resume (P2b):** the tailer captures its byte offset BEFORE the cold replay and dedupes events past the projector cursor (`eventsAfter`). Do NOT "optimize" this to compute the offset after `close(readyCh)` — the race detector caught that ordering dropping events that race the replay. +- **No `transientCityEventProvider`:** the ADR/old-prompt named it but it doesn't exist. P2b uses a self-contained read-only `events.ReadFrom` byte-offset loop (`runtailer.go`) — fork-owned, no supervisor-core wiring, no second writer. Keep P3's detail tailing on the same warm Projector. +- **Sessions read:** `/v0/city/{name}/sessions` returns `ListBody[sessionResponse]`; unmarshal `items` straight into `[]runproj.DashboardSession` (extra wire fields are ignored — equivalent to the frontend `normalizeSessions`). +- **Rebase** `feat/runs-event-projection` onto `origin/feat/dashboard-supervisor-hosting` whenever the dashboard branch advances (it has moved several times). +- The full `cmd/gc` test package times out as one monolith — use the sharded targets / targeted `-run` (see `TESTING.md`). Run the tailer/endpoint tests under `-race` (`go test -race ./internal/api/dashboardbff/`). + +## Pointers + +- Architecture: `plans/runs-view-architecture-adr.md` · Design + evidence: `plans/runs-view-event-sourcing.md` +- Code (object-model): `internal/runproj/` — P0 `fold.go`; P1 `{summary,types,marshal,phasemapping,scope,strip,formulaname}.go`; P2a `{enrich,session}.go` (+ available-arm marshalers); P2b `projector.go`. Goldens + fixtures: `internal/runproj/testdata/{beads_fixture,sessions_fixture,runsummary_golden,runsummary_enriched_golden,rundetail_golden}.json`. Generator: `internal/api/dashboardspa/web/scripts/gen-run-goldens.mts`. +- BFF plane: `internal/api/dashboardbff/{plane.go,samplers.go(citySampler pattern),runtailer.go(P2b tailer+endpoint to mirror for P3)}` · plane Start/Stop is called from `cmd/gc/supervisor_dashboard.go` (no edit needed — tailers enable inside `plane.Start`). +- TS sources to port for P3: `internal/api/dashboardspa/web/shared/src/runs/{enrich,formula-run,groups,edges,node-shape,execution-instances,formula-order,session-link,display-state}.ts` + types `shared/src/{run-detail,run-snapshot}.ts`. Frontend detail enrich (P4): `frontend/src/supervisor/runDetail.ts`. diff --git a/plans/runs-view-NEXT-SESSION-PROMPT.md b/plans/runs-view-NEXT-SESSION-PROMPT.md new file mode 100644 index 0000000000..6e62bf2fc1 --- /dev/null +++ b/plans/runs-view-NEXT-SESSION-PROMPT.md @@ -0,0 +1,35 @@ +# Next-session kickoff prompt + +> ⚠️ **OBSOLETE — P4 IS COMPLETE** (P4a `30dbb7214` + `ceaeac078`, P4b `c2aa9ac53`). +> The SPA cutover and the dead-fold deletion both landed and pushed on +> `feat/runs-event-projection`. The block below kicks off P4 and must NOT be +> re-run. The only remaining phase is **P5 (`runspec` codegen)** — deferrable; +> the system rests stably at single-home-in-Go. See `runs-view-HANDOFF.md`. + +Paste the block below into a fresh session to continue this work at **P4**. + +--- + +Continue the dashboard **Runs-view event-sourcing** work — next phase is **P4 (SPA cutover + delete the TS run logic)**. Read `plans/runs-view-HANDOFF.md` first (in the `runs-proj` worktree): it has the full state, the decided architecture (ADR: `plans/runs-view-architecture-adr.md`), the worktree map, the verification commands, and — most importantly — the **expanded P4 section** (concrete files/hooks/error-contract/gates). Also recall memory `runs-view-event-sourcing`, `gascity-runs-proj-worktree-layout`, and `gascity-precommit-hook-stale-absolute-hookspath`. + +Where things stand: PR **#3793** (branch `feat/runs-event-projection`, stacked **draft** on dashboard PR #3727) has **P0–P3 landed and pushed** (HEAD `366741c42`, rebased onto dashboard tip `05b3a8ca6`). The Go projection is done and golden-byte-green: `BuildRunSummary`/`EnrichRunSummary` (P1/P2) and `BuildRunDetail` (P3) in `internal/runproj`, plus **both BFF endpoints already serve the unchanged DTOs**: `GET /api/city/{city}/runs/summary` and `GET /api/city/{city}/runs/{runId}/detail`. So **P4 is frontend-only** — no new Go logic (only optionally strengthening `wire_contract_test.go`). + +Architecture is **SingleSource-RunProjection**: run semantics live once in Go; the SPA becomes a **pure renderer** of the existing `RunSummary`/`FormulaRunDetail` DTOs. P4 deletes ~4.3k LOC of TS run logic. + +Hard rules: +- Work ONLY in `/data/projects/gascity/.claude/worktrees/runs-proj`. The `new-dashboard` worktree has ~300 uncommitted non-mine changes — LEAVE IT UNTOUCHED. Shell cwd resets each Bash call, so prefix commands with `cd /data/projects/gascity/.claude/worktrees/runs-proj && …`. +- **Keep the DTO types byte-stable** — `shared/src/{snapshot/types,run-detail,run-snapshot}.ts` are now the Go↔TS contract; render components import them `import type`. The Go side marshals to exactly these shapes (golden-gated). +- Gate every change: `make dashboard-check` (`dashboard-build` → `npm run typecheck` → `go test ./internal/api/dashboardspa/... ./internal/api/dashboardbff/...`) green; keep `go test ./internal/runproj/ -count=1` + `-race ./internal/api/dashboardbff/` green; keep `npm run gen:run-goldens:check` (from `internal/api/dashboardspa/web`) green; keep `TestOpenAPISpecInSync` green. Rebuild + re-embed `dist` and check the `internal/api/dashboardspa/dist/` git diff (`make dashboard-ci`). +- The shared pre-commit hook is broken (stale absolute `core.hooksPath`) — run gates manually, then `git commit --no-verify` / `git push --no-verify`. The dashboard branch is periodically **rebased**; if it advanced, re-rebase with `git rebase --onto origin/feat/dashboard-supervisor-hosting ` (a plain rebase replays old dashboard commits and conflicts). Keep #3793 a draft until #3727 merges. +- ⚠️ An explore pass mis-listed the `RunPhase` enum — **re-read the actual `.ts` type files** before trusting any enum/field list in the handoff. + +**Do P4 in revertable phases (≤5 files each), then stop and report:** +1. **API client** (`frontend/src/api/client.ts`): add two GET methods mirroring `api.runDiff`'s `request('GET', cityPath(...), objectDecoder<…>())` pattern, returning the existing `RunSummary` / `FormulaRunDetail` types. +2. **Summary loader** (`frontend/src/supervisor/runSummary.ts`): replace the multi-read fold (`listBeads`+`formulaFeed`+per-rig `task`+`molecule(all=true)`+`listSessions` → `buildRunSummary`→`enrichRunSummary`) with one warm GET to `/runs/summary`; **preserve the export surface** (`loadSupervisorRunSummary{,Mount,Active,Preview}Source`) so `runs/runSummarySubscription.tsx` keeps working; **keep the SSE nudge + `REFRESH_DEBOUNCE_MS=10_000` debounce** — it now guards a sub-second warm read. +3. **Detail loader** (`frontend/src/supervisor/runDetail.ts`): replace `workflowRun`+`formulaDetail`+`enrichFormulaRun` with one GET to `/runs/{runId}/detail`; **map the BFF error contract** to the existing `useFormulaRunDetail` states (`frontend/src/hooks/useFormulaRunDetail.ts`): 422+`reason:'not_run_view'` → `'unsupported'`; 422+`reason:'invalid_snapshot'` → load error; 404 → `'not_found'`; 503 → transient retry. The detail route + its SSE wiring stay. +4. **Shadow-compare** both paths over a soak window against a live city before deleting (the golden covers only the simple path; live data exercises loops/retries/scope-check bridging). If a divergence shows, fix it in `internal/runproj` (+ fixture case + regenerate golden on the clean base), NOT by reviving TS. +5. **Strengthen `wire_contract_test.go`** to run emitted Go JSON through the real TS decoders in Vitest (ADR Phase 0 follow-up) — do this BEFORE the deletion, while the decoders still exist. +6. **Delete** `shared/src/runs/*.ts` (+ `*.test.ts`, ~4.3k LOC), the two `supervisor/run{Summary,Detail}.ts` fold bodies, and the `runs/*` re-exports in `shared/src/index.ts`; KEEP the three DTO type modules; repoint/delete the lone value-import test (`frontend/src/attention/registry.test.ts` → `selectBlockedRuns`). Rebuild + re-embed `dist`; `make dashboard-check` green; preview the SPA (`npm run preview -- --host 127.0.0.1 --port ` from `…/web`) and verify both views render live. +7. Land P4 as gated commit(s) on `feat/runs-event-projection`, push, update `plans/runs-view-HANDOFF.md` + the `runs-view-event-sourcing` memory, and report. (P5 = `runspec` codegen is a separate, deferrable phase; the system rests stably at P4 = single-home-in-Go.) + +--- diff --git a/plans/runs-view-architecture-adr.md b/plans/runs-view-architecture-adr.md new file mode 100644 index 0000000000..c909c7feb3 --- /dev/null +++ b/plans/runs-view-architecture-adr.md @@ -0,0 +1,256 @@ +# ADR: Run View End-State Architecture (Summary + Detail) + +Status: Proposed (Lead Architect decision) +Date: 2026-06-28 +Deciders: Lead Architect, operator +Priority order: maintainability > testability > UX/performance. Effort-agnostic. + +## Context + +The dashboard Run view (summary lanes + per-run detail diagram) is +reconstructed almost entirely client-side in TypeScript. The summary view +fans out to four slow supervisor `/v0` reads — `molecule(all=true)` (~6.8s +over ~340k rows), `formulaFeed` (~10s), per-rig `task(all=true)`, and the +active `listBeads` — then folds them in the browser via `buildRunSummary` +(`shared/src/runs/summary.ts`) and `enrichRunSummary` +(`frontend/src/supervisor/runSummary.ts`). The bottleneck is the **data +fetch**, not the fold. The detail view is **already fast**: it fetches a +server-folded `workflowRun` SQL snapshot (`buildWorkflowSnapshot` → +`tryFullWorkflowSQL`, ~190ms) plus the compiled formula +(`api.formulaDetail`), then enriches in TS. + +Verified code facts that drove this decision: + +- **Run logic is large and shared.** `shared/src/runs/*.ts` is ~5,000 LOC + non-test (`phaseMapping.ts` 708, `summary.ts` 510, `groups.ts` 359, + `formula-run.ts` 289, `execution-instances.ts` 264, `health.ts` 224, + `node-shape.ts` 190, …) plus ~779 LOC of frontend enrich. It powers + **both** the summary and the detail diagram. +- **Summary and detail share the classifier.** `formula-run.ts` imports and + calls the *same* `mapRunPhase` / `stageProgress` from `phaseMapping.ts` + (lines 17, 128, 130) that `summary.ts` uses. Any design that puts summary + phase in Go and leaves detail phase in TS **re-forks this exact surface** — + the outcome every credible candidate calls the worst. +- **The drift trap is real.** `phaseMapping.ts` `stagesForFormula` + (lines 439–575) hand-maintains per-formula step-id stage tables + (`mol-adopt-pr-v2`, `mol-design-review-v2`, `mol-bug-report-flow-v2`, + `mol-bug-report-implementation-v2`) that mirror the Go compiler's step IDs + with **no coupling and no CI gate**. A formula TOML edit silently desyncs + the run view. +- **The compiler does not own the phase taxonomy.** `recipe.Step.Phase` is + only `"vapor"`/`"liquid"` (an instantiation hint). The compiler + (`buildFormulaDetail`) owns step IDs + the node/edge graph, but the named + stage buckets/labels and the RunPhase vocabulary + (intake/implementation/review/approval/finalization) are **additive + interpretation** that lives only in TS today. So "derive stages from the + compiler and delete `phaseMapping`" is only partly possible — it relocates + that knowledge, it does not eliminate it. +- **`node-shape.ts` re-encodes Go-owned vocabulary.** The + `gc.kind`→`RunConstructKind` map and `HIDDEN_CONSTRUCTS` set encode kinds + (`ralph`, `check`, `spec`, `fanout`, …) that are stamped by the Go + dispatcher (`internal/dispatch/fanout.go`) and keyed in + `internal/beadmeta/keys.go`. Same drift class as the stage tables. +- **The substrate already exists.** `citySampler` (lazy per-city background + tailer), the `fetchStatus` loopback `127.0.0.1:port` pattern, the + `transientCityEventProvider` read-only `Watch` (no second writer), + `events.ReadFiltered` (which transparently walks rotated `.gz` archives), + and `wire_contract_test.go` (field-shape guard) are all present in + `internal/api/dashboardbff` and `internal/events`. +- **Rotation correction.** `EventsRotationConfig.EnabledOrDefault()` returns + `true` when `[events.rotation]` is absent, so the production city path + rotates at **256 MiB by default** (the design doc's "rotation off" claim is + wrong). But `archive_retain_age` defaults empty → 0 → archives kept + forever, and `ReadFiltered` walks them, so cold replay still reconstructs + full history. +- **Liveness exists.** The SPA already runs `useGcEventRefresh([bead])` with + a 10s debounce floor (`REFRESH_DEBOUNCE_MS`). + +## Decision + +Adopt **SingleSource-RunProjection**: run *semantics* get exactly one home +in Go, and run *knowledge* that is editable data is lifted into a versioned +declarative spec that is code-generated to both languages under one CI gate. + +1. **`internal/runproj` (new, fork-owned).** The single home of run + projection: the events→bead fold; `BuildRunSummary` (grouping precedence, + `isRunGroup`, `mapRunPhase`/`stepIdPhase`, `stageProgress`, counts); and + `BuildRunDetail` (`groupRunBeads`, semantic-id resolution, edges, + execution instances, display-state, lanes). Summary and detail are two + entry points over **one** set of primitives — they call the same grouping, + the same phase classifier, the same formula-identity resolver. Depends only + on `internal/beads`, `internal/beadmeta`, `internal/formula` (all Layer + 0–1; no upward dependency). +2. **`runspec/*.toml` + codegen (the data layer).** Per-formula stage tables, + phase-token vocabularies, lifecycle rank, the `gc.kind`→construct map + (sourced from `internal/beadmeta`/`internal/dispatch`), and the + hidden-construct set are authored once as data and `go:generate` + + npm-prebuild into `internal/runspec/spec_gen.go` and + `shared/src/runs/spec/spec.gen.ts`. A regenerate-and-diff CI test + (mirroring `TestOpenAPISpecInSync`) fails the build if either generated + file diverges from the source `.toml`. +3. **Per-city run-projection tailer in `internal/api/dashboardbff`** (modeled + on `citySampler`): lazy start, cold-replay `.gc/events.jsonl` via + `ReadFiltered` (archives included), live-tail via read-only `Watch`, fold + `bead.created/updated/closed/deleted` into `map[beadID]Bead`, and on each + tick republish the summary + detail projections under a brief lock. + Server-owned and singular per city, so all viewers converge. The + monotonic progress/thrash marks move **into the tailer** (shared across + viewers, survive reload). +4. **Two non-Huma BFF `/api` endpoints** (loopback-only, no OpenAPI churn): + `GET /api/city/{city}/runs/summary` and + `GET /api/city/{city}/runs/{runId}/detail`, serving the **existing** + `RunSummary` and `FormulaRunDetail` DTOs byte-for-byte. Each layers + session health/census at request time from one loopback `/v0` sessions + read. +5. **The SPA becomes a pure renderer.** Delete ~5,000 LOC of shared run logic + and ~779 LOC of frontend enrich. Keep only render components and DTO + types, plus an import of the generated spec for presentation-only + label/kind lookups. + +### Diagram + +``` + runspec/*.toml (single source of run KNOWLEDGE) + │ go:generate + npm prebuild (CI: regen-and-diff) + ┌─────────────┴──────────────┐ + ▼ ▼ + internal/runspec/spec_gen.go shared/src/runs/spec/spec.gen.ts + │ │ (presentation lookups only) + ▼ │ + .gc/events.jsonl ──ReadFiltered(+.gz)──▶ runproj fold map[beadID]Bead + bead.created/updated/closed/deleted │ (read-only Watch, no 2nd writer) + ├─▶ BuildRunSummary ─┐ + └─▶ BuildRunDetail ──┤ ONE classifier + ▼ + per-city tailer (citySampler-style, warm, server-owned) + │ + GET /api/city/{city}/runs/summary ◀──── cached summary ───┤ + GET /api/city/{city}/runs/{runId}/detail ◀── cached detail ─┘ + │ + request-time loopback /v0 sessions enrich (health/census) + ▼ + RunSummary / FormulaRunDetail DTO (unchanged shape) + │ SSE useGcEventRefresh([bead]) nudge, 10s debounce + ▼ + SPA render components + spec.gen.ts (zero run logic) +``` + +## How the detail diagram is handled (no silent duplication) + +The detail diagram is served by a **second projection in the same tailer +over the same fold reading the same spec** — not duplicated logic. Today the +detail view already consumes the compiled Go formula (`api.formulaDetail`, +which `orderRunNodeGroups` uses for ordering) and already fetches a +server-folded SQL snapshot; only the visual enrichment runs in TS. Under this +ADR, `runproj/BuildRunDetail` calls the **same** `mapRunPhase`/`stageProgress` +as `BuildRunSummary`, so the detail StageLadder and the summary lane's stages +are identical **by construction**, not by two TS call sites that happen to +agree. The graph-layout algorithm (`groups.ts` semantic-id disambiguation, +alias maps, loop instancing; `node-shape.ts` targeting) is genuinely hard to +express as data, so it is ported to Go as **code, single-homed**, not forced +into the spec. The only knowledge either view uses is the generated spec; the +only algorithm is the single Go interpreter pair. + +## Testing strategy + +- **Golden corpus, captured once.** Trimmed real `events.jsonl` slices (e.g. + the daytona-trial-city log → 10 lanes) with expected `RunSummary` and + `FormulaRunDetail` JSON captured from the *current* TS output **before** any + port. The Go interpreters must reproduce them exactly. This preserves the + ~13 encoded `gascity-dashboard-*` bug fixes through the port. +- **Spec-conformance gate.** Regenerate-and-diff test fails the build if + `spec_gen.go` or `spec.gen.ts` diverges from `runspec/*.toml` — turning the + highest-probability drift (formula/stage edits) into a build-time failure. +- **Classifier table/property tests in Go.** Port `phaseMapping.test.ts` + (411 LOC) token rules — whole-token matching, lead-up-qualifier rejection, + approval-before-finalization precedence, furthest-stage `LIFECYCLE_RANK` + determinism — into Go table tests. Property tests: adding a child bead never + changes a run's root; closing all beads ⇒ complete. +- **Wire contract, strengthened.** Upgrade `wire_contract_test.go` from + field-presence to running emitted JSON through the **real TS decoders** in + Vitest, so the DTO (a type with no behavior) cannot drift in shape. +- **Tailer integration test.** Write a temp `events.jsonl`, start the tailer, + assert the snapshot reflects appended events after a tick, and assert a + separate recorder can still append (no second-writer regression). +- **Summary/detail consistency test.** One fixture run resolves to the same + phase/stage through both endpoints — structural, not coincidental. +- **Endpoint degradation test.** Available health when sessions present; + `unavailable` + `lanesPartial` when the sessions read fails. + +## Session-health handling + +Health and census are **not** event-sourced and never status-filed — +consistent with the project rule "no status files — query live state." +`session.woke`/`session.stopped` carry only the session name; `lastActive`, +`running`, and `activity` are live process facts. `runproj.BuildRunSummary` +produces lanes with `health`/`census` in the `unavailable` shell. The +endpoint then layers them at request time: one loopback `/v0/.../sessions` +read (the existing `fetchStatus 127.0.0.1:port` pattern), then ported +`deriveRunHealth` + `buildCensus`. When the sessions read fails, health → +`unavailable`, `phaseConfidence` → `inferred`, census → unverifiable, and the +DTO sets `lanesPartial: true` — identical to today's contract, but decided +server-side so every viewer degrades identically. `needsOperator` stays valid +during a sessions outage because it derives from `lane.phase` (a structural +fold fact). The monotonic thrash/progress marks live server-side in the +tailer, so they are shared and survive reload. + +## Alternatives rejected + +- **ThinServer-FoldOnly** (Go folds events→beads, all run logic stays in TS). + Lowest execution risk and genuinely lowest *grep-enforceable* Go-vs-TS + drift, but it **freezes** the verified compiler-vs-stage-table trap rather + than removing it — disqualifying under an effort-agnostic, + maintainability-first mandate. It is the correct choice only if effort were + the priority, which the operator waived. +- **Full Go port without a spec (SingleSource-Go).** Adopted as the *first + phase* (it is the de-risking deliverable and the strict-subset fallback), + but as the *end state* it relocates the stage tables and kind map into Go + *code* rather than data, so adding a formula edits Go source rather than a + one-line `.toml` — a larger blast radius for the most frequent change. +- **HostedParity-Projection** (embedded SQL engine + `phaseMapping` as a SQL + UDF). Last on two of three lenses: the most new long-lived coupling for the + least drift-reduction, it couples the most-bug-fixed function to an engine + UDF API and smears it across three reasoning sites, and its "diff against + hosted" parity is aspirational because hosted reads ClickHouse, not the + embedded store. + +## Migration path + +Six phases, each ≤5 files, each verifiable and revertable; the fallback +(Phase 4 = single-home-in-Go) is a strict subset of the end state. + +- **Phase 0 — Freeze the contract.** Pin the existing DTO interfaces; + strengthen `wire_contract_test.go` to run emitted JSON through real TS + decoders (Vitest); capture golden fixtures from current TS output. *Gate:* + goldens captured, decoder check green. +- **Phase 1 — Go fold + summary interpreter, no wiring.** `internal/runproj` + with the fold + `BuildRunSummary`, tables as Go literals for now. *Gate:* + Go golden equals captured TS `RunSummary` (modulo health/census). +- **Phase 2 — Tailer + summary endpoint.** `citySampler`-style tailer + (cold replay + read-only `Watch`, server-side thrash marks); `runs/summary` + with request-time sessions enrich. *Gate:* handler + tailer integration + tests, `make dashboard-check`. +- **Phase 3 — Detail interpreter + endpoint, shared projection.** + `BuildRunDetail` calling the same classifier; `runs/{runId}/detail`. *Gate:* + Go golden equals captured TS `FormulaRunDetail`; summary/detail consistency + test. +- **Phase 4 — SPA cutover behind a flag, then delete TS logic.** Repoint + both views to BFF (keep SSE nudge + debounce), shadow-compare, then delete + `shared/src/runs/*` logic + frontend enrich. *Gate:* parity over a soak + window, SPA tests green, manual preview. +- **Phase 5 — Lift knowledge into the spec (additive upgrade).** Author + `runspec/*.toml`, add codegen, refactor Go literals to generated constants, + import generated TS for presentation. *Gate:* regenerate-and-diff CI test; + full golden corpus green. If deferred, the system rests stably at Phase 4. + +## Consequences + +- One canonical implementation of every run rule, consumed by both views and + any future client; AGENTS.md's "object model at the center, API as + projection" invariant is satisfied at the run layer. +- The most frequent real change (add/alter a formula or stage) becomes a + one-line data edit that lands atomically on both sides with a CI gate. +- Largest one-time port and a new codegen ritual; cross-runtime portability + of run *semantics* is sacrificed (the dashboard is always Go-backed). +- Summary first-paint collapses from ~10–38s to a sub-second warm read; + detail stays ~190ms; wire payload is a few KB; cross-viewer consistency is + structural. diff --git a/plans/runs-view-event-sourcing.md b/plans/runs-view-event-sourcing.md new file mode 100644 index 0000000000..5d1fd642be --- /dev/null +++ b/plans/runs-view-event-sourcing.md @@ -0,0 +1,269 @@ +# Event-Sourced Runs View — Design + +## 1. Problem and goal + +The dashboard **Runs** view is slow because there is no server-side run +projection. The SPA reconstructs runs entirely client-side: on every wide +refresh it fans out to four supervisor `/v0` reads — `molecule(all=true)` +(~6.8s over ~340k rows, capped at `MOLECULE_HISTORY_TIMEOUT_MS=3000`), +`formulaFeed` (~10s), one `task(all=true)` per discovered rig, and the core +active `listBeads` — then folds flat bead lists into run lanes via +`buildRunSummary` (`shared/src/runs/summary.ts`) and `enrichRunSummary` +(`frontend/src/supervisor/runSummary.ts`). The `molecule(all=true)` scan +exists *purely to surface historical run roots* that the view already caps at +50. + +The hosted product is fast because it reads ClickHouse, which is fed by +exported events. The OSS-local analog of that ClickHouse is the per-city +append log `.gc/events.jsonl`. **Goal: source the Runs view from +events.jsonl, mirroring the hosted fold, so the expensive bead scans +disappear.** + +## 2. Recommended architecture + +Add a **per-city background run-projection tailer** inside +`internal/api/dashboardbff`, modeled on the existing `citySampler` +(`samplers.go`). It: + +1. Resolves the city's on-disk root via `Deps.Resolver.CityPath(name)` + (already available; this is how `resolveCityPath` works). +2. Opens `/.gc/events.jsonl` **read-only** using the + `transientCityEventProvider` pattern (`cmd/gc/city_registry.go`): reads go + through `events.ReadFiltered`, the watcher through + `recorder.Watch(...)` then `recorder.Close()` — the watcher only needs the + path and holds **no second writer**, so it never contends with the + controller's appender. +3. Does a **one-time cold replay** from cursor zero, folding the latest + `bead.created`/`bead.updated`/`bead.closed`/`bead.deleted` snapshot per + bead id (delete removes), then **live-tails** via `Watch(ctx, lastSeq)`. +4. Rebuilds the bead-derived `RunSummary` (a Go port of `buildRunSummary`) + off the fold and publishes it under a brief lock — identical + off-request-path discipline to `citySampler.refresh`. +5. Serves a new **`GET /api/city/{cityName}/runs/summary`** endpoint that + returns the cached projection, then layers session-dependent health/census + on at request time from the live `/v0/.../sessions` read (the same data the + SPA fetches today). + +``` +.gc/events.jsonl ──(read-only Watch/ReadFiltered)──▶ runProjection (per city, warm) + bead.created/updated/closed/deleted fold: map[beadID]Bead + └▶ buildRunSummary (Go) + │ cached snapshot +GET /api/city/{city}/runs/summary ──▶ snapshot + live /v0/.../sessions enrich + ▼ + RunSummary DTO (unchanged shape) +``` + +### Why the BFF plane, not a new supervisor `/v0` endpoint + +| Consideration | BFF `/api` plane (recommended) | Supervisor `/v0` endpoint | +|---|---|---| +| OpenAPI/Huma contract | **Untouched** — the plane is the documented non-Huma exception | Requires a new Huma op + regenerated `openapi.json` + TS types | +| Upstream alignment (AGENTS.md) | New file in a fork-friendly package; no edit to upstream `internal/api` wire code | Edits upstream-owned wire surface | +| SPA wiring | Same-origin `/api/city/...`, identical to existing run-diff + sampler calls (`cityBase.ts`) | New typed client method | +| Background warm cache | `citySampler` lifecycle (lazy start, Start/Stop) already wired in `supervisor_dashboard.go` | Would need new lifecycle plumbing | +| Reuse of read substrate | `transientCityEventProvider` read-only pattern drops in | Same | + +The BFF plane already serves `POST /api/city/{cityName}/runs/{runId}/diff` +(`rundiff.go`), so a `runs/summary` sibling is idiomatic and the SPA's +`cityPath()` helper already targets `/api/city/:cityName/*`. + +### Why Go-side fold, not ship-events-to-browser + +Folding in TS would require streaming tens of thousands of event lines to +each client on each refresh to reproduce a fold that costs **0.9s / 27MB +once** server-side (measured below). The fold belongs where the log is. Cost: +`buildRunSummary` must be reimplemented in Go and pinned by a parity test +(see Phase 2). + +## 3. RunSummary field → event source mapping + +The fold keeps the latest `beads.Bead` snapshot per id. `buildRunSummary`'s +inputs are bead fields + `gc.*` metadata, **all of which are in the bead.* +payload** (`internal/api/event_payloads.go` `BeadEventPayload` = +`json.Marshal(beads.Bead)`; the controller emits the full bead via +`caching_store_events.go notifyChange`). + +| RunSummary field | Source | In events.jsonl? | +|---|---|---| +| Run grouping (`runRootId`) | `pr_review.*` / `bugflow.*` / `design_review.*` / `gc.root_bead_id` / `gc.kind` / `issue_type==molecule` / `molecule_id` metadata | **Yes** — bead.* payload metadata | +| Run-group promotion (`isRunGroup`) | `gc.formula_contract==graph.v2` / `issue_type==molecule` / `gc.kind==run` / `gc.formula` | **Yes** | +| `lanes` / `historicalLanes` / `blockedLanes` split | folded bead `status` + `gc.phase` (`mapRunPhase`) | **Yes** | +| `totalActive` / `totalHistorical` | counts over folded lanes | **Yes** | +| lane `title` | `pr_review.github_title` / root `title` | **Yes** | +| lane `formula` | `gc.formula` / `resolveRunFormulaIdentity` | **Yes** | +| lane `scope` | `gc.root_store_ref` / `gc.scope_ref` | **Yes** (replaces the `formulaFeed` discovery read) | +| lane `external` | `pr_review.pr_url` / `bugflow.github_issue_url` | **Yes** | +| `statusCounts` / `activeAssignees` | folded bead `status` / `assignee` | **Yes** | +| `updatedAt` / `recentChanges` | bead `updated_at` (present in payload) | **Yes** | +| `stages` / `progress` / `formulaStageResolved` | `gc.step_id` / `gc.step_ref` / `gc.phase` / `gc.attempt` + formula stage tables | **Yes** for the metadata; stage tables are static code | +| `runCounts` | derived from lanes | **Yes** | +| **lane `health`** (phaseConfidence, needsOperator, stuckNode, thrashingDetected, session) | `deriveRunHealth` over the **live sessions list** | **NO — genuine gap** (see below) | +| **`census`** | `buildCensus` over enriched lanes | **NO — depends on health** | +| `thrashing` / progress marks | `advanceProgressMarks` cross-generation state | Server-derivable but currently client-only (open question) | + +### The one genuine gap: session-derived health + +`session.woke`/`session.stopped` events carry only `{subject: session-name}` +(verified in a real log). They do **not** carry `lastActive`, `running`, or +`activity` — the `DashboardSession` fields `deriveRunHealth` needs. Those are +**live process facts**, consistent with the project rule "no status files — +query live state." So health/census **cannot be event-sourced** and must come +from the live `/v0/.../sessions` read. The endpoint layers them on at request +time (cheap; sessions is already the fast read). When sessions is unavailable, +health degrades to `unavailable` and `phaseConfidence` to `inferred`, exactly +as today. + +## 4. Read mechanism, cursor, rotation, cost + +- **Backfill:** on city-first-view, `events.ReadFiltered(path, + Filter{})` (or a `Type`-filtered pass for the four bead types) yields one + chronological stream across any gzip archives; fold to `map[beadID]Bead`, + record `lastSeq`. +- **Live tail:** `Watch(ctx, lastSeq)`; the file watcher polls 250ms, + advances a byte offset, dedupes by seq, and detects rotation by inode change + (resets offset, honors the `events.rotated` anchor). Apply each bead.* event + to the fold, republish the snapshot under a brief lock. +- **Cursor:** the `uint64` Seq. No persistence needed for v1 (replay is + sub-second); a future compacted checkpoint is an open question. +- **Memory/startup cost (measured):** folding the 70MB / **59,165-event** + `my-city/.gc/events.jsonl` took **0.9s wall, 27MB RSS** in Python; Go will + be faster. Distinct beads after fold: 3,546 (a ~16x event→bead ratio). This + is paid **once per city at first view**, then the tail is incremental. The + warm snapshot is the in-memory fold (a few MB of beads) plus the built + `RunSummary`. +- **Lazy + bounded:** start the tailer lazily per city (like `citySampler`), + so cities nobody views cost nothing. + +## 5. Historical question, answered + +**Does events.jsonl retain enough to replace `molecule(all=true)`? Yes, with +one caveat.** + +- **Retention:** rotation is OFF by default (`maxSize=0` unless + `[events.rotation] enabled`); `archive_retain_age` defaults to empty → + reaping is a no-op → **archives kept forever**. `ReadFiltered` walks them + transparently. The log retains run roots indefinitely — *more* than the + SPA's `MAX_HISTORICAL_LANES=50` wire cap. +- **Reconstruction proven on real data:** folding + `/data/projects/daytona-trial-city/.gc/events.jsonl` produced **10 run + lanes** (mol-dog-compactor, mol-dog-backup, …) with correct formula names — + the `gc.root_bead_id`/`gc.kind`/`gc.step_ref` keys are present in real + bead.created metadata. +- **The caveat:** the projection reconstructs exactly the runs whose + lifecycle events are in the log. A city running current code records them + completely → no backfill needed. But runs that occurred **before the city + ever recorded bead events** (e.g. the legacy `my-city` log folded to **0 run + groups** — that workload never ran graph.v2 formulas) are not in the log and + cannot be event-sourced. That history is equally invisible to a fresh + controller; only beads-as-system-of-record has it. +- **Conclusion:** steady-state needs **no** beads backfill. If product + requires deep pre-event history for legacy installs, add an **optional, + flag-gated** one-time `molecule(all=true)` read to seed a historical + checkpoint at cursor zero — not a structural requirement, and not run on + every city. + +## 6. Conceptual alignment with the hosted ClickHouse fold + +The hosted run plane builds runs at query time, not as a materialized view: +- **Run step-structure** (`forgebff Store.RunSteps`): `GROUP BY ref` over + `city_events.events FINAL`, pairing `bead.created`/`bead.closed` (minIf/maxIf + on ts) within `org_id + run_id`. A step = one bead's created/closed pair. +- **Run list/cost** (`manifold list_runs_sql`): `GROUP BY + coalesce(nullIf(run_id,''),agent)` over the spend fact table. + +The OSS local fold mirrors the **same shape**: group by run key, fold bead +lifecycle. Differences to keep documented so the two stay in sync: +- Local **groups by re-derived `runRootId`** from the folded snapshot + metadata (not by envelope `run_id`), because envelope `run_id` is stamped at + record time and an early event can carry `run_id=self` before + `molecule_id`/`workflow_id` was stamped — re-deriving from the latest + snapshot is the correctness-safe key (matches `buildRunSummary`). +- Local has **`bead.updated`** (status transitions); the hosted export + allowlist **excludes** it, so ClickHouse sees only created/closed boundaries. + The local projection is therefore *richer* (real per-lane status), and the + two are intentionally not byte-identical. Cost/token data lives only in the + hosted spend plane and is out of scope locally. +- Keep the run-key precedence and `isRunGroup` rule identical to + `summary.ts` so a future hosted run-list-over-city_events can reuse the same + predicate. + +## 7. Fidelity gaps vs the molecule scan, and SPA degradation + +| Aspect | molecule scan today | event-sourced projection | SPA degrade | +|---|---|---|---| +| Historical depth | beads system-of-record (all roots ever) | log-resident roots (complete for current-code cities) | Pre-event-history runs absent until optional backfill | +| Health/census | live sessions read (client) | live sessions read at request time | `unavailable` when sessions down (same as today) | +| Progress/thrashing marks | per-browser, in-memory | TBD: client-side (v1) or relocated to tailer | Resets on reload if left client-side | +| Dependency edges (parent/child) | full bead read | `ParentID` in payload, but `caching_store_events` can drop deps after removals | Step→root child edges may be incomplete; lane still renders from grouping | +| Freshness | per-refresh fan-out | 250ms tail latency | Snapshot up to one tick stale (negligible) | +| Cold-start | n/a (always re-fetches) | one 0.9s replay at first view | First view slightly slower than a warm cache, far faster than 6.8s scan | + +Degradation contract: a tailer/log error returns the endpoint with +`lanesPartial: true` (existing convention) rather than blanking; the SPA can +keep the `/v0` path as a fallback behind a flag during rollout. + +## 8. Phased implementation plan (≤5 files/phase, each verifiable) + +**Phase 1 — Go fold + buildRunSummary port (no wiring).** +Files: `internal/api/dashboardbff/runprojection.go` (new: fold + run grouping ++ `RunSummary` build), `runprojection_test.go` (new), +`testdata/runs/daytona.jsonl` (new fixture, trimmed from the real log), +optionally a shared `runmodel.go` for the DTO structs. +Verify: golden test folds the fixture and asserts the same lanes/counts the TS +`buildRunSummary` produces for the same beads (capture the TS output once as +the golden). `go test ./internal/api/dashboardbff/ -run RunProjection`. + +**Phase 2 — Parity test against TS.** +Files: `runprojection_parity_test.go` (new), a shared fixture under +`shared/src/runs/__fixtures__/` consumed by both a TS test and the Go test. +Verify: TS `summary.test.ts` and Go parity test build the **same RunSummary** +(modulo health/census) from one fixture. This is the contract that lets the Go +reimplementation track `summary.ts`. + +**Phase 3 — Per-city tailer (read substrate).** +Files: `runprojection_tailer.go` (new: lazy per-city loop, cold replay + +`Watch`, snapshot publish under brief lock, mirroring `citySampler`), +`runprojection_tailer_test.go` (new), small edit to `plane.go` (add the +tailer manager to `Plane`, enable in `Start`, drain in `Stop`). +Verify: test writes a temp `.gc/events.jsonl`, starts the tailer, asserts the +snapshot reflects appended bead events after a tick; asserts no second writer +(file still appendable by a separate recorder). + +**Phase 4 — Endpoint + session enrichment.** +Files: `runs_summary.go` (new: `GET /api/city/{cityName}/runs/summary`, +reads tailer snapshot, fetches live `/v0/.../sessions` over loopback like +`fetchStatus`, applies health/census), `runs_summary_test.go` (new), edit +`plane.go` `registerRoutes` to call `registerRunsSummary`. +Verify: handler test returns the DTO with available health when sessions +present, `unavailable` health + `lanesPartial` when sessions read fails; +`make dashboard-check` for the wire contract. + +**Phase 5 — SPA cutover behind a flag.** +Files: `frontend/src/supervisor/runSummary.ts` (add a BFF fetcher that calls +`/api/city/{city}/runs/summary` via `cityPath()`), `runSummarySubscription.tsx` +(select BFF vs `/v0` path by flag), `runSummary.test.ts` (cover the new +fetcher). +Verify: SPA tests green; manual `npm run preview` shows the Runs view +populated from the BFF path; shadow-compare DTOs from both paths for a period. + +## 9. Risks + +- **Go↔TS drift** in `buildRunSummary`: mitigated by the Phase 2 shared-fixture + parity test; treat `summary.ts` as the spec. +- **Replay growth** on very long-lived cities under keep-forever retention: + 0.9s today, but unbounded; the compacted-checkpoint open question hedges it. +- **Incomplete dependency edges** in some bead.updated payloads + (`updateEventDepsLocked` can drop deps): lanes still render from grouping; + document that step child-edge completeness is best-effort, matching the + existing client behavior. + +--- + +## Locked decisions (operator, 2026-06-28) + +1. **Historical: pure event-sourcing.** Runs = whatever `events.jsonl` contains (complete for any city on current code). No beads backfill; runs predating the city's first recorded bead event are not shown. +2. **Server scope: full server-side, including session enrich.** Go owns the event fold + `buildRunSummary` AND `enrichRunSummary` (per-lane health + census). The BFF reads `/v0/.../sessions` at request time (loopback) to layer session state; the SPA just renders the returned `RunSummary`. +3. **Rollout: direct cutover.** Repoint the SPA's run-summary source to the new endpoint and delete the 4-read `/v0` path, gated by a Go↔TS golden-parity test (Go `RunSummary` == current TS `RunSummary` on shared fixtures). +4. **Restart: cold-replay each supervisor start** (~0.9s) + live-tail. No persisted checkpoint in v1. +5. (defaulted) `progressStateByCity` monotonicity marks move server-side into the tailer (shared across viewers, survive reload) since the fold is now server-owned. From a2be625b91dd63c3e7cdea8c2a09edeb7717b10e Mon Sep 17 00:00:00 2001 From: William Bernting Date: Fri, 3 Jul 2026 16:25:39 +0200 Subject: [PATCH 15/77] feat: add default_sling_targets for multi-target random dispatch (#3670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Operators can now declare multiple default sling targets on a rig so that targetless `gc sling ` distributes new work across a pool of equivalent worker lanes without manual routing — no session restarts or provider mutations required. Add `default_sling_targets = ["rig/worker-a", "rig/worker-b"]` to a rig in `city.toml`. When `gc sling ` resolves the target automatically, one entry is chosen at random (uniform). The existing scalar `default_sling_target` keeps its behaviour; the plural form takes precedence when both are present. Closes gcw-2dd (gas-city-wbern bead). ## Changes - `RigConfig.DefaultSlingTargets []string` (toml: `default_sling_targets`) - `cmdSlingWithJSON`: switch on `len(DefaultSlingTargets) > 0` → `rand.Intn` pick; empty-entry guard returns `target_resolve_failed` immediately; fallback to scalar; error if neither set - `RigListItem`, `StatusRigJSON`: expose `default_sling_targets` in `gc rig list --json` and `gc status --json` for tooling introspection - Config round-trip test; three targeted sling tests (list pick, single entry, empty-entry rejection) - Schema, config reference, CLI help updated ## Test plan - [ ] `go test ./cmd/gc/... -run TestSling` — three new cases cover random pick from list, single-entry list, empty-entry rejection - [ ] `go test ./internal/config/... -run TestConfig` — round-trip for `default_sling_targets` - [ ] Existing sling tests unchanged (explicit target still routes exactly) Co-authored-by: wbern Co-authored-by: Stephanie Jarmak <36544495+sjarmak@users.noreply.github.com> --- cmd/gc/city_status_snapshot.go | 11 +- cmd/gc/cmd_citystatus.go | 11 +- cmd/gc/cmd_rig.go | 53 ++++--- cmd/gc/cmd_sling.go | 19 ++- cmd/gc/cmd_sling_test.go | 158 +++++++++++++++++++ docs/reference/cli.md | 3 +- docs/reference/config.md | 1 + docs/reference/schema/city-schema.json | 7 + docs/reference/schema/city-schema.txt | 7 + internal/beads/bdstore_exec_internal_test.go | 14 +- internal/config/config.go | 6 + internal/config/config_test.go | 26 +++ 12 files changed, 275 insertions(+), 41 deletions(-) diff --git a/cmd/gc/city_status_snapshot.go b/cmd/gc/city_status_snapshot.go index 03fb633668..43c138c339 100644 --- a/cmd/gc/city_status_snapshot.go +++ b/cmd/gc/city_status_snapshot.go @@ -291,11 +291,12 @@ func collectCityStatusSnapshotFromStoreSnapshot( } } snapshot.Rigs = append(snapshot.Rigs, StatusRigJSON{ - Name: r.Name, - Path: r.Path, - Prefix: r.EffectivePrefix(), - Suspended: suspended, - DefaultSlingTarget: r.DefaultSlingTarget, + Name: r.Name, + Path: r.Path, + Prefix: r.EffectivePrefix(), + Suspended: suspended, + DefaultSlingTarget: r.DefaultSlingTarget, + DefaultSlingTargets: r.DefaultSlingTargets, }) } diff --git a/cmd/gc/cmd_citystatus.go b/cmd/gc/cmd_citystatus.go index a8f60f9236..7edae96340 100644 --- a/cmd/gc/cmd_citystatus.go +++ b/cmd/gc/cmd_citystatus.go @@ -71,11 +71,12 @@ type PoolJSON struct { // StatusRigJSON represents a rig in the JSON status output. type StatusRigJSON struct { - Name string `json:"name"` - Path string `json:"path"` - Prefix string `json:"prefix,omitempty"` - Suspended bool `json:"suspended"` - DefaultSlingTarget string `json:"default_sling_target,omitempty"` + Name string `json:"name"` + Path string `json:"path"` + Prefix string `json:"prefix,omitempty"` + Suspended bool `json:"suspended"` + DefaultSlingTarget string `json:"default_sling_target,omitempty"` + DefaultSlingTargets []string `json:"default_sling_targets,omitempty"` } // StatusSummaryJSON is the agent count summary in JSON output. diff --git a/cmd/gc/cmd_rig.go b/cmd/gc/cmd_rig.go index 7ec2e9f76d..65745b0398 100644 --- a/cmd/gc/cmd_rig.go +++ b/cmd/gc/cmd_rig.go @@ -1086,21 +1086,24 @@ func renderRigListFromAPI(fs fsys.FS, cityPath string, cr api.CachedRead[[]api.R prefix := rig.Prefix defaultBranch := rig.DefaultBranch defaultSlingTarget := "" + var defaultSlingTargets []string if cfgRig, ok := rigsByName[rig.Name]; ok { path = cfgRig.Path prefix = cfgRig.EffectivePrefix() defaultBranch = cfgRig.EffectiveDefaultBranch() defaultSlingTarget = cfgRig.DefaultSlingTarget + defaultSlingTargets = cfgRig.DefaultSlingTargets } result.Rigs = append(result.Rigs, RigListItem{ - Name: rig.Name, - Path: path, - Prefix: prefix, - DefaultBranch: defaultBranch, - Suspended: rig.Suspended, - Running: rig.RunningCount > 0, - DefaultSlingTarget: defaultSlingTarget, - Beads: rigBeadsStatus(fs, path), + Name: rig.Name, + Path: path, + Prefix: prefix, + DefaultBranch: defaultBranch, + Suspended: rig.Suspended, + Running: rig.RunningCount > 0, + DefaultSlingTarget: defaultSlingTarget, + DefaultSlingTargets: defaultSlingTargets, + Beads: rigBeadsStatus(fs, path), }) } result.Summary.Total = len(result.Rigs) @@ -1182,14 +1185,15 @@ type RigListItem struct { // Path is the absolute filesystem path to the rig directory, resolved from // city.toml by resolveRigPaths. Always absolute in output, regardless of // the relative form stored in city.toml. - Path string `json:"path"` - Prefix string `json:"prefix"` - DefaultBranch string `json:"default_branch,omitempty"` - HQ bool `json:"hq"` - Suspended bool `json:"suspended"` - Running bool `json:"running"` - DefaultSlingTarget string `json:"default_sling_target,omitempty"` - Beads string `json:"beads"` + Path string `json:"path"` + Prefix string `json:"prefix"` + DefaultBranch string `json:"default_branch,omitempty"` + HQ bool `json:"hq"` + Suspended bool `json:"suspended"` + Running bool `json:"running"` + DefaultSlingTarget string `json:"default_sling_target,omitempty"` + DefaultSlingTargets []string `json:"default_sling_targets,omitempty"` + Beads string `json:"beads"` } type RigListSummary struct { @@ -1253,14 +1257,15 @@ func doRigList(fs fsys.FS, cityPath string, jsonOutput bool, stdout, stderr io.W for i := range cfg.Rigs { running := rigHasRunningAgent(cfg, cfg.Rigs[i].Name, sp) result.Rigs = append(result.Rigs, RigListItem{ - Name: cfg.Rigs[i].Name, - Path: cfg.Rigs[i].Path, - Prefix: cfg.Rigs[i].EffectivePrefix(), - DefaultBranch: cfg.Rigs[i].EffectiveDefaultBranch(), - Suspended: suspNames[cfg.Rigs[i].Name], - Running: running, - DefaultSlingTarget: cfg.Rigs[i].DefaultSlingTarget, - Beads: rigBeadsStatus(fs, cfg.Rigs[i].Path), + Name: cfg.Rigs[i].Name, + Path: cfg.Rigs[i].Path, + Prefix: cfg.Rigs[i].EffectivePrefix(), + DefaultBranch: cfg.Rigs[i].EffectiveDefaultBranch(), + Suspended: suspNames[cfg.Rigs[i].Name], + Running: running, + DefaultSlingTarget: cfg.Rigs[i].DefaultSlingTarget, + DefaultSlingTargets: cfg.Rigs[i].DefaultSlingTargets, + Beads: rigBeadsStatus(fs, cfg.Rigs[i].Path), }) } result.Summary.Total = len(result.Rigs) diff --git a/cmd/gc/cmd_sling.go b/cmd/gc/cmd_sling.go index cad7c3aa15..d80fd486c0 100644 --- a/cmd/gc/cmd_sling.go +++ b/cmd/gc/cmd_sling.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "maps" + "math/rand" "net" "os" "os/exec" @@ -81,7 +82,8 @@ The second argument is a bead ID, a formula name when --formula is set, or arbitrary text (which auto-creates a task bead). When target is omitted, the bead's rig prefix is used to look up the rig's -default_sling_target from config. Requires --formula to have an explicit target. +default_sling_targets (or default_sling_target) from config and one is chosen +at random. Requires --formula to have an explicit target. Inline text also requires an explicit target. With --formula, the formula is instantiated and its root bead is routed to @@ -284,10 +286,19 @@ func cmdSlingWithJSON(args []string, isFormula, doNudge, force bool, title strin if !found { return fail("target_resolve_failed", fmt.Sprintf("gc sling: no rig with prefix %q for bead %s", bp, beadOrFormula)) } - if rig.DefaultSlingTarget == "" { - return fail("target_resolve_failed", fmt.Sprintf("gc sling: rig %q has no default_sling_target", rig.Name)) + switch { + case len(rig.DefaultSlingTargets) > 0: + for _, t := range rig.DefaultSlingTargets { + if t == "" { + return fail("target_resolve_failed", fmt.Sprintf("gc sling: rig %q has an empty entry in default_sling_targets", rig.Name)) + } + } + target = rig.DefaultSlingTargets[rand.Intn(len(rig.DefaultSlingTargets))] //nolint:gosec // random target selection, not security-critical + case rig.DefaultSlingTarget != "": + target = rig.DefaultSlingTarget + default: + return fail("target_resolve_failed", fmt.Sprintf("gc sling: rig %q has no default_sling_target or default_sling_targets", rig.Name)) } - target = rig.DefaultSlingTarget } // Ensure rig paths are absolute before agent/rig context resolution. diff --git a/cmd/gc/cmd_sling_test.go b/cmd/gc/cmd_sling_test.go index 08c227d097..64a66747dd 100644 --- a/cmd/gc/cmd_sling_test.go +++ b/cmd/gc/cmd_sling_test.go @@ -8632,3 +8632,161 @@ func TestSlingStdinWithExtraArg(t *testing.T) { t.Errorf("stderr = %q, want to contain '--stdin requires exactly 1 argument'", stderr.String()) } } + +// setupCmdSlingMultiDefaultTargetsFixture creates a city with two worker agents +// in the "foundations" rig and optionally configures default_sling_targets with +// both. The bead fo-multi-work is pre-seeded in the rig store. +func setupCmdSlingMultiDefaultTargetsFixture(t *testing.T, targets []string) (cityDir, rigDir string) { + t.Helper() + configureIsolatedRuntimeEnv(t) + t.Setenv("GC_BEADS", "file") + + cityDir = t.TempDir() + t.Setenv("GC_CITY", cityDir) + t.Setenv("GC_CITY_PATH", "") + t.Setenv("GC_CITY_ROOT", "") + t.Setenv("GC_RIG", "") + t.Setenv("GC_RIG_ROOT", "") + rigDir = filepath.Join(cityDir, "foundations") + if err := os.MkdirAll(rigDir, 0o755); err != nil { + t.Fatalf("MkdirAll(rig): %v", err) + } + if err := ensureScopedFileStoreLayout(cityDir); err != nil { + t.Fatalf("ensureScopedFileStoreLayout: %v", err) + } + for _, dir := range []string{cityDir, rigDir} { + if err := ensurePersistedScopeLocalFileStore(dir); err != nil { + t.Fatalf("ensurePersistedScopeLocalFileStore(%s): %v", dir, err) + } + } + writeTestFileStoreBeads(t, rigDir, []beads.Bead{{ + ID: "fo-multi-work", + Title: "multi-target work bead", + Type: "task", + Status: "open", + Metadata: map[string]string{}, + }}) + + targetsLine := "" + if len(targets) > 0 { + quoted := make([]string, len(targets)) + for i, tgt := range targets { + quoted[i] = fmt.Sprintf("%q", tgt) + } + targetsLine = "default_sling_targets = [" + strings.Join(quoted, ", ") + "]\n" + } + cityToml := `[workspace] +name = "demo" + +[[rigs]] +name = "foundations" +path = "foundations" +prefix = "fo" +` + targetsLine + ` +[[agent]] +name = "worker-a" +dir = "foundations" + +[[agent]] +name = "worker-b" +dir = "foundations" +` + if err := os.WriteFile(filepath.Join(cityDir, "city.toml"), []byte(cityToml), 0o644); err != nil { + t.Fatalf("WriteFile(city.toml): %v", err) + } + t.Chdir(cityDir) + return cityDir, rigDir +} + +// TestCmdSlingMultiDefaultTargetsPicksFromList verifies that when +// default_sling_targets lists two agents, targetless gc sling routes the bead +// to one of them (the exact pick is random, so we accept either). +func TestCmdSlingMultiDefaultTargetsPicksFromList(t *testing.T) { + cityDir, rigDir := setupCmdSlingMultiDefaultTargetsFixture(t, + []string{"foundations/worker-a", "foundations/worker-b"}, + ) + + var stdout, stderr bytes.Buffer + code := cmdSling( + []string{"fo-multi-work"}, + false, false, false, + "", nil, "", + true, false, false, "", + false, false, false, + "", "", + &stdout, &stderr, + ) + if code != 0 { + t.Fatalf("cmdSling returned %d, want 0; stdout=%s stderr=%s", code, stdout.String(), stderr.String()) + } + + rigStore, err := openStoreAtForCity(rigDir, cityDir) + if err != nil { + t.Fatalf("openStoreAtForCity(rig): %v", err) + } + routed, err := rigStore.Get("fo-multi-work") + if err != nil { + t.Fatalf("rigStore.Get(fo-multi-work): %v", err) + } + got := routed.Metadata["gc.routed_to"] + if got != "foundations/worker-a" && got != "foundations/worker-b" { + t.Fatalf("gc.routed_to = %q, want one of [foundations/worker-a, foundations/worker-b]", got) + } +} + +// TestCmdSlingMultiDefaultTargetsSingleEntry verifies that a single-entry +// default_sling_targets list behaves identically to default_sling_target. +func TestCmdSlingMultiDefaultTargetsSingleEntry(t *testing.T) { + cityDir, rigDir := setupCmdSlingMultiDefaultTargetsFixture(t, + []string{"foundations/worker-a"}, + ) + + var stdout, stderr bytes.Buffer + code := cmdSling( + []string{"fo-multi-work"}, + false, false, false, + "", nil, "", + true, false, false, "", + false, false, false, + "", "", + &stdout, &stderr, + ) + if code != 0 { + t.Fatalf("cmdSling returned %d, want 0; stdout=%s stderr=%s", code, stdout.String(), stderr.String()) + } + + rigStore, err := openStoreAtForCity(rigDir, cityDir) + if err != nil { + t.Fatalf("openStoreAtForCity(rig): %v", err) + } + routed, err := rigStore.Get("fo-multi-work") + if err != nil { + t.Fatalf("rigStore.Get(fo-multi-work): %v", err) + } + if routed.Metadata["gc.routed_to"] != "foundations/worker-a" { + t.Fatalf("gc.routed_to = %q, want foundations/worker-a", routed.Metadata["gc.routed_to"]) + } +} + +// TestCmdSlingMultiDefaultTargetsEmptyEntryRejected verifies that an empty +// string inside default_sling_targets is rejected with a clear error. +func TestCmdSlingMultiDefaultTargetsEmptyEntryRejected(t *testing.T) { + setupCmdSlingMultiDefaultTargetsFixture(t, []string{"foundations/worker-a", ""}) + + var stdout, stderr bytes.Buffer + code := cmdSling( + []string{"fo-multi-work"}, + false, false, false, + "", nil, "", + true, false, false, "", + false, false, false, + "", "", + &stdout, &stderr, + ) + if code == 0 { + t.Fatalf("cmdSling returned 0, want non-zero for empty entry in default_sling_targets") + } + if !strings.Contains(stderr.String(), "empty entry") { + t.Errorf("stderr = %q, want to mention 'empty entry'", stderr.String()) + } +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ff4a1e0cab..0c51701983 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3866,7 +3866,8 @@ The second argument is a bead ID, a formula name when --formula is set, or arbitrary text (which auto-creates a task bead). When target is omitted, the bead's rig prefix is used to look up the rig's -default_sling_target from config. Requires --formula to have an explicit target. +default_sling_targets (or default_sling_target) from config and one is chosen +at random. Requires --formula to have an explicit target. Inline text also requires an explicit target. With --formula, the formula is instantiated and its root bead is routed to diff --git a/docs/reference/config.md b/docs/reference/config.md index 24402a27de..929bb302aa 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -717,6 +717,7 @@ Rig defines an external project registered in the city. | `overrides` | []AgentOverride | | | Overrides are per-agent patches applied after pack expansion. V2 renames this to "patches" for consistency with [[patches.agent]]. Both TOML keys are accepted during migration. | | `patches` | []AgentOverride | | | Patches is the V2 name for rig-level agent overrides. Takes precedence over Overrides if both are set. | | `default_sling_target` | string | | | DefaultSlingTarget is the agent qualified name used when gc sling is invoked with only a bead ID (no explicit target). Resolved via resolveAgentIdentity. Example: "rig/polecat" | +| `default_sling_targets` | []string | | | DefaultSlingTargets is the plural form of DefaultSlingTarget. When set, targetless gc sling picks one entry at random each dispatch. Takes precedence over DefaultSlingTarget when non-empty. Each entry is resolved the same way as DefaultSlingTarget. Example: default_sling_targets = ["rig/polecat-a", "rig/polecat-b"] | | `session_sleep` | SessionSleepConfig | | | SessionSleep overrides workspace-level idle sleep defaults for agents in this rig. | | `dolt_host` | string | | | DoltHost overrides the city-level Dolt host for this rig's beads. Use when the rig's database lives on a different Dolt server (e.g., shared from another city). | | `dolt_port` | string | | | DoltPort overrides the city-level Dolt port for this rig's beads. When set, controller commands (scale_check, work_query) prefix their shell invocations with BEADS_DOLT_SERVER_PORT=<port> so bd connects to the correct server instead of the city-level default. | diff --git a/docs/reference/schema/city-schema.json b/docs/reference/schema/city-schema.json index 3b85b93a7e..855649b8be 100644 --- a/docs/reference/schema/city-schema.json +++ b/docs/reference/schema/city-schema.json @@ -2500,6 +2500,13 @@ "type": "string", "description": "DefaultSlingTarget is the agent qualified name used when gc sling is\ninvoked with only a bead ID (no explicit target). Resolved via\nresolveAgentIdentity. Example: \"rig/polecat\"" }, + "default_sling_targets": { + "items": { + "type": "string" + }, + "type": "array", + "description": "DefaultSlingTargets is the plural form of DefaultSlingTarget.\nWhen set, targetless gc sling picks one entry at random each dispatch.\nTakes precedence over DefaultSlingTarget when non-empty. Each entry is\nresolved the same way as DefaultSlingTarget. Example:\n default_sling_targets = [\"rig/polecat-a\", \"rig/polecat-b\"]" + }, "session_sleep": { "$ref": "#/$defs/SessionSleepConfig", "description": "SessionSleep overrides workspace-level idle sleep defaults for agents in\nthis rig." diff --git a/docs/reference/schema/city-schema.txt b/docs/reference/schema/city-schema.txt index 3b85b93a7e..855649b8be 100644 --- a/docs/reference/schema/city-schema.txt +++ b/docs/reference/schema/city-schema.txt @@ -2500,6 +2500,13 @@ "type": "string", "description": "DefaultSlingTarget is the agent qualified name used when gc sling is\ninvoked with only a bead ID (no explicit target). Resolved via\nresolveAgentIdentity. Example: \"rig/polecat\"" }, + "default_sling_targets": { + "items": { + "type": "string" + }, + "type": "array", + "description": "DefaultSlingTargets is the plural form of DefaultSlingTarget.\nWhen set, targetless gc sling picks one entry at random each dispatch.\nTakes precedence over DefaultSlingTarget when non-empty. Each entry is\nresolved the same way as DefaultSlingTarget. Example:\n default_sling_targets = [\"rig/polecat-a\", \"rig/polecat-b\"]" + }, "session_sleep": { "$ref": "#/$defs/SessionSleepConfig", "description": "SessionSleep overrides workspace-level idle sleep defaults for agents in\nthis rig." diff --git a/internal/beads/bdstore_exec_internal_test.go b/internal/beads/bdstore_exec_internal_test.go index 50f0e00d1c..7d65947ac7 100644 --- a/internal/beads/bdstore_exec_internal_test.go +++ b/internal/beads/bdstore_exec_internal_test.go @@ -178,7 +178,13 @@ func TestExecCommandRunnerStopsBDSlowTimerForFastBDCommand(t *testing.T) { } oldThreshold := bdSlowTelemetryThreshold - bdSlowTelemetryThreshold = 30 * time.Millisecond + // Use a large threshold (5 s) so a trivial shell script reliably + // completes before the timer fires even on a heavily-loaded parallel + // test runner. 30 ms was too tight and caused spurious "bd.slow" fires + // (ga-2dd). The sleep after the call is decoupled from the threshold: + // we only need to drain any in-flight timer goroutine, not wait for the + // timer to expire. 100 ms is ample for the exporter to flush. + bdSlowTelemetryThreshold = 5 * time.Second t.Cleanup(func() { bdSlowTelemetryThreshold = oldThreshold }) exp := installBeadsRecordingLogExporter(t) @@ -191,7 +197,11 @@ printf '[]\n' if _, err := ExecCommandRunner()(t.TempDir(), "bd", "list"); err != nil { t.Fatalf("ExecCommandRunner bd: %v", err) } - time.Sleep(2 * bdSlowTelemetryThreshold) + // After ExecCommandRunner returns, defer slowTimer.Stop() has already + // been called. If Stop returned true the timer was defused; if false it + // fired and the goroutine may still be recording. 100 ms gives that + // goroutine time to complete before we assert. + time.Sleep(100 * time.Millisecond) if got := exp.countByBody("bd.slow"); got != 0 { t.Fatalf("bd.slow records = %d, want 0 for fast bd command", got) } diff --git a/internal/config/config.go b/internal/config/config.go index 7385536275..40b7d40259 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -616,6 +616,12 @@ type Rig struct { // invoked with only a bead ID (no explicit target). Resolved via // resolveAgentIdentity. Example: "rig/polecat" DefaultSlingTarget string `toml:"default_sling_target,omitempty"` + // DefaultSlingTargets is the plural form of DefaultSlingTarget. + // When set, targetless gc sling picks one entry at random each dispatch. + // Takes precedence over DefaultSlingTarget when non-empty. Each entry is + // resolved the same way as DefaultSlingTarget. Example: + // default_sling_targets = ["rig/polecat-a", "rig/polecat-b"] + DefaultSlingTargets []string `toml:"default_sling_targets,omitempty"` // SessionSleep overrides workspace-level idle sleep defaults for agents in // this rig. SessionSleep SessionSleepConfig `toml:"session_sleep,omitempty"` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 718200da60..f248a0389e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5744,6 +5744,32 @@ func TestDefaultSlingTargetRoundTrip(t *testing.T) { } } +func TestDefaultSlingTargetsRoundTrip(t *testing.T) { + c := City{ + Workspace: Workspace{Name: "test"}, + Rigs: []Rig{ + {Name: "hello-world", Path: "/tmp/hw", DefaultSlingTargets: []string{"hello-world/polecat-a", "hello-world/polecat-b"}}, + }, + } + data, err := c.Marshal() + if err != nil { + t.Fatalf("Marshal: %v", err) + } + got, err := Parse(data) + if err != nil { + t.Fatalf("Parse(Marshal output): %v", err) + } + want := []string{"hello-world/polecat-a", "hello-world/polecat-b"} + if len(got.Rigs[0].DefaultSlingTargets) != len(want) { + t.Fatalf("DefaultSlingTargets len = %d, want %d", len(got.Rigs[0].DefaultSlingTargets), len(want)) + } + for i, v := range want { + if got.Rigs[0].DefaultSlingTargets[i] != v { + t.Errorf("DefaultSlingTargets[%d] = %q, want %q", i, got.Rigs[0].DefaultSlingTargets[i], v) + } + } +} + // --------------------------------------------------------------------------- // SessionConfig accessor tests // --------------------------------------------------------------------------- From e152df850aff56cfe3cbbc4884da09065273102e Mon Sep 17 00:00:00 2001 From: Julian Knutsen Date: Fri, 3 Jul 2026 08:33:16 -0700 Subject: [PATCH 16/77] feat(api): pack-CRUD over the city API (gc import family) + importsvc extraction (#3829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes the `gc import` family as city-scoped write routes so a city's packs are manageable through the API (the forge-web Packs lens consumes these). Built TDD + workflow-red-teamed. ## Routes - `POST /v0/city/{cityName}/packs` — add a pack by `{source, name?, version?}` (resolve → lock → install). - `DELETE /v0/city/{cityName}/packs/{name}` — remove a pack import. - `GET /v0/city/{cityName}/packs` — lists the `[imports.]` bindings (the **same namespace** add/remove operate on, not the legacy `[packs]` table). `packResponse` is `{name, source, version}`. ## `internal/importsvc` extraction The `gc import add/remove` orchestration lived in `package main` (unimportable). Extracted into a shared `internal/importsvc` (`AddImport`/`RemoveImport`/`ListImports`, `Deps` injection, typed sentinels). `cmd/gc/cmd_import.go` delegates — **CLI behavior + tests unchanged**, exact-line error parity preserved (`ErrNameDerive`/`ErrReservedPrefix`). The mirrored manifest/scope helpers in `importsvc` vs `cmd/gc` are documented as a known dup to converge. ## Red-team (workflow → adversarial verify → synth) Caught + fixed: the **must-fix** GET-vs-write **namespace mismatch** (GET listed legacy `[packs]` while writes used `[imports]` → a POSTed pack was invisible, DELETE mis-targeted); the install-vs-resolve error status (500 vs 502); CLI add message parity. **SSRF note:** `AddImport` resolves + clones the operator-provided `source` synchronously server-side; the single `git`-fetch point is `internal/importsvc/source.go`. The caller is an authenticated+authorized city owner via write-auth; an egress/source allowlist is a reasonable hardening follow-up. ## Verification `go build` · `vet` · `go test ./internal/importsvc/` (12) · `./internal/api/` (incl. `TestPackListAddRemoveShareNamespace`, `TestOpenAPISpecInSync`, `TestHandlePack*`) · `genclient` (no drift) · `cmd/gc -run Import` · `docsync` — all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- cmd/gc/api_state.go | 15 +- cmd/gc/builtin_include_doctor_check.go | 4 +- cmd/gc/cmd_import.go | 334 +++---------- cmd/gc/cmd_import_status.go | 2 +- cmd/gc/cmd_import_test.go | 98 +++- cmd/gc/cmd_rig.go | 2 +- cmd/gc/import_state_doctor_check.go | 8 +- cmd/gc/init_provider_readiness.go | 2 +- cmd/gc/legacy_pack_preflight.go | 2 +- docs/reference/schema/openapi.json | 220 ++++++++- docs/reference/schema/openapi.txt | 220 ++++++++- internal/api/fake_state_test.go | 17 + internal/api/genclient/client_gen.go | 359 +++++++++++++- internal/api/handler_beads_test.go | 118 ++++- internal/api/handler_packs.go | 12 +- internal/api/handler_packs_write_test.go | 144 ++++++ internal/api/huma_handlers_packs.go | 171 ++++++- internal/api/openapi.json | 220 ++++++++- internal/api/pack_source_policy.go | 237 +++++++++ internal/api/pack_source_policy_test.go | 238 +++++++++ internal/api/state.go | 14 + internal/api/supervisor_city_routes.go | 9 + internal/configedit/configedit.go | 13 + internal/configedit/configedit_test.go | 55 +++ internal/git/git.go | 35 ++ internal/git/git_test.go | 36 ++ internal/importsvc/importsvc.go | 505 +++++++++++++++++++ internal/importsvc/importsvc_test.go | 565 ++++++++++++++++++++++ internal/importsvc/manifest.go | 463 ++++++++++++++++++ internal/importsvc/source.go | 180 +++++++ internal/importsvc/source_test.go | 39 ++ internal/importsvc/testenv_import_test.go | 5 + internal/packman/cache.go | 9 +- internal/packman/cache_test.go | 25 + internal/packman/install.go | 51 +- internal/packman/install_test.go | 82 ++++ test/integration/skill_lifecycle_test.go | 2 +- 37 files changed, 4164 insertions(+), 347 deletions(-) create mode 100644 internal/api/handler_packs_write_test.go create mode 100644 internal/api/pack_source_policy.go create mode 100644 internal/api/pack_source_policy_test.go create mode 100644 internal/importsvc/importsvc.go create mode 100644 internal/importsvc/importsvc_test.go create mode 100644 internal/importsvc/manifest.go create mode 100644 internal/importsvc/source.go create mode 100644 internal/importsvc/source_test.go create mode 100644 internal/importsvc/testenv_import_test.go diff --git a/cmd/gc/api_state.go b/cmd/gc/api_state.go index 753e46ad44..ca78a82ca0 100644 --- a/cmd/gc/api_state.go +++ b/cmd/gc/api_state.go @@ -36,7 +36,8 @@ import ( "github.com/gastownhall/gascity/internal/workspacesvc" ) -// controllerState implements api.State and api.StateMutator. +// controllerState implements api.State, api.StateMutator, and +// api.ConfigWriteSerializer. // Protected by an RWMutex for hot-reload: readers take RLock, // the controller loop takes Lock when updating cfg/sp/stores. type controllerState struct { @@ -1298,6 +1299,18 @@ func (cs *controllerState) DisableOrder(name, rig string) error { }) } +// SerializeConfigWrite runs fn under the same per-city mutation lock the +// configedit.Editor uses for agent/rig/provider/formula edits. The HTTP pack +// import add/remove handlers write pack.toml, packs.lock, and sometimes +// city.toml outside the Editor callback shape, so routing them through this +// shared lock keeps concurrent config writers from interleaving and losing an +// update or desyncing the manifest and lockfile. +func (cs *controllerState) SerializeConfigWrite(fn func() error) error { + return cs.editor.Do(fn) +} + +var _ api.ConfigWriteSerializer = (*controllerState)(nil) + // SuspendAgent writes suspended=true to durable agent config. // Uses configedit.Editor for provenance-aware edit (inline vs discovered vs patch). func (cs *controllerState) SuspendAgent(name string) error { diff --git a/cmd/gc/builtin_include_doctor_check.go b/cmd/gc/builtin_include_doctor_check.go index 6d11c22fa1..4c99a2e047 100644 --- a/cmd/gc/builtin_include_doctor_check.go +++ b/cmd/gc/builtin_include_doctor_check.go @@ -586,7 +586,7 @@ func (c *builtinImportDoctorCheck) Fix(_ *doctor.CheckContext) error { if len(order) == 0 && !changed { return nil } - allImports, err := collectAllImportsFS(fsys.OSFS{}, c.cityPath) + allImports, err := collectAllImportsFS(c.cityPath) if err != nil { return fmt.Errorf("reading declared imports: %w", err) } @@ -615,7 +615,7 @@ func (c *builtinImportDoctorCheck) missingAfterIncludeStrip() []string { if cfg, loadErr := loadCityConfigWithoutBuiltinPackRefresh(c.cityPath, io.Discard); loadErr == nil { return missingRequiredBuiltinImports(fsys.OSFS{}, maskLegacySystemPacksRoutes(cfg, c.cityPath), c.cityPath) } - declared, err := collectAllImportsFS(fsys.OSFS{}, c.cityPath) + declared, err := collectAllImportsFS(c.cityPath) if err != nil { declared = nil } diff --git a/cmd/gc/cmd_import.go b/cmd/gc/cmd_import.go index 65ba6aa732..c1a8ea1fcd 100644 --- a/cmd/gc/cmd_import.go +++ b/cmd/gc/cmd_import.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "net/url" "os" "os/exec" "path/filepath" @@ -18,6 +17,7 @@ import ( "github.com/gastownhall/gascity/internal/config" "github.com/gastownhall/gascity/internal/fsys" "github.com/gastownhall/gascity/internal/git" + "github.com/gastownhall/gascity/internal/importsvc" "github.com/gastownhall/gascity/internal/packman" "github.com/gastownhall/gascity/internal/pricing" "github.com/spf13/cobra" @@ -388,16 +388,20 @@ func findNearestImportRoot(dir string) (string, bool, error) { } } +// KNOWN DUPLICATION (follow-up to converge): importScopeState plus the manifest +// helpers below (loadImportScopeFS, collectAllImportsFS, loadCityPackManifestFS, +// writeCityPackManifest, ...) are mirrored in internal/importsvc, which the +// add/remove path now delegates to. The other gc import subcommands (install / +// check / upgrade / list / why) still use these package-main copies. Keep the +// two copies behavior-equivalent: any change to the pack.toml round-trip rules +// here must be mirrored in internal/importsvc/manifest.go (and vice versa) until +// those subcommands are migrated to delegate as well. type importScopeState struct { imports map[string]config.Import syntheticTag string save func() error } -func (s *importScopeState) syntheticKey(name string) string { - return s.syntheticTag + name -} - func (s *importScopeState) isRootPackScope() bool { return s != nil && s.syntheticTag == "pack:" } @@ -448,7 +452,8 @@ func loadImportScopeFS(fs fsys.FS, cityPath string) (*importScopeState, error) { }, nil } -func collectAllImportsFS(fs fsys.FS, cityPath string) (map[string]config.Import, error) { +func collectAllImportsFS(cityPath string) (map[string]config.Import, error) { + fs := fsys.OSFS{} all := make(map[string]config.Import) packManifest, err := loadCityPackManifestFS(fs, cityPath) @@ -546,18 +551,6 @@ func loadCityRootImportsFS(fs fsys.FS, cityPath string) (map[string]config.Impor return cfg.Imports, nil } -// cityRootImportExistsFS reports whether city.toml's root [imports] table -// defines name. City entries own the effective root import wholesale, so the -// add/remove write paths must consult this before mutating pack.toml. -func cityRootImportExistsFS(fs fsys.FS, cityPath, name string) (bool, error) { - overrides, err := loadCityRootImportsFS(fs, cityPath) - if err != nil { - return false, err - } - _, ok := overrides[name] - return ok, nil -} - func lookupInspectableImport(target string, imports map[string]config.Import) (config.Import, bool) { if imp, ok := imports[target]; ok { return imp, true @@ -606,216 +599,73 @@ func findImportRigIndex(cityPath string, rigs []config.Rig, target string) (int, return -1, "", fmt.Errorf("rig %q not found", target) } -//nolint:unparam // keep fs injectable for parity with the other import helpers and direct tests. -func doImportAdd(fs fsys.FS, cityPath, source, nameOverride, versionFlag string, stdout, stderr io.Writer) int { - scope, err := loadImportScopeFS(fs, cityPath) - if err != nil { - fmt.Fprintf(stderr, "gc import add: %v\n", err) //nolint:errcheck - return 1 +// importSvcDeps builds the importsvc dependency seam from the CLI's package +// vars and the active rig flag. Routing through main's stubbable vars keeps the +// existing command tests (which override syncImports/resolveImportVersion/...) +// driving the shared add/remove code path. +func importSvcDeps() importsvc.Deps { + return importsvc.Deps{ + Rig: strings.TrimSpace(rigFlag), + SyncLock: syncImports, + WriteLockfile: writeImportLockfile, + ResolveVersion: resolveImportVersion, + DefaultConstraint: defaultImportConstraint, + ResolveHeadCommit: resolveImportHeadCommit, } +} - source, gitBacked, err := normalizeImportAddSource(fs, cityPath, source) +//nolint:unparam // keep fs injectable for parity with the other import helpers and direct tests. +func doImportAdd(fs fsys.FS, cityPath, source, nameOverride, versionFlag string, stdout, stderr io.Writer) int { + res, err := importsvc.AddImportWith(fs, cityPath, source, nameOverride, versionFlag, importSvcDeps()) if err != nil { - fmt.Fprintf(stderr, "gc import add %q: %v\n", source, err) //nolint:errcheck - return 1 - } - - name := nameOverride - if name == "" { - name = deriveImportName(source) - } - if name == "" { - fmt.Fprintln(stderr, "gc import add: could not derive import name; use --name") //nolint:errcheck - return 1 - } - if strings.HasPrefix(name, "default-rig:") { - fmt.Fprintf(stderr, "gc import add: import name %q uses reserved prefix \"default-rig:\"\n", name) //nolint:errcheck - return 1 - } - if _, exists := scope.imports[name]; exists { - fmt.Fprintf(stderr, "gc import add: import %q already exists\n", name) //nolint:errcheck - return 1 - } - if scope.isRootPackScope() { - cityOwned, err := cityRootImportExistsFS(fs, cityPath, name) - if err != nil { - fmt.Fprintf(stderr, "gc import add: %v\n", err) //nolint:errcheck - return 1 - } - if cityOwned { - fmt.Fprintf(stderr, "gc import add: import %q is defined by city.toml [imports], which overrides pack.toml; edit city.toml instead\n", name) //nolint:errcheck - return 1 - } - } - - version := versionFlag - if gitBacked { - if hasRepositoryRefInSource(source) { - fmt.Fprintf(stderr, "gc import add %q: embed refs in --version, not in the source URL\n", source) //nolint:errcheck - return 1 - } - if version == "" { - version, err = defaultImportVersionForSource(source) - if err != nil { - fmt.Fprintf(stderr, "gc import add %q: %v\n", source, err) //nolint:errcheck - return 1 - } - } - } else if version != "" { - fmt.Fprintf(stderr, "gc import add %q: --version is only valid for git-backed imports\n", source) //nolint:errcheck + fmt.Fprintln(stderr, importAddErrorLine(source, nameOverride, err)) //nolint:errcheck return 1 } + fmt.Fprintf(stdout, "Added import %q from %s\n", res.Name, res.Source) //nolint:errcheck + return 0 +} - scope.imports[name] = config.Import{ - Source: source, - Version: version, - } - allImports, err := collectAllImportsFS(fs, cityPath) - if err != nil { - fmt.Fprintf(stderr, "gc import add %q: %v\n", source, err) //nolint:errcheck - return 1 - } - allImports[scope.syntheticKey(name)] = scope.imports[name] - lock, err := syncImports(cityPath, allImports, packman.InstallResolveIfNeeded) - if err != nil { - fmt.Fprintf(stderr, "gc import add %q: %v\n", source, err) //nolint:errcheck - return 1 - } - if err := scope.save(); err != nil { - fmt.Fprintf(stderr, "gc import add %q: %v\n", source, err) //nolint:errcheck - return 1 - } - if err := writeImportLockfile(fs, cityPath, lock); err != nil { - fmt.Fprintf(stderr, "gc import add %q: %v\n", source, err) //nolint:errcheck - return 1 +// importAddErrorLine frames the `gc import add` exit-1 stderr line. The +// name-resolution arms (underivable name, reserved prefix) are byte-identical +// to the historical CLI: they print bare, with no source and no sentinel +// wrapper. The scope/ownership and default arms print importsvc's typed error +// verbatim, which now carries the sentinel prefix it wraps (for example +// `import already exists: import "x" already exists` or `invalid import +// source: ...`); this is the intended, clearer post-extraction contract, not +// byte-identical to the pre-extraction line. The exact-match tests in +// cmd_import_test.go pin each arm so the contract cannot drift silently. +func importAddErrorLine(source, nameOverride string, err error) string { + switch { + case errors.Is(err, importsvc.ErrNameDerive): + return "gc import add: could not derive import name; use --name" + case errors.Is(err, importsvc.ErrReservedPrefix): + return fmt.Sprintf("gc import add: import name %q uses reserved prefix \"default-rig:\"", nameOverride) + case errors.Is(err, importsvc.ErrScopeLoad), errors.Is(err, importsvc.ErrImportExists): + return fmt.Sprintf("gc import add: %v", err) + default: + return fmt.Sprintf("gc import add %q: %v", source, err) } - fmt.Fprintf(stdout, "Added import %q from %s\n", name, source) //nolint:errcheck - return 0 } //nolint:unparam // FS seam is intentional for command tests and symmetry with doImportAdd. func doImportRemove(fs fsys.FS, cityPath, name string, stdout, stderr io.Writer) int { - scope, err := loadImportScopeFS(fs, cityPath) + res, err := importsvc.RemoveImportWith(fs, cityPath, name, importSvcDeps()) if err != nil { - fmt.Fprintf(stderr, "gc import remove: %v\n", err) //nolint:errcheck - return 1 - } - if _, exists := scope.imports[name]; !exists { - removed, err := removeCityRootImportFS(fs, cityPath, scope, name) - if err != nil { + // Failures during the lock-sync/save tail carry the name; earlier scope + // and ownership failures keep the bare prefix, mirroring the old CLI. + if errors.Is(err, importsvc.ErrInstallFailed) { + fmt.Fprintf(stderr, "gc import remove %q: %v\n", name, err) //nolint:errcheck + } else { fmt.Fprintf(stderr, "gc import remove: %v\n", err) //nolint:errcheck - return 1 - } - if !removed { - removed, err = removeRootDefaultRigImportFS(fs, cityPath, scope, name) - if err != nil { - fmt.Fprintf(stderr, "gc import remove: %v\n", err) //nolint:errcheck - return 1 - } - } - if !removed { - fmt.Fprintf(stderr, "gc import remove: import %q not found\n", name) //nolint:errcheck - return 1 - } - } else { - if scope.isRootPackScope() { - cityOwned, err := cityRootImportExistsFS(fs, cityPath, name) - if err != nil { - fmt.Fprintf(stderr, "gc import remove: %v\n", err) //nolint:errcheck - return 1 - } - if cityOwned { - fmt.Fprintf(stderr, "gc import remove: import %q is overridden by city.toml [imports]; remove the city.toml entry first\n", name) //nolint:errcheck - return 1 - } } - delete(scope.imports, name) - } - - allImports, err := collectAllImportsFS(fs, cityPath) - if err != nil { - fmt.Fprintf(stderr, "gc import remove %q: %v\n", name, err) //nolint:errcheck - return 1 - } - delete(allImports, scope.syntheticKey(name)) - delete(allImports, "default-rig:"+strings.TrimPrefix(name, "default-rig:")) - lock, err := syncImports(cityPath, allImports, packman.InstallResolveIfNeeded) - if err != nil { - fmt.Fprintf(stderr, "gc import remove %q: %v\n", name, err) //nolint:errcheck - return 1 - } - if err := scope.save(); err != nil { - fmt.Fprintf(stderr, "gc import remove %q: %v\n", name, err) //nolint:errcheck return 1 } - if err := writeImportLockfile(fs, cityPath, lock); err != nil { - fmt.Fprintf(stderr, "gc import remove %q: %v\n", name, err) //nolint:errcheck - return 1 - } - fmt.Fprintf(stdout, "Removed import %q\n", name) //nolint:errcheck + fmt.Fprintf(stdout, "Removed import %q\n", res.Name) //nolint:errcheck return 0 } -// removeCityRootImportFS removes a root import owned by city.toml [imports]. -// City-only root imports are visible in list/why output, so remove must be -// able to delete them; they live in city.toml, so the save is redirected -// there, mirroring removeRootDefaultRigImportFS. -func removeCityRootImportFS(fs fsys.FS, cityPath string, scope *importScopeState, name string) (bool, error) { - if !scope.isRootPackScope() { - return false, nil - } - if _, err := fs.Stat(filepath.Join(cityPath, "city.toml")); err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - cfg, err := loadCityImportManifestFS(fs, cityPath) - if err != nil { - return false, err - } - if _, ok := cfg.Imports[name]; !ok { - return false, nil - } - delete(cfg.Imports, name) - scope.save = func() error { - return writeCityImportManifestFS(fs, cityPath, cfg) - } - return true, nil -} - -func removeRootDefaultRigImportFS(fs fsys.FS, cityPath string, scope *importScopeState, name string) (bool, error) { - if !scope.isRootPackScope() { - return false, nil - } - defaultName := strings.TrimPrefix(name, "default-rig:") - cfg, err := loadCityImportManifestFS(fs, cityPath) - if err != nil { - return false, err - } - if _, ok := cfg.Defaults.Rig.Imports[defaultName]; !ok { - manifest, err := loadCityPackManifestFS(fs, cityPath) - if err != nil { - return false, err - } - if _, ok := manifest.Defaults.Rig.Imports[defaultName]; !ok { - return false, nil - } - delete(manifest.Defaults.Rig.Imports, defaultName) - scope.save = func() error { - return writeCityPackManifest(fs, cityPath, manifest) - } - return true, nil - } - delete(cfg.Defaults.Rig.Imports, defaultName) - scope.save = func() error { - return writeCityImportManifestFS(fs, cityPath, cfg) - } - return true, nil -} - func doImportInstall(cityPath string, stdout, stderr io.Writer) int { - allImports, err := collectAllImportsFS(fsys.OSFS{}, cityPath) + allImports, err := collectAllImportsFS(cityPath) if err != nil { fmt.Fprintf(stderr, "gc import install: %v\n", err) //nolint:errcheck return 1 @@ -840,7 +690,7 @@ func doImportInstall(cityPath string, stdout, stderr io.Writer) int { } func doImportCheck(cityPath string, stdout, stderr io.Writer) int { - allImports, err := collectAllImportsFS(fsys.OSFS{}, cityPath) + allImports, err := collectAllImportsFS(cityPath) if err != nil { fmt.Fprintf(stderr, "gc import check: %v\n", err) //nolint:errcheck return 1 @@ -892,7 +742,7 @@ func doImportUpgrade(cityPath, target string, stdout, stderr io.Writer) int { return 1 } - allImports, collectErr := collectAllImportsFS(fsys.OSFS{}, cityPath) + allImports, collectErr := collectAllImportsFS(cityPath) if collectErr != nil { fmt.Fprintf(stderr, "gc import upgrade: %v\n", collectErr) //nolint:errcheck return 1 @@ -969,7 +819,7 @@ func doImportList(cityPath string, tree bool, stdout, stderr io.Writer) int { return 0 } - allImports, err := collectAllImportsFS(fsys.OSFS{}, cityPath) + allImports, err := collectAllImportsFS(cityPath) if err != nil { fmt.Fprintf(stderr, "gc import list: %v\n", err) //nolint:errcheck return 1 @@ -1511,29 +1361,6 @@ func defaultImportVersionForSource(source string) (string, error) { return "sha:" + commit, nil } -func normalizeImportAddSource(fs fsys.FS, cityPath, source string) (string, bool, error) { - if isRemoteImportSource(source) { - return source, true, nil - } - - targetDir, err := resolveImportAddPath(cityPath, source) - if err != nil { - return "", false, err - } - if err := validateImportPackTarget(fs, targetDir); err != nil { - return "", false, err - } - - canonical, ok, err := canonicalizeLocalGitImportSource(targetDir) - if err != nil { - return "", false, err - } - if ok { - return canonical, true, nil - } - return source, false, nil -} - func resolveImportAddPath(cityPath, source string) (string, error) { switch { case strings.HasPrefix(source, "//"): @@ -1551,45 +1378,6 @@ func resolveImportAddPath(cityPath, source string) (string, error) { } } -func validateImportPackTarget(fs fsys.FS, targetDir string) error { - info, err := fs.Stat(targetDir) - if err != nil { - return fmt.Errorf("resolving source: %w", err) - } - if !info.IsDir() { - return fmt.Errorf("source is not a directory") - } - packPath := filepath.Join(targetDir, "pack.toml") - if _, err := fs.Stat(packPath); err != nil { - return fmt.Errorf("invalid pack target: missing pack.toml") - } - if _, err := config.Load(fs, packPath); err != nil { - return fmt.Errorf("invalid pack target: %w", err) - } - return nil -} - -func canonicalizeLocalGitImportSource(targetDir string) (string, bool, error) { - repoRoot, ok, err := localGitRepoRoot(targetDir) - if err != nil || !ok { - return "", ok, err - } - resolvedTarget, err := filepath.EvalSymlinks(targetDir) - if err != nil { - resolvedTarget = targetDir - } - rel, err := filepath.Rel(repoRoot, resolvedTarget) - if err != nil { - return "", false, fmt.Errorf("computing import subpath: %w", err) - } - u := url.URL{Scheme: "file", Path: filepath.ToSlash(repoRoot)} - canonical := u.String() - if rel != "." { - canonical += "//" + filepath.ToSlash(rel) - } - return canonical, true, nil -} - func localGitRepoRoot(targetDir string) (string, bool, error) { cmd := exec.Command("git", "-C", targetDir, "rev-parse", "--show-toplevel") // Strip git-locating env vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, ...) diff --git a/cmd/gc/cmd_import_status.go b/cmd/gc/cmd_import_status.go index 81c0105eb3..d7513550e0 100644 --- a/cmd/gc/cmd_import_status.go +++ b/cmd/gc/cmd_import_status.go @@ -132,7 +132,7 @@ func doImportStatus(cityPath string, jsonOut bool, stdout, stderr io.Writer) int // buildImportStatus assembles the import status document for cityPath. func buildImportStatus(cityPath string) (*ImportStatusJSON, error) { fs := fsys.OSFS{} - allImports, err := collectAllImportsFS(fs, cityPath) + allImports, err := collectAllImportsFS(cityPath) if err != nil { return nil, err } diff --git a/cmd/gc/cmd_import_test.go b/cmd/gc/cmd_import_test.go index a5e0e261d0..89d753451e 100644 --- a/cmd/gc/cmd_import_test.go +++ b/cmd/gc/cmd_import_test.go @@ -627,7 +627,12 @@ source = "packs/tools" } } -func TestDoImportRemoveRefusesCityOverriddenPackImport(t *testing.T) { +// A city.toml [imports] override owns the effective binding that list surfaces, +// so removing a name defined by BOTH pack.toml and city.toml peels the city +// override (leaving the pack.toml entry declared and effective again) rather +// than refusing — otherwise list would surface a binding remove could never +// delete. +func TestDoImportRemovePeelsCityOverriddenPackImport(t *testing.T) { clearGCEnv(t) dir := t.TempDir() writePackToml(t, dir, `[pack] @@ -647,33 +652,37 @@ source = "packs/tools" prevSync := syncImports t.Cleanup(func() { syncImports = prevSync }) - syncImports = func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { - t.Fatal("syncImports must not run for a refused remove") - return nil, nil + var synced map[string]config.Import + syncImports = func(_ string, imports map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + synced = imports + return &packman.Lockfile{Schema: packman.LockfileSchema, Packs: map[string]packman.LockedPack{}}, nil } var stdout, stderr bytes.Buffer code := doImportRemove(fsys.OSFS{}, dir, "tools", &stdout, &stderr) - if code != 1 { - t.Fatalf("code = %d, want 1; stderr = %s", code, stderr.String()) - } - if !strings.Contains(stderr.String(), "city.toml") { - t.Fatalf("stderr must point at city.toml ownership:\n%s", stderr.String()) + if code != 0 { + t.Fatalf("code = %d, want 0; stderr = %s", code, stderr.String()) } + // The pack.toml entry survives the peel and is effective again... manifest, err := loadCityPackManifestFS(fsys.OSFS{}, dir) if err != nil { t.Fatalf("loadCityPackManifestFS: %v", err) } - if _, ok := manifest.Imports["tools"]; !ok { - t.Fatal("pack.toml imports.tools must survive a refused remove") + if got, ok := manifest.Imports["tools"]; !ok || got.Source != "https://example.com/tools.git" { + t.Fatalf("pack.toml imports.tools = %#v ok=%v; must survive the peel", got, ok) } + // ...and the city.toml override is removed. cfg, err := loadCityImportManifestFS(fsys.OSFS{}, dir) if err != nil { t.Fatalf("loadCityImportManifestFS: %v", err) } - if _, ok := cfg.Imports["tools"]; !ok { - t.Fatal("city.toml imports.tools must survive a refused remove") + if _, ok := cfg.Imports["tools"]; ok { + t.Fatal("city.toml imports.tools override must be peeled off by remove") + } + // Lock sync keeps tools re-pointed to the pack value, not dropped. + if got, ok := synced["pack:tools"]; !ok || got.Source != "https://example.com/tools.git" { + t.Fatalf("synced pack:tools = %#v ok=%v; want the pack binding preserved", got, ok) } } @@ -1397,8 +1406,67 @@ func TestDoImportAddRejectsReservedDefaultRigPrefix(t *testing.T) { if code == 0 { t.Fatal("expected reserved prefix import add to fail") } - if !strings.Contains(stderr.String(), "reserved prefix") { - t.Fatalf("stderr = %q", stderr.String()) + // The historical CLI printed this bare, with no source-quoted prefix and no + // "invalid import source:" wrapper. Pin the exact line to catch drift. + want := "gc import add: import name \"default-rig:worker\" uses reserved prefix \"default-rig:\"\n" + if stderr.String() != want { + t.Fatalf("stderr = %q, want %q", stderr.String(), want) + } +} + +func TestDoImportAddBareMessageWhenNameUnderivable(t *testing.T) { + clearGCEnv(t) + dir := t.TempDir() + writeCityToml(t, dir, "[workspace]\nname = \"demo\"\n") + writePackToml(t, dir, "[pack]\nname = \"demo\"\nschema = 1\n") + + var stdout, stderr bytes.Buffer + // A bare scheme with no path derives to an empty name. + code := doImportAdd(fsys.OSFS{}, dir, "https://", "", "", &stdout, &stderr) + if code == 0 { + t.Fatal("expected underivable name to fail") + } + want := "gc import add: could not derive import name; use --name\n" + if stderr.String() != want { + t.Fatalf("stderr = %q, want %q", stderr.String(), want) + } +} + +// The ErrImportExists arm surfaces importsvc's sentinel prefix verbatim after +// the extraction. Pin the exact line so this blessed (non-byte-identical) +// contract cannot drift silently. +func TestDoImportAddExactLineWhenImportExists(t *testing.T) { + clearGCEnv(t) + dir := t.TempDir() + writeCityToml(t, dir, "[workspace]\nname = \"demo\"\n") + writePackToml(t, dir, "[pack]\nname = \"demo\"\nschema = 1\n\n[imports.tools]\nsource = \"https://example.com/tools.git\"\nversion = \"^1.4\"\n") + + var stdout, stderr bytes.Buffer + code := doImportAdd(fsys.OSFS{}, dir, "https://example.com/tools.git", "", "", &stdout, &stderr) + if code == 0 { + t.Fatal("expected duplicate import add to fail") + } + want := "gc import add: import already exists: import \"tools\" already exists\n" + if stderr.String() != want { + t.Fatalf("stderr = %q, want %q", stderr.String(), want) + } +} + +// The remove ErrNotFound arm likewise surfaces the sentinel prefix verbatim. +func TestDoImportRemoveExactLineWhenNotFound(t *testing.T) { + clearGCEnv(t) + dir := t.TempDir() + writeCityToml(t, dir, "[workspace]\nname = \"demo\"\n") + writePackToml(t, dir, "[pack]\nname = \"demo\"\nschema = 1\n") + + var stdout, stderr bytes.Buffer + code := doImportRemove(fsys.OSFS{}, dir, "ghost", &stdout, &stderr) + if code == 0 { + t.Fatal("expected removing a missing import to fail") + } + want := "gc import remove: import not found: import \"ghost\" not found\n" + if stderr.String() != want { + t.Fatalf("stderr = %q, want %q", stderr.String(), want) } } diff --git a/cmd/gc/cmd_rig.go b/cmd/gc/cmd_rig.go index 65745b0398..2682f43b70 100644 --- a/cmd/gc/cmd_rig.go +++ b/cmd/gc/cmd_rig.go @@ -738,7 +738,7 @@ func ensureBundledRigImportsInstalled(cityPath string, imports []config.BoundImp if len(declared) == 0 { return pinned, nil, nil } - existing, err := collectAllImportsFS(fsys.OSFS{}, cityPath) + existing, err := collectAllImportsFS(cityPath) if err != nil { return nil, nil, err } diff --git a/cmd/gc/import_state_doctor_check.go b/cmd/gc/import_state_doctor_check.go index 1bfc721cb3..335822a8ce 100644 --- a/cmd/gc/import_state_doctor_check.go +++ b/cmd/gc/import_state_doctor_check.go @@ -42,7 +42,7 @@ func (c *importStateDoctorCheck) Name() string { return "packv2-import-state" } func (c *importStateDoctorCheck) Run(_ *doctor.CheckContext) *doctor.CheckResult { r := &doctor.CheckResult{Name: c.Name()} - imports, err := collectAllImportsFS(fsys.OSFS{}, c.cityPath) + imports, err := collectAllImportsFS(c.cityPath) if err != nil { r.Status = doctor.StatusError r.Message = fmt.Sprintf("reading declared imports: %v", err) @@ -109,7 +109,7 @@ func durableRegistryImportDetails(imports map[string]config.Import) []string { func (c *importStateDoctorCheck) CanFix() bool { return true } func (c *importStateDoctorCheck) Fix(_ *doctor.CheckContext) error { - imports, err := collectAllImportsFS(fsys.OSFS{}, c.cityPath) + imports, err := collectAllImportsFS(c.cityPath) if err != nil { return fmt.Errorf("reading declared imports: %w", err) } @@ -124,7 +124,7 @@ func (c *importStateDoctorCheck) Fix(_ *doctor.CheckContext) error { if _, err := rewriteLegacyPublicPackImportsFS(fsys.OSFS{}, c.cityPath, targets); err != nil { return err } - imports, err = collectAllImportsFS(fsys.OSFS{}, c.cityPath) + imports, err = collectAllImportsFS(c.cityPath) if err != nil { return fmt.Errorf("reading migrated imports: %w", err) } @@ -133,7 +133,7 @@ func (c *importStateDoctorCheck) Fix(_ *doctor.CheckContext) error { if err := rewriteSupersededBundledPinsFS(fsys.OSFS{}, c.cityPath); err != nil { return err } - imports, err = collectAllImportsFS(fsys.OSFS{}, c.cityPath) + imports, err = collectAllImportsFS(c.cityPath) if err != nil { return fmt.Errorf("reading re-pinned imports: %w", err) } diff --git a/cmd/gc/init_provider_readiness.go b/cmd/gc/init_provider_readiness.go index 41117b30a2..00aac8102d 100644 --- a/cmd/gc/init_provider_readiness.go +++ b/cmd/gc/init_provider_readiness.go @@ -238,7 +238,7 @@ func runInitProviderPreflightForConfig(cityPath string, cfg *config.City, stdout } func initHasRemoteImports(cityPath string) (bool, error) { - allImports, err := collectAllImportsFS(fsys.OSFS{}, cityPath) + allImports, err := collectAllImportsFS(cityPath) if err != nil { return false, err } diff --git a/cmd/gc/legacy_pack_preflight.go b/cmd/gc/legacy_pack_preflight.go index 25171256b6..728b045407 100644 --- a/cmd/gc/legacy_pack_preflight.go +++ b/cmd/gc/legacy_pack_preflight.go @@ -124,7 +124,7 @@ func lockedBundledImportsUsable(cityPath string) bool { var ensureInitRemoteImportsInstalled = installInitRemoteImports func installInitRemoteImports(cityPath string) error { - allImports, err := collectAllImportsFS(fsys.OSFS{}, cityPath) + allImports, err := collectAllImportsFS(cityPath) if err != nil { return err } diff --git a/docs/reference/schema/openapi.json b/docs/reference/schema/openapi.json index 8eb312b04d..9797adce3c 100644 --- a/docs/reference/schema/openapi.json +++ b/docs/reference/schema/openapi.json @@ -4986,6 +4986,61 @@ ], "type": "object" }, + "PackAddInputBody": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Optional local binding name override; derived from the source when omitted.", + "type": "string" + }, + "source": { + "description": "Pack source: a remote git URL or registry ref (a sub-path of a repo is allowed).", + "examples": [ + "https://github.com/org/repo/tree/main/packs/review" + ], + "minLength": 1, + "type": "string" + }, + "version": { + "description": "Optional semver constraint for a git-backed pack.", + "examples": [ + "^1.2.0" + ], + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, + "PackAddedOutputBody": { + "additionalProperties": false, + "properties": { + "git_backed": { + "description": "Whether the resolved source is git-backed (has a lock entry).", + "type": "boolean" + }, + "name": { + "description": "The local binding name written to [imports.\u003cname\u003e].", + "type": "string" + }, + "source": { + "description": "The canonical source string written to the manifest.", + "type": "string" + }, + "version": { + "description": "The version constraint written, if any.", + "type": "string" + } + }, + "required": [ + "name", + "source", + "git_backed" + ], + "type": "object" + }, "PackListBody": { "additionalProperties": false, "properties": { @@ -5005,19 +5060,29 @@ ], "type": "object" }, - "PackResponse": { + "PackRemovedOutputBody": { "additionalProperties": false, "properties": { "name": { + "description": "The binding name removed.", "type": "string" - }, - "path": { + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PackResponse": { + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, - "ref": { + "source": { "type": "string" }, - "source": { + "version": { "type": "string" } }, @@ -24524,6 +24589,151 @@ } }, "summary": "Get v0 city by city name packs" + }, + "post": { + "description": "Imports a pack into the city by source (a remote git URL or registry ref), resolving + installing it so its templates compose into the city.", + "operationId": "add-pack", + "parameters": [ + { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "in": "header", + "name": "X-GC-Request", + "required": true, + "schema": { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "City name.", + "in": "path", + "name": "cityName", + "required": true, + "schema": { + "description": "City name.", + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackAddInputBody" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackAddedOutputBody" + } + } + }, + "description": "Created", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + }, + "default": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + }, + "description": "Error", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + } + }, + "summary": "Add a pack" + } + }, + "/v0/city/{cityName}/packs/{name}": { + "delete": { + "operationId": "delete-v0-city-by-city-name-packs-by-name", + "parameters": [ + { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "in": "header", + "name": "X-GC-Request", + "required": true, + "schema": { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "City name.", + "in": "path", + "name": "cityName", + "required": true, + "schema": { + "description": "City name.", + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + }, + { + "description": "The import binding name to remove (the [imports.\u003cname\u003e] key).", + "in": "path", + "name": "name", + "required": true, + "schema": { + "description": "The import binding name to remove (the [imports.\u003cname\u003e] key).", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackRemovedOutputBody" + } + } + }, + "description": "OK", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + }, + "default": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + }, + "description": "Error", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + } + }, + "summary": "Delete v0 city by city name packs by name" } }, "/v0/city/{cityName}/patches/agent/{base}": { diff --git a/docs/reference/schema/openapi.txt b/docs/reference/schema/openapi.txt index 8eb312b04d..9797adce3c 100644 --- a/docs/reference/schema/openapi.txt +++ b/docs/reference/schema/openapi.txt @@ -4986,6 +4986,61 @@ ], "type": "object" }, + "PackAddInputBody": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Optional local binding name override; derived from the source when omitted.", + "type": "string" + }, + "source": { + "description": "Pack source: a remote git URL or registry ref (a sub-path of a repo is allowed).", + "examples": [ + "https://github.com/org/repo/tree/main/packs/review" + ], + "minLength": 1, + "type": "string" + }, + "version": { + "description": "Optional semver constraint for a git-backed pack.", + "examples": [ + "^1.2.0" + ], + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, + "PackAddedOutputBody": { + "additionalProperties": false, + "properties": { + "git_backed": { + "description": "Whether the resolved source is git-backed (has a lock entry).", + "type": "boolean" + }, + "name": { + "description": "The local binding name written to [imports.\u003cname\u003e].", + "type": "string" + }, + "source": { + "description": "The canonical source string written to the manifest.", + "type": "string" + }, + "version": { + "description": "The version constraint written, if any.", + "type": "string" + } + }, + "required": [ + "name", + "source", + "git_backed" + ], + "type": "object" + }, "PackListBody": { "additionalProperties": false, "properties": { @@ -5005,19 +5060,29 @@ ], "type": "object" }, - "PackResponse": { + "PackRemovedOutputBody": { "additionalProperties": false, "properties": { "name": { + "description": "The binding name removed.", "type": "string" - }, - "path": { + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PackResponse": { + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, - "ref": { + "source": { "type": "string" }, - "source": { + "version": { "type": "string" } }, @@ -24524,6 +24589,151 @@ } }, "summary": "Get v0 city by city name packs" + }, + "post": { + "description": "Imports a pack into the city by source (a remote git URL or registry ref), resolving + installing it so its templates compose into the city.", + "operationId": "add-pack", + "parameters": [ + { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "in": "header", + "name": "X-GC-Request", + "required": true, + "schema": { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "City name.", + "in": "path", + "name": "cityName", + "required": true, + "schema": { + "description": "City name.", + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackAddInputBody" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackAddedOutputBody" + } + } + }, + "description": "Created", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + }, + "default": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + }, + "description": "Error", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + } + }, + "summary": "Add a pack" + } + }, + "/v0/city/{cityName}/packs/{name}": { + "delete": { + "operationId": "delete-v0-city-by-city-name-packs-by-name", + "parameters": [ + { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "in": "header", + "name": "X-GC-Request", + "required": true, + "schema": { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "City name.", + "in": "path", + "name": "cityName", + "required": true, + "schema": { + "description": "City name.", + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + }, + { + "description": "The import binding name to remove (the [imports.\u003cname\u003e] key).", + "in": "path", + "name": "name", + "required": true, + "schema": { + "description": "The import binding name to remove (the [imports.\u003cname\u003e] key).", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackRemovedOutputBody" + } + } + }, + "description": "OK", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + }, + "default": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + }, + "description": "Error", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + } + }, + "summary": "Delete v0 city by city name packs by name" } }, "/v0/city/{cityName}/patches/agent/{base}": { diff --git a/internal/api/fake_state_test.go b/internal/api/fake_state_test.go index 2685ecbc86..73e35c8069 100644 --- a/internal/api/fake_state_test.go +++ b/internal/api/fake_state_test.go @@ -5,6 +5,8 @@ import ( "io" "net/http" "net/http/httptest" + "sync" + "sync/atomic" "testing" "time" @@ -158,6 +160,12 @@ func (f *fakeState) RawConfig() *config.City { type fakeMutatorState struct { *fakeState suspended map[string]bool + + // serializeMu + serializeCalls make fakeMutatorState a ConfigWriteSerializer + // so pack handler tests exercise the real per-city write-lock seam and can + // assert mutations route through it. + serializeMu sync.Mutex + serializeCalls atomic.Int32 } func newFakeMutatorState(t *testing.T) *fakeMutatorState { @@ -168,6 +176,15 @@ func newFakeMutatorState(t *testing.T) *fakeMutatorState { } } +// SerializeConfigWrite runs fn under a real lock and counts the call, mirroring +// the production controllerState seam that shares the configedit.Editor lock. +func (f *fakeMutatorState) SerializeConfigWrite(fn func() error) error { + f.serializeMu.Lock() + defer f.serializeMu.Unlock() + f.serializeCalls.Add(1) + return fn() +} + func (f *fakeMutatorState) SuspendAgent(name string) error { f.suspended[name] = true; return nil } func (f *fakeMutatorState) ResumeAgent(name string) error { delete(f.suspended, name); return nil } func (f *fakeMutatorState) EnableOrder(name, rig string) error { diff --git a/internal/api/genclient/client_gen.go b/internal/api/genclient/client_gen.go index 33617d5dd3..ffa893ce77 100644 --- a/internal/api/genclient/client_gen.go +++ b/internal/api/genclient/client_gen.go @@ -2132,18 +2132,50 @@ type OutputTurn struct { Timestamp *string `json:"timestamp,omitempty"` } +// PackAddInputBody defines model for PackAddInputBody. +type PackAddInputBody struct { + // Name Optional local binding name override; derived from the source when omitted. + Name *string `json:"name,omitempty"` + + // Source Pack source: a remote git URL or registry ref (a sub-path of a repo is allowed). + Source string `json:"source"` + + // Version Optional semver constraint for a git-backed pack. + Version *string `json:"version,omitempty"` +} + +// PackAddedOutputBody defines model for PackAddedOutputBody. +type PackAddedOutputBody struct { + // GitBacked Whether the resolved source is git-backed (has a lock entry). + GitBacked bool `json:"git_backed"` + + // Name The local binding name written to [imports.]. + Name string `json:"name"` + + // Source The canonical source string written to the manifest. + Source string `json:"source"` + + // Version The version constraint written, if any. + Version *string `json:"version,omitempty"` +} + // PackListBody defines model for PackListBody. type PackListBody struct { // Packs Registered packs. Packs *[]PackResponse `json:"packs"` } +// PackRemovedOutputBody defines model for PackRemovedOutputBody. +type PackRemovedOutputBody struct { + // Name The binding name removed. + Name string `json:"name"` +} + // PackResponse defines model for PackResponse. type PackResponse struct { - Name string `json:"name"` - Path *string `json:"path,omitempty"` - Ref *string `json:"ref,omitempty"` - Source *string `json:"source,omitempty"` + Name string `json:"name"` + Source *string `json:"source,omitempty"` + Version *string `json:"version,omitempty"` } // PaginationInfo defines model for PaginationInfo. @@ -6390,6 +6422,18 @@ type GetV0CityByCityNameOrdersHistoryParams struct { Before *string `form:"before,omitempty" json:"before,omitempty"` } +// AddPackParams defines parameters for AddPack. +type AddPackParams struct { + // XGCRequest Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks. + XGCRequest string `json:"X-GC-Request"` +} + +// DeleteV0CityByCityNamePacksByNameParams defines parameters for DeleteV0CityByCityNamePacksByName. +type DeleteV0CityByCityNamePacksByNameParams struct { + // XGCRequest Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks. + XGCRequest string `json:"X-GC-Request"` +} + // DeleteV0CityByCityNamePatchesAgentByBaseParams defines parameters for DeleteV0CityByCityNamePatchesAgentByBase. type DeleteV0CityByCityNamePatchesAgentByBaseParams struct { // XGCRequest Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks. @@ -6807,6 +6851,9 @@ type SendMailJSONRequestBody = MailSendInputBody // ReplyMailJSONRequestBody defines body for ReplyMail for application/json ContentType. type ReplyMailJSONRequestBody = MailReplyInputBody +// AddPackJSONRequestBody defines body for AddPack for application/json ContentType. +type AddPackJSONRequestBody = PackAddInputBody + // PutV0CityByCityNamePatchesAgentsJSONRequestBody defines body for PutV0CityByCityNamePatchesAgents for application/json ContentType. type PutV0CityByCityNamePatchesAgentsJSONRequestBody = AgentPatchSetInputBody @@ -12660,6 +12707,14 @@ type ClientInterface interface { // GetV0CityByCityNamePacks request GetV0CityByCityNamePacks(ctx context.Context, cityName string, reqEditors ...RequestEditorFn) (*http.Response, error) + // AddPackWithBody request with any body + AddPackWithBody(ctx context.Context, cityName string, params *AddPackParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + AddPack(ctx context.Context, cityName string, params *AddPackParams, body AddPackJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteV0CityByCityNamePacksByName request + DeleteV0CityByCityNamePacksByName(ctx context.Context, cityName string, name string, params *DeleteV0CityByCityNamePacksByNameParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeleteV0CityByCityNamePatchesAgentByBase request DeleteV0CityByCityNamePatchesAgentByBase(ctx context.Context, cityName string, base string, params *DeleteV0CityByCityNamePatchesAgentByBaseParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -14299,6 +14354,42 @@ func (c *Client) GetV0CityByCityNamePacks(ctx context.Context, cityName string, return c.Client.Do(req) } +func (c *Client) AddPackWithBody(ctx context.Context, cityName string, params *AddPackParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddPackRequestWithBody(c.Server, cityName, params, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) AddPack(ctx context.Context, cityName string, params *AddPackParams, body AddPackJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddPackRequest(c.Server, cityName, params, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteV0CityByCityNamePacksByName(ctx context.Context, cityName string, name string, params *DeleteV0CityByCityNamePacksByNameParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteV0CityByCityNamePacksByNameRequest(c.Server, cityName, name, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DeleteV0CityByCityNamePatchesAgentByBase(ctx context.Context, cityName string, base string, params *DeleteV0CityByCityNamePatchesAgentByBaseParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDeleteV0CityByCityNamePatchesAgentByBaseRequest(c.Server, cityName, base, params) if err != nil { @@ -21293,6 +21384,120 @@ func NewGetV0CityByCityNamePacksRequest(server string, cityName string) (*http.R return req, nil } +// NewAddPackRequest calls the generic AddPack builder with application/json body +func NewAddPackRequest(server string, cityName string, params *AddPackParams, body AddPackJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewAddPackRequestWithBody(server, cityName, params, "application/json", bodyReader) +} + +// NewAddPackRequestWithBody generates requests for AddPack with any type of body +func NewAddPackRequestWithBody(server string, cityName string, params *AddPackParams, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "cityName", cityName, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/city/%s/packs", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + if params != nil { + + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "X-GC-Request", params.XGCRequest, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + req.Header.Set("X-GC-Request", headerParam0) + + } + + return req, nil +} + +// NewDeleteV0CityByCityNamePacksByNameRequest generates requests for DeleteV0CityByCityNamePacksByName +func NewDeleteV0CityByCityNamePacksByNameRequest(server string, cityName string, name string, params *DeleteV0CityByCityNamePacksByNameParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "cityName", cityName, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/city/%s/packs/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "X-GC-Request", params.XGCRequest, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + req.Header.Set("X-GC-Request", headerParam0) + + } + + return req, nil +} + // NewDeleteV0CityByCityNamePatchesAgentByBaseRequest generates requests for DeleteV0CityByCityNamePatchesAgentByBase func NewDeleteV0CityByCityNamePatchesAgentByBaseRequest(server string, cityName string, base string, params *DeleteV0CityByCityNamePatchesAgentByBaseParams) (*http.Request, error) { var err error @@ -25263,6 +25468,14 @@ type ClientWithResponsesInterface interface { // GetV0CityByCityNamePacksWithResponse request GetV0CityByCityNamePacksWithResponse(ctx context.Context, cityName string, reqEditors ...RequestEditorFn) (*GetV0CityByCityNamePacksResponse, error) + // AddPackWithBodyWithResponse request with any body + AddPackWithBodyWithResponse(ctx context.Context, cityName string, params *AddPackParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*AddPackResponse, error) + + AddPackWithResponse(ctx context.Context, cityName string, params *AddPackParams, body AddPackJSONRequestBody, reqEditors ...RequestEditorFn) (*AddPackResponse, error) + + // DeleteV0CityByCityNamePacksByNameWithResponse request + DeleteV0CityByCityNamePacksByNameWithResponse(ctx context.Context, cityName string, name string, params *DeleteV0CityByCityNamePacksByNameParams, reqEditors ...RequestEditorFn) (*DeleteV0CityByCityNamePacksByNameResponse, error) + // DeleteV0CityByCityNamePatchesAgentByBaseWithResponse request DeleteV0CityByCityNamePatchesAgentByBaseWithResponse(ctx context.Context, cityName string, base string, params *DeleteV0CityByCityNamePatchesAgentByBaseParams, reqEditors ...RequestEditorFn) (*DeleteV0CityByCityNamePatchesAgentByBaseResponse, error) @@ -27610,6 +27823,52 @@ func (r GetV0CityByCityNamePacksResponse) StatusCode() int { return 0 } +type AddPackResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *PackAddedOutputBody + ApplicationproblemJSONDefault *ErrorModel +} + +// Status returns HTTPResponse.Status +func (r AddPackResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r AddPackResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteV0CityByCityNamePacksByNameResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PackRemovedOutputBody + ApplicationproblemJSONDefault *ErrorModel +} + +// Status returns HTTPResponse.Status +func (r DeleteV0CityByCityNamePacksByNameResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteV0CityByCityNamePacksByNameResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DeleteV0CityByCityNamePatchesAgentByBaseResponse struct { Body []byte HTTPResponse *http.Response @@ -30033,6 +30292,32 @@ func (c *ClientWithResponses) GetV0CityByCityNamePacksWithResponse(ctx context.C return ParseGetV0CityByCityNamePacksResponse(rsp) } +// AddPackWithBodyWithResponse request with arbitrary body returning *AddPackResponse +func (c *ClientWithResponses) AddPackWithBodyWithResponse(ctx context.Context, cityName string, params *AddPackParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*AddPackResponse, error) { + rsp, err := c.AddPackWithBody(ctx, cityName, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseAddPackResponse(rsp) +} + +func (c *ClientWithResponses) AddPackWithResponse(ctx context.Context, cityName string, params *AddPackParams, body AddPackJSONRequestBody, reqEditors ...RequestEditorFn) (*AddPackResponse, error) { + rsp, err := c.AddPack(ctx, cityName, params, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseAddPackResponse(rsp) +} + +// DeleteV0CityByCityNamePacksByNameWithResponse request returning *DeleteV0CityByCityNamePacksByNameResponse +func (c *ClientWithResponses) DeleteV0CityByCityNamePacksByNameWithResponse(ctx context.Context, cityName string, name string, params *DeleteV0CityByCityNamePacksByNameParams, reqEditors ...RequestEditorFn) (*DeleteV0CityByCityNamePacksByNameResponse, error) { + rsp, err := c.DeleteV0CityByCityNamePacksByName(ctx, cityName, name, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteV0CityByCityNamePacksByNameResponse(rsp) +} + // DeleteV0CityByCityNamePatchesAgentByBaseWithResponse request returning *DeleteV0CityByCityNamePatchesAgentByBaseResponse func (c *ClientWithResponses) DeleteV0CityByCityNamePatchesAgentByBaseWithResponse(ctx context.Context, cityName string, base string, params *DeleteV0CityByCityNamePatchesAgentByBaseParams, reqEditors ...RequestEditorFn) (*DeleteV0CityByCityNamePatchesAgentByBaseResponse, error) { rsp, err := c.DeleteV0CityByCityNamePatchesAgentByBase(ctx, cityName, base, params, reqEditors...) @@ -33741,6 +34026,72 @@ func ParseGetV0CityByCityNamePacksResponse(rsp *http.Response) (*GetV0CityByCity return response, nil } +// ParseAddPackResponse parses an HTTP response from a AddPackWithResponse call +func ParseAddPackResponse(rsp *http.Response) (*AddPackResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &AddPackResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest PackAddedOutputBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest ErrorModel + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSONDefault = &dest + + } + + return response, nil +} + +// ParseDeleteV0CityByCityNamePacksByNameResponse parses an HTTP response from a DeleteV0CityByCityNamePacksByNameWithResponse call +func ParseDeleteV0CityByCityNamePacksByNameResponse(rsp *http.Response) (*DeleteV0CityByCityNamePacksByNameResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteV0CityByCityNamePacksByNameResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PackRemovedOutputBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest ErrorModel + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSONDefault = &dest + + } + + return response, nil +} + // ParseDeleteV0CityByCityNamePatchesAgentByBaseResponse parses an HTTP response from a DeleteV0CityByCityNamePatchesAgentByBaseWithResponse call func ParseDeleteV0CityByCityNamePatchesAgentByBaseResponse(rsp *http.Response) (*DeleteV0CityByCityNamePatchesAgentByBaseResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/internal/api/handler_beads_test.go b/internal/api/handler_beads_test.go index 15ad53710f..ae1b1a581f 100644 --- a/internal/api/handler_beads_test.go +++ b/internal/api/handler_beads_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "io" "net/http" "net/http/httptest" "os" @@ -15,6 +16,8 @@ import ( "github.com/gastownhall/gascity/internal/beads" "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/importsvc" "github.com/gastownhall/gascity/internal/session" ) @@ -2152,15 +2155,20 @@ func TestBeadUpdateParentOpenAPISchemaAllowsNull(t *testing.T) { } } +// GET /packs lists the import-binding namespace (the same scope add/remove +// operate on), via the packListImports seam, so the list shape matches the +// forge-web {name, source, version} contract. func TestPackList(t *testing.T) { - state := newFakeState(t) - state.cfg.Packs = map[string]config.PackSource{ - "gastown": { - Source: "https://github.com/example/gastown-pack", - Ref: "v1.0.0", - Path: "packs/gastown", - }, + orig := packListImports + packListImports = func(_ fsys.FS, _ string) (map[string]config.Import, error) { + return map[string]config.Import{ + "gastown": {Source: "https://github.com/example/gastown-pack", Version: "^1.0.0"}, + "local": {Source: "../packs/local"}, + }, nil } + defer func() { packListImports = orig }() + + state := newFakeState(t) h := newTestCityHandler(t, state) req := httptest.NewRequest("GET", cityURL(state, "/packs"), nil) @@ -2175,18 +2183,31 @@ func TestPackList(t *testing.T) { Packs []packResponse `json:"packs"` } json.NewDecoder(rec.Body).Decode(&resp) //nolint:errcheck - if len(resp.Packs) != 1 { - t.Fatalf("packs count = %d, want 1", len(resp.Packs)) + if len(resp.Packs) != 2 { + t.Fatalf("packs count = %d, want 2: %#v", len(resp.Packs), resp.Packs) } + // Bindings are returned sorted by name. if resp.Packs[0].Name != "gastown" { - t.Errorf("Name = %q, want %q", resp.Packs[0].Name, "gastown") + t.Errorf("Packs[0].Name = %q, want gastown", resp.Packs[0].Name) } if resp.Packs[0].Source != "https://github.com/example/gastown-pack" { - t.Errorf("Source = %q", resp.Packs[0].Source) + t.Errorf("Packs[0].Source = %q", resp.Packs[0].Source) + } + if resp.Packs[0].Version != "^1.0.0" { + t.Errorf("Packs[0].Version = %q, want ^1.0.0", resp.Packs[0].Version) + } + if resp.Packs[1].Name != "local" || resp.Packs[1].Version != "" { + t.Errorf("Packs[1] = %#v, want local with empty version", resp.Packs[1]) } } func TestPackListEmpty(t *testing.T) { + orig := packListImports + packListImports = func(_ fsys.FS, _ string) (map[string]config.Import, error) { + return map[string]config.Import{}, nil + } + defer func() { packListImports = orig }() + state := newFakeState(t) h := newTestCityHandler(t, state) @@ -2207,6 +2228,81 @@ func TestPackListEmpty(t *testing.T) { } } +// TestPackListAddRemoveShareNamespace is the regression for the red-team +// MUST-FIX: a binding surfaced by add must be listable by GET and removable by +// DELETE {name}. All three handlers are stubbed at the importsvc seam, and the +// stub's in-memory binding store is the single namespace they share. +func TestPackListAddRemoveShareNamespace(t *testing.T) { + bindings := map[string]config.Import{} + + origList, origAdd, origRemove := packListImports, packAddImport, packRemoveImport + packListImports = func(_ fsys.FS, _ string) (map[string]config.Import, error) { + out := make(map[string]config.Import, len(bindings)) + for k, v := range bindings { + out[k] = v + } + return out, nil + } + packAddImport = func(_ fsys.FS, _, source, name, version string) (*importsvc.AddResult, error) { + if name == "" { + name = "review" + } + bindings[name] = config.Import{Source: source, Version: version} + return &importsvc.AddResult{Name: name, Source: source, Version: version, GitBacked: true}, nil + } + packRemoveImport = func(_ fsys.FS, _, name string) (*importsvc.RemoveResult, error) { + if _, ok := bindings[name]; !ok { + return nil, importsvc.ErrNotFound + } + delete(bindings, name) + return &importsvc.RemoveResult{Name: name}, nil + } + defer func() { + packListImports, packAddImport, packRemoveImport = origList, origAdd, origRemove + }() + + state := newFakeMutatorState(t) + h := newTestCityHandler(t, state) + + doReq := func(method, path, body string) *httptest.ResponseRecorder { + var rdr io.Reader + if body != "" { + rdr = strings.NewReader(body) + } + req := httptest.NewRequest(method, cityURL(state, path), rdr) + req.Header.Set("X-GC-Request", "true") + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + return rec + } + + // POST a pack -> it must appear in the GET listing by the same binding name. + if rec := doReq("POST", "/packs", `{"source":"https://github.com/org/repo/tree/main/packs/review"}`); rec.Code != http.StatusCreated { + t.Fatalf("POST status = %d, want 201; body = %s", rec.Code, rec.Body.String()) + } + rec := doReq("GET", "/packs", "") + if rec.Code != http.StatusOK { + t.Fatalf("GET status = %d, want 200", rec.Code) + } + var resp struct { + Packs []packResponse `json:"packs"` + } + json.NewDecoder(rec.Body).Decode(&resp) //nolint:errcheck + if len(resp.Packs) != 1 || resp.Packs[0].Name != "review" { + t.Fatalf("GET after POST = %#v, want one binding named review", resp.Packs) + } + + // DELETE by that listed name -> the GET listing is empty again. + if rec := doReq("DELETE", "/packs/review", ""); rec.Code != http.StatusOK { + t.Fatalf("DELETE status = %d, want 200; body = %s", rec.Code, rec.Body.String()) + } + rec = doReq("GET", "/packs", "") + json.NewDecoder(rec.Body).Decode(&resp) //nolint:errcheck + if len(resp.Packs) != 0 { + t.Fatalf("GET after DELETE = %#v, want empty", resp.Packs) + } +} + func TestBeadPrefixAPI(t *testing.T) { tests := []struct { id string diff --git a/internal/api/handler_packs.go b/internal/api/handler_packs.go index 03eb3c72e7..284a85c0c3 100644 --- a/internal/api/handler_packs.go +++ b/internal/api/handler_packs.go @@ -1,8 +1,12 @@ package api +// packResponse is the per-binding shape returned by GET /v0/city/{cityName}/packs. +// It mirrors the import model the add/remove handlers operate on — a binding +// name plus its durable source and optional version constraint — so what a +// client lists is exactly what it can add and remove (the forge-web UI contract +// is {name, source, version}). type packResponse struct { - Name string `json:"name"` - Source string `json:"source,omitempty"` - Ref string `json:"ref,omitempty"` - Path string `json:"path,omitempty"` + Name string `json:"name"` + Source string `json:"source,omitempty"` + Version string `json:"version,omitempty"` } diff --git a/internal/api/handler_packs_write_test.go b/internal/api/handler_packs_write_test.go new file mode 100644 index 0000000000..21f2746ca4 --- /dev/null +++ b/internal/api/handler_packs_write_test.go @@ -0,0 +1,144 @@ +package api + +import ( + "net" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/importsvc" +) + +// The pack write handlers delegate to importsvc and only map its typed errors to +// HTTP, so the seams are stubbed here — no real source resolve / clone happens. + +func TestHandlePackAdd(t *testing.T) { + for _, tc := range []struct { + name string + add func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) + want int + }{ + {"created", func(_ fsys.FS, _, source, _, version string) (*importsvc.AddResult, error) { + return &importsvc.AddResult{Name: "review", Source: source, Version: version, GitBacked: true}, nil + }, http.StatusCreated}, + {"already imported -> 409", func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return nil, importsvc.ErrImportExists + }, http.StatusConflict}, + {"invalid source -> 400", func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return nil, importsvc.ErrInvalidSource + }, http.StatusBadRequest}, + {"name derive failed -> 400", func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return nil, importsvc.ErrNameDerive + }, http.StatusBadRequest}, + {"reserved prefix -> 400", func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return nil, importsvc.ErrReservedPrefix + }, http.StatusBadRequest}, + {"version resolve failed -> 502", func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return nil, importsvc.ErrVersionResolveFailed + }, http.StatusBadGateway}, + {"install failed -> 500", func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return nil, importsvc.ErrInstallFailed + }, http.StatusInternalServerError}, + } { + t.Run(tc.name, func(t *testing.T) { + orig := packAddImport + packAddImport = tc.add + defer func() { packAddImport = orig }() + + fs := newFakeMutatorState(t) + h := newTestCityHandler(t, fs) + req := httptest.NewRequest("POST", cityURL(fs, "/packs"), + strings.NewReader(`{"source":"https://github.com/org/repo/tree/main/packs/review"}`)) + req.Header.Set("X-GC-Request", "true") + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + + if w.Code != tc.want { + t.Fatalf("status = %d, want %d; body = %s", w.Code, tc.want, w.Body.String()) + } + if tc.want == http.StatusCreated && !strings.Contains(w.Body.String(), `"review"`) { + t.Errorf("created body missing binding name: %s", w.Body.String()) + } + }) + } +} + +func TestHandlePackRemove(t *testing.T) { + for _, tc := range []struct { + name string + remove func(fsys.FS, string, string) (*importsvc.RemoveResult, error) + want int + }{ + {"ok", func(_ fsys.FS, _, name string) (*importsvc.RemoveResult, error) { + return &importsvc.RemoveResult{Name: name}, nil + }, http.StatusOK}, + {"not found -> 404", func(fsys.FS, string, string) (*importsvc.RemoveResult, error) { + return nil, importsvc.ErrNotFound + }, http.StatusNotFound}, + } { + t.Run(tc.name, func(t *testing.T) { + orig := packRemoveImport + packRemoveImport = tc.remove + defer func() { packRemoveImport = orig }() + + fs := newFakeMutatorState(t) + h := newTestCityHandler(t, fs) + req := httptest.NewRequest("DELETE", cityURL(fs, "/packs/review"), nil) + req.Header.Set("X-GC-Request", "true") + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + + if w.Code != tc.want { + t.Fatalf("status = %d, want %d; body = %s", w.Code, tc.want, w.Body.String()) + } + }) + } +} + +// TestPackAddRemoveSerializeThroughConfigWriteLock is the regression for the +// concurrency finding: the pack add/remove handlers must route their mutation +// through the per-city config write lock (ConfigWriteSerializer), so they can +// not interleave with each other or with configedit mutations of the same city. +func TestPackAddRemoveSerializeThroughConfigWriteLock(t *testing.T) { + restore := stubPackSourceResolver(t, map[string][]net.IP{ + "github.com": {net.ParseIP("140.82.112.3")}, + }) + defer restore() + + origAdd, origRemove := packAddImport, packRemoveImport + packAddImport = func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + return &importsvc.AddResult{Name: "review", Source: "https://github.com/org/repo", GitBacked: true}, nil + } + packRemoveImport = func(fsys.FS, string, string) (*importsvc.RemoveResult, error) { + return &importsvc.RemoveResult{Name: "review"}, nil + } + defer func() { packAddImport, packRemoveImport = origAdd, origRemove }() + + state := newFakeMutatorState(t) + h := newTestCityHandler(t, state) + + addReq := httptest.NewRequest("POST", cityURL(state, "/packs"), + strings.NewReader(`{"source":"https://github.com/org/repo/tree/main/packs/review"}`)) + addReq.Header.Set("X-GC-Request", "true") + addRec := httptest.NewRecorder() + h.ServeHTTP(addRec, addReq) + if addRec.Code != http.StatusCreated { + t.Fatalf("add status = %d, want %d; body=%s", addRec.Code, http.StatusCreated, addRec.Body.String()) + } + if got := state.serializeCalls.Load(); got != 1 { + t.Fatalf("add routed through config write lock %d times, want 1", got) + } + + delReq := httptest.NewRequest("DELETE", cityURL(state, "/packs/review"), nil) + delReq.Header.Set("X-GC-Request", "true") + delRec := httptest.NewRecorder() + h.ServeHTTP(delRec, delReq) + if delRec.Code != http.StatusOK { + t.Fatalf("remove status = %d, want %d; body=%s", delRec.Code, http.StatusOK, delRec.Body.String()) + } + if got := state.serializeCalls.Load(); got != 2 { + t.Fatalf("remove routed through config write lock; total calls = %d, want 2", got) + } +} diff --git a/internal/api/huma_handlers_packs.go b/internal/api/huma_handlers_packs.go index f5ad9d17a2..8a1f924baf 100644 --- a/internal/api/huma_handlers_packs.go +++ b/internal/api/huma_handlers_packs.go @@ -2,9 +2,39 @@ package api import ( "context" + "errors" "sort" + + "github.com/danielgtaylor/huma/v2" + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/importsvc" +) + +// Seams over importsvc so handler tests can drive list/add/remove without a real +// network fetch (the same injection style cmd/gc uses for its import tests). +var ( + packListImports = importsvc.ListImports + packAddImport = packAddImportFenced + packRemoveImport = packRemoveImportFenced ) +// packAddImportFenced is the default add seam. It threads validateHTTPPackSource +// into importsvc as the untrusted-source policy so the SSRF fence covers not just +// the caller-supplied source (also pre-checked in humaHandlePackAdd) but every +// transitive import packman resolves during lock sync — a nested internal, file, +// or link-local import in an accepted public pack's pack.toml is rejected before +// its git/cache seam runs. +func packAddImportFenced(fs fsys.FS, cityPath, source, name, version string) (*importsvc.AddResult, error) { + return importsvc.AddImportWith(fs, cityPath, source, name, version, importsvc.Deps{SourcePolicy: validateHTTPPackSource}) +} + +// packRemoveImportFenced is the default remove seam. Remove re-syncs the lock +// graph, so it threads the same source policy to fence any transitive import a +// re-resolution would otherwise fetch. +func packRemoveImportFenced(fs fsys.FS, cityPath, name string) (*importsvc.RemoveResult, error) { + return importsvc.RemoveImportWith(fs, cityPath, name, importsvc.Deps{SourcePolicy: validateHTTPPackSource}) +} + // PackListBody is the response body for GET /v0/packs. type PackListBody struct { Packs []packResponse `json:"packs" doc:"Registered packs."` @@ -15,25 +45,148 @@ type PackListOutput struct { Body PackListBody } -// humaHandlePackList is the Huma-typed handler for GET /v0/packs. +// humaHandlePackList lists the city's direct, removable pack imports — the same +// [imports.] binding namespace that humaHandlePackAdd writes and +// humaHandlePackRemove deletes by name — so list/add/remove all operate on one +// namespace. It deliberately does NOT list the legacy [packs] migration table +// nor the transitive CollectAllImports closure. GET /v0/city/{cityName}/packs. func (s *Server) humaHandlePackList(_ context.Context, _ *PackListInput) (*PackListOutput, error) { - cfg := s.state.Config() - names := make([]string, 0, len(cfg.Packs)) - for name := range cfg.Packs { + imports, err := packListImports(fsys.OSFS{}, s.state.CityPath()) + if err != nil { + return nil, packImportHTTPError(err) + } + names := make([]string, 0, len(imports)) + for name := range imports { names = append(names, name) } sort.Strings(names) packs := make([]packResponse, 0, len(names)) for _, name := range names { - src := cfg.Packs[name] + imp := imports[name] packs = append(packs, packResponse{ - Name: name, - Source: src.Source, - Ref: src.Ref, - Path: src.Path, + Name: name, + Source: imp.Source, + Version: imp.Version, }) } out := &PackListOutput{} out.Body.Packs = packs return out, nil } + +// PackAddInput is the body for POST /v0/city/{cityName}/packs. +type PackAddInput struct { + CityScope + Body struct { + Source string `json:"source" minLength:"1" doc:"Pack source: a remote git URL or registry ref (a sub-path of a repo is allowed)." example:"https://github.com/org/repo/tree/main/packs/review"` + Name string `json:"name,omitempty" doc:"Optional local binding name override; derived from the source when omitted."` + Version string `json:"version,omitempty" doc:"Optional semver constraint for a git-backed pack." example:"^1.2.0"` + } +} + +// PackAddedOutput echoes the binding importsvc durably wrote. +type PackAddedOutput struct { + Body struct { + Name string `json:"name" doc:"The local binding name written to [imports.]."` + Source string `json:"source" doc:"The canonical source string written to the manifest."` + Version string `json:"version,omitempty" doc:"The version constraint written, if any."` + GitBacked bool `json:"git_backed" doc:"Whether the resolved source is git-backed (has a lock entry)."` + } +} + +// humaHandlePackAdd adds a pack to the city by import (the gc-import path): +// fence the caller-supplied source, write the [imports.] entry, resolve + +// lock + install, so the pack's templates compose into the city. +// POST /v0/city/{cityName}/packs. +func (s *Server) humaHandlePackAdd(_ context.Context, input *PackAddInput) (*PackAddedOutput, error) { + // SSRF fence: AddImport shells `git ls-remote ` synchronously and + // its contract requires HTTP callers to validate the source first. Reject + // local/file sources and internal-network destinations before the import + // seam runs. Kept outside the write lock — it is read-only and may resolve + // DNS. + if err := validateHTTPPackSource(input.Body.Source); err != nil { + return nil, packImportHTTPError(err) + } + var res *importsvc.AddResult + if err := s.serializeConfigWrite(func() error { + var addErr error + res, addErr = packAddImport(fsys.OSFS{}, s.state.CityPath(), input.Body.Source, input.Body.Name, input.Body.Version) + return addErr + }); err != nil { + return nil, packImportHTTPError(err) + } + out := &PackAddedOutput{} + out.Body.Name = res.Name + out.Body.Source = res.Source + out.Body.Version = res.Version + out.Body.GitBacked = res.GitBacked + return out, nil +} + +// PackRemoveInput targets DELETE /v0/city/{cityName}/packs/{name}. +type PackRemoveInput struct { + CityScope + Name string `path:"name" doc:"The import binding name to remove (the [imports.] key)."` +} + +// PackRemovedOutput echoes the removed binding. +type PackRemovedOutput struct { + Body struct { + Name string `json:"name" doc:"The binding name removed."` + } +} + +// humaHandlePackRemove drops a pack import from the city; its templates leave the +// composed config on the next reload. DELETE /v0/city/{cityName}/packs/{name}. +func (s *Server) humaHandlePackRemove(_ context.Context, input *PackRemoveInput) (*PackRemovedOutput, error) { + var res *importsvc.RemoveResult + if err := s.serializeConfigWrite(func() error { + var rmErr error + res, rmErr = packRemoveImport(fsys.OSFS{}, s.state.CityPath(), input.Name) + return rmErr + }); err != nil { + return nil, packImportHTTPError(err) + } + out := &PackRemovedOutput{} + out.Body.Name = res.Name + return out, nil +} + +// serializeConfigWrite runs fn under the per-city config write lock when the +// state supports it, so pack import add/remove serialize against the +// configedit.Editor boundary the other city-config mutation handlers use. +// A State that does not implement ConfigWriteSerializer (e.g. a read-only test +// double) runs fn directly. +func (s *Server) serializeConfigWrite(fn func() error) error { + if ser, ok := s.state.(ConfigWriteSerializer); ok { + return ser.SerializeConfigWrite(fn) + } + return fn() +} + +// packImportHTTPError maps importsvc sentinels to RFC 9457 problem responses. +func packImportHTTPError(err error) error { + switch { + case errors.Is(err, importsvc.ErrInvalidSource), errors.Is(err, importsvc.ErrScopeLoad), + errors.Is(err, importsvc.ErrNameDerive), errors.Is(err, importsvc.ErrReservedPrefix): + // ErrNameDerive and ErrReservedPrefix are client input-validation failures + // (no derivable name, or a reserved "default-rig:" name), so they are 400s + // like ErrInvalidSource, not 500s. + return huma.Error400BadRequest(err.Error()) + case errors.Is(err, importsvc.ErrImportExists): + return huma.Error409Conflict(err.Error()) + case errors.Is(err, importsvc.ErrNotFound): + return huma.Error404NotFound(err.Error()) + case errors.Is(err, importsvc.ErrVersionResolveFailed): + // Resolving the operator-named source via `git ls-remote` is a genuinely + // upstream dependency, so a failure here is a bad gateway. + return huma.Error502BadGateway(err.Error()) + case errors.Is(err, importsvc.ErrInstallFailed): + // ErrInstallFailed wraps LOCAL failures too (the import-graph read, + // manifest save, lockfile write), not just an upstream clone, so it maps + // to a server error — matching importsvc's documented HTTP 500. + return huma.Error500InternalServerError("pack install failed", err) + default: + return huma.Error500InternalServerError("pack import failed", err) + } +} diff --git a/internal/api/openapi.json b/internal/api/openapi.json index 8eb312b04d..9797adce3c 100644 --- a/internal/api/openapi.json +++ b/internal/api/openapi.json @@ -4986,6 +4986,61 @@ ], "type": "object" }, + "PackAddInputBody": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Optional local binding name override; derived from the source when omitted.", + "type": "string" + }, + "source": { + "description": "Pack source: a remote git URL or registry ref (a sub-path of a repo is allowed).", + "examples": [ + "https://github.com/org/repo/tree/main/packs/review" + ], + "minLength": 1, + "type": "string" + }, + "version": { + "description": "Optional semver constraint for a git-backed pack.", + "examples": [ + "^1.2.0" + ], + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, + "PackAddedOutputBody": { + "additionalProperties": false, + "properties": { + "git_backed": { + "description": "Whether the resolved source is git-backed (has a lock entry).", + "type": "boolean" + }, + "name": { + "description": "The local binding name written to [imports.\u003cname\u003e].", + "type": "string" + }, + "source": { + "description": "The canonical source string written to the manifest.", + "type": "string" + }, + "version": { + "description": "The version constraint written, if any.", + "type": "string" + } + }, + "required": [ + "name", + "source", + "git_backed" + ], + "type": "object" + }, "PackListBody": { "additionalProperties": false, "properties": { @@ -5005,19 +5060,29 @@ ], "type": "object" }, - "PackResponse": { + "PackRemovedOutputBody": { "additionalProperties": false, "properties": { "name": { + "description": "The binding name removed.", "type": "string" - }, - "path": { + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PackResponse": { + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, - "ref": { + "source": { "type": "string" }, - "source": { + "version": { "type": "string" } }, @@ -24524,6 +24589,151 @@ } }, "summary": "Get v0 city by city name packs" + }, + "post": { + "description": "Imports a pack into the city by source (a remote git URL or registry ref), resolving + installing it so its templates compose into the city.", + "operationId": "add-pack", + "parameters": [ + { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "in": "header", + "name": "X-GC-Request", + "required": true, + "schema": { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "City name.", + "in": "path", + "name": "cityName", + "required": true, + "schema": { + "description": "City name.", + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackAddInputBody" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackAddedOutputBody" + } + } + }, + "description": "Created", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + }, + "default": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + }, + "description": "Error", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + } + }, + "summary": "Add a pack" + } + }, + "/v0/city/{cityName}/packs/{name}": { + "delete": { + "operationId": "delete-v0-city-by-city-name-packs-by-name", + "parameters": [ + { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "in": "header", + "name": "X-GC-Request", + "required": true, + "schema": { + "description": "Anti-CSRF header required on mutation requests. Any non-empty value is accepted; the header's presence is what the server checks.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "City name.", + "in": "path", + "name": "cityName", + "required": true, + "schema": { + "description": "City name.", + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + }, + { + "description": "The import binding name to remove (the [imports.\u003cname\u003e] key).", + "in": "path", + "name": "name", + "required": true, + "schema": { + "description": "The import binding name to remove (the [imports.\u003cname\u003e] key).", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackRemovedOutputBody" + } + } + }, + "description": "OK", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + }, + "default": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + }, + "description": "Error", + "headers": { + "X-GC-Request-Id": { + "$ref": "#/components/headers/X-GC-Request-Id" + } + } + } + }, + "summary": "Delete v0 city by city name packs by name" } }, "/v0/city/{cityName}/patches/agent/{base}": { diff --git a/internal/api/pack_source_policy.go b/internal/api/pack_source_policy.go new file mode 100644 index 0000000000..09da1320ca --- /dev/null +++ b/internal/api/pack_source_policy.go @@ -0,0 +1,237 @@ +package api + +import ( + "context" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "github.com/gastownhall/gascity/internal/importsvc" +) + +// packSourceHostResolver resolves a hostname to its IP addresses for the SSRF +// fence. It is a package var so tests can stub DNS without touching the network; +// the default uses the process resolver. +var packSourceHostResolver = func(host string) ([]net.IP, error) { + addrs, err := net.DefaultResolver.LookupIPAddr(context.Background(), host) + if err != nil { + return nil, err + } + ips := make([]net.IP, len(addrs)) + for i, a := range addrs { + ips[i] = a.IP + } + return ips, nil +} + +// validateHTTPPackSource is the HTTP-layer SSRF fence for POST /packs. The +// import service shells `git ls-remote ` synchronously and documents +// that HTTP callers must validate the source first (importsvc/source.go's +// defaultHeadCommit). A network caller must not be able to (a) point the server +// at arbitrary local filesystem paths or file:// repos, or (b) drive git +// fetches at loopback, private, link-local, or cloud-metadata destinations. The +// CLI is a trusted local caller and keeps its local-path support; this fence is +// only applied on the HTTP path. +// +// Host validation alone is NOT sufficient: a fenced public host can redirect +// the git fetch to an internal target, and git re-resolves the host at fetch +// time (DNS rebinding). The redirect and transport-abuse classes are closed at +// the git subprocess by git.UntrustedRemoteGitConfigArgs (redirects disabled, +// transports constrained) on both the HEAD probe and the packman clone/tags +// fetch. The DNS-rebinding TOCTOU window remains an accepted residual — git +// re-resolves at fetch time and pinning the resolved IP is out of scope — so +// this fence is one layer of defense in depth, not the sole control. +// +// Blocked sources return an ErrInvalidSource so packImportHTTPError maps them to +// 400, and importantly they never reach the packAddImport seam. +func validateHTTPPackSource(source string) error { + host, local, file := packSourceHost(source) + switch { + case file: + return fmt.Errorf("%w: file:// sources are not permitted over the API", importsvc.ErrInvalidSource) + case local: + return fmt.Errorf("%w: local filesystem sources are not permitted over the API; use a remote git URL", importsvc.ErrInvalidSource) + case host == "": + return fmt.Errorf("%w: could not determine a host from the pack source", importsvc.ErrInvalidSource) + } + return ensurePublicPackSourceHost(host) +} + +// packSourceHost classifies an import source and extracts its network host. +// It reports local=true for local filesystem paths and file=true for file:// +// sources; for remote git sources it returns the host and local=file=false. +// The remote-source detection mirrors importsvc.isRemoteImportSource. +func packSourceHost(source string) (host string, local, file bool) { + switch { + case strings.HasPrefix(source, "file://"): + return "", false, true + case strings.HasPrefix(source, "git@"): + // scp-like syntax: user@host:path — the host ends at the first ':' or '/'. + rest := strings.TrimPrefix(source, "git@") + if strings.HasPrefix(rest, "[") { + // Bracketed IPv6 literal host, e.g. git@[::1]:repo. Take the address + // between the brackets; scanning for ':' would otherwise cut at the + // first ':' inside the literal and yield the bogus host "[". + if end := strings.IndexByte(rest, ']'); end > 1 { + return rest[1:end], false, false + } + return "", false, false + } + if i := strings.IndexAny(rest, ":/"); i >= 0 { + return rest[:i], false, false + } + return rest, false, false + case strings.HasPrefix(source, "ssh://"), + strings.HasPrefix(source, "https://"), + strings.HasPrefix(source, "http://"): + if u, err := url.Parse(source); err == nil { + return u.Hostname(), false, false + } + return "", false, false + case strings.HasPrefix(source, "github.com/"): + return "github.com", false, false + default: + // Everything else is a local path (//, ~, absolute, or relative), the + // same set importsvc resolves against the city directory. + return "", true, false + } +} + +// ensurePublicPackSourceHost rejects a host that names or resolves to an +// internal destination (loopback, private, link-local, unique-local, +// unspecified, or a cloud metadata IP such as 169.254.169.254). Hostnames are +// resolved through packSourceHostResolver; a resolution error is not treated as +// a block, since the subsequent git fetch performs its own resolution and will +// surface the failure — the fence only blocks on a positively-internal address. +func ensurePublicPackSourceHost(host string) error { + lower := strings.ToLower(strings.TrimSpace(host)) + if lower == "" { + return fmt.Errorf("%w: could not determine a host from the pack source", importsvc.ErrInvalidSource) + } + if lower == "localhost" || strings.HasSuffix(lower, ".localhost") { + return blockedPackHostErr(host, "loopback host") + } + if ip := net.ParseIP(host); ip != nil { + if isInternalIP(ip) { + return blockedPackHostErr(host, "internal IP address") + } + return nil + } + if ip := parseLooseIPv4(host); ip != nil { + // Encoded numeric literal (hex, octal, or dotless integer) that net.ParseIP + // rejects but git's C resolver (getaddrinfo) still decodes to a real + // address — 0x7f000001, 2130706433, and 0177.0.0.1 all reach 127.0.0.1, and + // 0xA9FEA9FE reaches the 169.254.169.254 metadata endpoint. Classify the + // decoded destination so these forms cannot slip an internal target past + // the fence on a resolver that errors for them. + if isInternalIP(ip) { + return blockedPackHostErr(host, "internal IP address") + } + return nil + } + ips, err := packSourceHostResolver(host) + if err != nil { + return nil + } + for _, ip := range ips { + if isInternalIP(ip) { + return blockedPackHostErr(host, "host resolves to an internal IP address") + } + } + return nil +} + +// isInternalIP reports whether ip is one an internet-facing pack source must +// never be. IsPrivate covers RFC1918 and IPv6 unique-local (fc00::/7); +// link-local covers 169.254.0.0/16 (including the 169.254.169.254 metadata +// endpoint) and fe80::/10. +func isInternalIP(ip net.IP) bool { + return ip.IsLoopback() || + ip.IsPrivate() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsInterfaceLocalMulticast() || + ip.IsUnspecified() +} + +// parseLooseIPv4 decodes the legacy inet_aton host forms that net.ParseIP +// rejects but the C resolver (getaddrinfo, which git and libcurl use) still +// accepts: a dotless 32-bit integer, hex (0x…) or octal (leading 0) parts, and +// the short a.b / a.b.c groupings. It returns the decoded IPv4 address, or nil +// when host is not one of those numeric forms (a normal hostname, or a form +// net.ParseIP already handled). Classifying the decoded address lets the SSRF +// fence see the destination git will actually connect to rather than trusting +// net.ParseIP to recognize every literal the resolver decodes. +func parseLooseIPv4(host string) net.IP { + if host == "" { + return nil + } + parts := strings.Split(host, ".") + if len(parts) > 4 { + return nil + } + vals := make([]uint64, len(parts)) + for i, p := range parts { + v, ok := parseInetAtonPart(p) + if !ok { + return nil + } + vals[i] = v + } + // inet_aton spreads the trailing part across the low-order bytes: a.b puts b + // in the low 24 bits, a.b.c puts c in the low 16, a.b.c.d is one byte each. + var addr uint64 + switch len(parts) { + case 1: + addr = vals[0] + case 2: + if vals[0] > 0xFF || vals[1] > 0xFFFFFF { + return nil + } + addr = vals[0]<<24 | vals[1] + case 3: + if vals[0] > 0xFF || vals[1] > 0xFF || vals[2] > 0xFFFF { + return nil + } + addr = vals[0]<<24 | vals[1]<<16 | vals[2] + case 4: + for _, v := range vals { + if v > 0xFF { + return nil + } + } + addr = vals[0]<<24 | vals[1]<<16 | vals[2]<<8 | vals[3] + } + if addr > 0xFFFFFFFF { + return nil + } + return net.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)) +} + +// parseInetAtonPart parses one component of a loose IPv4 literal with C +// inet_aton radix rules: a 0x/0X prefix is hex, a leading 0 is octal, everything +// else is decimal. It rejects an empty or malformed component. +func parseInetAtonPart(p string) (uint64, bool) { + base := 10 + digits := p + switch { + case len(p) >= 2 && (p[0:2] == "0x" || p[0:2] == "0X"): + base, digits = 16, p[2:] + case len(p) >= 2 && p[0] == '0': + base, digits = 8, p[1:] + } + if digits == "" { + return 0, false + } + v, err := strconv.ParseUint(digits, base, 64) + if err != nil { + return 0, false + } + return v, true +} + +func blockedPackHostErr(host, why string) error { + return fmt.Errorf("%w: pack source host %q is blocked (%s)", importsvc.ErrInvalidSource, host, why) +} diff --git a/internal/api/pack_source_policy_test.go b/internal/api/pack_source_policy_test.go new file mode 100644 index 0000000000..f8431a95a5 --- /dev/null +++ b/internal/api/pack_source_policy_test.go @@ -0,0 +1,238 @@ +package api + +import ( + "errors" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/importsvc" +) + +func TestValidateHTTPPackSource_AllowsPublicRemotes(t *testing.T) { + // A public host must resolve to a public address; stub DNS so the unit test + // never touches the network. + restore := stubPackSourceResolver(t, map[string][]net.IP{ + "github.com": {net.ParseIP("140.82.112.3")}, + "gitlab.example": {net.ParseIP("203.0.113.10")}, + }) + defer restore() + + for _, src := range []string{ + "https://github.com/org/repo/tree/main/packs/review", + "http://github.com/org/repo", + "ssh://git@gitlab.example/org/repo.git", + "git@github.com:org/repo.git", + "github.com/org/repo", + } { + if err := validateHTTPPackSource(src); err != nil { + t.Errorf("validateHTTPPackSource(%q) = %v, want nil", src, err) + } + } +} + +func TestValidateHTTPPackSource_RejectsLocalAndFileSources(t *testing.T) { + for _, src := range []string{ + "file:///etc/passwd", + "//packs/local", + "~/secret", + "/abs/path", + "relative/path", + "../packs/local", + } { + err := validateHTTPPackSource(src) + if err == nil { + t.Errorf("validateHTTPPackSource(%q) = nil, want rejection", src) + continue + } + if !errors.Is(err, importsvc.ErrInvalidSource) { + t.Errorf("validateHTTPPackSource(%q) error = %v, want ErrInvalidSource", src, err) + } + } +} + +func TestValidateHTTPPackSource_RejectsInternalIPLiterals(t *testing.T) { + for _, src := range []string{ + "https://127.0.0.1/repo.git", // loopback + "http://10.0.0.5/repo.git", // RFC1918 + "http://192.168.1.9/repo.git", // RFC1918 + "http://172.16.4.4/repo.git", // RFC1918 + "http://169.254.169.254/latest/meta", // link-local / cloud metadata + "ssh://git@[::1]/repo.git", // IPv6 loopback + "http://[fd00::1]/repo.git", // IPv6 unique-local + "git@10.0.0.5:repo.git", // scp-like, private + "http://0.0.0.0/repo.git", // unspecified + } { + err := validateHTTPPackSource(src) + if err == nil { + t.Errorf("validateHTTPPackSource(%q) = nil, want rejection", src) + continue + } + if !errors.Is(err, importsvc.ErrInvalidSource) { + t.Errorf("validateHTTPPackSource(%q) error = %v, want ErrInvalidSource", src, err) + } + } +} + +func TestValidateHTTPPackSource_RejectsEncodedInternalIPLiterals(t *testing.T) { + // Encoded-integer IP literals that net.ParseIP does not recognize but git's C + // resolver (getaddrinfo/inet_aton) decodes to an internal address must be + // blocked. The fence decodes them the same way (hex, octal, dotless integer) + // and classifies the decoded destination, so these cannot slip past on a + // resolver that merely errors for them. + for _, src := range []string{ + "http://0x7f000001/repo.git", // hex -> 127.0.0.1 (loopback) + "http://2130706433/repo.git", // dotless decimal -> 127.0.0.1 + "http://0177.0.0.1/repo.git", // octal octet -> 127.0.0.1 + "http://0xA9FEA9FE/latest/meta-data", // hex -> 169.254.169.254 (metadata) + "http://0xa9fea9fe/repo.git", // lowercase hex -> 169.254.169.254 + "http://3232235521/repo.git", // dotless decimal -> 192.168.0.1 + } { + err := validateHTTPPackSource(src) + if err == nil { + t.Errorf("validateHTTPPackSource(%q) = nil, want rejection", src) + continue + } + if !errors.Is(err, importsvc.ErrInvalidSource) { + t.Errorf("validateHTTPPackSource(%q) error = %v, want ErrInvalidSource", src, err) + } + } +} + +func TestValidateHTTPPackSource_AllowsEncodedPublicIPLiterals(t *testing.T) { + // Decoding must not over-block: an encoded literal that decodes to a public + // address is allowed, matching the plain dotted-decimal behavior. No DNS is + // consulted, so no resolver stub is needed. + for _, src := range []string{ + "http://0x08080808/repo.git", // hex -> 8.8.8.8 (public) + "http://134744072/repo.git", // dotless decimal -> 8.8.8.8 (public) + } { + if err := validateHTTPPackSource(src); err != nil { + t.Errorf("validateHTTPPackSource(%q) = %v, want nil", src, err) + } + } +} + +func TestValidateHTTPPackSource_RejectsBracketedIPv6ScpForm(t *testing.T) { + // scp-like SSH syntax with a bracketed IPv6 host (git@[::1]:repo) must extract + // the address between the brackets, not stop at the first ':' inside the + // literal and yield the bogus host "[" that slips past the fence. + for _, src := range []string{ + "git@[::1]:repo.git", // IPv6 loopback + "git@[fd00::1]:repo.git", // IPv6 unique-local + } { + if err := validateHTTPPackSource(src); !errors.Is(err, importsvc.ErrInvalidSource) { + t.Errorf("validateHTTPPackSource(%q) error = %v, want ErrInvalidSource", src, err) + } + } +} + +func TestValidateHTTPPackSource_RejectsLoopbackHostname(t *testing.T) { + for _, src := range []string{ + "https://localhost/repo.git", + "http://LOCALHOST:8080/repo.git", + "https://api.localhost/repo.git", + } { + if err := validateHTTPPackSource(src); !errors.Is(err, importsvc.ErrInvalidSource) { + t.Errorf("validateHTTPPackSource(%q) error = %v, want ErrInvalidSource", src, err) + } + } +} + +func TestValidateHTTPPackSource_RejectsHostResolvingToInternal(t *testing.T) { + restore := stubPackSourceResolver(t, map[string][]net.IP{ + // A public-looking name that resolves to an internal address (DNS-based + // SSRF) must be blocked. + "evil.example": {net.ParseIP("10.1.2.3")}, + }) + defer restore() + + if err := validateHTTPPackSource("https://evil.example/repo.git"); !errors.Is(err, importsvc.ErrInvalidSource) { + t.Errorf("validateHTTPPackSource(evil.example) error = %v, want ErrInvalidSource", err) + } +} + +func TestValidateHTTPPackSource_ResolutionErrorDoesNotBlock(t *testing.T) { + // A transient DNS failure must not block: the git fetch performs its own + // resolution and surfaces the failure there. The fence only blocks on a + // positively-internal address. + restore := stubPackSourceResolver(t, nil) + defer restore() + + if err := validateHTTPPackSource("https://unresolvable.example/repo.git"); err != nil { + t.Errorf("validateHTTPPackSource on resolution error = %v, want nil", err) + } +} + +// The add handler must fence the source BEFORE the importsvc seam runs, so a +// blocked source can never drive a git fetch. +func TestHandlePackAdd_BlocksSSRFSourceBeforeSeam(t *testing.T) { + restore := stubPackSourceResolver(t, map[string][]net.IP{ + "internal.example": {net.ParseIP("169.254.169.254")}, + }) + defer restore() + + orig := packAddImport + var seamCalled bool + packAddImport = func(fsys.FS, string, string, string, string) (*importsvc.AddResult, error) { + seamCalled = true + return &importsvc.AddResult{Name: "review"}, nil + } + defer func() { packAddImport = orig }() + + for _, body := range []string{ + `{"source":"http://169.254.169.254/latest/meta-data"}`, + `{"source":"file:///etc/passwd"}`, + `{"source":"//packs/local"}`, + `{"source":"https://internal.example/repo.git"}`, + } { + fs := newFakeMutatorState(t) + h := newTestCityHandler(t, fs) + req := httptest.NewRequest("POST", cityURL(fs, "/packs"), strings.NewReader(body)) + req.Header.Set("X-GC-Request", "true") + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("body %s: status = %d, want %d; body=%s", body, w.Code, http.StatusBadRequest, w.Body.String()) + } + } + if seamCalled { + t.Fatal("packAddImport seam was reached for a blocked source; the SSRF fence must run first") + } +} + +// TestPackAddImportFenced_ThreadsSSRFPolicyIntoImportsvc proves the default add +// seam threads validateHTTPPackSource into importsvc as the SourcePolicy, so the +// fence reaches the direct git probe and (via SyncLockWithPolicy) every +// transitive import, not just the handler's top-level pre-check. An internal +// direct source is rejected before any git seam, with no network. +func TestPackAddImportFenced_ThreadsSSRFPolicyIntoImportsvc(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "city.toml"), []byte("[workspace]\nname = \"demo\"\n"), 0o644); err != nil { + t.Fatalf("write city.toml: %v", err) + } + _, err := packAddImportFenced(fsys.OSFS{}, dir, "http://10.0.0.5/repo.git", "", "") + if !errors.Is(err, importsvc.ErrInvalidSource) { + t.Fatalf("default add seam did not thread the SSRF fence into importsvc: err = %v", err) + } +} + +// stubPackSourceResolver swaps the DNS seam for the test and returns a restore +// func. Hosts absent from table resolve with an error (no address). +func stubPackSourceResolver(t *testing.T, table map[string][]net.IP) func() { + t.Helper() + orig := packSourceHostResolver + packSourceHostResolver = func(host string) ([]net.IP, error) { + if ips, ok := table[strings.ToLower(host)]; ok { + return ips, nil + } + return nil, errors.New("no such host") + } + return func() { packSourceHostResolver = orig } +} diff --git a/internal/api/state.go b/internal/api/state.go index adda55313c..0e51459215 100644 --- a/internal/api/state.go +++ b/internal/api/state.go @@ -334,3 +334,17 @@ type FormulaMutator interface { // DeleteFormula removes a city-local formula source. DeleteFormula(name string) error } + +// ConfigWriteSerializer is an optional State extension that runs fn under the +// per-city config write lock. Pack import add/remove mutate city config files +// (pack.toml, packs.lock, and sometimes city.toml) outside the +// configedit.Editor callback shape, so running them through this seam +// serializes them against the agent/rig/provider/formula mutations that take +// the same Editor lock — otherwise two concurrent net/http goroutines could +// interleave load→mutate→write and lose an update or desync manifest and +// lockfile. Like StateMutator it is type-asserted by handlers; a State that +// does not implement it runs the mutation without extra serialization. +type ConfigWriteSerializer interface { + // SerializeConfigWrite runs fn while holding the per-city config write lock. + SerializeConfigWrite(fn func() error) error +} diff --git a/internal/api/supervisor_city_routes.go b/internal/api/supervisor_city_routes.go index a8e3c22206..890c9f8494 100644 --- a/internal/api/supervisor_city_routes.go +++ b/internal/api/supervisor_city_routes.go @@ -248,6 +248,15 @@ func (sm *SupervisorMux) registerCityRoutes() { // Packs. cityGet(sm, "/packs", (*Server).humaHandlePackList) + cityRegister(sm, huma.Operation{ + OperationID: "add-pack", + Method: http.MethodPost, + Path: "/packs", + Summary: "Add a pack", + Description: "Imports a pack into the city by source (a remote git URL or registry ref), resolving + installing it so its templates compose into the city.", + DefaultStatus: http.StatusCreated, + }, (*Server).humaHandlePackAdd) + cityDelete(sm, "/packs/{name}", (*Server).humaHandlePackRemove) // Sling. cityPost(sm, "/sling", (*Server).humaHandleSling) diff --git a/internal/configedit/configedit.go b/internal/configedit/configedit.go index 3be693bc7d..054ce29747 100644 --- a/internal/configedit/configedit.go +++ b/internal/configedit/configedit.go @@ -143,6 +143,19 @@ func (e *Editor) EditExpanded(fn func(raw, expanded *config.City) error) error { return e.write(raw) } +// Do runs fn while holding the Editor's mutation lock, serializing it against +// every other Editor mutation of this city. Use it for city-config writes that +// do not fit the load → mutate → validate → write callback shape — for example +// a multi-file pack import that writes pack.toml, packs.lock, and sometimes +// city.toml — so they still pass through the single per-city serialization +// boundary the [Editor] provides. The Editor does not load, validate, or write +// city.toml for a Do call; fn owns its own I/O. +func (e *Editor) Do(fn func() error) error { + e.mu.Lock() + defer e.mu.Unlock() + return fn() +} + func validateCityForEdit(cfg *config.City) error { if err := config.ValidateAgents(cfg.Agents); err != nil { return fmt.Errorf("%w: agents: %w", ErrValidation, err) diff --git a/internal/configedit/configedit_test.go b/internal/configedit/configedit_test.go index 2e05923ee2..afa85af4b9 100644 --- a/internal/configedit/configedit_test.go +++ b/internal/configedit/configedit_test.go @@ -5,7 +5,10 @@ import ( "os" "path/filepath" "strings" + "sync" + "sync/atomic" "testing" + "time" "github.com/gastownhall/gascity/internal/config" "github.com/gastownhall/gascity/internal/configedit" @@ -164,6 +167,58 @@ func TestEdit_SetsAgentSuspended(t *testing.T) { t.Error("mayor not found after edit") } +// TestDo_SerializesConcurrentCalls proves Editor.Do runs its callbacks under +// the same mutex as Edit, so a config-write surface that runs outside the +// load→mutate→write shape (pack import add/remove) never overlaps another +// mutation of the same city. If Do did not lock, the concurrent callbacks would +// observe more than one in-flight at once. +func TestDo_SerializesConcurrentCalls(t *testing.T) { + dir := t.TempDir() + path := writeTOML(t, dir, minimalCity()) + ed := configedit.NewEditor(fsys.OSFS{}, path) + + var inFlight, overlaps, ran int32 + var wg sync.WaitGroup + for i := 0; i < 32; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = ed.Do(func() error { + if atomic.AddInt32(&inFlight, 1) != 1 { + atomic.StoreInt32(&overlaps, 1) + } + time.Sleep(time.Millisecond) + atomic.AddInt32(&ran, 1) + atomic.AddInt32(&inFlight, -1) + return nil + }) + }() + } + wg.Wait() + + if overlaps != 0 { + t.Fatal("Editor.Do allowed concurrent callbacks to overlap; the lock did not serialize") + } + if ran != 32 { + t.Fatalf("ran = %d, want 32", ran) + } +} + +// TestDo_PropagatesResult confirms Do surfaces the callback's error unchanged. +func TestDo_PropagatesResult(t *testing.T) { + dir := t.TempDir() + path := writeTOML(t, dir, minimalCity()) + ed := configedit.NewEditor(fsys.OSFS{}, path) + + sentinel := errors.New("boom") + if err := ed.Do(func() error { return sentinel }); !errors.Is(err, sentinel) { + t.Fatalf("Do error = %v, want %v", err, sentinel) + } + if err := ed.Do(func() error { return nil }); err != nil { + t.Fatalf("Do(nil) = %v, want nil", err) + } +} + func TestEdit_ValidationFailure(t *testing.T) { dir := t.TempDir() path := writeTOML(t, dir, minimalCity()) diff --git a/internal/git/git.go b/internal/git/git.go index 35168ff43f..1107c6302a 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -359,6 +359,41 @@ func HermeticEnv() []string { return cleaned } +// UntrustedRemoteGitConfigArgs returns leading `git -c` overrides that harden a +// network git invocation (ls-remote, fetch, clone) whose remote URL may be +// attacker-influenced — the pack-import add path, where an API caller supplies +// the source string. Callers prepend it to the git arguments, before the +// subcommand. +// +// It closes the two classic ways a resolve-then-fetch SSRF host fence is +// bypassed at the git subprocess: +// +// - http.followRedirects=false stops git from following a 30x redirect, so a +// fenced public host cannot bounce the fetch to an internal target (e.g. +// 169.254.169.254) after the host check has already passed. +// - protocol.allow=never plus an explicit allowlist constrains the transports +// git will use to the schemes pack sources legitimately need (https, http, +// ssh, git, and file for CLI-local packs), so a crafted URL, redirect, or +// submodule cannot escalate to a dangerous transport such as ext:: (which +// runs an arbitrary command). +// +// It does NOT close a DNS-rebinding TOCTOU window: git re-resolves the host at +// fetch time, so a name that resolved to a public address during the fence can +// still resolve to an internal one here. That residual is documented at the +// pack SSRF fence (internal/api/pack_source_policy.go); pinning the resolved IP +// is out of scope for this hardening. +func UntrustedRemoteGitConfigArgs() []string { + return []string{ + "-c", "http.followRedirects=false", + "-c", "protocol.allow=never", + "-c", "protocol.https.allow=always", + "-c", "protocol.http.allow=always", + "-c", "protocol.ssh.allow=always", + "-c", "protocol.git.allow=always", + "-c", "protocol.file.allow=always", + } +} + // sanitizeGitEnv returns environ with git-specific variables removed. It is the // single filtering implementation shared by SanitizedEnv and runCtx so the // blacklist has exactly one enforcement path. diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 6f63ba95a8..cbfbe514be 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -744,3 +744,39 @@ func TestParseWorktreeList_Empty(t *testing.T) { t.Errorf("len(worktrees) = %d, want 0", len(wts)) } } + +// TestUntrustedRemoteGitConfigArgs pins the hardening applied to git invocations +// whose remote URL is attacker-influenced (the pack-import add path). Redirect +// following must be disabled and the transport allowlist constrained, so a +// fenced public host cannot 30x to an internal target and a crafted URL cannot +// escalate to a dangerous transport such as ext::. +func TestUntrustedRemoteGitConfigArgs(t *testing.T) { + args := UntrustedRemoteGitConfigArgs() + + // Every override is passed as a leading "-c key=value" pair. + joined := strings.Join(args, " ") + for _, want := range []string{ + "-c http.followRedirects=false", + "-c protocol.allow=never", + "-c protocol.https.allow=always", + "-c protocol.http.allow=always", + "-c protocol.ssh.allow=always", + "-c protocol.git.allow=always", + "-c protocol.file.allow=always", + } { + if !strings.Contains(joined, want) { + t.Errorf("UntrustedRemoteGitConfigArgs missing %q; got %v", want, args) + } + } + + // The args must be well-formed -c pairs so they can be prepended before a git + // subcommand. + if len(args)%2 != 0 { + t.Fatalf("expected an even number of args (-c pairs), got %d: %v", len(args), args) + } + for i := 0; i < len(args); i += 2 { + if args[i] != "-c" { + t.Fatalf("arg %d = %q, want -c; full: %v", i, args[i], args) + } + } +} diff --git a/internal/importsvc/importsvc.go b/internal/importsvc/importsvc.go new file mode 100644 index 0000000000..b532c37fe3 --- /dev/null +++ b/internal/importsvc/importsvc.go @@ -0,0 +1,505 @@ +package importsvc + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/packman" +) + +// Typed errors let callers map a failure to a transport-appropriate status +// (HTTP code or CLI exit) without string matching. Each wraps the underlying +// cause via %w so errors.Is and the detail message both survive. +var ( + // ErrInvalidSource means the source argument could not be normalized into a + // durable import source (bad path, missing pack.toml, embedded git ref, or + // a version flag on a non-git source). HTTP: 400. + ErrInvalidSource = errors.New("invalid import source") + // ErrNameDerive means no binding name was given and none could be derived + // from the source. Distinct from ErrInvalidSource so the CLI can reproduce + // its historical bare "use --name" message. HTTP: 400. + ErrNameDerive = errors.New("could not derive import name; use --name") + // ErrReservedPrefix means the requested binding name uses the reserved + // "default-rig:" prefix. Distinct from ErrInvalidSource for the same + // CLI-message-parity reason. HTTP: 400. + ErrReservedPrefix = errors.New("import name uses reserved prefix") + // ErrImportExists means the resolved binding name is already imported in the + // target scope (or is owned by a city.toml [imports] override). HTTP: 409. + ErrImportExists = errors.New("import already exists") + // ErrVersionResolveFailed means version/HEAD resolution for a git-backed + // source failed. HTTP: 502 (upstream git probe) or 400 depending on caller. + ErrVersionResolveFailed = errors.New("import version resolution failed") + // ErrInstallFailed means the lock sync or lockfile write failed. HTTP: 500. + ErrInstallFailed = errors.New("import install failed") + // ErrNotFound means RemoveImport was asked to remove a binding that is not + // present in any scope. HTTP: 404. + ErrNotFound = errors.New("import not found") + // ErrScopeLoad means the import scope could not be loaded (missing/invalid + // city.toml for a rig-scoped edit, unreadable pack.toml, etc.). HTTP: 400. + ErrScopeLoad = errors.New("import scope load failed") +) + +// AddResult reports what AddImport durably wrote so callers can echo the final +// binding without re-reading the manifest. +type AddResult struct { + // Name is the local binding name written as the [imports.] key. + Name string + // Source is the canonical, durable source string written to the manifest + // (remote URL as given, or a file:// promotion of a local git worktree). + Source string + // Version is the version constraint written to the manifest: a semver + // constraint, a "sha:" pin, or "" for plain path imports. + Version string + // GitBacked reports whether the resolved source is a git source (and thus + // has a lock entry); false for plain local path imports. + GitBacked bool +} + +// RemoveResult reports the binding RemoveImport deleted. +type RemoveResult struct { + // Name is the binding name that was removed. + Name string +} + +// Deps lets a caller inject the network/git-touching seams (the same vars the +// CLI stubs in its command tests) and the target rig scope. The zero value uses +// the package defaults, which call packman directly; this is what the HTTP +// handler wants. Any nil function field falls back to the package default. +type Deps struct { + // Rig selects a rig scope for the edit. Empty means the root pack.toml + // [imports] table. + Rig string + + // SourcePolicy fences every remote import source before it is probed or + // fetched — the caller-supplied source and every transitive import packman + // resolves during lock sync. The HTTP handler injects its SSRF fence here so + // an accepted public pack cannot pull an internal, file, or link-local nested + // import past the API. Leave nil (the trusted CLI/local path) to allow every + // source. + SourcePolicy func(source string) error + + // SyncLock, WriteLockfile, ResolveVersion, DefaultConstraint, and + // ResolveHeadCommit mirror the packman seams. Leave nil to use the package + // defaults. + SyncLock func(cityRoot string, imports map[string]config.Import, mode packman.InstallMode) (*packman.Lockfile, error) + WriteLockfile func(fs fsys.FS, cityRoot string, lock *packman.Lockfile) error + ResolveVersion func(source, constraint string) (packman.ResolvedVersion, error) + DefaultConstraint func(version string) (string, error) + ResolveHeadCommit func(source string) (string, error) +} + +func (d Deps) syncLock() func(string, map[string]config.Import, packman.InstallMode) (*packman.Lockfile, error) { + if d.SyncLock != nil { + return d.SyncLock + } + if d.SourcePolicy == nil { + return syncLock + } + // Route the default sync through the policy-aware packman entry point so the + // fence reaches transitive imports discovered inside lock resolution, which + // the caller can't see to pre-check. + return func(cityRoot string, imports map[string]config.Import, mode packman.InstallMode) (*packman.Lockfile, error) { + return packman.SyncLockWithPolicy(cityRoot, imports, mode, d.SourcePolicy) + } +} + +func (d Deps) writeLockfile() func(fsys.FS, string, *packman.Lockfile) error { + if d.WriteLockfile != nil { + return d.WriteLockfile + } + return writeLockfile +} + +func (d Deps) resolveVersion() func(string, string) (packman.ResolvedVersion, error) { + if d.ResolveVersion != nil { + return d.ResolveVersion + } + return resolveVersion +} + +func (d Deps) defaultConstraint() func(string) (string, error) { + if d.DefaultConstraint != nil { + return d.DefaultConstraint + } + return defaultConstraint +} + +func (d Deps) resolveHeadCommit() func(string) (string, error) { + if d.ResolveHeadCommit != nil { + return d.ResolveHeadCommit + } + return resolveHeadCommit +} + +func (d Deps) defaultImportVersionForSource(source string) (string, error) { + resolved, err := d.resolveVersion()(source, "") + if err == nil { + return d.defaultConstraint()(resolved.Version) + } + if !errors.Is(err, packman.ErrNoSemverTags) { + return "", err + } + commit, err := d.resolveHeadCommit()(source) + if err != nil { + return "", err + } + return "sha:" + commit, nil +} + +// fenceSource applies the injected untrusted-source policy to source. It is the +// service-boundary SSRF fence: the HTTP handler injects its host/file policy so +// AddImportWith never drives a git probe at an internal target, even for a +// caller that skips the handler's own pre-check. A nil policy allows everything. +func (d Deps) fenceSource(source string) error { + if d.SourcePolicy == nil { + return nil + } + return d.SourcePolicy(source) +} + +// resolveImportVersion validates and resolves the version constraint for an add. +// Git-backed sources reject a ref embedded in the URL and, when no constraint is +// given, default to the resolved semver/HEAD; non-git path sources reject a +// constraint outright. The returned error already carries the transport-mapped +// sentinel (ErrInvalidSource or ErrVersionResolveFailed). +func (d Deps) resolveImportVersion(source, versionConstraint string, gitBacked bool) (string, error) { + if !gitBacked { + if versionConstraint != "" { + return "", fmt.Errorf("%w: --version is only valid for git-backed imports", ErrInvalidSource) + } + return "", nil + } + if hasRepositoryRefInSource(source) { + return "", fmt.Errorf("%w: embed refs in --version, not in the source URL", ErrInvalidSource) + } + if versionConstraint != "" { + return versionConstraint, nil + } + version, err := d.defaultImportVersionForSource(source) + if err != nil { + return "", fmt.Errorf("%w: %w", ErrVersionResolveFailed, err) + } + return version, nil +} + +// AddImport resolves source once and writes it as a durable [imports.] +// entry plus a matching packs.lock entry for git-backed sources. It performs +// the git fetch (version/HEAD resolution and lock sync) synchronously: callers +// that need SSRF fencing must validate source before calling. The single +// remote git-fetch line lives in defaultHeadCommit (source.go); lock-time +// fetches happen inside packman.SyncLock. +func AddImport(fs fsys.FS, cityPath, source, nameOverride, versionConstraint string) (*AddResult, error) { + return AddImportWith(fs, cityPath, source, nameOverride, versionConstraint, Deps{}) +} + +// AddImportWith is AddImport with injectable seams and rig scope. +func AddImportWith(fs fsys.FS, cityPath, source, nameOverride, versionConstraint string, deps Deps) (*AddResult, error) { + scope, err := loadImportScope(fs, cityPath, deps.Rig) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + + source, gitBacked, err := normalizeImportAddSource(fs, cityPath, source) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidSource, err) + } + + // Fence the resolved source before the HEAD probe below shells `git + // ls-remote`. The HTTP handler pre-checks the caller-supplied source too, but + // applying the policy here keeps AddImportWith self-fencing so no future + // caller can drive the direct git probe at an internal target. + if err := deps.fenceSource(source); err != nil { + return nil, err + } + + name := nameOverride + if name == "" { + name = deriveImportName(source) + } + if name == "" { + return nil, ErrNameDerive + } + if strings.HasPrefix(name, "default-rig:") { + return nil, fmt.Errorf("import name %q uses reserved prefix \"default-rig:\": %w", name, ErrReservedPrefix) + } + if _, exists := scope.imports[name]; exists { + return nil, fmt.Errorf("%w: import %q already exists", ErrImportExists, name) + } + if scope.isRootPackScope() { + cityOwned, err := cityRootImportExists(fs, cityPath, name) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + if cityOwned { + return nil, fmt.Errorf("%w: import %q is defined by city.toml [imports], which overrides pack.toml; edit city.toml instead", ErrImportExists, name) + } + } + + version, err := deps.resolveImportVersion(source, versionConstraint, gitBacked) + if err != nil { + return nil, err + } + + scope.imports[name] = config.Import{ + Source: source, + Version: version, + } + allImports, err := CollectAllImports(fs, cityPath) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + allImports[scope.syntheticKey(name)] = scope.imports[name] + lock, err := deps.syncLock()(cityPath, allImports, packman.InstallResolveIfNeeded) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + if err := scope.save(); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + if err := deps.writeLockfile()(fs, cityPath, lock); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + return &AddResult{ + Name: name, + Source: source, + Version: version, + GitBacked: gitBacked, + }, nil +} + +// RemoveImport deletes the binding name from its owning scope (rig, root pack, +// city.toml root override, or root default-rig) and rewrites packs.lock to the +// remaining graph. When a root name is defined by both pack.toml and a city.toml +// [imports] override, the city override owns the effective (listed) binding, so +// remove peels the city override first and leaves the pack.toml entry declared; +// a second remove then deletes it. It returns ErrNotFound when no scope owns +// name. +func RemoveImport(fs fsys.FS, cityPath, name string) (*RemoveResult, error) { + return RemoveImportWith(fs, cityPath, name, Deps{}) +} + +// RemoveImportWith is RemoveImport with injectable seams and rig scope. +func RemoveImportWith(fs fsys.FS, cityPath, name string, deps Deps) (*RemoveResult, error) { + scope, err := loadImportScope(fs, cityPath, deps.Rig) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + + target, err := resolveRemoval(fs, cityPath, name, scope) + if err != nil { + return nil, err + } + + allImports, err := CollectAllImports(fs, cityPath) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + if target.packRemnant != nil { + // The pack.toml binding survives the peel and is effective again; + // CollectAllImports still layered the not-yet-saved city override on top, + // so re-point the merged graph to the pack binding. + allImports[scope.syntheticKey(name)] = *target.packRemnant + } else { + // Drop exactly the removed binding's graph key. Keying off removedKey (not + // an unconditional "default-rig:"+name delete) stops a bare root-import + // removal from silently dropping a same-named default-rig binding. + delete(allImports, target.removedKey) + } + lock, err := deps.syncLock()(cityPath, allImports, packman.InstallResolveIfNeeded) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + if err := scope.save(); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + if err := deps.writeLockfile()(fs, cityPath, lock); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInstallFailed, err) + } + return &RemoveResult{Name: name}, nil +} + +// removalTarget records what RemoveImportWith resolved to delete. Exactly one +// field is set: removedKey is the CollectAllImports synthetic key to drop (it +// depends on which scope owned the removal, not on the requested name — a bare +// name can resolve to a "default-rig:" binding); packRemnant is set +// instead when a city.toml root override is peeled off a same-named pack.toml +// import, so the surviving pack binding is re-pointed rather than dropped. +type removalTarget struct { + removedKey string + packRemnant *config.Import +} + +// resolveRemoval finds the scope that owns name, mutates that scope's in-memory +// state and save closure, and reports the graph effect. It isolates the +// scope-precedence branching (rig / root-pack / city-override / default-rig) +// from RemoveImportWith's collect→sync→save→write flow. +func resolveRemoval(fs fsys.FS, cityPath, name string, scope *importScopeState) (removalTarget, error) { + if _, exists := scope.imports[name]; exists { + return resolveScopedRemoval(fs, cityPath, name, scope) + } + return resolveFallbackRemoval(fs, cityPath, name, scope) +} + +// resolveScopedRemoval handles a name that lives directly in the loaded scope: a +// rig scope, a plain root-pack binding, or a root-pack binding shadowed by a +// city.toml [imports] override that must be peeled first. +func resolveScopedRemoval(fs fsys.FS, cityPath, name string, scope *importScopeState) (removalTarget, error) { + if !scope.isRootPackScope() { + delete(scope.imports, name) + return removalTarget{removedKey: scope.syntheticKey(name)}, nil + } + cityOwned, err := cityRootImportExists(fs, cityPath, name) + if err != nil { + return removalTarget{}, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + if !cityOwned { + delete(scope.imports, name) + return removalTarget{removedKey: scope.syntheticKey(name)}, nil + } + // city.toml [imports] owns the effective binding ListImports surfaces, so + // removing the listed name peels the city override (removeCityRootImport + // redirects the save to city.toml) and leaves the shadowed pack.toml entry + // declared. A follow-up remove of the same name then deletes the pack entry. + // Without this, GET listed a binding DELETE could never remove. + removed, err := removeCityRootImport(fs, cityPath, scope, name) + if err != nil { + return removalTarget{}, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + if !removed { + return removalTarget{}, fmt.Errorf("%w: import %q not found", ErrNotFound, name) + } + remnant := scope.imports[name] + return removalTarget{packRemnant: &remnant}, nil +} + +// resolveFallbackRemoval handles a name absent from the loaded scope by trying +// the city.toml root [imports] overrides first, then the root default-rig +// imports, returning ErrNotFound when no scope owns it. +func resolveFallbackRemoval(fs fsys.FS, cityPath, name string, scope *importScopeState) (removalTarget, error) { + removed, err := removeCityRootImport(fs, cityPath, scope, name) + if err != nil { + return removalTarget{}, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + if removed { + // A city-only root override lives under the root pack synthetic key. + return removalTarget{removedKey: scope.syntheticKey(name)}, nil + } + removed, err = removeRootDefaultRigImport(fs, cityPath, scope, name) + if err != nil { + return removalTarget{}, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + if !removed { + return removalTarget{}, fmt.Errorf("%w: import %q not found", ErrNotFound, name) + } + // The default-rig binding is keyed by its bare name in the graph, whether the + // caller passed "default-rig:" or just "". + return removalTarget{removedKey: "default-rig:" + strings.TrimPrefix(name, "default-rig:")}, nil +} + +// ListImports returns the direct, removable import bindings a client can list, +// add, and remove over one namespace. For the root scope that is the root +// pack.toml [imports] table, the city.toml root [imports] overrides layered on +// top, and root default-rig imports surfaced as "default-rig:" — the +// exact names DELETE accepts. It is deliberately NOT the transitive +// CollectAllImports closure, whose synthetic keys and resolved dependencies are +// not individually removable. +func ListImports(fs fsys.FS, cityPath string) (map[string]config.Import, error) { + return ListImportsWith(fs, cityPath, Deps{}) +} + +// ListImportsWith is ListImports with an injectable rig scope. For the root +// pack scope it returns the full inspectable namespace that AddImport and +// RemoveImport treat as in scope: the root pack.toml [imports] table, the +// city.toml root [imports] overrides layered on top (city entries own the +// effective root import, so AddImport rejects and RemoveImport redirects to +// them), and root default-rig imports keyed "default-rig:" (removable +// by that name). This mirrors the CLI's collectInspectableImports so GET, +// POST, and DELETE all agree on one namespace. For a rig scope it returns that +// rig's [rigs.imports] table. +func ListImportsWith(fs fsys.FS, cityPath string, deps Deps) (map[string]config.Import, error) { + scope, err := loadImportScope(fs, cityPath, deps.Rig) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + imports := copyImports(scope.imports) + if !scope.isRootPackScope() { + return imports, nil + } + if err := applyCityRootImportOverrides(fs, cityPath, imports); err != nil { + return nil, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + defaults, err := config.LoadRootPackDefaultRigImports(fs, cityPath) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrScopeLoad, err) + } + for _, bound := range defaults { + key := "default-rig:" + bound.Binding + if _, exists := imports[key]; exists { + return nil, fmt.Errorf("%w: import %q conflicts with reserved default-rig inspection key", ErrScopeLoad, key) + } + imports[key] = bound.Import + } + return imports, nil +} + +// removeCityRootImport removes a root import owned by city.toml [imports]. +// City-only root imports are visible in list/why output, so remove must be able +// to delete them; they live in city.toml, so the save is redirected there. +func removeCityRootImport(fs fsys.FS, cityPath string, scope *importScopeState, name string) (bool, error) { + if !scope.isRootPackScope() { + return false, nil + } + if _, err := fs.Stat(filepath.Join(cityPath, "city.toml")); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + cfg, err := loadCityImportManifest(fs, cityPath) + if err != nil { + return false, err + } + if _, ok := cfg.Imports[name]; !ok { + return false, nil + } + delete(cfg.Imports, name) + scope.save = func() error { + return writeCityImportManifest(fs, cityPath, cfg) + } + return true, nil +} + +func removeRootDefaultRigImport(fs fsys.FS, cityPath string, scope *importScopeState, name string) (bool, error) { + if !scope.isRootPackScope() { + return false, nil + } + defaultName := strings.TrimPrefix(name, "default-rig:") + cfg, err := loadCityImportManifest(fs, cityPath) + if err != nil { + return false, err + } + if _, ok := cfg.Defaults.Rig.Imports[defaultName]; !ok { + manifest, err := loadCityPackManifest(fs, cityPath) + if err != nil { + return false, err + } + if _, ok := manifest.Defaults.Rig.Imports[defaultName]; !ok { + return false, nil + } + delete(manifest.Defaults.Rig.Imports, defaultName) + scope.save = func() error { + return writeCityPackManifest(fs, cityPath, manifest) + } + return true, nil + } + delete(cfg.Defaults.Rig.Imports, defaultName) + scope.save = func() error { + return writeCityImportManifest(fs, cityPath, cfg) + } + return true, nil +} diff --git a/internal/importsvc/importsvc_test.go b/internal/importsvc/importsvc_test.go new file mode 100644 index 0000000000..d8b8db3d63 --- /dev/null +++ b/internal/importsvc/importsvc_test.go @@ -0,0 +1,565 @@ +package importsvc + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/packman" +) + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%s): %v", path, err) + } +} + +// stubDeps returns a Deps whose lock sync records the imports it was handed and +// returns a fixed lockfile, so add/remove can exercise the real manifest writes +// without touching the network or git. +func stubDeps(t *testing.T, captured *map[string]config.Import) Deps { + t.Helper() + return Deps{ + ResolveVersion: func(_, _ string) (packman.ResolvedVersion, error) { + return packman.ResolvedVersion{Version: "1.4.2", Commit: "abc123"}, nil + }, + DefaultConstraint: func(_ string) (string, error) { return "^1.4", nil }, + SyncLock: func(_ string, imports map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + if captured != nil { + *captured = imports + } + return &packman.Lockfile{ + Schema: packman.LockfileSchema, + Packs: map[string]packman.LockedPack{ + "https://github.com/example/tools.git": {Version: "1.4.2", Commit: "abc123"}, + }, + }, nil + }, + } +} + +func TestAddImportHappyPathWritesManifestAndResult(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + res, err := AddImportWith( + fsys.OSFS{}, dir, + "https://github.com/example/tools.git", "", "", + stubDeps(t, nil), + ) + if err != nil { + t.Fatalf("AddImportWith: %v", err) + } + if res.Name != "tools" { + t.Fatalf("Name = %q, want tools", res.Name) + } + if res.Source != "https://github.com/example/tools.git" { + t.Fatalf("Source = %q", res.Source) + } + if res.Version != "^1.4" { + t.Fatalf("Version = %q, want ^1.4", res.Version) + } + if !res.GitBacked { + t.Fatal("GitBacked = false, want true for a remote source") + } + + cfg, err := config.Load(fsys.OSFS{}, filepath.Join(dir, "pack.toml")) + if err != nil { + t.Fatalf("Load(pack.toml): %v", err) + } + imp, ok := cfg.Imports["tools"] + if !ok { + t.Fatalf("imports = %#v, want tools", cfg.Imports) + } + if imp.Version != "^1.4" { + t.Fatalf("imports.tools.version = %q, want ^1.4", imp.Version) + } + lock, err := packman.ReadLockfile(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ReadLockfile: %v", err) + } + if _, ok := lock.Packs["https://github.com/example/tools.git"]; !ok { + t.Fatalf("lock = %#v, want tools entry", lock.Packs) + } +} + +func TestAddImportAlreadyExistsReturnsTypedError(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + writeFile(t, filepath.Join(dir, "pack.toml"), `[pack] +name = "demo" +schema = 1 + +[imports.tools] +source = "https://github.com/example/tools.git" +version = "^1.4" +`) + + deps := Deps{ + SyncLock: func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + t.Fatal("SyncLock must not run when the import already exists") + return nil, nil + }, + } + _, err := AddImportWith(fsys.OSFS{}, dir, "https://github.com/example/tools.git", "", "^1.4", deps) + if err == nil { + t.Fatal("AddImportWith = nil error, want ErrImportExists") + } + if !errors.Is(err, ErrImportExists) { + t.Fatalf("err = %v, want ErrImportExists", err) + } +} + +func TestAddImportBadSourceReturnsInvalidSource(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + deps := Deps{ + SyncLock: func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + t.Fatal("SyncLock must not run for an invalid source") + return nil, nil + }, + } + // A local path with no pack.toml is not a valid pack target. + _, err := AddImportWith(fsys.OSFS{}, dir, "./packs/missing", "", "", deps) + if err == nil { + t.Fatal("AddImportWith = nil error, want ErrInvalidSource") + } + if !errors.Is(err, ErrInvalidSource) { + t.Fatalf("err = %v, want ErrInvalidSource", err) + } +} + +func TestAddImportVersionOnPathSourceReturnsInvalidSource(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + localPack := filepath.Join(dir, "packs", "local") + if err := os.MkdirAll(localPack, 0o755); err != nil { + t.Fatal(err) + } + writeFile(t, filepath.Join(localPack, "pack.toml"), "[pack]\nname = \"local\"\nschema = 1\n") + + _, err := AddImportWith(fsys.OSFS{}, dir, "./packs/local", "", "^1.2", Deps{}) + if err == nil { + t.Fatal("AddImportWith = nil error, want ErrInvalidSource for version on path import") + } + if !errors.Is(err, ErrInvalidSource) { + t.Fatalf("err = %v, want ErrInvalidSource", err) + } +} + +func TestAddImportReservedPrefixReturnsReservedPrefixError(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + _, err := AddImportWith(fsys.OSFS{}, dir, "https://example.com/worker.git", "default-rig:worker", "^1.0", Deps{}) + if err == nil { + t.Fatal("AddImportWith = nil error, want ErrReservedPrefix") + } + if !errors.Is(err, ErrReservedPrefix) { + t.Fatalf("err = %v, want ErrReservedPrefix", err) + } +} + +func TestAddImportEmptyDerivedNameReturnsNameDeriveError(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + // A bare "https://" trailing-slash source derives to an empty name. + _, err := AddImportWith(fsys.OSFS{}, dir, "https://", "", "", Deps{ + SyncLock: func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + t.Fatal("SyncLock must not run when the name cannot be derived") + return nil, nil + }, + }) + if err == nil { + t.Fatal("AddImportWith = nil error, want ErrNameDerive") + } + if !errors.Is(err, ErrNameDerive) { + t.Fatalf("err = %v, want ErrNameDerive", err) + } +} + +func TestListImportsReturnsDirectRemovableBindings(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + writeFile(t, filepath.Join(dir, "pack.toml"), `[pack] +name = "demo" +schema = 1 + +[imports.tools] +source = "https://github.com/example/tools.git" +version = "^1.4" + +[imports.local] +source = "../packs/local" +`) + + imports, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports: %v", err) + } + if len(imports) != 2 { + t.Fatalf("len(imports) = %d, want 2: %#v", len(imports), imports) + } + if got := imports["tools"]; got.Source != "https://github.com/example/tools.git" || got.Version != "^1.4" { + t.Fatalf("tools = %#v", got) + } + if got := imports["local"]; got.Source != "../packs/local" { + t.Fatalf("local = %#v", got) + } + // The returned map must be a copy: mutating it must not affect a re-read. + delete(imports, "tools") + again, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports (re-read): %v", err) + } + if _, ok := again["tools"]; !ok { + t.Fatal("ListImports returned an aliased map; mutation leaked back") + } +} + +func TestListImportsEmptyPackHasNoBindings(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + writeFile(t, filepath.Join(dir, "pack.toml"), "[pack]\nname = \"demo\"\nschema = 1\n") + + imports, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports: %v", err) + } + if len(imports) != 0 { + t.Fatalf("len(imports) = %d, want 0: %#v", len(imports), imports) + } +} + +func TestListImportsRoundTripsAddedBinding(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + if _, err := AddImportWith( + fsys.OSFS{}, dir, + "https://github.com/example/tools.git", "", "", + stubDeps(t, nil), + ); err != nil { + t.Fatalf("AddImportWith: %v", err) + } + + imports, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports: %v", err) + } + if _, ok := imports["tools"]; !ok { + t.Fatalf("added binding not surfaced by ListImports: %#v", imports) + } +} + +// ListImports must surface city.toml root [imports] (which RemoveImport can +// delete and AddImport rejects) so the list namespace matches add/remove. +func TestListImportsSurfacesCityRootImports(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), `[workspace] +name = "demo" + +[imports.cityonly] +source = "https://github.com/example/cityonly.git" +version = "^3.0" + +[imports.tools] +source = "https://github.com/example/city-tools.git" +version = "^9.9" +`) + writeFile(t, filepath.Join(dir, "pack.toml"), `[pack] +name = "demo" +schema = 1 + +[imports.tools] +source = "https://github.com/example/tools.git" +version = "^1.4" +`) + + imports, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports: %v", err) + } + // A city-only root import, invisible before this fix, is now listed. + if got, ok := imports["cityonly"]; !ok || got.Source != "https://github.com/example/cityonly.git" { + t.Fatalf("cityonly = %#v, ok=%v; want the city.toml root import surfaced", imports["cityonly"], ok) + } + // city.toml [imports] override wins over the same-named pack.toml entry, so + // the listed binding matches the effective (removable) one. + if got := imports["tools"]; got.Source != "https://github.com/example/city-tools.git" { + t.Fatalf("tools = %#v; want the city.toml override to win", got) + } +} + +// ListImports must surface root default-rig imports keyed "default-rig:", +// the exact form DELETE /packs/{name} accepts to remove them. +func TestListImportsSurfacesDefaultRigImports(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), `[workspace] +name = "demo" + +[imports.tools] +source = "https://github.com/example/tools.git" +version = "^1.4" + +[defaults.rig.imports.shared] +source = "https://github.com/example/shared.git" +version = "^2.0" +`) + + imports, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports: %v", err) + } + if _, ok := imports["tools"]; !ok { + t.Fatalf("root import missing: %#v", imports) + } + got, ok := imports["default-rig:shared"] + if !ok || got.Source != "https://github.com/example/shared.git" || got.Version != "^2.0" { + t.Fatalf("default-rig:shared = %#v, ok=%v; want the default-rig import surfaced", got, ok) + } +} + +func TestRemoveImportFoundRewritesManifest(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + writeFile(t, filepath.Join(dir, "pack.toml"), `[pack] +name = "demo" +schema = 1 + +[imports.tools] +source = "https://github.com/example/tools.git" +version = "^1.4" +`) + + var captured map[string]config.Import + deps := Deps{ + SyncLock: func(_ string, imports map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + captured = imports + return &packman.Lockfile{Schema: packman.LockfileSchema, Packs: map[string]packman.LockedPack{}}, nil + }, + } + res, err := RemoveImportWith(fsys.OSFS{}, dir, "tools", deps) + if err != nil { + t.Fatalf("RemoveImportWith: %v", err) + } + if res.Name != "tools" { + t.Fatalf("Name = %q, want tools", res.Name) + } + if _, ok := captured["pack:tools"]; ok { + t.Fatalf("synced imports still contain removed import: %#v", captured) + } + cfg, err := config.Load(fsys.OSFS{}, filepath.Join(dir, "pack.toml")) + if err != nil { + t.Fatalf("Load(pack.toml): %v", err) + } + if _, ok := cfg.Imports["tools"]; ok { + t.Fatal("imports.tools still present after remove") + } +} + +func TestRemoveImportNotFoundReturnsTypedError(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + writeFile(t, filepath.Join(dir, "pack.toml"), "[pack]\nname = \"demo\"\nschema = 1\n") + + deps := Deps{ + SyncLock: func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + t.Fatal("SyncLock must not run when the import is absent") + return nil, nil + }, + } + _, err := RemoveImportWith(fsys.OSFS{}, dir, "ghost", deps) + if err == nil { + t.Fatal("RemoveImportWith = nil error, want ErrNotFound") + } + if !errors.Is(err, ErrNotFound) { + t.Fatalf("err = %v, want ErrNotFound", err) + } +} + +func TestAddImportDefaultExportsUsePackmanSeams(t *testing.T) { + // The exported AddImport (no Deps) must wire the package-default seams. + // Stub the package vars so the happy path runs without the network. + prevResolve := resolveVersion + prevConstraint := defaultConstraint + prevSync := syncLock + prevWrite := writeLockfile + t.Cleanup(func() { + resolveVersion = prevResolve + defaultConstraint = prevConstraint + syncLock = prevSync + writeLockfile = prevWrite + }) + resolveVersion = func(_, _ string) (packman.ResolvedVersion, error) { + return packman.ResolvedVersion{Version: "1.4.2", Commit: "abc123"}, nil + } + defaultConstraint = func(_ string) (string, error) { return "^1.4", nil } + syncLock = func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + return &packman.Lockfile{Schema: packman.LockfileSchema, Packs: map[string]packman.LockedPack{}}, nil + } + writeLockfile = func(_ fsys.FS, _ string, _ *packman.Lockfile) error { return nil } + + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + res, err := AddImport(fsys.OSFS{}, dir, "https://github.com/example/tools.git", "", "") + if err != nil { + t.Fatalf("AddImport: %v", err) + } + if res.Name != "tools" || res.Version != "^1.4" { + t.Fatalf("res = %#v, want tools/^1.4", res) + } +} + +// TestAddImportWithSourcePolicyFencesDirectSource is the regression for the +// transitive-import SSRF fix at the service boundary: a SourcePolicy must fence +// the resolved source before the HEAD probe or lock sync runs, so the injected +// HTTP fence governs the direct git seam (and, via SyncLockWithPolicy, the +// transitive one) rather than only the handler's pre-check. +func TestAddImportWithSourcePolicyFencesDirectSource(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), "[workspace]\nname = \"demo\"\n") + + sentinel := errors.New("blocked by source policy") + deps := Deps{ + SourcePolicy: func(string) error { return sentinel }, + ResolveHeadCommit: func(string) (string, error) { + t.Fatal("HEAD probe must not run when the source policy blocks") + return "", nil + }, + ResolveVersion: func(string, string) (packman.ResolvedVersion, error) { + t.Fatal("version resolve must not run when the source policy blocks") + return packman.ResolvedVersion{}, nil + }, + SyncLock: func(_ string, _ map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + t.Fatal("SyncLock must not run when the source policy blocks") + return nil, nil + }, + } + _, err := AddImportWith(fsys.OSFS{}, dir, "https://github.com/example/tools.git", "", "", deps) + if !errors.Is(err, sentinel) { + t.Fatalf("AddImportWith err = %v, want the source-policy sentinel", err) + } +} + +// TestRemoveImportPeelsCityOverrideShadowingPack is the regression for the +// GET/DELETE namespace mismatch: when a root name is defined by BOTH pack.toml +// and a city.toml [imports] override, ListImports surfaces the city override as +// the effective binding, so remove must peel that override (not reject it 409). +// The shadowed pack.toml entry stays declared and becomes effective again, and +// lock sync re-points to it rather than dropping the name. +func TestRemoveImportPeelsCityOverrideShadowingPack(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), `[workspace] +name = "demo" + +[imports.tools] +source = "https://github.com/example/city-tools.git" +version = "^9.9" +`) + writeFile(t, filepath.Join(dir, "pack.toml"), `[pack] +name = "demo" +schema = 1 + +[imports.tools] +source = "https://github.com/example/pack-tools.git" +version = "^1.0" +`) + + // GET surfaces the city override as the effective binding. + before, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports (before): %v", err) + } + if got := before["tools"]; got.Source != "https://github.com/example/city-tools.git" { + t.Fatalf("listed tools before remove = %#v; want the city override", got) + } + + var captured map[string]config.Import + deps := Deps{ + SyncLock: func(_ string, imports map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + captured = imports + return &packman.Lockfile{Schema: packman.LockfileSchema, Packs: map[string]packman.LockedPack{}}, nil + }, + } + // The listed binding must be removable, not a 409. + res, err := RemoveImportWith(fsys.OSFS{}, dir, "tools", deps) + if err != nil { + t.Fatalf("RemoveImportWith(tools) = %v, want peel of the city override", err) + } + if res.Name != "tools" { + t.Fatalf("Name = %q, want tools", res.Name) + } + + // The pack.toml entry survives the peel and is effective again... + cfg, err := config.Load(fsys.OSFS{}, filepath.Join(dir, "pack.toml")) + if err != nil { + t.Fatalf("Load(pack.toml): %v", err) + } + if got, ok := cfg.Imports["tools"]; !ok || got.Source != "https://github.com/example/pack-tools.git" { + t.Fatalf("pack.toml tools after peel = %#v ok=%v; want the pack entry preserved", got, ok) + } + // ...and the city override is gone, so a re-list shows the pack binding. + after, err := ListImports(fsys.OSFS{}, dir) + if err != nil { + t.Fatalf("ListImports (after): %v", err) + } + if got := after["tools"]; got.Source != "https://github.com/example/pack-tools.git" { + t.Fatalf("listed tools after remove = %#v; want the pack binding now effective", got) + } + // Lock sync must keep tools re-pointed to the pack value, not drop it. + synced, ok := captured["pack:tools"] + if !ok || synced.Source != "https://github.com/example/pack-tools.git" { + t.Fatalf("synced pack:tools = %#v ok=%v; want the pack binding preserved in the lock graph", synced, ok) + } +} + +// TestRemoveRootImportKeepsSameNamedDefaultRig is the regression for the +// lock-sync deletion bug: removing a bare root import named "shared" must NOT +// also drop a same-named "default-rig:shared" binding from the synced graph. +func TestRemoveRootImportKeepsSameNamedDefaultRig(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "city.toml"), `[workspace] +name = "demo" + +[defaults.rig.imports.shared] +source = "https://github.com/example/dr-shared.git" +version = "^2.0" +`) + writeFile(t, filepath.Join(dir, "pack.toml"), `[pack] +name = "demo" +schema = 1 + +[imports.shared] +source = "https://github.com/example/root-shared.git" +version = "^1.0" +`) + + var captured map[string]config.Import + deps := Deps{ + SyncLock: func(_ string, imports map[string]config.Import, _ packman.InstallMode) (*packman.Lockfile, error) { + captured = imports + return &packman.Lockfile{Schema: packman.LockfileSchema, Packs: map[string]packman.LockedPack{}}, nil + }, + } + if _, err := RemoveImportWith(fsys.OSFS{}, dir, "shared", deps); err != nil { + t.Fatalf("RemoveImportWith(shared): %v", err) + } + + // The removed root import is dropped from the graph... + if _, ok := captured["pack:shared"]; ok { + t.Fatalf("synced graph still contains removed root import: %#v", captured) + } + // ...but the same-named default-rig binding must survive. + dr, ok := captured["default-rig:shared"] + if !ok || dr.Source != "https://github.com/example/dr-shared.git" { + t.Fatalf("default-rig:shared = %#v ok=%v; a same-named default-rig import was dropped", dr, ok) + } +} diff --git a/internal/importsvc/manifest.go b/internal/importsvc/manifest.go new file mode 100644 index 0000000000..7e736b6df1 --- /dev/null +++ b/internal/importsvc/manifest.go @@ -0,0 +1,463 @@ +// Package importsvc holds the shared orchestration for adding and removing +// pack imports. It is the single code path behind both the `gc import add` / +// `gc import remove` CLI commands and the supervisor HTTP handlers, so it +// operates on an injected [fsys.FS] plus a city path and carries no cobra, +// io.Writer, or working-directory coupling. Callers map the typed errors it +// returns to whatever surface they speak (exit codes or HTTP status). +// +// KNOWN DUPLICATION (follow-up to converge): the manifest/scope helpers below +// — loadCityPackManifest, writeCityPackManifest, loadImportScope, +// CollectAllImports and their support funcs — were lifted verbatim from the +// unimportable package-main copies in cmd/gc (cmd_import.go still keeps +// loadCityPackManifestFS/collectAllImportsFS/loadImportScopeFS, shared with the +// other gc import subcommands). The two copies must stay byte-equivalent in +// behavior; a divergence in pack.toml round-trip rules would silently desync +// the CLI and the HTTP path. The intended end state is for cmd/gc to delegate +// these reads to importsvc too; until then, treat any edit here as needing the +// mirror edit in cmd_import.go (and vice versa). +package importsvc + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/BurntSushi/toml" + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/packman" + "github.com/gastownhall/gascity/internal/pathutil" + "github.com/gastownhall/gascity/internal/pricing" +) + +// Seam variables wrap the packman entry points the orchestration drives. They +// are package vars so importsvc's own tests can stub the network- and +// git-touching steps; the CLI injects its own stubbable copies through [Deps] +// so the existing command tests keep working unchanged. +var ( + syncLock = packman.SyncLock + writeLockfile = packman.WriteLockfile + resolveVersion = packman.ResolveVersion + defaultConstraint = packman.DefaultConstraint + resolveHeadCommit = defaultHeadCommit +) + +const cityPackSchema = 1 + +type cityPackManifest struct { + Pack config.PackMeta `toml:"pack"` + Imports map[string]config.Import `toml:"imports,omitempty"` + AgentDefaults config.AgentDefaults `toml:"agent_defaults,omitempty"` + AgentsDefaults config.AgentDefaults `toml:"agents,omitempty" jsonschema:"-"` + Defaults cityPackDefaults `toml:"defaults,omitempty"` + DefaultRigImportOrder []string `toml:"-"` + Agents []config.Agent `toml:"agent,omitempty"` + NamedSessions []config.NamedSession `toml:"named_session,omitempty"` + Services []config.Service `toml:"service,omitempty"` + Providers map[string]config.ProviderSpec `toml:"providers,omitempty"` + Upstreams map[string]config.UpstreamSpec `toml:"upstreams,omitempty"` + Formulas config.FormulasConfig `toml:"formulas,omitempty"` + Patches config.Patches `toml:"patches,omitempty"` + Doctor []config.PackDoctorEntry `toml:"doctor,omitempty"` + Commands []config.PackCommandEntry `toml:"commands,omitempty"` + Global config.PackGlobal `toml:"global,omitempty"` + Pricing []pricing.ModelPricing `toml:"pricing,omitempty"` +} + +type cityPackDefaults struct { + Rig cityPackRigDefaults `toml:"rig,omitempty"` +} + +type cityPackRigDefaults struct { + Imports map[string]config.Import `toml:"imports,omitempty"` +} + +type cityPackManifestBody struct { + Pack config.PackMeta `toml:"pack"` + Imports map[string]config.Import `toml:"imports,omitempty"` + AgentDefaults config.AgentDefaults `toml:"agent_defaults,omitempty"` + Agents []config.Agent `toml:"agent,omitempty"` + NamedSessions []config.NamedSession `toml:"named_session,omitempty"` + Services []config.Service `toml:"service,omitempty"` + Providers map[string]config.ProviderSpec `toml:"providers,omitempty"` + Upstreams map[string]config.UpstreamSpec `toml:"upstreams,omitempty"` + Formulas config.FormulasConfig `toml:"formulas,omitempty"` + Patches config.Patches `toml:"patches,omitempty"` + Doctor []config.PackDoctorEntry `toml:"doctor,omitempty"` + Commands []config.PackCommandEntry `toml:"commands,omitempty"` + Global config.PackGlobal `toml:"global,omitempty"` + Pricing []pricing.ModelPricing `toml:"pricing,omitempty"` +} + +// importScopeState captures the imports table being edited (root pack.toml or a +// rig in city.toml), the synthetic-key prefix used to address it in the merged +// import graph, and a save closure that writes the mutated table back. +type importScopeState struct { + imports map[string]config.Import + syntheticTag string + save func() error +} + +func (s *importScopeState) syntheticKey(name string) string { + return s.syntheticTag + name +} + +func (s *importScopeState) isRootPackScope() bool { + return s != nil && s.syntheticTag == "pack:" +} + +// loadImportScope loads the writable import table for the requested scope. When +// rig is empty the root pack.toml [imports] table is edited; otherwise the +// named rig's [rigs.imports] table inside city.toml is edited. +func loadImportScope(fs fsys.FS, cityPath, rig string) (*importScopeState, error) { + targetRig := strings.TrimSpace(rig) + if targetRig == "" { + manifest, err := loadCityPackManifest(fs, cityPath) + if err != nil { + return nil, err + } + if manifest.Imports == nil { + manifest.Imports = make(map[string]config.Import) + } + return &importScopeState{ + imports: manifest.Imports, + syntheticTag: "pack:", + save: func() error { + return writeCityPackManifest(fs, cityPath, manifest) + }, + }, nil + } + + if _, err := fs.Stat(filepath.Join(cityPath, "city.toml")); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("rig-scoped imports require a city directory: %s", cityPath) + } + return nil, err + } + + cfg, err := loadCityImportManifest(fs, cityPath) + if err != nil { + return nil, err + } + rigIndex, rigName, err := findImportRigIndex(cityPath, cfg.Rigs, targetRig) + if err != nil { + return nil, err + } + if cfg.Rigs[rigIndex].Imports == nil { + cfg.Rigs[rigIndex].Imports = make(map[string]config.Import) + } + return &importScopeState{ + imports: cfg.Rigs[rigIndex].Imports, + syntheticTag: "rig:" + rigName + ":", + save: func() error { + return writeCityImportManifest(fs, cityPath, cfg) + }, + }, nil +} + +// CollectAllImports returns the full effective import graph keyed by synthetic +// scope tags ("pack:", "default-rig:", "rig::"). It +// is exported for callers (e.g. a future list/GET handler) that need the same +// merged view the add/remove sync uses. +func CollectAllImports(fs fsys.FS, cityPath string) (map[string]config.Import, error) { + all := make(map[string]config.Import) + + packManifest, err := loadCityPackManifest(fs, cityPath) + if err != nil { + return nil, err + } + rootImports := copyImports(packManifest.Imports) + if err := applyCityRootImportOverrides(fs, cityPath, rootImports); err != nil { + return nil, err + } + for name, imp := range rootImports { + all["pack:"+name] = imp + } + defaults, err := config.LoadRootPackDefaultRigImports(fs, cityPath) + if err != nil { + return nil, err + } + for _, bound := range defaults { + all["default-rig:"+bound.Binding] = bound.Import + } + + if _, err := fs.Stat(filepath.Join(cityPath, "city.toml")); err != nil { + if os.IsNotExist(err) { + return all, nil + } + return nil, err + } + + cfg, err := loadCityImportManifest(fs, cityPath) + if err != nil { + return nil, err + } + for _, rig := range cfg.Rigs { + for name, imp := range rig.Imports { + all["rig:"+rig.Name+":"+name] = imp + } + } + return all, nil +} + +func copyImports(imports map[string]config.Import) map[string]config.Import { + out := make(map[string]config.Import, len(imports)) + for name, imp := range imports { + out[name] = imp + } + return out +} + +func applyCityRootImportOverrides(fs fsys.FS, cityPath string, imports map[string]config.Import) error { + overrides, err := loadCityRootImports(fs, cityPath) + if err != nil { + return err + } + for name, imp := range overrides { + imports[name] = imp + } + return nil +} + +// loadCityRootImports returns the root-level [imports] entries from city.toml, +// or nil when no city.toml exists. +func loadCityRootImports(fs fsys.FS, cityPath string) (map[string]config.Import, error) { + if _, err := fs.Stat(filepath.Join(cityPath, "city.toml")); err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + cfg, err := loadCityImportManifest(fs, cityPath) + if err != nil { + return nil, err + } + return cfg.Imports, nil +} + +// cityRootImportExists reports whether city.toml's root [imports] table defines +// name. City entries own the effective root import wholesale, so the +// add/remove write paths must consult this before mutating pack.toml. +func cityRootImportExists(fs fsys.FS, cityPath, name string) (bool, error) { + overrides, err := loadCityRootImports(fs, cityPath) + if err != nil { + return false, err + } + _, ok := overrides[name] + return ok, nil +} + +func loadCityImportManifest(fs fsys.FS, cityPath string) (*config.City, error) { + return loadCityConfigForEdit(fs, filepath.Join(cityPath, "city.toml")) +} + +func writeCityImportManifest(fs fsys.FS, cityPath string, cfg *config.City) error { + if cfg == nil { + cfg = &config.City{} + } + return writeCityConfigForEdit(fs, filepath.Join(cityPath, "city.toml"), cfg) +} + +// loadCityConfigForEdit loads the raw city config WITHOUT pack/include +// expansion, preserving include directives, pack references, and patches for a +// faithful round-trip on rewrite. +func loadCityConfigForEdit(fs fsys.FS, tomlPath string) (*config.City, error) { + cfg, err := config.Load(fs, tomlPath) + if err != nil { + return nil, err + } + if _, err := config.ApplySiteBindingsForEdit(fs, filepath.Dir(tomlPath), cfg); err != nil { + return nil, err + } + return cfg, nil +} + +func writeCityConfigForEdit(fs fsys.FS, tomlPath string, cfg *config.City) error { + return config.WriteCityAndRigSiteBindingsForEdit(fs, tomlPath, cfg) +} + +func findImportRigIndex(cityPath string, rigs []config.Rig, target string) (int, string, error) { + for i, rig := range rigs { + if rig.Name == target { + return i, rig.Name, nil + } + } + + resolvedRigs := append([]config.Rig(nil), rigs...) + resolveRigPaths(cityPath, resolvedRigs) + + targetPath := target + if !filepath.IsAbs(targetPath) { + abs, err := filepath.Abs(filepath.Join(cityPath, targetPath)) + if err == nil { + targetPath = abs + } + } + for i, rig := range resolvedRigs { + if pathutil.SamePath(rig.Path, targetPath) { + return i, rigs[i].Name, nil + } + } + + return -1, "", fmt.Errorf("rig %q not found", target) +} + +// resolveRigPaths resolves relative rig paths to absolute (relative to +// cityPath), mutating rigs in place. +func resolveRigPaths(cityPath string, rigs []config.Rig) { + for i := range rigs { + if strings.TrimSpace(rigs[i].Path) == "" { + continue + } + if !filepath.IsAbs(rigs[i].Path) { + rigs[i].Path = filepath.Join(cityPath, rigs[i].Path) + } + } +} + +func loadCityPackManifest(fs fsys.FS, cityPath string) (*cityPackManifest, error) { + path := filepath.Join(cityPath, "pack.toml") + data, err := fs.ReadFile(path) + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + manifest := &cityPackManifest{ + Pack: config.PackMeta{ + Name: defaultCityPackName(fs, cityPath), + Schema: cityPackSchema, + }, + Imports: make(map[string]config.Import), + } + return manifest, nil + } + + var manifest cityPackManifest + md, err := toml.Decode(string(data), &manifest) + if err != nil { + return nil, fmt.Errorf("parsing pack.toml: %w", err) + } + // Fold the legacy [agents] alias into [agent_defaults] before any rewrite: + // the manifest body emits only [agent_defaults], so without this the + // import-manifest rewrite would silently drop an [agents] table even though + // the key-loss guard recognizes it. Mirrors parse-time normalization. + config.FoldAgentDefaultsAlias(&manifest.AgentDefaults, manifest.AgentsDefaults, md) + manifest.AgentsDefaults = config.AgentDefaults{} + if manifest.Pack.Name == "" { + manifest.Pack.Name = defaultCityPackName(fs, cityPath) + } + if manifest.Pack.Schema == 0 { + manifest.Pack.Schema = cityPackSchema + } + if manifest.Imports == nil { + manifest.Imports = make(map[string]config.Import) + } + if len(manifest.Defaults.Rig.Imports) > 0 { + ordered, err := config.LoadRootPackDefaultRigImports(fs, cityPath) + if err != nil { + return nil, err + } + manifest.DefaultRigImportOrder = make([]string, 0, len(ordered)) + for _, bound := range ordered { + manifest.DefaultRigImportOrder = append(manifest.DefaultRigImportOrder, bound.Binding) + } + } + return &manifest, nil +} + +func writeCityPackManifest(fs fsys.FS, cityPath string, manifest *cityPackManifest) error { + if manifest == nil { + manifest = &cityPackManifest{} + } + if manifest.Pack.Name == "" { + manifest.Pack.Name = defaultCityPackName(fs, cityPath) + } + if manifest.Pack.Schema == 0 { + manifest.Pack.Schema = cityPackSchema + } + if manifest.Imports == nil { + manifest.Imports = make(map[string]config.Import) + } + + var buf bytes.Buffer + body := cityPackManifestBody{ + Pack: manifest.Pack, + Imports: manifest.Imports, + AgentDefaults: manifest.AgentDefaults, + Agents: manifest.Agents, + NamedSessions: manifest.NamedSessions, + Services: manifest.Services, + Providers: manifest.Providers, + Upstreams: manifest.Upstreams, + Formulas: manifest.Formulas, + Patches: manifest.Patches, + Doctor: manifest.Doctor, + Commands: manifest.Commands, + Global: manifest.Global, + Pricing: manifest.Pricing, + } + if err := toml.NewEncoder(&buf).Encode(body); err != nil { + return fmt.Errorf("encoding pack.toml: %w", err) + } + if err := writeOrderedDefaultRigImports(&buf, manifest); err != nil { + return err + } + // Resolve before the rename: pack.toml may be a symlink into a checked-out + // repo, and renaming over the unresolved path would replace the link with a + // regular file and strand the stale manifest in the checked-in target. + writePath, err := fsys.ResolveSymlinks(fs, filepath.Join(cityPath, "pack.toml")) + if err != nil { + return err + } + // Refuse the rewrite when the on-disk pack.toml carries keys this binary + // does not recognize: the manifest round-trip would silently drop newer or + // manual keys at the checked-in target. + if err := config.GuardRewriteKeyLoss[cityPackManifest](fs, writePath); err != nil { + return err + } + return fsys.WriteFileAtomic(fs, writePath, buf.Bytes(), 0o644) +} + +func writeOrderedDefaultRigImports(buf *bytes.Buffer, manifest *cityPackManifest) error { + if manifest == nil || len(manifest.Defaults.Rig.Imports) == 0 { + return nil + } + + seen := make(map[string]bool, len(manifest.Defaults.Rig.Imports)) + names := make([]string, 0, len(manifest.Defaults.Rig.Imports)) + for _, name := range manifest.DefaultRigImportOrder { + if _, ok := manifest.Defaults.Rig.Imports[name]; ok && !seen[name] { + names = append(names, name) + seen[name] = true + } + } + var remaining []string + for name := range manifest.Defaults.Rig.Imports { + if !seen[name] { + remaining = append(remaining, name) + } + } + sort.Strings(remaining) + names = append(names, remaining...) + + for _, name := range names { + imp := manifest.Defaults.Rig.Imports[name] + fmt.Fprintf(buf, "\n[defaults.rig.imports.%s]\n", strconv.Quote(name)) //nolint:errcheck + if err := toml.NewEncoder(buf).Encode(imp); err != nil { + return fmt.Errorf("encoding defaults.rig.imports.%s: %w", name, err) + } + } + return nil +} + +func defaultCityPackName(fs fsys.FS, cityPath string) string { + cfg, err := config.Load(fs, filepath.Join(cityPath, "city.toml")) + if err == nil { + return config.EffectiveCityName(cfg, filepath.Base(cityPath)) + } + return filepath.Base(cityPath) +} diff --git a/internal/importsvc/source.go b/internal/importsvc/source.go new file mode 100644 index 0000000000..9645105fb1 --- /dev/null +++ b/internal/importsvc/source.go @@ -0,0 +1,180 @@ +package importsvc + +import ( + "errors" + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/gastownhall/gascity/internal/config" + "github.com/gastownhall/gascity/internal/fsys" + "github.com/gastownhall/gascity/internal/git" +) + +func deriveImportName(source string) string { + trimmed := strings.TrimSuffix(strings.TrimRight(source, "/"), ".git") + if i := strings.LastIndex(trimmed, "/"); i >= 0 { + trimmed = trimmed[i+1:] + } + if i := strings.LastIndex(trimmed, ":"); i >= 0 && !strings.Contains(trimmed, string(filepath.Separator)) { + trimmed = trimmed[i+1:] + } + return trimmed +} + +func isRemoteImportSource(source string) bool { + return strings.HasPrefix(source, "git@") || + strings.HasPrefix(source, "ssh://") || + strings.HasPrefix(source, "https://") || + strings.HasPrefix(source, "http://") || + strings.HasPrefix(source, "file://") || + strings.HasPrefix(source, "github.com/") +} + +func hasRepositoryRefInSource(source string) bool { + if i := strings.Index(source, "://"); i >= 0 { + return strings.Contains(source[i+3:], "#") + } + return strings.Contains(source, "#") +} + +// normalizeImportAddSource canonicalizes the user-supplied source. Remote git +// sources pass through unchanged; local paths are validated as pack targets and +// promoted to file:// repo sources when they sit at the HEAD of a git worktree. +// The boolean reports whether the resolved source is git-backed. +func normalizeImportAddSource(fs fsys.FS, cityPath, source string) (string, bool, error) { + if isRemoteImportSource(source) { + return source, true, nil + } + + targetDir, err := resolveImportAddPath(cityPath, source) + if err != nil { + return "", false, err + } + if err := validateImportPackTarget(fs, targetDir); err != nil { + return "", false, err + } + + canonical, ok, err := canonicalizeLocalGitImportSource(targetDir) + if err != nil { + return "", false, err + } + if ok { + return canonical, true, nil + } + return source, false, nil +} + +func resolveImportAddPath(cityPath, source string) (string, error) { + switch { + case strings.HasPrefix(source, "//"): + return filepath.Join(cityPath, strings.TrimPrefix(source, "//")), nil + case source == "~" || strings.HasPrefix(source, "~/"): + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("resolving home dir: %w", err) + } + return filepath.Join(home, strings.TrimPrefix(source, "~/")), nil + case filepath.IsAbs(source): + return source, nil + default: + return filepath.Join(cityPath, source), nil + } +} + +func validateImportPackTarget(fs fsys.FS, targetDir string) error { + info, err := fs.Stat(targetDir) + if err != nil { + return fmt.Errorf("resolving source: %w", err) + } + if !info.IsDir() { + return fmt.Errorf("source is not a directory") + } + packPath := filepath.Join(targetDir, "pack.toml") + if _, err := fs.Stat(packPath); err != nil { + return fmt.Errorf("invalid pack target: missing pack.toml") + } + if _, err := config.Load(fs, packPath); err != nil { + return fmt.Errorf("invalid pack target: %w", err) + } + return nil +} + +func canonicalizeLocalGitImportSource(targetDir string) (string, bool, error) { + repoRoot, ok, err := localGitRepoRoot(targetDir) + if err != nil || !ok { + return "", ok, err + } + resolvedTarget, err := filepath.EvalSymlinks(targetDir) + if err != nil { + resolvedTarget = targetDir + } + rel, err := filepath.Rel(repoRoot, resolvedTarget) + if err != nil { + return "", false, fmt.Errorf("computing import subpath: %w", err) + } + u := url.URL{Scheme: "file", Path: filepath.ToSlash(repoRoot)} + canonical := u.String() + if rel != "." { + canonical += "//" + filepath.ToSlash(rel) + } + return canonical, true, nil +} + +func localGitRepoRoot(targetDir string) (string, bool, error) { + cmd := exec.Command("git", "-C", targetDir, "rev-parse", "--show-toplevel") + // Strip git-locating env vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, ...) + // so the toplevel resolves from targetDir, not a parent repo leaked through + // a pre-commit hook or nested worktree tooling. + cmd.Env = git.SanitizedEnv() + out, err := cmd.CombinedOutput() + if err != nil { + text := string(out) + if strings.Contains(text, "not a git repository") { + return "", false, nil + } + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 128 { + return "", false, nil + } + return "", false, fmt.Errorf("probing git target: %w", err) + } + return strings.TrimSpace(string(out)), true, nil +} + +// lsRemoteHeadArgs builds the `git ls-remote HEAD` argument vector for the +// remote HEAD probe, prefixed with the untrusted-remote hardening overrides so +// the probe cannot follow a redirect off the fenced host or use an unexpected +// transport. +func lsRemoteHeadArgs(cloneURL string) []string { + args := git.UntrustedRemoteGitConfigArgs() + return append(args, "ls-remote", cloneURL, "HEAD") +} + +// defaultHeadCommit is the single network/git-fetch line for remote HEAD +// resolution. SSRF fencing for the HTTP handler must gate the source string +// before AddImport reaches this probe; the host fence alone is not sufficient, +// so this probe additionally disables HTTP redirect following and constrains +// git transports (git.UntrustedRemoteGitConfigArgs) so a fenced public host +// cannot redirect the probe to an internal target once the URL is shelled to +// git. +func defaultHeadCommit(source string) (string, error) { + cloneURL := config.NormalizeRemoteSource(source) + cmd := exec.Command("git", lsRemoteHeadArgs(cloneURL)...) + // Strip git-locating env vars so a leaked GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE + // (or config injection) from a parent pre-commit hook or worktree tooling + // cannot perturb how this remote HEAD probe runs. + cmd.Env = git.SanitizedEnv() + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("resolving HEAD for %q: %w", source, err) + } + fields := strings.Fields(string(out)) + if len(fields) == 0 { + return "", fmt.Errorf("resolving HEAD for %q: empty response", source) + } + return fields[0], nil +} diff --git a/internal/importsvc/source_test.go b/internal/importsvc/source_test.go new file mode 100644 index 0000000000..4cc3547e77 --- /dev/null +++ b/internal/importsvc/source_test.go @@ -0,0 +1,39 @@ +package importsvc + +import ( + "strings" + "testing" + + "github.com/gastownhall/gascity/internal/git" +) + +// TestLsRemoteHeadArgsHardened proves the remote HEAD probe is hardened against +// redirect-based SSRF: the SSRF host fence alone is not sufficient because git +// can follow a redirect off the fenced host, so the probe must carry the +// untrusted-remote git config overrides ahead of `ls-remote HEAD`. +func TestLsRemoteHeadArgsHardened(t *testing.T) { + const url = "https://github.com/example/tools.git" + args := lsRemoteHeadArgs(url) + + joined := strings.Join(args, " ") + if !strings.Contains(joined, "-c http.followRedirects=false") { + t.Errorf("HEAD probe args do not disable redirect following: %v", args) + } + if !strings.Contains(joined, "-c protocol.allow=never") { + t.Errorf("HEAD probe args do not constrain transports: %v", args) + } + + // The hardening flags must lead the subcommand, and the tail must be the + // ls-remote HEAD probe against the given URL. + hardening := git.UntrustedRemoteGitConfigArgs() + if len(args) < len(hardening)+3 { + t.Fatalf("args too short: %v", args) + } + tail := args[len(hardening):] + wantTail := []string{"ls-remote", url, "HEAD"} + for i, w := range wantTail { + if tail[i] != w { + t.Fatalf("tail[%d] = %q, want %q; full args %v", i, tail[i], w, args) + } + } +} diff --git a/internal/importsvc/testenv_import_test.go b/internal/importsvc/testenv_import_test.go new file mode 100644 index 0000000000..1d3712e8e0 --- /dev/null +++ b/internal/importsvc/testenv_import_test.go @@ -0,0 +1,5 @@ +// Code generated by go run scripts/add-testenv-import.go; DO NOT EDIT. + +package importsvc + +import _ "github.com/gastownhall/gascity/internal/testenv" diff --git a/internal/packman/cache.go b/internal/packman/cache.go index 17335461a5..b78c9f4a3f 100644 --- a/internal/packman/cache.go +++ b/internal/packman/cache.go @@ -252,11 +252,18 @@ func normalizeRemoteSource(source string) remoteSource { } func defaultRunGit(dir string, args ...string) (string, error) { + // The pack source URL can be attacker-influenced on the API import path, and + // this runner drives the network fetch/clone/ls-remote for it. Harden every + // invocation against redirect-based SSRF and transport abuse; the flags are + // inert for the local cache operations (rev-parse, checkout, reset, ...) that + // also flow through here. The remaining DNS-rebinding residual is documented + // at the pack SSRF fence (internal/api/pack_source_policy.go). cmdArgs := append([]string{ "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "core.untrackedCache=false", - }, args...) + }, gitutil.UntrustedRemoteGitConfigArgs()...) + cmdArgs = append(cmdArgs, args...) cmd := exec.Command("git", cmdArgs...) if dir != "" { cmd.Dir = dir diff --git a/internal/packman/cache_test.go b/internal/packman/cache_test.go index e688523ab1..e7317ddfa5 100644 --- a/internal/packman/cache_test.go +++ b/internal/packman/cache_test.go @@ -3,6 +3,7 @@ package packman import ( "fmt" "os" + "os/exec" "path/filepath" "reflect" "strings" @@ -394,3 +395,27 @@ func TestEnsureRepoInCacheReclonesCacheFileWithoutGit(t *testing.T) { t.Fatalf("EnsureRepoInCache path = %q, want %q", got, path) } } + +// TestDefaultRunGitBlocksDisallowedTransport is the regression for the API +// pack-import SSRF hardening: defaultRunGit drives the attacker-influenced +// clone/ls-remote, so it must constrain git transports. An ext:: URL (which +// would otherwise execute an arbitrary command) must be refused by the +// protocol allowlist rather than run. +func TestDefaultRunGitBlocksDisallowedTransport(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not available") + } + // ext:: is not in the allowlist; git must refuse it before running the + // command. Without the hardening, git would execute `true` and fail with a + // different (protocol-parse) error instead. + _, err := defaultRunGit("", "ls-remote", "ext::true") + if err == nil { + t.Fatal("defaultRunGit ran a disallowed ext:: transport; want a protocol block") + } + msg := err.Error() + blocked := strings.Contains(msg, "ext") && + (strings.Contains(msg, "not allowed") || strings.Contains(msg, "protocol")) + if !blocked { + t.Fatalf("error = %q; want a git transport 'ext' not allowed / protocol block", msg) + } +} diff --git a/internal/packman/install.go b/internal/packman/install.go index 6c50586ad3..6067406a85 100644 --- a/internal/packman/install.go +++ b/internal/packman/install.go @@ -26,6 +26,16 @@ const ( InstallUpgrade ) +// SourcePolicy validates a remote import source before packman resolves or +// fetches it. It is applied to every source in the reachable closure — the +// direct imports AND every transitive import discovered from a cached pack.toml +// — before any `ResolveVersion` (git ls-remote) or `EnsureRepoInCache` +// (clone/checkout) seam runs for that source. A non-nil error aborts the whole +// sync before that source is touched, so an accepted top-level pack cannot +// smuggle an internal, file, or link-local nested import past an API-layer +// fence. A nil policy (the trusted CLI/local path) allows every source. +type SourcePolicy func(source string) error + type packConfig struct { Imports map[string]config.Import `toml:"imports,omitempty"` } @@ -135,16 +145,24 @@ func EnsureBundledPacksCurrent(cityRoot string) error { // SyncLock resolves the reachable remote-import closure and returns the updated lock. func SyncLock(cityRoot string, imports map[string]config.Import, mode InstallMode) (*Lockfile, error) { - return syncLock(cityRoot, imports, mode, nil) + return syncLock(cityRoot, imports, mode, nil, nil) +} + +// SyncLockWithPolicy is SyncLock with an untrusted-source policy applied to every +// reachable source (direct and transitive) before it is resolved or fetched, so +// an accepted public pack cannot pull an internal or file-backed nested import +// past the caller's fence. A nil policy behaves exactly like SyncLock. +func SyncLockWithPolicy(cityRoot string, imports map[string]config.Import, mode InstallMode, policy SourcePolicy) (*Lockfile, error) { + return syncLock(cityRoot, imports, mode, nil, policy) } // SyncLockSelectiveUpgrade refreshes only the listed remote sources while // preserving every other reachable import from the existing lock when possible. func SyncLockSelectiveUpgrade(cityRoot string, imports map[string]config.Import, upgradeSources map[string]struct{}) (*Lockfile, error) { - return syncLock(cityRoot, imports, InstallResolveIfNeeded, upgradeSources) + return syncLock(cityRoot, imports, InstallResolveIfNeeded, upgradeSources, nil) } -func syncLock(cityRoot string, imports map[string]config.Import, mode InstallMode, upgradeSources map[string]struct{}) (*Lockfile, error) { +func syncLock(cityRoot string, imports map[string]config.Import, mode InstallMode, upgradeSources map[string]struct{}, policy SourcePolicy) (*Lockfile, error) { existing, err := ReadLockfile(fsys.OSFS{}, cityRoot) if err != nil { return nil, err @@ -154,8 +172,10 @@ func syncLock(cityRoot string, imports map[string]config.Import, mode InstallMod mode: mode, existing: existing, upgradeSources: upgradeSources, + policy: policy, chosen: make(map[string]LockedPack), refreshed: make(map[string]bool), + validated: make(map[string]bool), } constraints, reachable, err := mergeDirectConstraints(imports) @@ -191,8 +211,25 @@ type syncState struct { mode InstallMode existing *Lockfile upgradeSources map[string]struct{} + policy SourcePolicy chosen map[string]LockedPack refreshed map[string]bool + validated map[string]bool +} + +// checkPolicy runs the untrusted-source policy for source once per sync. It is +// the single gate every source passes before resolveSource resolves it or +// walkImport caches it, so a policy rejection aborts the sync before any git or +// cache seam runs for that source — the transitive-import fence. +func (s *syncState) checkPolicy(source string) error { + if s.policy == nil || s.validated[source] { + return nil + } + if err := s.policy(source); err != nil { + return err + } + s.validated[source] = true + return nil } func (s *syncState) ensureChosen(constraints map[string]string, reachable map[string]struct{}) (bool, error) { @@ -216,6 +253,14 @@ func (s *syncState) ensureChosen(constraints map[string]string, reachable map[st } func (s *syncState) resolveSource(source, constraint string) (bool, error) { + // Fence the source before any resolution or cache fetch. resolveSource is the + // choke point every reachable source (direct and transitive) flows through + // before it is chosen, and walkImport only caches already-chosen sources, so + // gating here blocks both the ResolveVersion and EnsureRepoInCache seams. + if err := s.checkPolicy(source); err != nil { + return false, err + } + forceUpgrade := s.mode == InstallUpgrade if !forceUpgrade && s.upgradeSources != nil { _, forceUpgrade = s.upgradeSources[source] diff --git a/internal/packman/install_test.go b/internal/packman/install_test.go index 419acadbd2..ed36ab31d7 100644 --- a/internal/packman/install_test.go +++ b/internal/packman/install_test.go @@ -56,6 +56,88 @@ schema = 1 } } +// TestSyncLockWithPolicyBlocksTransitiveInternalImport is the regression for the +// transitive-import SSRF finding: a public top-level pack that passes the caller's +// source fence can declare a nested internal/link-local/file import in its +// pack.toml, and SyncLock resolves that closure. SyncLockWithPolicy must apply the +// untrusted-source policy to every reachable source — including transitive ones — +// so the nested internal import is rejected before any git/cache seam runs for it. +func TestSyncLockWithPolicyBlocksTransitiveInternalImport(t *testing.T) { + home := t.TempDir() + city := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("GC_HOME", filepath.Join(home, ".gc")) + stubCachedPackGit(t) + + const internalSource = "http://169.254.169.254/b.git" + lock := &Lockfile{ + Packs: map[string]LockedPack{ + "https://example.com/a.git": {Version: "1.2.0", Commit: "aaaa", Fetched: time.Unix(10, 0).UTC()}, + internalSource: {Version: "2.0.0", Commit: "bbbb", Fetched: time.Unix(20, 0).UTC()}, + }, + } + if err := WriteLockfile(fsys.OSFS{}, city, lock); err != nil { + t.Fatalf("WriteLockfile: %v", err) + } + // A public top-level pack whose pack.toml pulls an internal transitive import. + stageCachedPack(t, "https://example.com/a.git", "aaaa", ` +[pack] +name = "a" +schema = 1 + +[imports.b] +source = "http://169.254.169.254/b.git" +version = "^2.0" +`) + stageCachedPack(t, internalSource, "bbbb", ` +[pack] +name = "b" +schema = 1 +`) + + direct := map[string]config.Import{ + "a": {Source: "https://example.com/a.git", Version: "^1.0"}, + } + + // Without a policy the closure walks cleanly, so the block below is provably the + // policy's doing and not a broken graph. + if got, err := SyncLock(city, direct, InstallFromLock); err != nil { + t.Fatalf("SyncLock (no policy): %v", err) + } else if len(got.Packs) != 2 { + t.Fatalf("SyncLock (no policy) len(Packs) = %d, want 2", len(got.Packs)) + } + + // The policy fences internal hosts. The direct public source passes; the + // transitive internal import must be rejected. + var consulted []string + policy := func(source string) error { + consulted = append(consulted, source) + if strings.Contains(source, "169.254.169.254") { + return fmt.Errorf("blocked internal source %q", source) + } + return nil + } + _, err := SyncLockWithPolicy(city, direct, InstallFromLock, policy) + if err == nil { + t.Fatal("SyncLockWithPolicy allowed a transitive internal import; want rejection") + } + if !strings.Contains(err.Error(), "169.254.169.254") { + t.Fatalf("error = %v, want it to name the blocked internal host", err) + } + if !contains(consulted, internalSource) { + t.Fatalf("policy was not consulted for the transitive internal source; saw %v", consulted) + } +} + +func contains(ss []string, want string) bool { + for _, s := range ss { + if s == want { + return true + } + } + return false +} + func TestSyncLockHonorsTransitiveFalse(t *testing.T) { home := t.TempDir() city := t.TempDir() diff --git a/test/integration/skill_lifecycle_test.go b/test/integration/skill_lifecycle_test.go index b0a6c4ea8c..5be4e46fcb 100644 --- a/test/integration/skill_lifecycle_test.go +++ b/test/integration/skill_lifecycle_test.go @@ -6,7 +6,7 @@ // supervisor tick uses — materialize.Run and the // end-to-end catalog discovery wiring — against a real filesystem. // Fast (no runtime.Provider spawned) but with real os.Symlink, -// os.Readlink, and filepath.EvalSymlinks behaviour. +// os.Readlink, and filepath.EvalSymlinks behavior. // // The spec-called-out "full add/edit/delete lifecycle with drain/ // restart observation" is covered in two layers: From 248222527187909314a6d68079f189583384f9a6 Mon Sep 17 00:00:00 2001 From: Jim Wordelman Date: Fri, 3 Jul 2026 10:24:02 -0700 Subject: [PATCH 17/77] fix(hook): prevent pool workers from adopting named-session work (#3863) ## What this changes `gc hook --claim` no longer lets a suffixed pool worker treat the bare pool template name as one of its own identities when adopting already-assigned work. That prevents a pool worker such as `builder-1` from picking up an in-progress bead owned by the named holder `builder`. Fresh routed claims still use the template route target, so unassigned work can continue to wake and claim through the normal pool path. The change only narrows the identity set used for adopting existing work. ## Review notes - The production change is in `cmd/gc/cmd_hook.go`, where `IdentityCandidates` and `RouteTargets` intentionally diverge. - `cmd/gc/cmd_hook_test.go` covers the suffixed-worker rejection case, the named-holder adoption guard, and the identity-candidate constructor contract. - This does not change pool demand calculation, named-session config, or other bare-template routing surfaces. ## Test plan - [x] `go test ./cmd/gc -run 'TestCmdHookClaimSuffixedPoolWorkerDoesNotAdoptBareTemplateInProgressWork|TestCmdHookClaimNamedHolderStillAdoptsOwnInProgressWork|TestPoolWorkerIdentityCandidatesExcludeBareTemplate' -count=1` - [x] `make test-fast-parallel` - [x] `go vet ./...` - [x] Release gate: [`release-gates/pool-worker-claim-identity-gate.md`](release-gates/pool-worker-claim-identity-gate.md) --------- Co-authored-by: quad341 --- cmd/gc/cmd_hook.go | 11 +- cmd/gc/cmd_hook_test.go | 189 ++++++++++++++++++ .../pool-worker-claim-identity-gate.md | 45 +++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 release-gates/pool-worker-claim-identity-gate.md diff --git a/cmd/gc/cmd_hook.go b/cmd/gc/cmd_hook.go index 2263af6eb9..e8fc53d57a 100644 --- a/cmd/gc/cmd_hook.go +++ b/cmd/gc/cmd_hook.go @@ -367,13 +367,22 @@ func cmdHookWithOptions(args []string, opts hookCommandOptions, stdout, stderr i assignee := firstNonEmptyHookValue(sessionName, sessionID, alias, agentForQuery, resolvedAgentName) claimOpts := hookClaimOptions{ Assignee: assignee, + // IdentityCandidates governs ADOPTION of already-owned in_progress/open + // work (hookClaimExistingOrAssigned); it must be scoped to this + // session's OWN runtime identity, never the bare pool template. A + // suffixed pool worker resolves config via the GC_TEMPLATE fallback, so + // resolvedAgentName == a.QualifiedName() is the bare template, which is + // ALSO the [[named_session]] holder's identity — including it let a + // suffixed worker adopt the holder's in_progress bead (ga-80pen8). The + // bare template stays in RouteTargets, which governs FRESH claims of + // UNASSIGNED routed work. The canonical slot / named holder keep it via + // `alias` (GC_ALIAS == qualified bare name); only suffixed workers drop it. IdentityCandidates: hookClaimIdentityCandidates( assignee, sessionID, sessionName, alias, agentForQuery, - resolvedAgentName, ), RouteTargets: hookClaimRouteTargets(hookClaimPrimaryRouteTarget(&a), resolvedAgentName, strings.TrimSpace(overrides["GC_TEMPLATE"])), Env: queryEnv, diff --git a/cmd/gc/cmd_hook_test.go b/cmd/gc/cmd_hook_test.go index bb71436117..e6cf3488f6 100644 --- a/cmd/gc/cmd_hook_test.go +++ b/cmd/gc/cmd_hook_test.go @@ -1548,6 +1548,195 @@ esac } } +// TestCmdHookClaimSuffixedPoolWorkerDoesNotAdoptBareTemplateInProgressWork is +// the ga-80pen8 end-to-end regression: "builder" is BOTH a [[named_session]] +// holder's own identity AND a pool template shared by suffixed instances +// (max_active_sessions > 1), mirroring the config shape confirmed in the +// field incident. A suffixed pool worker resolves its config via the +// GC_TEMPLATE fallback, so its resolvedAgentName is the bare template — which +// is ALSO the named holder's identity. Before the fix, that let the worker +// adopt the holder's in_progress bead through hookClaimExistingOrAssigned +// without ever going through the store.Claim CAS, so two identities worked +// (and closed) the same bead. The worker must instead drain no_work, and the +// claim mutation must never run for a bead it does not own. +func TestCmdHookClaimSuffixedPoolWorkerDoesNotAdoptBareTemplateInProgressWork(t *testing.T) { + clearGCEnv(t) + disableManagedDoltRecoveryForTest(t) + cityDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(cityDir, ".gc"), 0o755); err != nil { + t.Fatal(err) + } + cityToml := `[workspace] +name = "test-city" + +[[agent]] +name = "builder" +max_active_sessions = 3 +work_query = "printf '[{\"id\":\"ga-frpt4k\",\"status\":\"in_progress\",\"assignee\":\"builder\",\"metadata\":{\"gc.routed_to\":\"builder\"}}]'" + +[[named_session]] +template = "builder" +mode = "on_demand" +` + if err := os.WriteFile(filepath.Join(cityDir, "city.toml"), []byte(cityToml), 0o644); err != nil { + t.Fatal(err) + } + + fakeBin := t.TempDir() + logPath := filepath.Join(t.TempDir(), "bd.log") + script := fmt.Sprintf(`#!/bin/sh +printf '%%s\n' "$*" >> %q +printf '[]' +`, logPath) + if err := os.WriteFile(filepath.Join(fakeBin, "bd"), []byte(script), 0o755); err != nil { + t.Fatal(err) + } + + t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("GC_CITY", cityDir) + // Suffixed pool worker: GC_TEMPLATE is the bare pool binding, GC_ALIAS and + // GC_SESSION_NAME are this instance's own suffixed runtime identity. + t.Setenv("GC_TEMPLATE", "builder") + t.Setenv("GC_ALIAS", "builder-1") + t.Setenv("GC_SESSION_NAME", "builder-1") + t.Setenv("GC_SESSION_ID", "session-builder-1") + + var stdout, stderr bytes.Buffer + code := cmdHookWithOptions(nil, hookCommandOptions{Claim: true, JSON: true}, &stdout, &stderr) + if code != 1 { + t.Fatalf("cmdHookWithOptions(--claim, suffixed pool worker) = %d, want 1 (no_work drain); stdout=%q stderr=%s", code, stdout.String(), stderr.String()) + } + var result hookClaimJSONResult + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("stdout is not JSON: %v\nraw: %s", err, stdout.String()) + } + if result.Action == "work" && result.Reason == "existing_assignment" { + t.Fatalf("REGRESSION ga-80pen8: suffixed pool worker %q adopted named holder %q's in_progress bead %q (%+v)", + "builder-1", "builder", result.BeadID, result) + } + if result.Action != "drain" || result.Reason != "no_work" { + t.Fatalf("result = %+v, want action=drain reason=no_work", result) + } + // A foreign in_progress bead must never reach the claim mutation. bd may + // not run at all once the candidate is excluded from both the adoption + // and fresh-claim paths — that's an even stronger signal than an empty + // log, so a missing log file is not a failure. + logData, err := os.ReadFile(logPath) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("ReadFile(%s): %v", logPath, err) + } + if strings.Contains(string(logData), "--claim") { + t.Fatalf("claim mutation ran for a foreign in_progress bead; bd log:\n%s", logData) + } +} + +// TestCmdHookClaimNamedHolderStillAdoptsOwnInProgressWork is the companion +// guard for ga-80pen8: the named-session holder (or a canonical max=1 pool +// slot) whose own runtime identity IS the bare template must still adopt its +// own in_progress bead. Its alias/assignee already carry the bare qualified +// name via GC_ALIAS independent of resolvedAgentName, so the fix (dropping +// resolvedAgentName from the suffixed-worker IdentityCandidates) must not +// change this case. +func TestCmdHookClaimNamedHolderStillAdoptsOwnInProgressWork(t *testing.T) { + clearGCEnv(t) + disableManagedDoltRecoveryForTest(t) + cityDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(cityDir, ".gc"), 0o755); err != nil { + t.Fatal(err) + } + cityToml := `[workspace] +name = "test-city" + +[[agent]] +name = "builder" +max_active_sessions = 3 +work_query = "printf '[{\"id\":\"ga-frpt4k\",\"status\":\"in_progress\",\"assignee\":\"builder\",\"metadata\":{\"gc.routed_to\":\"builder\"}}]'" + +[[named_session]] +template = "builder" +mode = "on_demand" +` + if err := os.WriteFile(filepath.Join(cityDir, "city.toml"), []byte(cityToml), 0o644); err != nil { + t.Fatal(err) + } + + fakeBin := t.TempDir() + if err := os.WriteFile(filepath.Join(fakeBin, "bd"), []byte("#!/bin/sh\nprintf '[]'\n"), 0o755); err != nil { + t.Fatal(err) + } + + t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("GC_CITY", cityDir) + // Named holder / canonical slot: GC_ALIAS IS the bare template. + t.Setenv("GC_TEMPLATE", "builder") + t.Setenv("GC_ALIAS", "builder") + t.Setenv("GC_SESSION_NAME", "builder-session") + t.Setenv("GC_SESSION_ID", "session-builder") + + var stdout, stderr bytes.Buffer + code := cmdHookWithOptions(nil, hookCommandOptions{Claim: true, JSON: true}, &stdout, &stderr) + if code != 0 { + t.Fatalf("cmdHookWithOptions(--claim, named holder) = %d, want 0; stdout=%q stderr=%s", code, stdout.String(), stderr.String()) + } + var result hookClaimJSONResult + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("stdout is not JSON: %v\nraw: %s", err, stdout.String()) + } + if result.Action != "work" || result.Reason != "existing_assignment" || result.BeadID != "ga-frpt4k" { + t.Fatalf("over-fix regression: named holder no longer adopts its own in_progress bead: %+v", result) + } +} + +// TestPoolWorkerIdentityCandidatesExcludeBareTemplate is a mechanism-level +// guard for ga-80pen8, pinned to the post-fix contract: a suffixed pool +// worker's claim IdentityCandidates must never include the bare pool +// template, because the bare template is also the [[named_session]] holder's +// own identity. Including it let a suffixed worker adopt the holder's +// in_progress bead via hookClaimExistingOrAssigned without ever reaching the +// store.Claim CAS. +func TestPoolWorkerIdentityCandidatesExcludeBareTemplate(t *testing.T) { + const ( + poolTemplate = "gascity/builder" // bare template == named-session holder's identity + workerReal = "gascity/builder-1" // this suffixed pool worker's own identity + foreignBeadID = "ga-frpt4k" + ) + // Fixed contract: cmd_hook.go's --claim block passes only this session's + // own runtime identity (assignee, sessionID, sessionName, alias, + // agentForQuery) into hookClaimIdentityCandidates — never the bare + // template resolved via the GC_TEMPLATE fallback. + identityCandidates := hookClaimIdentityCandidates(workerReal, "", workerReal, workerReal, workerReal) + runner := func(string, string) (string, error) { + return `[{"id":"` + foreignBeadID + `","status":"in_progress","assignee":"` + poolTemplate + + `","metadata":{"gc.routed_to":"` + poolTemplate + `"}}]`, nil + } + ops := hookClaimOps{ + Runner: runner, + Claim: func(context.Context, string, []string, string, string) (beads.Bead, bool, error) { + t.Fatal("store.Claim ran: a foreign in_progress bead must never reach the CAS") + return beads.Bead{}, false, nil + }, + } + opts := hookClaimOptions{ + Assignee: workerReal, + IdentityCandidates: identityCandidates, + RouteTargets: hookClaimRouteTargets(poolTemplate, poolTemplate), + JSON: true, + } + var stdout, stderr bytes.Buffer + code := doHookClaim("bd ready --json", "/tmp/work", opts, ops, &stdout, &stderr) + var result hookClaimJSONResult + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("stdout is not JSON: %v\nraw: %s", err, stdout.String()) + } + if result.Action == "work" && result.Reason == "existing_assignment" { + t.Fatalf("REGRESSION ga-80pen8: pool worker %q adopted %q's in_progress bead %q (%+v)", + workerReal, poolTemplate, result.BeadID, result) + } + if result.Action != "drain" || result.Reason != "no_work" || code != 1 { + t.Fatalf("want no_work drain, got action=%q reason=%q code=%d", result.Action, result.Reason, code) + } +} + func TestHookInjectAlwaysExitsZero(t *testing.T) { // Even on command failure, inject mode exits 0. runner := func(string, string) (string, error) { return "", fmt.Errorf("command failed") } diff --git a/release-gates/pool-worker-claim-identity-gate.md b/release-gates/pool-worker-claim-identity-gate.md new file mode 100644 index 0000000000..15f70ef6ea --- /dev/null +++ b/release-gates/pool-worker-claim-identity-gate.md @@ -0,0 +1,45 @@ +# Release Gate: Pool worker claim identity + +Bead: ga-h9lsg4 +Source bead: ga-aq6xfs +Implementation bead: ga-0cymgz +Branch under review: builder/ga-0cymgz +Reviewed commit: 6ee9ba04f49df788a5d7ec134c4b02d44c9d9d6c +Gate date: 2026-07-01 + +Note: docs/PROJECT_MANIFEST.md is not present in this worktree. This gate uses +the deployer release criteria and the repo testing guidance in TESTING.md. + +## Gate Results + +| # | Criterion | Result | Evidence | +|---|-----------|--------|----------| +| 1 | Review PASS present | PASS | Review bead ga-aq6xfs is closed with `REVIEW VERDICT: PASS`; deploy bead ga-h9lsg4 was created by reviewer-gm-u4aay with reviewed commit 6ee9ba04f49df788a5d7ec134c4b02d44c9d9d6c. | +| 2 | Acceptance criteria met | PASS | `cmd/gc/cmd_hook.go` removes `resolvedAgentName` from `IdentityCandidates` while retaining it in `RouteTargets`. The branch adds the required suffixed-worker acceptance test, named-holder over-fix guard, and mechanism-level identity-candidate test. | +| 3 | Tests pass | PASS | `go test ./cmd/gc -run 'TestCmdHookClaimSuffixedPoolWorkerDoesNotAdoptBareTemplateInProgressWork|TestCmdHookClaimNamedHolderStillAdoptsOwnInProgressWork|TestPoolWorkerIdentityCandidatesExcludeBareTemplate' -count=1` passed. `make test-fast-parallel` passed all 8 fast shards. `go vet ./...` passed. | +| 4 | No high-severity review findings open | PASS | Reviewer notes list no unresolved HIGH findings; the review classifies the change as an access-control correctness improvement with no new attack surface. | +| 5 | Final branch is clean | PASS | No uncommitted changes before gate file creation; this gate file is committed as the branch tip. | +| 6 | Branch diverges cleanly from main | PASS | `git merge-tree --write-tree origin/main HEAD` succeeded and produced tree d4889ccb4dd55e295788597b4a03757ed70f77a1. | +| 7 | Single feature theme | PASS | The commit set touches one subsystem: `gc hook --claim` identity adoption behavior in `cmd/gc`, plus tests for that behavior. | + +## Acceptance Checks + +- PASS: Suffixed pool workers no longer include the bare pool template in + adoption identity candidates. +- PASS: Fresh-claim routing still includes the resolved template through + `RouteTargets`. +- PASS: Named holders and canonical slots still adopt their own in-progress + work. +- PASS: The change is scoped to `cmd/gc/cmd_hook.go` and + `cmd/gc/cmd_hook_test.go`. + +## Commands + +```text +gofmt -l cmd/gc/cmd_hook.go cmd/gc/cmd_hook_test.go +go test ./cmd/gc -run 'TestCmdHookClaimSuffixedPoolWorkerDoesNotAdoptBareTemplateInProgressWork|TestCmdHookClaimNamedHolderStillAdoptsOwnInProgressWork|TestPoolWorkerIdentityCandidatesExcludeBareTemplate' -count=1 +make test-fast-parallel +go vet ./... +git diff --check origin/main...HEAD +git merge-tree --write-tree origin/main HEAD +``` From 38c51fd5f26e23a6440ec6699c1575811dc83e19 Mon Sep 17 00:00:00 2001 From: William Bernting Date: Fri, 3 Jul 2026 19:43:05 +0200 Subject: [PATCH 18/77] fix(hook): skip self-blocked routed beads so a blocked head doesn't force an idle exit (#3881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Extend `filterUnreadyHookCandidates` to also drop a candidate carrying its own `is_blocked == true` (or `status == "blocked"`), and widen the routed-tier query so a blocked head has the rest of the ready routed work behind it to fall through to. ## Motivation `filterUnreadyHookCandidates` (from #2124) currently filters two things: future `defer_until`, and open deps in `blocked_by`. It does **not** look at the bead's *own* `is_blocked`/`status`. Separately, the routed ready-tier query uses `--limit=1`. Together, a single blocked bead at the head of the routed tier can be the only candidate the hook sees — and the agent idle-exits with ready work right behind it. This sits inside a line of work already in progress upstream: - #2124 established defensive hook-layer readiness filtering — this extends that exact function. - #3818 makes the *claim loop* skip an unclaimable candidate instead of wedging — complementary, different layer (`cmd_hook_claim.go` vs the selection filter in `cmd_hook.go`). - #3819 / #3827 harden `is_blocked` accuracy in the ready projection — which *strengthens* this filter (it trusts `is_blocked` when present, treats absent as not-blocked). ## What changed - `cmd/gc/cmd_hook.go`: `isSelfBlockedHookCandidate` added to the filter. - `internal/config/config.go`: routed-tier query `--limit=1 → 20`. The workflow/`run_target` tier in the same query already uses `--limit=20`, so this makes the routed tier consistent with a sibling rather than introducing a new pattern. - Regression tests for the self-blocked skip and the fall-through. ## Testing - `go build ./...`, `go vet` on touched packages. - `go test ./cmd/gc/ -run 'Hook|Blocked|Defer'` and `go test ./internal/config/` pass locally. - Full suite via CI. ## Open questions / happy to adjust - The `1 → 20` widening changes routed-tier query cost. We matched the workflow tier's existing `20`, but if you'd prefer a smaller widen — or to solve this at the store/projection layer (à la #3819) rather than in the hook filter — happy to follow that. - Absent `is_blocked` is treated as not-blocked (fail-open) to avoid starving work when the projection is sparse; say the word if you'd rather fail-closed. ## References #2124, #3818, #3819, #3827, #3817. --------- Co-authored-by: wbern --- cmd/gc/cmd_hook.go | 28 ++++++++++++--- cmd/gc/cmd_hook_claim_test.go | 39 ++++++++++++++++++++ cmd/gc/hook_defer_blocked_test.go | 59 +++++++++++++++++++++++++++++++ internal/config/config.go | 8 +++-- internal/config/config_test.go | 18 +++++----- 5 files changed, 137 insertions(+), 15 deletions(-) diff --git a/cmd/gc/cmd_hook.go b/cmd/gc/cmd_hook.go index e8fc53d57a..a156d32bad 100644 --- a/cmd/gc/cmd_hook.go +++ b/cmd/gc/cmd_hook.go @@ -662,10 +662,11 @@ func workQueryHasReadyWork(output string) bool { } // filterUnreadyHookCandidates strips beads from work_query output that fail -// bd ready semantics: future defer_until, or any open blocking dep in the -// row's blocked_by array. The work_query is expected to gate these, but -// defensive filtering here prevents a single broken query from cascading -// into agent action on a bead it cannot progress. +// bd ready semantics: future defer_until, any open blocking dep in the row's +// blocked_by array, or the row's own is_blocked / status=="blocked" marker. +// The work_query is expected to gate these, but defensive filtering here +// prevents a single broken query from cascading into agent action on a bead +// it cannot progress. // Pure function over JSON; takes time.Time so tests stay deterministic. func filterUnreadyHookCandidates(output string, now time.Time) string { if output == "" { @@ -692,6 +693,9 @@ func filterUnreadyHookCandidates(output string, now time.Time) string { if isDepBlockedHookCandidate(obj) { continue } + if isSelfBlockedHookCandidate(obj) { + continue + } filtered = append(filtered, obj) } reencoded, err := json.Marshal(filtered) @@ -739,6 +743,22 @@ func isDepBlockedHookCandidate(item map[string]any) bool { return false } +// isSelfBlockedHookCandidate reports whether a candidate carries bd's own +// is_blocked marker or an explicit status=="blocked", independent of the +// blocked_by dependency array checked by isDepBlockedHookCandidate. An +// absent is_blocked field is treated as NOT blocked — bd's denormalized +// projection is not always populated, and over-filtering here would strand +// otherwise-ready work. +func isSelfBlockedHookCandidate(item map[string]any) bool { + if blocked, ok := item["is_blocked"].(bool); ok && blocked { + return true + } + if status, ok := item["status"].(string); ok && strings.EqualFold(strings.TrimSpace(status), "blocked") { + return true + } + return false +} + func normalizeWorkQueryOutput(output string) string { if output == "" { return output diff --git a/cmd/gc/cmd_hook_claim_test.go b/cmd/gc/cmd_hook_claim_test.go index 3f1c726e90..3d76305a9e 100644 --- a/cmd/gc/cmd_hook_claim_test.go +++ b/cmd/gc/cmd_hook_claim_test.go @@ -92,3 +92,42 @@ func TestDoHookClaimUsesSelectedStoreContextForMutationAndContinuation(t *testin t.Fatalf("assignedBead = %q, want sib-1", assignedBead) } } + +// TestDoHookClaimSkipsBlockedRoutedHeadAndClaimsReadyBehindIt guards the +// widened-routed-tier fix: a routed tier's oldest candidate can be +// is_blocked (e.g. gated on a PR), and the hook must fall through to a +// Ready routed bead behind it rather than idle-exiting on the blocked head. +func TestDoHookClaimSkipsBlockedRoutedHeadAndClaimsReadyBehindIt(t *testing.T) { + candidates := []beads.Bead{ + {ID: "blocked-head", Status: "open", IsBlocked: boolPtr(true), Metadata: map[string]string{"gc.routed_to": "route-1"}}, + {ID: "ready-behind", Status: "open", Metadata: map[string]string{"gc.routed_to": "route-1"}}, + } + output, err := json.Marshal(candidates) + if err != nil { + t.Fatalf("marshal candidates: %v", err) + } + + var claimedBead string + ops := hookClaimOps{ + Runner: func(string, string) (string, error) { return string(output), nil }, + Claim: func(_ context.Context, _ string, _ []string, beadID, assignee string) (beads.Bead, bool, error) { + claimedBead = beadID + return beads.Bead{ID: beadID, Assignee: assignee, Status: "in_progress"}, true, nil + }, + DrainAck: func(io.Writer) error { return nil }, + } + + var stdout, stderr bytes.Buffer + code := doHookClaim("query", ".", hookClaimOptions{ + Assignee: "worker-1", + IdentityCandidates: []string{"worker-1"}, + RouteTargets: []string{"route-1"}, + JSON: true, + }, ops, &stdout, &stderr) + if code != 0 { + t.Fatalf("doHookClaim() = %d, want 0; stderr=%s", code, stderr.String()) + } + if claimedBead != "ready-behind" { + t.Fatalf("claimedBead = %q, want ready-behind (blocked-head must be skipped)", claimedBead) + } +} diff --git a/cmd/gc/hook_defer_blocked_test.go b/cmd/gc/hook_defer_blocked_test.go index 874f270385..0a069e8046 100644 --- a/cmd/gc/hook_defer_blocked_test.go +++ b/cmd/gc/hook_defer_blocked_test.go @@ -51,6 +51,65 @@ func TestDoHookFiltersDepBlockedBeads(t *testing.T) { } } +func TestDoHookFiltersIsBlockedBeads(t *testing.T) { + runner := func(_, _ string) (string, error) { + return `[ + {"id":"blocked-head","status":"open","is_blocked":true}, + {"id":"ready-behind","status":"open","is_blocked":false} + ]`, nil + } + + var stdout, stderr bytes.Buffer + code := doHook("bd ready", ".", false, runner, &stdout, &stderr) + if code != 0 { + t.Fatalf("doHook() = %d, want 0; stderr=%s", code, stderr.String()) + } + out := stdout.String() + if strings.Contains(out, "blocked-head") { + t.Errorf("is_blocked bead surfaced in hook output: %s", out) + } + if !strings.Contains(out, "ready-behind") { + t.Errorf("ready bead behind blocked head missing from hook output: %s", out) + } +} + +func TestDoHookFiltersStatusBlockedBeads(t *testing.T) { + runner := func(_, _ string) (string, error) { + return `[ + {"id":"status-blocked","status":"blocked"}, + {"id":"clear-2","status":"open"} + ]`, nil + } + + var stdout, stderr bytes.Buffer + code := doHook("bd ready", ".", false, runner, &stdout, &stderr) + if code != 0 { + t.Fatalf("doHook() = %d, want 0; stderr=%s", code, stderr.String()) + } + out := stdout.String() + if strings.Contains(out, "status-blocked") { + t.Errorf("status=blocked bead surfaced in hook output: %s", out) + } + if !strings.Contains(out, "clear-2") { + t.Errorf("ready bead missing from hook output: %s", out) + } +} + +func TestDoHookKeepsAbsentIsBlocked(t *testing.T) { + runner := func(_, _ string) (string, error) { + return `[{"id":"no-is-blocked-field","status":"open"}]`, nil + } + + var stdout, stderr bytes.Buffer + code := doHook("bd ready", ".", false, runner, &stdout, &stderr) + if code != 0 { + t.Fatalf("doHook() = %d, want 0; stderr=%s", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "no-is-blocked-field") { + t.Errorf("bead with absent is_blocked treated as blocked: %s", stdout.String()) + } +} + func TestDoHookKeepsPastDeferredAndClosedBlockers(t *testing.T) { past := "2000-01-01T00:00:00Z" runner := func(_, _ string) (string, error) { diff --git a/internal/config/config.go b/internal/config/config.go index 40b7d40259..1a1cf8406c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3595,8 +3595,12 @@ func poolDemandFirstRowFunctionScript(includeEphemeralReady bool) string { func routedReadyTierCommand(includeEphemeralReady bool) string { // The shared predicate stays order-free so the count-form does no wasted - // sorting; the worker first-row path asks bd for the oldest candidate. - return bdReadyPoolDemandShell("--sort oldest --limit=1", includeEphemeralReady) + ` 2>/dev/null` + // sorting; the worker first-row path asks bd for the oldest candidates. + // The tier is widened past a single row (limit=20, not limit=1) so a + // self-blocked head (is_blocked / status==blocked) has Ready routed work + // behind it to fall through to instead of idle-exiting; the hook layer + // (filterUnreadyHookCandidates) strips the blocked head from the result. + return bdReadyPoolDemandShell("--sort oldest --limit=20", includeEphemeralReady) + ` 2>/dev/null` } // poolDemandCountShell emits the reconciler count-form for target: it counts diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f248a0389e..e88b14e8bc 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1829,7 +1829,7 @@ func TestEffectiveWorkQueryDefault(t *testing.T) { if strings.Contains(got, `--include-ephemeral`) { t.Errorf("EffectiveWorkQuery() default must be bd 1.0.4-compatible without --include-ephemeral: %q", got) } - if !strings.Contains(got, `bd ready --metadata-field "gc.routed_to=$target" --unassigned --exclude-type=epic --json --sort oldest --limit=1`) { + if !strings.Contains(got, `bd ready --metadata-field "gc.routed_to=$target" --unassigned --exclude-type=epic --json --sort oldest --limit=20`) { t.Errorf("EffectiveWorkQuery() missing tier 3 pool-demand probe: %q", got) } if !strings.Contains(got, "-- mayor") { @@ -1851,7 +1851,7 @@ func TestEffectiveWorkQueryDefault(t *testing.T) { func TestEffectiveWorkQueryBD105CompatibilityOptIn(t *testing.T) { a := Agent{Name: "mayor"} got := a.EffectiveWorkQueryForBeads(BeadsConfig{BDCompatibility: BeadsBDCompatibility105}) - if !strings.Contains(got, `bd ready --include-ephemeral --metadata-field "gc.routed_to=$target" --unassigned --exclude-type=epic --json --sort oldest --limit=1`) { + if !strings.Contains(got, `bd ready --include-ephemeral --metadata-field "gc.routed_to=$target" --unassigned --exclude-type=epic --json --sort oldest --limit=20`) { t.Errorf("EffectiveWorkQueryForBeads(bd-1.0.5) missing include-ephemeral routed probe: %q", got) } if !strings.Contains(got, `bd ready --include-ephemeral --assignee="$id" --json --limit=1`) { @@ -2224,13 +2224,13 @@ func TestEffectiveWorkQueryControlDispatcherClaimsLegacyUnassignedRoute(t *testi out := runEffectiveWorkQuery(t, a, nil, `#!/bin/sh set -eu case "$*" in - *"ready --include-ephemeral"*"--metadata-field gc.routed_to=gascity/control-dispatcher"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=1"*) + *"ready --include-ephemeral"*"--metadata-field gc.routed_to=gascity/control-dispatcher"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=20"*) printf '[]' ;; - *"ready --metadata-field gc.routed_to=gascity/control-dispatcher"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=1"*) + *"ready --metadata-field gc.routed_to=gascity/control-dispatcher"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=20"*) printf '[]' ;; - *"ready --metadata-field gc.routed_to=gascity/workflow-control"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=1"*) + *"ready --metadata-field gc.routed_to=gascity/workflow-control"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=20"*) printf '[{"id":"ga-legacy-route"}]' ;; *) @@ -2259,7 +2259,7 @@ func TestEffectiveWorkQueryRoutedQueueUsesNativeOldestSortAcrossReadyTiers(t *te }, `#!/bin/sh set -eu case "$*" in - "ready --metadata-field gc.routed_to=hello-world/worker --unassigned --exclude-type=epic --json --sort oldest --limit=1") + "ready --metadata-field gc.routed_to=hello-world/worker --unassigned --exclude-type=epic --json --sort oldest --limit=20") printf '[{"id":"older-no-history","priority":2,"created_at":"2026-05-20T06:09:30Z","no_history":true}]' ;; *) @@ -2304,7 +2304,7 @@ func TestEffectiveWorkQueryRoutedQueueUsesOldestBeforePriority(t *testing.T) { }, `#!/bin/sh set -eu case "$*" in - *"ready --metadata-field gc.routed_to=hello-world/worker"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=1"*) + *"ready --metadata-field gc.routed_to=hello-world/worker"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=20"*) printf '[{"id":"older-p2","priority":2,"created_at":"2026-05-20T06:09:30Z"}]' ;; *) @@ -2327,7 +2327,7 @@ func TestEffectiveWorkQueryRoutedFallbackUsesNativeOldestSort(t *testing.T) { }, `#!/bin/sh set -eu case "$*" in - *"ready --metadata-field gc.routed_to=hello-world/worker"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=1"*) + *"ready --metadata-field gc.routed_to=hello-world/worker"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=20"*) printf '[]' ;; *"ready --metadata-field gc.run_target=hello-world/worker"*"--metadata-field gc.kind=workflow"*"--unassigned"*"--exclude-type=epic"*"--json"*"--sort oldest"*"--limit=20"*) @@ -2724,7 +2724,7 @@ func TestPoolDemandPredicateSharedWithWorkQuery(t *testing.T) { t.Run(tt.name, func(t *testing.T) { wq := tt.agent.EffectiveWorkQuery() demand := tt.agent.EffectivePoolDemandQuery() - workPredicate := bdReadyPoolDemandShell("--sort oldest --limit=1", false) + workPredicate := bdReadyPoolDemandShell("--sort oldest --limit=20", false) if !strings.Contains(wq, workPredicate) { t.Errorf("EffectiveWorkQuery() missing shared predicate %q in %q", workPredicate, wq) } From b234f64978aa2e72d8ac2ba60146c890b8ab5c19 Mon Sep 17 00:00:00 2001 From: Stephanie Jarmak <36544495+sjarmak@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:37:09 -0700 Subject: [PATCH 19/77] fix(dispatch): resolve pack-relative ralph check_path against pack/city root when work_dir lacks it (#3782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3008 ## Problem A pack-relative `gc.check_path` (e.g. `assets//scripts/check.sh`) names a pack-shipped script that lives under the store/city root, **not** the per-task `gc.work_dir` worktree. When the control bead carries a `work_dir` pointing at a worktree that does not contain the pack tree, `runRalphCheck` set `scriptBase=work_dir`; the relative join `/assets/...` did not exist, `ResolveConditionPath` returned a not-exist error, and the control-dispatcher **quarantined the gate while letting the step advance unevaluated**. ## Fix Fall back to the store/city root for a relative `check_path` when the worktree join misses with `fs.ErrNotExist` — exactly the base used when `work_dir` is empty, so **no new trusted root is introduced** and `ResolveConditionPath`'s containment checks still apply. - The fallback fires only on a not-exist miss, so a check that *does* exist under the worktree keeps precedence. - The original `work_dir` error is preserved when the fallback also misses. - Absolute paths keep their existing behavior. This is issue option 3 (least-surprising, no new config surface) — no `$PACK_DIR` expansion / anchor syntax added. ## Tests `internal/dispatch/ralph_test.go` (+99): reproduces a pack-relative check path with `gc.work_dir` pointing at a worktree lacking the pack tree, asserts the gate resolves (not control_quarantined), plus worktree-precedence and fallback-also-misses cases. `go build` + `go vet` + `internal/dispatch` suite green. Co-authored-by: sjarmak --- internal/dispatch/ralph.go | 16 ++++++ internal/dispatch/ralph_test.go | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/internal/dispatch/ralph.go b/internal/dispatch/ralph.go index 0e8042141e..0f1f78eb16 100644 --- a/internal/dispatch/ralph.go +++ b/internal/dispatch/ralph.go @@ -2,7 +2,9 @@ package dispatch import ( "context" + "errors" "fmt" + "io/fs" "path/filepath" "sort" "strconv" @@ -196,6 +198,20 @@ func runRalphCheck(store beads.Store, bead, subject beads.Bead, attempt int, opt return convergence.GateResult{}, fmt.Errorf("%s: absolute gc.check_path %q escapes trusted roots", bead.ID, checkPath) } scriptPath, err := convergence.ResolveConditionPath(cityPath, scriptBase, checkPath) + if err != nil && scriptBase != storePath && !filepath.IsAbs(checkPath) && errors.Is(err, fs.ErrNotExist) { + // Pack-shipped check scripts live in the pack/city tree, not the + // per-task gc.work_dir worktree, so a relative gc.check_path joined + // against a work_dir worktree that lacks the pack tree resolves to a + // nonexistent path (gastownhall/gascity#3008). Fall back to the + // store/city root — exactly the base used when work_dir is empty, so + // it introduces no new trusted root and stays subject to + // ResolveConditionPath's containment checks. Only on a not-exist miss, + // so a check that does exist under the worktree keeps precedence; the + // original work_dir error is preserved when the fallback also misses. + if fallbackPath, fallbackErr := convergence.ResolveConditionPath(cityPath, storePath, checkPath); fallbackErr == nil { + scriptPath, err = fallbackPath, nil + } + } if err != nil { return convergence.GateResult{}, fmt.Errorf("%s: resolving check path: %w", bead.ID, err) } diff --git a/internal/dispatch/ralph_test.go b/internal/dispatch/ralph_test.go index c7d927d17f..5fbe78b310 100644 --- a/internal/dispatch/ralph_test.go +++ b/internal/dispatch/ralph_test.go @@ -149,6 +149,105 @@ func TestResolveRalphCheckMoleculePaths_UnsafeRootID(t *testing.T) { } } +// TestRunRalphCheckPackRelativeCheckPathWorkDirFallback covers +// gastownhall/gascity#3008: a pack-relative gc.check_path +// (e.g. assets//scripts/check.sh) names a pack-shipped script that lives +// under the store/city root, not the per-task gc.work_dir worktree. When the +// control bead carries a work_dir pointing at a worktree that lacks the pack +// tree, the relative join /assets/... does not exist and the check +// was control-quarantined. The fallback resolves the relative path against the +// store root instead, so the gate is evaluated. +func TestRunRalphCheckPackRelativeCheckPathWorkDirFallback(t *testing.T) { + cityPath := t.TempDir() + // Pack-shipped check script lives under the city/store root. + checkRel := filepath.Join("assets", "demo-pack", "scripts", "check.sh") + storeScript := filepath.Join(cityPath, checkRel) + if err := os.MkdirAll(filepath.Dir(storeScript), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(storeScript, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + // Per-task worktree under the city root that does NOT contain the pack tree. + workDir := filepath.Join(cityPath, "worktrees", "task1") + if err := os.MkdirAll(workDir, 0o755); err != nil { + t.Fatal(err) + } + + store := beads.NewMemStore() + root := mustCreate(t, store, beads.Bead{Title: "workflow", Metadata: map[string]string{"gc.kind": "workflow"}}) + control := mustCreate(t, store, beads.Bead{ + Title: "review loop", + Metadata: map[string]string{ + "gc.kind": "ralph", + "gc.root_bead_id": root.ID, + "gc.check_path": filepath.ToSlash(checkRel), + "gc.work_dir": workDir, + "gc.max_attempts": "3", + }, + }) + subject := mustCreate(t, store, beads.Bead{ + Title: "review loop iteration 1", + Metadata: map[string]string{"gc.kind": "scope", "gc.root_bead_id": root.ID}, + }) + + result, err := runRalphCheck(store, control, subject, 1, ProcessOptions{CityPath: cityPath}) + if err != nil { + t.Fatalf("runRalphCheck: %v (the pack-relative check_path should fall back to the store root)", err) + } + if result.Outcome != convergence.GatePass { + t.Fatalf("Outcome = %q (stderr=%q), want pass via store-root fallback", result.Outcome, result.Stderr) + } +} + +// TestRunRalphCheckWorkDirRelativeCheckPathKeepsPrecedence guards that the +// #3008 fallback only fires when the work_dir join is missing: a check_path +// that DOES exist under the worktree must still resolve against the worktree, +// not the store root. +func TestRunRalphCheckWorkDirRelativeCheckPathKeepsPrecedence(t *testing.T) { + cityPath := t.TempDir() + checkRel := "check.sh" + // Same relative name exists in both the store root and the worktree; the + // worktree copy must win. Distinguish them by exit code. + if err := os.WriteFile(filepath.Join(cityPath, checkRel), []byte("#!/bin/sh\nexit 7\n"), 0o755); err != nil { + t.Fatal(err) + } + workDir := filepath.Join(cityPath, "worktrees", "task1") + if err := os.MkdirAll(workDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(workDir, checkRel), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + + store := beads.NewMemStore() + root := mustCreate(t, store, beads.Bead{Title: "workflow", Metadata: map[string]string{"gc.kind": "workflow"}}) + control := mustCreate(t, store, beads.Bead{ + Title: "review loop", + Metadata: map[string]string{ + "gc.kind": "ralph", + "gc.root_bead_id": root.ID, + "gc.check_path": checkRel, + "gc.work_dir": workDir, + "gc.max_attempts": "3", + }, + }) + subject := mustCreate(t, store, beads.Bead{ + Title: "review loop iteration 1", + Metadata: map[string]string{"gc.kind": "scope", "gc.root_bead_id": root.ID}, + }) + + result, err := runRalphCheck(store, control, subject, 1, ProcessOptions{CityPath: cityPath}) + if err != nil { + t.Fatalf("runRalphCheck: %v", err) + } + // The worktree copy exits 0 (pass); the store copy exits 7 (fail). A pass + // proves the worktree script ran, i.e. the fallback did not shadow it. + if result.Outcome != convergence.GatePass { + t.Fatalf("Outcome = %q (stderr=%q), want pass from the worktree-relative script", result.Outcome, result.Stderr) + } +} + // TestRunRalphCheckEnvTracksSubject pins gastownhall/gascity#2558 review // feedback: GC_BEAD_ID and the molecule/artifact dirs must describe the SAME // bead. The per-attempt agent runs on the subject (attempt) bead and writes From cdc933ba5ceaacefe0e4c17de63e2be29d379cc1 Mon Sep 17 00:00:00 2001 From: Jim Wordelman Date: Fri, 3 Jul 2026 13:14:36 -0700 Subject: [PATCH 20/77] fix(reconciler): ignore bare-template demand for expanded identity templates (#3865) ## What this changes The reconciler no longer treats a bead assigned to the bare template name as named-session demand when that template can also have expanded per-instance identities. In the reported shape, a template is both a `[[named_session]]` and a multi-slot pool, so one bare-template assignment could make the named holder and a pool slot both look eligible for the same work. The guard uses the existing `Agent.SupportsExpandedSessionIdentities()` contract. Plain named-only agents and canonical singleton pools keep their current behavior; templates that can produce concrete `-N` identities do not count a bare-template assignee as named-session demand. ## Review notes - This touches shared reconciler desired-state code used by pool reconciliation across rigs. - This is defense-in-depth for misassigned work; it does not change prompt templates, pool demand calculation, or config validation for named-session plus pool coexistence. - The new tests are self-contained in-memory desired-state tests, not subprocess or timing-sensitive tests. ## Test plan - [x] `go test ./cmd/gc -run 'TestBuildDesiredState_MultiSlotPoolNamedSession_OneRoutedBeadProvisionsTwoWorkers|TestSharedTemplateAssignee_Tier1CrashRecoveryCrossAdopts' -count=1` - [x] `go test ./cmd/gc -run 'TestBuildDesiredState|TestComputePoolDesiredStates|TestBuildAwakeInputFromReconciler|TestNamedWorkReady' -count=1` - [x] `make test-fast-parallel` - [x] `go vet ./...` - [x] Release gate: [`release-gates/reconciler-named-work-ready-guard-gate.md`](release-gates/reconciler-named-work-ready-guard-gate.md) --------- Co-authored-by: quad341 --- cmd/gc/build_desired_state.go | 14 ++ cmd/gc/build_desired_state_ga80pen8_test.go | 185 ++++++++++++++++++ .../reconciler-named-work-ready-guard-gate.md | 46 +++++ 3 files changed, 245 insertions(+) create mode 100644 cmd/gc/build_desired_state_ga80pen8_test.go create mode 100644 release-gates/reconciler-named-work-ready-guard-gate.md diff --git a/cmd/gc/build_desired_state.go b/cmd/gc/build_desired_state.go index 63003c5a4d..f65c5fb3e6 100644 --- a/cmd/gc/build_desired_state.go +++ b/cmd/gc/build_desired_state.go @@ -892,6 +892,20 @@ func buildDesiredStateWithSessionBeads( if assignee != identity { continue } + if spec.Agent.SupportsExpandedSessionIdentities() { + // Defense in depth (ga-i1d0tr Candidate B): a bare-template Assignee + // is only a legitimate "this IS my identity" match for a template + // with exactly one possible live identity. For a template that + // supports expanded per-instance identities (a multi-slot pool or + // namepool coexisting with this named session), a bare-template + // Assignee means some other path wrote the wrong value — a pool + // slot's claim, a human running `bd update --assignee=