fix(teams): the wrong repo path was in the TEAM templates too

The `/workspace/repo` guard was written on 2026-08-19 against `skills/`
only. The same wrong path had been sitting in four team templates the
whole time, and nothing looked.

`rust_sdlc` is the default team for five of the six workflow recipes. Its
CODER was told "your working directory is /workspace/repo. All edits
happen there." Its COMMITTER was told to `cd /workspace/repo`. The
platform mounts /mission/repo — `stamp_workspace_paths` pins it there.
Same for the frontend, three.js and mobile coders.

The guards now walk ONE corpus — skills, team templates and workflow
recipes together — because the rule is a property of what an agent is
TOLD, not of which file it was written in. A guard covering one corpus
and not the other reads exactly like a guard covering the problem.
Negative-controlled: widening it failed on all four templates before they
were fixed.

Two more defects in the same committer prompt, both found by reading it:

  - `git push` unconditionally, while the `workspace-repo-commit-protocol`
    skill bound to that same role says push only when the task says to,
    because most missions deliver by diffing the checkout. The role prompt
    and its own skill contradicted each other in one prompt.
  - `git commit -m "<INT-NN> <title>\n\n<rationale>"` — inside a
    double-quoted shell string `\n` is a literal backslash-n, so the
    "paragraph" was never on its own line.

