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
+32 -13
View File
@@ -186,13 +186,15 @@ pub async fn on_launch(
mission.title, purpose, template.template.name
);
let team_id = mint_team_from_template(
pool,
workspace_id,
user_id,
provisioner.as_ref(),
&template,
&team_name,
"claude-sonnet-5",
TeamMint {
pool,
workspace_id,
user_id,
provisioner: provisioner.as_ref(),
template: &template,
team_name: &team_name,
default_model: "claude-sonnet-5",
},
&mut provisioned_claws,
)
.await?;
@@ -298,16 +300,33 @@ pub async fn on_launch(
Ok(Some(team_id))
}
async fn mint_team_from_template(
pool: &PgPool,
/// The read-only inputs for minting a team. Grouped into a struct so the
/// 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,
user_id: cm_domain::UserId,
provisioner: Option<&RuntimeProvisioner>,
template: &TeamTemplateDetail,
team_name: &str,
default_model: &str,
provisioner: Option<&'a RuntimeProvisioner>,
template: &'a TeamTemplateDetail,
team_name: &'a str,
default_model: &'a str,
}
async fn mint_team_from_template(
mint: TeamMint<'_>,
provisioned_claws: &mut Vec<cm_domain::AgentId>,
) -> 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`
// NOT NULL column is satisfied + downstream topology executors
// have a valid shape to iterate over.