fix(skills): reconcile team-template skill names so role bindings actually bind
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 10s
ci / frontend (push) Failing after 19s
ci / e2e (push) Skipped
ci / publish (push) Skipped

Every skill reference in every team template was failing to resolve. The
TOMLs used snake_case slugs (`write_rust`, `index_selection`) while the
authored skills under `skills/**/*.md` declare kebab-case names
(`write-rust-current-edition`, `postgres-index-selection`), so
`get_by_name` missed on all of them: 128 skipped bindings across 51
distinct names, and no mission agent received any of its template's
skills.

The mirror-image half was equally invisible: ten authored skills —
including `int-xx-marker-protocol`, whose own `when_to_use` says "pin on
every coding role" — were referenced by no role at all, so nothing could
ever load them.

- Rename the 14 references that have authored skills behind them, and
  dedupe the two that now collapse onto the commit-protocol skill.
- Attach all ten orphaned skills to the roles their `when_to_use` names.
  All 23 authored skills now reach at least one role.
- Aggregate the loader's per-name logging into one line per template.
  The old per-name spam is why this went unnoticed; a bound/unresolved
  count is noticeable. References with no authored skill are kept and
  listed — they record intent for skills not yet written.
- Two regression tests: no authored skill may be orphaned, and every
  authored skill must be referenced by its exact name.

