feat(request): let a deployment omit a boundary it has no seat for #310

Merged
coilysiren merged 2 commits from feat/boundary-omit into main 2026-08-19 02:21:07 +00:00
Member

Closes #304, and implements the knob that issue specified.

Why

docs/ownership.md is explicit that a boundary is a relationship: "Every boundary names the role holding the other side with a required owner. The owner is a relationship, not authority." The defer side is therefore routing - hand this to ops.

A deployment where ops is not a seat has nowhere to route. The rule reads as a stop rather than a handoff, and the agent defers work nobody will pick up. A single-agent deployment hits this on every boundary its role defers: engineer defers two, to ops and to exec, and gets neither.

The reporting case is the sirens-dowel lane, the only agent in its guild. It currently ships a local skill saying the composed boundary "does not reach these verbs" and "this file wins", which is a workaround in prose for something that should not have composed.

The knob

compose {
    role "engineer"
    boundary-omit "modify-live-system" "seek-external-validation"
}

The boundary leaves the composed set completely: no body in the bundle, no name on the identity card, no manifest entry. That completeness is the point. Naming a boundary whose body is absent is worse than either state, because the card then describes doctrine the agent cannot read, which is the failure sirens-echo#859 was about.

Three refusals, all from #304's own list:

  • An unknown name fails rather than no-opping, matching the rest of the request parser.
  • A boundary the role owns cannot be omitted. An owner losing its own boundary is a larger claim than a deferrer losing one, and it would leave the boundary with no side holding it. validateBoundaryOwners already refuses to let one role sit on both sides; this keeps it from being reachable the other way round.
  • A boundary the role does not activate fails too, so a stale request surfaces instead of quietly expressing nothing.

The decision trace records each omission as an excluded profile decision, per #304's requirement that the review surface keep telling the truth.

Two paths, not one

Worth flagging for review, because I got it wrong first. The boundary body reaches a bundle through two routes:

  1. The explicit boundary loop in Resolve, which builds res.Skills.
  2. person.Source attaches each role's active boundaries to src.RoleSkills[role], and a catch-all loop admits anything an admitted source offers.

Filtering only the first left the manifest, card, and trace all correct while the bundle still shipped both SKILL.md files - the worst possible outcome, since every review surface would have said they were gone. TestBoundaryOmitLeavesNoTraceInTheBundle caught it, and that failure is why the test globs the tree rather than trusting the manifest.

What is not in scope

I considered deriving the omission from a declared seat set (roster-seats "engineer") rather than listing boundaries, and wrote it up on #304. It generalises better and needs no per-deployment list. I built the explicit form instead: this issue specified it, it is more auditable at review time, and it is the smaller change. The derived form can sit on top later without changing this grammar.

Also unchanged: roster.go still renders every role's full boundary set into the roster projection, because that describes the roster rather than one deployment.

Verification

go test ./..., go vet ./..., and pre-commit run --all-files all green.

End to end through the CLI, on the new boundary-omit.kdl contract fixture:

bundle verified: 5 skills // 8 files
--- composed skills ---
personality-curious  personality-meticulous  personality-tenacious  role-engineer
--- card boundary lines ---
(no boundary line on the card)

RenderRoleIdentityCard gains the composed boundary list as a parameter rather than deriving it from the role, so the bundle card and the roster card can differ truthfully. Two callers and one test updated.

