The documenter writes and commits docs onto the task branch in its own
workspace clone, but i_documented had no push step — so the commit stayed
local and the PM merged the already-open PR without the docs, which then
vanished on merge. i_documented now pushes the task branch before handoff,
mirroring the developer's _ensure_branch_pushed; a push failure holds the
task in awaiting_documentation for a retry instead of silently dropping the
docs. Extract _finalize_documented to keep the verb under the return-count
ceiling.
Use cast("ProjectTable", project) at the 3 _resolve() call sites where
SimpleNamespace was passed as a ProjectTable argument, satisfying mypy
without any # type: ignore suppressions. ProjectTable import is kept
under TYPE_CHECKING since the quoted cast form requires no runtime
symbol; cast is imported from typing for the runtime call.
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
A self-heal fix task was opened confirmed_by_human=false and held out of
dispatch until "Approve & Start" — but that button only renders for a
board-reviewed Intake task (pending + board_review_complete + team != main_pm),
never for a self-heal task (team=main_pm, no board review). So there was no way
to start it: it sat in pending forever and the Main PM never picked it up.
Self-heal is RoboCo healing itself, not an Intake draft — it shouldn't need a
manual Approve & Start. Origination now opens the fix task confirmed + assigned
to the Main PM agent, the PM dispatcher's self-heal hold is dropped, and the
now-dead approve_and_start special-case is removed. The fix still ships through
the normal gates (dev -> QA -> PR review -> the CEO's merge); the loop never
starts, merges, or deploys.
get_latest_ci_conclusion filtered CI runs by `project.default_branch or "main"`
— the only "main" fallback in the codebase (everywhere else falls back to
"master"). A repo whose default branch is master (like RoboCo) with an unset
default_branch matched zero runs, so the signal silently returned None: no fix
task and no notification, an invisible no-op. Align the fallback to "master".
Also make an armed-but-no-signal case loud: when self-heal is enabled and the CI
read returns None (missing/expired token, a non-default branch filter, or a
GitHub error), the telemetry source now logs a warning instead of silently
treating "couldn't read" the same as "green".
The read-clone refresh reused the orchestrator's token-less best-effort fetch,
but _clone_repo scrubs the token from the remote URL — so on a PRIVATE repo the
refresh fetch failed silently and the clone stayed frozen at clone-time, never
seeing commits merged afterwards. The panel then showed "auto-derived defaults"
for a project whose .roboco/conventions.yml was already on the default branch.
Replace the refresh with a token-authenticated fetch + hard-reset to the default
branch (the token is injected transiently into the fetch argv, mirroring the
clone; the read clone is orchestrator-side and never mounted into an agent
container). A public repo with no token still refreshes unauthenticated.
Each row of the Conventions editor grid now stretches its two cards to an equal
height (Waivers and Custom rules line up; Module boundaries and Rules line up),
and the Module-boundaries list scrolls internally so it matches the Rules card
instead of running long. Single column on mobile is unchanged.
Bump the canonical version refs (pyproject, roboco.__init__, config.app_version,
panel/package.json, uv.lock) plus the version-pin examples in the docs. The
release tag + CHANGELOG date are deferred to the actual 0.9.0 cut.
The per-project Conventions tab was one long single column in a narrow modal,
wasting all the horizontal space. Lay the sections out in a responsive grid —
Module boundaries | Rules, then Waivers | Custom rules — with Recent violations
full-width on its own row, and widen the modal on large viewports (only on the
Conventions tab; Settings stays compact). Collapses to a single column on
mobile and is capped at xl so it stays sane up to a 27" display.
The conventions standard is dev-first by design — the developer receives the
architecture map + per-task constraints at spawn and owns conforming code from
the start; QA and the PR reviewer are the downstream net. Make that explicit in
the developer prompt and add a dedicated conventions section to the RAG
developer doc (it previously only mentioned the gate, reactively).
Also reconcile the docs with the hardened behavior: env-reference now shows the
flag is off by config default but on in the compose orchestrator block (left off
in the registry), mirroring toolchain matching; and the RAG standard's example
comment no longer implies a misplaced helper blocks (it warns).
ROBOCO_CONVENTIONS_ENABLED was absent from every compose, so it ran on the
config default (off). Set it the same way ROBOCO_TOOLCHAIN_MATCH_ENABLED is —
default-true in docker-compose.yml + docker-compose.yaml, deliberately left off
in docker-compose.registry.yml so the published default stays conservative.
Override with ROBOCO_CONVENTIONS_ENABLED=false to disable.
Harden the architectural-conventions standard so it works out-of-the-box on
any project and resolves for projects that predate it, and make RoboCo pass
its own gate.
General defaults (apply to every project, not just one with a tuned file):
- The auto-scan excludes test and documentation trees (tests/, docs/) — those
legitimately define fixtures and aren't enforced code.
- Helper placement seeds at warn, not block: `helper` matches any top-level
function, too blunt a signal to hard-block a route file's small private glue.
Misplaced model/route/component stay block; the body-level thin_routes check
remains the real fat-handler guard.
- thin_routes no longer counts transaction-lifecycle calls (commit/flush/
refresh) as data access — an explicit `db.commit()` after delegating to a
service is a valid pattern.
- no_lint_suppressions exempts a small allowlist of structurally-unavoidable
framework codes (ruff TC001-TC003, pydantic prop-decorator); bare or other
suppressions still flag.
- CLAUDE.md rule-lifting skips bare common-word tokens that would match
everywhere (e.g. "commit"), keeping only specific identifiers.
- The ambient prompt block lists only constrained modules and truncates at a
line boundary with a "+N more" pointer instead of cutting mid-line.
Backfill: the standard previously read the committed file + repo scan from
project.workspace_path, a field only a manual API call set — so an older
project (or one whose workspace was cleared) showed an empty "missing" map no
matter what was pushed. The service now ensures a dedicated, default-branch
read clone on demand (WorkspaceService.ensure_read_clone) and resolves from
it, persisting the resolved path + real HEAD. The panel tab, the spawn-time
ambient block, and the per-task constraints all resolve the committed standard
with no manual setup.
Adopt in-repo: relocate the inline request/response models from the system and
*_live route modules into roboco/api/schemas/ so the codebase passes its own
placement gate, and ship a canonical .roboco/conventions.yml. no_models_in_routes
and modular_cohesion are now clean and enforced at block.
Docs updated across the user guide, the agent-facing RAG standard, the
developer and pr_reviewer role prompts, CLAUDE.md, and the changelog. New unit
tests cover the scan exclusions, helper-warn, the suppression allowlist, the
commit exemption, and the resolve/backfill path; the conventions + project
integration suites pass against Postgres.
roboco.dev is not ours, so the docs.roboco.dev custom domain can never resolve. Remove the docs/CNAME and the custom-domain site_url, and point the advertised docs URL at the free GitHub Pages project URL (https://rennf93.github.io/roboco/) — no DNS required.
The gh-pages branch deploy (mkdocs gh-deploy --force) raced GitHub's built-in branch deployment and got canceled, and each force-push wiped the custom-domain CNAME. Switch to GitHub's official Pages Actions flow (build -> upload-pages-artifact -> deploy-pages) with a single 'pages' concurrency group, so there is one deterministic deployment and no branch to force-push.
- Set the custom domain to docs.roboco.dev (site_url + a docs/CNAME that ships in the build artifact, so the domain persists across deploys).
- Point the advertised docs URL at https://docs.roboco.dev across README, the usage/deployment stubs, the Makefile help, pyproject, and CLAUDE.md.
- Requires a one-time Settings -> Pages -> Source = "GitHub Actions"; the gh-pages branch is no longer used.
Build a complete user-facing documentation site (MkDocs Material) under docs/, served at roboco.dev/docs via a new gh-pages deploy workflow.
- Sections: Get Started, The Company, the Tour, Operating the Panel, Choosing & Running Models, Cost & Observability, Optional Subsystems, Configure & Deploy, API Reference, Troubleshooting & Security (55 pages).
- mkdocs.yml (Material theme; excludes the agent-facing rag/ corpus, internal scratch, and orphaned stub trees) and .github/workflows/docs.yml (mkdocs gh-deploy to gh-pages).
- Retire the stale root usage.md and deployment.md to redirect stubs into the site.
- Fix the docs tooling: add the pymarkdownlnt dependency + .pymarkdown.json, run serve-docs/lint-docs/fix-docs under the docs extra, add a build-docs strict gate.
- Fix the roboco console-script entry point (cli, not the un-awaited async main).
- README: correct the project-structure tree (optimal.py, alembic) and link the docs site.
Documenter docs only ever reached DOCS_BASE_PATH=/app/docs (a host-mounted, RAG-indexed knowledge store) and were never committed to the project's git repository — so the documenter's deliverable never landed in the repo. write_doc now also writes the doc into the agent's workspace clone under docs/<type>/<file> and commits it onto the task branch via GitService.commit, so it rides the existing PR into the repository on merge. Best-effort: a documenter without a cloned workspace or task branch still succeeds (logged, never fatal); the /app/docs knowledge store + RAG indexing are unchanged. Adds tests that the doc is written into the workspace and committed onto the task branch, and that it no-ops cleanly without a branch.
The baseline-constraint and conventions-map integration tests asserted 'no models in routers' — the old UNIVERSAL block default. Now that placement rules are scan-derived (only seeded where the target module exists), an empty-scan test project carries no_models_in_routers no longer; the universal block rule is no_lint_suppressions. Updated the five assertions accordingly; behaviour (baseline attaches, isn't suppressed, is idempotent) is unchanged.
Make the docs and role prompts match the shipped modularity enforcement. The standards doc gains a Modularity section (cohesion / thin routes / thin components / god class, scan-derived + language-aware); the developer prompt tells agents to write modular code (thin routes that delegate, one concern per file, components that delegate to hooks) and that block-level findings refuse i_am_done; QA + PR-reviewer prompts note the modularity findings in evidence / the pr_pass block. Also fixes the two lifecycle diagrams (usage.md, roboco/models/README.md) that omitted the awaiting_pr_review gate.
The Conventions tab was read-mostly: it listed modules and toggled rule levels, but you could not add a module, a custom rule, or a waiver from the UI — you had to hand-edit YAML, which defeated the point of a managed standard. It is now a real editor: add / edit / remove module boundaries (with click-to-toggle forbidden kinds), add / edit / remove custom regex rules and their level, and add / edit / remove waivers (path + rule + reason). Saving commits the edited map back to the repo via PR, the same as before.
An existing project with no committed .roboco/conventions.yml showed an alarming amber 'Conventions degraded — missing' banner, even though that is the normal starting state (defaults apply and are already enforced). Now only an unparseable committed file is 'degraded'; missing/unknown shows a neutral 'Using auto-derived defaults' note. Save to repo is enabled in that state so an already-created project can adopt the derived map in one click (backfill), instead of being stuck with no file forever.
The standard was architectural LINTING (placement + hygiene) — things ruff/eslint already do — and it forced backend rules onto frontend projects. This makes it enforce MODULARIZATION, the separation-of-concerns a senior demands that linters are blind to:
- modular_cohesion: a file that mixes architectural concerns (a model defined in a router, a schema in a component) is a monolith — split it. One concern per file.
- thin_routes (Python): a route handler that runs its own DB access instead of delegating to a service.
- thin_components (TypeScript/React): a component that fetches data in its body instead of using a hook.
- god_class: a class past a method-count threshold (single responsibility).
The checks inspect a definition's BODY and a file's COMPOSITION via tree-sitter, precision-over-recall (fire only on a confident structural signal). Rules are now scan-derived and language-aware: hygiene seeds universally, placement only for modules that exist, and modularity per stack — so a frontend project carries no_models_in_components + thin_components, never a backend no_models_in_routers. BUILTIN_RULES is reduced to language-agnostic hygiene.
The canonical task-states doc enumerated every waiting state EXCEPT awaiting_pr_review, and its transition table + flow diagrams omitted the in-path PR gate. Adds the state, a PR Review Gate flow (submit_up / submit_root -> awaiting_pr_review -> pr_pass / pr_fail), and the role-restricted transition rows (PM opens, pr_reviewer passes or fails).
The Board is the three oversight roles — Product Owner, Head of Marketing, Auditor. Intake (Prompter), the Secretary, and the root PR Reviewer are CEO-direct helpers (per the org chart), but they carry team=board internally, so both agent groupings bucketed them under 'Board' — and on the agents page the helpers were even duplicated into both Board and On-Demand. They now render in a dedicated Support group in the journals list and the agents page; cell PR reviewers keep their cell's team and stay grouped under that cell. Board is now exactly PO/HoM/Auditor.
Documentation had drifted behind the post-0.8.0 work. Adds a CHANGELOG [Unreleased] section, documents the three new feature flags in the config reference (and removes the retired ROBOCO_RAG_USE_HYDE), a new Architectural Conventions Standard page, the provider-overload break in CLAUDE.md, the >=3.13 Python floor + feature flags in the README, and the toolchain/conventions delivery gates + structured-note model across the developer / QA / PR-reviewer role docs and the task-model doc.
_handle_stopped_container reached cyclomatic rank C (11) after the provider-overload parking branch was added, failing the xenon --max-absolute B gate on master. Extract the crash-retry-or-escalate tail into _crash_retry_or_escalate — a pure move, no behaviour change — dropping the method back under the threshold. Covered by the existing stopped-container tests (graceful exit, grok 429 park, overload park, crash-retry, escalate).
An agent's make quality runs against no Postgres, so the conftest skips every integration test and coverage collapses far below the 80% threshold — a self-hosted PM read 71% on a suite that is ~96% with a DB and chased it as a code regression. _append_gate_env now injects ROBOCO_TEST_DB_* (host/port/user/password/admin-db) from the orchestrator's own DB settings into each spawn; agents share the Docker network so the host resolves, and the conftest creates throwaway test databases isolated from the live one. The app runtime reads ROBOCO_DATABASE_*, never ROBOCO_TEST_DB_*, so this only feeds the test harness. Gated on toolchain_match_enabled, the faithful-gate flag.
The toolchain guard fails open on a recorded 'unknown' status (precision over recall — never strand a task on an inconclusive smoke). But an 'unknown' means provisioning ran yet the smoke could not confirm the suite is collectable under the interpreter, so the gate was proceeding blind with no trace — a silent hollow pass. It now emits a 'toolchain.unverified_gate_pass' warning with the agent and task ids when proceeding past 'unknown', while still not blocking; a missing marker (None) stays silent so the warning carries signal.
test_crypto's round-trip tests called the real encrypt/decrypt path, which needs settings.encryption_key configured — so they silently depended on ROBOCO_ENCRYPTION_KEY being present in the environment. In agent gate containers it is not, so four tests failed there and the agent mis-read it as a code regression. An autouse fixture now monkeypatches a valid generated Fernet key, so the tests pass without any ambient secret and agents never need the production key injected to gate.
RoboCo's code imports tomllib (3.11+) and the stack runs on 3.13, but requires-python declared >=3.10. The toolchain resolver picks the lowest satisfying version, so it provisioned agent workspaces of the self-hosted roboco-api project at Python 3.10 — an interpreter the suite cannot even be collected under, leaving the workspace .venv unusable and the gate running in an ad-hoc fallback env. Raising the floor to >=3.13 makes resolve_target_python return 3.13, matching the agent image. Re-locks to drop the now-unreachable 3.10-3.12 backports; a guard test pins the repo's own resolution to 3.13.
* feat(conventions): standard schema models + effective-map merge
* feat(conventions): tree-sitter Python classifier + placement checks
* feat(conventions): TS classifier, hygiene/custom checks, runner + CLI
* feat(conventions): ROBOCO_CONVENTIONS_ENABLED flag + cache table + migration
* feat(conventions): repo auto-scan + scaffold draft renderer
* feat(conventions): ConventionsService (cache/baseline/ambient/scaffold/restore)
* feat(conventions): auto-scaffold on project registration (flag-gated)
* feat(conventions): TaskDescription.constraints + auto-baseline attach
* feat(conventions): ambient architecture-map injection at spawn
* test(conventions): subprocess CLI smoke for the agent-image entrypoint
* feat(conventions): block i_am_done on block-level convention violations
* feat(conventions): block pr_pass on unresolved convention violations
* feat(conventions): surface convention findings into QA evidence
* docs(prompts): convention awareness for PO/Intake/Dev/QA/PR-reviewer
* feat(conventions): panel Conventions tab + flag toggle + parity
* test(conventions): end-to-end block, fix, and waiver through the gate
* refactor(conventions): extract pr_pass guards to keep pr_gate under the gate
* style(conventions): format the baseline-constraints attach in task.create
* test(conventions): type-annotate test helpers for the full mypy gate
* build(conventions): ignore types-PyYAML in deptry (mypy-only type stub)
* docs(conventions): document the standard in CLAUDE.md + PM prompt awareness
* fix(conventions): baseline constraints are non-suppressible (dedup-append)
* feat(conventions): scaffold on first workspace clone (threaded workspace)
* feat(conventions): multi-project ambient map for PO/Intake (per-product)
* feat(conventions): persist findings + violations-feed route (migration 044)
* feat(conventions): panel violations feed in the Conventions tab
* test(conventions): intake-spawn mock accepts the ambient layer kwarg
* fix(docker): ollama-init best-effort pull, gate startup on cached models present
A degraded/slow ollama registry made the model manifest re-check fail under
set -e, so ollama-init exited 1 and blocked the orchestrator's
service_completed_successfully gate — taking the whole stack down even though
both models were already cached. Pulls are now best-effort; success is gated on
the models being present, so a flaky registry can't down a cached deployment.
* refactor(content): drop dead TaskDescription.with_baseline_constraints
The structured baseline-merge helper had zero production callers. Project-task baseline constraints are attached by the wired string backstop (TaskService._attach_baseline_constraints), and a real task is free-form prose that cannot form a valid TaskDescription (requires a non-trivial objective + non-empty the_work), so the helper was unreachable from any live path — a leftover from the structured-merge -> string-append design pivot. Removing it leaves a single enforcement path. The constraints field itself stays: it is a member of the well-formed-spec schema (Objective / What This Builds / The Work / Notes / Constraints / Acceptance Criteria), rendered by render_markdown and unit-tested.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
A degraded/slow ollama registry made the model manifest re-check fail under
set -e, so ollama-init exited 1 and blocked the orchestrator's
service_completed_successfully gate — taking the whole stack down even though
both models were already cached. Pulls are now best-effort; success is gated on
the models being present, so a flaky registry can't down a cached deployment.
* feat(conventions): standard schema models + effective-map merge
* feat(orchestrator): park provider on persistent server overload (529/500)
A 429 rate limit already parks a provider — queue its spawns, probe until it
recovers — but a persistent 529/500/503 overload had no such break: the run
died and the orchestrator crash-retried straight back into the overload,
burning tokens in a respawn loop.
Generalize the park to provider-unavailability. On a non-graceful Anthropic
agent exit, match the API's overload markers (overloaded_error /
internal_server_error / "API Error: 5xx") against the dead container's own
output and park the provider with kind="overloaded"; the existing spawn gate
already queues any parked provider, and the probe-resume loop revives the task
when it recovers. Grok keeps its exit-75 path; both now route through one
_park_provider_unavailable helper. Markers are kept specific so an agent that
merely writes about HTTP 500/529 can't trip the break.
Fix the recovery probe to require a 2xx: it treated any non-429 as recovered,
so a probe that itself got a 529 would have resumed agents straight back into
the overload — wrong for the new path and for a 429 that lifts into a 5xx.
Gated by ROBOCO_OVERLOAD_BREAK_ENABLED (default on; off => crash-retry).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Adds toolchain_match_enabled to FEATURE_FLAGS so the panel's Settings ->
Feature Flags card can arm/disarm it (overriding the env default at the next
backend restart). The card is data-driven; only a one-line description blurb is
added. Validator, effective-value, and startup overlay auto-wire from the
tuple.
ROBOCO_TOOLCHAIN_MATCH_ENABLED on (default true, .env-overridable) in the
orchestrator env of docker-compose.yml + docker-compose.yaml only;
docker-compose.registry.yml is intentionally left off so the published default
stays conservative until live-verified.
The scope='handoff' branch added to note() pushed its cyclomatic complexity to
rank C (full-package xenon, --max-absolute B). Extract the non-handoff journal
validate+persist body into _write_journal_note so note() is a thin dispatch and
both stay within bound. Behavior-preserving; note tests unchanged.
make quality runs mypy over tests/ too; the toolchain test fixtures
(monkeypatch params, the fake-subprocess factories) were missing annotations.
No behavior change.
Ties the pieces at the logic level: a target whose .python-version (3.13)
conflicts with requires-python (>=3.14) provisions against 3.14, a collection
error records 'broken', and the gate guard then refuses the pass. Flag-off
provisions exactly as today and never blocks.
The agent runner stage had build-essential only in the builder. When toolchain
matching is on, uv provisions the workspace against the target project's Python
and must compile an sdist for any dependency lacking a wheel for that version —
so build-essential now stays at runtime. The runner already permits uv managed
python downloads (only the builder pins UV_PYTHON_PREFERENCE=only-system, for
RoboCo's own 3.13 venv). Verified locally: in the built image,
`uv venv --python 3.14 && uv run python` prints Python 3.14.6 while the system
interpreter stays 3.13.
Adds _toolchain_broken_guard: when toolchain matching is on and the acting
agent's workspace recorded a 'broken' status (the project's suite cannot be
collected under the provisioned interpreter), i_am_done, pass_review, and
pr_pass refuse with an i_am_blocked remediation instead of letting the role
"pass" on a source read. fail_review / pr_fail stay available. git gains
toolchain_status_for_task (resolves the actor's workspace, reads the marker,
fail-open None). Inert when the flag is off; a missing/unknown status never
strands a task.
When toolchain matching is enabled, install_dev_deps resolves the target's
Python and provisions the venv against it (uv sync --extra dev --python <v>,
uv auto-downloads), then runs a runnability smoke (uv run pytest --collect-only)
and records {python, status} in a .git/.roboco-toolchain marker the gates read.
Precision over recall: only a pytest collection error (the interpreter-mismatch
signature) reports 'broken'; pytest-absent / timeout report 'unknown' so a gate
never blocks on an inconclusive smoke. Flag off => provisioning is unchanged.
ROBOCO_TOOLCHAIN_MATCH_ENABLED (default-off) gates the whole subsystem.
Adds work_sessions.toolchain_python / toolchain_status (nullable VARCHAR(20))
recording the interpreter the workspace was provisioned with and whether the
target's suite can be executed (ok | broken | unknown). Migration 042 verified
with a real upgrade head + downgrade -1 + re-upgrade on a throwaway Postgres.
Pure resolver (services/toolchain.py) that derives the Python version an agent
should provision a target workspace with. Defends against uv's resolution
order — a .python-version file overrides requires-python during interpreter
selection — by honoring the pin only when it satisfies requires-python, else
resolving a concrete version from requires-python for the caller to pass via
--python. This is the root cause behind the live guard-core-app failure (pin
3.13 vs packages needing 3.14). Promotes packaging to a direct dependency.
The obligations added in 8cf69781 (dev_notes@i_am_done, quick_context@delegate,
pr_reviewer_notes@pr verbs, auditor@i_am_idle) were only discoverable at
runtime via the gate's remediate field. Surface them upfront so agents satisfy
them on the first call instead of looping into a tracing_gap:
- base.md: the gap-key reference gains rows for dev_notes>=min /
quick_context>=min / pr_reviewer_notes>=min (parity with the journal rows).
- developer.md: a note(scope='handoff') step before i_am_done, and dev_notes
added to the i_am_done precondition list.
- cell_pm.md / main_pm.md: fill quick_context (done+next) before the first
delegate (it persists across the whole queue).
- auditor.md: must record an observation before i_am_idle.
The pre-write cases (dev_notes, quick_context, auditor) carry the real loop
risk; pr_reviewer / doc notes are satisfied by the verb's own argument, so the
base.md row alone suffices for those.