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 ") 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",
];
} }
+7 -1
View File
@@ -57,7 +57,13 @@ At the end of the planning turn, emit:
TASK: INT-01 — <title> TASK: INT-01 — <title>
TASK: INT-02 — <title> TASK: INT-02 — <title>
... ...
PLAN_COMPLETE: INT-01..05 PLAN_COMPLETE: INT-01
PLAN_COMPLETE: INT-02
``` ```
The parser creates `mission_tasks` rows for each TASK line. `PLAN_COMPLETE` records that the plan pass finished so the mission's coding phase can begin iterating. The parser creates `mission_tasks` rows for each TASK line. `PLAN_COMPLETE` records that the plan pass finished so the mission's coding phase can begin iterating.
**One id per line — never a range.** This section used to show
`PLAN_COMPLETE: INT-01..05`, and a live planner emitted exactly that. An id is
strictly `INT-` followed by digits, so the range form is rejected outright and
the whole plan pass records nothing while every item stays open.
@@ -48,8 +48,12 @@ Refs: INT-NN
" "
``` ```
- **Put the INT-XX marker on the subject line.** The task-card parser advances - **Put the INT-XX id on the subject line.** This is for the humans and for
mission state on it. `git log --oneline` — nothing in the platform reads your commit messages.
Mission state advances on the marker you emit **in your turn output**
(`COMPLETED: INT-NN`, below), which is the only text the task-card parser
reads. Committing with the id and never emitting the marker leaves the
mission open on an item you have already finished.
- **One INT per commit** unless the change genuinely cannot be split. Split when - **One INT per commit** unless the change genuinely cannot be split. Split when
in doubt: a commit covering three items cannot be reverted for one of them. in doubt: a commit covering three items cannot be reverted for one of them.
- **Never `--force`, never rewrite pushed history** without an explicit - **Never `--force`, never rewrite pushed history** without an explicit