Skip to content

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
mainfrom
gc/va-i2uc
Closed

fix(gastown): patrol pour+assign must set --status=in_progress or wisps orphan past the resume-check#1
voxist-merge-bot[bot] wants to merge 78 commits into
mainfrom
gc/va-i2uc

Conversation

@voxist-merge-bot

Copy link
Copy Markdown

What

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 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_progress at 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 sites
  • gastown/formulas/mol-deacon-patrol.toml — startup snippet + pour-next site
  • gastown/formulas/mol-witness-patrol.toml — startup snippet + pour-next site
  • gastown/agents/{witness,deacon,refinery}/prompt.template.md — restart snippets

Alternatives considered

Verification

Reproduced live on voxist-api: gc bd update <wisp> --status=in_progress made 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).

sjarmak and others added 30 commits July 9, 2026 13:02
…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>
julianknutsen and others added 27 commits July 24, 2026 14:09
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.