fix(missions): state the INT-XX marker contract where agents actually see it
task_card_parser.rs scans every mission turn line-by-line for TASK/WORK/
HANDOFF/TEST_PASS/TEST_FAIL/REVIEW_APPROVE/REVIEW_BLOCK/COMPLETED and
materializes mission_tasks rows from them. The exact syntax it demands --
literal, own line, with the colon, no bold, no code fence, one INT id per
line -- was documented in two places the agent does not reliably read:
1. the team-template role prompts, which are NEVER injected into mission
turns (runtime_provision writes model_provider / risk_profile /
mcp_bundles and nothing else), and
2. a foundation skill the agent had to choose to fetch.
The phase directives said "emit INT-XX markers" without ever saying what one
looks like. So the parser's contract was stated nowhere load-bearing, and
whether a mission produced task cards came down to whether the model guessed
the format. This is a machine contract, not a style hint -- it belongs in
phase_task_text, the one text every mission turn receives.
Added a regression test that feeds every marker example from the generated
prompt through the real parser, so the syntax we advertise and the syntax we
accept cannot drift apart again.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d9a1d8bb5a
commit
6926107e4f
@@ -290,6 +290,28 @@ fn phase_task_text(kind: &str, title: &str, description: Option<&str>) -> String
|
|||||||
or content_search first, then file_edit to patch. Write your outputs\n\
|
or content_search first, then file_edit to patch. Write your outputs\n\
|
||||||
as REAL files with file_edit — do NOT paste code blocks in your reply\n\
|
as REAL files with file_edit — do NOT paste code blocks in your reply\n\
|
||||||
expecting the platform to save them; nothing else writes files for you.\n";
|
expecting the platform to save them; nothing else writes files for you.\n";
|
||||||
|
// The INT-XX markers are a machine contract, not a style preference:
|
||||||
|
// task_card_parser.rs scans turn output line-by-line for these literals and
|
||||||
|
// materializes `mission_tasks` rows from them. The rules used to live only
|
||||||
|
// in the team-template role prompts -- which are never injected into mission
|
||||||
|
// turns (runtime_provision.rs writes model/risk_profile/mcp_bundles and
|
||||||
|
// nothing else) -- and in a skill the agent had to choose to fetch. So the
|
||||||
|
// parser's contract was stated nowhere the agent reliably saw it. It is
|
||||||
|
// stated here because this is the one text every mission turn receives.
|
||||||
|
let marker_protocol = "\
|
||||||
|
TASK MARKERS (parsed literally, line by line — this is a machine contract):\n\
|
||||||
|
Emit these on their own line, with the colon, no bold, no code fence,\n\
|
||||||
|
exactly one INT id per line, at the END of a substantive turn:\n\
|
||||||
|
- TASK: INT-NN — <title> open a new item\n\
|
||||||
|
- WORK: INT-NN started implementing\n\
|
||||||
|
- HANDOFF: INT-NN passed to test/review\n\
|
||||||
|
- TEST_PASS: INT-NN tests green\n\
|
||||||
|
- TEST_FAIL: INT-NN — <reason> build/tests failed\n\
|
||||||
|
- REVIEW_APPROVE: INT-NN diff approved\n\
|
||||||
|
- REVIEW_BLOCK: INT-NN — <reason> changes requested\n\
|
||||||
|
- COMPLETED: INT-NN done and pushed\n\
|
||||||
|
Never emit a marker you can't back up — COMPLETED without a corresponding\n\
|
||||||
|
commit desynchronizes the mission from the repo.\n";
|
||||||
let directive = match kind {
|
let directive = match kind {
|
||||||
"research" => {
|
"research" => {
|
||||||
"Your team is running the RESEARCH phase of this mission. \
|
"Your team is running the RESEARCH phase of this mission. \
|
||||||
@@ -323,7 +345,7 @@ fn phase_task_text(kind: &str, title: &str, description: Option<&str>) -> String
|
|||||||
}
|
}
|
||||||
_ => "Execute this mission phase according to the mission brief.",
|
_ => "Execute this mission phase according to the mission brief.",
|
||||||
};
|
};
|
||||||
format!("MISSION: {title}\n\n{tool_preamble}\n{directive}\n\nBRIEF:\n{base}")
|
format!("MISSION: {title}\n\n{tool_preamble}\n{marker_protocol}\n{directive}\n\nBRIEF:\n{base}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close phases whose topology_runs are all terminal.
|
/// Close phases whose topology_runs are all terminal.
|
||||||
@@ -472,6 +494,47 @@ mod tests {
|
|||||||
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
|
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The marker syntax we hand the agent must be the syntax we parse back.
|
||||||
|
///
|
||||||
|
/// These two sides used to live far apart — the rules were in team-template
|
||||||
|
/// role prompts that mission turns never receive — so nothing caught a
|
||||||
|
/// drift between what we asked for and what `task_card_parser` accepts.
|
||||||
|
/// Every example line in the prompt is fed through the real parser here.
|
||||||
|
#[test]
|
||||||
|
fn task_text_marker_examples_parse() {
|
||||||
|
let text = phase_task_text("coding", "Demo", Some("brief"));
|
||||||
|
|
||||||
|
let examples: Vec<&str> = text
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|l| l.starts_with("- ") && l.contains("INT-NN"))
|
||||||
|
.map(|l| l.trim_start_matches("- "))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
examples.len() >= 8,
|
||||||
|
"expected the full marker ladder in the prompt, found {}: {examples:?}",
|
||||||
|
examples.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
for ex in examples {
|
||||||
|
// Strip the trailing prose column ("open a new item") and the
|
||||||
|
// <placeholder>, leaving a marker line an agent would actually emit.
|
||||||
|
let line = ex.replace("INT-NN", "INT-05");
|
||||||
|
let line = line.split(" ").next().unwrap_or(&line).trim();
|
||||||
|
let line = line.replace("<title>", "Add retry").replace(
|
||||||
|
"<reason>",
|
||||||
|
"compile error",
|
||||||
|
);
|
||||||
|
let parsed = crate::task_card_parser::parse(&line);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.len(),
|
||||||
|
1,
|
||||||
|
"prompt advertises a marker the parser does not accept: {line:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(parsed[0].int_id, "INT-05", "wrong id parsed from {line:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The regression guard: the alias must survive a round-trip through the
|
/// The regression guard: the alias must survive a round-trip through the
|
||||||
/// real `TopologyGraph` deserializer and land in `attrs`. A top-level
|
/// real `TopologyGraph` deserializer and land in `attrs`. A top-level
|
||||||
/// `"agent"` key alone is dropped by serde, which silently routed every
|
/// `"agent"` key alone is dropped by serde, which silently routed every
|
||||||
|
|||||||
Reference in New Issue
Block a user