fix(gastown): patrol pour+assign must set --status=in_progress or wisps orphan past the resume-check - #1
Closed
voxist-merge-bot[bot] wants to merge 78 commits into
Closed
fix(gastown): patrol pour+assign must set --status=in_progress or wisps orphan past the resume-check#1voxist-merge-bot[bot] wants to merge 78 commits into
voxist-merge-bot[bot] wants to merge 78 commits into
Conversation
…te work, and failed drains (gastownhall#185) # Harden the polecat work protocol against races, duplicate work, and failed drains ## Why An architecture audit of the polecat work loop (across gascity, beads, and this pack) found the gastown polecat protocol pushes concurrency safety onto model reasoning where the sibling `gascity` pack already enforces it mechanically. The gaps produce the failure modes operators see: polecats racing each other, duplicating work, failing to claim, and failing to drain. This PR ports the proven `gascity/roles/prompts/shared/gc-role-worker.md.tmpl` discipline into the gastown polecat and closes the surrounding protocol holes. Pack text and formulas only; no gascity or beads changes. ## What changed - **Claim discipline** (`agents/polecat/prompt.template.md`): one canonical scripted claim via `gc hook --claim --json`, a post-claim ownership verification (assignee and status must match this session) before any work, STOP-and-drain on rejection, a resume-time re-verify against the work bead (pool restarts mint new session identities), and a ban on freelance `bd ready`/`bd list` discovery. Read failures retry rather than being treated as terminal, so a transient blip can neither strand a claimed bead nor drop a push. - **One done-sequence** (`prompt.template.md`, `template-fragments/approval-fallacy.template.md`): the three divergent copies collapse to the formula's `submit-and-exit` as the single source of truth, with a mechanical "already submitted?" guard so a compaction or restart can't double-push or wrongly drain. - **Evidence-gated shutdown dance** (`formulas/mol-shutdown-dance.toml`): a progress check on the target's claimed-bead `updated_at` and worktree mtime pardons a worker that is alive inside a long tool call, instead of killing it as dead. - **Warrant dedup** (`mol-witness-patrol.toml`, `mol-deacon-patrol.toml`): all warrant filers skip when an open warrant already exists for the same target, so patrols can't spawn concurrent shutdown dances against one worker. - **Refinery already-merged short-circuit** (`mol-refinery-patrol.toml`): a branch already merged to the base closes as merged instead of escalating a false completion to a human, gated on a real post-fork commit so a zero-commit branch still trips the false-completion guard, with explicit git-error handling. The path flows into the normal cleanup and next-iteration tail rather than stalling the patrol loop. - **Churn-watcher** (`assets/scripts/polecat-churn-watcher.sh`): keys on an exact `metadata.polecat_session` (now stamped on claim) and only flags open-and-unassigned beads, so a normal refinery handoff is no longer a false positive. ## Test plan - `python3 -m pytest tests/`: 60 passed - `bash gastown/tests/test_gastown_pack_assets.sh`: pass - `bash gastown/tests/test_polecat_churn_watcher.sh`: new test; asserts detection of a dead-session orphan and non-detection of the handoff, live-session, and work_dir-substring cases - `python3 validate_registry.py registry.toml`: ok - `bash -n` on the churn-watcher and every edited embedded-bash block; all touched TOML parses under `tomllib` ## Review Reviewed across multiple rounds by independent reviewers plus a Codex grounded pass on the embedded shell. Each fix round was re-reviewed until clean; findings and their closures are captured in the branch history. --------- Co-authored-by: sjarmak <sjarmak@users.noreply.github.com>
Use the store-aware gc bd wrapper throughout pack prompts, formulas, scripts, and runtime adapters so graph and rig beads resolve in the intended store. Add a repository-wide guard for shell, multiline, Python, Go, and serialized argv forms.
…c-bd-main Route all shipped beads commands through gc bd
Recognize valid gc global scope flags without letting bare leading-flag, dynamic, multiline, or marker-suppressed bd invocations escape the guard. Clarify close-metadata guidance and align the remaining Polecat and Discord wording.
…corrections Harden the gc bd migration guard
Restore store-aware bead routing and all claim identifiers while bounding operational retries without draining ambiguous assignments. Align continuation handling with the fresh-session boundary contract and add real CLI registration/render coverage.
…e-claim-prompts refactor(gascity): deduplicate gc role worker prompts, reduce tokens
Port the accepted Discord company-rooms contract to Slack with durable event admission (record + dedup before ack, redrivable, restart-safe) as the foundational layer and per-agent DMs staged into final scope. Includes the phased implementation plan with pinned Phase 1 interfaces. Reviewed by a four-lens adversarial design pass (durability, Slack platform, implementation fit; 30 findings incorporated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y, routing Port the accepted Discord company-rooms contract to Slack with durable event admission as the foundational layer: - Durable ingress receipts: claim-and-content-atomic Admit (temp+fsync+ link), origin-key dedup (team, channel, ts), generation-checked updates, 503-without-no-retry on persistence failure so Slack redelivery is the redrive path, parked receipts, company-scoped recovery barrier, sweep with stale-reclaim, degraded mode (store failure => 503, never legacy). - Company directory + singleton (room, agent) -> session bindings: two CLI-written registries with never-fatal loads, last-known-good reloads outside the atomic six-registry SIGHUP set. - Deterministic routing: mention exclusivity over ambient wake, subtype allowlist, union mention extraction (rich_text + canonical tokens), fail-closed author classification, frozen-route redrives, company rooms own their app_mention twins; bot-authored wakes deferred to Phase 2. - Delivery: per-target Idempotency-Key session submission with bounded attempts, definitive-4xx terminal failures, /healthz company detail as the paging surface. - CLI: gc slack import-company-directory / bind-company-agent / peers (TOML validation parity with the Go loader, hardened atomic writes, best-effort membership warnings). Design docs aligned with reviewed behavior. Reviewed pre-commit by a four-lens adversarial pass with per-finding verification (28 confirmed findings fixed, including the degraded-store legacy-fallthrough blocker). Verified: go test -race (adapter, cli, channel, mini) and the full CI pytest battery (793 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expand the implementation plan's Phase 2 into pinned cross-language contracts after a two-lens adversarial spec review (18 findings incorporated): normative thread-root derivation, metadata-gated requester-scoped result claims with a cancel verb, receipt-store-based intent reconciliation (lazy, CLI-owned — barrier claim amended), cross-process flock + O_EXCL create-once record protocol, content- addressed nonces with retry_seq freshness, per-session current-turn pointer files as the deterministic verb context, frozen hydration, transient-vs-definitive author-resolution outcomes, lazy pruning, and a byte-for-byte filename sanitizer spec with golden fixtures shared by both language suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ults Per-agent real identities and the delegation/result machinery on top of Phase 1's durable admission: - Python outbound (slack_company_outbound.py + gc slack delegate): durable intents (op: delegation|result|synthesis) created before every chat.postMessage, content-addressed nonces with monotonic retry_seq, cross-process flock + O_EXCL create-once delegation records, receipt-store-based crash reconciliation (author + event_type gated, never reposts on ambiguity), prepared-intent resume, lazy pruning retaining the per-tuple watermark, per-agent token files with permission/symlink refusal, entity-escaped text-only composition with validated mention IDs, --cancel recovery, and reply-current company awareness (peer results, synthesis, thread-rooted ambient replies). - Go ingress: bots.info author resolution (transient failures park, the definitive path fails closed, corroborated against directory app_id), five-condition peer trust, metadata-gated requester-scoped result claims (clarifying questions deliver as peer_input and consume nothing), frozen hydration (verified human root + bounded untrusted excerpt, byte-identical across redrives), per-session current-turn pointer files driving the CLI verbs, unknown-field-preserving fail-closed record rewrites. - Provisioning: agent-app manifest template, switchboard scope additions (channels:read, groups:read, users:read), runbook. - Cross-language contract pinned by golden + interop fixtures generated through the real Python code paths and claimed through the real Go paths; lock-filename derivation verified identical in both languages. Reviewed pre-commit by a four-lens adversarial pass with per-finding verification (18 confirmed findings fixed, incl. the keyless peer pointer blocker; 7 claims refuted). Verified: go test -race across all adapters and the full CI pytest battery (856 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expand the plan's Phase 3 into a pinned spec drafted from the Discord reference machinery and hardened by a two-lens adversarial review (17 findings incorporated): strict one-hop parity restored (delegate only from human-rooted turns), honest D1 session-reincarnation consequence, reference-exact correlation backoff with a non-counting error park, outcome-typed sequential chain delivery with per-root ownership, target-less redrive recovery leg, bounded claim re-lock loop, sweep- healed terminal acks reusing the existing reactions client, pruner clock-skew floor, UDS-capable admin client, omitzero receipt fields, and gate-level proofs for the replay-window amendment and all one-hop refusals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…redrive, acks Sibling synthesis snapshots frozen atomically with result claims (group locks, bounded re-lock loop, replay window with retention bound), monotonic per-root replay chains with typed delivery outcomes and per-root ownership, root-serialized live result ordering, reference- exact correlation backoff (per-reason budgets, non-counting error parks, terminal exhaustion), the operator redrive surface (two legs incl. Agent-based re-resolution of unbound-failed targets, 422 on data-loss empties, pattern-validated receipt ids), config-gated visible acks (once-only failure reply via the warned cursor, sweep-healed reactions, strict taxonomy), synthesis-gated reply-current with --allow-partial, strict one-hop enforcement, company-status/redrive verbs with a UDS-capable client, /healthz park counter, and strict cross-language snapshot normalization pinned by shared and interop fixtures. Reviewed pre-commit by a four-lens adversarial pass with per-finding verification (22 confirmed findings fixed, 6 refuted), seeded with live-workspace wire-capture facts. Verified: go test -race across all adapters and the full CI pytest battery (919 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the legacy binding-era fragment guidance with the company-rooms agent contract mirrored from the reviewed design: wake kinds and native mention exclusivity, reply-current as the sole visible reply path with posted_ts success discipline, formal delegation rules (human-rooted turns only, one hop, one pending per peer per thread, siblings before waiting), synthesis_ready gating with the --allow-partial escape, and the DM boundary. Slack bold/handle conventions retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live org runs one team per gc city, each with its own supervisor API, while the switchboard must stay the single admission owner. A company binding may now carry an optional city: the frozen target and current-turn pointer record it, delivery resolves the target city's API base from SLACK_COMPANY_CITY_APIS (unmapped city = definitive per-target config failure, operator-redriveable), redrive re-resolution preserves the qualifier, and gc slack bind-company-agent gains --city with fail-closed URL-safety validation mirrored in both languages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…any verbs gc runs a configured named session (teams.it) with a sanitized GC_SESSION_NAME (teams__it) while bindings and delivery address the configured dot form. Pointer lookups and the spoof-guard comparisons now accept the deterministic alias pair so cross-city sessions resolve their company turns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The same session name in different cities is a different session under city-qualified bindings; the guard predated the city field and blocked legitimate cross-city PM bindings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-lens adversarial review (26 agents, 19 confirmed findings, 5 blockers) folded in: self-echo DMs admit as terminal dm_self_echo receipts so receipt-scan reconciliation stays sound; new agent_apps.json registry + register-agent-app verb (import-app has no secret/agent fields); owner-agent identity joins the company directory; verification order restores the legacy trial carve-out while keeping rule-12 strict binding for registered apps; Go gateway gains per-agent token custody for DM hydration/acks; session-existence guard is advisory-only; dm_unbound follows the rooms failed-target + redrive rule; DM turns get a dedicated pointer file; reactions:write and Delayed Events added to the per-app runbook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements phase4-dm-spec.md v2. Go adapter: agent_apps.json registry (register-agent-app custody, SIGHUP reload, directory-join warnings), four-rule verification order (app-bound HMAC with rule-12 cross-app rejection, SLACK_APP_ID env-secret pinning, legacy trial carve-out that still rejects registered-agent matches), DM admission into the shared receipt store (kind dm, owner_app_id, self-echoes admitted as terminal dm_self_echo so receipt-scan reconciliation holds), allowed-human policy with present-even-empty allowlist semantics, routing to singleton dm_bindings (unbound → failed_dm_unbound, redrive- recoverable; registry-unavailable → recoverable park, not a policy denial), advisory session-existence guard (SLACK_COMPANY_VERIFY_ SESSIONS, never terminal, 60s sweep re-check), per-agent owner-token custody (0700/0600/O_NOFOLLOW fstat loader; switchboard token never touches a DM), owner-token hydration/acks with counted degradation, dm/ pointer subdirectory (collision-proof), /healthz DM counters folded into the single sweep scan. Python: bind-company-dm (singleton guard with dunder aliasing, --remove unbind), register-agent-app verb (0600 atomic write, argv-safe --signing-secret-file), dm pointer kind + newest-wins reply-current with --kind override and DM-wins tie-break, dm_bindings spoof guard, owner-token DM posting through the durable-intent machinery, delegation refused from dm roots, dm_allowed_humans directory import. Two independent Opus implementations (Go/Python) against the spec; pre-commit four-lens adversarial review: 4 majors + 13 minors confirmed, all fixed. go build/vet/test clean; pytest 340 passed. Golden fixtures regenerated from the real writers with cross-language byte-parity tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observed on the first live pilot DM: the reply threaded under the human's message per room discipline and hid behind the thread indicator. DMs have no channel timeline to protect — a reply whose root equals the turn's own ts now posts in-channel; threading applies only when the human replied inside a thread. Root derivation and the intent tuple key are unchanged. Spec §Hydration/pointer/reply amended; tests updated (341 passing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The durable-request-ledger v0 remains uncommitted design drafts with zero implementation, no wire schemas until its Slice 20 OpenAPI, a provisional state/error vocabulary until its Slice 0, an unlanded Beads create-only/CAS dependency, an unimplemented multi-target delivery rework, an unspecified Slack projector wake mechanism, and the unchanged DedupKey no-op gate. Phase 5 therefore ships no wire client yet: the readiness doc pins the gate ledger, a conformance matrix showing Phases 1-4 already satisfy the Slack companion's adapter obligations (two divergences noted), the corrected six-state adapter journal mapping (spooled/admitting/core_bound/body_redacted/ rejected/quarantined — supersedes the plan's three-state note), the body-store split as the one invariant-pinned increment, and six Slice-0 asks for the ledger owners. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review (22 agents; 18 confirmed findings, 5 blockers, 1 rejected) folded in. Blockers fixed: mpim pointers get their own mpim/ subdirectory (a shared dm/ file was last-writer-destroys and would clobber unanswered 1:1 turns); routing extends the DM delivery worker's multi-target machinery, NOT the room path (which parks non-directory channels forever); pointer owner_app_id for mpim is the WOKEN agent's app_id per target while the receipt-level field stays the admission winner (ack actor), so non-winner agents' replies pass the guard. Majors: normative kind-dispatch inventory with an isDMFamilyKind predicate across every dm seam (acks, redrive, gauges, delegation gate); single frozen hydration blob with deterministic token fallback; membership-probe mitigation for the widened signing-secret blast radius; allowlist provenance downgrade for unlisted excerpted authors; switchboard mpim copies acked with no receipt and no legacy dispatch; rollout explicitly sequences adapter deploy before the manifest bulk update (interim legacy N-plication documented); live mpim wire fixture required before trusting mention extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Receipts stop embedding raw inner events: a bodies/ sidecar (<id>.body.json, 0600, O_EXCL-linked BEFORE the receipt) holds the payload, referenced by body_ref + sha256 event_digest; both languages read through a single accessor and accept the legacy embedded shape forever. thread_root_ts freezes onto receipts at admission so every root-keyed derivation (dgser lock, replay chain, rendered root, failure reply) is body-independent. Operator company-redact verb (HTTP + CLI) truncates a body to a digest-bearing tombstone, guarded by terminal-status and reconciliation-horizon 409s. Pre-commit adversarial review (11 agents; 8 confirmed + 9 minors, 0 rejected) hardened the lifecycle: digest-checked orphan adoption (divergent crash-orphan replaced, never a born-mismatched receipt — verifier had a live repro); body GC only on affirmative receipt absence with a cross-process grace window; quarantine takes the body aside for forensics; missing/mismatched bodies PARK recoverably instead of terminalizing; sweep classifies by existence only (digest verified on read, counted); redact-vs-retention race returns 404 not a resurrected tombstone; redacted receipts stay visible in admin listings; cross-language golden fixture pins the split shape. go build/vet/test (-race) clean; pytest 355. Readiness doc updated with the hardened semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements phase4b-mpim-spec.md v2. Admission: channel_type mpim from registered agent apps; switchboard copies swallowed (200, no receipt, no legacy dispatch); origin-key dedup absorbs multi-app observation; receipt owner_app_id = admission winner = ack actor only. Routing: mention-only wake set via dm_bindings through dedicated deliverMpimReceipt/deliverMpimTargets sharing the DM worker's primitives; membership probe with the woken agent's own token before first delivery (missing token BLOCKS as failed_mpim_not_member — fail-closed blast-radius mitigation; advisory only for transport errors); owner-join loss degrades acks, never delivery. Pointers: new mpim/ namespace, per-target woken-agent owner_app_id, three-way newest-wins with --kind room|dm|mpim (dm>mpim>room privacy tie-break); vanished agents fail recoverably, never a poisoned pointer. Hydration: one frozen blob, admission-owner-first token fallback; allowlist provenance verdict frozen with the snapshot (byte-identical retries), human-authors-only downgrade covering both the thread root and excerpt lines. Replies: woken agent's token, flat unless human-threaded, intent op dm for receipt-scan reconciliation. Two review rounds: spec review (22 agents, 18 confirmed, 5 blockers) + implementation review (11 agents, 7 confirmed, 5 minors), all fixed and pinned by tests. go build/vet/test clean (616 PASS); pytest 372. Wire fixture is SYNTHETIC pending live message.mpim capture (rollout step 1). Rollout order pinned: adapter deploy BEFORE manifest update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live message.mpim event (pilot group DM, 2026-07-19): confirms channel_type=mpim and rich_text mention-element synthesis in group DMs, closing test-plan item 10's trust gap on the mention extractor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session-not-found is now a healed condition, never a user-visible failure while recovery remains possible: delivery 404s re-resolve the frozen target against current bindings automatically (the petra outage class); cold pool sessions are auto-materialized via the supervisor sessions POST (throttled per (city,session) per sweep); company-redrive re-resolves EVERY failed target from current bindings; the⚠️ failure notice fires only on genuine budget exhaustion or non-recoverable rejection. New healthz counters company_target_reresolved + company_materialize_requests. Also fixes the mpim live wire fixture (re-wrapped in its event_callback envelope; the live-capture swap had stored the bare inner event). 621 Go subtests + race clean; pytest 372. Deployed with SLACK_COMPANY_VERIFY_SESSIONS=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixture swap to the live capture left the test asserting synthetic ids; it now validates against the real event (ollie app A0BHQ812PL7, ollie+olivia mentions), proving rich_text mention synthesis in mpim with production identities. Full suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An asleep/drained configured session accepted queued messages without waking — delivered receipts, no processing. The guard GET now surfaces session state; sleeping targets get an advisory wake POST before the message, and a still-asleep-after-delivery re-check increments company_delivered_asleep for observability. Guard-off behavior byte-stable. 7 new tests; full suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…townhall#229) ## The bug `validate_registry.py` failed **open** on external pack sources. `source_pack_path()` pulled a tree path out of *any* https URL without checking it pointed at this repo, then resolved it against the local checkout: - A source URL **without `/tree/`** returned `""` — falsy — so the walrus guard and `if pack_path:` short-circuited, skipping pack.toml existence, content-hash verification **and** the name match entirely. **An entry with an all-zero `sha256` and a commit absent from the object database validated `ok`.** - A **foreign** `/tree/` URL mis-resolved locally, and `git_pack_content_hash` returned `None`, which the caller read as "verified". Foreign sources were therefore *more* permissive than our own — the inversion you least want in a supply-chain check. **Two live in-repo instances:** `pr-pipeline` and `contributing` use a legacy `gascity-packs//<dir>` form matching neither regex, so **they have never been verified.** Their hashes were already correct (checked); this migrates both to the canonical form **in the same commit**, so CI is never red in between. ## The rule `registry.toml` describes packs in **this repository only**. Community packs publish directly to the Gas City registry under a scoped `<owner>/<pack>` name and keep their author as owner — so rejecting foreign sources costs nothing and keeps this check local, network-free, and actually verifiable. (The registry server already does the authoritative external check.) ## Changes - **`parse_source()` → `SourceSpec(ref, pack_path)`**, raising `SourceError`; accepts only canonical-repo URLs. Classifies every form that used to slip through: legacy `//` and `.git//`, `/blob/`, `/tree/<ref>` with no directory, bare paths like `/packs/foo` (previously mis-read as repo root), non-`https`, foreign host/owner, fragments, query/credentials/port, invalid segments. - **`""` now means repo-root and nothing else** — a fully verified state, never a skip signal. Root packs were previously unchecked entirely. - **`git_pack_content_hash()` raises with a reason instead of returning `None`**, and checks commit presence separately so a shallow clone is named as such. Deletes the `compute_pack_hash` wrapper whose only job was hiding that `None`. - **`validate()` parses the source once *before* the release loop**, so an unparseable source can't silently disable the release checks. - **`--require-git`** turns "git unavailable" from a silent downgrade into an error — wired into CI (already `fetch-depth: 0`) and into the release-hash fallback in `scripts/pack_release_compat.py`, which could not verify a hash without it. - **`--emit-entry` refuses to mint a source its own validator would reject.** Deliberately **not** added: a `release.ref` vs `/tree/<ref>` consistency check. The source URL carries one ref while releases are per-version, so a pack releasing from two refs would be unsatisfiable and `make registry-publish REGISTRY_REF=<tag>` would mint self-rejecting entries. `commit` + content hash are the real anchor. ## Verification - 11 packs / 28 releases still validate (`registry.toml: ok`, with and without `--require-git`). - Ten attack forms each rejected with a **specific** reason, including the reproduced all-zero-hash bypass. - Positive controls: a tampered hash is caught with exact expected/actual; a repo-root pack with a wrong hash is caught (previously zero checks ran); a pack releasing from two refs still validates. - Suite: **101 passed, 7 skipped.** ## Heads-up This will correctly fail CI on **gastownhall#180** (registry entry pointing at `boshu2/agentops`). The rejection message tells the author exactly what to do instead. gastownhall#6 is *not* affected — it only adds a README, which this validator doesn't police (tracked separately). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: CI Bot <ci@beads.test> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…owers (gastownhall#230) ## Why These four have lived in the repo and carry the **most expensive first-party test investment we have** — they're the only packs in the nightly model-backed inference gate alongside `gascity` and `gastown` — yet none had a `registry.toml` entry, so the registry couldn't see them at all. Publishing at `0.1.0`, matching the version each `pack.toml` already declares. Catalog goes **11 → 15 packs**, the maintained roster. ## How Minted via the documented maintainer flow (`make registry-publish` → `gc pack release stamp` → `registry_release.py set-source`), so format and provenance match every existing entry — no hand-pasted blocks. ## Verification - `gc` computed each content hash and **`validate_registry.py` independently recomputed and matched** it (two implementations agreeing). - **Tampering each of the four hashes is caught** → these entries are genuinely verified, not skipped. - `registry.toml: ok` with and without `--require-git`; suite **101 passed, 7 skipped**. - Confirmed empirically that the live **`[imports.gc] source = "../gascity"`** resolves for a consumer: registry install clones the whole repo at the pinned commit and strips the subpath from the cache key, so `bmad/` and `gascity/` land as siblings in one clone. The README's `gc import add …git//bmad` path is the *same* mechanism, not a divergent one. (These are the first published packs with a live relative import, so this needed proving rather than assuming.) ## Known gap — pre-existing, not introduced here `gc import add` resolves versions from **repo-wide semver tags**, and the latest tag `v0.3.0` predates these packs. It also predates the **already-published `pr-pipeline` and `contributing`**. So until a tag containing them is cut, the documented no-version install fails for all six and only `--version sha:<commit>` works. That's a live pre-existing bug in the release process, tracked separately. Publishing these entries makes the packs discoverable and regresses nothing — but the tag is what actually completes delivery for all six. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: CI Bot <ci@beads.test> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three golden claimed-delegation fixtures hardcode an absolute result_claimed_at/created_at (2026-07-17). post_peer_synthesis prunes before the gate reads, and prune keys terminal (result_claimed/expired) records on result_claimed_at, falling back to created_at, against a 7-day retention floor. Once wall-clock passed the fixtures' authoring date + retention, the record was evicted before the gate ran — so the three refuse-path tests (reply-current D2, gate-not-ready-exits, gate-allow-partial) stopped raising and went red on their own clock. Re-stamp created_at + result_claimed_at to _now() at fixture install in all three _install_claimed helpers, mirroring the file's own _write_delegation convention. Prune now retains the record; the gate sees the intended available+not-ready snapshot and refuses as designed. Test-only; no production behavior changes. Full slack-full suite green (Python 17 files / Go build+vet+test). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slack company-rooms: slack-full adapter, per-agent identities, delegation/synthesis, DMs/mpim, self-healing delivery
`runtime-cloudflare` has existed as a top-level pack directory since `28c7e24` (2026-06-13) but was never added to `registry.toml`, so the registry cannot serve it. That's a real gap rather than a cosmetic one: the Gas City SDK **deleted** its Cloudflare builtin and now depends on this pack being installable. From `gascity/internal/runtime/REQUIREMENTS.md`: > **RUNTIME-SEL-012** — The Cloudflare runtime ships as the `runtime-cloudflare` pack > (gascity-packs), not a gascity builtin… `internal/runtime/cloudflare` is deleted; bumping the > pack changes runtime behavior under an unchanged `gc` binary. `cmd/gc/runtime_registry_test.go::TestCloudflareIsNoLongerABuiltin` enforces the builtin's absence, and RUNTIME-SEL-002 lists `cloudflare` as deliberately not a builtin. The failure mode of the omission is the quiet kind: a city selecting `session = "cloudflare"` without the pack doesn't error — it falls through to the tmux fallback (RUNTIME-SEL-006). ## What's here One `[[pack]]` entry, 13 insertions, nothing else touched. Produced by the repo's own `make registry-publish` (`gc pack release stamp` for the content hash, then `scripts/registry_release.py set-source` for the canonical `/tree/main/<dir>` source form) rather than hand-authored. The pin is content-stable: `28c7e24` is the only commit that has ever touched the directory, `git diff 28c7e24 HEAD -- runtime-cloudflare` is empty, and the validator's own hasher returns an identical hash at both commits. Pinned at the `main` tip, matching the bmad/gstack/superpowers precedent. ## Verification - `python3 validate_registry.py --require-git` → `registry.toml: ok` (the CI gate) - `make registry-format-validate` → `registry.toml: ok` - `gc pack release validate registry.toml` → `registry release hashes ok (33 checked)` - `python3 -m pytest …` → 1077 passed, 7 skipped, 9412 subtests - `go test .` → ok - `scripts/pack_release_compat.py --pack runtime-cloudflare` → `compatibility smoke passed` — this is the real consumer path: `gc import install` fetched the pack from GitHub at the pinned commit, verified the hash, and `import check` / `config show` passed. The entry is genuinely servable, not merely well-formed. ## Noted, not fixed here - `validate_registry.py --emit-entry` renders unindented keys, which doesn't match this file's actual 2/4-space style — anyone pasting its output has to reindent. The `make registry-publish` path writes correct style. - `runtime-cloudflare/conformance.sh` is still `continue-on-error: true` in CI because `gc runtime check` doesn't exist on gascity `main` yet, so the pack's RPP conformance isn't actually gated today. Unrelated to this entry. - `contributing` and `pr-pipeline` were last modified in `9b4df3e`, after their newest pinned releases — they have unreleased content on `main`. Pre-existing. Co-authored-by: CI Bot <ci@beads.test> Co-authored-by: Claude <noreply@anthropic.com>
…ownhall#237) Adopts the one genuinely new idea from gastownhall#99 (thanks @sarendipitee) into gastown, and closes a gap that turns out to be sharper than that PR realized. ## The failure mode A witness whose self-scheduled patrol loop has died still reports a healthy session state. It parks in `asleep` or `active` forever, so the controller's liveness reconcile sees nothing wrong. Patrol stalls of **14 to 63 hours** were observed in production this way, with no alert and work quietly accumulating. ## Why gastown didn't already catch it Both existing checks explicitly *excused* the exact production case: - `health-scan`: "if there's no active work in the rig, the witness is legitimately idle — not stuck." - `queue-starvation-check` Step 4: "A genuinely-idle session (no assigned beads) is NOT flagged." So the **empty-queue silent stall** was reasoned away twice. This PR amends that sentence so the file no longer contradicts the new pass: idle now only excuses a stale *wisp*, and if the heartbeat pass returns `stalled`, don't talk yourself out of it. Heartbeat age is also precisely the kind of measurement an LLM shouldn't be eyeballing, which is why it's a deterministic script rather than more prompt. ## Shape A read-only script (`gastown/assets/scripts/witness-heartbeat-check.sh`) run as the first pass of the existing `health-scan` step — not a new step, not a parallel order. It measures, prints TSV, and exits; it never mails, nudges, or files warrants. `health-scan` keeps the decision, which is what avoids hardcoding an escalation target (gastownhall#99 hardcoded `mayor`, bypassing core's `escalate.sh`). Matches the existing `polecat-churn-watcher.sh` shape: same city resolution, same `gc` + `jq`, same env-only config, sibling test under `gastown/tests/`. ## The threshold is deliberately not gastownhall#99's **gastownhall#99 used 15 minutes. In gastown that would be a misfire machine.** gastownhall#99 calibrated against a witness with a self-scheduled ~60s `ScheduleWakeup` loop. Gastown's witness deliberately does *not* self-schedule — `mol-witness-patrol`'s `next-iteration` says "Do NOT sleep, call ScheduleWakeup, or use Monitor", and `agents/witness/agent.toml` sets `idle_timeout = "1h"`. So 1h is the longest legitimate silence for a *healthy* gastown witness. Default is **90 minutes** (1.5× that): clear of legitimate idle, still an order of magnitude under the 14h stall floor. Configurable via `[vars.witness_stale_min]`. ## Robustness Roster field names verified against the real binary (`gc session list --json-schema=result`, gc 1.1.1) rather than trusting gastownhall#99: `last_active` is **required**, `last_nudge_delivered_at` optional. So a missing nudge field is normal, but a missing `last_active` is schema drift → exit 2 and an explicit "NOT measured", mirroring the fail-safe `mol-witness-patrol` already carries for gc-3tn8g. - Zero-time sentinel (`0001-…`) → `no-heartbeat`, never `stalled` — a brand-new witness must not be killed — but still a finding, because "nothing was measured" must not read as health. - GNU `date -d` then BSD `date -j -f`; fractional seconds stripped first, which gastownhall#99 didn't do and which BSD `date -f` can't parse. Verified against a BSD-only `date` stub on PATH: identical verdicts. - bash 3.2 throughout (no `declare -A` — gastownhall#99 used it, and the fleet includes macOS bash 3.2). A test greps for bash-4 constructs to keep it that way. - Heartbeat candidates compared as epochs, not gastownhall#99's `sort | last` string sort, which is wrong across mixed UTC/offset. A future heartbeat clamps to age 0 — clock skew isn't staleness. ## Verification `bash -n` clean; 19 new tests; CI's exact loop (`for t in gastown/tests/test_*.sh`) all four suites pass; formula TOML parses; `check-scripts` executable gate passes; `validate_registry.py --require-git` ok; `pytest` 276 passed / 9385 subtests; `go test .` ok. `gc lint gastown` findings byte-identical to the base commit (35 pre-existing, 0 new). Six timestamp cases exercised end to end: fresh, 20h-stale, zero-time, malformed, stale-`last_active` -with-fresh-nudge, and `last_active` absent. **Test-teeth note, for honesty:** mutating the staleness window makes the suite fail as expected. The zero-time path, though, is defended four layers deep (prefix sentinel → non-numeric guard → pre-epoch guard → the `best=0` max comparison), so removing fewer than all four leaves it still correct and the suite still green. That's intentional defense-in-depth, not a gap in the assertions — but it means no single-guard mutation can demonstrate that particular test's teeth. ## Not ported from gastownhall#99 Its dead-state check (the controller owns process liveness), its refinery merge-freshness check (`mol-refinery-patrol` + `queue-starvation-check` cover it), and its JSON dedup ledger (`health-scan`'s existing warrant-dedup plus the `boot` watchdog cover the deacon side). Also fixes a ghost reference in this formula's header claiming mechanical housekeeping runs as exec orders "in the maintenance pack" — that pack was folded into the SDK's `core` pack in gascity `f895c0ff4`. Co-authored-by: CI Bot <ci@beads.test> Co-authored-by: Claude <noreply@anthropic.com>
…all#238) Adopts gastownhall#158 by @csells as a first-party pack. This is the **one** adopt out of 17 open new-pack PRs — the rest are being routed to self-publish through the registry — so it needed to actually meet first-party standards rather than just be merged. csells' three commits are cherry-picked with authorship intact; the `profiler/` tree after the cherry-pick was byte-identical to PR head `917764fd`. The capture pipeline is genuinely good work — the `gc.var.convoy_id` repointing and content-match-fallback findings in csells' own validation are the kind of detail you only get from running something against a real 3.5-hour run. ## sjarmak's three review items — all three were still present at head 1. **`[imports.gc]` missing — the pack could not run standalone.** All 7 `bench-nullop` steps carry `gc.run_target = "gc.run-operator"` with nothing importing gascity. Added, matching the four other packs that do this. 2. **`TypeError` on in-flight roots.** Reproduced verbatim at `profile_report.py:400`. Fixed, and the axis now anchors on the latest timestamp the *capture itself* observed rather than wall clock, so re-rendering an old capture can't move the axis. A **second, worse** degradation was found that the review didn't mention: `analyze()` did `if not closed: continue`, so started-but-unclosed steps were dropped entirely and an early in-flight run rendered a *blank* report. Those now render as `running`. Fully-closed runs are byte-unchanged, so csells' validated reference figures hold. 3. **The README promised a `usage.jsonl` token/cost rollup that didn't exist.** Implemented rather than deleted, because the evidence supported it: `.gc/usage.jsonl` is the *default* sink (`cmd/gc/providers.go:984`), the schema is a declared struct (`internal/usage/usage.go` `usage.Fact`), and a first-party consumer already existed to copy semantics from (`gc costs`). Mirrors `gc costs` exactly: dedup on `idempotency_key`, and unpriced facts counted but **excluded** from the estimate rather than read as free — per `usage-facts-v0.md`, "the `Unpriced` flag must be surfaced in any rollup, else cost sums silently omit unpriced models." Two schema details the README had wrong: `step_id` is the *formula* `gc.step_id`, not a bead id, so per-step attribution joins through the step bead's metadata and only when exactly one bead matches (else `unattributed`, not spread by guesswork). And `collect`'s `u.get("bead_id") in ids` filter was dead code — no such field exists on a usage fact. ## Four further breaks found while verifying - **The `profile-analyst` role was unreachable — the whole judgment layer could not run.** It sat in `profiler/roles/agents/`, which `gc` does not read, *and* routed to `gc.profile-analyst` where `gc` is the gascity binding. Confirmed with the real binary: `gc prime --strict` → `agent "fixture/profiler.profile-analyst" not found in city config`. - **`prompt.template.md` was a 237-line inlined fork of the pre-rewrite shared worker prompt** with zero profiler-specific content. Main has since centralized that as a `{{ define }}` fragment; keeping the fork would have run this role on a stale claim protocol. Now a 20-line persona. - **`collect` shelled out to bare `bd`**, failing `tests/test_no_bare_bd_commands.py` — a gate added to main after this PR opened. - **`closure()` returned beads in set-iteration order**, so `beads.json` and its manifest hash differed between two collects of the same run. Caught because it broke a test under a different `PYTHONHASHSEED`. Now sorted and byte-stable. Plus: every doc said `gc profile <cmd>`, but gc namespaces pack commands under the import binding — the real form is `gc profiler <cmd>`. And added the `help.md` that 57 of the repo's other 58 commands ship. ## Path traversal (review item 3) — fixed, not deferred `session_key` comes from bead metadata, which the profiler does not author, and was concatenated straight into a path: `os.path.join(base, slug, session_key + ".jsonl")`. A crafted key read **any `.jsonl` on the machine into the capture** — and captures get shared. An absolute key was worse: `os.path.join` discards the root outright. Rejected rather than sanitized — a session key is a UUID, so anything that isn't a single safe path component can't name the right transcript anyway, and rewriting it would only produce a confidently wrong join. Resolution is additionally containment-checked *after* symlink resolution, using the same idiom as `discord_intake_service.rig_workdir`. Worth flagging how this test was gotten wrong first: traversal through a **missing** path component fails on Linux regardless of the guard, so the initial test passed against the vulnerable join while exercising nothing. It now redirects `HOME`, creates a real slug directory, computes the relative traversal instead of counting depth, and asserts up front that the target genuinely resolves — so a failure means the guard declined it, not that the path was unreachable. ## Tests: 87 new across 4 files, on a pack that had none Every other script-bearing pack in this repo has tests; ~707 LOC here had zero. Conventions matched from `gascity/tests/`. Covers both bug paths, collect/report/compare happy paths, malformed input, HTML escaping, and regression pins for every break above. `profile-analyst` was added to the existing parametrized `test_role_prompts_render_public_worker_fragment` — that's the one that actually catches an unreachable role. **Red→green proven for all five fixes** by reverting each and showing the failure, not asserted from reasoning. Reverting fix 2 → 7 failed incl. the original `TypeError`; fix 3 → 14 failed; fix 1 → the import assertions; the role fix → `gc prime` failing for real; the traversal fix → the test naming the exact escaped path. ## Verification `pytest` (repo's own invocation, `profiler/tests` added) **1170 passed, 8 skipped, 9412 subtests** · `gc lint profiler` ok · `validate_registry.py --require-git` ok · `go test .` ok · `bash -n` × 3 · `py_compile` × 7 · gastown shell suites × 4. **Exercised for real** with `gc` on PATH: `gc profiler collect` on an in-flight root, `report` rendering `IN FLIGHT`/`running` plus the cost rollup, `--html` at 5817 bytes with no literal `None`, `compare` across two in-flight runs, and two collects producing an identical `beads.json` hash. ## Deliberately not in this PR - **No `registry.toml` entry** — publishing needs a post-merge commit+hash, so it's always a follow-up commit. - **sjarmak's item 2** (keyword heuristics and hardcoded thresholds in `analyze()`, which the README assigns to the analyst role) is a design question rather than a defect. No new heuristics were added — only rollup numbers. Worth a follow-up. - `gc` warns that `profiler report` doesn't declare JSON support. **Zero** of the repo's 60 `command.toml` files do, so this is repo-wide and will bite when strict mode flips — separate issue rather than inventing a schema declaration here. - **`collect` against a live dolt-backed bead store is unverified** — `bd init` requires a dolt server and `--backend=sqlite` is refused, so the real end-to-end run used the real `gc`, real command registration and real scripts with only the inner `gc bd` read stubbed. --------- Co-authored-by: Chris Sells <csells@sellsbrothers.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: CI Bot <ci@beads.test>
…ep-dropping formulas (gastownhall#258) ## What this adds `pr-review`, the maintainer-side incoming-PR adoption pack. It is the counterpart to `pr-pipeline`, which already ships here and covers the author-side outgoing flow. This pack has been running locally for months but has never existed on any branch of this repo. Five formulas plus a `review-pr` skill overlay: | Formula | Purpose | |---|---| | `mol-adopt-pr` | 5-step adopt workflow: intake, rebase-check, review, human gate, finalize | | `mol-pr-ci-diagnose` | Diagnose a failing CI run on an incoming PR | | `mol-pr-iterate` | Iterate on review feedback | | `mol-pr-merge-only` | Merge-handoff without a fresh review | | `mol-pr-revert` | Revert a landed PR | No agents are defined. The consuming pack supplies the polecat. `mol-adopt-pr` gates on a human: after the review step, a checkpoint blocks until a maintainer closes it, and only then can finalize run. The polecat never runs `gh pr merge`, `git push`, `gh pr create`, or `gh pr comment` on the branch-ready paths; it prepares artifacts locally and hands publish to the mayor. ## Why `mol-adopt-pr` is formulas v2 As v1 its root compiled to a molecule container rather than Ready-visible work, so `slingFormula` rejected every attempt to route it to the polecat pool it is written for: ``` formula "mol-adopt-pr" root is a molecule container, not Ready-visible work; scale-from-zero pools will not wake for this wisp. ``` There was no working entry point at all. Routing to a city-level agent instead does not help, because formula search paths derive from the target agent's `Dir` and this pack is rig-scoped. ## Two formulas were silently dropping every step | Formula | Was | Now | |---|---|---| | `pr-pipeline/mol-pr-from-issue` | `RootOnly=true`, 9 steps dropped | `RootOnly=false` | | `pr-review/mol-pr-revert` | `RootOnly=true`, 7 steps dropped | `RootOnly=false` | `mol-pr-from-issue` is pre-existing on `main`. Both now set `pour = true`. The compiler predicate is: ```go rootOnly := (!f.Pour && f.Phase == "vapor") || len(f.Steps) == 0 ``` It keys only on `pour` and `phase`. The graph declaration form is never consulted, so `contract = "graph.v2"` offers no protection. `RecipeHasReadySurface` returns true when `RootOnly` is set, since a root-only recipe is trivially Ready-visible. That is what hides the bug. A pool sling is accepted, the root goes `in_progress`, and only the steps go missing, with no error anywhere. Anyone reading the guard alone would conclude the formula is healthy. That also makes `phase = "vapor"` without `pour` an attractive wrong fix for the v1 rejection above: it satisfies the guard while deleting the human gate from `mol-adopt-pr`, which would hand a polecat an unreviewed merge. The spec says as much, that the bare-vapor form is accepted for compatibility and must not be used to design new formulas. ## Formula name collision `pr-review` carried its own repaired copy of `mol-pr-from-issue`, which would have declared the same formula name as `pr-pipeline`'s. The repair is ported into `pr-pipeline`'s copy and the duplicate dropped. No formula name is now declared by two packs. ## Verification Every formula in both packs was compiled and checked for `RootOnly`: ``` pr-pipeline mol-pr-blast-radius RootOnly=false pr-pipeline mol-pr-from-issue RootOnly=false (was true) pr-pipeline mol-pr-review RootOnly=false pr-pipeline mol-pr-ship RootOnly=false pr-pipeline mol-pr-start RootOnly=false pr-pipeline mol-pr-triage RootOnly=false pr-review mol-adopt-pr RootOnly=false, rootKind=workflow, 7 steps pr-review mol-pr-ci-diagnose RootOnly=false pr-review mol-pr-iterate RootOnly=false pr-review mol-pr-merge-only RootOnly=false pr-review mol-pr-revert RootOnly=false (was true) ``` For `mol-adopt-pr` specifically, four assertions: the root is Ready-visible, `RootOnly` is false, all five steps survive, and the `finalize` step still has a blocking edge on `human-gate`. A mutation probe confirms the suite fails if `pour` is dropped, so the wrong fix cannot pass. `go build ./...` and `go test ./...` are green. ## Test plan - [ ] `go build ./... && go test ./...` - [ ] Import `pr-review` into a city alongside `pr-pipeline` and confirm no formula name resolves ambiguously - [ ] `gc sling <rig>/polecat mol-adopt-pr --formula --var pr=<N>` starts a workflow and routes to the pool - [ ] Confirm the human gate blocks finalize until it is closed - [ ] `gc sling` `mol-pr-from-issue` and `mol-pr-revert` and confirm their steps materialize --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…all#259) ## Summary - restore a wrapper-authored Observer run id after the canonical claim command updates session pointers - resolve source beads through the claimed step's input convoy and `city:` provenance metadata - declare only the explicit `GC_BEADS_PROJECT_ID` / source-bead tuple; do not infer project aliases - preserve claim success while reporting partial telemetry failures on stderr ## Tests - `python3 -m pytest gascity/tests/test_formula_assets.py -q` - `python3 -m pytest tests/test_no_bare_bd_commands.py gascity/tests/test_formula_assets.py -q` - full Python pack suite: 1171 passed, 8 skipped, 9412 subtests - `python3 validate_registry.py --require-git` - `go test .` Co-authored-by: CI Bot <ci@beads.test>
…#284) ## Summary - package an optional city-executive-status skill inside oversight-rig - request structured project-owner updates and aggregate them into an Obsidian-compatible portfolio brief - keep semantic health judgments with project owners while scripts handle validation, atomic writes, and optional hash-deduplicated publishing - keep the example schedules inert until a consuming city explicitly configures them - document pack and standalone sharing paths ## Safety - rejects malformed, oversized, and symlinked status inputs - escapes owner-provided HTML before rendering - executes dispatch and publishing adapters without a shell - preserves the existing brief when no valid inputs are available - contains no machine-specific paths, personal identifiers, or credentials ## Test plan - GC_TEST_BIN=/path/to/gc PYTHONDONTWRITEBYTECODE=1 pytest -q oversight-rig/tests oversight-rig/assets/scripts/test_resolve_rig_channel.py - PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s oversight-rig/skills/city-executive-status/tests -q - coverage report for both scripts: 96 percent total - ruff format --check and ruff check - skill-creator quick_validate.py - gitleaks scan of upstream/main..HEAD --------- Co-authored-by: sjarmak <t@t.co>
…not stale local HEAD (gastownhall#228) ## Problem `do-work`'s `prepare-worktree` step creates the ephemeral worktree from the launcher's **local `HEAD`** and never fetches: ``` git worktree add "$WORKTREE" --detach HEAD ``` There is no `git fetch` anywhere in the do-work steps, so every do-work worktree inherits whatever the launcher checkout happens to be — commonly **behind `origin`**. Workers dispatched onto city rigs then branch off a stale base, producing diffs against an old tree and spurious conflicts. ## Fix Mirror the pattern the other worktree-creating formulas already use — `core/formulas/mol-scoped-work.toml` (`git fetch --prune origin` + `git worktree add … --detach origin/{{base_branch}}`) and `gastown/formulas/mol-polecat-work.toml` (`git fetch origin {{base_branch}}` + checkout of `origin/{{base_branch}}`). `do-work` is a workflow (prose steps), not a formula with a `base_branch` var, so the default branch is resolved dynamically using the same idiom already present elsewhere in this repo (`pr-pipeline`, `contributing`, `discord` formulas): ``` DEFAULT_BRANCH=$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p') git fetch --prune origin "$DEFAULT_BRANCH" git worktree add "$WORKTREE" --detach "origin/$DEFAULT_BRANCH" ``` - **No hardcoded `main`** — the default branch is discovered from the remote. - **Fail closed** — if the default branch can't be resolved, the step fails rather than silently falling back to (stale) local `HEAD`. - **`work_dir` recording (step 5) is unchanged.** ## Scope One hunk, in `gascity/assets/workflows/do-work/prepare-worktree.md` step 4. No formula graph, metadata, or step-ordering changes. ## Test `gascity/tests/test_formula_assets.py::FormulaAssetTests::test_do_work_formula_requires_persisted_item_worktree` passes — all asserted description fragments (`worktrees/<source-anchor-id>`, `git worktree add`, `gc bd update … work_dir=`, the convoy/anchor invariants) are preserved. ## Notes - Reported downstream as a stale-base symptom: do-work workers on city rigs branching off a base behind `origin/main`. - A companion improvement (not in this PR) is a periodic per-rig "freshen rig source from `origin`" order, so agents doing reads/research against a rig's checked-out source also get a current tree — tracked separately. Co-authored-by: Jeff Burn <jeff.burn@Jeffs-MacBook-Pro-2.local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…h-blind collection query (gastownhall#310) ## The bug The three `gascity` review gates found their molecule members with a metadata collection query: ```sh gc bd list --all --metadata-field "gc.root_bead_id=$ROOT" --json --limit=0 2>/dev/null ``` A metadata collection query carries **no bead id**, so on a city that relocates the graph class `bd` has nothing to route on and **refuses the read (exit 1)** rather than answering from the wrong store. Two of the three call sites also carried an explicit `|| printf '[]'`, and all three carried `2>/dev/null` — so the refusal became an **empty member set**, not an error. The gate then never saw its verdict key and returned `iterate`, which is indistinguishable from a review that genuinely hadn't finished. The loop ran until Ralph exhausted its attempts. | script | starved key | |---|---| | `design-review-approved.sh` | `design_review.verdict` | | `gap-analysis-approved.sh` | `gap_analysis.verdict` | | `implementation-review-approved.sh` | `code_review.verdict` | ## The fix `gc ready` is the federating reader — city store, rig stores, and the relocated graph store, across both tiers. It takes exactly one `--status` and has no `--all`, so the member set is the union of one leg per status. The four legs run **concurrently**. A check gate has a 10m budget and one `gc ready` costs ~17s on a loaded city; four sequential legs (~67s) inside a five-iteration poll loop would come uncomfortably close to that budget. A leg that fails **names itself on stderr and fails the helper**, which propagates under `set -euo pipefail` to a nonzero script exit — the gate iterates instead of silently approving or silently starving. Silent starvation is the defect being fixed; the replacement must not reintroduce it. ## Verification **Live**, against maintainer-city's relocated graph store (root `gcg-284771409692055`): - **70 members**, all 70 carrying `gc.step_id` - sqlite ground truth: **70** — exact match - **17.27s** (vs ~67s for the sequential form) **Control + negative**, with a stub `gc` on `PATH`: | case | rc | members | stderr | |---|---|---|---| | control — all legs OK | 0 | 4 | *(none)* | | negative — `closed` leg refuses | 1 | 3 | `gmol: gc ready failed for status: closed` | Under `set -euo pipefail` the negative **propagates**: the caller aborts rather than continuing on the short set. ## Scope Same fix already landed for the workflows-pack gates (`adopt-pr-ci-ready`, `adopt-pr-review-approved`, `code-review-approved`, `design-review-approved`) and for the gc-plan-pack copies (gastownhall#309). These three gates are **latent on maintainer-city today** — its graph store has no `design-review-fixes` continuation group and no `design_review`/`gap_analysis` verdict keys — so this closes the gap before the formula next runs rather than repairing a live stall. The same three scripts also exist, still unfixed, in the detached-HEAD `build-methodology-packs` worktree (`f77fab4`), consumed by `paxel-city`, `paxel-city-cherry`, `maintainer-city-cherry` and `gc-methodology-smoke-city`. None of those cities is currently running; that worktree needs a rebase onto this fix. --------- Co-authored-by: CI Bot <ci@beads.test>
…ression (ga-2pej2) (gastownhall#315) Reconciles two months of uncommitted working-tree drift in the fleet's live pack source (bead ga-2pej2). The live checkout had been carrying ~4 uncommitted edit waves (69 modified + 13 untracked files) over a branch 155 commits behind main — including a June-14 wave that silently regressed every role prompt to the pre-CLAIM_JSON hook protocol with the in-tool-call claim poll loop (the aggravator behind this week's claim-orphaning incidents, ga-fylee/ga-4qdfn). Method: verbatim rescue snapshot (`rescue/worktree-20260814`, pushed) → three-agent wave-by-wave triage vs the old HEAD (28 DROP / 50 KEEP, diff-evidenced) → three-way re-validation of every keep against main → this branch, from `origin/main`: - `68ff798` — superpowers build summary artifact requirement (novel vs main; note: main's `summarize-implementation` step is the structural answer to the same problem — this prose is idempotent alongside it, reviewers may drop it) - `5443a07` — June asset guards: build-basic-review lanes anchored to the implementation worktree, outcome-close footers, one-element-list guards - `0ebccbb` — the discord multibot slice: restores origin-pushed `feat/discord-multibot-bridge` content (committed-but-unmerged work found live in the tree), tests taken at the tip the scripts shipped with (`4e0b3d2`) - `e537461` — July artifact gates: rig-root artifact resolution, retry-aware review evidence, no-self-approval rule Dropped: the 20-file protocol regression (never committed anywhere; HEAD was always correct), 11 conflict paths where main evolved the same code further (incl. the initial-review adapter contract — reasons on ga-2pej2), runtime residue dirs. One salvageable follow-up flagged: the authoritative-lane selection block from the skipped `{target}.md` region. Gates: full pack suite green per-commit (1249 passed + 9462 subtests; registry, gc lint ×7, role-prompt integration, shell suites, go/slack/runtime-cloudflare). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: CI Bot <ci@beads.test>
gastownhall#330) Closes gastownhall#308. `gc bd create --prefix hq-` in `gastown/assets/prompts/crew.template.md` is one of the lint findings this fixes: `bd create` has no `--prefix` flag. The line now reads `gc bd create --city {{ .CityRoot }}`. --- `gc lint gastown` reports 14 findings on `main`. This fixes the 7 that are pack defects, pins the 7 that are defects in gc's own linter, and puts gastown under CI so the count cannot drift again. This is the follow-up gastownhall#329 said it would be: *"Those 14 `gastown` findings are pre-existing and are not fixed here, because they are a change to a different pack and belong in their own PR."* ## Why gastown had 14 findings and nothing failed `.github/workflows/ci.yml` lints `gascity gascity/roles bmad compound-engineering gstack superpowers profiler`. gastown is not in that loop, and it is a supported pack: `supported-pack-nightly.yml` exercises it. So the findings accumulated with every check green. ## The 7 pack defects The trap is an asymmetry in `bd` itself: | command | flag | | --- | --- | | `bd list` | `-l, --label` (singular) | | `bd create` | `-l, --labels` (plural, comma-separated) | | `bd update` | `--add-label` / `--remove-label` / `--set-labels` | | `bd close` | `-r, --reason`, and no `--status` | Every wrong site is a `create` or `update` written in the `list` spelling. - `bd create --label=warrant` becomes `--labels=warrant` in the boot, deacon and witness prompt templates, and in `mol-deacon-patrol`, `mol-witness-patrol`, `mol-shutdown-dance` and `mol-digest-generate`. The formulas matter more than the prompts: they run the command rather than suggesting it. The `bd list --label=warrant` calls next to them are correct and are left alone. - `bd update --label=pool:...` becomes `--add-label pool:...` in the mayor and crew templates. Both sit in a "Common mistake" column, so the flag was never going to be run, but an anti-example built on a flag that does not exist teaches the wrong lesson twice. - `bd create --prefix hq-` becomes `--city {{ .CityRoot }}` in the crew template. `--prefix` does not exist. `--rig hq` does not work either: `--rig` names a work rig, and HQ is the city store, whose flag is `--city <path>` (`cmd/gc/cmd_bd.go`). `.CityRoot` is already used six times in that same file. - The polecat template forbade setting `--status=closed`, a flag `bd close` does not have. It now forbids `gc bd update -s closed`, which is the real way to do the thing it was warning against. Three of these sit on backslash-continued lines. `internal/bdflags/scan.go` is line-oriented and does not join continuations, so `gc lint` never reported them at all. They were found by grep. ## The fixture that held the defect in place `scripts/gascity_pack_inference_gate.py` pinned the literal `gc bd create --type=task --label=warrant` as a required contract fragment, and `tests/test_gascity_pack_inference_gate.py` asserted the same literal about the contract table. Fixing the formula turned both red. Three layers agreed on a command that does not work. Same shape as the `slack-full` defect in gastownhall#329, where `test_manifest.py` asserted the empty `slash_commands` array that made the manifest unimportable. ## The 7 that are gc linter defects Pinned with their gc-side cause and a refuting command in `tests/gastown_lint_upstream_defects.txt`. **Five `named_session "<name>" targets pool-controlled agent`.** `cmd/gc/cmd_lint.go`'s `agentHasPoolControls` treats any `max_active_sessions` as pool control, and its escape hatch requires `min==0 && max==0`. gc's own core disagrees in two places. `internal/config/session_capacity.go`'s `SupportsInstanceExpansion` doc comment names "Named-session agents (MaxActiveSessions=1 with a `[[named_session]]` entry, no Min/ScaleCheck)" as a shape it deliberately handles. `internal/doctor/checks_named_session.go` warns only when `EffectiveMinActiveSessions() > 0`, which is 0 here. Following the remediation would delete the cap that holds the singleton: `internal/agentutil/pool.go`'s `ScaleParamsFor` sets `Min` from `EffectiveMinActiveSessions()`, so a bare cap spawns nothing on its own. Taking the documented hatch is worse: `SupportsGenericEphemeralSessions` returns false at `max=0`, so it changes what the agent can do rather than annotating intent. **Two `bd mol wisp uses unrecognized flag "--age"`.** `internal/bdflags` keys top out at two words. "mol wisp" matches, `gc` is read as a positional, and `--age` is validated against the parent instead of the subcommand that owns it. `bd mol wisp gc --help` documents `--age` in its own examples. The scanner's design is right about the general case (an unmatched subcommand is skipped because there is no ground truth for it); this fires because the *parent* matched. Both are worth fixing in `gastownhall/gascity`. Neither can be fixed here. ## The guard `tests/test_gastown_lint_findings.py` runs the real `gc lint gastown --json` and asserts the result equals the pinned set exactly, in both directions. A new finding fails. A finding that *disappears* also fails, so an upstream fix has to be un-waived deliberately instead of quietly outliving its waiver. **Fidelity, per seam.** The guard replaces no seam. It invokes the same `gc` binary CI already installs for the shared-role-prompt step, against the real pack directory, and parses the linter's own JSON. There is no reimplementation of the linter to drift from it, and no fixture standing in for the pack. **Where it stops, stated in the test's own docstring.** It cannot see a flag outside the `bdflags` manifest, and it is line-oriented, so the three backslash-continued sites fixed here were never visible to it. A green means "no new finding of a kind `gc lint` can see", not "every `bd` invocation in this pack is valid". **Proven by mutation**, each rail run against the real binary: | mutation | result | | --- | --- | | reintroduce `--label=warrant` in a template | red, both tests | | delete a line from the waiver | red | | add a waiver entry gc does not report | red | | fake `gc` reporting on a different pack | red | | fake `gc` exiting 2 | red | | fake `gc` duplicating an already-pinned finding | red | Restored, both tests pass. The last two rails exist because they were missing from the first draft. The review caught that it ignored the exit code and collapsed diagnostics into a `set`, so a crashing linter and a duplicated finding both read as green. It now rejects any exit code other than 0 or 1, rejects a report naming another pack, and counts findings as a multiset. ## Files that are CI machinery Flagging these explicitly, because a reviewer applying a "no CI changes" criterion should see them rather than discover them: - `.github/workflows/ci.yml` adds one step, running the guard with the `gc` binary the preceding step already installed. - `scripts/gascity_pack_inference_gate.py` is invoked by `supported-pack-nightly.yml` and `gascity-pack-inference.yml`. The change is two string literals in `GASTOWN_BUILD_WORKFLOW_CONTRACTS`. ## Verification - `gc lint gastown` reports 7, down from 14. - `python3 -m pytest tests -q`: 103 passed, 8 skipped. - The full CI pytest line has 14 pre-existing failures on this branch (7 discord, 2 gascity, 5 github). They are the env-inheritance defect tracked in gastownhall#307 and fixed in gastownhall#329, which this branch does not carry. Baseline measured on unmodified `main`: identical 14. This branch adds none. - `gastown/tests/test_*.sh` pass. --------- Co-authored-by: sjarmak <t@t.co>
…twork (gastownhall#331) Closes gastownhall#305. ## The reported defect, and three more sites `git remote show origin` opens a connection every time it runs, to read a value git already stores locally. The issue names the `prepare-worktree` step in the `gascity` pack. Grepping for the command finds four: | asset | what it gates | |---|---| | `gascity/assets/workflows/do-work/prepare-worktree.md` | the base branch every do-work worktree is created from | | `contributing/formulas/mol-contributing-fine-tune.formula.toml` | refuses to ship from the default branch | | `pr-pipeline/formulas/mol-pr-ship.formula.toml` | same guard | | `pr-pipeline/formulas/mol-pr-from-issue.formula.toml` | same guard | ## A second defect the issue does not mention The three formulas ended the line in `|| echo "main"`. That fallback can never fire. A pipeline's exit status is its last command's, and `sed` exits 0 on empty input, so a failed `git remote show` sets `DEFAULT=""` and the fallback is skipped. Every caller that read `DEFAULT` as `main` was reading the empty string. This needs no repository to reproduce: ```console $ X=$( (exit 3) 2>/dev/null | sed -n 's/.*HEAD branch: //p' || echo "main") $ echo "[$X]" [] ``` Downstream, `[ "$DEFAULT" = main ]` and `[ "$DEFAULT" = master ]` both fail, so the guard silently stops guarding. ## The fix All four now read `refs/remotes/origin/HEAD`. The issue calls that "worth confirming", and it is. `refs/remotes/origin/HEAD` is written by `git clone` and refreshed by `git remote set-head origin --auto`. It is **not** written by `git init` plus `git fetch`, which is how `actions/checkout` builds a workspace. Of three checkouts on the machine this was written on, one had no `origin/HEAD` at all and `git symbolic-ref` there returns `fatal: ref refs/remotes/origin/HEAD is not a symbolic ref`. So a bare swap would have failed closed on a real repository. Each site therefore reads the ref, refreshes once if it is missing, re-reads, and only then falls back: ```sh DEFAULT=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||') if [ -z "$DEFAULT" ]; then git remote set-head origin --auto >/dev/null 2>&1 || true DEFAULT=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||') fi [ -n "$DEFAULT" ] || DEFAULT=main ``` The refresh branch is one network call in the case that needs it, against one on every run. A stale ref costs nothing at the `prepare-worktree` site because the next line fetches the branch anyway. Failure behavior is unchanged per site and deliberate: `prepare-worktree` still fails closed with an empty value (gastownhall#228 landed because basing a worktree on the wrong branch is worse than not making one), and the three formulas keep their explicit `main` fallback, which now actually runs. ## The test `tests/test_default_branch_resolution.py` pulls the shell out of each shipped asset and runs it under `sh` against git repositories built on disk. It reads the TOML formulas through `tomllib` and the workflow step out of its fenced block, so there is no second copy of the snippet to keep in sync: edit a formula and the test runs the edit. Fixture repositories default to a branch named `trunk`. `main` is the answer a broken resolution reaches by accident through the fallback, so a fixture on `main` cannot tell a working resolution from a failing one. Each extractor matches the superseded shape as well as the current one, which makes this an A/B rather than a presence check. Reverting the four files and re-running: ``` 4 failed, 3 passed '' != 'trunk', '' != 'main' -- at the shipped site ``` Restoring them, with nothing else changed: ``` 7 passed ``` The two cases that pass either way are the network-reachable one, where the old command did work, and the control that runs the superseded line directly to show it yields `""` rather than `"main"`. Cases covered: resolution with the remote directory renamed away (proves it is local), resolution via the `set-head` refresh with `origin/HEAD` deleted, the fallback with both paths broken, `prepare-worktree` failing closed instead of guessing, and a repository-wide check that no asset still shells out to `git remote show origin` for this. ## Verification ``` $ python3 -m pytest tests contributing/tests gascity/tests pr-pipeline/tests -q 2 failed, 322 passed, 8 skipped ``` The two failures are `gascity/tests/test_formula_assets.py::...city_claim_command...`, which fail identically on `upstream/main` with this branch's changes stashed. They are not touched by this change. Co-authored-by: sjarmak <t@t.co>
… data plane (gastownhall#289) ## Problem `mol-deacon-patrol`'s dolt-health step evaluates its threshold table unconditionally, and the first row escalates **CRITICAL to the mayor** on `server.reachable == false`. A city whose bead ledger lives on another backend (MySQL, Postgres) has no managed Dolt runtime at all — there is no data plane to report on, and there never will be. On such a city `gc dolt health --json` reports `applicable: false` with a reason, and the remaining fields are zero-valued placeholders: `server.reachable` is `false` because **no Dolt server exists**, not because one is down. Evaluating the table on that payload pages an unactionable CRITICAL every patrol cycle. ## Change One step description in `gastown/formulas/mol-deacon-patrol.toml`: check `applicable` before the threshold table, and when it is false, log `Dolt health: not applicable (<reason>)` and move on — no escalation, no dog nudge. The `applicable` field is added to the documented payload shape, and the Step 2 heading now states the precondition. ## Backward compatible — safe to land in either order An older `gc dolt health` omits the field entirely. `jq` yields `null`, `null == false` is false, so a payload without `applicable` is evaluated exactly as it is today. This can land before or after the producer half. ## Pairs with the gascity-side fix Tracked as gas-e05. Before that fix, `gc dolt health` on a non-Dolt city exited **78 with no payload at all** (it dies while sourcing `runtime.sh`, which resolves the port at source time), so this step could not distinguish "Dolt is not in use here" from "healthy" from "the check itself is broken" — commit bloat, stale backups, zombie servers and orphan databases went unreported with no signal that monitoring had gone blind. The producer now emits a well-formed report and exits 0; this is the consumer half that reads it. Verified on the MySQL city that filed the bug: `gc dolt health --json` went from exit 78 with no payload to exit 0 with `applicable: false`, `reason: "city beads backend is mysql; no managed Dolt runtime to probe"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#326) ## Summary - explain the Gas City SDLC from requirements through optional publication, including testing, review fixes, and final reporting - map operator starting points to the five full-lifecycle public formulas and the phase where each begins - separate focused workflows from full-lifecycle continuations and document their actual outputs and boundaries ## Why The previous entrypoint table listed artifacts and formulas without telling operators where they were in the lifecycle or what would happen next. It also blurred requirements (the operator's plan for what to build) with the implementation plan that follows. This update makes the path explicit for operators arriving with an idea, requirements document, reviewed implementation plan, implementation convoy, GitHub issue, or GitHub PR. ## Validation - `git diff --check` - `python3 -m unittest gascity.tests.test_formula_assets` (88 tests passed) No deployment steps are required; this is a documentation-only change.
…roll) (gastownhall#205) ## Problem Scrolling the wheel over a full-screen TUI on the **alternate screen** (Claude Code, vim, less) opens an empty copy-mode — the indicator shows `[0/0]` and nothing scrolls. Users read this as "tmux can't scroll back." ## Why `tmux-keybindings.sh` binds `WheelUpPane` to force copy-mode over any pane not already in copy-mode. That's deliberate (per the comment): so tmux scrollback wins over app-local scrolling on mouse-reporting apps. But the alternate screen has **no tmux scrollback to enter** — the TUI owns the alternate buffer — so forcing copy-mode there just opens an empty `[0/0]`. The binding's own goal ("scrollback wins") can't be met on the alternate screen; the result is "nothing works." ## Fix Add `#{alternate_on}` to the passthrough condition: ```diff -if-shell -F -t= "#{pane_in_mode}" "send-keys -M" "copy-mode -e" +if-shell -F -t= "#{||:#{pane_in_mode},#{alternate_on}}" "send-keys -M" "copy-mode -e" ``` - **Alternate screen** → `send -M`: hand the wheel to the app so it scrolls its own history (Claude Code scrolls its conversation; less/vim scroll). - **Main screen** (incl. mouse-reporting apps) → `copy-mode -e`: unchanged — the original "scrollback wins" intent is preserved exactly where scrollback actually exists. - **Already in copy-mode** → `send -M`: unchanged passthrough. Keying on `alternate_on` (rather than `mouse_any_flag`) is intentional: it targets the actual defect (no scrollback on the alternate screen) without reversing the deliberate main-screen behavior. ## Testing - `sh -n` clean. - tmux accepts the format string live (`if-shell -F "#{||:#{pane_in_mode},#{alternate_on}}" ...`). - Verified on a live Claude Code pane: `alternate_on=1`, so the wheel now routes to the app instead of an empty copy-mode; main-screen shell panes still enter scrollback. Co-authored-by: Jeremy Irish <jeremy@geocaching.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…astownhall#207) status-line.sh piles up concurrent renders that each hit the bead store, self-amplifying exactly when the store is already slow. Observed live: 4 concurrent renders for one session, the oldest at 32s. Two compounding bugs: 1. **`run_bounded` silently runs unbounded on macOS** — there is no coreutils `timeout`, so the fallback branch runs `gc hook` + `gc mail check` with no bound; under a slow store a single render hangs for 30s+. A plain `alarm`+`exec` fallback is not enough either: the Go runtime swallows SIGALRM, so `gc` sails past the alarm (verified: python child dies at 2s, gc child survives). The perl fallback forks and SIGTERMs (then SIGKILLs) the child, which Go honors. 2. **No single-flight**: while one slow render holds a stale cache, every tmux status tick (5s in gastown sessions) misses the cache and spawns its own full refresh. Refreshes now take a `mkdir` lock; losers serve the stale cache instead of piling on. Locks older than 120s are broken so a killed refresher cannot wedge the status line. Measured on the affected host (dolt-backed store under load): refresher render 28s → 5.2s worst case; concurrent render 28s → 0.12s (stale-served); cache hit 0.09s. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…_ROOTS (gastownhall#187) ## Problem `gascity/REQUIREMENTS.md §Artifact Layout And Schemas` invites derived packs to extend the schema set: > "Derived packs may add stricter methodology-specific schemas or extension fields, but they must not relax the base schema or replace required base sections, fields, statuses, traceability, or coverage." …but the shared validator gives them no loading mechanism. `validate_build_artifact.py` hardcodes `SCHEMA_ROOT` to the base pack's own tree and `load_schema()` globs only that directory — no env override, no derived-pack search path. A derived pack's new-id schema (e.g. `acme.build.bdd.v1`) fails as `unknown build artifact schema`, so a derived pack cannot use the shared artifact gate (`build-artifact-valid.sh`) for the stages it adds. Today no derived pack in this repo ships a `schemas/build/` dir — plausibly *because* this resolution gap makes it a dead end. This is the schema-side sibling of the check-script delivery work in gastownhall/gascity#3834 / `feat/migrate-checks-to-assets`: that migration makes the *gate script* resolvable from pack layers; this PR makes *derived-pack schemas* resolvable by the validator the script invokes. ## Change - `schema_roots()` — the base `SCHEMA_ROOT` always first, then any extra roots from a new `GC_BUILD_SCHEMA_ROOTS` env var (`os.pathsep`-separated; blank/missing entries skipped). - `load_schema()` iterates the roots in order. **Immutability is preserved by construction**: base-first ordering means a published base schema id always resolves from the base pack before any extra root is consulted — extra roots can only ADD new ids, never shadow or relax a published one (per "Schema IDs are immutable compatibility contracts"). `validate_schema_definition()` continues to apply to schemas from extra roots unchanged. **Zero behavior change when the env var is unset** — `schema_roots()` returns exactly `[SCHEMA_ROOT]` and `load_schema()` is unchanged in effect (covered by an explicit test). The CLI surface (`--schema`/`--path`) and `build-artifact-valid.sh` are untouched: a city/deployment that wants derived-pack schemas exports the env var wherever the check runs. ## Tests New `BuildArtifactSchemaRootsTests` in `gascity/tests/test_validators.py`: 1. `test_extra_root_resolves_new_schema_id` — a new-id schema in an extra root loads. 2. `test_extra_root_cannot_shadow_base_schema_id` — a `gc.build.requirements.v1` file in an extra root is never consulted; the base definition (with `workflow.id` required) wins. 3. `test_unset_env_is_byte_identical_base_behavior` — unset env → `[SCHEMA_ROOT]` only; unknown id still raises. 4. `test_missing_or_blank_extra_roots_are_skipped` — blank/nonexistent entries degrade silently. Full `gascity/tests` directory: 171/171 green on this branch. ## Alternatives considered - `--schema-root` CLI flag: rejected — would require changing `build-artifact-valid.sh` and every formula `[steps.check]` contract; the env var flows through the existing exec seam untouched. - Globbing derived packs' `schemas/build/` automatically: rejected — the validator has no reliable view of the import graph from its execution context; explicit deployment-owned roots are simpler and auditable. --- Context: we build a methodology pack downstream that extends `build-base` with additional gated stages (test-first + verification lanes) and hit this as the one blocker to schema-gating them with the shared validator. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…nhall#243) ## Failure mode gastownhall#237 added a deterministic witness-heartbeat pass to `mol-deacon-patrol`, but the formula resolves its helper through `${GC_PACK_DIR:-}`. Gas City supplies that variable while dispatching pack commands, orders, and doctor checks; it is not part of a managed agent session's environment. In a deacon session the path therefore becomes `/assets/scripts/witness-heartbeat-check.sh`. The executable check fails its existence guard, the formula falls back to the LLM judgment pass, and the deterministic protection added by gastownhall#237 never runs. ## Fix - expose the existing read-only helper as `gc gastown witness-heartbeat-check` - invoke that pack command from `health-scan`, letting Gas City's dispatcher supply the canonical city and pack context - document the command's output and exit-code contract - add a regression that starts with no `GC_PACK_DIR` in the agent environment and proves pack-command dispatch reaches the heartbeat implementation The checker itself and its threshold/verdict behavior are unchanged. ## Validation - `bash gastown/tests/test_witness_heartbeat_check.sh` - all `gastown/tests/test_*.sh` suites - `bash -n` on every Gastown shell script; `sh -n` on the new wrapper - all 17 Gastown TOML files parse - `python3 -m pytest tests contributing/tests gascity/tests discord/tests github/tests slack-full/tests slack-channel/tests pr-pipeline/tests profiler/tests -q` — 1170 passed, 8 skipped, 9412 subtests passed - `go test .` - real convention-discovered dispatch against Gas City main `bccc52f`, in an isolated city with both city- and rig-scoped Gastown imports, with `GC_PACK_DIR` explicitly removed from the caller environment - `gc lint gastown` output is byte-identical to the base after path normalization: the same 35 pre-existing findings, no new finding
## Summary - remove demo city, rig, prefix, and organization assumptions from the Mayor prompt - replace generated startup query blobs with atomic `gc hook --claim --json` for Mayor, crew, and dog roles - report the effective Dolt port through `gc dolt status` instead of hardcoding 3307 - add regression coverage for generic and compact prompt contracts ## Verification - `bash gastown/tests/test_gastown_pack_assets.sh` - `python3 validate_registry.py` - `go test ./...` - independent Codex review; addressed its Mayor dispatch and rendered dog prompt findings Co-authored-by: Cherub Kumar <5449665+ckumar1@users.noreply.github.com>
… number (gastownhall#334) `main` has been red since `4d0211e5` and every open PR in this repo inherits the failure. Nothing about the pack or the linter changed. gastownhall#265 collapsed the mayor prompt's four-row rig-routing table into one generic row. That moved the same `bd create --rig` diagnostic from lines 118-121 to lines 23 and 114, and dropped its count from four to two. `tests/gastown_lint_upstream_defects.txt` pinned `file:line` pairs, so the guard read a pure content move as two new findings plus a partially-reported version section. ## The change The finding key is now path and message. Occurrences are counted, so three identical waiver lines waive exactly three findings and a fourth still fails. Given up, deliberately: the waiver no longer says where to look, and a finding that moves within a file is invisible. The section comments already carry the gc-side cause and a refute command, which is what a reader actually needs, and a moved finding is not an event. Still caught: a new path, a new message, one more occurrence of an existing pair. `tests/test_no_bare_bd_commands.py` moves with it. Its exemption grammar required `:<line>:`, so without the same change every waiver entry quoting a `bd` invocation would start reading as a bare `bd` command — a failure about the wrong file. The line number is optional there now, asserted in both directions. ## Test plan A post-5220 local `gc` emits none of the `pre-5220` section, so running the suite locally goes green without ever reading the part that broke. Everything below runs against a stub `gc` reporting what v1.4.1 reports for this tree: the universal seven copied from a real run, and the twenty `pre-5220` entries at the line numbers CI itself reported at `4d0211e5`. | Mutation | Expected | Result | | --- | --- | --- | | pre-fix test + pre-fix waiver, simulated v1.4.1 | red | reproduces all three named CI failures | | post-fix, same simulated output | green | 4 passed | | post-fix + a third `--rig` in the mayor prompt | red | 2 failed | | post-fix + one `pre-5220` entry withheld | red | all-or-nothing subtest fails | | post-fix + a waived finding moved, count unchanged | green | 4 passed | Full suite on this branch with the real `gc`: 112 passed, 8 skipped. Refute the diagnosis: `git log --oneline -3 upstream/main -- gastown/agents/mayor/prompt.template.md`, then `git show 4d0211e^:gastown/agents/mayor/prompt.template.md | grep -n -- --rig` against `grep -n -- --rig gastown/agents/mayor/prompt.template.md`. --------- Co-authored-by: sjarmak <t@t.co>
`gc lint` reads each pack on its own, and the per-pack suites read their
own TOML. Neither says what happens when a city loads the pack, which is
where a user meets it.
`pr-pipeline` linted clean for months while putting a
`formula-requirements` deprecation warning into every importing city's
`gc doctor`. `mol-pr-from-issue.formula.toml` still declared `contract =
"graph.v2"`, and the test covering it asserted that by reading the
file's own text back to itself, with `gc` never involved.
## What this adds
`tests/gc_live_city.py` builds a scratch city, imports one pack into it,
and runs the real binary through `GC_TEST_BIN`.
`tests/test_maintained_packs_live_gc.py` stands up `oversight-rig`,
`pr-pipeline`, `slack-channel`, `slack-full` and `slack-mini`, each on
its own, and checks four things per pack:
- `gc doctor` reports nothing the pack did not cause
- every command verb the pack ships is registered (`gc <pack> ...
--help`)
- every formula resolves (`gc formula list`)
- every agent resolves (`gc agent list`)
Each pack is imported alone so a finding names one pack. They cannot all
share a city anyway: the three Slack packs declare `[[service]]`, which
gc rejects in a rig-scoped import, and `oversight-rig` declares a
rig-scoped `[[named_session]]` that needs a rig binding.
## Reading gc doctor
A scratch city reports a couple of dozen findings that belong to the
environment, so the doctor reading is a set difference against a
baseline city of the same shape. The baseline is not empty: several
checks only run once a city has something to check, so an empty baseline
surfaces them as findings the pack did not cause. It carries one inert
order and one inert agent.
The delta is asserted by set equality rather than emptiness, so a
finding that disappears fails too and fixed work cannot leave a stale
allowance behind.
One entry is non-empty. `slack-full` ships its adapter as source and its
own doctor check reports the binary as not yet built, which is the right
message on a fresh install. `slack-channel` and `slack-mini` ship the
same unbuilt adapter and no check that notices it, which is why their
entries are empty rather than matching.
## Proving the tests can fail
Reverting the `mol-pr-from-issue` fix turns `pr-pipeline` red with the
delta exactly `{formula-requirements}`, and green with the fix back.
That is the property this PR exists for, written as a command.
A command leaf gc will not register (a `command.toml` naming no runner)
turns the surface test red. Two other mutations stayed green and should
have: renaming a leaf, and dropping `help.md` from a leaf that has
`run.sh`. gc registers the second, and both sides of the first derive
from disk.
Every delta assertion rests on the canary pack in `write_canary_pack`,
which earns a `formula-requirements` finding on purpose. Without it, an
empty delta and a doctor that stopped reporting are the same reading.
## Also in this branch
`95b1fe2` pins the seat environment three pack suites silently depended
on. `write_city` strips `GC_*` and `BEADS_*` from the inherited
environment, so running the suite inside a live city cannot reach that
city's store.
## Test plan
```
go install github.com/gastownhall/gascity/cmd/gc@latest
GC_TEST_BIN="$(go env GOPATH)/bin/gc" python3 -m pytest tests/test_maintained_packs_live_gc.py -q
```
12 passed, 9 skipped. The skips are packs that ship no formulas, agents,
or commands. CI runs this as a new step after the lint loop.
---------
Co-authored-by: sjarmak <t@t.co>
…ably (gastownhall#348) ## What this fixes `mol-pr-review` already fetches `additions`, `deletions` and `changedFiles` in step 1, and uses them for nothing but a `**Stats**` line in step 4. The only size handling was a Failure Modes row reading "finish what fits", which spends the whole budget before deciding it could not finish. That combination is what makes an oversized PR expensive rather than merely slow. The review burns the budget and then fails *last*, and a supervising retry loop cannot tell "this can never succeed" from "this timed out", so it re-spends at full price on every attempt. One maintainer reported three Max accounts drained this way. The important part of this change is not the threshold. It is that a refusal is a **distinct, non-retryable outcome**: - `pr review` exits **3**, a code no other path uses, and refuses before `gc sling` starts an agent. - The formula records verdict **`too_large`**, distinct from `approve`, `request_changes` and `block`. It is a statement about the request, not a judgement about the code. - The bead is **closed**, not left failed, so nothing re-queues it. ## Where the checks are, and why there are three | Where | When it runs | What it protects | |---|---|---| | `commands/pr/review/run.sh` | before `gc sling` | the only genuinely pre-spend point, no session exists yet | | formula step 1, before the diff fetch | inside the session | direct slings that bypass the command | | formula step 1, after the diff fetch | inside the session | the diff that was actually fetched | The formula's copies cannot be pre-spend by construction: they execute inside the very session whose cost they are avoiding. That is why the command carries the real gate and the formula carries defence in depth for direct slings. The third check exists because the size read and the diff fetch are two reads of a moving target. A commit pushed between them turns a passing PR into an oversized one, and the fetched diff is the thing that actually costs money, so its lines are counted on disk before anything reads them. ## Failure directions this deliberately does not take - **An unreadable size is not zero.** Zero is the reassuring answer and would wave through exactly the largest PRs. Both an unparseable size and a failing `gh` refuse. - **A malformed limit refuses.** An earlier draft interpolated the limit straight into `[ ... ]`, where a non-numeric value made the test error and the oversized PR proceed. A guard that fails open on bad configuration is worse than no guard, because it still reads as protection. - **`0` disables the guard**, deliberately and explicitly. ## Threshold `max_diff_lines` defaults to **5000** (additions + deletions). Measured against the 200 most recent open PRs on `gastownhall/gascity`: median 248 lines, 188 pass, 12 refuse. The largest is 113,042 lines across 432 files. Set per invocation with `--max-diff-lines <n>`, or per city with `GC_PR_MAX_DIFF_LINES`. ## Tests `pr-pipeline/tests/test_pr_review_size_guard.py`, 20 cases. They **execute** the guard's shell against stubbed `gh`/`gc` rather than reading the TOML back to itself, this pack already carries a test that pinned a deprecated value that way and passed for months while every importing city threw a `gc doctor` warning. The `gh` stub logs every call and exits non-zero on anything it was not expected to be asked, including `pr diff`. So "nothing was read" is asserted from the invocations, not inferred from where a heading sits in the source. The harness reads the default from the formula rather than restating it, and supplies only the variables step 1 genuinely sets, an earlier version injected `REPORT_PATH` and concealed a real defect where the guard wrote to a name the formula does not assign until 2,600 characters further down. Each guard was mutated to confirm the tests can go red, and each mutation kills a disjoint set: | Mutation | Kills | |---|---| | remove the pre-fetch guard | refusal + no-diff-fetched cases | | `-gt` → `-ge` | the at-the-limit case only | | remove the post-fetch recheck | both post-fetch cases only | | drop `exit 3` from `run.sh` | the command preflight refusal only | | drop the guard's `REPORT_PATH` | "refusal is never recorded as an approval" | `gc lint pr-pipeline` passes, and the pack passes the live-`gc` harness (`GC_TEST_BIN` set, real binary): 3 passed, 1 skipped (pr-pipeline ships no agents). Co-authored-by: sjarmak <t@t.co>
…not one precomputed form (gastownhall#360) ## Summary The claim protocol computed `EXPECTED_ASSIGNEE` as `BEADS_ACTOR > GC_SESSION_NAME > GC_SESSION_ID` and required strict equality at verification. `gc hook --claim` now records an unaliased pool worker's claim under its OCCUPANT identity — the session bead id (gastownhall/gascity#5663) — because the slot-derived session name is a chair shared by every occupant of the slot. The strict compare therefore rejected the session's OWN claim forever (`CLAIM_REJECTED assignee mismatch` spin, gascity `ga-jrnou`) without releasing it. Accept the claim when the verified assignee matches ANY of the session's own identities (`BEADS_ACTOR`, `GC_ALIAS`, `GC_SESSION_ID`, `GC_SESSION_NAME`, `GC_AGENT`): correct under both hook generations and both aliased and unaliased shapes, and still rejects a claim owned by a different session. Validated live on maintainer-city (as a hot-patch of the serving gc-plan-pack worktree): the wedged reviewer seat's next incarnation printed `CLAIMED_BEAD_ID=...` on first try and the adopt-pr pipeline proceeded through merge. ## Testing - Behavior probe: occupant-id and chair forms accepted; a foreign assignee still rejected - `bash -n` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: CI Bot <ci@beads.test> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ps orphan past the resume-check gc bd update --assignee alone does not change status. A freshly poured patrol wisp is created open and stays open, but every session-start resume check filters on status=in_progress — so any wisp that outlives its pour moment (most obviously the find-work IDLE exit) is invisible to the next session, which pours a duplicate instead of resuming, and the orphan is never burned. Observed live on voxist-api and voxmemo refineries (stacked orphaned wisps). Fixed at every pour+assign callsite: - mol-refinery-patrol.toml: startup snippet + 5 pour-next sites - mol-deacon-patrol.toml: startup snippet + pour-next site - mol-witness-patrol.toml: startup snippet + pour-next site - witness/deacon/refinery prompt.template.md restart snippets gc bd update is a single write, so assignee+status is atomic and idempotent — no claim-race handling needed. Refs: Voxist va-i2uc (cross-rig confirmation va-i2uc notes); companion Voxist override of this formula already carries the same predicate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
gc bd update --assigneealone does not change status. A freshly poured patrol wisp is createdopenand staysopen, but every session-start resume check filters onstatus=in_progress— so any wisp that outlives its pour moment (most obviously the refinery find-work IDLE exit: "no work found → write IDLE, stop, do NOT burn") is invisible to the next session, which pours a duplicate instead of resuming, and the orphan is never burned because nothing ever burns it.Observed live on two independent rigs (voxist-api refinery: duplicate orphaned wisps
va-wisp-*; voxmemo refinery: 4 stacked wisps never reconciled to one).Change
Add
--status=in_progressat every pour+assign callsite (single write → atomic and idempotent, no claim-race handling needed):gastown/formulas/mol-refinery-patrol.toml— startup snippet + all 5 pour-next sitesgastown/formulas/mol-deacon-patrol.toml— startup snippet + pour-next sitegastown/formulas/mol-witness-patrol.toml— startup snippet + pour-next sitegastown/agents/{witness,deacon,refinery}/prompt.template.md— restart snippetsAlternatives considered
--claiminstead: not idempotent against a bead already assigned (errors "issue already assigned"), so it cannot be dropped in mechanically without also handling that error path.--type=wispCURRENT_WISP fallback (already fixed upstream in refactor(contributing): rename lifecycle skills, drop role name, add mayor-mode umbrella gastownhall/gascity-packs#143 as--type=molecule): not sufficient — the resume/burn queries also filter--status=in_progress, which stays empty while pours never transition status.Verification
Reproduced live on voxist-api:
gc bd update <wisp> --status=in_progressmade the wisp visible to the documented resume-check one-liner; identical query returned[]before the update.Refs
Voxist tracking bead va-i2uc (fleet-wide confirmation in its notes).