Invert the stack: data, compositor, bundle, then Go. Drop KDL, move composition out of the engine, mount the roster #329

Open
opened 2026-08-25 14:17:16 +00:00 by coilyco-ops · 10 comments
Member

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 *.kdl plus SKILL.md, bound in by //go:embed person.kdl data at internal/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.
  • The evals reach it only by shelling out. scripts/eval-matrix.sh and scripts/eval-prompts.sh both open with go run ./cmd/agent-compose, render into a mktemp -d, read person.json, and delete the directory. The roster has no durable existence outside a temp dir.
  • docs/evaluation.md codifies the direction as policy: "Python consumes what Go emits and never restates it."

The settled flow

data/*.yaml  ->  compositor  ->  bundle  ->  acompose delivers and launches
                     ^
                  evals live here

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.v3 is already in go.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 verify already 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:embed stops being the path to it. roster:core becomes an ordinary mounted package resolved through the existing person_source mechanism. 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 in docs/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-level data/ 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

  • acompose - Go binary from brew and scoop. Fixed. Never rebuilt for a config change. Selects, verifies, projects, launches.
  • compositor - Python, from PyPI. Installed by whoever authors configs. Turns data into a bundle, and grades it.
  • data - YAML, authored by a human, in the repo that owns that roster.

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

  • Staleness refusal. An author edits YAML, forgets to compose, and acompose mounts a stale bundle that looks fine. Go cannot compose but it can compare. Bundles already retain provenance and content digests, so a bundle carries a digest of its source and acompose refuses when the source has moved on, naming the compositor command.
  • No silent fallback. A missing or stale mount fails loudly rather than reverting to a default. Generalize the existing precedent that refresh-then-exec refuses last-known-good fallback because that projection may use the default.
  • Zero-config via a seed, not via the live roster. The binary carries core as a seed. First run materializes it to the mount location, after which it is an ordinary editable package and upgrades never clobber it. The seed serves a first-time user and the terminal demo. Kai's roster is not the seed, and conflating the two is what created this problem.

Scope

Sequenced. The last one carries a fleet rollout.

  • A. Compositor. Grow evalkit into a published package that composes data into bundles. Move every semantic step out of Go into it.
  • B. Data. Roster source becomes YAML. Kai's roster moves to agentic-os-kai. agent-compose retains a public seed roster only.
  • C. Go engine. Delete the semantic layer from internal/person, drop kdl-go, route core through the ordinary mount path, add source-digest staleness refusal, remove silent fallback. Note that request.kdl and its fixtures are deleted rather than converted, because a Go binary that does not compose has no compose request.
  • D. .agents/roles.kdl provider selector. Cross-repo. Discovery stats that exact filename at internal/nativelaunch/launch.go:366 and 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

  • No .kdl file remains in the repo and no Go file imports kdl-go.
  • internal/person carries no merge, validation, meld-resolution, or color-derivation logic.
  • The compositor is installable from PyPI and produces a bundle from YAML data with no Go toolchain present.
  • acompose mounts a roster from a path, refuses a stale bundle by digest, and never silently falls back to the seed.
  • A roster change is live without rebuilding or re-releasing the binary. This is the acceptance test for the whole program.
  • scripts/eval-matrix.sh derives the board without invoking the Go binary.
  • A coverage check reports unauthored and ungraded cases implied by the current roster, with a switch that can make it blocking, defaulted to report.
  • docs/evaluation.md, docs/person-contract.md, docs/person-packages.md, docs/ownership.md, docs/bundle-protocol.md, and docs/kdl-contracts.md describe the new direction.
  • docs/FEATURES.md reflects the change, since the config language, the roster's location, and where composition happens are all public boundaries.

Boundaries

  • Scope D does not land without its ansible rollout and a both-filenames compatibility window.
  • Do not reintroduce a compose path in Go for external packages. That is the engine surviving in a corner, and it will drift from the real compositor.
  • Do not commit a generated JSON projection as the seam.
  • Do not ship the seed and Kai's roster as the same artifact.
  • #320 - the eval pipeline is documented one way only and the grade-to-edit return path has no page. That missing return path is this inversion seen from the docs side.
## 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 `*.kdl` plus `SKILL.md`, bound in by `//go:embed person.kdl data` at `internal/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. * The evals reach it only by shelling out. `scripts/eval-matrix.sh` and `scripts/eval-prompts.sh` both open with `go run ./cmd/agent-compose`, render into a `mktemp -d`, read `person.json`, and delete the directory. The roster has no durable existence outside a temp dir. * `docs/evaluation.md` codifies the direction as policy: "Python consumes what Go emits and never restates it." ## The settled flow ```text data/*.yaml -> compositor -> bundle -> acompose delivers and launches ^ evals live here ``` 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.v3` is already in `go.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 verify` already 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:embed` stops being the path to it. `roster:core` becomes an ordinary mounted package resolved through the existing `person_source` mechanism. 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 in `docs/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-level `data/` 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 * **acompose** - Go binary from brew and scoop. Fixed. Never rebuilt for a config change. Selects, verifies, projects, launches. * **compositor** - Python, from PyPI. Installed by whoever authors configs. Turns data into a bundle, and grades it. * **data** - YAML, authored by a human, in the repo that owns that roster. 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 * **Staleness refusal.** An author edits YAML, forgets to compose, and acompose mounts a stale bundle that looks fine. Go cannot compose but it can compare. Bundles already retain provenance and content digests, so a bundle carries a digest of its source and acompose refuses when the source has moved on, naming the compositor command. * **No silent fallback.** A missing or stale mount fails loudly rather than reverting to a default. Generalize the existing precedent that refresh-then-exec refuses last-known-good fallback because that projection may use the default. * **Zero-config via a seed, not via the live roster.** The binary carries core as a seed. First run materializes it to the mount location, after which it is an ordinary editable package and upgrades never clobber it. The seed serves a first-time user and the terminal demo. Kai's roster is not the seed, and conflating the two is what created this problem. ## Scope Sequenced. The last one carries a fleet rollout. * **A. Compositor.** Grow evalkit into a published package that composes data into bundles. Move every semantic step out of Go into it. * **B. Data.** Roster source becomes YAML. Kai's roster moves to `agentic-os-kai`. agent-compose retains a public seed roster only. * **C. Go engine.** Delete the semantic layer from `internal/person`, drop `kdl-go`, route core through the ordinary mount path, add source-digest staleness refusal, remove silent fallback. Note that `request.kdl` and its fixtures are **deleted rather than converted**, because a Go binary that does not compose has no compose request. * **D. `.agents/roles.kdl` provider selector.** Cross-repo. Discovery stats that exact filename at `internal/nativelaunch/launch.go:366` and 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 * No `.kdl` file remains in the repo and no Go file imports `kdl-go`. * `internal/person` carries no merge, validation, meld-resolution, or color-derivation logic. * The compositor is installable from PyPI and produces a bundle from YAML data with no Go toolchain present. * `acompose` mounts a roster from a path, refuses a stale bundle by digest, and never silently falls back to the seed. * A roster change is live without rebuilding or re-releasing the binary. This is the acceptance test for the whole program. * `scripts/eval-matrix.sh` derives the board without invoking the Go binary. * A coverage check reports unauthored and ungraded cases implied by the current roster, with a switch that can make it blocking, defaulted to report. * `docs/evaluation.md`, `docs/person-contract.md`, `docs/person-packages.md`, `docs/ownership.md`, `docs/bundle-protocol.md`, and `docs/kdl-contracts.md` describe the new direction. * `docs/FEATURES.md` reflects the change, since the config language, the roster's location, and where composition happens are all public boundaries. ## Boundaries * Scope D does not land without its ansible rollout and a both-filenames compatibility window. * Do not reintroduce a compose path in Go for external packages. That is the engine surviving in a corner, and it will drift from the real compositor. * Do not commit a generated JSON projection as the seam. * Do not ship the seed and Kai's roster as the same artifact. ## Related * #320 - the eval pipeline is documented one way only and the grade-to-edit return path has no page. That missing return path is this inversion seen from the docs side.
Author
Member

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.kdl
  • coilyco-bridge/agentic-os-kai/.agents/roles.kdl

So 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.

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.kdl` * `coilyco-bridge/agentic-os-kai/.agents/roles.kdl` So 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.
Author
Member

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

data/*.yaml  ->  compositor  ->  bundle  ->  Go delivers and launches
                     ^
                  evals live here

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

  • Semantic validation moves upstream, out of Go. Does this meld exist, does a boundary owner illegally declare its own boundary, is the copy contract satisfied, does the role skill have valid frontmatter and three paragraphs under 400 words. All of it leaves 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.
  • Structural verification stays in Go. 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.md already specifies the bundle as an immutable tree entered through manifest.json and 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

  • Where the compositor runs. A brew-installed and scoop-installed Go binary cannot shell out to Python on every host without a real distribution regression, so bundles most likely get built at release time and shipped rather than composed at convergence time.
  • What an external person package becomes. Today Go resolves and composes one at runtime. If Go cannot compose, an external package is either pre-composed by its author into a bundle, or some compose ability has to survive in Go for that path alone. Note that a package author who cannot run the compositor also cannot grade their own roster, and grading is meant to be the gate, so coupling those two may be correct rather than a cost.

These two supersede the "Open, to settle during implementation" list in the body, which was written under the wrong flow.

## 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 ```text data/*.yaml -> compositor -> bundle -> Go delivers and launches ^ evals live here ``` 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 * **Semantic validation moves upstream, out of Go.** Does this meld exist, does a boundary owner illegally declare its own boundary, is the copy contract satisfied, does the role skill have valid frontmatter and three paragraphs under 400 words. All of it leaves `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. * **Structural verification stays in Go.** `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.md` already specifies the bundle as an immutable tree entered through `manifest.json` and 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 * **Where the compositor runs.** A brew-installed and scoop-installed Go binary cannot shell out to Python on every host without a real distribution regression, so bundles most likely get built at release time and shipped rather than composed at convergence time. * **What an external person package becomes.** Today Go resolves and composes one at runtime. If Go cannot compose, an external package is either pre-composed by its author into a bundle, or some compose ability has to survive in Go for that path alone. Note that a package author who cannot run the compositor also cannot grade their own roster, and grading is meant to be the gate, so coupling those two may be correct rather than a cost. These two supersede the "Open, to settle during implementation" list in the body, which was written under the wrong flow.
Author
Member

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:embed stops being the path to the roster. A roster the binary carries is a roster that needs a rebuild to change. The default roster:core therefore 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.go already reads core through an fs.FS in 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_source and person_policy in host config resolve an external package today. What is missing is that core does not go through it.

The resulting shape

  • acompose - Go binary from brew and scoop. Fixed. Never rebuilt for a config change. Selects, verifies, projects, launches.
  • compositor - Python. Installed by whoever authors configs. Turns data into a bundle.
  • data - YAML, authored by a human, living in whatever repo owns that roster.
  • bundle - the composed artifact acompose mounts.

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

  • Staleness is now a real failure mode. An author edits YAML, forgets to compose, and acompose mounts a stale bundle that looks fine. Go cannot compose, but it can compare. Bundles already retain provenance and content digests per 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.
  • "Reliably" rules out silent fallback. A missing or stale mount must fail loudly rather than quietly falling back to a default. There is precedent to generalize: docs/person-packages.md already records that refresh-then-exec refuses last-known-good fallback because that projection may use the default.
  • Zero-config still needs to work, for strangers and for the demo. The proposal is that the binary carries core as a seed rather than as the live roster: first run materializes it to the mount location, after which it is an ordinary editable mounted package and upgrades never clobber it. Kai's own roster is then not the seed at all. It is a mounted package she edits directly, and the seed exists for a first-time user and the terminal demo. Those are two artifacts with two lifecycles and conflating them is what created this problem.

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.

## 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:embed` stops being the path to the roster.** A roster the binary carries is a roster that needs a rebuild to change. The default `roster:core` therefore 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.go` already reads core through an `fs.FS` in 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_source` and `person_policy` in host config resolve an external package today. What is missing is that core does not go through it. ### The resulting shape * **acompose** - Go binary from brew and scoop. Fixed. Never rebuilt for a config change. Selects, verifies, projects, launches. * **compositor** - Python. Installed by whoever authors configs. Turns data into a bundle. * **data** - YAML, authored by a human, living in whatever repo owns that roster. * **bundle** - the composed artifact acompose mounts. 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 * **Staleness is now a real failure mode.** An author edits YAML, forgets to compose, and acompose mounts a stale bundle that looks fine. Go cannot compose, but it can compare. Bundles already retain provenance and content digests per `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. * **"Reliably" rules out silent fallback.** A missing or stale mount must fail loudly rather than quietly falling back to a default. There is precedent to generalize: `docs/person-packages.md` already records that refresh-then-exec refuses last-known-good fallback because that projection may use the default. * **Zero-config still needs to work, for strangers and for the demo.** The proposal is that the binary carries core as a **seed** rather than as the live roster: first run materializes it to the mount location, after which it is an ordinary editable mounted package and upgrades never clobber it. Kai's own roster is then not the seed at all. It is a mounted package she edits directly, and the seed exists for a first-time user and the terminal demo. Those are two artifacts with two lifecycles and conflating them is what created this problem. ### 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.
coilyco-ops changed title from 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 roster 2026-08-25 16:48:28 +00:00
Author
Member

A 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#1147 is 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:

boundary "seek-external-validation" skill="boundary-seek-external-validation" owner="creator" ...

In core, tpm owns that boundary. The person package re-owns it to creator for 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:

roster:core becomes an ordinary mounted package resolved through the existing person_source mechanism. The core-versus-external distinction collapses, and there is one contract rather than two.

With one contract, ops defers seek-external-validation against 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-echo fail role-drift-check today: #1148, #1149, #1150, and #1152. #1152 carries five children of coilysiren/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-echo re-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.

**A 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#1147` is 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: ```kdl boundary "seek-external-validation" skill="boundary-seek-external-validation" owner="creator" ... ``` In core, `tpm` owns that boundary. The person package re-owns it to `creator` for 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: > `roster:core` becomes an ordinary mounted package resolved through the existing `person_source` mechanism. The core-versus-external distinction collapses, and there is one contract rather than two. With one contract, `ops` defers `seek-external-validation` against 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-echo` fail `role-drift-check` today: #1148, #1149, #1150, and #1152. #1152 carries five children of `coilysiren/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-echo` re-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.
Author
Member

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:

  • umbra owns the policy language and the engine that reads it.
  • mcp-beaver "renders an umbra guardfile into a guarded MCP server and HTTP tool API inside one generic OCI image. One runtime, many guardfiles." It is "downstream of umbra, never a peer to it."

Mapped onto this program:

  • compositor repo takes umbra's position. It owns the roster data language, the composition engine, and the artifact.
  • acompose takes mcp-beaver's position. One generic runtime, many bundles. Downstream of the compositor, never a peer to it.

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

  • Dependency direction is fixed and one-way. acompose consumes the compositor's artifact. The compositor never depends on acompose, never imports from it, and is never described as its peer.
  • The eval runner stays in the compositor repo. Precedent: umbra "ships 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-eval grading stays in agentic-os as it is today.
  • Roster data is authored by consumers, not shipped by the engine. A deployment writes its own guardfile. The compositor repo owns the schema and a reference roster. Kai's roster stays in agentic-os-kai per 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

  • Name. Needed to create the repository and to claim a distribution name. Repo name and PyPI distribution name may differ, and PyPI availability needs checking before either is locked.
  • License. MIT, as a shareable library under standing policy, unless there is a reason to differ.
## 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: * **umbra** owns the policy language and the engine that reads it. * **mcp-beaver** "renders an umbra guardfile into a guarded MCP server and HTTP tool API inside one generic OCI image. One runtime, many guardfiles." It is "downstream of umbra, never a peer to it." Mapped onto this program: * **compositor repo** takes umbra's position. It owns the roster data language, the composition engine, and the artifact. * **acompose** takes mcp-beaver's position. One generic runtime, many bundles. Downstream of the compositor, never a peer to it. 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 * **Dependency direction is fixed and one-way.** acompose consumes the compositor's artifact. The compositor never depends on acompose, never imports from it, and is never described as its peer. * **The eval runner stays in the compositor repo.** Precedent: umbra "ships `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-eval` grading stays in agentic-os as it is today. * **Roster data is authored by consumers, not shipped by the engine.** A deployment writes its own guardfile. The compositor repo owns the schema and a reference roster. Kai's roster stays in `agentic-os-kai` per 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 * **Name.** Needed to create the repository and to claim a distribution name. Repo name and PyPI distribution name may differ, and PyPI availability needs checking before either is locked. * **License.** MIT, as a shareable library under standing policy, unless there is a reason to differ.
Author
Member

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.

  • #331 - Prove acompose consumes a bundle it did not compose. AFK. No blockers, start here.
  • #332 - Tracer: one role, YAML to bundle to launched agent, no Go rebuild. AFK. Blocked by #331.
  • #333 - Port the semantic layer from Go, with the Go tests as the oracle. AFK. Blocked by #332.
  • #334 - acompose refuses a stale bundle and never silently falls back. AFK. Blocked by #332.
  • #335 - Convert all 22 roster entities from KDL to YAML. AFK. Blocked by #333.
  • #336 - acompose embeds no roster and mounts core through the ordinary path. AFK. Blocked by #334.
  • #337 - Create the compositor repository, package it, publish it. HITL. Blocked by #330 and #333.
  • #338 - evalkit derives the board from the data, with no Go in the path. AFK. Blocked by #335.
  • #339 - Delete the Go semantic layer and drop the KDL dependency. AFK. Blocked by #338 and #336.
  • #340 - Coverage check with a switch to make it blocking. AFK. Blocked by #338.
  • #341 - Retire .agents/roles.kdl across 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.md states that intent, but intent is not evidence about what verify and project require. 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.

## 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. * #331 - Prove acompose consumes a bundle it did not compose. **AFK. No blockers, start here.** * #332 - Tracer: one role, YAML to bundle to launched agent, no Go rebuild. AFK. Blocked by #331. * #333 - Port the semantic layer from Go, with the Go tests as the oracle. AFK. Blocked by #332. * #334 - acompose refuses a stale bundle and never silently falls back. AFK. Blocked by #332. * #335 - Convert all 22 roster entities from KDL to YAML. AFK. Blocked by #333. * #336 - acompose embeds no roster and mounts core through the ordinary path. AFK. Blocked by #334. * #337 - Create the compositor repository, package it, publish it. **HITL.** Blocked by #330 and #333. * #338 - evalkit derives the board from the data, with no Go in the path. AFK. Blocked by #335. * #339 - Delete the Go semantic layer and drop the KDL dependency. AFK. Blocked by #338 and #336. * #340 - Coverage check with a switch to make it blocking. AFK. Blocked by #338. * #341 - Retire `.agents/roles.kdl` across 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.md` states that intent, but intent is not evidence about what `verify` and `project` require. 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.
Author
Member

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.

  • P2: #329, #330, #331, #332. The epic, the naming brief that blocks repo creation, the de-risking spike, and the tracer that is the program's sole bottleneck.
  • P3: #333 through #341. The body of the work, promoted when the program is scheduled.
  • 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/engineer to platform work 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.

## 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. * P2: #329, #330, #331, #332. The epic, the naming brief that blocks repo creation, the de-risking spike, and the tracer that is the program's sole bottleneck. * P3: #333 through #341. The body of the work, promoted when the program is scheduled. * `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/engineer` to `platform` work 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.
Author
Member

Decision 7 has its name: housecast

Decision 7 in the body says the compositor "Needs a name, a public API, and a
release train." The name is settled. Kai chose housecast in #330.

  • Repository - coilyco-flight-deck/housecast, free on Forgejo and the GitHub
    mirror as of 2026-08-25T18:50:55Z.
  • PyPI distribution - housecast, free on both PyPI surfaces at the same moment.
    house-cast is 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.
house is the authority noun, and cast carries both casting a play and casting
metal, 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 audit
gateway, July), chartulary (multi-agent analysis, four days before the search),
inspeximus (agent memory, 114 releases), vidimus (agent record custody),
charter, and investiture.

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.

## Decision 7 has its name: `housecast` Decision 7 in the body says the compositor "Needs a name, a public API, and a release train." The name is settled. Kai chose `housecast` in #330. * Repository - `coilyco-flight-deck/housecast`, free on Forgejo and the GitHub mirror as of 2026-08-25T18:50:55Z. * PyPI distribution - `housecast`, free on both PyPI surfaces at the same moment. `house-cast` is 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. `house` is the authority noun, and `cast` carries both casting a play and casting metal, 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 audit gateway, July), `chartulary` (multi-agent analysis, four days before the search), `inspeximus` (agent memory, 114 releases), `vidimus` (agent record custody), `charter`, and `investiture`. 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.
Author
Member

#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-bundle was authored from docs/bundle-protocol.md and internal/bundle/verify.go, not captured from a composition run. Its source id is fixture:handmade and its role is courier. 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 files
  • project --layout claude - placed CLAUDE.md and .claude/skills/{role-courier,personality-steady}/SKILL.md
  • statusline - ⏱️ Wren [they] zq49 // courier@claude // frontier // 2 skills / ~182 catalog // ✓ composed
  • whoami - Wren [they] zq49
  • describe - rendered the full profile, provider, budget, selection, and delivery tree
  • bundle export - produced a 1782-byte archive

The 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 verify requires 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_bytes must equal the byte sum of that source's selected skill trees and approximate_tokens must 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 string nonsense verifies.
  • delivery.body_bytes - never read by verify. 999999 verifies.
  • identity - optional, and omitting it does not fail. statusline, whoami, and describe degrade 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.json and trace.json keep the raw id, so roster:core is roster%3Acore under content/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.md was already at exactly its 120-line cap and docs/ 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.

## #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-bundle` was authored from `docs/bundle-protocol.md` and `internal/bundle/verify.go`, not captured from a composition run. Its source id is `fixture:handmade` and its role is `courier`. 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 files` * `project --layout claude` - placed `CLAUDE.md` and `.claude/skills/{role-courier,personality-steady}/SKILL.md` * `statusline` - `⏱️ Wren [they] zq49 // courier@claude // frontier // 2 skills / ~182 catalog // ✓ composed` * `whoami` - `Wren [they] zq49` * `describe` - rendered the full profile, provider, budget, selection, and delivery tree * `bundle export` - produced a 1782-byte archive The 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 `verify` requires 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_bytes` must equal the byte sum of that source's selected skill trees and `approximate_tokens` must 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 string `nonsense` verifies. * `delivery.body_bytes` - never read by verify. `999999` verifies. * `identity` - optional, and omitting it does not fail. `statusline`, `whoami`, and `describe` degrade 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.json` and `trace.json` keep the raw id, so `roster:core` is `roster%3Acore` under `content/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.md` was already at exactly its 120-line cap and `docs/` 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.
Author
Member

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.

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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agent-compose#329
No description provided.