fix(skills): two more skills that contradicted the platform

Same class as the `/workspace/repo` path and the ZeroClaw tool names: the
skills were written alongside the platform and never compared to it again.
Both found by reading the source of truth before writing a check against
it.

1. `decompose-int-items` showed `PLAN_COMPLETE: INT-01..05`. An id is
   strictly `INT-<digits>`, so the range form is rejected outright — the
   plan pass records nothing while every item stays open. A live planner
   emitted exactly that line. Now one id per line.

2. `workspace-repo-commit-protocol` said the task-card parser advances
   mission state on the INT id in the commit subject. Nothing in the
   platform reads commit messages; the parser reads `run_events` — the
   agent's turn output. An agent that believed it could commit with the id
   and never emit `COMPLETED: INT-NN`, leaving the mission open on an item
   it had finished. The convention is kept, the mechanism corrected.

`no_skill_shows_a_marker_the_parser_would_reject` runs the real parser
over every marker in every skill's fenced blocks, negative-controlled
against the range form.

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 08:24:01 -07:00
co-authored by Claude Opus 5
parent 1a6fdfc0e6
commit d0b657a24b
3 changed files with 82 additions and 3 deletions
+69
View File
@@ -273,4 +273,73 @@ mod contradiction_tests {
offenders.join("\n ")
);
}
/// No skill may show a marker the real parser rejects.
///
/// Checked by running `task_card_parser::parse` itself, never a copy of its
/// rules — a second implementation of the contract drifts, and then the
/// test passes while the mission loop stalls.
///
/// This is the third instance of one class: the skills were written
/// alongside the platform and then never compared to it again. The first
/// was a repo path the platform does not mount; the second a tool the agent
/// does not have; this one is `PLAN_COMPLETE: INT-01..05` in
/// `decompose-int-items`, which a live planner emitted verbatim. Ids are
/// strictly `INT-<digits>`, so the range form parses to nothing — the plan
/// pass records no completion at all while every item stays open.
///
/// Scoped to fenced code blocks, which is where a skill puts the text it
/// tells an agent to EMIT. A marker named in a sentence is prose.
#[test]
fn no_skill_shows_a_marker_the_parser_would_reject() {
// The templates. `INT-NN` is a placeholder an agent substitutes, not a
// literal it emits, so it is not a contradiction.
const PLACEHOLDERS: &[&str] = &["INT-NN", "INT-XX", "INT-N", "INT-nn"];
let mut offenders = Vec::new();
for (name, body) in all_skills() {
let mut fenced = false;
for line in body.lines() {
if line.trim_start().starts_with("```") {
fenced = !fenced;
continue;
}
let t = line.trim();
if !fenced || !t.contains("INT-") || !t.contains(':') {
continue;
}
let Some((kind, _)) = t.split_once(':') else {
continue;
};
if !MARKER_KINDS.contains(&kind.trim()) {
continue;
}
if PLACEHOLDERS.iter().any(|p| t.contains(p)) {
continue;
}
if crate::task_card_parser::parse(t).is_empty() {
offenders.push(format!("{name}: {t}"));
}
}
}
assert!(
offenders.is_empty(),
"{} skill line(s) show a marker the parser rejects — an agent that \
follows them exactly is silently ignored:\n {}",
offenders.len(),
offenders.join("\n ")
);
}
/// The marker kinds, as the parser spells them.
const MARKER_KINDS: &[&str] = &[
"TASK",
"PLAN_COMPLETE",
"WORK",
"HANDOFF",
"TEST_PASS",
"TEST_FAIL",
"REVIEW_APPROVE",
"REVIEW_BLOCK",
"COMPLETED",
];
}