Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dfbcba4
feat(synth): deny subagent spawning for specialist agents
Oct 27, 2026
f4a9074
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 30, 2026
2840a6e
docs(agents): say that no specialist delegates and scope the spawn block
Oct 30, 2026
7e14682
test(synth): reject every spawn-tool alias in specialist specs
Oct 30, 2026
a1117bc
feat(synth): warn when a specialist's spawn grants are stripped
Oct 30, 2026
11fc362
chore(verify): record test results for the #124 review fixes
Oct 30, 2026
89a0a7a
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 30, 2026
2eec7f5
style(synth): move the frontmatter regression comment back onto its test
Oct 30, 2026
1d32adc
fix(synth): stop claiming synth strips specialist spawn grants
Oct 30, 2026
8a790e9
docs(agents): cite only the kiro-cli version the excludedTools probe …
Oct 30, 2026
221daaa
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 30, 2026
8f546fd
docs(guide): say step 6 runs through konductor's handoff routing
Oct 30, 2026
88584b5
test(agents): check kiroCli.allowedTools for spawn grants
Oct 30, 2026
b38c57f
docs(changelog): open the spawn-tool entry with what holds on every r…
Oct 30, 2026
ba82e1c
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 30, 2026
90b964c
fix(sops): return k-code-review-workflow step 6 as a handoff instead …
Oct 30, 2026
9d436d6
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 30, 2026
2b7294d
fix(sop): say how a run resumed at code review step 6 finishes
Oct 30, 2026
7c1363e
docs: say how to finish a code review report started without konductor
Oct 30, 2026
7523f79
fix(sop): carry the code review's parameters and file counts in the s…
Oct 31, 2026
c77bb0d
fix(sop): name only the constraints a run resumed at step 6 applies
Oct 31, 2026
cd89b6e
docs(user-guide): route a direct-start adversarial pass through kondu…
Oct 31, 2026
fccf74a
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 31, 2026
aabba29
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 31, 2026
0d9003f
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 31, 2026
0e76c9f
docs(sop): carry cr_url, the resolved base branch and the Step 1 diff…
Oct 31, 2026
bcd509f
docs(guide): route the standalone adversarial pass through konductor
Oct 31, 2026
422a82d
docs(agents): rewrap the step 6 paragraph to the page's width
Oct 31, 2026
96ce50a
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 31, 2026
63f3c71
Merge branch 'feat/specialists-return-routing' into feat/deny-special…
Oct 31, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ not per individual commit.
fan-out. When the note or split names an SOP step to resume at, the orchestrator returns the
results to that specialist to finish the SOP. A specialist that runs an SOP's independence step
itself says so in its output.
- Specialist specs no longer grant any agent-spawning tool. Kiro CLI v3 and Claude Code
configs block the runtime's spawn tools explicitly, so a specialist cannot call one even when
a prompt asks it to. The block does not cover shell: a specialist with shell access could
still run `kiro-cli` or `claude`. The Kiro CLI v3 block uses `excludedTools`, tested with
kiro-cli 2.35.0. Kiro CLI v2 output is not covered yet: its `@builtin` still
includes the `subagent` tool. `k-code-review-workflow` step 6 now returns the adversarial
pass as a handoff for `k-architect` instead of skipping it.
- Orchestrators delegate a step they can't do to the owning specialist and may run independent
parts in parallel, including same-type agents. Parallel write agents must own disjoint files.
- Agents ignore planted instructions; `.konductor/memory/` ranks with AGENTS.md, tracked or not,
Expand Down
5 changes: 3 additions & 2 deletions agent-sops/k-code-review-workflow.sop.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ Validate each finding to eliminate false positives. When in doubt, reject — fa

### 6. Adversarial Review

If `k-adversarial-pull-request-review` is available, spawn `k-architect` in adversarial mode with it, passing the same diff used in Step 1 as `diff_input`, to surface gaps missed by standard review.
Spawn `k-architect` in adversarial mode with `k-adversarial-pull-request-review`, passing the same diff used in Step 1 as `diff_input`, to surface gaps missed by standard review. If you cannot spawn `k-architect`, return step 6 as a handoff naming `k-architect`, `k-adversarial-pull-request-review`, this SOP, and step 6 as the step to resume at, per the core rules' specialist handoffs. Include the SOP parameters (`source_dir`, `review_type`, `output_file`, `cr_url` if given, and the base branch Step 1 resolved), the Step 1 diff or the command that reproduces it, the Step 2 file counts by category, and the Step 5 consolidated findings list in the handoff so the resumed run can merge into them and write the report. If the list is long, write it to `.konductor/handoff/k-developer-code-review.md` and name that path, per the `delegation-protocol` skill's Handoff Pattern.