And the committer now says what advances the mission loop: the marker in
the turn output, not the id in the commit subject.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
This commit is contained in:
Omar Sobh
2026-08-21 09:28:41 -07:00
co-authored by Claude Opus 5
parent 6f2b0a8f43
commit 4f4ce34203
5 changed files with 69 additions and 35 deletions
+43 -20
View File
@@ -175,33 +175,56 @@ mod tests {
mod contradiction_tests { mod contradiction_tests {
use std::path::PathBuf; use std::path::PathBuf;
fn skills_root() -> PathBuf { fn repo_root(rel: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../skills") .join("../..")
.join(rel)
.canonicalize() .canonicalize()
.expect("skills dir") .unwrap_or_else(|e| panic!("{rel}: {e}"))
} }
fn all_skills() -> Vec<(String, String)> { fn walk_ext(dir: &std::path::Path, ext: &str, out: &mut Vec<(String, String)>) {
fn walk(dir: &std::path::Path, out: &mut Vec<(String, String)>) { for e in std::fs::read_dir(dir).expect("read dir") {
for e in std::fs::read_dir(dir).expect("read skills dir") {
let p = e.expect("entry").path(); let p = e.expect("entry").path();
if p.is_dir() { if p.is_dir() {
walk(&p, out); walk_ext(&p, ext, out);
} else if p.extension().and_then(|x| x.to_str()) == Some("md") { } else if p.extension().and_then(|x| x.to_str()) == Some(ext) {
out.push(( out.push((
p.file_name().unwrap().to_string_lossy().to_string(), p.file_name().unwrap().to_string_lossy().to_string(),
std::fs::read_to_string(&p).expect("read skill"), std::fs::read_to_string(&p).expect("read file"),
)); ));
} }
} }
} }
/// Skill bodies alone.
fn all_skills() -> Vec<(String, String)> {
let mut out = Vec::new(); let mut out = Vec::new();
walk(&skills_root(), &mut out); walk_ext(&repo_root("skills"), "md", &mut out);
out out
} }
/// No skill may teach a workspace path the platform does not use. /// **Everything we ship that becomes prompt text an agent reads.**
///
/// Skills and team-template role prompts, in one corpus, because the rules
/// below are properties of *what an agent is told* — not of which file it
/// happened to be written in.
///
/// This function is the finding. The `/workspace/repo` guard was written on
/// 2026-08-19 against `skills/` only, and the same wrong path had been
/// sitting in **four team templates** the whole time — including
/// `rust_sdlc`, the default for five of the six workflow recipes, whose
/// coder was told "your working directory is /workspace/repo" and whose
/// committer was told to `cd` there. A guard that covers one corpus and not
/// the other reads exactly like a guard that covers the problem.
fn all_shipped_prompts() -> Vec<(String, String)> {
let mut out = all_skills();
walk_ext(&repo_root("templates/teams"), "toml", &mut out);
walk_ext(&repo_root("templates/workflows"), "toml", &mut out);
out
}
/// Nothing we ship may teach a workspace path the platform does not mount.
/// ///
/// `workspace-repo-commit-protocol` told agents that `/workspace/repo` was /// `workspace-repo-commit-protocol` told agents that `/workspace/repo` was
/// "the ONLY path where source-modifying edits belong". The platform mounts /// "the ONLY path where source-modifying edits belong". The platform mounts
@@ -210,18 +233,18 @@ mod contradiction_tests {
/// was delivered twice in a single measured run, so agents received the /// was delivered twice in a single measured run, so agents received the
/// platform's real path and a skill contradicting it in the SAME prompt. /// platform's real path and a skill contradicting it in the SAME prompt.
#[test] #[test]
fn no_skill_teaches_a_repo_path_the_platform_does_not_mount() { fn nothing_we_ship_teaches_a_repo_path_the_platform_does_not_mount() {
let mut offenders = Vec::new(); let mut offenders = Vec::new();
for (name, body) in all_skills() { for (name, body) in all_shipped_prompts() {
if body.contains("/workspace/repo") { if body.contains("/workspace/repo") {
offenders.push(name); offenders.push(name);
} }
} }
assert!( assert!(
offenders.is_empty(), offenders.is_empty(),
"{} skill(s) name /workspace/repo; the mission checkout is \ "{} shipped prompt file(s) name /workspace/repo; the mission \
/mission/repo, so an agent following them writes somewhere that is \ checkout is /mission/repo, so an agent following them writes \
never delivered: {}", somewhere that is never delivered: {}",
offenders.len(), offenders.len(),
offenders.join(", ") offenders.join(", ")
); );
@@ -239,7 +262,7 @@ mod contradiction_tests {
/// legitimately DISCUSS these names, as this one now does when warning /// legitimately DISCUSS these names, as this one now does when warning
/// against them. /// against them.
#[test] #[test]
fn no_skill_instructs_an_agent_to_call_a_zeroclaw_tool() { fn nothing_we_ship_instructs_an_agent_to_call_a_zeroclaw_tool() {
const ZEROCLAW_TOOLS: &[&str] = &[ const ZEROCLAW_TOOLS: &[&str] = &[
"`file_read`", "`file_read`",
"`file_write`", "`file_write`",
@@ -248,7 +271,7 @@ mod contradiction_tests {
"`glob_search`", "`glob_search`",
]; ];
let mut offenders = Vec::new(); let mut offenders = Vec::new();
for (name, body) in all_skills() { for (name, body) in all_shipped_prompts() {
// The line has to READ as an instruction. "Do not reach for // The line has to READ as an instruction. "Do not reach for
// `file_read`" is the correction, not the defect. // `file_read`" is the correction, not the defect.
for line in body.lines() { for line in body.lines() {
@@ -267,8 +290,8 @@ mod contradiction_tests {
} }
assert!( assert!(
offenders.is_empty(), offenders.is_empty(),
"{} skill line(s) tell an agent to use a tool its subprocess does \ "{} shipped prompt line(s) tell an agent to use a tool its \
not expose:\n {}", subprocess does not expose:\n {}",
offenders.len(), offenders.len(),
offenders.join("\n ") offenders.join("\n ")
); );
+1 -1
View File
@@ -38,7 +38,7 @@ skills = ["tailwind-v4-idioms", "workspace-repo-commit-protocol", "int-xx-marker
system_prompt = """ system_prompt = """
You are the CODER of a Frontend team. You are the CODER of a Frontend team.
Working directory /workspace/repo. Implement per the DESIGNER's spec. Working directory /mission/repo. Implement per the DESIGNER's spec.
Type strictness > convenience — no `any`, no `as` escapes without a Type strictness > convenience — no `any`, no `as` escapes without a
comment explaining why. comment explaining why.
""" """
+1 -1
View File
@@ -32,7 +32,7 @@ skills = ["expo-managed-vs-bare", "workspace-repo-commit-protocol", "int-xx-mark
system_prompt = """ system_prompt = """
You are the CODER of a Mobile team. You are the CODER of a Mobile team.
Working directory /workspace/repo. Prefer Expo's managed workflow; Working directory /mission/repo. Prefer Expo's managed workflow;
justify any drop to bare workflow. All new native modules ship with justify any drop to bare workflow. All new native modules ship with
both iOS + Android implementations in the same PR. both iOS + Android implementations in the same PR.
""" """
+17 -6
View File
@@ -47,7 +47,7 @@ skills = ["write-rust-current-edition", "cargo-test-driven-development", "worksp
system_prompt = """ system_prompt = """
You are the CODER of a Rust SDLC team. You are the CODER of a Rust SDLC team.
Your working directory is /workspace/repo. All edits happen there. Your working directory is /mission/repo. All edits happen there.
Follow the PLANNER's INT-XX brief: Follow the PLANNER's INT-XX brief:
- implement the change end-to-end - implement the change end-to-end
- keep files under 1500 LOC (see mission config) - keep files under 1500 LOC (see mission config)
@@ -121,12 +121,23 @@ You are the COMMITTER of a Rust SDLC team.
Only run when TEST_PASS and REVIEW_APPROVE have both been emitted for Only run when TEST_PASS and REVIEW_APPROVE have both been emitted for
the current INT item. Then: the current INT item. Then:
cd /workspace/repo cd /mission/repo
git status # what did the team actually touch
git add -A git add -A
git commit -m "<INT-NN> <title>\n\n<one-paragraph rationale>" git commit -m "INT-NN <title>
git push
Emit `COMPLETED: INT-<NN>` on its own line when done — the mission <one paragraph on WHY, not what>
loop advances on that marker.
Refs: INT-NN
"
Push ONLY if the mission's task says to. Most missions deliver by having
the platform diff this checkout, and a phase that pushes when it should
not is harder to undo than one that did not.
Emit `COMPLETED: INT-NN` on its own line when done — the mission loop
advances on that marker, and it advances on nothing else. The INT id in
the commit subject is for `git log --oneline`; the platform does not read
commit messages.
""" """
brain_seed = "" brain_seed = ""
+1 -1
View File
@@ -32,7 +32,7 @@ skills = ["threejs-perf-and-teardown", "workspace-repo-commit-protocol", "int-xx
system_prompt = """ system_prompt = """
You are the CODER of a three.js team. You are the CODER of a three.js team.
Working directory /workspace/repo. Prefer InstancedMesh over per-node Working directory /mission/repo. Prefer InstancedMesh over per-node
Meshes. Dispose geometries + textures on scene teardown — memory leaks Meshes. Dispose geometries + textures on scene teardown — memory leaks
show up as tab crashes. show up as tab crashes.
""" """