Commit Graph
5 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 5c2c63f8e8 feat(missions): a human can finally reach the plan/roster review gate
Phase 4 of the plan, plus the PLAN_COMPLETE decision and the gitea_forge
cleanup from Phase 5.

THE REVIEW UI

mission_plan and mission_roster have been complete and reachable by curl
since they shipped, with zero frontend. That matters more than a missing
screen usually would: the decide step is not a convenience, it IS the
safety mechanism. Approving a plan replaces the mission's phases; approving
a roster flips it to the composed engine. A gate nobody can reach is a gate
that is always open or always shut.

MissionProposalDrawer, modelled on LevelUpDrawer which already does
load → review → decide. Reached from a mission's SETUP tab. Verified end to
end against the live backend, not just compiled: a model proposed a roster,
approval flipped the mission to `composed`, and approval on a non-draft
mission was refused.

The plan view shows each phase's done_when, and says plainly when one is
absent — a phase without a completion condition is never judged and reports
completed whatever it did, so its absence is the thing worth seeing.

AND THE DEFECT BUILDING IT FOUND

Every refusal path computed a precise reason — "the mission is running, not
a draft", "no node can boot that backend any more" — logged it to stderr,
and returned a bare {"error":"bad request"}. The person who needed the
sentence was the one clicking Approve; they got two words, and the reason
went to a server log they cannot read.

