feat: name what a run hands to a person, and gate it as one resource - #178
Merged
Merged
Conversation
A proposal and a clarification were never one thing in the type system, yet
every layer treated them as one: the same `domain/hitl/` directory, the same
service, the same queue, the same owner fields, the same filter copied twice.
This names that concept `Handoff`, a point a run reached that only a person can
settle, and hangs the permission model off it.
The capability catalog goes from ten ids to eight, and the verb set closes to
read, write, manage, and run. A verb now says only whether a call changes the
resource, never what the call is called, so `history.write` covers restoring
rather than a `restore` verb that every new feature would want a sibling of.
`manage` means the part of a resource that reaches other people, which is a
wider scope rather than a stronger write, so `workspace.manage` covers members
and roles and also whether one person sees everybody else's work.
Three ids went because nothing was deciding anything with them. The check
behind `workspace.read` was `effectiveRole !== null`, identical to the
membership gate it always sat behind, so it passed everyone it was shown.
`proposal.read` and `clarification.read` were never mounted on a route at all,
which left the Studio nav enforcing them on its own.
Four holes close with it:
- `/batch` had no permission gate, so any member could start, stop, resume,
or archive a batch. It now takes `skill.run` against the per-unit skill
the ontology declares, since a batch is that skill run many times over.
- `GET /agui/runs/:runId` replayed any run's whole event log. The visibility
check lived on the `/runs` router, and `/agui` is a different prefix.
- `GET /proposals/:id` and `GET /clarifications/:id` had no owner check, so
the list filter was queue hygiene rather than a boundary.
- `/coverage` carried each stage's pending ids to everybody, so a reader who
could not open the queue could still count it.
Two behaviours change on purpose. A maintainer now sees what the reactor handed
over, which `includeServiceOwned: isOwner` had backwards, and which is the whole
point of the role. And a settled handoff follows whether it is still private
rather than whether it is still pending, so an applied proposal stays public as
the graph's provenance while a rejected one stays with whoever raised it.
The Documents surface lands on the same rule. Reading a document asks nothing,
since a document is a reading of the graph and every member reads the graph.
Writing one is the form's generator skill run on a projected subject, so
`POST /views` takes `skill.run` against that skill, and the two generators
inherit the owner-and-maintainer default from their manifests without declaring
anything. The gate resolves the form before it decides, so an unknown kind or
form is still answered as a bad request rather than as a refusal.
Studio gets one answer to which surfaces a viewer may open, because the sidebar
hiding a surface while the command palette still reached it was the same drift
twice. Chords collapse to the six surfaces the sidebar keeps a row for, which
is what lets every one of them be its own first letter. Several printed
shortcuts had no handler behind them, and `G B` went somewhere other than its
label said.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGKCWu7ACm78YVGmPAywnx
mroops0111
force-pushed
the
feat/handoff-capabilities
branch
from
September 15, 2026 06:50
ea83674 to
d1b921f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A proposal and a clarification were never one type, yet every layer already treated them as one. This names that concept and hangs the permission model off it.
Handoff
A handoff is a point a run reached that only a person can settle. The concept was already there in everything except the type system.
domain/hitl/HITLServiceowns all five verbsowner,ownerDisplayName, andownerKind, declared twiceProposalFilterandClarificationFilterwere byte-identical, matching comments includedThree things the schema now pins down, because the word carries baggage.
Capability Catalog
Ten ids become eight, and the verbs close to read, write, manage, and run.
handoff.readhandoff.writeskill.runworkspace.writeworkspace.managehistory.writeserver.writeserver.manageTwo rules hold the set together.
restoreordecideorapprovethat every new feature would want a sibling ofworkspace.managecovers both member governance and seeing another member's unsettled workAnything every member may read has no id at all. The membership gate upstream already answered it.
Removals
Three ids went because nothing was deciding anything with them.
workspace.read: the check waseffectiveRole !== null, identical to the membership condition it always sat behind, so it passed everyone it was shownproposal.readandclarification.read: never mounted on any route, so only the Studio nav read them, which made them a front-end convention rather than a permissionFour more changed shape.
proposal.writeandclarification.write: merge intohandoff.writeserver.admin: becomesserver.manage, sinceadminis a nounworkspace.create: becomesserver.write, and its check now readsserverRoledirectly rather than relying on a deliberately absent memberClosed Holes
Four gaps close with the rename.
/batchHad No Gate: any member, guest included, could start, stop, resume, or archive a batch, and it now takesskill.runagainst the per-unit skill the ontology declaresGET /agui/runs/:runIdReplayed Anything: the visibility check was mounted on the/runsrouter and/aguiis a separate prefix, so knowing a run id was enough to read somebody else's Ask transcript/coverageLeaked Queue Ids: every stage carried its pending ids to every member, so a reader who could not open the queue could still count itBehaviour Changes
Three changes are visible to existing users and worth a look before merging.
includeServiceOwned: isOwnerhad this backwards, and an autonomous handoff belongs to the workspace rather than to any one personskill.runand restore still takinghistory.writeThe second one is decided in
handoffVisibleTo, one function serving all four repositories. It avoids the incoherence of a maintainer being unable to see a proposal before the decision yet able to read it in full afterwards.Documents
Rebased onto #179, and the new surface lands on the same rule without a new capability.
GET /viewsand the read route stay open, and the sidebar row shows for every rolePOST /viewstakesskill.runagainst the form's generator skilldoc:referenceanddoc:tutorialdeclare noallowedRolesand inherit owner and maintainer, so a third generator needs no permission changeThe gate resolves the form to its skill before it decides, so an unknown kind or form is answered as a bad request rather than as a refusal. Observed behaviour on a write, covered by tests.
Studio disables the Write and Regenerate controls behind the same question, asked of the generator skills rather than of the surface. Hidden skills count there, since a generator is hidden because nobody picks it off the Actions list, not because nobody runs it.
Studio
One helper,
useSurfaceReach, now answers which surfaces a viewer may open. The sidebar hiding a surface while the command palette still reached it was the same drift written twice.Keyboard chords collapse to the surfaces the sidebar keeps a row for, which is what lets each be its own first letter.
G G,G A,G B,G I,G D,G H, andG S, plusG Wfor workspace detailsG Bwas labelled Build while jumping to Activity, andchordSecondKeycarried twocase 'b'arms where the second was deadThe standalone Proposals and Clarification surfaces are gone. They had no sidebar entry and the Inbox already renders the same detail components.
components/handoff/, which is also where the Show All control and the origin badge liveVerification
Local and CI both green.
pnpm lintandpnpm typecheckcleanhandoffVisibleTo, the four holes above, the maintainer and service-owned visibility rule, and both sides of the document write gateOut Of Scope
Three follow-ups are deliberately left out.
ticket: means only a clarification today and should becomehandoff, across 76 call sitesSkillArtifactKind'sclarify: a pre-existing inconsistency withclarificationthatHandoffKindmakes more visibleallowedRoles: so a read-onlyaskskill never has to remember to open itself to guests, which needs a transform because Zod's.default()cannot see a sibling field🤖 Generated with Claude Code
https://claude.ai/code/session_01NGKCWu7ACm78YVGmPAywnx