feat(prompt): make the plan handoff a versioned, validated contract
helexa#179's application-owned system prompts let tireless shape both ends of the cc->oc handoff, so the "will a 27B execute an Opus plan" risk becomes a tunable rather than a hope. Three prompts, versioned as one set: plan.cc.md tells Claude Code it is writing for a literal, absent reader; implement.oc.md tells OpenCode to execute exactly that and report rather than improvise; implement.cc.md covers unplanned issues. PromptSet::load refuses a mismatched set, and tests assert the prompts mention every section ChildSpec requires. The middle is validated, not trusted. plan::validate rejects a plan before any implementation job is enqueued unless every child carries a runnable acceptance command (a stopping condition) and a non-empty out-of-scope list (a boundary) -- the two sections a small model needs and a human reader does not. Dangling and cyclic dependencies are caught too, and implementation_order derives the start order. cc uses --append-system-prompt, never --system-prompt: replacing Claude Code's default discards the tool-use scaffolding that makes it a coding agent. Pin bumped to 2.1.220, the version this flag surface was verified against. The oc path depends on helexa#179's passthrough guarantee, which is still open and unverified for qwen3 arch templating. Stage 5 now opens with a PONG probe rather than debugging it through a failed implementation run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHhHtohxcdk1PL3tfnYJdH
This commit is contained in:
18
CLAUDE.md
18
CLAUDE.md
@@ -39,6 +39,24 @@ tests. If one seems redundant, read design.md §3 before touching it.
|
||||
best-effort mirror. Do not make a decision by reading a label that could be
|
||||
made by reading the database.
|
||||
|
||||
7. **Never use `--system-prompt` for Claude Code; append instead.** Replacing
|
||||
Claude Code's default discards the tool-use scaffolding that makes it a
|
||||
coding agent. `SYSTEM_PROMPT_FLAG` is `--append-system-prompt` for this
|
||||
reason.
|
||||
|
||||
8. **The three prompts in `prompt/` are one contract — edit them together.**
|
||||
`plan.cc.md` emits the structure `implement.oc.md` consumes and
|
||||
`tireless_core::plan::validate` enforces. Changing one alone breaks the
|
||||
handoff silently, as a bad pull request rather than an error. Bump
|
||||
`contract-version:` in all three plus `SYSTEM_PROMPT_CONTRACT_VERSION`
|
||||
together; `PromptSet::load` refuses a mismatched set.
|
||||
|
||||
9. **A plan is validated, not trusted.** Never enqueue implementation work from
|
||||
a plan that has not passed `plan::validate`. The `Acceptance` (runnable
|
||||
stopping condition) and `Out of scope` (boundary) requirements exist because
|
||||
a small model needs them; do not relax them because a plan looks fine to a
|
||||
human reader.
|
||||
|
||||
## Quality gate
|
||||
|
||||
Before considering a change complete:
|
||||
|
||||
@@ -51,6 +51,16 @@ state_claimed = "tireless/claimed"
|
||||
state_blocked = "tireless/blocked"
|
||||
state_done = "tireless/done"
|
||||
|
||||
[prompt]
|
||||
# System prompts are compiled into the binary from prompt/*.md and are versioned
|
||||
# as a set (see prompt/readme.md). Point these at files on disk to override —
|
||||
# useful for iterating on plan quality without a redeploy. An override must
|
||||
# declare the same `contract-version:` as the build, or the service refuses to
|
||||
# start rather than run a mismatched pair.
|
||||
# plan_cc = "/etc/tireless/prompt/plan.cc.md"
|
||||
# implement_oc = "/etc/tireless/prompt/implement.oc.md"
|
||||
# implement_cc = "/etc/tireless/prompt/implement.cc.md"
|
||||
|
||||
[work]
|
||||
# Per-repo bare mirrors and per-job clones live here.
|
||||
root = "/var/lib/tireless"
|
||||
@@ -80,6 +90,10 @@ failure_threshold = 3
|
||||
provider = "lair-helexa"
|
||||
model = "Qwen/Qwen3.6-27B"
|
||||
base_url = "http://hanzalova.internal:31313/v1"
|
||||
# The implementation system prompt reaches the model through
|
||||
# OpenCode -> cortex -> neuron, relying on helexa's faithful-passthrough
|
||||
# guarantee (helexa/helexa#179, still open). If a run behaves as though it never
|
||||
# saw its prompt, suspect the passthrough before the prompt.
|
||||
max_concurrent = 2
|
||||
max_runs_per_window = 240
|
||||
window_hours = 5
|
||||
|
||||
@@ -9,7 +9,20 @@ use std::path::PathBuf;
|
||||
|
||||
/// Pinned rather than `@latest`: an unattended driver should not pick up a new
|
||||
/// agent version between one job and the next.
|
||||
pub const CLAUDE_PACKAGE: &str = "@anthropic-ai/claude-code@2.1.119";
|
||||
///
|
||||
/// This is the version whose flag surface tireless was verified against —
|
||||
/// specifically `--append-system-prompt`, which the planning and implementation
|
||||
/// prompts depend on. Bumping it means re-checking that surface, not just the
|
||||
/// number.
|
||||
pub const CLAUDE_PACKAGE: &str = "@anthropic-ai/claude-code@2.1.220";
|
||||
|
||||
/// How tireless applies its system prompt to Claude Code.
|
||||
///
|
||||
/// **Append, never replace.** Claude Code also accepts `--system-prompt`, which
|
||||
/// discards its default entirely — including the tool-use and repository
|
||||
/// navigation scaffolding that makes it a coding agent. Replacing yields a less
|
||||
/// capable agent, not a more obedient one. See `prompt/readme.md`.
|
||||
pub const SYSTEM_PROMPT_FLAG: &str = "--append-system-prompt";
|
||||
|
||||
pub struct ClaudeCodeExecutor {
|
||||
/// Home directory of the service account. Claude Code keeps its credentials
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
//! `tireless-agent` (process orchestration).
|
||||
|
||||
pub mod budget;
|
||||
pub mod plan;
|
||||
pub mod port;
|
||||
pub mod prompt;
|
||||
pub mod routing;
|
||||
|
||||
pub use budget::{Governor, LaneBudget, LimitSignal, Verdict};
|
||||
pub use plan::{PlanDefect, implementation_order, validate};
|
||||
pub use prompt::{PromptSet, SYSTEM_PROMPT_CONTRACT_VERSION};
|
||||
pub use routing::{RouteDecision, route};
|
||||
|
||||
294
crates/tireless-core/src/plan.rs
Normal file
294
crates/tireless-core/src/plan.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
//! Parsing and validating tireless-authored plans.
|
||||
//!
|
||||
//! A planning run's output is not trusted because a strong model produced it.
|
||||
//! It is parsed and checked against [`tireless_entities::ChildSpec`] before any
|
||||
//! implementation job is enqueued.
|
||||
//!
|
||||
//! This is the cheapest possible place to catch a bad plan. A plan that fails
|
||||
//! validation costs one comment on an issue and a `tireless/blocked` label,
|
||||
//! within seconds of the planning run finishing. The same plan, unvalidated,
|
||||
//! costs an OpenCode run against a repo, a branch, and an operator's review
|
||||
//! attention before anyone notices the spec was unusable.
|
||||
|
||||
use tireless_entities::{Acceptance, ChildSpec, FileTouch, PlanSpec};
|
||||
|
||||
/// Why a plan was rejected.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PlanDefect {
|
||||
MissingSection {
|
||||
child: String,
|
||||
section: &'static str,
|
||||
},
|
||||
/// The planner named no files at all. It has not thought about where the
|
||||
/// change lands.
|
||||
NoFiles { child: String },
|
||||
/// Acceptance is prose-only. An implementing model has no way to know when
|
||||
/// it is finished, which is the single most common cause of a run that
|
||||
/// keeps going and rewrites things it was not asked to.
|
||||
NoRunnableAcceptance { child: String },
|
||||
/// No boundary was set, so nothing stops the implementer expanding scope.
|
||||
NoBoundary { child: String },
|
||||
/// A dependency names a child that is not in the plan.
|
||||
UnknownDependency { child: String, depends_on: String },
|
||||
/// Dependencies form a cycle, so no implementation order exists.
|
||||
DependencyCycle { children: Vec<String> },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PlanDefect {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MissingSection { child, section } => {
|
||||
write!(f, "child {child:?} is missing its `{section}` section")
|
||||
}
|
||||
Self::NoFiles { child } => {
|
||||
write!(f, "child {child:?} names no files it expects to change")
|
||||
}
|
||||
Self::NoRunnableAcceptance { child } => write!(
|
||||
f,
|
||||
"child {child:?} has no runnable acceptance command, so an \
|
||||
implementer has no stopping condition"
|
||||
),
|
||||
Self::NoBoundary { child } => write!(
|
||||
f,
|
||||
"child {child:?} declares nothing out of scope, so nothing bounds \
|
||||
the implementation"
|
||||
),
|
||||
Self::UnknownDependency { child, depends_on } => write!(
|
||||
f,
|
||||
"child {child:?} depends on {depends_on:?}, which is not in the plan"
|
||||
),
|
||||
Self::DependencyCycle { children } => {
|
||||
write!(f, "dependency cycle among {children:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check a plan is executable. Returns every defect, not just the first — a
|
||||
/// planning re-run is expensive, so the feedback comment should be complete.
|
||||
pub fn validate(plan: &PlanSpec) -> Vec<PlanDefect> {
|
||||
let mut defects = Vec::new();
|
||||
|
||||
for child in &plan.children {
|
||||
let name = child.title.clone();
|
||||
let spec = &child.spec;
|
||||
|
||||
if spec.goal.trim().is_empty() {
|
||||
defects.push(PlanDefect::MissingSection {
|
||||
child: name.clone(),
|
||||
section: "goal",
|
||||
});
|
||||
}
|
||||
if spec.steps.is_empty() {
|
||||
defects.push(PlanDefect::MissingSection {
|
||||
child: name.clone(),
|
||||
section: "steps",
|
||||
});
|
||||
}
|
||||
if spec.files.is_empty() {
|
||||
defects.push(PlanDefect::NoFiles {
|
||||
child: name.clone(),
|
||||
});
|
||||
}
|
||||
if spec.acceptance.is_empty() {
|
||||
defects.push(PlanDefect::MissingSection {
|
||||
child: name.clone(),
|
||||
section: "acceptance",
|
||||
});
|
||||
} else if !spec.acceptance.iter().any(Acceptance::is_command) {
|
||||
defects.push(PlanDefect::NoRunnableAcceptance {
|
||||
child: name.clone(),
|
||||
});
|
||||
}
|
||||
if spec.out_of_scope.is_empty() {
|
||||
defects.push(PlanDefect::NoBoundary {
|
||||
child: name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
for dep in &child.depends_on {
|
||||
if !plan.children.iter().any(|c| &c.title == dep) {
|
||||
defects.push(PlanDefect::UnknownDependency {
|
||||
child: name.clone(),
|
||||
depends_on: dep.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cycle) = find_cycle(plan) {
|
||||
defects.push(PlanDefect::DependencyCycle { children: cycle });
|
||||
}
|
||||
|
||||
defects
|
||||
}
|
||||
|
||||
/// Implementation order: children whose dependencies are all satisfied come
|
||||
/// first. Returns `None` if the graph does not admit an order.
|
||||
pub fn implementation_order(plan: &PlanSpec) -> Option<Vec<&str>> {
|
||||
let mut remaining: Vec<&str> = plan.children.iter().map(|c| c.title.as_str()).collect();
|
||||
let mut ordered: Vec<&str> = Vec::new();
|
||||
|
||||
while !remaining.is_empty() {
|
||||
// Take every child whose dependencies are already ordered. Taking all
|
||||
// ready children per round (rather than one) keeps siblings that could
|
||||
// run concurrently adjacent in the output.
|
||||
let ready: Vec<&str> = remaining
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|title| {
|
||||
let child = plan
|
||||
.children
|
||||
.iter()
|
||||
.find(|c| c.title == *title)
|
||||
.expect("present");
|
||||
child
|
||||
.depends_on
|
||||
.iter()
|
||||
.all(|dep| ordered.contains(&dep.as_str()) || !title_exists(plan, dep))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if ready.is_empty() {
|
||||
return None; // cycle
|
||||
}
|
||||
remaining.retain(|t| !ready.contains(t));
|
||||
ordered.extend(ready);
|
||||
}
|
||||
Some(ordered)
|
||||
}
|
||||
|
||||
fn title_exists(plan: &PlanSpec, title: &str) -> bool {
|
||||
plan.children.iter().any(|c| c.title == title)
|
||||
}
|
||||
|
||||
fn find_cycle(plan: &PlanSpec) -> Option<Vec<String>> {
|
||||
if implementation_order(plan).is_some() {
|
||||
return None;
|
||||
}
|
||||
// Everything that could not be ordered participates in, or depends on, a cycle.
|
||||
Some(plan.children.iter().map(|c| c.title.clone()).collect())
|
||||
}
|
||||
|
||||
/// A well-formed child, for tests and for the dry-run executor.
|
||||
pub fn example_child() -> ChildSpec {
|
||||
ChildSpec {
|
||||
goal: "Add an ETag cache to the Gitea poller so repeat polls are conditional.".into(),
|
||||
files: vec![FileTouch {
|
||||
path: "crates/tireless-data/src/forge.rs".into(),
|
||||
note: "store and send If-None-Match".into(),
|
||||
}],
|
||||
steps: vec![
|
||||
"Add `last_etag` to the repo row on read.".into(),
|
||||
"Send `If-None-Match` when it is present.".into(),
|
||||
"Treat 304 as an empty result.".into(),
|
||||
],
|
||||
acceptance: vec![
|
||||
Acceptance::Command("cargo test -p tireless-data".into()),
|
||||
Acceptance::Criterion("A second poll within the interval logs a 304.".into()),
|
||||
],
|
||||
out_of_scope: vec!["Do not change the poll scheduling logic.".into()],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tireless_entities::PlannedChild;
|
||||
|
||||
fn child(title: &str, depends_on: &[&str]) -> PlannedChild {
|
||||
PlannedChild {
|
||||
title: title.into(),
|
||||
spec: example_child(),
|
||||
depends_on: depends_on.iter().map(|s| s.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan(children: Vec<PlannedChild>) -> PlanSpec {
|
||||
PlanSpec {
|
||||
epic_title: "epic".into(),
|
||||
epic_goal: "goal".into(),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_well_formed_plan_validates() {
|
||||
assert!(validate(&plan(vec![child("a", &[])])).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prose_only_acceptance_is_rejected() {
|
||||
let mut c = child("a", &[]);
|
||||
c.spec.acceptance = vec![Acceptance::Criterion("it works".into())];
|
||||
let defects = validate(&plan(vec![c]));
|
||||
assert!(matches!(
|
||||
defects[0],
|
||||
PlanDefect::NoRunnableAcceptance { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plan_without_a_boundary_is_rejected() {
|
||||
let mut c = child("a", &[]);
|
||||
c.spec.out_of_scope.clear();
|
||||
let defects = validate(&plan(vec![c]));
|
||||
assert!(matches!(defects[0], PlanDefect::NoBoundary { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_child_naming_no_files_is_rejected() {
|
||||
let mut c = child("a", &[]);
|
||||
c.spec.files.clear();
|
||||
let defects = validate(&plan(vec![c]));
|
||||
assert!(matches!(defects[0], PlanDefect::NoFiles { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_defect_is_reported_not_just_the_first() {
|
||||
let mut c = child("a", &[]);
|
||||
c.spec.files.clear();
|
||||
c.spec.out_of_scope.clear();
|
||||
c.spec.goal = " ".into();
|
||||
let defects = validate(&plan(vec![c]));
|
||||
assert_eq!(defects.len(), 3, "got {defects:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dependency_on_a_missing_child_is_rejected() {
|
||||
let defects = validate(&plan(vec![child("a", &["nonexistent"])]));
|
||||
assert!(
|
||||
defects
|
||||
.iter()
|
||||
.any(|d| matches!(d, PlanDefect::UnknownDependency { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_determine_implementation_order() {
|
||||
let p = plan(vec![
|
||||
child("c", &["b"]),
|
||||
child("a", &[]),
|
||||
child("b", &["a"]),
|
||||
]);
|
||||
assert_eq!(implementation_order(&p), Some(vec!["a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_children_all_come_out() {
|
||||
let p = plan(vec![child("a", &[]), child("b", &[])]);
|
||||
let order = implementation_order(&p).expect("orderable");
|
||||
assert_eq!(order.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cycle_has_no_order_and_is_reported() {
|
||||
let p = plan(vec![child("a", &["b"]), child("b", &["a"])]);
|
||||
assert_eq!(implementation_order(&p), None);
|
||||
assert!(
|
||||
validate(&p)
|
||||
.iter()
|
||||
.any(|d| matches!(d, PlanDefect::DependencyCycle { .. }))
|
||||
);
|
||||
}
|
||||
}
|
||||
154
crates/tireless-core/src/prompt.rs
Normal file
154
crates/tireless-core/src/prompt.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! System prompts, as a versioned pair.
|
||||
//!
|
||||
//! tireless tailors both agents by system prompt, but the two surfaces differ
|
||||
//! in what they will let you do, and the difference is not cosmetic:
|
||||
//!
|
||||
//! - **Claude Code** — `--append-system-prompt`. tireless *appends*; it does not
|
||||
//! replace. Claude Code's default system prompt carries the tool-use and
|
||||
//! repository-navigation scaffolding that makes it a coding agent, and
|
||||
//! `--system-prompt` discards all of it. Replacing gets you a less capable
|
||||
//! agent, not a more obedient one.
|
||||
//!
|
||||
//! - **OpenCode** — `AgentConfig.prompt`. tireless owns the whole system prompt,
|
||||
//! which travels through OpenCode to helexa cortex and on to a neuron. That
|
||||
//! path depends on helexa's faithful-passthrough guarantee
|
||||
//! (helexa/helexa#179): no injection, no rewriting, no defaults.
|
||||
//!
|
||||
//! ## The pair is one contract
|
||||
//!
|
||||
//! The planning prompt tells Claude Code what a plan must contain. The
|
||||
//! implementation prompt tells OpenCode to execute exactly that and nothing
|
||||
//! more. Both describe the same structure — [`tireless_entities::ChildSpec`] —
|
||||
//! from opposite ends. Editing one without the other silently breaks the
|
||||
//! handoff: the planner emits a shape the implementer is not expecting, and the
|
||||
//! failure shows up as a puzzlingly bad PR rather than as an error.
|
||||
//!
|
||||
//! [`SYSTEM_PROMPT_CONTRACT_VERSION`] exists to make that breakage loud. Both
|
||||
//! prompt files declare it; [`PromptSet::load`] refuses a mismatched pair.
|
||||
|
||||
use tireless_entities::Error;
|
||||
|
||||
/// Bumped whenever the plan contract changes shape. Both prompt files carry a
|
||||
/// `contract-version:` line that must match.
|
||||
pub const SYSTEM_PROMPT_CONTRACT_VERSION: u32 = 1;
|
||||
|
||||
/// The paired prompts, loaded and checked together.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromptSet {
|
||||
/// Appended to Claude Code's system prompt for `Plan` jobs.
|
||||
pub plan_cc: String,
|
||||
/// The whole system prompt for OpenCode implementation agents.
|
||||
pub implement_oc: String,
|
||||
/// Appended to Claude Code's system prompt for `Implement` jobs on
|
||||
/// unplanned, human-written issues.
|
||||
pub implement_cc: String,
|
||||
}
|
||||
|
||||
impl PromptSet {
|
||||
/// Load a prompt set, verifying every member declares the same contract
|
||||
/// version as this build.
|
||||
pub fn load(plan_cc: &str, implement_oc: &str, implement_cc: &str) -> Result<Self, Error> {
|
||||
for (name, body) in [
|
||||
("plan.cc.md", plan_cc),
|
||||
("implement.oc.md", implement_oc),
|
||||
("implement.cc.md", implement_cc),
|
||||
] {
|
||||
let declared = parse_contract_version(body).ok_or_else(|| {
|
||||
Error::Config(format!("prompt {name} declares no `contract-version:`"))
|
||||
})?;
|
||||
if declared != SYSTEM_PROMPT_CONTRACT_VERSION {
|
||||
return Err(Error::Config(format!(
|
||||
"prompt {name} declares contract-version {declared}, but this build \
|
||||
expects {SYSTEM_PROMPT_CONTRACT_VERSION}; the planning and \
|
||||
implementation prompts must be updated together"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
plan_cc: plan_cc.to_string(),
|
||||
implement_oc: implement_oc.to_string(),
|
||||
implement_cc: implement_cc.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The prompts shipped with this build.
|
||||
pub fn builtin() -> Result<Self, Error> {
|
||||
Self::load(
|
||||
include_str!("../../../prompt/plan.cc.md"),
|
||||
include_str!("../../../prompt/implement.oc.md"),
|
||||
include_str!("../../../prompt/implement.cc.md"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `contract-version: N` from a prompt file's leading metadata block.
|
||||
fn parse_contract_version(body: &str) -> Option<u32> {
|
||||
body.lines()
|
||||
.take_while(|l| !l.starts_with("# "))
|
||||
.find_map(|l| l.trim().strip_prefix("contract-version:"))
|
||||
.and_then(|v| v.trim().parse().ok())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_shipped_prompts_are_a_consistent_set() {
|
||||
PromptSet::builtin().expect("shipped prompts must load");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_planning_prompt_names_every_required_section() {
|
||||
// If a section is added to ChildSpec but not to the planning prompt, the
|
||||
// planner will not emit it and every plan will fail validation. Catch
|
||||
// that here rather than on a live run.
|
||||
let set = PromptSet::builtin().expect("load");
|
||||
for section in ["Goal", "Files", "Steps", "Acceptance", "Out of scope"] {
|
||||
assert!(
|
||||
set.plan_cc.contains(section),
|
||||
"planning prompt does not mention required section {section:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_implementation_prompt_names_the_same_sections() {
|
||||
let set = PromptSet::builtin().expect("load");
|
||||
for section in ["Acceptance", "Out of scope"] {
|
||||
assert!(
|
||||
set.implement_oc.contains(section),
|
||||
"implementation prompt does not mention {section:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mismatched_pair_is_refused() {
|
||||
let good = format!("contract-version: {SYSTEM_PROMPT_CONTRACT_VERSION}\n\n# x\n");
|
||||
let stale = format!(
|
||||
"contract-version: {}\n\n# x\n",
|
||||
SYSTEM_PROMPT_CONTRACT_VERSION + 1
|
||||
);
|
||||
assert!(PromptSet::load(&good, &stale, &good).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_undeclared_version_is_refused() {
|
||||
let good = format!("contract-version: {SYSTEM_PROMPT_CONTRACT_VERSION}\n\n# x\n");
|
||||
assert!(PromptSet::load(&good, "# no metadata\n", &good).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_version_is_read_only_from_leading_metadata() {
|
||||
// A `contract-version:` mentioned in prose below the title must not be
|
||||
// mistaken for the declaration.
|
||||
let body = format!(
|
||||
"contract-version: {SYSTEM_PROMPT_CONTRACT_VERSION}\n\n# Title\n\ncontract-version: 99\n"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_contract_version(&body),
|
||||
Some(SYSTEM_PROMPT_CONTRACT_VERSION)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod error;
|
||||
pub mod forge;
|
||||
pub mod job;
|
||||
pub mod label;
|
||||
pub mod plan;
|
||||
pub mod repo;
|
||||
pub mod run;
|
||||
|
||||
@@ -15,5 +16,6 @@ pub use error::Error;
|
||||
pub use forge::{Forge, IssueRef, PullRequestRef};
|
||||
pub use job::{Job, JobKind, JobState};
|
||||
pub use label::LabelProtocol;
|
||||
pub use plan::{Acceptance, ChildSpec, FileTouch, PlanSpec, PlannedChild};
|
||||
pub use repo::{PollSchedule, TrackedRepo};
|
||||
pub use run::{AgentKind, AgentRun, BillingMode, RunOutcome};
|
||||
|
||||
78
crates/tireless-entities/src/plan.rs
Normal file
78
crates/tireless-entities/src/plan.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
/// The required structure of a tireless-authored child issue.
|
||||
///
|
||||
/// This type is one half of a contract. The Claude Code planning prompt is told
|
||||
/// to emit exactly these sections; the OpenCode implementation prompt is told to
|
||||
/// work from exactly these sections and nothing else. Neither prompt may be
|
||||
/// changed without the other — see `prompt/readme.md`.
|
||||
///
|
||||
/// The contract exists because a 27B model executing a plan needs two things a
|
||||
/// human reader does not: an explicit **stopping condition** (`acceptance`) and
|
||||
/// an explicit **boundary** (`out_of_scope`). Without the first it does not know
|
||||
/// when it is finished; without the second it improvises past the task. Both are
|
||||
/// therefore mandatory, and a plan lacking either is rejected before any
|
||||
/// implementation job is enqueued.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ChildSpec {
|
||||
/// What changes and why. One paragraph.
|
||||
pub goal: String,
|
||||
/// Files expected to change, each with a note on what changes there.
|
||||
/// Advisory rather than binding — but a plan that cannot name any file it
|
||||
/// expects to touch has not been thought through.
|
||||
pub files: Vec<FileTouch>,
|
||||
/// Ordered implementation steps.
|
||||
pub steps: Vec<String>,
|
||||
/// How to know the work is done. At least one must be a runnable command;
|
||||
/// prose-only acceptance gives an implementing model no stopping condition.
|
||||
pub acceptance: Vec<Acceptance>,
|
||||
/// What the implementer must *not* do. The boundary that keeps a capable
|
||||
/// model from redesigning adjacent code it was not asked to touch.
|
||||
pub out_of_scope: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct FileTouch {
|
||||
pub path: String,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
|
||||
pub enum Acceptance {
|
||||
/// A command that must exit zero. The stopping condition.
|
||||
Command(String),
|
||||
/// A prose criterion, for things no command can check.
|
||||
Criterion(String),
|
||||
}
|
||||
|
||||
impl Acceptance {
|
||||
pub fn is_command(&self) -> bool {
|
||||
matches!(self, Self::Command(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// A full plan: the epic framing plus its children.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct PlanSpec {
|
||||
/// Title for the epic issue.
|
||||
pub epic_title: String,
|
||||
/// Framing for the epic body: what the whole change achieves.
|
||||
pub epic_goal: String,
|
||||
pub children: Vec<PlannedChild>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct PlannedChild {
|
||||
pub title: String,
|
||||
pub spec: ChildSpec,
|
||||
/// Titles of siblings that must land first. Used to order implementation
|
||||
/// jobs so a child is never started before its dependency is merged.
|
||||
pub depends_on: Vec<String>,
|
||||
}
|
||||
3
dashboard/src/api/generated/Acceptance.ts
Normal file
3
dashboard/src/api/generated/Acceptance.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Acceptance = { "kind": "command", "value": string } | { "kind": "criterion", "value": string };
|
||||
44
dashboard/src/api/generated/ChildSpec.ts
Normal file
44
dashboard/src/api/generated/ChildSpec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Acceptance } from "./Acceptance";
|
||||
import type { FileTouch } from "./FileTouch";
|
||||
|
||||
/**
|
||||
* The required structure of a tireless-authored child issue.
|
||||
*
|
||||
* This type is one half of a contract. The Claude Code planning prompt is told
|
||||
* to emit exactly these sections; the OpenCode implementation prompt is told to
|
||||
* work from exactly these sections and nothing else. Neither prompt may be
|
||||
* changed without the other — see `prompt/readme.md`.
|
||||
*
|
||||
* The contract exists because a 27B model executing a plan needs two things a
|
||||
* human reader does not: an explicit **stopping condition** (`acceptance`) and
|
||||
* an explicit **boundary** (`out_of_scope`). Without the first it does not know
|
||||
* when it is finished; without the second it improvises past the task. Both are
|
||||
* therefore mandatory, and a plan lacking either is rejected before any
|
||||
* implementation job is enqueued.
|
||||
*/
|
||||
export type ChildSpec = {
|
||||
/**
|
||||
* What changes and why. One paragraph.
|
||||
*/
|
||||
goal: string,
|
||||
/**
|
||||
* Files expected to change, each with a note on what changes there.
|
||||
* Advisory rather than binding — but a plan that cannot name any file it
|
||||
* expects to touch has not been thought through.
|
||||
*/
|
||||
files: Array<FileTouch>,
|
||||
/**
|
||||
* Ordered implementation steps.
|
||||
*/
|
||||
steps: Array<string>,
|
||||
/**
|
||||
* How to know the work is done. At least one must be a runnable command;
|
||||
* prose-only acceptance gives an implementing model no stopping condition.
|
||||
*/
|
||||
acceptance: Array<Acceptance>,
|
||||
/**
|
||||
* What the implementer must *not* do. The boundary that keeps a capable
|
||||
* model from redesigning adjacent code it was not asked to touch.
|
||||
*/
|
||||
out_of_scope: Array<string>, };
|
||||
3
dashboard/src/api/generated/FileTouch.ts
Normal file
3
dashboard/src/api/generated/FileTouch.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FileTouch = { path: string, note: string, };
|
||||
15
dashboard/src/api/generated/PlanSpec.ts
Normal file
15
dashboard/src/api/generated/PlanSpec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PlannedChild } from "./PlannedChild";
|
||||
|
||||
/**
|
||||
* A full plan: the epic framing plus its children.
|
||||
*/
|
||||
export type PlanSpec = {
|
||||
/**
|
||||
* Title for the epic issue.
|
||||
*/
|
||||
epic_title: string,
|
||||
/**
|
||||
* Framing for the epic body: what the whole change achieves.
|
||||
*/
|
||||
epic_goal: string, children: Array<PlannedChild>, };
|
||||
9
dashboard/src/api/generated/PlannedChild.ts
Normal file
9
dashboard/src/api/generated/PlannedChild.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ChildSpec } from "./ChildSpec";
|
||||
|
||||
export type PlannedChild = { title: string, spec: ChildSpec,
|
||||
/**
|
||||
* Titles of siblings that must land first. Used to order implementation
|
||||
* jobs so a child is never started before its dependency is merged.
|
||||
*/
|
||||
depends_on: Array<string>, };
|
||||
@@ -95,6 +95,65 @@ This also produces a pleasing economic shape. The subscription is spent on the
|
||||
scarce thing (planning, and interpreting under-specified work), while the bulk
|
||||
of mechanical implementation runs on hardware already sitting in the office.
|
||||
|
||||
### 2.4 The plan contract
|
||||
|
||||
The handoff between the lanes is the load-bearing interface in this design: an
|
||||
Opus-authored plan must be specific enough for a 27B model to execute alone. That
|
||||
is not left to chance in either direction.
|
||||
|
||||
**Both ends are shaped by system prompts.** `prompt/plan.cc.md` tells Claude Code
|
||||
it is writing for a literal, competent, absent reader that cannot ask questions
|
||||
and will fill any gap with an invention. `prompt/implement.oc.md` tells OpenCode
|
||||
to execute the specification faithfully, stop when acceptance passes, and report
|
||||
rather than improvise. They are two halves of one contract and are versioned
|
||||
together — see `prompt/readme.md`.
|
||||
|
||||
The surfaces are not symmetric, and the difference matters:
|
||||
|
||||
| Lane | Mechanism | Ownership |
|
||||
| --- | --- | --- |
|
||||
| Claude Code | `--append-system-prompt` | Anthropic owns the base prompt; tireless appends |
|
||||
| OpenCode | `AgentConfig.prompt` | tireless owns the whole prompt |
|
||||
|
||||
Claude Code also offers `--system-prompt`, which replaces its default outright.
|
||||
tireless does not use it: the default carries the tool-use and repository
|
||||
navigation scaffolding that makes Claude Code a coding agent, and discarding it
|
||||
yields a less capable agent rather than a more obedient one.
|
||||
|
||||
**The middle is validated, not trusted.** A plan is parsed into
|
||||
`tireless_entities::ChildSpec` and checked by `tireless_core::plan::validate`
|
||||
before any implementation job is enqueued. Every child must carry five sections,
|
||||
two of which exist specifically because a small model needs them and a human
|
||||
reader does not:
|
||||
|
||||
- **Acceptance** must include at least one runnable command. Without a stopping
|
||||
condition a literal implementer does not know when it is finished, and keeps
|
||||
going — usually by rewriting adjacent code it was not asked to touch.
|
||||
- **Out of scope** must be non-empty. Without a declared boundary nothing stops a
|
||||
capable model expanding the work.
|
||||
|
||||
Plans are also checked for dangling and cyclic dependencies, and
|
||||
`implementation_order` derives the order in which children may be started.
|
||||
|
||||
This makes the risk cheap to discover. A plan that fails validation costs one
|
||||
comment and a `tireless/blocked` label, seconds after the planning run. The same
|
||||
plan unvalidated costs an OpenCode run, a branch, and an operator's review
|
||||
attention before anyone notices the spec was unusable.
|
||||
|
||||
**Dependency: helexa faithful passthrough.** The OpenCode system prompt reaches
|
||||
the model via OpenCode → cortex → neuron, which relies on helexa's guarantee of
|
||||
no injection, no rewriting, no defaults
|
||||
([helexa/helexa#179](https://git.lair.cafe/helexa/helexa/issues/179)).
|
||||
|
||||
That issue is **open** — the principle was decided 2026-07-17, but verification
|
||||
and documentation are outstanding, and two unchecked items bear directly here:
|
||||
neuron chat templating applying the system role correctly per arch family
|
||||
*including `/no_think` interaction* (Qwen3.6-27B is the target model), and
|
||||
behaviour with multiple system messages. Stage 5 is effectively the second
|
||||
consumer of that guarantee after the chat SPA, and is well placed to surface
|
||||
exactly those bugs. Debugging order follows from this: if an OpenCode run behaves
|
||||
as though it never saw its prompt, suspect the passthrough before the prompt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Constraints
|
||||
@@ -382,12 +441,19 @@ Spawn the pinned CLI, read `stream-json`, capture session id for `--resume`,
|
||||
record `apiKeySource` as billing mode, consume `rate_limit_event` into the
|
||||
governor, enforce budgets and the circuit breaker.
|
||||
|
||||
Apply `prompt/plan.cc.md` via `--append-system-prompt`; parse the result into
|
||||
`PlanSpec` and gate it through `plan::validate` before creating any issue.
|
||||
|
||||
First real capability: `tireless/plan` on a real issue produces an epic and child
|
||||
issues.
|
||||
|
||||
*Done when:* a planning run completes against a real issue, the children are
|
||||
sensible, the journal shows the billing mode, and an artificially lowered window
|
||||
budget demonstrably holds the lane.
|
||||
*Done when:* a planning run completes against a real issue, the plan passes
|
||||
validation, the children are sensible **when read as an implementer would read
|
||||
them** — cold, with no other context — the journal shows the billing mode, and an
|
||||
artificially lowered window budget demonstrably holds the lane.
|
||||
|
||||
This is also where plan quality is judged, while the only cost of a bad plan is a
|
||||
comment thread. Iterate on `prompt/plan.cc.md` here, not in stage 5.
|
||||
|
||||
*Why planning first:* the output is issues, not code. A bad plan is a comment
|
||||
thread; a bad implementation is a branch. Start where mistakes are cheapest.
|
||||
@@ -404,11 +470,19 @@ than duplicates.
|
||||
### Stage 5 — OpenCode executor
|
||||
|
||||
Spawn `opencode serve` on loopback with a per-spawn password, drive it over HTTP,
|
||||
target helexa cortex. Enforce `assert_not_anthropic` from config. Route
|
||||
plan-descended implementation jobs here.
|
||||
target helexa cortex. Enforce `assert_not_anthropic` from config. Register a
|
||||
custom OpenCode agent carrying `prompt/implement.oc.md` as its system prompt.
|
||||
Route plan-descended implementation jobs here.
|
||||
|
||||
**Start with a passthrough probe.** Before wiring anything real, confirm a system
|
||||
prompt reaches Qwen3.6-27B intact through OpenCode → cortex → neuron — the
|
||||
"reply only with the word PONG" test named in helexa#179. That guarantee is not
|
||||
yet verified (§2.4), and discovering it fails here is far cheaper than debugging
|
||||
it through a failed implementation run.
|
||||
|
||||
*Done when:* a child issue created by a stage-3 planning run is implemented
|
||||
end-to-end by OpenCode on the GPU fleet, with zero subscription usage.
|
||||
end-to-end by OpenCode on the GPU fleet, with zero subscription usage, and the
|
||||
run demonstrably respected its `Out of scope` section.
|
||||
|
||||
### Stage 6 — Scheduling and dashboard control
|
||||
|
||||
@@ -475,6 +549,20 @@ models implement — rests on tireless-authored plans being specific enough for
|
||||
fails, the fallback is routing more implementation to Claude Code, which costs
|
||||
subscription budget but not a redesign.
|
||||
|
||||
The plan contract (§2.4) narrows this considerably: paired system prompts shape
|
||||
both ends, and structural validation rejects a plan lacking a runnable stopping
|
||||
condition or a declared boundary before any implementation job is enqueued. What
|
||||
remains genuinely unknown is *semantic* quality — whether a plan that satisfies
|
||||
the schema is also correct and specific enough in substance. No validator
|
||||
catches a well-formed plan that is simply wrong about the codebase, and there is
|
||||
no unit test for whether a prompt produces good plans. That is measured on real
|
||||
issues in stage 3, before stage 5 spends anything on acting on them.
|
||||
|
||||
**The OpenCode prompt path is unverified end to end.** It depends on helexa#179,
|
||||
which is open (§2.4). Worth confirming with a trivial probe — a system prompt
|
||||
that measurably changes output — at the start of stage 5 rather than debugging it
|
||||
through a failed implementation run.
|
||||
|
||||
**Not yet decided:** whether a failed implementation should automatically open a
|
||||
`tireless/blocked` issue describing what it could not do, or simply comment on
|
||||
the original. Deferred to stage 4, when there is real failure data to look at.
|
||||
|
||||
55
prompt/implement.cc.md
Normal file
55
prompt/implement.cc.md
Normal file
@@ -0,0 +1,55 @@
|
||||
contract-version: 1
|
||||
surface: claude-code --append-system-prompt
|
||||
job-kind: Implement (unplanned)
|
||||
|
||||
# Implementing an unplanned issue
|
||||
|
||||
You are implementing an issue written by a human, which has **not** been through
|
||||
tireless planning. There is no spec: no file list, no acceptance commands, no
|
||||
declared boundary. Interpreting the request is part of your job, which is why
|
||||
this work was routed to you rather than to the local implementation model.
|
||||
|
||||
Work as a careful colleague would.
|
||||
|
||||
## Rules
|
||||
|
||||
**Establish the boundary yourself, then hold it.** The issue almost certainly
|
||||
does not say what is out of scope. Decide what the smallest complete change is,
|
||||
state that decision in the pull request description, and stay inside it.
|
||||
Adjacent problems you notice get *mentioned*, not fixed.
|
||||
|
||||
**Find your own stopping condition.** No acceptance command was given, so
|
||||
identify one: the test that ought to pass, the command that ought to succeed.
|
||||
Run it. If the repository has no way to verify the change, say so explicitly in
|
||||
the PR rather than implying verification you did not do.
|
||||
|
||||
**Interpret ambiguity the way a careful colleague would.** Make routine judgement
|
||||
calls and record them. Where different readings would produce materially
|
||||
different work, implement the most defensible one and flag the alternative in the
|
||||
pull request — do not silently pick and move on, and do not stall waiting for an
|
||||
answer that cannot arrive.
|
||||
|
||||
**If the issue is too vague to implement, say so and stop.** A one-line issue with
|
||||
no discernible acceptance criteria is a planning problem, not an implementation
|
||||
problem. Report what is missing and suggest the issue be relabelled
|
||||
`tireless/plan`. That is a correct outcome, not a failure.
|
||||
|
||||
**Match the surrounding code.** Naming, error handling, comment density, test
|
||||
placement. A repository `CLAUDE.md`, `AGENTS.md`, or contributing guide outranks
|
||||
your defaults.
|
||||
|
||||
**Never fake a green result.** Do not weaken assertions, skip tests, or
|
||||
special-case values to make something pass. Report failures with their output.
|
||||
|
||||
## Your output
|
||||
|
||||
A branch and a pull request describing:
|
||||
|
||||
1. what you implemented, and the scope you decided on;
|
||||
2. how you verified it, with the commands you actually ran;
|
||||
3. judgement calls you made where another reading was defensible;
|
||||
4. anything you noticed but deliberately left alone.
|
||||
|
||||
Be honest about what is unfinished or unverified. This work goes to a human
|
||||
reviewer who is deciding whether to merge it, and an accurate account of what you
|
||||
did not do is worth more than a confident summary that overstates.
|
||||
78
prompt/implement.oc.md
Normal file
78
prompt/implement.oc.md
Normal file
@@ -0,0 +1,78 @@
|
||||
contract-version: 1
|
||||
surface: opencode AgentConfig.prompt
|
||||
job-kind: Implement (plan-descended)
|
||||
|
||||
# Implementing a specified change
|
||||
|
||||
You are implementing one child issue from a plan that has already been written,
|
||||
reviewed for structure, and approved. Your job is to execute that specification
|
||||
faithfully and open a pull request. It is not to improve the plan.
|
||||
|
||||
The issue you have been given contains five sections. They are the whole of your
|
||||
brief:
|
||||
|
||||
- **Goal** — what changes and why.
|
||||
- **Files** — where the change goes.
|
||||
- **Steps** — what to do, in order.
|
||||
- **Acceptance** — how you know you are finished.
|
||||
- **Out of scope** — what you must not touch.
|
||||
|
||||
## Rules
|
||||
|
||||
**Do not deviate from the plan.** If a step seems wrong, inefficient, or
|
||||
incomplete, that is a signal to stop and report — not to improvise a better
|
||||
approach. The plan was written with context you do not have. A change that
|
||||
improves on the plan is still a change nobody asked for, and it will be rejected
|
||||
in review even if it is an improvement.
|
||||
|
||||
**Stop when Acceptance passes.** Run the acceptance commands. When they exit
|
||||
zero and the criteria are met, you are done. Do not continue looking for things
|
||||
to improve. Do not tidy adjacent code. Do not fix unrelated warnings you noticed
|
||||
along the way. Finishing early is correct behaviour, not a failure of diligence.
|
||||
|
||||
**Respect Out of scope absolutely.** The listed items are off limits even if
|
||||
touching them would make your change cleaner, even if they contain an obvious
|
||||
bug, and even if a step seems to require it. If a step genuinely cannot be
|
||||
completed without touching something out of scope, that is a contradiction in the
|
||||
plan: stop and report it.
|
||||
|
||||
**Stay inside the named files where you can.** The Files section is where the
|
||||
change is expected to land. Touching a file it does not name is sometimes
|
||||
unavoidable — a new import, a call site that must be updated for the code to
|
||||
compile. That is fine. Rewriting a file it does not name is not.
|
||||
|
||||
**Match the surrounding code.** Follow the conventions of the files you are
|
||||
editing: naming, error handling, comment density, test placement. Your change
|
||||
should be hard to pick out of a diff by style alone. When the repository has a
|
||||
`CLAUDE.md`, `AGENTS.md`, or contributing guide, it outranks your defaults.
|
||||
|
||||
**Write the tests the plan asks for, and no more.** If Acceptance names a test
|
||||
command, make it pass honestly. Never weaken an assertion, skip a test, or
|
||||
special-case a value to get a green result. A test that passes because you
|
||||
narrowed it is worse than a failing test, because it hides the failure.
|
||||
|
||||
## When you cannot proceed
|
||||
|
||||
Stop and report. Do not guess, and do not deliver a partial change quietly.
|
||||
Report rather than continue when:
|
||||
|
||||
- a step is ambiguous and the readings lead to materially different code;
|
||||
- the plan contradicts what you find in the repository;
|
||||
- completing a step would require going out of scope;
|
||||
- an acceptance command fails for a reason the plan does not cover;
|
||||
- a file the plan names does not exist, or does not contain what was described.
|
||||
|
||||
Say plainly what you found, which step you stopped at, and what you would need in
|
||||
order to continue. A precise report is a useful outcome. A confident pull request
|
||||
built on a guess is not — it costs a reviewer more than the work was worth.
|
||||
|
||||
## Your output
|
||||
|
||||
A branch containing the change, and a pull request describing:
|
||||
|
||||
1. what you did, referenced against the plan's steps;
|
||||
2. the acceptance commands you ran, and their results;
|
||||
3. anything you noticed but deliberately left alone, because it was out of scope.
|
||||
|
||||
That third item matters. It is how observations reach the operator without you
|
||||
acting on them unilaterally.
|
||||
93
prompt/plan.cc.md
Normal file
93
prompt/plan.cc.md
Normal file
@@ -0,0 +1,93 @@
|
||||
contract-version: 1
|
||||
surface: claude-code --append-system-prompt
|
||||
job-kind: Plan
|
||||
|
||||
# Planning for downstream execution
|
||||
|
||||
You are decomposing an issue into an epic and child issues. Your plans are not
|
||||
read by a person and then implemented by that person. Each child issue is handed,
|
||||
on its own and without you present, to a **27-billion-parameter local model** that
|
||||
will attempt to implement it end to end and open a pull request.
|
||||
|
||||
That model is competent but literal. It has no access to your reasoning, cannot
|
||||
ask you a follow-up question, and will not notice an omission — it will fill the
|
||||
gap with an invention. Everything it needs must be in the child issue.
|
||||
|
||||
Write for that reader. This is the single constraint that should shape every
|
||||
decision you make here.
|
||||
|
||||
## What this means in practice
|
||||
|
||||
**Decompose until each child is unambiguous, then stop.** A child that requires
|
||||
judgement about *what* to build has been under-decomposed; that judgement is your
|
||||
job, not the implementer's. A child so small it has no coherent acceptance test
|
||||
has been over-decomposed and adds review overhead without reducing risk. Aim for
|
||||
children that touch a handful of files and land as one reviewable commit.
|
||||
|
||||
**Name the files.** Before writing a child, look at the repository and find where
|
||||
the change actually goes. "Update the poller" is not a plan; "add `last_etag`
|
||||
handling to `crates/tireless-data/src/forge.rs`" is. If you cannot name a file,
|
||||
you have not investigated enough to plan the work.
|
||||
|
||||
**Give every child a runnable stopping condition.** The implementer needs to know
|
||||
when it is finished. A command that exits zero is the only reliable form of this.
|
||||
"Ensure it works correctly" gives a literal model nothing, and it will keep going
|
||||
— usually by rewriting adjacent code it was not asked to touch.
|
||||
|
||||
**Bound every child explicitly.** State what must *not* change. This is the
|
||||
section that most reliably prevents scope creep, and it is the one you will be
|
||||
most tempted to leave empty because the boundary feels obvious to you. It is not
|
||||
obvious to the implementer.
|
||||
|
||||
**Order the work.** If child B needs child A merged first, say so by title. Do
|
||||
not rely on the reader inferring an order from the numbering.
|
||||
|
||||
**Prefer describing the change to prescribing the code.** Say what must become
|
||||
true and how to verify it. Do not paste implementations — a literal implementer
|
||||
will transcribe your sketch verbatim, including its mistakes, instead of writing
|
||||
code that fits the surrounding file.
|
||||
|
||||
## Required structure
|
||||
|
||||
Emit an epic, then one section per child. Every child must carry **all five**
|
||||
headings below. A child missing any of them is rejected automatically and the
|
||||
whole plan is returned to you, so check before you finish.
|
||||
|
||||
```markdown
|
||||
## Goal
|
||||
One paragraph: what changes, and why. Enough context that the implementer
|
||||
understands the intent, not only the mechanics.
|
||||
|
||||
## Files
|
||||
- `path/to/file.ext` — what changes here
|
||||
- `path/to/other.ext` — what changes here
|
||||
|
||||
## Steps
|
||||
1. Ordered, concrete actions.
|
||||
2. Each one a thing the implementer can actually do.
|
||||
|
||||
## Acceptance
|
||||
- `command that must exit zero` <- at least one of these is REQUIRED
|
||||
- Prose criterion for anything no command can check.
|
||||
|
||||
## Out of scope
|
||||
- What must not be touched, and briefly why.
|
||||
```
|
||||
|
||||
If a child depends on another, add `Depends on: <exact title of sibling>` beneath
|
||||
its heading.
|
||||
|
||||
## Before you finish
|
||||
|
||||
Re-read each child as if you were the implementing model: no memory of this
|
||||
conversation, no ability to ask, no view of the other children. Ask of each one:
|
||||
|
||||
- Could I start work from this alone?
|
||||
- Do I know which files to open?
|
||||
- Do I know when to stop?
|
||||
- Do I know what I must leave alone?
|
||||
|
||||
If the answer to any of these is no, fix the child rather than trusting that it
|
||||
will work out. A vague child does not fail loudly — it produces a confident,
|
||||
plausible, wrong pull request, which costs more to review than it would have cost
|
||||
you to specify properly.
|
||||
80
prompt/readme.md
Normal file
80
prompt/readme.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# System prompts
|
||||
|
||||
Three prompts, one contract. They are versioned together and must be edited
|
||||
together.
|
||||
|
||||
| File | Surface | Used for |
|
||||
| --- | --- | --- |
|
||||
| `plan.cc.md` | Claude Code `--append-system-prompt` | `Plan` jobs |
|
||||
| `implement.oc.md` | OpenCode `AgentConfig.prompt` | `Implement` jobs descended from a tireless plan |
|
||||
| `implement.cc.md` | Claude Code `--append-system-prompt` | `Implement` jobs on unplanned, human-written issues |
|
||||
|
||||
## The contract
|
||||
|
||||
`plan.cc.md` tells Claude Code what a child issue must contain.
|
||||
`implement.oc.md` tells OpenCode to execute exactly that and nothing more. Both
|
||||
describe the same structure — `tireless_entities::ChildSpec` — from opposite
|
||||
ends:
|
||||
|
||||
```
|
||||
plan.cc.md ──emits──▶ ChildSpec ──validated by──▶ tireless_core::plan::validate
|
||||
│
|
||||
└──consumed by──▶ implement.oc.md
|
||||
```
|
||||
|
||||
Change the shape and all three must move together, or the planner emits something
|
||||
the implementer is not expecting and the failure surfaces as a puzzlingly bad pull
|
||||
request rather than as an error.
|
||||
|
||||
`contract-version:` on the first line of each file guards this.
|
||||
`PromptSet::load` refuses a mismatched set, and `SYSTEM_PROMPT_CONTRACT_VERSION`
|
||||
in `tireless-core/src/prompt.rs` is the version this build expects. Bump all four
|
||||
when the structure changes.
|
||||
|
||||
Tests in `tireless-core::prompt` assert that the prompts actually mention every
|
||||
section `ChildSpec` requires — so adding a field without updating the prompts
|
||||
fails the build rather than every future planning run.
|
||||
|
||||
## Append, don't replace (Claude Code)
|
||||
|
||||
Both Claude Code prompts are **appended** via `--append-system-prompt`. Claude
|
||||
Code also offers `--system-prompt`, which replaces its default entirely — do not
|
||||
use it. The default carries the tool-use and repository-navigation scaffolding
|
||||
that makes Claude Code a coding agent at all. Replacing it produces a less
|
||||
capable agent, not a more obedient one.
|
||||
|
||||
## Faithful passthrough (OpenCode)
|
||||
|
||||
`implement.oc.md` is the whole system prompt for the OpenCode agent. It travels
|
||||
OpenCode → helexa cortex → neuron, and depends on helexa's passthrough guarantee:
|
||||
no injection, no rewriting, no defaults
|
||||
([helexa/helexa#179](https://git.lair.cafe/helexa/helexa/issues/179)).
|
||||
|
||||
That issue is **open**. The principle was decided 2026-07-17; verification and
|
||||
documentation are outstanding. Two of its unchecked items bear directly on this
|
||||
prompt:
|
||||
|
||||
- neuron chat templating applying the system role correctly per arch family,
|
||||
*including interaction with `/no_think` handling* — and Qwen3.6-27B is the
|
||||
model this prompt targets;
|
||||
- behaviour when multiple system messages are present.
|
||||
|
||||
tireless stage 5 is effectively the second consumer of that guarantee after the
|
||||
chat SPA, and a good way to surface exactly those bugs. If an OpenCode run
|
||||
behaves as though it never saw this prompt, suspect the passthrough before
|
||||
suspecting the prompt.
|
||||
|
||||
## Editing guidance
|
||||
|
||||
These prompts are behavioural specifications, and they are load-bearing in a way
|
||||
ordinary documentation is not — a weakened instruction here becomes a bad pull
|
||||
request three hours later, in a repository, unattended.
|
||||
|
||||
- Prefer stating the constraint *and the reason for it*. Both models follow a
|
||||
rule better when the rationale is present, and the rationale is what lets a
|
||||
model resolve a case the rule did not anticipate.
|
||||
- Keep the "stop and report" paths prominent. The most expensive failure mode is
|
||||
not an agent that gives up; it is an agent that guesses confidently and
|
||||
produces plausible, wrong work.
|
||||
- Test changes against a real issue before merging. There is no unit test for
|
||||
whether a prompt produces good plans.
|
||||
@@ -106,7 +106,7 @@ Remaining one-time steps (operator, on the target host):
|
||||
The OAuth flow is interactive and must be completed *as the service account*,
|
||||
because Claude Code reads credentials from $HOME:
|
||||
|
||||
sudo -u tireless -H /usr/bin/npx -y @anthropic-ai/claude-code@2.1.119
|
||||
sudo -u tireless -H /usr/bin/npx -y @anthropic-ai/claude-code@2.1.220
|
||||
# then: /login, and complete the browser flow
|
||||
|
||||
This writes /var/lib/tireless/.claude.json. The token refreshes in place,
|
||||
|
||||
Reference in New Issue
Block a user