Skip to content

Commit ea84995

Browse files
Merge pull request #594 from corbitsdev/cl-6941-define-the-subagents-package-and-its-three-tier-tool-surface
Define the subagents package and its three-tier tool surface
2 parents 1085c45 + 5b954db commit ea84995

29 files changed

Lines changed: 433 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1313

1414
## [Unreleased]
1515

16+
### Agent
17+
18+
- **Fleet authority tiers are now runtime-enforced, not documented in a prompt.**
19+
Every director package carries a required `tier` (`orchestrator` /
20+
`nested-orchestrator` / `leaf`): skywalker gets full fleet control, greybeard
21+
(and any package with `spawn.maySpawn`) is scoped to its own subtree, and every
22+
other director gets no fleet verbs at all. The gate lives in code
23+
(`src/subagent/authority.ts`, wired into `runSubAgent`'s tool-mount point) and
24+
fails closed: a caller whose tier cannot be resolved — including a
25+
project-local or plugin agent profile with `orchestrator: true` that has not
26+
explicitly opted in via `fleetTier: "nested-orchestrator"` — is denied
27+
`task`/`search_agents` rather than silently trusted. This is the foundation
28+
the next fleet-control verbs (spawn/list/steer a live agent) land against;
29+
the subtree-scoping rule for those is written and tested but not yet wired to
30+
a live call site. `task()` is unchanged and still the only spawn verb.
31+
1632
## [0.2.107] - 2026-08-24
1733

1834
### Agent

docs/ARCHITECTURE.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,25 @@ When profiles exist (local `.agents/agents/` and/or enabled **`kind: "agent"`**
227227

228228
Profiles with `orchestrator: true` may themselves call `task` (one hop only): nested dispatch installs `task` + `search_agents` with `allowOrchestrator: false` so the tree bottoms out. Unknown `agent` ids fail closed.
229229

230+
#### Fleet authority tiers (`src/subagent/authority.ts`) (CL-6941)
231+
232+
Every director package carries a required `tier: SubagentTier` field (`src/agent/directors/types.ts`) — data on the package, never a prompt instruction:
233+
234+
| Tier | Who | Fleet surface |
235+
| ------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
236+
| 1 — `orchestrator` | skywalker (primary) | Full fleet control over the whole tree. |
237+
| 2 — `nested-orchestrator` | greybeard, or any package with `spawn.maySpawn` | Same fleet surface, scoped to its own subtree: may manage only its own descendants, never a sibling or ancestor. |
238+
| 3 — `leaf` | every other director | No fleet verbs at all. |
239+
240+
Enforcement is runtime code at the existing tool-mount point, not prompt wording — this is the fix for four prior mechanisms (`writePaths`, `report.requiredSections`, a `--config` comment, the thrash matcher) that were documented-as-enforced while enforcing nothing:
241+
242+
- **Mount-time gate — live today, and fails closed.** `task-tool.ts` resolves the caller's tier at dispatch time — a closed director's `DirectorPackage.tier`, or an explicit non-leaf `AgentProfile.fleetTier` opt-in for a profile-sourced orchestrator — and forwards it as `RunSubAgentParams.orchestratorTier`. `runSubAgent` (`src/subagent/run.ts`) then calls `assertTierMayMountFleetVerb(tier, toolName)` (`src/subagent/authority.ts`) before installing `task` / `search_agents`, treating a **missing** `orchestratorTier` as `"leaf"` — deny, not skip. This is the case that matters most: a project-local or plugin `AgentProfile` with `orchestrator: true` is outside the closed director set and is **not** trusted with fleet verbs just because `orchestrator: true` is set — it must also declare `fleetTier: "nested-orchestrator"`, or the mount throws `FleetAuthorityError`. (`AgentProfile.fleetTier` is deliberately not named `tier` — that name is already used, ad hoc, by some profiles for an unrelated model-speed selector; reusing it would have silently broken schema validation for those profiles, which is exactly what happened during review and was caught by the full test suite, not by inspection.) `FLEET_VERBS` in `authority.ts` also names the not-yet-implemented verbs (`spawn_agent`, `wait_agents`, `list_agents`, `send_input`, `interrupt_agent`, `close_agent`, `resume_agent`, `read_agent_trace`, `followup_task`) so their future mount sites inherit the same gate.
243+
- **Subtree authority — a seam, not yet wired.** `assertCanTargetAgent(actor, targetId, nodes)` (`src/subagent/authority.ts`) implements the "root owns its tree; a child manages only its own descendants" rule (Tier 1 may target anyone, Tier 2 may target only its own descendants over the same `{id, parentSessionId}` shape `SubAgentSessionStore` already tracks, Tier 3 always fails closed) — but **it has no production call site yet**. No verb today lets one live agent address another (`task` only spawns), so this rule is exercised only by `authority.test.ts` and is not enforced at runtime in this PR. It exists so CL-6942 (split spawn from wait) and CL-6944 (`send_input` steering) — the first verbs that make an agent addressable by another — can call it from day one instead of each inventing its own check. Treat it as unenforced until one of those wires a call site.
244+
- `task()` is unaffected and remains the only spawn verb until the new verbs land beside it (deprecated-not-deleted per the CL-6940 epic). Its argument schema and wire contract are unchanged; the tier check only gates which packages may have it mounted at all.
245+
230246
#### Closed director fleet (`src/agent/directors/`)
231247

232-
Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`) registered in a **closed** set of 16 ids. There is no catch-all worker: `task` without `agent` or non-general `intent`, and `task(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `createTaskTool` (not prompt-only). Skywalker is the primary session identity: `task(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog.
248+
Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 16 ids. There is no catch-all worker: `task` without `agent` or non-general `intent`, and `task(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `createTaskTool` (not prompt-only). Skywalker is the primary session identity: `task(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog.
233249

234250
**Primary**
235251

src/agent/directors/brand-reviewer/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const brandReviewerPackage: DirectorPackage = {
1616
description: "DESIGN.md brand gate",
1717
tools: { allow: DOCS_TOOLS },
1818
spawn: { maySpawn: false },
19+
tier: "leaf",
1920
nudge: { maxTurns: 40 },
2021
modelRole: "docs",
2122
systemPrompt: `You are BrandReviewerDirector, a specialist in Corbits Code.

src/agent/directors/bruckheimer/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const bruckheimerPackage: DirectorPackage = {
1717
description: "Product discovery specialist — user/product shape docs, not code",
1818
tools: { allow: DOCS_TOOLS },
1919
spawn: { maySpawn: false },
20+
tier: "leaf",
2021
nudge: { maxTurns: 40 },
2122
modelRole: "docs",
2223
systemPrompt: `You are BruckheimerDirector, a specialist in Corbits Code.

src/agent/directors/build/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const buildDirectorPackage: DirectorPackage = {
1515
optionalSkills: ["style", "philosophy", "typescript"],
1616
tools: { allow: BUILD_TOOLS },
1717
spawn: { maySpawn: false },
18+
tier: "leaf",
1819
nudge: { maxTurns: 60 },
1920
modelRole: "implement",
2021
systemPrompt: `You are BuildDirector, a specialist in Corbits Code.

src/agent/directors/critique/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const critiquePackage: DirectorPackage = {
1919
optionalSkills: ["style", "philosophy"],
2020
tools: { allow: REVIEW_TOOLS },
2121
spawn: { maySpawn: false },
22+
tier: "leaf",
2223
nudge: { maxTurns: 45 },
2324
modelRole: "review",
2425
systemPrompt: `You are CritiqueDirector, a specialist in Corbits Code.

src/agent/directors/draper/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const draperPackage: DirectorPackage = {
1818
// Read-only critique — product write tools not mounted.
1919
tools: { allow: REVIEW_TOOLS },
2020
spawn: { maySpawn: false },
21+
tier: "leaf",
2122
nudge: { maxTurns: 40 },
2223
modelRole: "review",
2324
systemPrompt: `You are DraperDirector, a specialist in Corbits Code.

src/agent/directors/emil/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const emilPackage: DirectorPackage = {
1818
// Critique only — write tools not mounted.
1919
tools: { allow: REVIEW_TOOLS },
2020
spawn: { maySpawn: false },
21+
tier: "leaf",
2122
nudge: { maxTurns: 40 },
2223
modelRole: "review",
2324
systemPrompt: `You are EmilDirector, a specialist in Corbits Code.

src/agent/directors/explore/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ FINDINGS SHAPE: Findings must be a scannable map — key paths, symbols, call fl
2424
OUT OF LANE → report Blockers naming the right director: build, plan, critique, greybeard, intern.`,
2525
tools: { allow: READ_TOOLS },
2626
spawn: { maySpawn: false },
27+
tier: "leaf",
2728
nudge: { maxTurns: 35 },
2829
modelRole: "explore",
2930
};

src/agent/directors/gaasbot/package.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const gaasbotPackage: DirectorPackage = {
1919
optionalSkills: ["philosophy"],
2020
tools: { allow: REVIEW_TOOLS },
2121
spawn: { maySpawn: false },
22+
tier: "leaf",
2223
nudge: { maxTurns: 35 },
2324
modelRole: "plan",
2425
systemPrompt: `You are GaasbotDirector, a specialist in Corbits Code.

0 commit comments

Comments
 (0)