Compare commits

..
Author SHA1 Message Date
Libby
0ba536eec1 research: swarm dashboard sources and first cut
Some checks failed
Portable Eve contracts and browser lowering / ubuntu-latest eve-browser-lowering (push) Has been cancelled
Portable Eve contracts and browser lowering / ubuntu-latest eve-contracts (push) Has been cancelled
Portable Eve contracts and browser lowering / windows-latest eve-browser-lowering (push) Has been cancelled
Portable Eve contracts and browser lowering / windows-latest eve-contracts (push) Has been cancelled
Thing 2 contract state, Run/heartbeat/usage-readout sources, and the
smallest first cut for "is anyone working, and on what".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 06:17:41 +00:00
Meta Vens
e119e76301 Merge readme-front-door: README front door
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:59:26 +02:00
Meta Vens
afe05b0c7e README front door: entry points and live/planned up front; drop absent relay command and config, repo-relative paths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:55:21 +02:00
6 changed files with 256 additions and 159 deletions

73
Cargo.lock generated
View file

@ -17,15 +17,6 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "ar_archive_writer"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6"
dependencies = [
"object",
]
[[package]]
name = "autocfg"
version = "1.5.1"
@ -76,8 +67,8 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cultcache-rs"
version = "0.3.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=bbb98eaf660e43ef611dc2f5132df99fe21e2f01#bbb98eaf660e43ef611dc2f5132df99fe21e2f01"
version = "0.2.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7#b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7"
dependencies = [
"anyhow",
"chrono",
@ -88,16 +79,14 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"serde_stacker",
"stacker",
"uuid",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
name = "cultcache-rs-derive"
version = "0.2.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=bbb98eaf660e43ef611dc2f5132df99fe21e2f01#bbb98eaf660e43ef611dc2f5132df99fe21e2f01"
version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7#b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7"
dependencies = [
"proc-macro2",
"quote",
@ -223,15 +212,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@ -253,16 +233,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622"
dependencies = [
"ar_archive_writer",
"cc",
]
[[package]]
name = "quote"
version = "1.0.47"
@ -365,17 +335,6 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_stacker"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a"
dependencies = [
"serde",
"serde_core",
"stacker",
]
[[package]]
name = "shlex"
version = "2.0.1"
@ -388,19 +347,6 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "stacker"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.61.2",
]
[[package]]
name = "syn"
version = "2.0.119"
@ -586,15 +532,6 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"

View file

@ -4,6 +4,6 @@ resolver = "2"
[workspace.dependencies]
anyhow = "1"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "bbb98eaf660e43ef611dc2f5132df99fe21e2f01", package = "cultcache-rs" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7", package = "cultcache-rs" }
serde = { version = "1", features = ["derive"] }
rmp-serde = "1.3"

View file

@ -1,9 +1,41 @@
# Thing
This repository is Thing. Its present tree is Thing 1, kept running for live
consumers until each ports; Thing 2.0 is built here (`docs/thing2-target.md`).
The Thing 1 renderer repos are archived under their Eve names: EveUnity,
EveFlutter, EveElectron, EveTui, EvePlugins and EveConformance.
This repository is Thing (the Eve display, control and sensor edge for GameCult
apps). Its present tree is Thing 1, kept running for live consumers until each
ports; Thing 2.0 is built here (`docs/thing2-target.md`). The Thing 1 renderer
repos are archived under their Eve names: EveUnity, EveFlutter, EveElectron,
EveTui, EvePlugins and EveConformance.
## Start Here
Value: services publish a surface once, as a typed retained document
(`gamecult.eve.surface.v1`), and every runtime lowers it locally. Providers keep
authority over state and commands; Eve owns projection, input capture and
timestamped sensor publication.
Problem: without a shared surface contract, each service hand-builds its own
dashboard and each renderer re-implements layout, binding and commands.
Live (Thing 1):
- `packages/org.gamecult.eve.surface`: renderer-neutral C# surface contract and
composition builders; tests in `tests/GameCult.Eve.Surface.Serialization.Tests`.
- `packages/eve-contracts`: TypeScript types and validators generated from
`schemas/` (`npm run generate`, `npm test`).
- `packages/eve-browser-lowering`: browser DOM/CSS lowering of surface documents
(`npm test`).
- `web/`: browser reference oracle (`web/index.html`, `web/eve-dsl.js`; tests
with `node --test web/eve-dsl.test.mjs`).
- `docs/surface-contract-v1.md`: the contract. `docs/repo-strategy.md`: repo
ownership. `docs/parity-testing-harness.md` with
`scripts/run-parity-harness.ps1`: parity runs.
- `android/` and `Sources/` (iOS Theos app, `make package`): native proof
clients; build steps under Build Shape below.
Planned (Thing 2.0, in progress): `crates/thing-contract` is the first Rust
piece. The layout solver, text, accessibility tree, closed render-command list
and thin backends described in `docs/thing2-target.md` and `docs/thing2-map.md`
are not built yet; `docs/thing-campaign.md` holds the rename plan.
CultMesh-based surface web, portable control layer, and timestamped sensor
edge.
@ -214,11 +246,8 @@ render:
## VoidBot CEF Stream
Start the Starfire relay from `E:\Projects\VoidBot`:
```powershell
npm run swarm:eve-cef-relay -- --width 1620 --height 2160 --scale 2 --port 8792
```
The CEF relay that produced these frames is not in VoidBot's default branch, so
no relay command is documented here until one exists.
EveCanvas displays binary JPEG frames and returns touch events only when a
stream lowering is configured through `EVE_STREAM_URLS`. The native app no
@ -226,12 +255,11 @@ longer guesses a Starfire relay address.
## Mimir Sensor Uplink
Start Mimir with `config/mimir-runtime.raven-eve.example.json` or run the
receiver processes directly:
Run the receiver processes from a Mimir checkout (a sibling `..\Mimir` here):
```powershell
dotnet run --project E:\Projects\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8793 --path /eve/camera --source-id eve-camera --type video-frame
dotnet run --project E:\Projects\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8794 --path /eve/mic --source-id eve-mic --type audio-block
dotnet run --project ..\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8793 --path /eve/camera --source-id eve-camera --type video-frame
dotnet run --project ..\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8794 --path /eve/mic --source-id eve-mic --type audio-block
```
EveCanvas sends binary `mimir.eve_media_observation.v1` camera and microphone
@ -245,7 +273,7 @@ guess receiver URLs.
Start the dashboard authority on Starfire:
```powershell
dotnet run --project E:\Projects\Mimir\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --port 8795
dotnet run --project ..\Mimir\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --port 8795
```
EveCanvas connects to the Mimir dashboard WebSocket lowering for native retained

View file

@ -1,76 +0,0 @@
//! A surface payload inside CultCache's 256 nesting bound must decode on a small thread without
//! aborting the process. Soul's finding `thing2:finding:cut-contract-rs.s1.decode-stack-overflow-
//! within-bound` found a 2.8 KB Repeat chain aborting a 1 MiB thread in release at 68 levels and a
//! Box chain aborting a 2 MiB debug thread at 24 levels, while the bound admits 125 and 83. A
//! stack overflow kills the whole test binary, so these tests fail loudly on a decoder that
//! does not grow its stack.
mod common;
use common::*;
use cultcache_rs::decode_payload;
use thing_contract::*;
const REPEAT_LEVELS: usize = 125;
const BOX_LEVELS: usize = 83;
fn surface(root: Node) -> SurfaceDocument {
SurfaceDocument {
provider_id: ProviderId::new("provider.a"),
label: seg("deep"),
version: ver(1),
title: "deep".into(),
root,
commands: vec![],
}
}
fn repeat_chain(levels: usize) -> SurfaceDocument {
let mut chain = text("leaf", "bottom");
for depth in (0..levels).rev() {
chain = Node::Repeat(RepeatNode {
common: common(&format!("rep-{depth}")),
collection: CollectionSource::Param(param("rows")),
key_member: path(&["id"]),
template: std::boxed::Box::new(chain),
});
}
surface(chain)
}
fn box_chain(levels: usize) -> SurfaceDocument {
let mut chain = text("leaf", "bottom");
for depth in (0..levels).rev() {
chain = boxed(&format!("box-{depth}"), vec![chain]);
}
surface(chain)
}
/// Builds, encodes and drops the value on a roomy thread, so only the decode runs on `stack`.
fn decode_on_small_thread(build: fn() -> SurfaceDocument, stack: usize) {
let expected = std::thread::Builder::new().stack_size(64 << 20).spawn(build).unwrap().join().unwrap();
let payload = std::thread::Builder::new()
.stack_size(64 << 20)
.spawn(move || (rmp_serde::to_vec(&expected).unwrap(), expected))
.unwrap()
.join()
.unwrap();
let (bytes, expected) = payload;
let decoded = std::thread::Builder::new()
.stack_size(stack)
.spawn(move || decode_payload::<SurfaceDocument>(&bytes).unwrap())
.unwrap()
.join()
.unwrap();
std::thread::Builder::new().stack_size(64 << 20).spawn(move || assert!(decoded == expected)).unwrap().join().unwrap();
}
#[test]
fn repeat_chain_at_the_bound_decodes_on_a_1_mib_thread() {
decode_on_small_thread(|| repeat_chain(REPEAT_LEVELS), 1 << 20);
}
#[test]
fn box_chain_at_the_bound_decodes_on_a_2_mib_thread() {
decode_on_small_thread(|| box_chain(BOX_LEVELS), 2 << 20);
}

View file

@ -665,6 +665,13 @@ fn refusal_cases() {
#[test]
fn deep_tree() {
// Decoding a Node costs tens of KiB of debug stack per nesting level, so the chain runs on a
// thread that has the room; the 2 MiB default does not. The hazard is CultCache's, owned by
// cultlib-gaps:follow_up:decode-stack-safety; the test stays on the big thread until it lands.
std::thread::Builder::new().stack_size(16 * 1024 * 1024).spawn(deep_tree_body).unwrap().join().unwrap();
}
fn deep_tree_body() {
// A 64-deep Box chain.
let mut chain = text("leaf", "bottom");
for depth in (0..64).rev() {

View file

@ -0,0 +1,201 @@
# Swarm dashboard: contract state, sources, first cut
Serves Eureka's ruling `swarm-dashboard-first-thing2-surface` (not readable from
the libby mind; cited by the id in the claiming follow-up
`libby-body:follow_up:chat-s354-359`). Read-only pass by the `libby` Self run
`libby:run:mind-20261031T061423908Z`, 2026-10-31. Nothing was built or deployed.
Claims below were probed on 2026-10-31 unless marked **unverified**.
The question the dashboard answers for a stranger: **is anyone working, and on what.**
## 1. Where Thing 2's surface contract stands
Probed against the forge `GameCult/Thing` (main `e119e76`, 2026-10-22) and its
`hands/thing2-*` branches. The source of truth for cut status is the `thing2`
campaign in the Eureka mind, which this pass cannot read; the prose map
(`docs/thing2-map.md`, last edited 2026-10-12) is the evidence used.
| Piece | State |
| --- | --- |
| Contract, Rust (`crates/thing-contract`) | On main. 13-arm closed `Node` union, `SurfaceDocument`, `CompositionDocument`, `CommandDecl/Invocation/Result`, `AssetCatalog`; byte goldens under `tests/goldens/`. |
| Contract, C# and TypeScript | Not on main. Branches `hands/thing2-contract-cs` (`3dadfc6`, 2026-10-14) and `hands/thing2-contract-ts` (`aeede04`, 2026-10-12). The map lists the cross-runtime store check and publication as waiting. |
| Core (resolution, `thing.std` compositions, binding via the CultMesh port, Taffy layout, command dispatch) | Not built. No crate exists. Decoding Verse surfaces waits on `cultlib-gaps:follow_up:decode-stack-safety`. |
| TUI backend | Not built. Planned first backend. |
| Web backend and shell | Not built. |
| Discovery/advertisement | Not a Thing kind (`thing2:ruling:advertisement-owner`); read through a core port; today's `gamecult.eve.provider_advertisement.v1` is unchanged. |
`README.md` says the same: the layout solver, text, accessibility tree, render
commands and backends "are not built yet". **Nothing can render a Thing 2
surface today.** The only live renderers are Thing 1's (browser oracle, Ghostlight's vendored lowering,
the Nightwing browser reference), and Eve dashboards are quarantined (`gamecult.md`: no new Eve surfaces).
### What a provider must publish to render
From `docs/thing2-map.md` "The consumer contract" and `crates/thing-contract/src`:
1. A `SurfaceDocument` (`gamecult.thing.surface.v1`, key `thing:surface:<providerId>.<label>`,
`version` bumped only on structure change). Closed nodes: `Box, Text, Image, Button, Toggle,
Slider, TextInput, Select, Repeat, Overlay, Plot, Embed, Use`. A swarm board is `Box` + `Text` +
`Repeat` (one row per instance) + `Use` of `thing.std` compositions (`card`, `metric`, `key-value`).
2. The **bound state records** the document points at, under the provider's own schema and key. A
`Bind` is `{provider?, schema, key, member path, format?}` plus `Item` inside a `Repeat`. A
collection binds to `{provider, schema, key_prefix}` or a list member of one record. No
expressions, no derived values: **the state's owner publishes derived values** (map rationale,
P5). So the dashboard's "working / idle" must be a published field, not computed in the surface.
3. Discovery: the provider is found through the existing advertisement, not a Thing record.
4. Commands only if the surface acts. A read-only board declares none.
A provider needs no layout or renderer code. It needs the contract package in its language
(Rust is the only one on main) and typed CultMesh state.
## 2. Sources (all typed state)
### 2a. Run documents, eight minds
Each mind is its own Huginn daemon (`HEARTBEAT_PERSONAS` in `/etc/gamecult/heartbeat/heartbeat.env`):
eureka 17872, libby 17873, heimdall 17874, kiko 17875, aqua 17876, weksa 17877, nibu 17878,
void 17879, all `rudp://10.77.0.1:<port>`. Probed: from this Self run,
`EUREKA_INSTANCE=<name> HUGINN_ENDPOINT=<endpoint> huginn query '{"schemas":["epiphany.pipeline.run.v2"],"limit":200}'`
answers on all eight, read-only, with caller `client unknown` (no per-client read identity yet).
Counts that day: eureka 68, libby 78, heimdall 94, kiko 70, aqua 81, weksa 74, nibu 71, void 87.
`epiphany.pipeline.run.v2` (`huginn schema run`): `instance, label, operated_by (Operator|Mind),
turn (PersonaTurn|SelfRun), host, started_on (a date), budget_usd, claims [PipelineRef], campaigns`.
What the dashboard can and cannot take from it:
- **Open/closed is not on the Run.** It is derived: a Run is closed when a `Resolution` with
`subject` = the run is in force (`huginn query` prints `InForce` or `Resolved:<outcome>`).
Resolution key `<instance>:resolution:run.<label>.n<seq>`. `huginn history <runId>` reads it.
- **Start time is a date.** The label `mind-YYYYMMDDTHHMMSSmmmZ` encodes the start to the
millisecond, but that is a convention in a string. The admission header (`huginn view`) carries
`admitted <timestamp>`, and the Resolution's header gives the end. Both are header facts, not
document fields. A dashboard needing start and end times wants them as typed fields.
- **"Outcome" is nearly empty.** Of 78 libby runs, 77 resolved `Recorded` and one is in force.
The meaning lives in free text: `reason "answered 1"` (55), `"answered 2"` (5), `"the connector
refused the credential lease: UnknownCaller"` (3), `"unit not active"`, `"failed after 0
answered..."`, plus rationales `The Persona unit's start ended.` (61) and `The Self run's carrier
closed the run its claude left open.` (11). Two Self-run resolutions carry the run's whole result
message inside `reason` (long prose, operator-facing detail). A public board
must **never project `reason` or `rationale`**.
- **"On what" is only on Self runs.** `claims` holds the follow-up or other document the run took;
a `PersonaTurn` has no claims (its work is the chat room). The claimed `follow_up.item` is the
operator's chat-relayed brief, free text, sometimes private. The typed handle (claim id, campaign
slug, the claimed document's `repo`) is safe; the `item` is not.
- **An open Run can be a dead Run.** Eleven libby Self runs were closed with "carrier closed the
run its claude left open", so for some interval an in-force Run belonged to nobody. "Open" is
"admitted and not yet resolved", not "a process is working".
- **Dollar fields:** `budget_usd` is on every Run. Do not bind it.
Snapshot at 06:16 UTC: two Self runs in force, `libby:run:mind-20261031T061423908Z` (this one) and
`void:run:mind-20261031T061543899Z` (claims `void-body:follow_up:chat-s335-340`); no Persona run
in force in any of the eight.
### 2b. Heartbeat state and poll trace
Source: `Huginn/crates/mind-body/src/heartbeat.rs`. `mind-heartbeat` is one Idunn-managed unit, sole
writer of `heartbeat.cc` (a single-file CultCache store under the unit's state directory). Two kinds:
- `eureka.heartbeat.initiative.v1`, key = instance slug: `initiative_usd, as_of, urgency, urgency_at,
last_burned_run, filled_events, classified_seq, launched_run, launch_spend_usd,
launch_spend_known, completions_through, launch_window`.
- `eureka.heartbeat.poll_trace.v1`, key = zero-padded unix ms, written only for polls that changed
something: `at, instances [InstanceTrace], launches [run label], burns [BurnTrace], classifier`.
`InstanceTrace`: `instance, initiative_usd, rate_usd_per_h, urgency, urgency_age_s, busy,
released, crossing_at, waited_s, idle_s, filled_by, fill_reason, note`.
Usable without dollars: `busy` (an in-force Persona run, from the mind), `released`, `crossing_at`,
`waited_s`, `idle_s`, `filled_by`/`fill_reason` (`tag` or `name`), `note`, `launches`, `urgency`,
`classifier`. These say **why nobody is working** (waiting for initiative, held by the envelope).
Dollar fields to leave alone: `initiative_usd`, `rate_usd_per_h`, `launch_spend_usd`,
`spend_usd`, and in the Initiative record the same names. The metering teardown removes them, and
`initiative_usd` is the unit initiative itself is denominated in, so the heartbeat's own model will
change under whatever binds this.
Gaps:
- **Not verified live.** `heartbeat.cc` is not readable from this Self run's account, and no
`/var/lib/gamecult` path for it was visible. The shape above is from source and
`eureka-body-map.md:534-540`, not from a read of the file.
- **No wire.** The heartbeat writes a file; it does not serve one. A reader opening a single-file
store while the heartbeat rewrites it has no stated safety (`FileStore::open` pulls the whole file
per call). A dashboard source must be published state, not a second process reading the file.
`mind-heartbeat timeline` prints the latest poll for a human.
- The trace is written only when a poll "changed something", so its newest line can be old while
the system is healthy. Staleness must come from the heartbeat's own published liveness, which
does not exist as a field.
### 2c. Connector usage readout
Source: `ModelConnector/schemas/usage_readout.json`, `crates/model-connector/src/readout.rs`.
`gamecult.model.usage_readout.v1`: `{schema_id, at_unix_ms, plan_quota: [{window, status,
utilization_permille (nullable), resets_at_unix_ms, observed_at_unix_ms}]}`. Whole-plan windows, no
per-caller field, **no dollars**: the safe source for "how much capacity is left".
Gaps: it is a connector operation (`usage_readout` on `gamecult.model.connector`), reached at the
heartbeat's `MIND_CONNECTOR_ENDPOINT` (`tcp://127.0.0.1:4110`), a loopback port with a per-caller
credential. Three libby Self runs were already refused with `UnknownCaller` on a lease, so a new
caller needs registration before it can ask. It is a request/answer, not a document in a store. A
dashboard needs a publisher that polls it and republishes it as a CultMesh record. `plan_quota`
also rides on every lease grant. **Unverified:** the live values, since asking requires a caller
credential this pass was not given.
### 2d. Not a source: the presence provider
CultLib's 10-29 presence provider attests Idunn-managed **service** liveness, not attention or work
(`libby-body:follow_up:presence-attention-not-carried`). It can answer "is the heartbeat unit up".
It cannot answer "is anyone working".
## 3. Smallest first cut
**Cut 1: one published read model, `swarm.work.v1`, from the Runs of the eight minds. No heartbeat,
no usage, no renderer.**
One record per instance (eight, key `<instance>`), derived and published by a single projector
(owner: the swarm-dashboard provider), holding only closed-set or id fields:
- `state`: `Working` (a Self run in force), `Turn` (a Persona run in force), or `Idle`.
- `run`: label of the newest in-force run, its `turn`, its start (from the admission header), else the
newest closed run's end.
- `on`: the claim id(s) and campaign slug(s), no free text.
- `observed_at` and the per-mind `as_of`, so a dead projector reads as stale.
This answers the stranger's question with typed state only. It deliberately omits `reason`,
`rationale`, `item`, `budget_usd`, and every `*_usd` field.
The Thing 2 surface for it is a `SurfaceDocument` that is a `Repeat` over those eight records, one
`thing.std.card` each, bound to `state` / `on` / the time fields. It can be authored and golden-tested
with the Rust contract on main. It cannot be seen until core (2a) and a backend exist. That is the
actual gating item, and it is not this cut's to work around with a Thing 1 or Eve surface.
Cut 2: add heartbeat liveness and the why-idle fields (`busy`, `released`, `waited_s`, `idle_s`,
`note`, `launches`), once the heartbeat publishes them as typed CultMesh state (it currently writes a
file only, and the dollar-denominated initiative model is under teardown).
Cut 3: add the `plan_quota` windows once a publisher polls the connector under a registered caller.
### Forks for the operator (recommendations given)
1. **Who is the stranger?** If public, the board shows ids and campaign slugs only (cut 1 as
written); "on what" in words needs a typed `public_label` that the claiming agent sets, not
`follow_up.item`. Recommend ids only until that field exists.
2. **Where does the projector live?** Recommend a small reader that queries the eight Huginns (all
answer today) and publishes `swarm.work.v1`, rather than adding publication to `mind-heartbeat`,
which is Idunn-managed with a single-file store and a dollar model in flux. Confidence moderate:
the cost is a new daemon, which needs the independent-lifecycle account `gamecult.md` requires.
3. **Is "open" enough?** Recommend marking `Working` only when a Run is in force **and** its
carrier is attested (the Run was admitted by a launch that is still live); the data for that
attestation is not in the Run. Until it is, say in the surface that `Working` means "admitted,
unresolved".
4. **Typed times.** Recommend asking Huginn for `started_at` and `ended_at` as Run/Resolution
document fields, so no consumer parses labels or header facts.
5. **Sequencing.** A stranger sees nothing until core and a backend land. If the swarm board should
come earlier than the TUI backend, say so; the TUI is the planned first backend and gives agents
a reader, but a stranger needs the web backend (map cut 4).
### Substrate gaps this pass felt
- The `swarm-dashboard-first-thing2-surface` ruling and the `thing2` campaign are in Eureka's mind,
not readable from libby's: cut status here comes from a 2026-10-12 prose map.
- `heartbeat.cc` unreadable from a Self run, and a file, not a served document.
- No typed `ended_at` / `started_at` on Run; outcome is free text in `reason`.
- No per-client read identity on the eight Huginns (`client unknown`).