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 sourceblueflyio/blu/blucity)./opt/bluefly/cityis superseded. - Live Beads endpoint:
city_canonical@127.0.0.1:3308, databasehq, city prefixhq. Rigsinherited_citywith uniquedolt_database+issue_prefix. One Dolt server. No per-rig Dolt. - Tracked Git unbound default:
.beads/config.yamlmay recordgc.endpoint_origin: managed_city(upstream meaning: gc would own Dolt lifecycle). Bind (gc-site-bind/gc beads city use-external) writes the livecity_canonicalorigin. 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 commitproject_id. Bind injects liveproject_id/ host / port / site paths. - Rig paths:
city.tomldeclares 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. bluCLI wraps Bluefly policy/receipts/deploy wiring only. Anything expressible as a pack, order, or formula must not be implemented inblu.
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-rungc supervisoris 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) — nevergc initon a workstation.gc initcreates 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. SeeDUPLICATE_CITY_DOLT_AUTHORITYin the Failure Signature Catalog. - Verification:
gc context list(or equivalent) should show exactly one remote binding to the Oracle endpoint;bd context --jsonshould resolvedolt_modeagainst 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 rootpack.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 intoblucityby pinnedsource+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-rungc supervisoris 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) — nevergc initon a workstation.gc initcreates 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. SeeDUPLICATE_CITY_DOLT_AUTHORITYin the Failure Signature Catalog. - Verification:
gc context list(or equivalent) should show exactly one remote binding to the Oracle endpoint;bd context --jsonshould resolvedolt_modeagainst 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 rootpack.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 intoblucityby pinnedsource+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 runbd dolt set port/bd dolt startin an inherited rig. Usegc 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_requestconfig entity +http_client_manager_preconfigured_request:<entity_id> - the Command action's result keys are
received_result_storageandreceived_result_key(HttpActionResultTrait.php:117-119), defaulting toresult_cache.token_nameis not a key it accepts — setreceived_result_storage: eca_tokenor 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¶
Evidence Gaps¶
- ~~
city.tomlcontains[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 installedgc); the generating source (iac,terraform/oci/cloud-init.tftpl) no longer emits this block, so future Oracle provisions produce a cleancity.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 statussession-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/blucityplus.gc/machine state (ADR-0027). blucity/.gitlab-ci.ymlhas no TOML syntax/schema validation job before its deploy-to-Oracle stage, unlikeblucity-packswhich already has one (validate:pack-lint,validate:toml,validate:no-runtime-tracked).blucity-packs/kingstown-core/pack.tomland (pre-removal)digital-service-baseline/pack.tomlboth import a subpath namedgascity(sha3b3b89f2…) distinct from thegascitysubpath (sha33d3a43…) 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 canonicalblucity/agents/blu/prompt.template.mdcopy. Self-labeled "staged for migration" to a canonicalagentsrepo 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.