**Constraints:**

- You MUST skip this step if `k-adversarial-pull-request-review` is unavailable, or if `review_type` is `frontend` only (adversarial review targets backend/infra gaps)
- If you were resumed at step 6 with `k-architect`'s findings, You MUST NOT spawn or hand off again. Apply the false-positive, merge and no-re-report constraints below to those findings and the Step 5 list from the handoff, then continue to Step 7.
- You MUST skip this step only if `review_type` is `frontend` only (adversarial review targets backend/infra gaps)
- You MUST pass the same diff used in Step 1 as `diff_input`
- If the k-code-review-workflow was invoked with a CR URL, You MUST also pass it to `k-adversarial-pull-request-review` as its `pr_url` parameter — that is the name that SOP declares, and passing `cr_url` instead leaves it unset so the review degrades to diff-only. Do not send it to `adversarial-cr-review`, a different SOP that declares `cr_url`; on agents where both are loadable, name the target by filename.
- You MUST apply the same false-positive criteria from Step 5 to adversarial findings before merging them
Expand Down
14 changes: 2 additions & 12 deletions agents/k-architect.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,12 @@
},
"context": {
"contextNames": ["k-core-rules.md"]
},
"subagents": {
"agentNames": ["k-developer", "k-quality-assurance"]
}
},
"clientConfig": {
"kiroCli": {
"welcomeMessage": "I'm your solutions architect. I produce system designs, API specifications, data models, and threat models, validating each artifact before it hands off to implementation. Bring me a requirement or a design problem and I'll work the architecture through with you.\n\nBuilt-in workflows — type any to run it:\n /k-design-doc-creation — produce a design doc ready for principal-engineer review\n /k-existing-design-review — review existing design artifacts across 10 dimensions before implementation\n /k-principal-engineer-design-review — run a pre-submission quality gate on a design doc\n /k-adversarial-pull-request-review — run an adversarial review that catches what standard code review misses",
"tools": ["@builtin", "subagent", "@aws-mcp"],
"tools": ["@builtin", "@aws-mcp"],
"allowedTools": [
"fs_read",
"fs_write",
Expand All @@ -89,12 +86,6 @@
"@aws-mcp/aws___list_regions",
"@aws-mcp/aws___get_regional_availability"
],
"toolsSettings": {
"subagent": {
"availableAgents": ["k-developer", "k-quality-assurance"],
"trustedAgents": ["k-developer", "k-quality-assurance"]
}
},
"hooks": {},
"resources": [
"file://AGENTS.md",
Expand All @@ -113,8 +104,7 @@
"WebSearch",
"TodoWrite",
"Skill",
"mcp__aws-mcp__*",
"Agent(k-developer, k-quality-assurance)"
"mcp__aws-mcp__*"
],
"skills": [
"constraints",
Expand Down
12 changes: 1 addition & 11 deletions agents/k-developer.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"clientConfig": {
"kiroCli": {
"welcomeMessage": "I'm your development engineer. I break features into tasks, implement backend and frontend code, review my own changes, validate infrastructure, and track progress. Hand me a spec or a feature and I'll build it.\n\nBuilt-in workflows — type any to run it:\n /k-code-cleanup — strip AI-generated slop from a branch before raising a CR\n /k-pre-cr-critique — run a fast, read-only pre-CR critique of local changes\n /k-codebase-analysis — produce a comprehensive codebase analysis report with diagrams\n /k-code-review-workflow — run a full multi-skill code review consolidated into one report",
"tools": ["@builtin", "subagent", "@aws-mcp"],
"tools": ["@builtin", "@aws-mcp"],
"allowedTools": [
"fs_read",
"fs_write",
Expand All @@ -75,16 +75,6 @@
"@aws-mcp/aws___list_regions",
"@aws-mcp/aws___get_regional_availability"
],
"toolsSettings": {
"subagent": {
"availableAgents": [
"k-quality-assurance"
],
"trustedAgents": [
"k-quality-assurance"
]
}
},
"hooks": {},
"resources": [
"file://AGENTS.md",
Expand Down
6 changes: 0 additions & 6 deletions agents/k-media-analyzer.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@
"welcomeMessage": "I interpret media that raw-text tools can't read — PDFs, images, and diagrams. Give me a file and tell me what to extract or describe.",
"tools": ["@builtin"],
"allowedTools": ["fs_read", "web_fetch"],
"toolsSettings": {
"subagent": {
"trustedAgents": [],
"availableAgents": []
}
},
"hooks": {},
"mcpServers": {},
"resources": [
Expand Down
8 changes: 1 addition & 7 deletions agents/k-product-manager.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,8 @@
"clientConfig": {
"kiroCli": {
"welcomeMessage": "I'm your product manager. I turn ideas into user stories, requirements summaries, and decision research, validating each artifact before handoff. Tell me the problem you're solving and who it's for.",
"tools": ["@builtin", "subagent"],
"tools": ["@builtin"],
"allowedTools": ["fs_read", "fs_write"],
"toolsSettings": {
"subagent": {
"trustedAgents": [],
"availableAgents": []
}
},
"hooks": {},
"mcpServers": {},
"resources": [
Expand Down
9 changes: 1 addition & 8 deletions agents/k-quality-assurance.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,8 @@
"clientConfig": {
"kiroCli": {
"welcomeMessage": "I'm your QA engineer. I analyze test coverage, plan end-to-end test strategies, generate security tests, and validate UI text. Point me at the code or feature you want tested.\n\nBuilt-in workflows — type any to run it:\n /k-test-coverage-review — review test coverage and plan E2E and security tests",
"tools": ["@builtin", "subagent"],
"tools": ["@builtin"],
"allowedTools": ["fs_read", "fs_write", "shell"],
"toolsSettings": {
"subagent": {
"trustedAgents": ["k-developer", "k-browser"],
"availableAgents": ["k-developer", "k-browser"]
}
},
"hooks": {},
"mcpServers": {},
"resources": [
Expand All @@ -61,7 +55,6 @@
"WebFetch",
"WebSearch",
"TodoWrite",
"Agent(k-developer, k-browser)",
"Skill"
],
"skills": [
Expand Down
8 changes: 1 addition & 7 deletions agents/k-researcher.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,8 @@
"clientConfig": {
"kiroCli": {
"welcomeMessage": "I gather information — external documentation, web resources, AWS docs, and (when configured) Slack. Ask me a question and I'll research it and report back with sources.",
"tools": ["@builtin", "subagent"],
"tools": ["@builtin"],
"allowedTools": ["fs_read"],
"toolsSettings": {
"subagent": {
"availableAgents": [],
"trustedAgents": []
}
},
"resources": [
"file://AGENTS.md",
"skill://.kiro/skills/ws-*/SKILL.md"
Expand Down
15 changes: 1 addition & 14 deletions agents/k-tpm.agent-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,12 @@
"kiroCli": {
"welcomeMessage": "I'm your technical program manager. I coordinate across projects — timelines, status reporting, risk tracking, and program-level decision documents. Tell me the program or the coordination problem.",
"tools": [
"@builtin",
"subagent"
"@builtin"
],
"allowedTools": [
"fs_read",
"fs_write"
],
"toolsSettings": {
"subagent": {
"trustedAgents": [
"k-developer",
"k-researcher"
],
"availableAgents": [
"k-developer",
"k-researcher"
]
}
},
"hooks": {},
"mcpServers": {},
"resources": [
Expand Down
98 changes: 95 additions & 3 deletions cli/konductor-rs/src/cli/synth/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use super::content_writers::{write_all_context, write_all_skills, write_all_sops
use super::kiro_cli_v2::write_sop_scopes_sidecar;
#[cfg(test)]
use super::kiro_cli_v2::SOP_SCOPES_SIDECAR_FILE;
use super::model::CanonicalModel;
use super::model::{is_specialist, CanonicalModel};
#[cfg(test)]
use super::model::{AuxiliaryFile, ContextDef, SkillDef, SopDef};
use super::parser::{ClaudeCliConfig, McpServerDef};
Expand Down Expand Up @@ -74,6 +74,8 @@ struct ClaudeAgentFrontmatter<'a> {
skills: &'a [String],
#[serde(rename = "allowedTools", skip_serializing_if = "is_empty_str_slice")]
allowed_tools: &'a [String],
#[serde(rename = "disallowedTools", skip_serializing_if = "<[_]>::is_empty")]
disallowed_tools: &'a [&'static str],
#[serde(skip_serializing_if = "IndexMap::is_empty")]
hooks: IndexMap<String, serde_yaml::Value>,
#[serde(rename = "mcpServers", skip_serializing_if = "Vec::is_empty")]
Expand Down Expand Up @@ -339,6 +341,16 @@ impl HarnessTransformer for ClaudeTransformer {
}
}

/// Claude Code tools that start another agent. `Task` is the older name
/// of `Agent`.
const SPAWN_TOOLS: [&str; 3] = ["Agent", "Task", "Workflow"];

/// Matches a bare spawn tool or its scoped form, e.g. `Agent(k-developer)`.
pub(crate) fn is_spawn_tool(tool: &str) -> bool {
let base = tool.split('(').next().unwrap_or(tool).trim();
SPAWN_TOOLS.contains(&base)
}

const CLAUDE_MODEL_FAMILIES: &[&str] = &["opus", "sonnet", "haiku", "fable"];
const CLAUDE_CODE_MODEL_ALIASES: &[&str] = &["inherit", "sonnet", "opus", "haiku"];

Expand Down Expand Up @@ -461,6 +473,9 @@ fn is_date(part: &str) -> bool {
/// `merge_mcp_servers` (see that function's own doc comment for the
/// precedence rule) before rendering, so the frontmatter's `mcpServers:`
/// key reflects both sources rather than only the Claude-specific one.
///
/// For a specialist (`k-*`), spawn tools are dropped from `tools`; with no
/// `tools` list, `disallowedTools` denies them instead.
fn render_agent_md(
name: &str,
config: &super::parser::AgentConfig,
Expand All @@ -481,13 +496,29 @@ fn render_agent_md(

let model = claude_model_id(&config.model);
let merged_mcp_servers = merge_mcp_servers(mcp_registry, &claude.mcp_servers);
let specialist = is_specialist(name);
let tools: Option<Vec<String>> = claude.tools.as_ref().map(|tools| {
tools
.iter()
.filter(|tool| !(specialist && is_spawn_tool(tool)))
.cloned()
.collect()
});
// A specialist with no `tools` list inherits every default tool,
// spawn tools included, so it needs an explicit deny instead.
let disallowed_tools: &[&str] = if specialist && tools.is_none() {
&SPAWN_TOOLS
} else {
&[]
};
let frontmatter = ClaudeAgentFrontmatter {
name,
description: &config.description,
model: &model,
tools: claude.tools.as_deref(),
tools: tools.as_deref(),
skills: &claude.skills,
allowed_tools: &claude.allowed_tools,
disallowed_tools,
hooks: hooks_to_yaml(&claude.hooks),
mcp_servers: render_mcp_servers(&merged_mcp_servers),
};
Expand Down Expand Up @@ -934,6 +965,67 @@ mod tests {
);
}

fn render_frontmatter(name: &str, tools: Option<Vec<&str>>) -> serde_yaml::Value {
let claude = ClaudeCliConfig {
tools: tools.map(|t| t.into_iter().map(String::from).collect()),
..Default::default()
};
let agent = agent_with_claude(name, claude);
let rendered = render_agent_md(
name,
&agent.config,
agent.client_config.claude_cli.as_ref().unwrap(),
&[],
&[],
&HashSet::new(),
&BTreeMap::new(),
)
.unwrap();
let frontmatter = rendered.splitn(3, "---\n").nth(1).unwrap();
serde_yaml::from_str(frontmatter).unwrap()
}

#[test]
fn render_agent_md_strips_spawn_tools_from_specialist() {
let parsed = render_frontmatter(
"k-example",
Some(vec![
"Read",
"Agent(k-developer, k-browser)",
"Task",
"Workflow",
"Skill",
]),
);
assert_eq!(
parsed["tools"],
serde_yaml::from_str::<serde_yaml::Value>("[Read, Skill]").unwrap()
);
assert!(parsed.get("disallowedTools").is_none());
}

#[test]
fn render_agent_md_denies_spawn_tools_for_specialist_without_tools_list() {
let parsed = render_frontmatter("k-example", None);
assert!(parsed.get("tools").is_none());
assert_eq!(
parsed["disallowedTools"],
serde_yaml::from_str::<serde_yaml::Value>("[Agent, Task, Workflow]").unwrap()
);
}

#[test]
fn render_agent_md_keeps_spawn_tools_for_orchestrator() {
let parsed = render_frontmatter("konductor", Some(vec!["Workflow", "Agent(k-developer)"]));
assert_eq!(
parsed["tools"],
serde_yaml::from_str::<serde_yaml::Value>("[Workflow, Agent(k-developer)]").unwrap()
);
assert!(render_frontmatter("konductor", None)
.get("disallowedTools")
.is_none());
}

/// Regression guard: an agent whose `claudeCli` sets
/// `allowedTools`/`hooks`/`mcpServers` must render all three as
/// their own frontmatter keys, grounded against the real Claude Code
Expand Down Expand Up @@ -1031,7 +1123,7 @@ mod tests {
)
.unwrap();

assert!(!rendered.contains("allowedTools:"), "got: {rendered}");
assert!(!rendered.contains("\nallowedTools:"), "got: {rendered}");
assert!(!rendered.contains("hooks:"), "got: {rendered}");
assert!(!rendered.contains("mcpServers:"), "got: {rendered}");
}
Expand Down
Loading
Loading