Skip to content

Gas City Adoption

Decision

Bluefly adopts upstream Gas City as the orchestration owner. Bluefly is a Gas City distribution — curated packs, configuration, and integration wiring — not a parallel orchestration platform. Bluefly does not fork, wrap, or re-document upstream behavior. Upstream docs are the only reference for installation, CLI, configuration schema, formulas/orders, providers, API, Beads/Dolt topology, troubleshooting, and deprecations. Do not reproduce or maintain those materials locally (see upstream-lawbooks (NOT_FOUND — library/ has never existed in this repository)).

Ownership Boundary

Capability Owner
Orchestration engine Gas City
Agents, formulas, orders, packs Gas City configuration model
Work graph Beads
Versioned relational state Dolt
Bluefly policy and integration Bluefly
Source history Git
Runtime work history Beads

Bluefly Configuration

Gas City product semantics live in current docs.gascity.com and the factory-operating-contract. This section is Oracle deployment fact, not a restatement of those primitives.

Current Oracle city (ADR-0027, oracle-canonical-architecture, deploy/oracle/beads-topology.yaml in blueflyio/blu/blucity):

  • Canonical city path: /opt/bluefly/blucity (GitLab source blueflyio/blu/blucity). /opt/bluefly/city is superseded.
  • Live Beads endpoint: city_canonical @ 127.0.0.1:3308, database hq, city prefix hq. Rigs inherited_city with unique dolt_database + issue_prefix. One Dolt server. No per-rig Dolt.
  • Tracked Git unbound default: .beads/config.yaml may record gc.endpoint_origin: managed_city (upstream meaning: gc would own Dolt lifecycle). Bind (gc-site-bind / gc beads city use-external) writes the live city_canonical origin. Those are two real origins, not a “pre-bind” synonym.
  • Portable vs runtime identity: Git tracks issue_prefix: hq, dolt_database: hq, dolt_mode: server, and must not commit project_id. Bind injects live project_id / host / port / site paths.
  • Rig paths: city.toml declares portable names; .gc/site.toml (Oracle projection: deploy/oracle/site-rigs.toml) binds machine-local paths.
  • Packs: sha-pinned in pack.toml + packs.lock. Pack source of record: https://gitlab.com/blueflyio/blu/blucity-packs.
  • blu CLI wraps Bluefly policy/receipts/deploy wiring only. Anything expressible as a pack, order, or formula must not be implemented in blu.

Dated 2026-07-12 receipt (gc 1.3.2, path /opt/bluefly/city, prefix ci, origin managed_city, three rigs) is historical runtime evidence, not the current contract. Do not copy it forward as architecture.

One-City Model

Oracle is the sole Beads/Dolt authority for Bluefly's Gas City deployment. Every other host — workstations, NAS-mounted checkouts, CI runners — is a client of that one City, never a second authority.

  • MAC_CITY_AUTHORITY=NO — no workstation ever holds an authoritative .beads/ store, and no workstation-run gc supervisor is ever treated as the City of record.
  • LOCAL_DOLT_AUTHORITY=NO — a workstation-local Dolt process, if one is running at all, is a disposable cache, never a system of record.
  • Humans and agents connect to the one City as remote clients — the documented remote-city connection flow (gc context add, a --city-url-style flag, or the equivalent current upstream mechanism) — never gc init on a workstation. gc init creates a new, independent City; running it a second time on a second host is definitionally how a duplicate authority gets created, not how an operator joins the existing one. See DUPLICATE_CITY_DOLT_AUTHORITY in the Failure Signature Catalog.
  • Verification: gc context list (or equivalent) should show exactly one remote binding to the Oracle endpoint; bd context --json should resolve dolt_mode against that same endpoint, never a local port.

Rig Provisioning

A Rig is a full working-tree checkout, not a bare or mirror repository. The documented remote-provisioning flow (gc rig add --git-url <url>, or the equivalent current runbook) has the City server perform the clone itself, initialize that Rig's Beads scope, and compose the Rig's packs — the result is an ordinary Git working tree the assigned agent cds into, edits, and commits from directly, the same as any other checkout. Do not model a Rig as a bare repo that needs a separate worktree layer bolted on top; the upstream primitive already is the working tree.

Upstream references for this section: https://docs.gascity.com/getting-started/how-gas-city-works, https://docs.gascity.com/tutorials/01-cities-and-rigs, https://docs.gascity.com/runbooks/remote-hardened-city.