Closes #304, and implements the knob that issue specified. ## Why `docs/ownership.md` is explicit that a boundary is a relationship: "Every boundary names the role holding the other side with a required `owner`. **The owner is a relationship, not authority.**" The defer side is therefore routing - hand this to `ops`. A deployment where `ops` is not a seat has nowhere to route. The rule reads as a stop rather than a handoff, and the agent defers work nobody will pick up. A single-agent deployment hits this on every boundary its role defers: `engineer` defers two, to `ops` and to `exec`, and gets neither. The reporting case is the `sirens-dowel` lane, the only agent in its guild. It currently ships a local skill saying the composed boundary "does not reach these verbs" and "this file wins", which is a workaround in prose for something that should not have composed. ## The knob ```kdl compose { role "engineer" boundary-omit "modify-live-system" "seek-external-validation" } ``` The boundary leaves the composed set completely: no body in the bundle, no name on the identity card, no manifest entry. That completeness is the point. **Naming a boundary whose body is absent is worse than either state**, because the card then describes doctrine the agent cannot read, which is the failure sirens-echo#859 was about. Three refusals, all from #304's own list: - An **unknown** name fails rather than no-opping, matching the rest of the request parser. - A boundary the role **owns** cannot be omitted. An owner losing its own boundary is a larger claim than a deferrer losing one, and it would leave the boundary with no side holding it. `validateBoundaryOwners` already refuses to let one role sit on both sides; this keeps it from being reachable the other way round. - A boundary the role **does not activate** fails too, so a stale request surfaces instead of quietly expressing nothing. The decision trace records each omission as an excluded profile decision, per #304's requirement that the review surface keep telling the truth. ## Two paths, not one Worth flagging for review, because I got it wrong first. The boundary body reaches a bundle through **two** routes: 1. The explicit boundary loop in `Resolve`, which builds `res.Skills`. 2. `person.Source` attaches each role's active boundaries to `src.RoleSkills[role]`, and a catch-all loop admits anything an admitted source offers. Filtering only the first left the manifest, card, and trace all correct while the bundle **still shipped both `SKILL.md` files** - the worst possible outcome, since every review surface would have said they were gone. `TestBoundaryOmitLeavesNoTraceInTheBundle` caught it, and that failure is why the test globs the tree rather than trusting the manifest. ## What is not in scope I considered deriving the omission from a declared seat set (`roster-seats "engineer"`) rather than listing boundaries, and wrote it up on #304. It generalises better and needs no per-deployment list. I built the explicit form instead: this issue specified it, it is more auditable at review time, and it is the smaller change. The derived form can sit on top later without changing this grammar. Also unchanged: `roster.go` still renders every role's full boundary set into the roster projection, because that describes the roster rather than one deployment. ## Verification `go test ./...`, `go vet ./...`, and `pre-commit run --all-files` all green. End to end through the CLI, on the new `boundary-omit.kdl` contract fixture: ``` bundle verified: 5 skills // 8 files --- composed skills --- personality-curious personality-meticulous personality-tenacious role-engineer --- card boundary lines --- (no boundary line on the card) ``` `RenderRoleIdentityCard` gains the composed boundary list as a parameter rather than deriving it from the role, so the bundle card and the roster card can differ truthfully. Two callers and one test updated.
feat(request): let a deployment omit a boundary it has no seat for
Some checks failed
ci / test (pull_request) Failing after 56s
178a16b673
The defer side of a boundary is routing: hand this to the role that owns
it. A deployment where that role is not a seat has nowhere to route, so
the rule reads as a stop rather than a handoff and the agent defers work
nobody will pick up. A single-agent deployment hits this on every
boundary its role defers.

boundary-omit removes the boundary from the composed set entirely: no
body in the bundle, no name on the identity card, no manifest entry, and
an excluded decision in the trace. Naming a boundary whose body is absent
is worse than either, because the card then describes doctrine the agent
cannot read.

Three refusals keep it from meaning more than it should. An unknown name
fails, a boundary the role owns cannot be omitted, and a boundary the
role does not activate fails rather than quietly expressing nothing.

Closes #304.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
fix(docs): one trailing newline on the new page
All checks were successful
ci / test (pull_request) Successful in 50s
f498051068
end-of-file-fixer caught it in CI and not locally, because the file was
untracked when pre-commit ran and --all-files reads git's file list. The
fresh clone also had no hook installed, so the commit gated nothing.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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!310
No description provided.