Also clears the two standing clippy warnings: group
`mint_team_from_template`'s eight positional args into `TeamMint`, and
make `provider_alias_for` branch on `is_exact_provider_match` so the
helper is live code and the two can't disagree about what counts as an
exact family match.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-31 19:47:18 -07:00
co-authored by Claude Opus 5
parent 95bd65540c
commit 2eb0880fc0
14 changed files with 214 additions and 77 deletions
+29 -10
View File
@@ -186,13 +186,15 @@ pub async fn on_launch(
mission.title, purpose, template.template.name mission.title, purpose, template.template.name
); );
let team_id = mint_team_from_template( let team_id = mint_team_from_template(
TeamMint {
pool, pool,
workspace_id, workspace_id,
user_id, user_id,
provisioner.as_ref(), provisioner: provisioner.as_ref(),
&template, template: &template,
&team_name, team_name: &team_name,
"claude-sonnet-5", default_model: "claude-sonnet-5",
},
&mut provisioned_claws, &mut provisioned_claws,
) )
.await?; .await?;
@@ -298,16 +300,33 @@ pub async fn on_launch(
Ok(Some(team_id)) Ok(Some(team_id))
} }
async fn mint_team_from_template( /// The read-only inputs for minting a team. Grouped into a struct so the
pool: &PgPool, /// signature stays readable as the orchestrator accumulates context — the
/// growing positional list was also easy to mis-order at the call site,
/// since `team_name` and `default_model` are both `&str`.
struct TeamMint<'a> {
pool: &'a PgPool,
workspace_id: WorkspaceId, workspace_id: WorkspaceId,
user_id: cm_domain::UserId, user_id: cm_domain::UserId,
provisioner: Option<&RuntimeProvisioner>, provisioner: Option<&'a RuntimeProvisioner>,
template: &TeamTemplateDetail, template: &'a TeamTemplateDetail,
team_name: &str, team_name: &'a str,
default_model: &str, default_model: &'a str,
}
async fn mint_team_from_template(
mint: TeamMint<'_>,
provisioned_claws: &mut Vec<cm_domain::AgentId>, provisioned_claws: &mut Vec<cm_domain::AgentId>,
) -> Result<Uuid, String> { ) -> Result<Uuid, String> {
let TeamMint {
pool,
workspace_id,
user_id,
provisioner,
template,
team_name,
default_model,
} = mint;
// Build the topology graph from role slots so the team's `graph` // Build the topology graph from role slots so the team's `graph`
// NOT NULL column is satisfied + downstream topology executors // NOT NULL column is satisfied + downstream topology executors
// have a valid shape to iterate over. // have a valid shape to iterate over.
+15 -8
View File
@@ -29,14 +29,15 @@ pub fn claw_alias(claw_id: Uuid) -> String {
pub fn provider_alias_for(model: &str) -> &'static str { pub fn provider_alias_for(model: &str) -> &'static str {
let m = model.trim().to_ascii_lowercase(); let m = model.trim().to_ascii_lowercase();
// Prefix families first (covers claude-sonnet-5, claude-opus-4-8, // Prefix families first (covers claude-sonnet-5, claude-opus-4-8,
// claude-haiku-4-5-*, etc.) then explicit aliases. // claude-haiku-4-5-*, etc.) then explicit aliases. `is_exact_provider_match`
// decides what "its own family" means, so the two can't drift apart.
if is_exact_provider_match(&m) {
if m.starts_with("claude") { if m.starts_with("claude") {
return "anthropic.default"; return "anthropic.default";
} }
if m.starts_with("gemini") { if m.starts_with("gemini") {
return "gemini.default"; return "gemini.default";
} }
if m.starts_with("llama") || m.starts_with("groq") {
return "groq.default"; return "groq.default";
} }
match m.as_str() { match m.as_str() {
@@ -69,12 +70,13 @@ pub fn provider_alias_for(model: &str) -> &'static str {
} }
} }
/// Whether `provider_alias_for` resolved this model to its own family, or /// Whether `provider_alias_for` resolves this model to its own family, or
/// substituted a different one. /// substitutes a different one.
/// ///
/// Callers that surface a model choice to a user can use this to say so rather /// `provider_alias_for` branches on this, so it is the single definition of
/// than letting the substitution be discovered on an invoice. Kept alongside /// "its own family". Also public for callers that surface a model choice to a
/// `provider_alias_for` so the two can't disagree about what counts as a match. /// user, so a substitution can be said out loud rather than discovered on an
/// invoice.
pub fn is_exact_provider_match(model: &str) -> bool { pub fn is_exact_provider_match(model: &str) -> bool {
let m = model.trim().to_ascii_lowercase(); let m = model.trim().to_ascii_lowercase();
m.starts_with("claude") m.starts_with("claude")
@@ -350,7 +352,12 @@ mod tests {
"{m} resolves to anthropic.default by substitution, not by family" "{m} resolves to anthropic.default by substitution, not by family"
); );
} }
for m in ["claude-sonnet-5", "gemini-2.5-flash", "groq-llama", "llama3"] { for m in [
"claude-sonnet-5",
"gemini-2.5-flash",
"groq-llama",
"llama3",
] {
assert!( assert!(
super::is_exact_provider_match(m), super::is_exact_provider_match(m),
"{m} should resolve to its own family" "{m} should resolve to its own family"
+117 -6
View File
@@ -167,6 +167,13 @@ async fn load_one(pool: &PgPool, path: &std::path::Path) -> Result<String, Strin
{ {
eprintln!("team_template_loader: clear_template_role_skills({key}) failed: {e}"); eprintln!("team_template_loader: clear_template_role_skills({key}) failed: {e}");
} }
// Unresolved names are aggregated into one line per template rather than
// logged individually: the per-name spam (128 lines at last count) scrolled
// past unread for long enough that every template's skill bindings were
// silently empty, because the TOMLs used snake_case slugs while the authored
// skills in `skills/**/*.md` use kebab-case names. A count is noticeable.
let mut unresolved: Vec<String> = Vec::new();
let mut bound = 0usize;
for role in &file.roles { for role in &file.roles {
for (idx, skill_name) in role.skills.iter().enumerate() { for (idx, skill_name) in role.skills.iter().enumerate() {
match cm_db::repo::skills_catalog::get_by_name(pool, None, skill_name).await { match cm_db::repo::skills_catalog::get_by_name(pool, None, skill_name).await {
@@ -188,20 +195,124 @@ async fn load_one(pool: &PgPool, path: &std::path::Path) -> Result<String, Strin
"team_template_loader: attach skill {skill_name} → {key}.{}: {e}", "team_template_loader: attach skill {skill_name} → {key}.{}: {e}",
role.slot role.slot
); );
} else {
bound += 1;
} }
} }
Ok(None) => { Ok(None) => unresolved.push(format!("{}.{skill_name}", role.slot)),
eprintln!(
"team_template_loader: skill '{skill_name}' referenced by {key}.{} not found — skipped",
role.slot
);
}
Err(e) => { Err(e) => {
eprintln!("team_template_loader: lookup skill '{skill_name}' failed: {e}"); eprintln!("team_template_loader: lookup skill '{skill_name}' failed: {e}");
} }
} }
} }
} }
if unresolved.is_empty() {
eprintln!("team_template_loader: {key} — {bound} role skills bound");
} else {
eprintln!(
"team_template_loader: {key} — {bound} role skills bound, {} unresolved (no such skill authored under skills/): {}",
unresolved.len(),
unresolved.join(", "),
);
}
Ok(key) Ok(key)
} }
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::path::{Path, PathBuf};
fn repo_root() -> PathBuf {
// crates/cm-api → repo root
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.expect("repo root above crates/cm-api")
.to_path_buf()
}
fn authored_skill_names(dir: &Path, out: &mut HashSet<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
authored_skill_names(&p, out);
} else if p.extension().is_some_and(|x| x == "md") {
let body = std::fs::read_to_string(&p).unwrap_or_default();
if let Some(name) = body
.lines()
.find_map(|l| l.strip_prefix("name:").map(str::trim))
{
out.insert(name.to_string());
}
}
}
}
fn referenced_skill_names() -> HashSet<String> {
let mut refs = HashSet::new();
let dir = repo_root().join("templates/teams");
for e in std::fs::read_dir(&dir)
.expect("templates/teams readable")
.flatten()
{
let body = std::fs::read_to_string(e.path()).unwrap_or_default();
let parsed: toml::Value = match body.parse() {
Ok(v) => v,
Err(e) => panic!("{:?} is not valid TOML: {e}", e),
};
if let Some(roles) = parsed.get("roles").and_then(|r| r.as_array()) {
for role in roles {
if let Some(skills) = role.get("skills").and_then(|s| s.as_array()) {
refs.extend(skills.iter().filter_map(|s| s.as_str()).map(str::to_string));
}
}
}
}
refs
}
/// Every skill authored under `skills/**/*.md` must be reachable by at
/// least one team role.
///
/// This is the half of the naming drift that was invisible: the TOMLs used
/// snake_case slugs (`write_rust`) while the authored skills use kebab-case
/// names (`write-rust-current-edition`), so `get_by_name` missed on every
/// lookup — no role got any skill, and ten authored skills were reachable
/// by nobody. Both halves are silent at runtime; only a test catches them.
#[test]
fn every_authored_skill_is_referenced_by_some_role() {
let mut authored = HashSet::new();
authored_skill_names(&repo_root().join("skills"), &mut authored);
assert!(
!authored.is_empty(),
"no authored skills found — check the skills/ path"
);
let referenced = referenced_skill_names();
let orphans: Vec<_> = authored.difference(&referenced).cloned().collect();
assert!(
orphans.is_empty(),
"authored skills no team role references (they can never reach an agent): {orphans:?}"
);
}
/// A referenced name that matches no authored skill binds to nothing. Some
/// are deliberately aspirational, so this asserts the *resolvable* ones
/// stay resolvable rather than demanding every name exist.
#[test]
fn referenced_skills_that_exist_use_the_authored_spelling() {
let mut authored = HashSet::new();
authored_skill_names(&repo_root().join("skills"), &mut authored);
let referenced = referenced_skill_names();
let resolvable = referenced.intersection(&authored).count();
assert_eq!(
resolvable,
authored.len(),
"every authored skill should be referenced by its exact name",
);
}
}
+5 -5
View File
@@ -10,7 +10,7 @@ version = 1
[[roles]] [[roles]]
slot = "api_designer" slot = "api_designer"
order_idx = 0 order_idx = 0
skills = ["decompose_int_items", "openapi_schema", "small_focused_commits"] skills = ["decompose-int-items", "openapi_schema", "small-focused-commits", "api-pagination-day-1"]
system_prompt = """ system_prompt = """
You are the API DESIGNER of a Backend team. You are the API DESIGNER of a Backend team.
@@ -31,7 +31,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "db_engineer" slot = "db_engineer"
order_idx = 1 order_idx = 1
skills = ["postgres_migrations", "index_selection", "explain_analyze", "write_rust", "workspace_repo_edit"] skills = ["postgres-migrations-forward-only", "postgres-index-selection", "explain_analyze", "write-rust-current-edition", "workspace-repo-commit-protocol"]
system_prompt = """ system_prompt = """
You are the DB ENGINEER of a Backend team. You are the DB ENGINEER of a Backend team.
@@ -55,7 +55,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "coder" slot = "coder"
order_idx = 2 order_idx = 2
skills = ["write_rust", "cargo_build", "cargo_test", "workspace_repo_edit", "git_commit_protocol"] skills = ["write-rust-current-edition", "cargo_build", "cargo-test-driven-development", "workspace-repo-commit-protocol", "int-xx-marker-protocol", "rust-error-handling", "rust-async-tokio-idioms"]
system_prompt = """ system_prompt = """
You are the CODER of a Backend team. You are the CODER of a Backend team.
@@ -68,7 +68,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "tester" slot = "tester"
order_idx = 3 order_idx = 3
skills = ["cargo_test", "integration_tests_pg", "coverage_report"] skills = ["cargo-test-driven-development", "integration_tests_pg", "coverage_report", "tdd-red-green-refactor"]
system_prompt = """ system_prompt = """
You are the TESTER of a Backend team. You are the TESTER of a Backend team.
@@ -81,7 +81,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "committer" slot = "committer"
order_idx = 4 order_idx = 4
skills = ["git_commit_protocol", "small_focused_commits"] skills = ["workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the COMMITTER. Same protocol as rust_sdlc: only run when tests You are the COMMITTER. Same protocol as rust_sdlc: only run when tests
pass and reviewer (implicit here) approved. Emit COMPLETED: INT-<NN>. pass and reviewer (implicit here) approved. Emit COMPLETED: INT-<NN>.
+4 -4
View File
@@ -10,7 +10,7 @@ version = 1
[[roles]] [[roles]]
slot = "designer" slot = "designer"
order_idx = 0 order_idx = 0
skills = ["decompose_int_items", "design_system_check", "a11y_checklist"] skills = ["decompose-int-items", "design_system_check", "a11y_checklist"]
system_prompt = """ system_prompt = """
You are the DESIGNER of a Frontend team. You are the DESIGNER of a Frontend team.
@@ -34,7 +34,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "coder" slot = "coder"
order_idx = 1 order_idx = 1
skills = ["write_typescript_react", "tailwind_idioms", "workspace_repo_edit", "git_commit_protocol"] skills = ["write_typescript_react", "tailwind-v4-idioms", "workspace-repo-commit-protocol", "int-xx-marker-protocol", "component-4-state-model", "react-19-server-components"]
system_prompt = """ system_prompt = """
You are the CODER of a Frontend team. You are the CODER of a Frontend team.
@@ -53,7 +53,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "tester" slot = "tester"
order_idx = 2 order_idx = 2
skills = ["playwright_e2e", "vitest_unit", "a11y_axe"] skills = ["playwright_e2e", "vitest_unit", "a11y_axe", "tdd-red-green-refactor"]
system_prompt = """ system_prompt = """
You are the TESTER of a Frontend team. You are the TESTER of a Frontend team.
@@ -65,7 +65,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "committer" slot = "committer"
order_idx = 3 order_idx = 3
skills = ["git_commit_protocol", "small_focused_commits"] skills = ["workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the COMMITTER. Only run on green tests + a11y pass. Emit You are the COMMITTER. Only run on green tests + a11y pass. Emit
COMPLETED: INT-<NN>. COMPLETED: INT-<NN>.
+4 -4
View File
@@ -10,7 +10,7 @@ version = 1
[[roles]] [[roles]]
slot = "arch_analyst" slot = "arch_analyst"
order_idx = 0 order_idx = 0
skills = ["decompose_int_items", "gpu_arch_reference", "roofline_model"] skills = ["decompose-int-items", "gpu-coalescing-and-occupancy", "roofline-model"]
system_prompt = """ system_prompt = """
You are the ARCHITECTURE ANALYST of a GPU team. You are the ARCHITECTURE ANALYST of a GPU team.
@@ -32,7 +32,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "kernel_author" slot = "kernel_author"
order_idx = 1 order_idx = 1
skills = ["write_cuda", "write_metal", "write_rocm", "write_rust_ffi", "workspace_repo_edit", "git_commit_protocol"] skills = ["write_cuda", "write_metal", "write_rocm", "write_rust_ffi", "workspace-repo-commit-protocol"]
system_prompt = """ system_prompt = """
You are the KERNEL AUTHOR of a GPU team. You are the KERNEL AUTHOR of a GPU team.
@@ -65,7 +65,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "coder" slot = "coder"
order_idx = 3 order_idx = 3
skills = ["write_rust", "cargo_build", "cargo_test", "workspace_repo_edit"] skills = ["write-rust-current-edition", "cargo_build", "cargo-test-driven-development", "workspace-repo-commit-protocol", "int-xx-marker-protocol"]
system_prompt = """ system_prompt = """
You are the RUST-SIDE CODER. Integrate the kernel + FFI into the Rust You are the RUST-SIDE CODER. Integrate the kernel + FFI into the Rust
library, add safe wrappers, and expose ergonomic APIs. Own the library, add safe wrappers, and expose ergonomic APIs. Own the
@@ -76,7 +76,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "committer" slot = "committer"
order_idx = 4 order_idx = 4
skills = ["git_commit_protocol", "small_focused_commits"] skills = ["workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the COMMITTER. Only run when the kernel meets the roofline You are the COMMITTER. Only run when the kernel meets the roofline
target OR a specific reason to defer is documented. target OR a specific reason to defer is documented.
+4 -4
View File
@@ -10,7 +10,7 @@ version = 1
[[roles]] [[roles]]
slot = "designer" slot = "designer"
order_idx = 0 order_idx = 0
skills = ["decompose_int_items", "ios_hig_check", "material_you_check"] skills = ["decompose-int-items", "ios_hig_check", "material_you_check"]
system_prompt = """ system_prompt = """
You are the DESIGNER of a Mobile team. You are the DESIGNER of a Mobile team.
@@ -28,7 +28,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "coder" slot = "coder"
order_idx = 1 order_idx = 1
skills = ["write_typescript_react_native", "expo_managed_workflow", "workspace_repo_edit", "git_commit_protocol"] skills = ["write_typescript_react_native", "expo-managed-vs-bare", "workspace-repo-commit-protocol", "int-xx-marker-protocol", "component-4-state-model", "rn-flashlist-perf"]
system_prompt = """ system_prompt = """
You are the CODER of a Mobile team. You are the CODER of a Mobile team.
@@ -46,7 +46,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "tester" slot = "tester"
order_idx = 2 order_idx = 2
skills = ["detox_e2e", "jest_unit", "ios_simulator_check", "android_emulator_check"] skills = ["detox_e2e", "jest_unit", "ios_simulator_check", "android_emulator_check", "tdd-red-green-refactor"]
system_prompt = """ system_prompt = """
You are the TESTER of a Mobile team. You are the TESTER of a Mobile team.
@@ -59,7 +59,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "committer" slot = "committer"
order_idx = 3 order_idx = 3
skills = ["git_commit_protocol", "small_focused_commits"] skills = ["workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the COMMITTER. Only run on green tests both platforms. You are the COMMITTER. Only run on green tests both platforms.
Emit COMPLETED: INT-<NN>. Emit COMPLETED: INT-<NN>.
+5 -5
View File
@@ -10,7 +10,7 @@ version = 1
[[roles]] [[roles]]
slot = "planner" slot = "planner"
order_idx = 0 order_idx = 0
skills = ["read_roadmap", "decompose_int_items", "estimate_effort", "small_focused_commits"] skills = ["read_roadmap", "decompose-int-items", "estimate_effort", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the PLANNER of a Rust SDLC team. You are the PLANNER of a Rust SDLC team.
@@ -43,7 +43,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "coder" slot = "coder"
order_idx = 1 order_idx = 1
skills = ["write_rust", "cargo_build", "cargo_test", "workspace_repo_edit", "small_focused_commits", "git_commit_protocol"] skills = ["write-rust-current-edition", "cargo_build", "cargo-test-driven-development", "workspace-repo-commit-protocol", "small-focused-commits", "int-xx-marker-protocol", "rust-error-handling", "rust-async-tokio-idioms", "react-19-server-components"]
system_prompt = """ system_prompt = """
You are the CODER of a Rust SDLC team. You are the CODER of a Rust SDLC team.
@@ -78,7 +78,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "tester" slot = "tester"
order_idx = 2 order_idx = 2
skills = ["cargo_test", "cargo_nextest", "coverage_report", "criterion_bench"] skills = ["cargo-test-driven-development", "cargo_nextest", "coverage_report", "criterion_bench", "tdd-red-green-refactor"]
system_prompt = """ system_prompt = """
You are the TESTER of a Rust SDLC team. You are the TESTER of a Rust SDLC team.
@@ -96,7 +96,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "reviewer" slot = "reviewer"
order_idx = 3 order_idx = 3
skills = ["code_review_checklist", "read_diff", "small_focused_commits"] skills = ["code-review-checklist", "read_diff", "small-focused-commits", "cargo-audit-workflow", "secret-scanning-gitleaks"]
system_prompt = """ system_prompt = """
You are the REVIEWER of a Rust SDLC team. You are the REVIEWER of a Rust SDLC team.
@@ -115,7 +115,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "committer" slot = "committer"
order_idx = 4 order_idx = 4
skills = ["git_commit_protocol", "workspace_repo_edit", "small_focused_commits"] skills = ["workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the COMMITTER of a Rust SDLC team. You are the COMMITTER of a Rust SDLC team.
+3 -3
View File
@@ -10,7 +10,7 @@ version = 1
[[roles]] [[roles]]
slot = "scene_designer" slot = "scene_designer"
order_idx = 0 order_idx = 0
skills = ["decompose_int_items", "scene_graph_planning"] skills = ["decompose-int-items", "scene_graph_planning"]
system_prompt = """ system_prompt = """
You are the SCENE DESIGNER of a three.js team. You are the SCENE DESIGNER of a three.js team.
@@ -28,7 +28,7 @@ brain_seed = """
[[roles]] [[roles]]
slot = "coder" slot = "coder"
order_idx = 1 order_idx = 1
skills = ["write_typescript", "threejs_idioms", "workspace_repo_edit", "git_commit_protocol"] skills = ["write_typescript", "threejs-perf-and-teardown", "workspace-repo-commit-protocol", "int-xx-marker-protocol"]
system_prompt = """ system_prompt = """
You are the CODER of a three.js team. You are the CODER of a three.js team.
@@ -69,7 +69,7 @@ brain_seed = ""
[[roles]] [[roles]]
slot = "committer" slot = "committer"
order_idx = 4 order_idx = 4
skills = ["git_commit_protocol", "small_focused_commits"] skills = ["workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the COMMITTER. Only run when perf targets met. Emit You are the COMMITTER. Only run when perf targets met. Emit
COMPLETED: INT-<NN>. COMPLETED: INT-<NN>.