Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Fixed in-process child sessions losing their admission-issued nesting depth and delegation limit. `SubagentChildPolicy` now carries both the admitted `depth` and the effective `maxSubagentDepth`, so the subagent executor can enforce the configured and inherited limits without relying on the removed process-environment bridge ([#2220](https://github.com/bastani-inc/atomic/pull/2220), regression from [#2205](https://github.com/bastani-inc/atomic/pull/2205)).

## [0.9.13-alpha.1] - 2026-08-05

### Added
Expand Down
3 changes: 2 additions & 1 deletion packages/coding-agent/docs/subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ subagent({ agent: "worker", task: "Implement it in the background.", progress: t

Child-safety boundaries are enforced by typed admission policy and the bundled subagent extension:

- Normal child sessions do not receive the `subagent` tool or the parent-only subagents skill.
- In-process child sessions load bundled extensions through normal discovery. The `subagent` tool may therefore be registered when the child's active tool selection permits it, including the default no-allowlist case; an explicit allowlist may omit it. Tool presence does not grant fanout. The bundled subagents skill remains parent-only and is stripped from child prompts, including fanout-authorized children.
- Child context is filtered to remove parent orchestration artifacts, old control/status messages, and prior parent `subagent` tool calls/results.
- Non-fanout children are instructed that they are not the parent orchestrator and must not propose or run subagents.
- Nested fanout is available only for explicitly authorized agents whose resolved tools include `subagent`. Authorized fanout children receive narrower instructions that limit delegation to the assigned fanout.
- Typed admission policy lets a non-fanout child use only `list`, `get`, `status`, and `doctor`; delegation, `resume`, and `interrupt` receive the fanout refusal. A management-restricted child is also refused `create`, `update`, and `delete`.
- The recursion guard has a hard maximum of five delegated subagent levels. The admitted depth policy may choose a lower value from `0` to `5`; deeper admission is refused rather than inherited from process environment state.

This keeps the parent session responsible for orchestration unless you deliberately choose a fanout-capable custom agent.
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -2341,7 +2341,7 @@ readonly excludedTools?: readonly string[];

`tools` is an allowlist across built-in and bundled extension tools; list every tool the stage should see. `excludedTools` and `noTools: "all"` still win.

The bundled `subagent` tool is available by default with the same five delegated-level depth guard as main chat. Bundled subagent definitions from `@bastani/subagents` are available to that tool. Explicitly list tools such as `subagent`, `web_search`, `fetch_content`, or `intercom` when using an allowlist; workflow stages running inside subagent child processes retain isolated resource discovery and the nested-depth guard.
The bundled `subagent` tool is available by default with the same five delegated-level depth guard as main chat. The in-process admission door carries each child’s issued depth and effective delegation limit in its typed child policy; the executor blocks delegation when that depth reaches the stricter of the locally configured maximum and the limit inherited from the parent and the child agent’s own `maxSubagentDepth`, and the Rust `SubagentControl` admission door rejects a child beyond the hard maximum of five. Neither value is carried through process environment. Bundled subagent definitions from `@bastani/subagents` are available to that tool. Explicitly list tools such as `subagent`, `web_search`, `fetch_content`, or `intercom` when using an allowlist; nested in-process child sessions load the bundled resources while suppressing the workflow extension lifecycle and retain the nested-depth guard.

Workflow stages use the same upstream-compatible `bash` tool as normal Atomic sessions. Enabled commands run through the configured shell with the stage process permissions. There is no command-text allow/deny option: expose or hide shell access with these tool fields, prefer narrow custom tools for repeatable operations, and use a container, VM, or other sandbox for stronger isolation.

Expand Down
7 changes: 7 additions & 0 deletions packages/coding-agent/src/core/extensions/context-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ export interface SubagentChildPolicy {
readonly fanoutAuthorized: boolean;
readonly inheritProjectContext: boolean;
readonly inheritSkills: boolean;
/** Current admitted in-process nesting depth; absent for top-level sessions. */
readonly depth?: number;
/**
* Effective delegation limit inherited from the parent and the child's own
* agent definition; absent when only the local limit applies.
*/
readonly maxSubagentDepth?: number;
/** Undefined preserves MCP configuration defaults; [] explicitly disables direct tools. */
readonly mcpDirectTools?: readonly string[];
/** Admission-issued identity/capability; never inherited through process environment. */
Expand Down
7 changes: 7 additions & 0 deletions packages/subagents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

### Fixed

- Fixed every `subagent` action being refused with "Subagent fanout is not authorized for this child." for a child without fanout authorization. The fanout check ran before the management branch, so the observing actions — `list`, `get`, `status`, and `doctor` — were rejected with a message about delegation, which they do not perform. Those four now pass the gate. Fanout authorization gates delegation and every management action that can start or continue agent execution: `resume` revives a child and `interrupt` is privileged control over a running one, so both now receive the fanout refusal instead of reaching their handlers. Mutating management (`create`, `update`, `delete`) is still refused for a management-restricted child, by the narrower gate that this bug had made unreachable. Registration is not authority: a child loads bundled extensions through normal discovery and may therefore have the `subagent` tool registered, while typed admission policy decides which of its actions run. The bundled subagents skill stays parent-only and is stripped from every child prompt, including fanout-authorized children ([#2220](https://github.com/bastani-inc/atomic/pull/2220), regression from [#2205](https://github.com/bastani-inc/atomic/pull/2205)).
- Fixed the in-process depth guard and nested workflow-stage children. Admission-issued child depth now travels in the typed policy into every single, parallel, chain, async, and resume path, so the executor can reject delegation at the configured limit while Rust admission keeps the hard five-level ceiling; the orphaned process-environment depth bridge and its self-fulfilling tests are gone. In-process children now load the bundled package resources needed to register `subagent`, so a nested child no longer starts with only the base built-in tools and no way to delegate, while workflow-stage children suppress only the workflow extension lifecycle ([#2220](https://github.com/bastani-inc/atomic/pull/2220), regression from [#2205](https://github.com/bastani-inc/atomic/pull/2205)).
- Fixed an agent's `maxSubagentDepth` being dropped at the in-process admission door. A child admitted from an agent whose definition tightened the limit received a policy carrying no maximum, so it could keep delegating as if only the global five-level ceiling applied. The effective limit — the stricter of the parent's limit and the child agent's own — now travels on the admitted child spec and policy, is reissued unchanged by a cold reload, and is applied by the executor's depth check alongside the local configuration. Admission also derives the limit from the agent definition when a caller supplies a child spec without one, so the door no longer issues an unbounded policy for an agent that declared a limit ([#2220](https://github.com/bastani-inc/atomic/pull/2220), regression from [#2205](https://github.com/bastani-inc/atomic/pull/2205)).
- Fixed a resumed foreground child losing the delegation limit its agent definition had narrowed. Retained resume re-derived the limit from the current stage or process configuration, so a child that ran under an agent maximum of 1 resumed with the configured maximum instead. The effective limit is now recorded per retained child — parallel and chain branches can each carry a different one — and reused on resume, so editing an agent definition between a run and its resume cannot widen that child's budget ([#2220](https://github.com/bastani-inc/atomic/pull/2220), regression from [#2205](https://github.com/bastani-inc/atomic/pull/2205)).

## [0.9.13-alpha.1] - 2026-08-05

### Breaking Changes
Expand Down
4 changes: 2 additions & 2 deletions packages/subagents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Use the optional prompt shortcuts below when you want the pattern to be repeatab

Packaged `planner`, `worker`, and `oracle` default to forked context when a launch omits `context`; pass `context: "fresh"` when you intentionally want a fresh child run.

Child-safety boundaries are enforced at runtime. Spawned child sessions do not register the `subagent` tool or receive the bundled `pi-subagents` skill unless the parent intentionally selected an explicit fanout agent whose resolved builtin `tools` includes `subagent`. Non-fanout children receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents; authorized fanout children get a narrower boundary that limits nested delegation to the assigned fanout. Forked child context filtering also removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent `subagent` tool-call/tool-result history) while preserving ordinary prose and unrelated tool calls/results.
Child-safety boundaries are enforced at runtime by typed admission policy. In-process child sessions load bundled extensions through normal discovery. The `subagent` tool may therefore be registered when the child's active tool selection permits it, including the default no-allowlist case; an explicit allowlist may omit it. Tool presence does not grant fanout: fanout is authorized only when the resolved builtin `tools` list includes `subagent`. Typed admission policy lets a non-fanout child use only `list`, `get`, `status`, and `doctor`; delegation, `resume`, and `interrupt` receive the fanout refusal. A management-restricted child is also refused `create`, `update`, and `delete`. The bundled `pi-subagents` skill remains parent-only and is stripped from child prompts, including fanout-authorized children. Non-fanout children receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents; authorized fanout children get a narrower boundary that limits nested delegation to the assigned fanout. Forked child context filtering also removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent `subagent` tool-call/tool-result history) while preserving ordinary prose and unrelated tool calls/results.

## Optional shortcuts

Expand Down Expand Up @@ -641,7 +641,7 @@ Missing skills do not fail execution. The result summary shows a warning.

### Bundled skill

The package bundles a `subagent` skill that is automatically available to the parent agent when the extension is installed. It is for the orchestrating parent only: child subagents never receive it unless explicitly authorized for fanout, and their context is filtered to strip parent-only orchestration instructions.
The package bundles a `subagent` skill that is automatically available to the parent agent when the extension is installed. It is for the orchestrating parent only: it is stripped from every child prompt, including fanout-authorized children, and child context is filtered to strip parent-only orchestration instructions. A child may still have the `subagent` tool registered; typed admission policy, not the skill, decides which of its actions are allowed.

What the bundled skill covers:
- **Delegation patterns**: when to launch which agent, whether to use single, parallel, chain, or async mode, and whether to use fresh or forked context
Expand Down
2 changes: 1 addition & 1 deletion packages/subagents/skills/subagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ For complex or risky changes, increase review and validation fanout when user in

For very large work, split into serial milestones instead of launching a swarm of writers. Each milestone gets one writer, a validation contract, fresh-context review, a fix pass, and parent approval before the next milestone starts. Use parallel subagents inside a milestone for read-only context, research, and review only.

Keep orchestration authority in the parent session. Child subagents should not launch more subagents, read this skill, or run their own orchestration loops unless the parent intentionally selected an explicit fanout agent whose resolved builtin `tools` includes `subagent` for that assigned fanout. Spawned non-fanout subagents do not receive the `subagent` skill, parent-only status/control/slash messages, prior parent `subagent` tool-call/tool-result artifacts, or the `subagent` extension tool. Child context filtering also strips old hidden orchestration-instruction messages when they appear in inherited history. Every child also receives a boundary instruction that says the parent owns orchestration, the child must not propose or run subagents unless explicitly authorized for fanout, and writer children must call real edit/write tools instead of printing pseudo tool calls. Pass children concrete role-specific work instead.
Keep orchestration authority in the parent session. Child subagents should not launch more subagents or run their own orchestration loops unless the parent intentionally selected an explicit fanout agent whose resolved builtin `tools` includes `subagent` for that assigned fanout. This skill is parent-only: it is stripped from every child prompt, including fanout-authorized children. A child may still have the `subagent` extension tool registered, because bundled extensions load through normal discovery; registration is not authority. Typed admission policy lets a non-fanout child use only `list`, `get`, `status`, and `doctor`, and refuses delegation, `resume`, and `interrupt`. Spawned children also do not receive parent-only status/control/slash messages or prior parent `subagent` tool-call/tool-result artifacts, and child context filtering strips old hidden orchestration-instruction messages when they appear in inherited history. Every child also receives a boundary instruction that says the parent owns orchestration, the child must not propose or run subagents unless explicitly authorized for fanout, and writer children must call real edit/write tools instead of printing pseudo tool calls. Pass children concrete role-specific work instead.

1. Clarify only when needed. Use existing context first; gather missing code or research context selectively, then ask only unresolved questions that materially affect scope, completion criteria, constraints, or non-goals.
2. Define the validation contract. State completion expectations before implementation: expected behavior, checks to run, user flows to exercise, and evidence required in the writer handoff. For UI, CLI, integration, or workflow changes, include at least one validator angle that uses the product the way a user would rather than only reading code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export async function runDynamicParallelChainStep(input: {
foregroundControl: context.foregroundControl,
nestedRoute: context.params.nestedRoute,
maxSubagentDepth: context.params.maxSubagentDepth,
parentDepth: context.params.parentDepth,
workflowStageSubagentGuard: context.params.workflowStageSubagentGuard,
runSync: context.executeRunSync,
onDetachedExit: context.onDetachedExit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export async function runParallelChainTasks(input: ParallelChainRunInput): Promi
outputPath,
outputMode: behavior.outputMode,
maxSubagentDepth,
parentDepth: input.parentDepth,
workflowStageSubagentGuard: input.workflowStageSubagentGuard,
workflowSessionMetadata: workflowSessionMetadataFromContext(input.ctx),
controlConfig: input.controlConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export async function runStaticParallelChainStep(input: {
nestedRoute: context.params.nestedRoute,
worktreeSetup,
maxSubagentDepth: context.params.maxSubagentDepth,
parentDepth: context.params.parentDepth,
workflowStageSubagentGuard: context.params.workflowStageSubagentGuard,
runSync: context.executeRunSync,
onDetachedExit: (index, result) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export async function runSequentialChainStep(input: {
outputPath,
outputMode: behavior.outputMode,
maxSubagentDepth,
parentDepth: context.params.parentDepth,
workflowStageSubagentGuard: context.params.workflowStageSubagentGuard,
workflowSessionMetadata: workflowSessionMetadataFromContext(context.params.ctx),
controlConfig: context.controlConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface ChainExecutionParams {
chainDir?: string;
dynamicFanoutMaxItems?: number;
maxSubagentDepth: number;
parentDepth?: number;
workflowStageSubagentGuard?: boolean;
nestedRoute?: NestedRouteInfo;
worktreeSetupHook?: string;
Expand Down Expand Up @@ -146,6 +147,7 @@ export interface ParallelChainRunInput {
dynamicGroupStatuses?: ChainExecutionDetailsInput["dynamicGroupStatuses"];
worktreeSetup?: WorktreeSetup;
maxSubagentDepth: number;
parentDepth?: number;
workflowStageSubagentGuard?: boolean;
nestedRoute?: NestedRouteInfo;
runSync: RunSyncDependency;
Expand Down
3 changes: 2 additions & 1 deletion packages/subagents/src/runs/foreground/inprocess-run-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export async function runSingleInProcess(
const orchestrationContext = workflowOrchestrationContext(options);
const parent: ParentContext = {
path: options.runId,
depth: 0,
depth: options.parentDepth ?? 0,
...(options.intercomGroup ? { intercomGroup: options.intercomGroup } : {}),
...(orchestrationContext ? { orchestrationContext } : {}),
};
Expand Down Expand Up @@ -204,6 +204,7 @@ export async function runSingleInProcess(
cwd,
testSession: testSession,
sessionFile: options.sessionFile,
...(options.maxSubagentDepth === undefined ? {} : { maxSubagentDepth: options.maxSubagentDepth }),
structuredOutput: options.structuredOutput
? { schema: options.structuredOutput.schema, outputPath: options.structuredOutput.outputPath }
: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export async function runAsyncPath(
availableModels,
knownModelProviders,
maxSubagentDepth: resolveChildMaxSubagentDepth(depthPolicy.maxSubagentDepth, agent.maxSubagentDepth),
parentDepth: data.parentDepth,
workflowStageSubagentGuard: depthPolicy.workflowStageSubagentGuard,
worktreeSetupHook: deps.config.worktreeSetupHook,
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
Expand Down
Loading