feat(teams): staff research phases with a research team
`research_only` is repo-less, one research phase, "produce a markdown
artifact" — and it defaulted to `rust_sdlc`. So it was staffed with a
planner, a coder, a tester, a reviewer and a committer, four of whom had
nothing to do, each carrying the code-and-commit skills its role is bound
to. Measured 2026-08-21: 9 distinct skills across 5 role prompts, ~50KB,
one applicable. That is what "most skills score not_applicable" in the
Skill-Use baseline has been measuring all along — the skills were
correctly bound to their roles; the roles were wrong for the workflow.
None of the three existing research templates fit, so this adds
`topic_research`: frame the brief into answerable questions, gather
evidence with the URL and the quoted passage, check every claim against
its source, write the report. Three roles, four skills, each checked
against its own `when_to_use` before binding — and two obvious candidates
deliberately NOT bound, because `executive-summary-writing` tells the
writer to discard any item not tied to a named project and
`signal-to-noise-ranking` scores relevance the same way. On a standalone
topic report that discards the deliverable.
`default_phase_teams` lets a recipe staff each phase PURPOSE separately,
resolved into `config.phase_teams` at create. A multi-phase recipe does
not have one job: `research_and_code`'s research phase spends a paragraph
of `task` telling its team not to change source files, because
`rust_sdlc` gave that phase a coder and a committer and they did what
coders do — mission 01a00c57 shipped both INT items during RESEARCH and
the coding phase then delivered +0/-0. Prose was the only lever
available; staffing is the actual one.
Also fixed in the three existing research templates, all verified rather
than inferred:
- `papers_research` bound `arxiv-daily` to its DOMAIN SCOUT. That
skill's entire content is "Do not search arXiv yourself — the harvest
already ran", and its `when_to_use` names Continuous Research
missions, which are the only ones the platform writes a harvest
manifest for. The role whose job is searching was bound a skill
forbidding it.
- Its PAPER READER was told to "fetch the PDF, extract text". The
runtime image has no pdftotext, no mutool and no pypdf — checked in
the container. Every paper would have hit the `[read: abstract only]`
fallback, which reads identically to the fallback working as designed.
- `insight_research` cross-referenced "our repos'" history. A mission
binds ONE repo (`missions.repo_id`).
- `codebase_research` wrote to "the Obsidian vault"; no vault is
mounted, and both it and `papers_research` were committing in "PRs",
which the platform does not open.
And `research_only` itself had neither `task` nor `done_when` — the same
defect `benchmark`, `security_hardening` and `research_and_code` were each
fixed for, and it was left out. A phase with no `done_when` is never
judged. It also still asked for `pdf`, a format nothing generates.
Two new guards, both negative-controlled: every team a recipe names must
exist (a typo currently only logs, and the mission is staffed by the
fallback crew looking deliberate), and every `default_phase_teams` key
must be a purpose `purposes_for` actually emits.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
This commit is contained in:
co-authored by
Claude Opus 5
parent
4f4ce34203
commit
ceec0423ad
@@ -254,18 +254,31 @@ mod tests {
|
|||||||
"order_idx",
|
"order_idx",
|
||||||
"requires_repo",
|
"requires_repo",
|
||||||
"default_team_template",
|
"default_team_template",
|
||||||
|
"default_phase_teams",
|
||||||
"default_topology",
|
"default_topology",
|
||||||
"phases",
|
"phases",
|
||||||
"description",
|
"description",
|
||||||
];
|
];
|
||||||
|
// `[default_phase_teams]` maps a phase PURPOSE to a team template key,
|
||||||
|
// so its keys are not config keys and must not be checked as such.
|
||||||
|
// They are checked against the purposes `phase_runner::purposes_for`
|
||||||
|
// can actually emit instead — a typo'd purpose matches no phase and
|
||||||
|
// that phase silently falls back to the mission-wide team, which is
|
||||||
|
// exactly the kind of quiet wrong staffing this table exists to end.
|
||||||
|
const PURPOSES: &[&str] = &["research", "coding", "security", "mission"];
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
|
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let body = std::fs::read_to_string(&path).unwrap();
|
let body = std::fs::read_to_string(&path).unwrap();
|
||||||
|
let mut table = String::new();
|
||||||
for line in body.lines() {
|
for line in body.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
|
if line.starts_with('[') {
|
||||||
|
table = line.trim_matches(['[', ']'].as_slice()).to_string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if line.starts_with('#') || !line.contains('=') {
|
if line.starts_with('#') || !line.contains('=') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -273,6 +286,16 @@ mod tests {
|
|||||||
if key.is_empty() || key.contains(' ') || key.contains('[') {
|
if key.is_empty() || key.contains(' ') || key.contains('[') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if table == "default_phase_teams" {
|
||||||
|
assert!(
|
||||||
|
PURPOSES.contains(&key),
|
||||||
|
"{} staffs purpose `{key}`, which `purposes_for` never emits — \
|
||||||
|
that phase would fall back to the mission-wide team with \
|
||||||
|
nothing reporting it",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let accounted = ENVELOPE.contains(&key)
|
let accounted = ENVELOPE.contains(&key)
|
||||||
|| is_listed(key, KNOWN_KEYS)
|
|| is_listed(key, KNOWN_KEYS)
|
||||||
|| is_listed(key, DECLARED_BUT_UNREAD);
|
|| is_listed(key, DECLARED_BUT_UNREAD);
|
||||||
|
|||||||
@@ -294,6 +294,54 @@ pub async fn create(
|
|||||||
.get("phase_teams")
|
.get("phase_teams")
|
||||||
.and_then(|v| v.as_object())
|
.and_then(|v| v.as_object())
|
||||||
.is_some_and(|o| o.values().any(|v| v.as_array().is_some_and(|a| !a.is_empty())));
|
.is_some_and(|o| o.values().any(|v| v.as_array().is_some_and(|a| !a.is_empty())));
|
||||||
|
// Per-purpose defaults first: a multi-phase recipe does not have one job,
|
||||||
|
// and staffing every phase from one team is what put a coder, a tester and
|
||||||
|
// a committer on a repo-less markdown mission. Only applied when the caller
|
||||||
|
// named no team of any kind, so an explicit choice always wins.
|
||||||
|
let mut config = body.config;
|
||||||
|
if team_template_id.is_none() && body.team_id.is_none() && !has_phase_teams {
|
||||||
|
if let Some(r) = recipe {
|
||||||
|
let mut resolved = serde_json::Map::new();
|
||||||
|
for (purpose, key) in &r.default_phase_teams {
|
||||||
|
match cm_db::repo::team_templates::get_by_key(&state.pool, key).await {
|
||||||
|
Ok(Some(t)) => {
|
||||||
|
resolved.insert(
|
||||||
|
purpose.clone(),
|
||||||
|
serde_json::json!([t.id.to_string()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Loud, and it does NOT fall back silently: a recipe naming
|
||||||
|
// a template that is not loaded would otherwise stage the
|
||||||
|
// wrong crew and look deliberate.
|
||||||
|
Ok(None) => eprintln!(
|
||||||
|
"missions: recipe {} maps purpose {purpose:?} to team template \
|
||||||
|
{key:?}, which is not loaded — that phase will fall back to the \
|
||||||
|
mission-wide default",
|
||||||
|
body.template_kind.trim()
|
||||||
|
),
|
||||||
|
Err(e) => eprintln!("missions: looking up team template {key:?}: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !resolved.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"missions: {} staffs {} phase purpose(s) from the recipe",
|
||||||
|
body.template_kind.trim(),
|
||||||
|
resolved.len()
|
||||||
|
);
|
||||||
|
if let Some(obj) = config.as_object_mut() {
|
||||||
|
obj.insert("phase_teams".into(), serde_json::Value::Object(resolved));
|
||||||
|
} else {
|
||||||
|
config = serde_json::json!({ "phase_teams": resolved });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let has_phase_teams = has_phase_teams
|
||||||
|
|| config
|
||||||
|
.get("phase_teams")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.is_some_and(|o| o.values().any(|v| v.as_array().is_some_and(|a| !a.is_empty())));
|
||||||
|
|
||||||
if team_template_id.is_none() && body.team_id.is_none() && !has_phase_teams {
|
if team_template_id.is_none() && body.team_id.is_none() && !has_phase_teams {
|
||||||
if let Some(key) = recipe.and_then(|r| r.default_team_template.as_deref()) {
|
if let Some(key) = recipe.and_then(|r| r.default_team_template.as_deref()) {
|
||||||
match cm_db::repo::team_templates::get_by_key(&state.pool, key).await {
|
match cm_db::repo::team_templates::get_by_key(&state.pool, key).await {
|
||||||
@@ -324,7 +372,7 @@ pub async fn create(
|
|||||||
repo_id: body.repo_id,
|
repo_id: body.repo_id,
|
||||||
schedule: body.schedule,
|
schedule: body.schedule,
|
||||||
description: body.description.as_deref(),
|
description: body.description.as_deref(),
|
||||||
config: body.config,
|
config,
|
||||||
runtime_kind: Some(runtime_kind),
|
runtime_kind: Some(runtime_kind),
|
||||||
target_node_id: body.target_node_id,
|
target_node_id: body.target_node_id,
|
||||||
backend: body.backend.as_deref(),
|
backend: body.backend.as_deref(),
|
||||||
@@ -1761,6 +1809,12 @@ mod tests {
|
|||||||
blurb: String::new(),
|
blurb: String::new(),
|
||||||
requires_repo: true,
|
requires_repo: true,
|
||||||
default_team_template: Some("rust_sdlc".into()),
|
default_team_template: Some("rust_sdlc".into()),
|
||||||
|
default_phase_teams: [
|
||||||
|
("research".to_string(), "topic_research".to_string()),
|
||||||
|
("coding".to_string(), "rust_sdlc".to_string()),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
phases: vec![
|
phases: vec![
|
||||||
crate::workflow_registry::WorkflowPhase {
|
crate::workflow_registry::WorkflowPhase {
|
||||||
kind: "research".into(),
|
kind: "research".into(),
|
||||||
|
|||||||
@@ -353,6 +353,73 @@ mod contradiction_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every team a recipe names must be a team that exists.
|
||||||
|
///
|
||||||
|
/// `create()` logs and carries on when a recipe names a template that is
|
||||||
|
/// not loaded, because failing mission creation over it would be worse.
|
||||||
|
/// That makes a typo here invisible in exactly the way that matters: the
|
||||||
|
/// mission is staffed by the fallback crew and looks deliberate. `research_only`
|
||||||
|
/// pointed at `rust_sdlc` for months and nothing said a word.
|
||||||
|
#[test]
|
||||||
|
fn every_team_a_recipe_names_exists() {
|
||||||
|
let mut keys = std::collections::HashSet::new();
|
||||||
|
for (_, body) in {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
walk_ext(&repo_root("templates/teams"), "toml", &mut v);
|
||||||
|
v
|
||||||
|
} {
|
||||||
|
for line in body.lines() {
|
||||||
|
if let Some(rest) = line.trim().strip_prefix("key") {
|
||||||
|
if let Some((_, val)) = rest.split_once('=') {
|
||||||
|
keys.insert(val.trim().trim_matches('"').to_string());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(!keys.is_empty(), "no team templates found at all");
|
||||||
|
|
||||||
|
let mut recipes = Vec::new();
|
||||||
|
walk_ext(&repo_root("templates/workflows"), "toml", &mut recipes);
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
for (name, body) in recipes {
|
||||||
|
let mut table = String::new();
|
||||||
|
for line in body.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with('[') {
|
||||||
|
table = line.trim_matches(['[', ']'].as_slice()).to_string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let named = if let Some((_, v)) = line.split_once('=') {
|
||||||
|
if line.starts_with("default_team_template")
|
||||||
|
|| table == "default_phase_teams"
|
||||||
|
{
|
||||||
|
Some(v.trim().trim_matches('"').to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(k) = named {
|
||||||
|
if !keys.contains(&k) {
|
||||||
|
missing.push(format!("{name} -> {k}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
missing.is_empty(),
|
||||||
|
"{} recipe(s) name a team template that does not exist, so the mission \
|
||||||
|
is staffed by the fallback crew and looks deliberate: {}",
|
||||||
|
missing.len(),
|
||||||
|
missing.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The marker kinds, as the parser spells them.
|
/// The marker kinds, as the parser spells them.
|
||||||
const MARKER_KINDS: &[&str] = &[
|
const MARKER_KINDS: &[&str] = &[
|
||||||
"TASK",
|
"TASK",
|
||||||
|
|||||||
@@ -34,6 +34,20 @@ pub struct WorkflowRecipe {
|
|||||||
pub phases: Vec<WorkflowPhase>,
|
pub phases: Vec<WorkflowPhase>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub default_team_template: Option<String>,
|
pub default_team_template: Option<String>,
|
||||||
|
/// Default team **per phase purpose**, by template key:
|
||||||
|
/// `{ research = "topic_research", coding = "rust_sdlc" }`.
|
||||||
|
///
|
||||||
|
/// `default_team_template` names ONE team for a whole mission, and a
|
||||||
|
/// multi-phase recipe does not have one job. `research_and_code` staffs a
|
||||||
|
/// research phase and a coding phase from the same `rust_sdlc` crew, which
|
||||||
|
/// is why its research phase has to spend a paragraph of `task` telling
|
||||||
|
/// coders not to code — a workaround for staffing, written into the prompt.
|
||||||
|
///
|
||||||
|
/// Resolved to `config.phase_teams` at mission-create, which the
|
||||||
|
/// orchestrator and `composed_graph` already read. Purposes come from
|
||||||
|
/// `phase_runner::purposes_for`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub default_phase_teams: std::collections::BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ history. Look for:
|
|||||||
- Abandoned experiments (branches with orphan commits still visible
|
- Abandoned experiments (branches with orphan commits still visible
|
||||||
in reflog) — note the theory of why they were dropped
|
in reflog) — note the theory of why they were dropped
|
||||||
|
|
||||||
Output goes to the Obsidian vault under `Codebases/<repo>/History.md` as
|
Output goes to `/mission/repo/Codebases/<repo>/History.md` — inside the
|
||||||
a timeline with dated inflection points + one-paragraph explanations.
|
mission's own checkout, which is what the platform collects. There is no
|
||||||
|
separate vault mounted. Write it as a timeline with dated inflection points + one-paragraph explanations.
|
||||||
Never invent motives; when a commit's rationale is unclear, mark it
|
Never invent motives; when a commit's rationale is unclear, mark it
|
||||||
`[unknown motive]`.
|
`[unknown motive]`.
|
||||||
"""
|
"""
|
||||||
@@ -130,8 +131,10 @@ skills = ["obsidian-vault-conventions", "workspace-repo-commit-protocol", "small
|
|||||||
system_prompt = """
|
system_prompt = """
|
||||||
You are the VAULT SCRIBE of a Codebase Research team.
|
You are the VAULT SCRIBE of a Codebase Research team.
|
||||||
|
|
||||||
You own the Obsidian vault index for this codebase. Every other role
|
You own the note index for this codebase. Every other role writes to
|
||||||
writes to `Codebases/<repo>/*.md`; you keep the vault navigable:
|
`/mission/repo/Codebases/<repo>/*.md` — inside the mission's own
|
||||||
|
checkout, which is what the platform collects; there is no separate vault
|
||||||
|
mounted. You keep it navigable:
|
||||||
|
|
||||||
- Maintain `Codebases/<repo>/README.md` as the entrypoint with
|
- Maintain `Codebases/<repo>/README.md` as the entrypoint with
|
||||||
wikilinks to History, Architecture, Flows, and any subpages
|
wikilinks to History, Architecture, Flows, and any subpages
|
||||||
@@ -141,8 +144,9 @@ writes to `Codebases/<repo>/*.md`; you keep the vault navigable:
|
|||||||
so cross-repo searches surface useful hits
|
so cross-repo searches surface useful hits
|
||||||
- Merge overlapping notes; delete drafts explicitly marked SUPERSEDED
|
- Merge overlapping notes; delete drafts explicitly marked SUPERSEDED
|
||||||
|
|
||||||
Commit the vault changes in small, purposeful PRs. Never squash multiple
|
Commit in small, purposeful commits on the mission's own branch — the
|
||||||
authors' contributions into one commit.
|
platform delivers by diffing this checkout and does not open PRs. Never
|
||||||
|
squash multiple authors' contributions into one commit.
|
||||||
"""
|
"""
|
||||||
brain_seed = """
|
brain_seed = """
|
||||||
# Vault scribe memory seed
|
# Vault scribe memory seed
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ skills = ["git-log-forensics", "workspace-repo-commit-protocol"]
|
|||||||
system_prompt = """
|
system_prompt = """
|
||||||
You are the IMPLEMENTATION TRACKER of an Insight Research team.
|
You are the IMPLEMENTATION TRACKER of an Insight Research team.
|
||||||
|
|
||||||
Cross-reference the `Papers/` vault with our repos' commit history to
|
Cross-reference the `Papers/` notes with the commit history of the ONE
|
||||||
build a mapping of "which papers we've actually implemented." Signals:
|
repository this mission checked out at `/mission/repo`. A mission binds a
|
||||||
|
single repo (`missions.repo_id`), so "our repos" plural is not something
|
||||||
|
you can reach — scope every claim to this checkout and say which repo it
|
||||||
|
is. Signals:
|
||||||
|
|
||||||
- Commit messages that name a paper, method, or algorithm
|
- Commit messages that name a paper, method, or algorithm
|
||||||
- README / docs sections that credit a source
|
- README / docs sections that credit a source
|
||||||
- Comments in code that cite `(Author et al., YEAR)`
|
- Comments in code that cite `(Author et al., YEAR)`
|
||||||
|
|
||||||
Output goes to `Insights/implementation-map.md` — a table:
|
Output goes to `/mission/repo/Insights/implementation-map.md` — a table:
|
||||||
`{ paper, repo, first-commit-ref, form (verbatim / adapted / inspired) }`.
|
`{ paper, repo, first-commit-ref, form (verbatim / adapted / inspired) }`.
|
||||||
Never claim we implemented something without a direct code / commit
|
Never claim we implemented something without a direct code / commit
|
||||||
citation.
|
citation.
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ version = 1
|
|||||||
[[roles]]
|
[[roles]]
|
||||||
slot = "domain_scout"
|
slot = "domain_scout"
|
||||||
order_idx = 0
|
order_idx = 0
|
||||||
skills = ["arxiv-daily", "web-search-triage", "decompose-int-items"]
|
# `arxiv-daily` was bound here and is the wrong skill for this team. Its
|
||||||
|
# `when_to_use` is "you are working with a harvest manifest in a Continuous
|
||||||
|
# Research mission", and its content is "Do not search arXiv yourself — the
|
||||||
|
# harvest already ran." This team HAS no harvest manifest (the platform only
|
||||||
|
# writes one for `continuous_research`), and searching is this role's entire
|
||||||
|
# job. The scout was being told not to do the thing it exists to do.
|
||||||
|
skills = ["web-search-triage", "decompose-int-items"]
|
||||||
system_prompt = """
|
system_prompt = """
|
||||||
You are the DOMAIN SCOUT of a Papers & Online Research team.
|
You are the DOMAIN SCOUT of a Papers & Online Research team.
|
||||||
|
|
||||||
@@ -26,7 +32,8 @@ conference proceedings pages. For each candidate, capture:
|
|||||||
- Citation count (Semantic Scholar) as a proxy for signal
|
- Citation count (Semantic Scholar) as a proxy for signal
|
||||||
- Abstract verbatim (no paraphrase)
|
- Abstract verbatim (no paraphrase)
|
||||||
|
|
||||||
Output goes to `Papers/<topic>/candidates.jsonl` — one line per paper.
|
Output goes to `/mission/repo/Papers/<topic>/candidates.jsonl` — one line
|
||||||
|
per paper. That checkout is the only place this mission delivers from.
|
||||||
Never drop candidates because "they look weak"; the reader filters.
|
Never drop candidates because "they look weak"; the reader filters.
|
||||||
Deduplicate by DOI/arXiv id.
|
Deduplicate by DOI/arXiv id.
|
||||||
"""
|
"""
|
||||||
@@ -53,8 +60,21 @@ skills = ["structured-paper-summary", "workspace-repo-commit-protocol"]
|
|||||||
system_prompt = """
|
system_prompt = """
|
||||||
You are the PAPER READER of a Papers & Online Research team.
|
You are the PAPER READER of a Papers & Online Research team.
|
||||||
|
|
||||||
For each candidate from the scout, fetch the PDF, extract text, and
|
For each candidate from the scout, read as much of the paper as you can
|
||||||
produce a structured summary:
|
reach and produce a structured summary.
|
||||||
|
|
||||||
|
WHAT YOU CAN ACTUALLY READ. Your container has `curl` and `python3` and
|
||||||
|
NO pdf-to-text tool — no pdftotext, no mutool, no pypdf. Verified, not
|
||||||
|
assumed. So:
|
||||||
|
|
||||||
|
- arXiv: `curl` the `/abs/` page for the full abstract, and try
|
||||||
|
`https://ar5iv.org/abs/<id>` for an HTML rendering of the full text.
|
||||||
|
- Anything else: the landing page, and the HTML version if one exists.
|
||||||
|
- A paper that exists only as a PDF is `[read: abstract only]`. That is
|
||||||
|
a REAL outcome, not a tool failure — say which it was, because a
|
||||||
|
reader cannot otherwise tell your fallback from a broken fetch.
|
||||||
|
|
||||||
|
The summary:
|
||||||
|
|
||||||
- Problem statement (1-2 sentences)
|
- Problem statement (1-2 sentences)
|
||||||
- Method — new technique, not the recap of prior work
|
- Method — new technique, not the recap of prior work
|
||||||
@@ -62,10 +82,8 @@ produce a structured summary:
|
|||||||
- Assumptions / limitations the authors themselves flag
|
- Assumptions / limitations the authors themselves flag
|
||||||
- Adjacent papers cited that we should also pull
|
- Adjacent papers cited that we should also pull
|
||||||
|
|
||||||
Output goes to `Papers/<topic>/<paper-slug>.md` with frontmatter
|
Output goes to `/mission/repo/Papers/<topic>/<paper-slug>.md` with
|
||||||
carrying full metadata. Never summarize from the abstract alone; if the
|
frontmatter carrying full metadata.
|
||||||
PDF is unavailable, mark the paper `[read: abstract only]` in a
|
|
||||||
warning callout.
|
|
||||||
"""
|
"""
|
||||||
brain_seed = """
|
brain_seed = """
|
||||||
# Paper reader memory seed
|
# Paper reader memory seed
|
||||||
@@ -98,15 +116,16 @@ You own `Papers/`. Enforce structure:
|
|||||||
- A per-topic `README.md` index summarizes the strongest 3 papers,
|
- A per-topic `README.md` index summarizes the strongest 3 papers,
|
||||||
the most-cited paper, and the open questions
|
the most-cited paper, and the open questions
|
||||||
|
|
||||||
Commit in small, purposeful PRs. Never delete a paper note without
|
Commit in small, purposeful commits on the mission's own branch — the
|
||||||
explicit operator sign-off — even a weak paper is a signal about the
|
platform delivers by diffing this checkout and does not open PRs. Never
|
||||||
field's shape.
|
delete a paper note without explicit operator sign-off; even a weak paper
|
||||||
|
is a signal about the field's shape.
|
||||||
"""
|
"""
|
||||||
brain_seed = """
|
brain_seed = """
|
||||||
# Library curator memory seed
|
# Library curator memory seed
|
||||||
|
|
||||||
## Vault shape
|
## Vault shape
|
||||||
- `Papers/<topic>/README.md` is the entrypoint. `Papers/<topic>/<slug>.md`
|
- `/mission/repo/Papers/<topic>/README.md` is the entrypoint; `<slug>.md`
|
||||||
are the leaf notes.
|
are the leaf notes.
|
||||||
- Tags: `#paper/<topic>`, `#paper/method/<class>`, `#paper/reproducible`.
|
- Tags: `#paper/<topic>`, `#paper/method/<class>`, `#paper/reproducible`.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
key = "topic_research"
|
||||||
|
name = "Topic Research"
|
||||||
|
description = "Answer a question and write it up. Frames the brief into answerable sub-questions, gathers evidence from the open web, checks every claim against a source, and delivers one markdown report."
|
||||||
|
stack = ["research", "writing", "evidence"]
|
||||||
|
category = "research"
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "research_web_readonly"
|
||||||
|
mcp_bundles = ["clawmates_door", "clawmates_skills"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
# ── Why this template exists ─────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# `research_only` — repo-less, one research phase, "produce a styled MD
|
||||||
|
# artifact" — defaulted to `rust_sdlc`. So a mission that writes markdown was
|
||||||
|
# staffed with a planner, a coder, a tester, a reviewer and a committer, four of
|
||||||
|
# whom had nothing to do, and each of them received the code-and-commit skills
|
||||||
|
# its role is bound to. Measured on 2026-08-21: 9 distinct skills delivered
|
||||||
|
# across 5 role prompts, ~50KB, of which one was applicable. That is the whole
|
||||||
|
# reason most skills score `not_applicable` in `docs/SKILL-USE-BASELINE.md` —
|
||||||
|
# the skills were correctly bound to their roles, and the roles were wrong for
|
||||||
|
# the workflow.
|
||||||
|
#
|
||||||
|
# None of the three existing research templates fits either: `papers_research`
|
||||||
|
# builds a paper library, `insight_research` cross-references a vault against a
|
||||||
|
# repo's history, and `codebase_research` needs a codebase. All three answer a
|
||||||
|
# narrower question than "research this and write it up".
|
||||||
|
#
|
||||||
|
# ── On the skills bound below ────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Four, and each was checked against its own `when_to_use` before binding.
|
||||||
|
# Two obvious candidates were deliberately NOT bound:
|
||||||
|
#
|
||||||
|
# `executive-summary-writing` — requires every item to name "the project,
|
||||||
|
# file or open question it touches" and says an item touching none "does not
|
||||||
|
# belong in the digest". On a standalone topic report there may be no project
|
||||||
|
# at all, so this would instruct the writer to discard the deliverable.
|
||||||
|
#
|
||||||
|
# `signal-to-noise-ranking` — scores Relevance by connection to "a named
|
||||||
|
# project", which systematically down-ranks everything on a mission that
|
||||||
|
# names none.
|
||||||
|
#
|
||||||
|
# Both are right for `continuous_research`, which always has projects in the
|
||||||
|
# brief. Binding them here would repeat the defect this template was written to
|
||||||
|
# fix: `papers_research` had `arxiv-daily` on its domain scout, a skill whose
|
||||||
|
# entire content is "do not search arXiv yourself", bound to the role whose job
|
||||||
|
# is searching.
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "lead_researcher"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["web-search-triage"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the LEAD RESEARCHER of a Topic Research team.
|
||||||
|
|
||||||
|
Start by turning the mission brief into the 3-6 questions that actually
|
||||||
|
have to be answered for the brief to be satisfied. Write them down first,
|
||||||
|
in `/mission/repo/research/questions.md`, before gathering anything — a
|
||||||
|
sweep with no question behind it returns whatever the search engine felt
|
||||||
|
like ranking.
|
||||||
|
|
||||||
|
Then gather. `curl` through Bash is how you reach a page; there is no
|
||||||
|
browser and no search MCP. Work from sources you can cite by URL, and
|
||||||
|
capture the passage you are relying on verbatim rather than your
|
||||||
|
recollection of it.
|
||||||
|
|
||||||
|
Put the evidence in `/mission/repo/research/evidence.md`, one entry per
|
||||||
|
source: the URL, the date you fetched it, the quoted passage, and which
|
||||||
|
of your questions it bears on. An entry that bears on no question does
|
||||||
|
not belong in the file.
|
||||||
|
|
||||||
|
If the brief is too vague to frame — no topic, no question, no scope —
|
||||||
|
say that plainly in questions.md and stop. A report written against a
|
||||||
|
guess about what was wanted is worse than one sentence saying the brief
|
||||||
|
was unusable.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Lead researcher memory seed
|
||||||
|
|
||||||
|
## Framing
|
||||||
|
- The questions come first and in writing. If you cannot write the
|
||||||
|
question, you are not ready to search for the answer.
|
||||||
|
- A question that cannot be answered wrong is not a question. "Is X
|
||||||
|
good?" is not; "What does X cost at 10k requests/sec?" is.
|
||||||
|
|
||||||
|
## Redlines
|
||||||
|
- Never cite a source you did not fetch. A plausible URL is not a source.
|
||||||
|
- Quote the passage. A summary of a source, filed as the evidence FOR a
|
||||||
|
claim, is the claim citing itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "evidence_checker"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["structured-paper-summary"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the EVIDENCE CHECKER of a Topic Research team.
|
||||||
|
|
||||||
|
Read `/mission/repo/research/evidence.md` against the questions in
|
||||||
|
questions.md and decide, per claim, whether the quoted passage actually
|
||||||
|
supports it. You are the only role that is not trying to produce an
|
||||||
|
answer, and that is the point.
|
||||||
|
|
||||||
|
Write `/mission/repo/research/verification.md`. For each claim:
|
||||||
|
|
||||||
|
- SUPPORTED — the passage says it. Quote the words that do.
|
||||||
|
- OVERSTATED — the source says something weaker. Say what it says.
|
||||||
|
- UNSUPPORTED — no passage backs this. It does not reach the report.
|
||||||
|
|
||||||
|
Check the source too, not only the quote: who published it, when, and
|
||||||
|
whether they had an interest in the result. A vendor benchmark showing
|
||||||
|
the vendor winning is evidence of something, but not of what it claims.
|
||||||
|
|
||||||
|
Finding that most claims are supported is a real result. Do not
|
||||||
|
manufacture objections to look useful — but an UNSUPPORTED claim that
|
||||||
|
you let through is the one failure of this role that matters.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Evidence checker memory seed
|
||||||
|
|
||||||
|
## Discipline
|
||||||
|
- Read the quote, not the claim. The gap between them is the entire job.
|
||||||
|
- "The paper says X" and "the paper's abstract says X" are different
|
||||||
|
findings. So are "measured" and "projected".
|
||||||
|
|
||||||
|
## Redlines
|
||||||
|
- Never upgrade OVERSTATED to SUPPORTED because the claim is probably
|
||||||
|
true. Probably-true with no source is UNSUPPORTED.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "report_writer"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["scientific-writing-conventions", "workspace-repo-commit-protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the REPORT WRITER of a Topic Research team.
|
||||||
|
|
||||||
|
Write `/mission/repo/research/REPORT.md`. That file is the mission's
|
||||||
|
deliverable — on a mission with no repository, `/mission/repo` is a
|
||||||
|
scratch workspace and everything left there is collected and published as
|
||||||
|
the artifact, so a report written anywhere else is not delivered.
|
||||||
|
|
||||||
|
Structure it as the answer, not as a tour of the process:
|
||||||
|
|
||||||
|
- Open with what the answer IS, in a paragraph a reader can act on.
|
||||||
|
- Then each question from questions.md, with its answer and the
|
||||||
|
sources that support it.
|
||||||
|
- Then what you could not establish. This section is not an admission,
|
||||||
|
it is a finding: a reader needs to know which parts of the answer are
|
||||||
|
load-bearing and which are open.
|
||||||
|
|
||||||
|
Use ONLY claims the checker marked SUPPORTED. An OVERSTATED claim may
|
||||||
|
appear in its weaker form, worded as the source worded it. An
|
||||||
|
UNSUPPORTED claim does not appear at all — not hedged, not softened.
|
||||||
|
|
||||||
|
Cite inline with the URL. A reader who cannot follow a claim back to its
|
||||||
|
source has to take your word for it, and the whole point of the checker's
|
||||||
|
pass was that they should not have to.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Report writer memory seed
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
- The answer goes first. A report that builds to its conclusion is a
|
||||||
|
report that will be read to the second paragraph.
|
||||||
|
- Say the specific thing. "Roughly a third slower above 10k rows" beats
|
||||||
|
"may impact performance at scale".
|
||||||
|
|
||||||
|
## Redlines
|
||||||
|
- Do not restore a claim the checker rejected. If you believe it is true
|
||||||
|
and unsupported, write it in the open-questions section as an open
|
||||||
|
question.
|
||||||
|
- Do not pad to length. A short report that answers the brief is finished.
|
||||||
|
"""
|
||||||
@@ -5,11 +5,23 @@ requires_repo = true
|
|||||||
|
|
||||||
default_team_template = "rust_sdlc"
|
default_team_template = "rust_sdlc"
|
||||||
|
|
||||||
|
# Per-purpose staffing. The research phase's `task` below spends a paragraph
|
||||||
|
# telling the team NOT to change source files, because `rust_sdlc` gave that
|
||||||
|
# phase a coder, a tester and a committer and they did what coders do — mission
|
||||||
|
# 01a00c57 shipped both INT items during RESEARCH (+276/-57) and the coding
|
||||||
|
# phase then opened a clean tree and delivered +0/-0. Prose was the only lever
|
||||||
|
# available; staffing is the actual one. The `task` stays as the belt to this
|
||||||
|
# braces.
|
||||||
|
[default_phase_teams]
|
||||||
|
research = "topic_research"
|
||||||
|
coding = "rust_sdlc"
|
||||||
|
|
||||||
[[phases]]
|
[[phases]]
|
||||||
kind = "research"
|
kind = "research"
|
||||||
order_idx = 0
|
order_idx = 0
|
||||||
[phases.config]
|
[phases.config]
|
||||||
produces = ["md", "pdf"]
|
# `pdf` names a format nothing generates — artifacts are served as Markdown.
|
||||||
|
produces = ["md"]
|
||||||
default_topology = "hub_spoke"
|
default_topology = "hub_spoke"
|
||||||
# Research PLANS; coding BUILDS. Without this the split is a fiction:
|
# Research PLANS; coding BUILDS. Without this the split is a fiction:
|
||||||
# `rust_sdlc` gives the research team coding roles and a writable
|
# `rust_sdlc` gives the research team coding roles and a writable
|
||||||
|
|||||||
@@ -1,19 +1,54 @@
|
|||||||
key = "research_only"
|
key = "research_only"
|
||||||
title = "Research only"
|
title = "Research only"
|
||||||
blurb = "Produce a styled MD + PDF artifact in the workspace. One-shot or scheduled."
|
blurb = "Answer a question and deliver a sourced markdown report. One-shot or scheduled."
|
||||||
requires_repo = false
|
requires_repo = false
|
||||||
|
|
||||||
# Phases run in order. Each entry gets a `mission_phases` row on
|
# Phases run in order. Each entry gets a `mission_phases` row on
|
||||||
# mission create; the orchestrator dispatches per-kind executors.
|
# mission create; the orchestrator dispatches per-kind executors.
|
||||||
default_team_template = "rust_sdlc"
|
#
|
||||||
|
# `rust_sdlc` until 2026-08-21, which staffed this repo-less markdown mission
|
||||||
|
# with a planner, a coder, a tester, a reviewer and a committer — four of whom
|
||||||
|
# had nothing to do, each carrying the code-and-commit skills its role is bound
|
||||||
|
# to. Measured: 9 distinct skills across 5 role prompts, ~50KB, one applicable.
|
||||||
|
# See docs/SKILL-USE-BASELINE.md finding 7.
|
||||||
|
default_team_template = "topic_research"
|
||||||
|
|
||||||
[[phases]]
|
[[phases]]
|
||||||
kind = "research"
|
kind = "research"
|
||||||
order_idx = 0
|
order_idx = 0
|
||||||
# Phase-scoped config, merged into mission_phases.config on insert.
|
# Phase-scoped config, merged into mission_phases.config on insert.
|
||||||
[phases.config]
|
[phases.config]
|
||||||
produces = ["md", "pdf"]
|
# `pdf` dropped — PDF rendering was removed from the delivery path and
|
||||||
|
# artifacts are served as Markdown, so asking for it named a format nothing
|
||||||
|
# generates. `security_hardening` was corrected for this; this recipe was not.
|
||||||
|
produces = ["md"]
|
||||||
default_topology = "hub_spoke"
|
default_topology = "hub_spoke"
|
||||||
|
# This recipe had NEITHER of the two keys below, which is the same defect
|
||||||
|
# `benchmark`, `security_hardening` and `research_and_code` were each fixed
|
||||||
|
# for: a phase with no `done_when` never enters `evaluating`, is never judged,
|
||||||
|
# and reports `completed` whatever it did. The empty-delivery rule still caught
|
||||||
|
# a phase that wrote nothing at all (`research` is in PRODUCING_KINDS), so the
|
||||||
|
# gap was narrower here — a mission that wrote one junk file went green.
|
||||||
|
task = """
|
||||||
|
Answer the mission brief and deliver a sourced report.
|
||||||
|
|
||||||
|
There is no repository on this mission. `/mission/repo` is your workspace, everything you leave there is collected and published as the mission's artifact, and anything written anywhere else is not delivered.
|
||||||
|
|
||||||
|
Work in three passes and leave the trail behind: research/questions.md (what actually has to be answered), research/evidence.md (the sources, quoted, with the URL and the date fetched), and research/REPORT.md (the answer).
|
||||||
|
|
||||||
|
Cite every claim to a source you actually fetched. A claim you believe but cannot source belongs in the report's open-questions section, named as open — not hedged into the body, and not dropped silently.
|
||||||
|
|
||||||
|
If the brief is too vague to answer, say so in research/REPORT.md and say what you would need. That is a real result. A report written against a guess about what was wanted is worse than one sentence saying the brief was unusable.
|
||||||
|
"""
|
||||||
|
# Wording follows the measured rule: say what the file must CONTAIN. Positional
|
||||||
|
# phrasing, or "and nothing else", makes the judge invent requirements it was
|
||||||
|
# never given.
|
||||||
|
done_when = "research/REPORT.md exists and answers the mission brief, with each claim in it carrying the URL of a source, and a section naming what could not be established"
|
||||||
|
max_iterations = 2
|
||||||
|
# Nothing is compiled here, so `on_green_tests` would gate on a suite that does
|
||||||
|
# not exist. There is also usually nothing to commit — a repo-less mission
|
||||||
|
# delivers by collection, not by diff.
|
||||||
|
commit_policy = "always"
|
||||||
|
|
||||||
# Which team template is the "sensible default" for the picker when
|
# Which team template is the "sensible default" for the picker when
|
||||||
# the user hasn't explicitly picked one. UI honors this.
|
# the user hasn't explicitly picked one. UI honors this.
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ requires_repo = true
|
|||||||
|
|
||||||
default_team_template = "rust_sdlc"
|
default_team_template = "rust_sdlc"
|
||||||
|
|
||||||
|
# The middle phase turns findings into a patch strategy — reading, judging and
|
||||||
|
# writing, not coding. It gets the research team; the scan and the fix keep the
|
||||||
|
# SDLC crew, which is the right shape for both.
|
||||||
|
[default_phase_teams]
|
||||||
|
research = "topic_research"
|
||||||
|
security = "rust_sdlc"
|
||||||
|
coding = "rust_sdlc"
|
||||||
|
|
||||||
# ── What is real here, and what is decoration ────────────────────────
|
# ── What is real here, and what is decoration ────────────────────────
|
||||||
#
|
#
|
||||||
# The scanners themselves are REAL: `gitleaks`, `trivy`, `semgrep` and
|
# The scanners themselves are REAL: `gitleaks`, `trivy`, `semgrep` and
|
||||||
|
|||||||
Reference in New Issue
Block a user