feat(auto-drive): enforce finish evidence and paste fallback

This commit is contained in:
James Peter
2026-02-14 09:13:17 +10:00
parent 41e8b6f058
commit 46d537e4dc
3 changed files with 412 additions and 138 deletions
@@ -461,7 +461,12 @@ mod tests {
.get("properties")
.and_then(|v| v.as_object())
.expect("schema properties");
assert!(!props.contains_key("goal"));
assert!(props.contains_key("goal"), "goal property missing");
assert!(props.contains_key("phase"), "phase property missing");
assert!(
props.contains_key("finish_evidence"),
"finish_evidence property missing"
);
assert!(props.contains_key("status_title"), "status_title property missing");
assert!(
props.contains_key("status_sent_to_user"),
@@ -480,9 +485,10 @@ mod tests {
.get("required")
.and_then(|v| v.as_array())
.expect("root required");
assert!(schema_required.contains(&json!("finish_status")));
assert!(schema_required.contains(&json!("status_title")));
assert!(schema_required.contains(&json!("status_sent_to_user")));
assert!(schema_required.contains(&json!("prompt_sent_to_cli")));
assert!(!schema_required.contains(&json!("prompt_sent_to_cli")));
let agents_obj = props
.get("agents")
@@ -530,7 +536,7 @@ mod tests {
}
#[test]
fn schema_sets_prompt_sent_to_cli_min_but_no_max_length() {
fn schema_sets_prompt_sent_to_cli_min_and_max_length() {
let active_agents: Vec<String> = Vec::new();
let schema = build_schema(&active_agents, SchemaFeatures::default());
let prompt_schema = schema
@@ -545,9 +551,10 @@ mod tests {
Some(&json!(CLI_PROMPT_MIN_CHARS)),
"schema minLength should match CLI_PROMPT_MIN_CHARS"
);
assert!(
!prompt_schema.contains_key("maxLength"),
"schema should omit maxLength to avoid provider truncation"
assert_eq!(
prompt_schema.get("maxLength"),
Some(&json!(CLI_PROMPT_MAX_CHARS)),
"schema maxLength should match CLI_PROMPT_MAX_CHARS"
);
}
@@ -664,7 +671,7 @@ mod tests {
.get("description")
.and_then(|v| v.as_str())
.expect("goal description");
assert!(description.contains("primary coding goal"));
assert!(description.contains("bootstrapping/clarifying"));
}
#[test]
@@ -776,6 +783,78 @@ mod tests {
assert!(decision.agents_timing.is_none());
}
#[test]
fn parse_decision_continue_rejects_finish_evidence() {
let raw = r#"{
"finish_status": "continue",
"status_title": "Implementing",
"status_sent_to_user": "Driving the implementation milestone.",
"prompt_sent_to_cli": "Deliver the feature end-to-end and validate.",
"finish_evidence": {
"primary_outcome_achieved": "done",
"validation_checks_passed": ["cargo test"],
"edge_cases_handled": []
}
}"#;
let err = parse_decision(raw).expect_err("continue should reject finish evidence");
assert!(
err.to_string()
.contains("finish_evidence must be null when finish_status is continue")
);
}
#[test]
fn parse_decision_finish_requires_finish_evidence() {
let raw = r#"{
"finish_status": "finish_success",
"status_title": "Completed",
"status_sent_to_user": "Everything is green.",
"prompt_sent_to_cli": null
}"#;
let err = parse_decision(raw).expect_err("finish status should require evidence");
assert!(err.to_string().contains("missing finish_evidence"));
}
#[test]
fn parse_decision_finish_rejects_cli_prompt() {
let raw = r#"{
"finish_status": "finish_success",
"status_title": "Completed",
"status_sent_to_user": "Everything is green.",
"prompt_sent_to_cli": "Keep working",
"finish_evidence": {
"primary_outcome_achieved": "Resolved primary task.",
"validation_checks_passed": ["cargo test --workspace"],
"edge_cases_handled": ["empty input"]
}
}"#;
let err = parse_decision(raw).expect_err("finish statuses require null CLI prompt");
assert!(err.to_string().contains("must set prompt_sent_to_cli to null"));
}
#[test]
fn parse_decision_finish_accepts_finish_evidence() {
let raw = r#"{
"finish_status": "finish_success",
"phase": "lockdown",
"status_title": "Completed",
"status_sent_to_user": "All checks are green.",
"prompt_sent_to_cli": null,
"finish_evidence": {
"primary_outcome_achieved": "Resolved primary task end-to-end.",
"validation_checks_passed": ["cargo test --workspace", "./build-fast.sh"],
"edge_cases_handled": ["empty payload", "large payload"]
}
}"#;
let (decision, _) = parse_decision(raw).expect("finish decision should parse");
assert_eq!(decision.status, AutoCoordinatorStatus::Success);
assert!(decision.cli.is_none());
}
#[test]
fn classify_missing_cli_prompt_is_recoverable() {
let err = anyhow!("model response missing prompt_sent_to_cli for continue");
@@ -907,6 +986,8 @@ mod tests {
struct CoordinatorDecisionNew {
finish_status: String,
#[serde(default)]
phase: Option<String>,
#[serde(default)]
status_title: Option<String>,
#[serde(default)]
status_sent_to_user: Option<String>,
@@ -917,9 +998,21 @@ struct CoordinatorDecisionNew {
#[serde(default)]
agents: Option<AgentsField>,
#[serde(default)]
finish_evidence: Option<FinishEvidencePayload>,
#[serde(default)]
goal: Option<String>,
}
#[derive(Debug, Deserialize)]
struct FinishEvidencePayload {
#[serde(default)]
primary_outcome_achieved: Option<String>,
#[serde(default)]
validation_checks_passed: Option<Vec<String>>,
#[serde(default)]
edge_cases_handled: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct ProgressPayload {
#[serde(default)]
@@ -1829,6 +1922,7 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
let models_request_property = json!({
"type": "array",
"maxItems": 4,
"description": models_description,
"items": models_items_schema,
});
@@ -1841,21 +1935,29 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
json!({
"type": "string",
"enum": ["continue", "finish_success", "finish_failed"],
"description": "Prefer 'continue' unless the mission is fully complete or truly blocked. Always consider what further work might be possible to confirm the goal is complete before ending."
"description": "Prefer 'continue' until the solution is rock solid. Do not finish if there are obvious edge cases or missing tests to write. You MUST populate the 'finish_evidence' object when finishing."
}),
);
required.push(Value::String("finish_status".to_string()));
properties.insert(
"phase".to_string(),
json!({
"type": ["string", "null"],
"enum": ["explore", "implement", "validate", "lockdown", null],
"description": "Optional: tracks mission state. Use 'explore' for recon/planning, 'implement' for coding, and 'validate/lockdown' to trigger deep validation before finishing."
}),
);
properties.insert(
"goal".to_string(),
json!({
"type": ["string", "null"],
"maxLength": 200,
"description": "Use only when bootstrapping/clarifying the mission goal is required."
}),
);
if features.include_goal_field {
properties.insert(
"goal".to_string(),
json!({
"type": "string",
"minLength": 4,
"maxLength": 200,
"description": "Provide the single primary coding goal derived from the recent conversation history to begin Auto Drive without a user-supplied prompt."
}),
);
required.push(Value::String("goal".to_string()));
}
@@ -1865,7 +1967,7 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
"type": ["string", "null"],
"minLength": 2,
"maxLength": 80,
"description": "1-4 words, present-tense, what you asked the CLI to work on now."
"description": "1-4 words, present-tense milestone headline."
}),
);
required.push(Value::String("status_title".to_string()));
@@ -1876,24 +1978,20 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
"type": ["string", "null"],
"minLength": 4,
"maxLength": 600,
"description": "1-2 sentences shown to the user explaining what you asked the CLI to work on now. Will be shown in the UI to keep the user updated on the progress."
"description": "1-2 sentences explaining the high-level milestone the CLI (and any agents) are tackling."
}),
);
required.push(Value::String("status_sent_to_user".to_string()));
// NOTE: We intentionally omit `maxLength` here. Some providers truncate
// responses to satisfy schema length caps, which would hide overlong
// prompts. We validate length after parsing and treat >600 as recoverable
// so the coordinator can retry with guidance instead of silently chopping.
properties.insert(
"prompt_sent_to_cli".to_string(),
json!({
"type": ["string", "null"],
"minLength": CLI_PROMPT_MIN_CHARS,
"description": "Instruction sent to the CLI to push it forward with the task (4-600 chars). Write this like a human maintainer pushing the CLI forwards, without digging too deep into the technical side. Provide when finish_status is 'continue'. Keep it high-level; the CLI has more context and tools than you do. e.g. 'Execute the first two steps of the plan you provided in parellel using agents.' NEVER ask the CLI to show you files so you solve problems directly. ALWAYS allow the CLI to take control. You are the COORDINATOR not the WORKER. Prompts over 600 characters will be rejected as this indicates the CLI is not being given sufficient autonomy."
"maxLength": CLI_PROMPT_MAX_CHARS,
"description": "Single milestone instruction to the CLI. Outcome-focused, non-procedural. Set to null ONLY when finishing."
}),
);
required.push(Value::String("prompt_sent_to_cli".to_string()));
if features.include_agents {
properties.insert(
@@ -1901,48 +1999,80 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
json!({
"type": ["object", "null"],
"additionalProperties": false,
"description": "Parallel help agents for the CLI to spawn. Use often. Agents are faster, parallelize work and allow exploration of a range of approaches.",
"description": "Optional parallel helper agents. Default to null. Use strategically for parallel research, diverse planning, or parallel isolated implementation using fast models (spark/flash). Do NOT use for trivial CLI tool usage (like running tests or grep).",
"properties": {
"timing": {
"type": "string",
"enum": ["parallel", "blocking"],
"description": "Parallel: run while the CLI works. Blocking: wait for results before the CLI executes the prompt you provided."
"enum": ["parallel", "blocking"]
},
"list": {
"type": "array",
"maxItems": 5,
"maxItems": 4,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"write": {
"type": "boolean",
"description": "Creates an isolated worktree for each agent and enable writes to that worktree. Default false so that the agent can only read files."
"description": "If true, agent can write in isolated worktree. Set to true for coding implementation."
},
"context": {
"type": ["string", "null"],
"maxLength": 1500,
"description": "Background details (agents can not see the conversation - you must provide ALL neccessary information here). You might want to include parts of the plan or conversation history relevant to the work given to the agent."
"description": "All necessary background (agents do not see chat history)."
},
"prompt": {
"type": "string",
"minLength": 8,
"maxLength": 400,
"description": "Outcome-oriented instruction (what to produce)."
"description": "Outcome-oriented instruction for the agent."
},
"models": models_request_property.clone()
},
"required": ["prompt", "context", "write", "models"]
},
"description": "Up to 3 batches per turn with up to 4 agents in each. Use agents whenever it will help to source a variety of opinions when planning/researching or when there a mulitple workstreams which can be extecuted at once. Instruct the agent to carefully merge in the results of the agents work. Another great reason to use agents is that it helps to split the work up in small batches with a new context history - this speeds up work and dramatically improve focus. Having said that, the CLI has to be responible for merging in the results and producing the final product, so you need to balance the work given to the agents vs work given to the CLI at each step."
},
},
"required": ["timing", "list"]
}),
);
required.push(Value::String("agents".to_string()));
}
properties.insert(
"finish_evidence".to_string(),
json!({
"type": ["object", "null"],
"additionalProperties": false,
"description": "MANDATORY when finish_status is not 'continue'. Leave null if 'continue'. Concrete proof that the task is entirely complete, edge cases are handled, and no further work is needed.",
"properties": {
"primary_outcome_achieved": {
"type": "string",
"maxLength": 600,
"description": "Summary of what was achieved and fully resolved end-to-end."
},
"validation_checks_passed": {
"type": "array",
"maxItems": 12,
"items": {
"type": "string",
"maxLength": 140
},
"description": "Specific validation commands executed by the CLI that are now passing (e.g., 'npm test', 'cargo clippy', 'browser UX verified')."
},
"edge_cases_handled": {
"type": "array",
"maxItems": 12,
"items": {
"type": "string",
"maxLength": 180
},
"description": "Specific edge cases that were actively handled and verified before finishing. (Leave empty if none apply, but do NOT use this to list unhandled risks)."
}
},
"required": ["primary_outcome_achieved", "validation_checks_passed", "edge_cases_handled"]
}),
);
let mut schema = serde_json::Map::new();
schema.insert(
"title".to_string(),
@@ -1952,6 +2082,50 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
schema.insert("additionalProperties".to_string(), Value::Bool(false));
schema.insert("properties".to_string(), Value::Object(properties));
schema.insert("required".to_string(), Value::Array(required));
schema.insert(
"allOf".to_string(),
json!([
{
"if": {
"properties": {
"finish_status": {
"const": "continue"
}
}
},
"then": {
"required": ["prompt_sent_to_cli"],
"properties": {
"prompt_sent_to_cli": {
"type": "string",
"minLength": CLI_PROMPT_MIN_CHARS,
"maxLength": CLI_PROMPT_MAX_CHARS
},
"finish_evidence": {
"type": "null"
}
}
}
},
{
"if": {
"properties": {
"finish_status": {
"enum": ["finish_success", "finish_failed"]
}
}
},
"then": {
"required": ["finish_evidence"],
"properties": {
"prompt_sent_to_cli": {
"type": "null"
}
}
}
}
]),
);
Value::Object(schema)
}
@@ -2778,6 +2952,36 @@ fn classify_recoverable_decision_error(err: &anyhow::Error) -> Option<Recoverabl
});
}
if lower.contains("missing finish_evidence") {
return Some(RecoverableDecisionError {
summary: "missing `finish_evidence` for finish status".to_string(),
guidance: Some(
"Include a `finish_evidence` object whenever `finish_status` is `finish_success` or `finish_failed`."
.to_string(),
),
});
}
if lower.contains("finish_evidence must be null") {
return Some(RecoverableDecisionError {
summary: "`finish_evidence` must be null for continue turns".to_string(),
guidance: Some(
"Set `finish_evidence` to null (or omit it) when `finish_status` is `continue`."
.to_string(),
),
});
}
if lower.contains("must set prompt_sent_to_cli to null") {
return Some(RecoverableDecisionError {
summary: "`prompt_sent_to_cli` must be null for finish statuses".to_string(),
guidance: Some(
"When finishing (`finish_success`/`finish_failed`), set `prompt_sent_to_cli` to null and include `finish_evidence`."
.to_string(),
),
});
}
if lower.contains("length limit")
|| lower.contains("cut off")
|| lower.contains("exceeds") && lower.contains("prompt_sent_to_cli")
@@ -2921,14 +3125,18 @@ fn convert_decision_new(
) -> Result<ParsedCoordinatorDecision> {
let CoordinatorDecisionNew {
finish_status: _,
phase,
status_title,
status_sent_to_user,
progress,
prompt_sent_to_cli,
agents: agent_payloads,
finish_evidence,
goal,
} = decision;
validate_phase(phase)?;
let mut status_title = clean_optional(status_title);
let mut status_sent_to_user = clean_optional(status_sent_to_user);
@@ -2947,6 +3155,8 @@ fn convert_decision_new(
let cli_prompt = clean_optional(prompt_sent_to_cli);
validate_finish_evidence_for_status(status, finish_evidence)?;
let cli = match (status, cli_prompt) {
(AutoCoordinatorStatus::Continue, Some(prompt)) => {
let prompt = clean_required(&prompt, "prompt_sent_to_cli")?;
@@ -2963,7 +3173,11 @@ fn convert_decision_new(
"model response missing prompt_sent_to_cli for continue"
));
}
(_, Some(_prompt)) => None,
(_, Some(_prompt)) => {
return Err(anyhow!(
"model response must set prompt_sent_to_cli to null when finish_status is finish_success or finish_failed"
));
}
(_, None) => None,
};
@@ -3040,6 +3254,12 @@ fn convert_decision_legacy(
let context = clean_optional(cli_context);
let goal = clean_optional(goal);
if matches!(status, AutoCoordinatorStatus::Success | AutoCoordinatorStatus::Failed) {
return Err(anyhow!(
"legacy model response missing finish_evidence for finish_status finish_success or finish_failed"
));
}
let cli = match (status, cli_prompt) {
(AutoCoordinatorStatus::Continue, Some(prompt)) => Some(CliAction {
prompt: clean_required(&prompt, "cli_prompt")?,
@@ -3049,11 +3269,11 @@ fn convert_decision_legacy(
(AutoCoordinatorStatus::Continue, None) => {
return Err(anyhow!("legacy model response missing cli_prompt for continue"));
}
(_, Some(prompt)) => Some(CliAction {
prompt: clean_required(&prompt, "cli_prompt")?,
context: context.clone(),
suppress_ui_context: false,
}),
(_, Some(_prompt)) => {
return Err(anyhow!(
"model response must set prompt_sent_to_cli to null when finish_status is finish_success or finish_failed"
));
}
(_, None) => None,
};
@@ -3125,6 +3345,71 @@ fn clean_required(value: &str, field: &str) -> Result<String> {
}
}
fn clean_required_list(values: Option<Vec<String>>, field: &str) -> Result<Vec<String>> {
let values = values.ok_or_else(|| anyhow!("{field} is missing"))?;
values
.into_iter()
.enumerate()
.map(|(idx, value)| clean_required(&value, &format!("{field}[{idx}]")))
.collect()
}
fn validate_phase(phase: Option<String>) -> Result<()> {
let Some(phase) = clean_optional(phase) else {
return Ok(());
};
match phase.as_str() {
"explore" | "implement" | "validate" | "lockdown" => Ok(()),
_ => Err(anyhow!(
"unexpected phase '{phase}'; use one of: explore, implement, validate, lockdown"
)),
}
}
fn validate_finish_evidence_for_status(
status: AutoCoordinatorStatus,
finish_evidence: Option<FinishEvidencePayload>,
) -> Result<()> {
match status {
AutoCoordinatorStatus::Continue => {
if finish_evidence.is_some() {
return Err(anyhow!(
"finish_evidence must be null when finish_status is continue"
));
}
Ok(())
}
AutoCoordinatorStatus::Success | AutoCoordinatorStatus::Failed => {
let finish_evidence = finish_evidence.ok_or_else(|| {
anyhow!(
"model response missing finish_evidence for finish_status finish_success or finish_failed"
)
})?;
let FinishEvidencePayload {
primary_outcome_achieved,
validation_checks_passed,
edge_cases_handled,
} = finish_evidence;
let _primary_outcome_achieved = clean_required(
&primary_outcome_achieved
.ok_or_else(|| anyhow!("finish_evidence.primary_outcome_achieved is missing"))?,
"finish_evidence.primary_outcome_achieved",
)?;
let _validation_checks_passed = clean_required_list(
validation_checks_passed,
"finish_evidence.validation_checks_passed",
)?;
let _edge_cases_handled =
clean_required_list(edge_cases_handled, "finish_evidence.edge_cases_handled")?;
Ok(())
}
}
}
fn ensure_cli_prompt_length(prompt: &str) -> Result<()> {
let len = prompt.chars().count();
if len < CLI_PROMPT_MIN_CHARS {
+38 -69
View File
@@ -1,78 +1,47 @@
You have a special role within Code. You are the Auto Drive Coordinator — a mission lead orchestrating this coding session. You direct the Code CLI (role: user) and helper agents, but you never implement work yourself.
You have a special role within Code. You are the Auto Drive Coordinator — the mission lead orchestrating this coding session.
You direct the Code CLI (role: user) and an optional fleet of helper agents. You never run tools, write code, or implement changes yourself. You only output a single JSON object matching the coordinator schema each turn.
# North Star
- **CLI Autonomy**: The CLI is a highly autonomous senior agent that persists until tasks are resolved end-to-end. Let it handle its own multi-step workflows. Delegate whole milestones to it.
- **Strategic Swarming**: You decide *who* does the work. Use the CLI for stateful, sequential coding and running local tools. Use agents for parallel research, gathering diverse opinions, or offloading well-defined isolated coding tasks.
- **Absolute Completion**: "Done" requires hard evidence. Never finish a task based on code just "looking complete." Prove it works with verified tests and edge-case handling.
# Mission Lead Responsibilities
- **Set direction**: Define the outcome and success criteria each turn.
- **Delegate execution**: The CLI (and agents you launch) handle all tool use, coding, and testing.
- **Sequence work**: Keep a steady research → test → patch → verify rhythm.
- **Maintain momentum**: Track evidence, escalate blockers, and decide when to continue, pivot, or finish.
- **Protect focus**: Provide one atomic CLI instruction per turn—short, outcome-oriented, and non-procedural.
- **Set outcomes**: Define the next milestone and what "done" means for it.
- **Delegate execution**: Hand off entire phases of work. The CLI handles the step-by-step tactics via its internal `update_plan` tool.
- **Maintain cadence**: Phase progression typically goes Explore -> Implement -> Validate -> Harden.
- **Manage risk**: Proactively command agents or the CLI to hunt for regressions, test edge cases, and ensure production readiness. Do not leave work for the user - if a fixable risk exists, fix it before finishing.
# Operating Context
The CLI already understands the codebase and has far more tactical control than you. Equip it with goals, not tactics.
# The Single Most Important Rule: Milestones, Not Micromanagement
You must provide ONE MILESTONE per turn to the CLI, not one tiny step. A milestone is a coherent outcome (e.g., "investigate + patch + validate").
## CLI Capabilities (high level)
- **Shell & local tools**: build, test, git, package managers, diagnostics, apply patches.
- **File operations**: read, edit, create, search across the workspace.
- **Browser tooling**: open pages, interact with UIs, capture screenshots for UX validation.
- **Web fetch/search**: retrieve content from known URLs or perform multi-step browsing.
- **Agent coordination**: run helper agents you request; you control their goals and timing.
- **Quality gates**: run `./build-fast.sh`, targeted tests, linting, and reviews.
In `prompt_sent_to_cli`, **DO** provide:
- The milestone outcome.
- Constraints / scope boundaries.
- Definition of done (what validation must be run).
- A stop condition ("Iterate until tests pass, only ask me if irrecoverably blocked").
## Helper Agents (your parallel force)
- Up to **3 agents** per turn; each works in an isolated worktree.
- Pick `timing`: `parallel` (CLI proceeds) or `blocking` (CLI waits for results).
- Set `write` to `true` for prototypes or fixes, `false` for research/review tasks.
- Provide outcome-focused prompts and the full context they need (agents do not see chat history).
- Try to distribute work evenly across models and particularly source a large range of opinions from many agents during planning
- Use at least 2 agents to attempt major coding tasks
**Do NOT** provide:
- Step-by-step shell commands (e.g., "Run npm test").
- File-by-file directions or exact line numbers.
- Requests to show you file contents or diffs (you cannot read them directly, let the CLI evaluate them).
# Decision Schema (strict JSON)
Every turn you must reply with a single JSON object matching the coordinator schema:
| Field | Requirement |
| `finish_status` | Required string: `"continue"`, `"finish_success"`, or `"finish_failed"`. Should almost always be `"continue"`. |
| `status_title` | Required string (1–4 words). Present-tense headline describing what you asked the CLI to work on. |
| `status_sent_to_user` | Required string (1–2 sentences). Present-tense message shown to the user explaining what you've asked the CLI to do. |
| `prompt_sent_to_cli` | Required string (4–600 chars). The single atomic instruction for the CLI when `finish_status` is `"continue"`. Set to `null` only when finishing. |
| `agents` | Optional object with `timing` (`"parallel"` or `"blocking"`) and `list` (≤4 agent entries). Each entry requires `prompt` (8–400 chars), optional `context` (≤1500 chars), `write` (bool), and optional `models` (array of preferred models). |
| `goal` | Optional (≤200 chars). Used only if bootstrapping a derived mission goal is required. |
# Agent Policy (When to Swarm vs. When to use the CLI)
Agents work in isolated, parallel worktrees. Use them strategically based on their strengths.
- **Broad Research & Planning:** Spawn multiple agents with diverse models to evaluate different architectural approaches, search for root causes of a complex bug, or draft competing implementation plans.
- **Parallel Coding:** Offload straightforward, well-scoped coding tasks to fast, efficient models (like `-spark` or `-flash` model). Launch them in parallel on different tasks to implement distinct components simultaneously while the main CLI focuses on integration.
- **No Highly Dependent Chains:** Don't use agents if the task requires step-by-step stateful changes where each step depends on the previous one. The CLI's native loop is better for stateful persistence.
Always include both status fields and a meaningful `prompt_sent_to_cli` string whenever `finish_status` is `"continue"`.
# Completion Gate
Code completion is not task completion. Never set `finish_status` to `"finish_success"` unless you can explicitly populate the `finish_evidence` object with proof that:
1. The primary task is fully resolved end-to-end.
2. Relevant validation is green (tests, builds, linting run by the CLI).
3. Obvious edge cases were tested and handled.
# Guardrails (never cross these)
- Do **not** write code, show diffs, or quote implementation snippets.
- Do **not** prescribe step-by-step shell commands, tool syntax, or file edits.
- Do **not** run git, commit plans, or mention specific line numbers as instructions.
- Do **not** restate context the CLI already has unless compaction or new info requires it.
- Keep prompts short; trust the CLI to plan and execute details.
**Do not leave unresolved risks, missing tests, or "todos" for the user.** If you identify a gap, you MUST stay in `"continue"` and issue a **"Ship Sweep"** milestone to the CLI to fix it. Only output `"finish_success"` when the solution is rock solid.
## Good vs Bad CLI Instructions
- ✅ “Investigate the failing integration tests and summarize root causes.”
- ✅ “Continue with the OAuth rollout plan; validate with CI results.”
- ✅ “What blockers remain before we can ship the caching change?”
- ❌ “Run `npm test`, then edit cache.ts line 42, then commit the fix.”
- ❌ “Use `rg` to find TODOs in src/ and patch them with this diff: …”
- ❌ “Here is the code to paste into auth.rs: `fn verify(...) { … }`.”
## WARNING
- ❌❌❌ Never ask the CLI to show you files e.g. “Open and show contents of xyz.js” This is the WRONG pattern. It means you are taking too much control and micro managing the task. ALWAYS let the CLI choose what to do with the files using HIGH level information.
## Good vs Bad Agent Briefs
- ✅ Outcome-first: “Prototype a minimal WebSocket reconnect strategy and report trade-offs.”
- ✅ Outcome-first: “Research recent regressions touching the payment flow and list likely root causes.”
- ❌ Procedural: “cd services/api && cargo test payments::happy_path, then edit processor.rs.”
- ❌ Loops: “Deploy prototype and monitor” followed by “Deploy and monitor” if the first deploy fails. Frame strategy instead e.g. “Deploy must succeed. Fix errors and continue to resolve until deploy succeeds.”
# Mission Rhythm
1. **Early — Explore broadly**: launch agents for research/prototypes, ask the CLI for reconnaissance, map risks.
2. **Mid — Converge**: focus the CLI on the leading approach, keep one scout exploring risk or upside, tighten acceptance criteria.
3. **Late — Lock down**: drive validation (tests, reviews), address polish, and finish only with hard evidence.
Maintain the research → test → patch → verify cadence: ensure the CLI captures a repro or test, applies minimal changes, and validates outcomes before you advance the mission.
# Final Reminders
- Lead with outcomes; let the CLI design the path.
- Keep text concise—short prompts, short progress updates.
- Launch agents early for breadth, keep one scout during convergence, and focus on validation before finishing.
- Prefer `continue` unless the mission is truly complete or irrecoverably blocked.
- The overseer can override you—bias toward decisive action rather than deferring.
Act with confidence, delegate clearly, and drive the mission to completion. All goals can be achieved with time and diverse strategies.
# Good Milestone Prompts
- ✅ "Take the failing auth flow from red to green; patch minimally, validate with the strongest available checks, and report evidence."
- ✅ "Harden the feature for production: add missing tests, run validation, and verify edge cases."
- ✅ "Execute the architectural plan. I've spawned 3 agents to handle the modular components in parallel; CLI, please coordinate merging their work and running the integration tests."
+49 -29
View File
@@ -409,35 +409,14 @@ impl App<'_> {
}
// Otherwise fall through
}
// Fallback: attempt clipboard image paste on common shortcuts.
// Many terminals (e.g., iTerm2) do not emit Event::Paste for raw-image
// clipboards. When the user presses paste shortcuts, try an image read
// by dispatching a paste with an empty string. The composer will then
// attempt `paste_image_to_temp_png()` and no-op if no image exists.
KeyEvent {
code: KeyCode::Char('v'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
self.dispatch_paste_event(String::new());
}
KeyEvent {
code: KeyCode::Char('v'),
modifiers: m,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} if m.contains(crossterm::event::KeyModifiers::CONTROL)
&& m.contains(crossterm::event::KeyModifiers::SHIFT) =>
{
self.dispatch_paste_event(String::new());
}
KeyEvent {
code: KeyCode::Insert,
modifiers: crossterm::event::KeyModifiers::SHIFT,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
// Explicit image paste shortcut fallback.
//
// Standard text paste shortcuts (Ctrl/Cmd+V, Ctrl+Shift+V,
// Shift+Insert) must flow through terminal paste events to avoid
// truncating text on terminals that also emit partial key streams.
// Keep an opt-in image path on Ctrl+Alt+V for environments that
// don't emit Event::Paste for image clipboards.
key_event if is_image_clipboard_paste_shortcut(&key_event) => {
self.dispatch_paste_event(String::new());
}
KeyEvent {
@@ -2404,6 +2383,24 @@ impl App<'_> {
}
fn is_image_clipboard_paste_shortcut(key_event: &KeyEvent) -> bool {
if !matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
return false;
}
match key_event {
KeyEvent {
code: KeyCode::Char('v' | 'V'),
modifiers,
..
} => {
modifiers.contains(crossterm::event::KeyModifiers::CONTROL)
&& modifiers.contains(crossterm::event::KeyModifiers::ALT)
}
_ => false,
}
}
fn next_event_priority_impl(
high_rx: &Receiver<AppEvent>,
bulk_rx: &Receiver<AppEvent>,
@@ -2485,4 +2482,27 @@ mod next_event_priority_tests {
"bulk event should not be starved behind continuous high-priority events"
);
}
#[test]
fn image_clipboard_fallback_shortcut_is_ctrl_alt_v_only() {
assert!(is_image_clipboard_paste_shortcut(&KeyEvent::new(
KeyCode::Char('v'),
crossterm::event::KeyModifiers::CONTROL | crossterm::event::KeyModifiers::ALT,
)));
assert!(!is_image_clipboard_paste_shortcut(&KeyEvent::new(
KeyCode::Char('v'),
crossterm::event::KeyModifiers::CONTROL,
)));
assert!(!is_image_clipboard_paste_shortcut(&KeyEvent::new(
KeyCode::Char('v'),
crossterm::event::KeyModifiers::CONTROL | crossterm::event::KeyModifiers::SHIFT,
)));
assert!(!is_image_clipboard_paste_shortcut(&KeyEvent::new(
KeyCode::Insert,
crossterm::event::KeyModifiers::SHIFT,
)));
}
}