ApiError::Refused(String) carries it now. Same argument ApiError::Unavailable
was added for ("a 500 with 'internal error' sent them looking for a bug that
was not there"), one status code down. Live: the 400 now reads "this mission
is completed — a roster can only be approved while it is a draft, because
approving one rewrites how the mission will run".

PLAN_COMPLETE, decided

The Skill-Use measurement found that int-xx-marker-protocol documents
PLAN_COMPLETE and task_card_parser never implemented it, so an agent
following the skill exactly was silently ignored. Implemented rather than
removed from the skill: the planner needs a way to say it is done
specifying, and agents already emit it.

Marker ids are now strictly INT-<digits>. `starts_with("INT-")` accepted the
range form `INT-01..02` — observed live — which parsed into an id matching
no real item, so a task card appeared for something that did not exist while
the two items it covered stayed open. Rejecting is right: an ignored marker
is visible, a plausible row is not.

GITEA_FORGE, REMOVED

Named in nine places, defined in none. Harmless while provision_claw ignored
the bundle list; once the list was honoured, an undefined name became a
capability an agent is told it has and does not. Removed from seven team
templates, a workflow recipe, the auto-provision path, and a dropdown a user
could pick it from.

A new test asserts every bundle a template names is defined in the runtime
config — and it immediately found `web_fetch` in two templates I had missed
removing by hand. Same shape as the skill-binding test, one layer up.

Agents reach the forge through git over HTTPS with the ambient GITEA_TOKEN,
which is why nothing ever broke.

Full workspace suite green (106 binaries); frontend builds clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-19 11:56:54 -07:00
Omar SobhandClaude Opus 5 18dc0b964b fix(missions): the security scan phase now scans, and task upserts work
Four defects, found by checking the audit's claims instead of trusting
them. Two of the audit's own findings turned out to be wrong, and the
registry that exists to record which config keys are read was itself
inaccurate — so the corrections are part of the change.

upsert_task raised 42P10 on every call, for every caller
  `mission_tasks_external_uniq` is a PARTIAL unique index (WHERE
  external_id IS NOT NULL). Postgres will not match a partial index to an
  ON CONFLICT target unless the statement repeats the predicate, so the
  upsert failed on its first row. Both callers — the task-card parser that
  turns INT markers into tasks, and the security scanner — map the error to
  a string their caller logs. Two features were broken and nothing was red.
  Regression test in cm-db with a negative control: reverting the WHERE
  reproduces 42P10 exactly.

the security scan never ran
  `security_scan::run` was reachable only from an operator button, so
  security_hardening.toml — a workflow whose entire first phase is a scan —
  ran an agent that was never told to scan and never fired the scanner
  either. phase_runner now sweeps finished security_scan phases, mirroring
  the benchmark baseline sweep that was added for the identical defect.
  Guarded on a new completion marker rather than on findings: a clean scan
  writes no findings, so a findings-guard would rescan forever. The marker
  also answers the question an operator actually asks, which is not "how
  many findings" but "was this looked at, by what, and when".

two recipes could not fail
  security_hardening.toml and benchmark.toml carried no `task` and no
  `done_when` on any phase. A phase without done_when never enters
  evaluating, is never judged, and reports completed whatever it did — so a
  security mission could scan nothing and go green, and a benchmark mission
  could record no baseline that the next refactor would then compare
  against. Both now state the work and the condition, with inert keys
  annotated inline rather than deleted, so the gap between what a recipe
  asks for and what a phase receives stays visible.

the config registry was wrong in both directions
  `harness` was listed NOT IMPLEMENTED while benchmark_runner reads it and
  phase_runner runs a baseline through it. `tools` was listed NOT
  IMPLEMENTED while security_scan::run reads it. A registry that exists so
  an operator can trust what a recipe does is worse than useless when it is
  inaccurate. Both corrected, `bench_name` and `cmd` added, and
  `test_command` deleted — it had neither a reader nor a writer, so it
  described a situation that could not arise.

Also: CLAWMATES_JUDGE_MODEL had two different defaults (opus-4-8 in
routes/topology.rs vs opus-5 in cm_runtime::judge_model) and a doc comment
naming a third; topology now calls the one function. GITEA_TOKEN's absence
in mission_plan is stated rather than degrading to the same "could not be
read" string a private repo produces.

BRAINHUB_API_KEY needed no change — hub::push already rejects an unset key
with a named error. That half of the finding was overstated.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-19 08:08:27 -07:00
Omar SobhandClaude Opus 5 49bcf53b84 feat(missions): wire the workflow registry so phase config reaches the database
workflow_registry.rs had zero call sites -- lib.rs declared the module and
nothing ever called load() or get(). So templates/workflows/*.toml was never
read, and because the client's TEMPLATE_PRESETS carries only {kind, order_idx}
with no config, PhaseSpec.config defaulted to Value::Null and every
wizard-created mission stored a null mission_phases.config.

Every per-phase setting was therefore inert. `loop = "until_no_more_int_items"`
and `commit_policy = "on_green_tests"` described a scheduler that does not
exist AND had no path to the database. benchmark_runner and security_scan
already read phase_config(); they were reading from null.

- Mission create derives phases from the recipe when none are sent, and
  backfills config per phase (matched on kind+order_idx, then kind) when the
  caller sends shape without config. An explicit config always wins.
- phases_for_create takes Option<&WorkflowRecipe> rather than reaching for the
  global, because the registry resolves its directory relative to the process
  cwd -- which under cargo test is the crate root, not the repo root.
- GET /api/workflows serves the catalog; the wizard fetches it and falls back
  to TEMPLATE_PRESETS. Adding a TOML now adds a template with no FE change.
- load() runs at boot so a malformed recipe appears in the boot log instead of
  silently producing a mission with no phase config.

Also fixes a latent bug in all five recipes: `default_team_template` was
written below the first [[phases]] block, and TOML scopes a bare key after a
table header INTO that table -- so it parsed as
phases[last].config.default_team_template and the real field was always None.
Invisible while the registry was dead code. Moved above the phases, with a
test asserting it neither returns None nor leaks into a phase config.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 12:43:32 -07:00
Omar SobhandClaude Opus 5 285d0c82f2 chore: delete dead scaffolding and stop fabricating claw capability cards
Tier 0 of the prompt-ablation pass -- subtraction only, none of this
reached a model.

- cm-brain: drop ClawBrain::export_markdown (zero callers).
- workflows: drop the `task_preamble` keys. No Rust code ever read them --
  WorkflowPhase.config is an opaque serde_json::Value -- so the comment
  calling the preamble "the belt, the skill the suspenders" described a belt
  that was never implemented. (`commit_policy` is unread for the same reason;
  left in place as documentation pending a decision.)
- mcp_door: derive the unknown-tool error from EXPOSED_TOOLS. The literal had
  drifted to naming one of the three tools the door exposes.
- Dashboard.tsx: drop TEAM_TEMPLATES/COMPANY_TEMPLATES, defined and never
  referenced, and disconnected from the real templates/teams/*.toml.

The substantive one: GET /api/claws/{id}/compartments returned hardcoded
strings for tools/capabilities/safety, identical for every claw. Every card
read "Network: none" and "Shell . blocked" regardless of the claw's real
risk_profile -- which is the actual capability boundary, so the card was
most wrong exactly where it mattered, on a coding_readwrite claw that does
have shell. Now derived from the claw's effective risk_profile (its team's
setting, else the same role-derived default the provisioner applies), with
the allowlists mirroring [risk_profiles.*] in the runtime config.

Note: cm-topology/src/heuristics.rs was slated for deletion here as unused.
It is not -- routes/topology.rs:43 serves it and p0_endpoints.rs:302 asserts
it. Left alone.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 10:44:33 -07:00
Omar SobhandClaude Opus 4.7 565f6cae65 slice 4: 5 workflow templates as TOML recipes + mission-launch orchestrator
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m20s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m50s
Two things land together:

1. Ships the 5 workflow recipes as TOML files under
   templates/workflows/*.toml:
     - research_only          (hub_spoke research → MD + PDF)
     - research_and_code      (research → coding loop until INT-XX drained)
     - security_hardening     (scan → research patches → coding with
                               reviewer approval + full MCP bundle)
     - refactor               (single-pass coding with dep audit
                               preamble + before/after benchmarks)
     - benchmark              (author + baseline benchmarks per stack)
   Each declares phases[], per-phase config, default_team_template.
   Loaded read-only into an in-memory registry (workflow_registry)
   via OnceLock — no DB row per recipe.

2. Ships the mission-launch orchestrator that closes the loop from
   Slice 3.5d's mechanics. When a mission transitions draft→running,
   `mission_orchestrator::on_launch`:
     - Reads mission.team_template_id (skips if unset)
     - Loads the team template detail (roles + skills bindings)
     - Builds a topology graph from role slots via cm_topology::build
     - Inserts the teams row + stamps template_id/version/risk_profile/mcp_bundles
     - For each role: agent insert, model binding, runtime provision
       (opt-in via RuntimeProvisioner::from_env), brain_seed::ingest
       (Slice 3.5d), agent_template_link::upsert (Slice 3.5d),
       team_members bind, audit trail
     - UPDATE missions SET team_id = ...
   Wired into routes::missions::set_status when prior.status='draft'
   and new='running'. Failures log + are non-fatal (mission still
   flips to running so the user can inspect + retry).

With this, Slice 3.5d's brain-seed + link machinery actually gets
populated, and the MCP skills server's template-defaults-merge path
(Slice 3.5b/d) starts serving real bindings to real agents.

Follow-ups (Slice 5-8):
  - Task-card parser watches run events for TASK/COMPLETED markers
    → mission_tasks rows
  - PDF renderer worker turns MD artifacts into PDFs
  - Before/after benchmark runner honors phases[].config.benchmark
  - Security scan MCP bundle exposes cargo-audit/gitleaks/trivy/semgrep
  - Level-up endpoints diff learned-vs-seeded via agent_template_link

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 14:34:26 -07:00