Two Bluefly repositories, never merged

  • blucity (https://gitlab.com/blueflyio/blu/blucity) — the thin City/Rig composition: city.toml, the root pack.toml, and Bluefly's deployment wiring. This is deployment configuration, not reusable capability.
  • blucity-packs (https://gitlab.com/blueflyio/blu/blucity-packs) — the reusable Bluefly-owned Pack catalog: agents, formulas, orders, doctor checks. Packs are imported into blucity by pinned source + version; they are not copied into it.

These are two separate GitLab repositories with two separate histories and two separate release cadences — not a monorepo, and not two views onto the same source. A change to Bluefly-owned capability belongs in blucity-packs; a change to how a deployment is wired belongs in blucity. Every registered Rig likewise pushes to its own GitLab repository — blucity and blucity-packs are never the same working tree as a product Rig such as city (HQ) or bluguide.

One-City Model

Oracle is the sole Beads/Dolt authority for Bluefly's Gas City deployment. Every other host — workstations, NAS-mounted checkouts, CI runners — is a client of that one City, never a second authority.

  • MAC_CITY_AUTHORITY=NO — no workstation ever holds an authoritative .beads/ store, and no workstation-run gc supervisor is ever treated as the City of record.
  • LOCAL_DOLT_AUTHORITY=NO — a workstation-local Dolt process, if one is running at all, is a disposable cache, never a system of record.
  • Humans and agents connect to the one City as remote clients — the documented remote-city connection flow (gc context add, a --city-url-style flag, or the equivalent current upstream mechanism) — never gc init on a workstation. gc init creates a new, independent City; running it a second time on a second host is definitionally how a duplicate authority gets created, not how an operator joins the existing one. See DUPLICATE_CITY_DOLT_AUTHORITY in the Failure Signature Catalog.
  • Verification: gc context list (or equivalent) should show exactly one remote binding to the Oracle endpoint; bd context --json should resolve dolt_mode against that same endpoint, never a local port.

Rig Provisioning

A Rig is a full working-tree checkout, not a bare or mirror repository. The documented remote-provisioning flow (gc rig add --git-url <url>, or the equivalent current runbook) has the City server perform the clone itself, initialize that Rig's Beads scope, and compose the Rig's packs — the result is an ordinary Git working tree the assigned agent cds into, edits, and commits from directly, the same as any other checkout. Do not model a Rig as a bare repo that needs a separate worktree layer bolted on top; the upstream primitive already is the working tree.

Upstream references for this section: https://docs.gascity.com/getting-started/how-gas-city-works, https://docs.gascity.com/tutorials/01-cities-and-rigs, https://docs.gascity.com/runbooks/remote-hardened-city.

Two Bluefly repositories, never merged

  • blucity (https://gitlab.com/blueflyio/blu/blucity) — the thin City/Rig composition: city.toml, the root pack.toml, and Bluefly's deployment wiring. This is deployment configuration, not reusable capability.
  • blucity-packs (https://gitlab.com/blueflyio/blu/blucity-packs) — the reusable Bluefly-owned Pack catalog: agents, formulas, orders, doctor checks. Packs are imported into blucity by pinned source + version; they are not copied into it.

These are two separate GitLab repositories with two separate histories and two separate release cadences — not a monorepo, and not two views onto the same source. A change to Bluefly-owned capability belongs in blucity-packs; a change to how a deployment is wired belongs in blucity. Every registered Rig likewise pushes to its own GitLab repository — blucity and blucity-packs are never the same working tree as a product Rig such as city (HQ) or bluguide.

Bluefly Invariants

  • Never infer identity from filesystem paths; identity comes from configuration (GT_ROLE, agent config, bead metadata).
  • Never hand-edit managed Dolt endpoint files (.beads/dolt-server.port, gc.endpoint_origin); never run bd dolt set port / bd dolt start in an inherited rig. Use gc rig set-endpoint.
  • Never create duplicate work ledgers outside Beads (no TodoWrite, no markdown TODO/progress files, no MEMORY.md).
  • Never recreate Gas City roles in custom code when packs/config own them; prefer an exec order over a new helper agent.
  • Never copy upstream CLI, schema, or specification documentation locally; link to the official pages instead.
  • Always use pinned upstream imports (version = "sha:…" + packs.lock); no registry handles in committed TOML.
  • Runtime findings and execution history belong in Beads; permanent architecture belongs in the Engineering Standard.
  • Index Gas City documentation via the live machine-readable index at https://docs.gascity.com/llms.txt (authoritative as of 2026-08-18).

Approved Deviations

None.

(Unexplained non-upstream config keys are tracked under Evidence Gaps, not approved here.)

Verification

gc version
gc status
gc doctor --verbose
gc config show --validate
gc import status
gc rig list
gc session list
gc beads health
bd context --json

Drupal consumes Gas City events — the measured contract

Measured on Oracle canonical and mac-local, 2026-09-14. Source of record for blu_fleet + recipe_agent_platform.

Four axes. Naming fewer than four gives a contract that returns NULL.

SOURCE    raw .gc/events.jsonl          -> FLAT   payload.*        (payload.bead ABSENT)
          REST /v0/city/{city}/events   -> NESTED payload.bead.*   (payload.id   ABSENT)
          SSE  /v0/city/{city}/events/stream -> NESTED payload.bead.*
CLASS     ordinary work beads   -> bead.updated is the carrier, metadata populated
          order-tracking beads  -> bead.created + bead.closed ONLY, deleted at 7d
IDENTITY  envelope.subject      (payload(.bead).id is the same value by a longer path)
DEDUPE    envelope.seq

The SOURCE split was confirmed against one event read two ways in the same second — seq 745891, subject bl-d5ga9n — which rules out host, event class and bead class simultaneously. Join on envelope.subject and the shape question disappears from the join entirely.

The CLASS axis is the one that fails silently

ALL bead.* events       updated 30,196 | created 4,833 | closed 4,686 | deleted 200
ORDER-TRACKING ONLY     updated      3 | created   726 | closed   712 | deleted 200

bead.updated is 76% of all bead events and 0.18% of order-tracking ones. "Use bead.updated" is correct advice for ordinary work beads and misses ~99.6% of order runs. Order-tracking beads carry no payload.metadata until bead.closed, where close_reason is the outcome, and the bead is deleted at 7d per [beads.policies.order_tracking] — so the Drupal receipt is the durable record; the bead is not. Why order-tracking beads are exempt from bead.updated is unexplained and is gc's to answer; do not design around the exemption.

run_id is a trap

It mirrors envelope.subject — 500/500 sampled on Oracle, confirmed on mac-local (run_id == subject == payload.id). It is typed, populated and conveniently named, and it does not distinguish two runs of one order. It is NULL on order.*. Identity is envelope.subject; dedupe is envelope.seq.

Proven contrib gaps — stop searching, these are closed

Both are architectural, not missing modules. Neither can be closed by finding a module, so re-running these searches is wasted work.

1. Drupal cannot consume SSE. CONTRIB_GAP_PROVEN=YES

Every SSE-capable module in contrib and in the installed estate is a producer — agui, ai, ai_agents_agui all emit StreamedResponse Drupal→browser; php_sse and server_sent_events push server→client. Grep for Last-Event-ID, EventSource, readStream across all three installed modules' src/ returns zero hits.

Why none exists: consuming a long-lived SSE stream requires a persistent process holding an open connection. Drupal under PHP-FPM has no daemon model — the request ends and the connection with it. SSE=YES and POLLING=NO cannot both be satisfied by a Drupal site process.

The supported shape is a bounded cursored read with an opaque server cursor: it cannot miss or double-count and it resumes, but it is not SSE and must be labelled PROOF_CONSUMER, never relabelled. A persistent out-of-request consumer (a long-running Drush command) meets the literal SSE bar at the cost of custom PHP plus a supervised process on every host — an operator decision, not a default.

2. ECA has no generic list iterator. CONTRIB_GAP_PROVEN=YES

The complete ECA 3.1.7 action inventory contains exactly two per-item dispatchers: eca_trigger_custom_event (fires once) and eca_trigger_content_entity_custom_event (content entities only). The eca_list_* family mutates lists without iterating them. A fetched JSON array of N items cannot be fanned out into N operations by configuration.

Consequence: fetch with limit: 1 and let the cron schedule be the loop, which the server-side cursor makes safe. Cost, which must be stated and not hidden: one item per tick. Steady state is fine; a large backlog is not.

http_client_manager — the ECA action does not replace tokens

This single fact decides whether a cursored read is expressible in configuration.

Command.php:141           $params[$id] = $this->configuration[$id];     RAW. No token replacement.
HttpConfigRequest.php:139 $params[$key] = $token->replace($value);      Replaces.
eca.services.yml:104      eca.service.token  decorates: token          (Drupal\eca\Token\CoreToken)

The ECA http_client_manager_command:<service>:<op> action sends '[my_token]' to the API as that literal string. The http_config_request config entity replaces tokens, and because ECA decorates the core token service, ECA runtime tokens resolve inside it. So:

  • dynamic params → http_config_request config entity + http_client_manager_preconfigured_request:<entity_id>
  • the Command action's result keys are received_result_storage and received_result_key (HttpActionResultTrait.php:117-119), defaulting to result_cache. token_name is not a key it accepts — set received_result_storage: eca_token or the response never reaches ECA.

ECA 2.x → 3.x: the Tool event moved modules

2.1.23  Drupal\eca_base\BaseEvents      = CRON, CUSTOM, TOOL, FIELD_WIDGET
        Drupal\eca_base\Event\ToolEvent  exists;  event plugin 'eca_base:eca_tool'
3.1.7   Drupal\eca_base\BaseEvents      = CRON, CUSTOM only — TOOL REMOVED
        capability moved to contrib module `eca_tool`:
          plugin id 'eca_tool'  ·  Drupal\eca_tool\ToolEvents::TOOL
          Drupal\eca_tool\Event\ToolEvent  ·  core ^11.3 || ^12  ·  requires tool:tool

A model built on eca_base:eca_tool still imports on ECA 3.x and never fires. Silent. Use eca_base:eca_custom + eca_trigger_custom_event for Drupal-internal dispatch — present and identical in both majors.

Upstream defect, drupal/orchestration 1.0.0: orchestration/modules/eca/src/ServicesProvider.php:91 does new ToolEvent(...) from Drupal\eca_base\Event\ToolEvent and :93 dispatches Drupal\eca_base\BaseEvents::TOOL. Neither exists on ECA 3.1.7, and there is no class_exists guard — so ServicesProvider::execute() fatals on ECA 3.x. orchestration_eca.info.yml depends on eca:eca_base with no version bound, which is what lets Drupal enable it there; drupal/eca: ^3.0 sits in require-dev, so the module is tested against the major its runtime code cannot support. Goes to the drupal.org issue queue — genuine contrib, no carried patch.

Sequencing, binding: fix orchestration_eca for 3.x before converging any site's ECA to 3.x. Converging first lights both failure modes at once and only one of them announces itself.

Upstream References

Topic Official page
Installation https://docs.gascity.com/getting-started/installation
Quickstart https://docs.gascity.com/getting-started/quickstart
Configuration reference https://docs.gascity.com/reference/config
Pack specification https://docs.gascity.com/reference/specs/pack-spec
Formula specification (v2) https://docs.gascity.com/reference/specs/formula-spec-v2
CLI reference https://docs.gascity.com/reference/cli
Beads topology https://docs.gascity.com/reference/internal/beads-topology
Trust boundaries https://docs.gascity.com/reference/trust-boundaries (reproduced in full, with Bluefly cross-links, at gas-city-command-execution-trust-boundaries.md — the one exception in this document to "link, do not restate," because it is a fixed security contract Bluefly agents author against, not drifting product behavior)
Machine-readable index https://docs.gascity.com/llms.txt
Coming from Gas City / command map https://docs.gascity.com/getting-started/coming-from-gascity · https://docs.gascity.com/reference/gascity-command-map
How Gas City works https://docs.gascity.com/getting-started/how-gas-city-works
Tutorial: Cities and Rigs https://docs.gascity.com/tutorials/01-cities-and-rigs
Runbook: remote hardened City https://docs.gascity.com/runbooks/remote-hardened-city

Evidence Gaps

  • ~~city.toml contains [federation.wasteland] and [routing] tables that do not appear in the upstream configuration reference.~~ RESOLVED 2026-07-30: provenance confirmed (aspirational Wasteland federation concept, Engineering-Standard/reference/upstream-architectural-reference.md §2.5, unsupported by installed gc); the generating source (iac, terraform/oci/cloud-init.tftpl) no longer emits this block, so future Oracle provisions produce a clean city.toml. The currently running Oracle host is unverified against this fix — see 2026-07-30__gas-city__workspace-conformance__audit.md §4.
  • Native beads store eligibility on Oracle is unverified (bd context → dolt_mode); gc status session-snapshot probes time out on this host.
  • Gas City (gt, ~/gt) on Oracle is incomplete cutover, not the target execution runtime. Target runtime is Gas City at /opt/bluefly/blucity plus .gc/ machine state (ADR-0027).
  • blucity/.gitlab-ci.yml has no TOML syntax/schema validation job before its deploy-to-Oracle stage, unlike blucity-packs which already has one (validate:pack-lint, validate:toml, validate:no-runtime-tracked).
  • blucity-packs/kingstown-core/pack.toml and (pre-removal) digital-service-baseline/pack.toml both import a subpath named gascity (sha 3b3b89f2…) distinct from the gascity subpath (sha 33d3a43…) every other pack imports — whether these are genuinely different upstream subpackages or a drifted/typo'd import target is unverified.
  • blucity-packs/kingstown-core/agents/blu/ defines a second, independent "BLU" agent identity (OSSA manifest) alongside the canonical blucity/agents/blu/prompt.template.md copy. Self-labeled "staged for migration" to a canonical agents repo that does not yet exist as a reachable clone.

Operator Decisions

  • Whether to enable off-box JSONL archive push for the city bead databases (upstream: troubleshooting → "JSONL archive push failures").
  • ADR-0008 (location for read-only upstream authorities) remains OPEN.