Invert the stack: data, compositor, bundle, then Go. Drop KDL, move composition out of the engine, mount the roster #329
Labels
No labels
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/devrel
role/eval
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/sysadmin
role/tpm
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agent-compose#329
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Outcome
Reshape agent-compose from
{go CLI} => {data} => {python evals}into{data} => {python evals} => {go CLI}. Composition leaves the Go engine. Go is handed a fully composed bundle and delivers it. KDL leaves agent-compose. The roster becomes mounted YAML rather than embedded KDL, so a config change never needs a binary rebuild.Design record. Implementation slices not yet cut. Session of origin: 2026-08-25, TPM seat, with Kai. Read at revision
c03e85a.This body was rewritten after the design converged. The decision history, including two corrections to my own wrong flow, is preserved in the comments below and is worth reading for the reasoning rather than just the outcome.
What the current shape is
Stronger than "the CLI comes first". The data lives inside the CLI.
internal/person/data/holds all 22 entities (7 roles, 10 personalities, 4 boundaries, invariant) as*.kdlplusSKILL.md, bound in by//go:embed person.kdl dataatinternal/person/person.go:27.internal/is a Go visibility keyword. Nothing outside the module may import that path. The roster is sealed to the binary by the language rather than by convention.scripts/eval-matrix.shandscripts/eval-prompts.shboth open withgo run ./cmd/agent-compose, render into amktemp -d, readperson.json, and delete the directory. The roster has no durable existence outside a temp dir.docs/evaluation.mdcodifies the direction as policy: "Python consumes what Go emits and never restates it."The settled flow
Go never sees an entity. It never merges a role with its meld, never resolves a boundary owner, never derives a favorite color. It mounts a finished bundle and materializes, verifies, projects, and launches it.
Decisions
1. Drop KDL from agent-compose. Umbra's product claim is a dense, UX-focused config language. Every acompose surface that also speaks KDL dilutes that claim and muddies the demo. Only two Go files import
github.com/calico32/kdl-go, so the parser surface is narrow and the dependency drops.gopkg.in/yaml.v3is already ingo.mod.2. No committed JSON as a seam artifact. JSON is for purely machine-readable payloads. Anything a human would read is YAML.
3. Composition moves out of Go entirely. Go is handed a composed bundle. Semantic validation, meld resolution, boundary allocation, copy-contract and role-skill frontmatter checks, merge and conflict detection, the role-by-tier matrix, and the OKLab centroid that derives each role's favorite color all leave
internal/person/person.go, which largely dissolves.4. Structural verification stays in Go.
agent-compose verifyalready treats the tree as opaque and rejects symlinks, escaping paths, missing entry points, unknown delivery modes, and traces disagreeing with identity trees. That is a program refusing to write a malformed tree to disk, not an engine judging whether a roster is meaningful. Keeping it does not reinstate the veto that decision 3 removes.5. The roster is mounted, never embedded. Driving requirement, from Kai: "python devs should have a way to reliably mount acompose configs without needing to rebuild the binary." A roster the binary carries needs a rebuild to change, so
go:embedstops being the path to it.roster:corebecomes an ordinary mounted package resolved through the existingperson_sourcemechanism. The core-versus-external distinction collapses, and there is one contract rather than two.6. Kai's roster lives in
agentic-os-kai. Follows the rule already indocs/person-packages.md: a custom package and its machine rollout "belong in their own repository or host configuration. They do not belong in the public agent-compose engine." Once core stops being privileged, Kai's roster is a custom package and that rule already applies to it. This supersedes the earlier decision to put it at top-leveldata/here, which was made while the binary still embedded it.7. The compositor is
evalkit, grown up and published to PyPI. evalkit already reads the roster and derives the board, so composition is the adjacent step. Keeping compose and eval in one package is what makes the graded artifact and the shipped artifact the same artifact. Needs a name, a public API, and a release train.8. The eval gate is recorded and visible, non-blocking for now, with an explicit expectation of becoming blocking later. Build the coverage check and the switch from day one. Default it to report.
The three artifacts
A host that only launches needs the bundle, not the compositor. Only an author needs both, and an author writing configs for a Python tool already has Python.
Design requirements this creates
Scope
Sequenced. The last one carries a fleet rollout.
agentic-os-kai. agent-compose retains a public seed roster only.internal/person, dropkdl-go, route core through the ordinary mount path, add source-digest staleness refusal, remove silent fallback. Note thatrequest.kdland its fixtures are deleted rather than converted, because a Go binary that does not compose has no compose request..agents/roles.kdlprovider selector. Cross-repo. Discovery stats that exact filename atinternal/nativelaunch/launch.go:366and native role launch refuses without it. Two consumer repos on this host (coilyco-flight-deck/agentic-os,coilyco-bridge/agentic-os-kai) plus an ansible rollout and a both-filenames compatibility window, because a host can run a released acompose older than the repos it reads.Complete when
.kdlfile remains in the repo and no Go file importskdl-go.internal/personcarries no merge, validation, meld-resolution, or color-derivation logic.acomposemounts a roster from a path, refuses a stale bundle by digest, and never silently falls back to the seed.scripts/eval-matrix.shderives the board without invoking the Go binary.docs/evaluation.md,docs/person-contract.md,docs/person-packages.md,docs/ownership.md,docs/bundle-protocol.md, anddocs/kdl-contracts.mddescribe the new direction.docs/FEATURES.mdreflects the change, since the config language, the roster's location, and where composition happens are all public boundaries.Boundaries
Related
Sizing correction on scope C, measured rather than assumed.
The body says skill-provider repositories "across the fleet" carry
.agents/roles.kdl. Counted on this host, there are exactly two:coilyco-flight-deck/agentic-os/.agents/roles.kdlcoilyco-bridge/agentic-os-kai/.agents/roles.kdlSo scope C is two consumer repos plus the ansible rollout and the compatibility window, not a broad fan-out. That moves it from "expensive, defer indefinitely" to "tractable, sequence it last". The both-filenames compatibility window is still required, because a host can run a released acompose older than the repos it reads.
Correction: the seam section in the body has the flow wrong
The body says "Go's loader remains the validator and runs in CI, so Python only ever reads data Go has already blessed." That is wrong and it should not stand as the design.
It keeps Go as the gatekeeper and only relocates the gate to CI. The inversion is not real while Go still holds the veto over what counts as a valid roster. Kai's correction, verbatim: Go should be handed the data in a fully "composed" state.
The corrected flow
Go never sees an entity. It never merges a role with its meld, never resolves a boundary owner, never derives a favorite color. It is handed a finished bundle and its job is to materialize, verify, project, and launch it.
What this splits, and why the split is not a veto in disguise
internal/person/person.go, which largely dissolves. So do the OKLab centroid derivation for favorite colors, the meld resolution, and the role-by-tier matrix, because every one of those is a composition step.agent-compose verify <bundle-dir>already exists and already treats the tree as opaque: it rejects symlinks and special files, paths escaping the root, missing or unsafe entry points, unknown delivery modes, invalid traces, and identity trees that disagree with the trace. That is not gatekeeping the roster's meaning. That is a program refusing to write a malformed tree onto a filesystem and hand it to a harness. It stays, and it does not reinstate the veto.docs/bundle-protocol.mdalready specifies the bundle as an immutable tree entered throughmanifest.jsonand otherwise opaque to consumers. The seam this design needs is therefore already written down. What changes is which side of it the composition happens on.The product consequence, which is favourable
Go stops being a personality engine with a config language and becomes a delivery and launch tool. That is a sharper pitch and a better demo, which was the original reason for dropping KDL.
What this opens, unresolved
These two supersede the "Open, to settle during implementation" list in the body, which was written under the wrong flow.
Requirement that settles the compositor question
From Kai, verbatim: "python devs should have a way to reliably mount acompose configs without needing to rebuild the binary."
This is a product requirement rather than a scheduling preference, and it decides the open item in the previous comment by rejecting its framing. Release-time composition with the roster embedded in the binary fails it directly, because a roster edit would then ride the Go release train.
What it forces
go:embedstops being the path to the roster. A roster the binary carries is a roster that needs a rebuild to change. The defaultroster:coretherefore has to become an ordinary mounted package, resolved from a path like any other. That is the same conclusion the first read of this repo reached from the other direction:internal/person/data_layout.goalready reads core through anfs.FSin the identical layout external packages use, so core has always been an ordinary package wearing a privileged coat.The mounting mechanism also already exists.
person_sourceandperson_policyin host config resolve an external package today. What is missing is that core does not go through it.The resulting shape
The compositor being Python is a feature for the named audience rather than a cost. A Python dev writing roster configs already has Python, so the install is
pip-shaped and the iteration loop stays in one language. The distribution regression I raised in the previous comment only bites if Python is forced onto every acompose host, and mounting removes that: a host that only launches needs the bundle, not the compositor.External packages: same answer
Kai's response on the external-package question was "see prior", so there is no separate contract. Core and external mount identically. The distinction between them collapses, which is the correct outcome and removes the third option in the previous comment (Go retaining a compose path for one case) entirely.
Consequences to design for
docs/bundle-protocol.md, so the bundle should carry a digest of its source data and acompose should refuse a bundle whose source has moved on, naming the compositor command. Refusing beats silently mounting yesterday's roster.docs/person-packages.mdalready records that refresh-then-exec refuses last-known-good fallback because that projection may use the default.Reopened by this
Turn one settled the roster at top-level
data/in this repo, but that was decided while the binary still embedded it. Once the roster is mounted rather than embedded, whether it belongs in the agent-compose repo at all is an open question again. Tracked in the next comment.Invert the stack: data first, evals second, Go CLI last. Drop KDL, move the roster to top-level data/to Invert the stack: data, compositor, bundle, then Go. Drop KDL, move composition out of the engine, mount the rosterA consumer is already blocked on decision 5, and this issue does not record it. Angie (platform seat). Evidence for scope sequencing, not a request to reorder anything.
The dependency
coilyco-gaming/sirens-echo#1147is building a person package that owns three deployed lane roles (engineer,creator,ops). Its PR #1151 is complete and green, and Kai has paused it to wait for this issue rather than land what it currently does.What it currently does is the workaround decision 5 removes:
In core,
tpmowns that boundary. The person package re-owns it tocreatorfor one reason: a person-package role cannot defer a boundary whose owner lives in the core roster. There are two contracts, so the owner has to be in the same package as the deferrer.Decision 5 is exactly the fix:
With one contract,
opsdefersseek-external-validationagainst its real owner and nothing is re-owned.Why it is worth having on this issue
The boundary skills ship the same body on both sides so that "a task does not become permitted by arriving through a different charter". Re-owning a boundary to fit a packaging limit is that invariant bending to the tooling. It is the kind of thing that gets normalised once it ships and is hard to see later, which is why holding it was the right call and why the unblock should be a named consequence of scope C rather than a coincidence.
What is queued behind it
Four PRs in
sirens-echofailrole-drift-checktoday: #1148, #1149, #1150, and #1152. #1152 carries five children ofcoilysiren/inbox#426, all green on their own content.Not an argument to hurry. It is the cost of the pause, and it should be visible on the issue the pause is waiting for rather than only on the PRs doing the waiting.
One observation from the consumer side
sirens-echore-derives the core roster by cloning this repository at CI time rather than reading a pin, which is how a rename here turned every branch there red at once. Decision 5's mounted package plus the staleness refusal in "design requirements this creates" would give that consumer a version to pin against. Worth checking that scope C's digest refusal reaches a consumer mounting core, not only one mounting its own package.End condition: a new repo, paired the way umbra pairs with mcp-beaver
From Kai: "a new repo for the python package. I'll pair with acompose the way umbra pairs with beaver."
This supersedes decision 7 in the body. The compositor is not evalkit grown up inside agent-compose and published from here. It is its own repository with its own release train.
It also converts the program's definition of done from a checklist into a structural end state: two repos, correctly paired, with acompose generic.
The pairing, mapped
The reference pairing, from the agent-systems stack:
Mapped onto this program:
The mapping is load-bearing rather than decorative, because "one runtime, many guardfiles" is the same sentence as decision 5's mount requirement, arrived at from the architecture side instead of the product side. The pattern already encodes what Kai asked for.
What the pairing settles without further argument
specgen, which builds standalone guarded CLIs and native agent skills from that policy. specgen is not a separate repo." The generator lives with the engine that owns the language. Compose, run, and the board therefore stay together, which is also what keeps the graded artifact and the shipped artifact the same artifact.aos-evalgrading stays in agentic-os as it is today.agentic-os-kaiper decision 6, unchanged.Tension this creates, and the resolution
"One runtime, many bundles" is in direct tension with the seed proposal in the body, which had acompose embedding a core roster to materialize on first run. A generic runtime that carries one privileged roster is not generic, and it reintroduces the rebuild-to-change-config problem in a smaller form.
Resolution: acompose embeds no roster at all. The seed ships as a separate release asset, installed alongside the binary to the mount location by brew and scoop. The binary stays generic, zero-config still works on a fresh install, and the seed is upgradable and replaceable without touching the binary. This replaces the "binary carries core as a seed" bullet in the body's design requirements.
Open before the repo can exist
Slice index
Decomposed and filed. Granularity and filing location approved by Kai. All children live here under this parent, and the compositor-side ones migrate to the new repository once #337 creates it.
.agents/roles.kdlacross the fleet. HITL. Blocked by #339.Plus #330, the naming brief handed to the Developer Advocate seat, which blocks #337.
Two lanes after the tracer
#333 through #335 and #338 through #340 are the data and compositor lane. #334, #336, and #339 are the acompose lane. They rejoin at #339. Both wait on #332, so the tracer is the only true bottleneck in the program.
Where the program can still die
#331 is a spike rather than a feature slice, and it is first on purpose. Every other slice assumes the bundle protocol is genuinely producer-agnostic.
docs/bundle-protocol.mdstates that intent, but intent is not evidence about whatverifyandprojectrequire. If a bundle turns out to be producible only by the Go compositor that #339 deletes, the program's shape changes and this is where that is cheap to learn.Not labeled
AWS credentials were stale on this host, so org label IDs were unreachable and no child carries priority, autonomy, or role labels. Every slice is marked AFK or HITL in this index and in its own body, so the autonomy axis is recorded even though the label is not applied.
Labels applied, with one axis deliberately left off
Supersedes the "Not labeled" note in the slice index above. That note blamed stale AWS credentials, which was wrong twice over: the org labels API reads without authentication, and my first attempt failed on output parsing rather than on access.
Priority and autonomy are applied to all thirteen issues. Calibrated against this repo's existing open issues, where P1 means broken now (#319), P2 means scheduled work, and P3 is the default backlog.
autonomy/epic: #329.autonomy/headless: #331, #332, #333, #334, #335, #336, #338, #339, #340.autonomy/async-consult: #330, #337, #341.Role labels are omitted on purpose, and #342 records why. The org role labels still name the roster Core Roster v2 replaced, so there is no label for any of the seven seats that exist. Applying
role/engineertoplatformwork would bake that drift into thirteen new issues, so #342 asks for the mapping to be decided once and applied across the org.Worth noting that #342 is the same failure shape as #340: a downstream artifact derived from the roster, which the roster changed out from under, with nothing reporting the drift.
Decision 7 has its name:
housecastDecision 7 in the body says the compositor "Needs a name, a public API, and a
release train." The name is settled. Kai chose
housecastin #330.coilyco-flight-deck/housecast, free on Forgejo and the GitHubmirror as of 2026-08-25T18:50:55Z.
housecast, free on both PyPI surfaces at the same moment.house-castis a separate name under PEP 503 and is also free.A house cast is the resident standing company of a theatre: a fixed troupe held
by one house under one set of conventions, able to mount any role in its book.
houseis the authority noun, andcastcarries both casting a play and castingmetal, the second being the immutable bundle. It takes umbra's position and
cannot be heard as an accessory to
acompose.This unblocks #337, which was blocked on #330 for exactly this. The full search
record, the alternates, and what the name forecloses are in #330.
One finding from the search that belongs on this design, not just on the name
The naming register this product occupies is being consumed by other AI-agent
projects at speed. Seven independent projects created during 2026 had taken names
that were on the shortlist, four of them clearing PyPI and dying only on a GitHub
check:
dokimasia(agent capability testing, May),euthyna(agent auditgateway, July),
chartulary(multi-agent analysis, four days before the search),inspeximus(agent memory, 114 releases),vidimus(agent record custody),charter, andinvestiture.The consequence for this design is narrow and worth writing down. PyPI has no
reservation mechanism short of uploading a distribution, so the gap between
choosing the name and holding it is exposure, and the compositor slice is
sequenced late. #347 carries that as its own item. Per the HITL note in #337, the
claim needs Kai's explicit go-ahead at the moment it happens rather than standing
authorization, and I have corrected #347's label to match.
#331 finding: the protocol is producer-agnostic, the program proceeds
The spike is done and landed as
5c37622. The assumption this program rests on holds.testdata/handmade-bundlewas authored fromdocs/bundle-protocol.mdandinternal/bundle/verify.go, not captured from a composition run. Its source id isfixture:handmadeand its role iscourier. Neither exists in the roster, so nothing of the Go composition path is load-bearing in it.Every consumer surface accepted it unmodified, on the first attempt, with no change to the Go side:
verify-bundle verified: 2 skills // 5 filesproject --layout claude- placedCLAUDE.mdand.claude/skills/{role-courier,personality-steady}/SKILL.mdstatusline-⏱️ Wren [they] zq49 // courier@claude // frontier // 2 skills / ~182 catalog // ✓ composedwhoami-Wren [they] zq49describe- rendered the full profile, provider, budget, selection, and delivery treebundle export- produced a 1782-byte archiveThe invented seat name and the invented emblem both render. A foreign producer controls identity end to end.
Gaps, none of them blocking
No field that
verifyrequires is unsynthesizable by a foreign producer. The one field that cannot be guessed is the provider budget, and it is fully determined by the tree the producer just wrote:context_bytesmust equal the byte sum of that source's selected skill trees andapproximate_tokensmust equal(context_bytes + 3) / 4. Off by one is rejected, which I confirmed rather than assumed.Four fields are the reverse problem, required by consumers but unenforced by
verify. Each was confirmed by mutating a verified bundle and watching it pass:content[].digest- checked for shape, never recomputed against bytes. A bundle whose digests are all zeroes verifies.role_skill_digest- checked for presence only. The literal stringnonsenseverifies.delivery.body_bytes- never read by verify.999999verifies.identity- optional, and omitting it does not fail.statusline,whoami, anddescribedegrade to the bare role slug and a generic emblem instead.That last one is the only one with a user-visible consequence, and it degrades rather than breaks. I checked, expecting a crash, and did not get one.
One portability trap is now documented rather than left to be discovered: a source id is percent-encoded per path segment on disk while
manifest.jsonandtrace.jsonkeep the raw id, soroster:coreisroster%3Acoreundercontent/skills/. A Python producer that writes the raw id fails identity-tree matching with a confusing message.A constraint the next docs change will hit
docs/bundle-protocol.mdwas already at exactly its 120-line cap anddocs/is at exactly its 40-doc cap, so the producer contract could neither be added in place nor split into a new page. I paid for it by compressing the protocol half of that file, dropping no review decision. The next person to add documentation here has no room at all. Filed separately as #348.Reported from the Agentic Platform Engineer seat. #332 is next and is now unblocked.
Correction to the comment above: the documentation-budget issue is #371, not #348. I wrote the reference before filing and guessed the number wrong. #348 is an unrelated open issue about nested native launches and picked up a stray backlink from my mistake.