Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e690afd
feat(conventions): standard schema models + effective-map merge
rennf93 Jun 22, 2026
f8c4e70
feat(conventions): tree-sitter Python classifier + placement checks
rennf93 Jun 22, 2026
50e1881
feat(conventions): TS classifier, hygiene/custom checks, runner + CLI
rennf93 Jun 22, 2026
dcb9bbd
feat(conventions): ROBOCO_CONVENTIONS_ENABLED flag + cache table + mi…
rennf93 Jun 22, 2026
0b7b9fd
feat(conventions): repo auto-scan + scaffold draft renderer
rennf93 Jun 22, 2026
8fe0925
feat(conventions): ConventionsService (cache/baseline/ambient/scaffol…
rennf93 Jun 22, 2026
811b91b
feat(conventions): auto-scaffold on project registration (flag-gated)
rennf93 Jun 22, 2026
df6b9cc
feat(conventions): TaskDescription.constraints + auto-baseline attach
rennf93 Jun 22, 2026
0bd6357
feat(conventions): ambient architecture-map injection at spawn
rennf93 Jun 22, 2026
eae3c3c
test(conventions): subprocess CLI smoke for the agent-image entrypoint
rennf93 Jun 22, 2026
3b064ac
feat(conventions): block i_am_done on block-level convention violations
rennf93 Jun 22, 2026
d7a60bd
feat(conventions): block pr_pass on unresolved convention violations
rennf93 Jun 22, 2026
bf7a769
feat(conventions): surface convention findings into QA evidence
rennf93 Jun 22, 2026
6b24f4f
docs(prompts): convention awareness for PO/Intake/Dev/QA/PR-reviewer
rennf93 Jun 22, 2026
8f76db2
feat(conventions): panel Conventions tab + flag toggle + parity
rennf93 Jun 22, 2026
3ee5dd9
test(conventions): end-to-end block, fix, and waiver through the gate
rennf93 Jun 22, 2026
7555c59
refactor(conventions): extract pr_pass guards to keep pr_gate under t…
rennf93 Jun 22, 2026
7da5f01
style(conventions): format the baseline-constraints attach in task.cr…
rennf93 Jun 22, 2026
306443e
test(conventions): type-annotate test helpers for the full mypy gate
rennf93 Jun 22, 2026
3b18f1d
build(conventions): ignore types-PyYAML in deptry (mypy-only type stub)
rennf93 Jun 22, 2026
f5d418d
docs(conventions): document the standard in CLAUDE.md + PM prompt awa…
rennf93 Jun 22, 2026
c79ca4d
fix(conventions): baseline constraints are non-suppressible (dedup-ap…
rennf93 Jun 22, 2026
1e4bf09
feat(conventions): scaffold on first workspace clone (threaded worksp…
rennf93 Jun 22, 2026
e99c710
feat(conventions): multi-project ambient map for PO/Intake (per-product)
rennf93 Jun 22, 2026
c1c3efa
feat(conventions): persist findings + violations-feed route (migratio…
rennf93 Jun 22, 2026
448686f
feat(conventions): panel violations feed in the Conventions tab
rennf93 Jun 22, 2026
03f0006
test(conventions): intake-spawn mock accepts the ambient layer kwarg
rennf93 Jun 22, 2026
6f2582b
fix(docker): ollama-init best-effort pull, gate startup on cached mod…
rennf93 Jun 22, 2026
0127551
refactor(content): drop dead TaskDescription.with_baseline_constraints
rennf93 Jun 22, 2026
bbb107c
Merge branch 'master' into feature/architectural-conventions-standard
rennf93 Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,17 @@ Agent backends are pluggable. `roboco/llm/providers/` defines an `AgentProvider`

**Self-healing CI loop (default-off).** RoboCo can watch its own repository's CI (a single named workflow) and, on a detected regression, open a fix task that is held out of dispatch until the CEO approves it (it terminates at `awaiting_ceo_approval`), then dispatch it through the normal delivery flow. It is dormant by default and armed by `ROBOCO_SELF_HEAL_ENABLED` plus a second opt-in `ROBOCO_SELF_HEAL_ORIGINATE_ENABLED`; origination is bounded by `ROBOCO_SELF_HEAL_MAX_OPEN_TASKS` / `_MAX_PER_CYCLE` so it can't flood the backlog. It never auto-merges or self-deploys (`roboco/services/self_heal_engine.py`).

**Feature flags / company-in-a-box.** Env-gated, default-off subsystems toggle from the panel's Settings → Feature Flags card (`panel/src/components/settings/feature-flags-card.tsx`) instead of hand-editing env: web research (`ROBOCO_RESEARCH_ENABLED`), the strategy engine (`ROBOCO_STRATEGY_ENGINE_ENABLED`), pitch provisioning (`ROBOCO_PROVISIONING_*`), external / internal PR review, and the self-heal flags above. A toggle persists in the settings store and takes effect on the next backend restart; an unset flag falls back to its environment / config default.
**Feature flags / company-in-a-box.** Env-gated, default-off subsystems toggle from the panel's Settings → Feature Flags card (`panel/src/components/settings/feature-flags-card.tsx`) instead of hand-editing env: web research (`ROBOCO_RESEARCH_ENABLED`), the strategy engine (`ROBOCO_STRATEGY_ENGINE_ENABLED`), pitch provisioning (`ROBOCO_PROVISIONING_*`), external / internal PR review, the agent-runtime toolchain match (`ROBOCO_TOOLCHAIN_MATCH_ENABLED`), the architectural-conventions standard (`ROBOCO_CONVENTIONS_ENABLED`), and the self-heal flags above. A toggle persists in the settings store and takes effect on the next backend restart; an unset flag falls back to its environment / config default.

## Architectural Conventions Standard

**Per-project architectural standard (default-off).** Beyond the `make`-style gates (which check syntax/types/tests, not *where code lives*), each project can carry a repo-canonical `.roboco/conventions.yml` — an architecture map (which definition *kinds* belong in which modules), a toggleable rule set, custom regex rules, and waivers — so an agent cannot land a Pydantic model defined inside a router, a helper in a route file, or a `# noqa` / `# type: ignore`. Gated by `ROBOCO_CONVENTIONS_ENABLED`; fully inert when off.

**Effective map.** Consumers read the *effective* map — auto-derived defaults (from a repo scan + `BUILTIN_RULES`) overlaid by the committed file — so behaviour is identical whether the file is present, absent, or partial. `ConventionsService` (`roboco/services/conventions.py`) builds it, caches it per `(project, HEAD sha)` in `project_conventions_cache` (migration `043`), renders the per-task baseline constraints + the ambient prompt block, and scaffolds/restores the file via a PR (`GitService.open_conventions_pr`). The schema lives in `roboco/foundation/policy/conventions/` (pure).

**Validator.** A single Python CLI, `python -m roboco.conventions check --root <repo> --files <a> <b> ...` (`roboco/conventions/`), uses tree-sitter (Python + TypeScript grammars, shipped in the agent image) to classify each changed definition and flag forbidden placements + hygiene + custom-rule matches as JSONL findings, after waiver filtering. Precision over recall (it abstains when uncertain so a `block` gate can't false-positive-strand a task) and fail-loud (a validator that cannot run exits 3 so the gate blocks, never silently passes).

**Threading + enforcement.** The standard reaches the work two ways: an ambient "Architectural Standard" block injected at spawn (`compose_prompt`) and an auto-attached `## Constraints` section on every project task (`TaskService.create`). Enforcement is deterministic: a `block`-level finding refuses `i_am_done` (dev pre-submit) and `pr_pass` (the in-path PR gate) with the offending `file:line` + fix hint; findings also surface in QA's `claim_review` evidence (`convention_findings`). A false positive is relieved by a `waiver` the dev commits in their branch — accountable, reviewed in the PR. The panel's per-project Conventions tab (in the edit-project dialog) shows the map + health and offers Save / Restore.

## Services

Expand Down
2 changes: 1 addition & 1 deletion agents/prompts/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Every verb returns a JSON envelope. There are exactly two shapes:
The envelope's top-level `error` is one of four categories:

- `tracing_gap` — a precondition (commit, PR, journal entry, plan, etc.) is missing. Look at `missing` for the literal field key. See the cheatsheet below.
- `invalid_state` — task is in a status that doesn't allow this verb (e.g. cannot `start` a `cancelled` task). The `message` names the actual status. Common phrasings: "task X is in <status>; cannot start work", "task X is in <status>, expected awaiting_qa for review", "parent task X is in pending; must be in_progress to accept subtasks", "claim failed", "start failed for task X", "fail_review requires at least one issue", "no commits on this task yet", "parent already has N subtasks; cap is 12".
- `invalid_state` — task is in a status that doesn't allow this verb (e.g. cannot `start` a `cancelled` task). The `message` names the actual status. Common phrasings: "task X is in <status>; cannot start work", "task X is in <status>, expected awaiting_qa for review", "parent task X is in pending; must be in_progress to accept subtasks", "claim failed", "start failed for task X", "fail_review requires at least one issue", "no commits on this task yet", "parent already has N subtasks; cap is 12", "N architectural-convention violation(s) must be fixed" (a definition is in the wrong module per `.roboco/conventions.yml`, or a lint/type suppression slipped in — `remediate` lists each `file:line` + fix; move it, or for a genuine false positive add a `waiver` to `.roboco/conventions.yml` in your branch).
- `not_authorized` — your role / assignment / channel-access doesn't permit this. The `message` names the rule. Common phrasings: "not assigned to you", "role 'cell_pm' may not commit code; only developers and documenters write commits", "Cell PM cannot claim code tasks. PMs coordinate, never execute code.", "you are not the assignee of {task_id}; cannot post content to it", "agent '{X}' may not write to channel '{Y}'", "role X cannot send formal notifications".
- `not_found` — task / agent / channel id doesn't exist.

Expand Down
2 changes: 2 additions & 0 deletions agents/prompts/roles/board.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ When the briefing carries `company_goals`, that charter is your reference for tr
4. If it's CEO-worthy: `escalate_to_ceo(task_id, reason="...")`. (PO + Head of Marketing only — Auditor cannot escalate; record critical observations as reflect-notes for the CEO to find.)
5. If it's just an observation: `note(scope='reflect', text='...')` and `i_am_idle()`.

When you refine product scope or review a cell's delivery (Product Owner especially), consult the project's architectural map (`.roboco/conventions.yml`) and name the load-bearing placement constraints — which definition kinds live in which modules — so the cells carry them; the standard is enforced at `i_am_done` / `pr_pass`, so scope that ignores it only creates rework.

## Journaling cadence

The Board's journal IS the work product. Most of what you do never produces a verb call — it produces a recorded observation that the CEO and Main PM consume. **Decision and reflect scopes take structured fields — fill them; a flat phrase is a regression.**
Expand Down
2 changes: 2 additions & 0 deletions agents/prompts/roles/cell_pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ You are a coordinator. You receive a task from Main PM, you break it into focuse

You merge what your developers submit (leaf PRs into your cell branch via `complete`), and you submit your cell branch up to Main PM via `submit_up`. You never merge to master — that is the CEO's seat.

When the architectural-conventions standard is on, every subtask you `delegate` already carries the project's placement constraints (auto-attached as a `## Constraints` section), and your `submit_up` cell PR runs the conventions gate — a definition in the wrong module per `.roboco/conventions.yml` or a lint/type suppression makes the PR reviewer `pr_fail` your branch. So before you `complete` a dev's leaf or `submit_up`, confirm the work sits where the architecture map says; never ship a block-level violation up the chain.

When the briefing carries `company_goals`, let the charter guide how you scope and prioritize the subtasks you cut: favour decomposition that advances the stated objectives and respects the constraints.

## Inputs you start with
Expand Down
1 change: 1 addition & 0 deletions agents/prompts/roles/developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ The gateway enforces some of these; the rest are convention but failing one of t
5. ✅ `note(scope='reflect', task_id=...)` walks through every criterion (gateway-enforced as `journal:reflect`).
6. ✅ `open_pr(task_id)` has been called and the response returned a PR number (gateway-enforced via `pr_number` set).
7. ✅ `notes` argument to `i_am_done` is your self-verification summary — what you tested, edge cases considered, anything QA should look at first.
8. ✅ Each definition lives in the module the project's architectural map (`.roboco/conventions.yml`) assigns it and follows the task's `## Constraints` — a Pydantic model belongs in `models/`, not the router; no helpers in routers; no lint/type suppressions. A block-level violation refuses `i_am_done` with the `file:line` + fix; move it, and if a finding is a genuine false positive, add a `waiver` to `.roboco/conventions.yml` in your branch for the PR to review.

If any item fails, do not retry `i_am_done`; fix the missing piece first.

Expand Down
2 changes: 2 additions & 0 deletions agents/prompts/roles/main_pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

You are a coordinator at the org level. You receive a root task from the Board or CEO, you decide which cells need to work on it, you delegate ONE subtask per cell to that cell's PM (`be-pm`, `fe-pm`, `ux-pm`), and once those cell-PMs come back with merged work you open the master PR and escalate the root to the CEO. That is the entire job.

When the architectural-conventions standard is on, each cell's task already carries the standard's placement constraints, and your `submit_root` master PR runs the conventions gate — a block-level placement or hygiene violation in the assembled diff (a definition in the wrong module per `.roboco/conventions.yml`, a lint/type suppression) makes the PR reviewer `pr_fail` it before the CEO ever sees it. Route scope that respects the architecture map, and never escalate a root whose diff carries an unresolved block-level violation.

**You do NOT write code. Ever.** **You do NOT delegate to a developer directly** — every code subtask goes to a Cell PM, who breaks it down further. **You do NOT call `Bash git ...`** — you have no commit verb, and the orchestrator denies raw git anyway. **You do NOT call `i_will_work_on`** — that is the developer's claim verb; yours is `i_will_plan`. **You do NOT merge to master** — that is the CEO's seat. If a Cell PM escalates a blocker to you, your job is to fix the *delegation problem* (clarify scope, reassign, unblock) — not to "just do the change yourself". If you find yourself reaching for `Edit`, `Write`, or `Bash git`, stop — you are about to step out of role; the right move is `unblock`, `delegate`, or `escalate_up`.

You merge what your Cell PMs submit (cell PRs into your root branch via `complete`). When all cell-PM subtasks are terminal, you open the master PR via `complete` on the root task, which transitions it to `awaiting_ceo_approval`. The CEO approves and merges to master.
Expand Down
1 change: 1 addition & 0 deletions agents/prompts/roles/pr_reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ The PR is from an outside contributor: its code is **untrusted**. Until a human
- ❌ Pushing to the contributor's fork, or editing/merging the PR. You review; you never write or merge.
- ❌ A trickle of vague comments. Post ONE complete review; each finding names file + line + expected vs actual.
- ❌ Approving without reading the full diff.
- ❌ Being lax on the architectural standard. Be mega-strict: on an in-path gate review, a `block`-level convention violation (a definition in the wrong module per `.roboco/conventions.yml`, a helper/model in a router, a lint/type suppression) is an automatic `pr_fail` — the gate already refuses `pr_pass`, and an introduced or expanded `waiver` must be justified in the diff or rejected. Hold placement and house-style to the same bar as correctness.

## When the gateway returns an error

Expand Down
1 change: 1 addition & 0 deletions agents/prompts/roles/prompter.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ When — and only when — you can write a complete spec:
- `team` is the lead cell for single-cell work: one of `backend`, `frontend`, `ux_ui`. `scale` is `single` (one cell) or `multi` (board-led across cells). Each cell's `items` is its ordered list of independently-shippable units (one per intended PR), dependency-first so independent units run in parallel.
- Call `propose_draft` only once you're confident — it's what the human reviews and confirms. If the conversation continues and the spec changes, call it again with the updated draft.
- Don't call it with a partial or speculative draft just to fill a turn. Prose-only is correct until the spec is real.
- The project's architectural standard (`.roboco/conventions.yml`) is auto-attached to every task as a `## Constraints` section server-side, so you don't restate the generic rules. Do add any *task-specific* placement constraint you learned in the interview — a shared DTO's exact home, a cross-cell contract — to `notes` so each cell builds it in the right module.

## What happens after you call `propose_draft`

Expand Down
1 change: 1 addition & 0 deletions agents/prompts/roles/qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The gateway requires `learning` before `pass`/`fail`. Your `notes` argument carr
6. ✅ `note(scope='learning', task_id=...)` written.
7. ✅ For `pass`: `notes` >= 80 chars, names the criteria you verified and the artifact behind each.
8. ✅ For `fail`: each entry in `issues` is concrete and actionable — criterion + file + line + expected/actual. "Doesn't work" is not an issue.
9. ✅ Read `convention_findings` in your `claim_review` evidence — it lists architectural-standard violations on the diff (misplaced definitions, lint suppressions). Flag any block-level finding in your `issues`; a `could_not_run` entry means the validator failed and the placement is unverified, so don't pass on a clean-looking diff.

## Channels

Expand Down
56 changes: 56 additions & 0 deletions alembic/versions/043_conventions_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Add the project_conventions_cache table.

Caches the parsed *effective* architectural-conventions map per
``(project_id, commit_sha)`` so the map is re-derived only when HEAD moves.
``status`` records how the repo's ``.roboco/conventions.yml`` resolved at that
SHA (``ok`` | ``degraded`` | ``missing``). Pure schema change; no backfill.
Inert until ``ROBOCO_CONVENTIONS_ENABLED``.

Revision ID: 043_conventions_cache
Revises: 042_worksession_toolchain
Create Date: 2026-06-22

NOTE: revision id is 21 chars — alembic's ``alembic_version.version_num`` is
``VARCHAR(32)`` and a longer id raises at record time.
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

revision = "043_conventions_cache"
down_revision = "042_worksession_toolchain"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"project_conventions_cache",
sa.Column("id", sa.UUID(as_uuid=True), nullable=False),
sa.Column("project_id", sa.UUID(as_uuid=True), nullable=False),
sa.Column("commit_sha", sa.String(length=40), nullable=False),
sa.Column("effective_map", postgresql.JSONB(), nullable=False),
sa.Column("status", sa.String(length=20), nullable=False),
sa.Column("derived_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"project_id", "commit_sha", name="uq_project_conventions_cache_sha"
),
)
op.create_index(
"ix_project_conventions_cache_project_id",
"project_conventions_cache",
["project_id"],
)


def downgrade() -> None:
op.drop_index(
"ix_project_conventions_cache_project_id",
table_name="project_conventions_cache",
)
op.drop_table("project_conventions_cache")
63 changes: 63 additions & 0 deletions alembic/versions/044_convention_findings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Add the project_convention_findings table (violations feed).

Persists architectural-conventions validator findings per task so the panel
can show recent block/warn violations across a project. Pure schema change;
no backfill. Inert until ``ROBOCO_CONVENTIONS_ENABLED``.

Revision ID: 044_convention_findings
Revises: 043_conventions_cache
Create Date: 2026-06-22

NOTE: revision id is 23 chars — alembic's ``alembic_version.version_num`` is
``VARCHAR(32)`` and a longer id raises at record time.
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op

revision = "044_convention_findings"
down_revision = "043_conventions_cache"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"project_convention_findings",
sa.Column("id", sa.UUID(as_uuid=True), nullable=False),
sa.Column("project_id", sa.UUID(as_uuid=True), nullable=False),
sa.Column("task_id", sa.UUID(as_uuid=True), nullable=True),
sa.Column("file", sa.String(length=500), nullable=False),
sa.Column("line", sa.Integer(), nullable=False),
sa.Column("rule", sa.String(length=100), nullable=False),
sa.Column("level", sa.String(length=20), nullable=False),
sa.Column("kind", sa.String(length=40), nullable=True),
sa.Column("message", sa.Text(), nullable=False),
sa.Column("detected_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_project_convention_findings_project_id",
"project_convention_findings",
["project_id"],
)
op.create_index(
"ix_project_convention_findings_detected_at",
"project_convention_findings",
["detected_at"],
)


def downgrade() -> None:
op.drop_index(
"ix_project_convention_findings_detected_at",
table_name="project_convention_findings",
)
op.drop_index(
"ix_project_convention_findings_project_id",
table_name="project_convention_findings",
)
op.drop_table("project_convention_findings")
Loading
Loading