Files
roboco/roboco/models/project.py
T
153723406e Feat/autonomous maintenance (#264)
* feat(ci-watch): config flags

Default-off CI-watch config (mirrors self_heal_*): ci_watch_enabled,
ci_watch_default_workflow (ci.yml), ci_watch_interval_seconds (1800),
ci_watch_max_open_tasks (3), ci_watch_max_per_cycle (1). Registers
ci_watch_enabled in the panel FEATURE_FLAGS. 4 tests.

* feat(ci-watch): per-project ci_watch_enabled/workflow (migration 048)

Adds projects.ci_watch_enabled (bool NOT NULL default false) +
projects.ci_watch_workflow (varchar null) — the per-project opt-in for
multi-repo CI-watch. ProjectTable + Pydantic Project fields + migration 048
(off 047_ws_single_active). Real upgrade->downgrade->upgrade chain verified
against a throwaway Postgres; 2 ORM round-trip tests.

* feat(runtime): prune dangling agent images in the background sweeper

Every agent-image rebuild orphans the prior build's layers as an untagged
<none> image; across deploys these pile up (the operator hit ~80). The sweeper
now runs 'docker image prune -f --filter dangling=true' (dangling only — a
tagged image or one backing a running container is never dangling), throttled
to settings.image_prune_interval_seconds (default 6h) and gated by
image_prune_enabled (default on). Best-effort: any failure is logged, never
raised into the sweeper. Mirrors the transcript-retention prune. 4 tests.

* feat(ci-watch): source tag + open-task dedupe query

CI_WATCH_SOURCE='ci_watch' + TaskService.list_open_ci_watch_tasks(git_url=None):
non-terminal ci_watch tasks (the dedupe + open-cap basis), optionally scoped to
one repo by git_url — a monorepo registers several cell-projects on one git_url,
so dedupe keys on the repo, not the slug. 2 real-PG tests.

* feat(ci-watch): multi-project CI telemetry fan-out

MultiProjectCITelemetrySource.fetch(projects) reuses the hardened per-project
get_latest_ci_conclusion for each opted-in project (passing its ci_watch_workflow
or the configured default). Per-project isolation: a GitHub error or absent
signal yields NO sample (unknown, never read as green) and never aborts the
sweep; only a real conclusion yields a sample (fail→breach, pass→non-breach).
self-heal source untouched. 3 tests + self-heal regression green.

* feat(ci-watch): engine — fan-out, originate, dedupe, cap

CiWatchEngine.run_cycle(projects) mirrors SelfHealEngine: assess via
MultiProjectCITelemetrySource, open one PENDING ci_watch fix task per red repo
(team=main_pm, assigned_to=main-pm, confirmed_by_human=True so it dispatches
without an Approve-&-Start — the fe029fe3 lesson), never starts/approves/merges.
Dedupe per git_url (monorepo → one fix task per repo) + per-cycle/rolling caps.
Default-off; disabled → no-op. 5 real-PG tests (red→one task, dedupe, cap,
green/none→nothing, disabled).

* feat(ci-watch): orchestrator loop tick + watch-set loader

_ci_watch_loop (registered in start(), cancelled in stop(), separate from the
untouched self-heal loop): dormant unless ci_watch_enabled; each interval loads
the watch set (ci_watch_enabled projects, collapsed one-per-repo via the
existing _projects_one_per_repo) and runs CiWatchEngine.run_cycle, committing
opened tasks. _run_ci_watch_cycle extracted for testing; loud warning when
enabled-but-empty. confirmed_by_human=True on the originated task means it
dispatches without an Approve-&-Start (no stranding, the fe029fe3 lesson).
5 tests (disabled no-op, watch-set filter+one-per-repo, empty warn, engine run).

* docs(ci-watch): CHANGELOG + CLAUDE.md for multi-repo CI-watch

Document CI-watch (Added) in the CHANGELOG and the Self-Healing & Feature Flags
section of CLAUDE.md — it generalizes self-heal to opted-in projects, reuses the
hardened per-project CI lookup, never auto-merges, default-off. Adds the
ci_watch_enabled flag to the feature-flags enumeration.

* feat(dep-update): config flags

Default-off dep-update config (mirrors self_heal_*/ci_watch_*): dep_update_enabled,
dep_update_interval_seconds (604800 = weekly), dep_update_max_open_tasks (3),
dep_update_max_per_cycle (1). Registers dep_update_enabled in FEATURE_FLAGS. 4 tests.

* feat(dep-update): per-project dep_update_command/paths (migration 049)

Adds projects.dep_update_command (varchar null) + dep_update_paths (varchar[]
null) — the per-project opt-in for the dependency-update bot. ProjectTable +
Pydantic Project fields + migration 049 (off 048_ci_watch_project_cols). Real
upgrade->downgrade->upgrade chain verified on a throwaway Postgres; 2 ORM tests.

* feat(dep-update): source tag + open-task dedupe query

DEP_UPDATE_SOURCE='dep_update' + TaskService.list_open_dep_update_tasks(git_url=None):
non-terminal dep_update tasks (dedupe + open-cap basis), optionally scoped to one
repo by git_url (monorepo → one open dependency-update task per repo). 2 real-PG
tests.

* feat(dep-update): read-only lockfile-diff probe

WorkspaceService.dry_upgrade_changes_lockfile(project): clones the project's
read clone into a throwaway dir (--no-hardlinks, so the read clone is never
mutated), runs project.dep_update_command (no shell, shlex.split), and reports
whether any lockfile path (dep_update_paths or inferred uv.lock/pnpm-lock.yaml)
is dirty. Fail-safe: null/failing command → False (don't originate on a broken
probe), logged; throwaway always removed; never commits/pushes. 5 real-git tests.

* feat(dep-update): engine — detect, originate, dedupe, cap

DepUpdateEngine.run_cycle(projects) mirrors SelfHealEngine/CiWatchEngine: for
each opted-in project (dep_update_command set) with updates available (the
read-only probe), open one PENDING dep_update task (team=main_pm, assigned-to
main-pm, confirmed_by_human=True), never starts/approves/merges. Cheap checks
(command, per-git_url dedupe) before the expensive probe; per-cycle + rolling
caps. Default-off; disabled → no-op. 6 real-PG tests.

* feat(dep-update): weekly orchestrator loop tick

_dep_update_loop (registered in start(), cancelled in stop(), separate from the
self-heal + CI-watch loops): dormant unless dep_update_enabled; each interval
(default weekly) loads projects with a dep_update_command (one-per-repo) and runs
DepUpdateEngine.run_cycle, committing opened tasks. _run_dep_update_cycle
extracted for testing; loud warning when enabled-but-no-commands. Refactored
stop() to cancel background tasks via a shared _cancel_background_task loop
(keeps it under xenon B as the loop count grows). 4 loop tests.

Task 7 (anti-stranding dispatch guard) is satisfied by construction: no
dispatcher skip targets source='dep_update', and the engine sets
confirmed_by_human=True (the fe029fe3 lesson), asserted in the engine tests —
so the originated task dispatches via the assigned-PM path, never stranded.

* docs(dep-update): CHANGELOG + CLAUDE.md for the dependency-update bot

Document the dep-update bot (Added) in the CHANGELOG and the Self-Healing &
Feature Flags section of CLAUDE.md — read-only lockfile-diff probe, never
auto-merges, per-project opt-in via dep_update_command, default-off. Adds the
dep_update_enabled flag to the feature-flags enumeration.

* feat(ci-watch): route fix-task notification to the project's cell PM

On opening a fix task, CiWatchEngine notifies the red project's own cell PM
(resolved from project.assigned_cell via foundation AGENTS — e.g. BACKEND →
be-pm), not the CEO, once per project per cycle. Best-effort: a notification
failure never rolls back the origination. Adds _cell_pm_slug_for +
_notify_cell_pm. 1 real-PG test (asserts to_agent='be-pm', not 'ceo').

* feat(ci-watch,dep-update): expose per-project opt-ins in the project API

Add ci_watch_enabled/ci_watch_workflow + dep_update_command/dep_update_paths to
ProjectUpdate, ProjectUpdateRequest, the PATCH route mapping, ProjectResponse,
and project_to_response — so the panel edit-project dialog can read + set the
per-project autonomy opt-ins (the columns were unreachable through the API
before). Also threads the previously-dropped quality_command through the update
route. 1 real-PG update round-trip test.

* feat(ci-watch,dep-update): panel project-edit fields for the per-project opt-ins

Adds an 'Autonomous Maintenance' section to the edit-project dialog: a CI-watch
enable switch + workflow input, and a dependency-update command + lockfile-paths
input (comma-separated → list). Threads the four fields through the Project /
ProjectUpdate TS types and the mock-mode create fixture. The global on/off
toggles already live in Settings → Feature Flags; these are the per-project
opt-ins. panel tsc --noEmit + eslint green.

* docs(0.12): CI-watch + dep-update bot + image-prune across user docs + RAG

New docs/optional/autonomous-maintenance.md (mirrors self-heal.md) covering both
engines; optional/index rows; panel settings + projects-and-products notes for
the Feature Flags toggles + the edit-project Autonomous Maintenance fields;
resilience note for the dangling-image prune; env-reference + RAG config-reference
tables for all ROBOCO_CI_WATCH_* / ROBOCO_DEP_UPDATE_* / ROBOCO_IMAGE_PRUNE_*
vars; mkdocs nav entry. reflow-check green; prompts unchanged (operator-facing,
not agent-facing).

* chore(release): 0.12.0

Cut [Unreleased] -> [0.12.0] (CI-watch + dep-update bot + image-prune housekeeping
+ the post-0.11.1 run-hardening fixes). Bumps all 8 canonical version refs to
0.12.0 (pyproject / uv.lock roboco pkg / panel package.json / __init__ /
config.app_version + the README / deployment / agent-image-tag examples).

* fix(pr-review): repo-scope external-PR dedupe (no duplicate review on a monorepo)

external_review_task_exists keyed on (project_id, pr, head_sha), but a monorepo
registers several cell-projects on one git_url and the poll already collapses to
one canonical project per repo — so once a review task was re-pointed to a
sibling project, the next poll (checking the canonical project) no longer saw it
and opened a second review of the same PR (observed: PR #131 reviewed once on
guard-core-saas-frontend, once on -backend). Dedupe now spans every project
sharing the PR's repo (git_url); re-review on a new head SHA still works; a
genuinely different repo with the same PR number is independent. 3 real-PG tests.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-25 21:11:36 +02:00

179 lines
5.9 KiB
Python

"""
Project Model
A git repository that agents work on. Projects are registered by PMs
and contain configuration for test commands, branch policies, and
cell assignments.
"""
from datetime import datetime
from enum import StrEnum
from uuid import UUID, uuid4
from pydantic import Field
from roboco.models.base import RobocoBase, Team, TimestampMixin
class BranchReason(StrEnum):
"""Reason/type prefix for branch naming."""
FEATURE = "feature"
BUG = "bug"
CHORE = "chore"
DOCS = "docs"
HOTFIX = "hotfix"
class Project(TimestampMixin):
"""
A git repository that agents work on.
Projects are registered by Main PM or Cell PM and contain
configuration for the development workflow.
"""
# Identity
id: UUID = Field(default_factory=uuid4, description="Unique project identifier")
name: str = Field(..., min_length=1, max_length=100, description="Project name")
slug: str = Field(
...,
min_length=1,
max_length=50,
pattern=r"^[a-z0-9-]+$",
description="URL-safe identifier (e.g., 'roboco', 'roboco-panel')",
)
# Git Configuration
git_url: str = Field(..., description="Git repository URL")
default_branch: str = Field(default="master", description="Default branch name")
protected_branches: list[str] = Field(
default_factory=lambda: ["main", "master"],
description="Branches that cannot be pushed to directly",
)
# CI/CD Commands (optional - project may not have all)
test_command: str | None = Field(
default=None, description="Command to run tests (e.g., 'uv run pytest')"
)
lint_command: str | None = Field(
default=None, description="Command to run linter (e.g., 'uv run ruff check .')"
)
format_command: str | None = Field(
default=None,
description="Command to format code (e.g., 'uv run ruff format .')",
)
typecheck_command: str | None = Field(
default=None,
description="Command to run type checker (e.g., 'uv run mypy src/')",
)
build_command: str | None = Field(
default=None, description="Command to build (e.g., 'pnpm build')"
)
quality_command: str | None = Field(
default=None,
description=(
"Fast pre-submit gate command run in the dev's workspace at "
"i_am_done (lint+types+complexity, no tests; e.g. 'make gate'). "
"When set it replaces the lint/typecheck pair in the gate."
),
)
# Access Control
assigned_cell: Team = Field(..., description="Which cell owns this project")
allowed_agents: list[UUID] | None = Field(
default=None, description="Specific agents allowed (None = all in cell)"
)
# Git Authentication (token stored encrypted, never exposed)
has_git_token: bool = Field(
default=False,
description="Whether a git token is configured (token never exposed)",
)
# Runtime State
workspace_path: str | None = Field(
default=None,
description="Local path to workspace (e.g., /data/workspaces/{slug})",
)
last_synced_at: datetime | None = Field(
default=None, description="Last time project was synced from remote"
)
head_commit: str | None = Field(default=None, description="Current HEAD commit SHA")
# Autonomous maintenance opt-in (multi-repo CI-watch)
ci_watch_enabled: bool = Field(
default=False, description="Watch this project's CI and auto-open fix tasks"
)
ci_watch_workflow: str | None = Field(
default=None, description="Workflow file to scope the CI-watch signal to"
)
# Dependency-update bot opt-in
dep_update_command: str | None = Field(
default=None,
description="Command to refresh lockfiles, e.g. 'uv lock --upgrade'",
)
dep_update_paths: list[str] | None = Field(
default=None,
description="Lockfile globs to inspect (null → infer uv.lock/pnpm-lock.yaml)",
)
# Metadata
created_by: UUID = Field(..., description="PM who registered the project")
is_active: bool = Field(default=True, description="Whether project is active")
class ProjectCreate(RobocoBase):
"""Schema for creating/registering a new project."""
name: str = Field(..., min_length=1, max_length=100)
slug: str = Field(..., min_length=1, max_length=50, pattern=r"^[a-z0-9-]+$")
git_url: str
default_branch: str = "master"
protected_branches: list[str] = Field(default_factory=lambda: ["main", "master"])
assigned_cell: Team
# Git authentication (will be encrypted and stored securely)
git_token: str | None = Field(
default=None,
description="GitHub PAT for clone/push/PR operations (stored encrypted)",
)
# Optional commands
test_command: str | None = None
lint_command: str | None = None
format_command: str | None = None
typecheck_command: str | None = None
build_command: str | None = None
quality_command: str | None = None
class ProjectUpdate(RobocoBase):
"""Schema for updating a project."""
name: str | None = None
git_url: str | None = None
default_branch: str | None = None
protected_branches: list[str] | None = None
# Git authentication (empty string clears token, None leaves unchanged)
git_token: str | None = Field(
default=None,
description="GitHub PAT (empty string clears, None leaves unchanged)",
)
test_command: str | None = None
lint_command: str | None = None
format_command: str | None = None
typecheck_command: str | None = None
build_command: str | None = None
quality_command: str | None = None
assigned_cell: Team | None = None
allowed_agents: list[UUID] | None = None
is_active: bool | None = None
ci_watch_enabled: bool | None = None
ci_watch_workflow: str | None = None
dep_update_command: str | None = None
dep_update_paths: list[str] | None = None