Commit Graph
319 Commits
Author SHA1 Message Date
Renn F b18bdcd41a fix(optimal): serialize singleton init, normalize kb_search index_types, surface mentor errors
The OptimalService singleton published the instance before initialize()
finished, so a concurrent caller could observe _initialized=False and hit
"OptimalService not initialized" during RAG indexing. Build the instance,
initialize it, then publish under a lazily-bound asyncio lock so all callers
share a fully-initialized singleton.

roboco_kb_search forwarded the legacy alias index_types=['docs'], which is
not a valid IndexType value (the enum value is 'documentation'), producing a
400 at the route. Normalize the alias in the client before the request is
sent and fix the misleading tool docstring.

The mentor route let exceptions from mentor.ask escape as a bare 500 that
masked the real cause. Catch, log the true upstream error with stack, and
surface it in the response detail so failures are diagnosable.
2026-06-03 18:54:07 +02:00
Renn F 2c96bd09f6 fix(gateway): heartbeat on content-write success, single-claimant gate, progress soft-warn
commit()/progress()/note() now refresh last_heartbeat_at on the success
path (best-effort, suppressed), not only on rejection — an actively
writing agent no longer looks idle to the reaper between verb successes.

commit()/progress() verify the caller holds the active claim
(active_claimant_id), not merely the historical assigned_to, so a reaped
or handed-off assignee can no longer write onto a freed task; a non-holder
gets a not_authorized envelope with a clear remediate.

progress() with no plan_step on a task that has steps is accepted (product
decision for narrative mid-step updates) and logs a soft warning instead of
rejecting.
2026-06-03 18:44:33 +02:00
Renn F 61e80495c3 fix(workspace): scope _ensure_agent_owned walk to .git subtree only
Walking the entire workspace (incl node_modules) to chown+chmod every
entry cost 2.7-15.5s per git op. The agent only needs write ownership on
.git/ during git ops; working-tree files don't need chowning. Restrict
the walk to .git, and no-op when .git is absent.
2026-06-03 18:34:31 +02:00
Renn F e1c3f926f2 fix(orchestrator): a cell code task never routes to board / main_pm by keyword
A code task whose title/description hit a board keyword (launch, release,
architecture, security) was classified to 'board' — so a cell dev code task
got 'reviewed' by the Product Owner + Head of Marketing — and a high-complexity
cell code task was escalated to 'main_pm', which is how a PM ended up owning
(and deadlocking) a dev code task. A code task that belongs to a cell
(backend/frontend/ux_ui) is implementation work: route it WITHIN the cell
(cell_pm for high/pm-keyword, else dev), never to board/main_pm. The strategic
board/cross-cell heuristics now apply only to team-less top-level tasks.
2026-06-03 16:01:03 +02:00
Renn F 8f7e9c09e1 fix(git): scope fetches, dedicate a git thread pool, raise network timeout + instrument
The panel dev's commit/open_pr verbs timed out. Verified it is NOT chown
(node_modules is agent-owned, the walk is sub-second) and the commit even
landed locally; the branch never reached origin. The bottleneck is the
server-side verb path under concurrent load — git status (a 0.044s op) also
timed out, which only happens when the path AROUND git is slow.

Defensible fixes + instrumentation to confirm under load:
- Scope every all-refs 'git fetch origin' to just the refs the path needs
  (base/default/branch). An all-refs fetch on a monorepo with dozens of
  branches is pure network cost on every branch creation/checkout.
- Run git subprocesses + the ownership repair in a dedicated bounded thread
  pool so concurrent agents don't queue behind / starve the event loop's
  shared default executor.
- Give fetch/pull/push a dedicated git_network_timeout_seconds (120s) instead
  of the 30s local-op default — a push on a large private monorepo can
  legitimately exceed it.
- Log any git subprocess or chown slower than 1s so the next run pinpoints
  where the time goes (op vs ownership repair) instead of guessing.
2026-06-03 15:55:32 +02:00
Renn F 5f61ea7b82 test: fix stale activate-guard test + time-fragile grace-window test
- The activate test asserted the old 'no project set' guard; activate now
  needs a project OR a product (coordination tasks carry only a product), so
  it raises only when BOTH are absent. Renamed, set product_id=None, and match
  the current 'no project or product' message.
- The grace-window test used a module-load _FRESH timestamp, but the grace
  check uses wall-clock now(), so _FRESH aged out of the window during a long
  full-suite run and flaked. Compute 'fresh' at test time.
2026-06-03 08:46:50 +02:00
Renn F 93c46d86e7 fix(logging): host-side log fallback writes to ./data/logs, not a duplicate ./logs
_resolve_log_dir fell back to ./logs (relative to CWD) whenever /data was
absent — i.e. every host-side run (pytest, scripts, the dev orchestrator).
The container writes to /data/logs, whose host side is the compose mount
${ROBOCO_DATA_DIR:-./data}/logs, so ./logs and ./data/logs were two
different directories. Point the host-side fallback at that same
${ROBOCO_DATA_DIR:-./data}/logs, eliminating the duplicate ./logs at the
repo root.
2026-06-03 08:36:45 +02:00
Renn F 4c59bfa840 fix(prompt): Main PM delegate description is a goal+constraints brief, not a solution-dump
delegate already requires substantive acceptance_criteria (TASK_AT_CREATE
completeness), so the defect was not missing criteria — it was the Main PM
writing the cell-PM subtask description as prescriptive prose that dictates
the cell's solution (e.g. a full UX layout for a design task), doing the
cell's job and wasting the expertise it delegated to. The role doc now tells
the Main PM the description is a brief: state the goal + the constraints to
fit, leave the HOW to the cell, and for design tasks give the problem, not a
mockup.
2026-06-03 08:32:51 +02:00
Renn F 9aacd45c26 fix(gateway): board co-reviewer may inspect the shared board task via evidence
note/say/dm let a board co-reviewer act on a board coordination task
assigned to the other board member (via _board_may_co_review), but
evidence used its own inline ownership gate that didn't — so the Head of
Marketing got not_authorized inspecting the PO-assigned board task. Honor
_board_may_co_review in evidence too.
2026-06-03 08:30:21 +02:00
Renn F a5af6159d7 fix(gateway): evidence surfaces the task's board/PM handoff journal entries
journal_highlights_for_task was a Phase-1 stub returning [], so evidence()
never surfaced the upstream Product Owner / Head of Marketing review. The
Main PM picked up a board-reviewed coordination task, saw no handoff, and
re-researched from scratch (duplicated work). Query the task's
decision/reflection/note entries across authors (slug + role), oldest
first, so the Main PM builds on the board's analysis instead of redoing it.
2026-06-03 08:27:10 +02:00
Renn F 815f3ebad3 refactor(schemas): migrate Pydantic class Config to ConfigDict
Class-based `Config` is deprecated in Pydantic v2 and removed in v3.
Replace all 11 `class Config: from_attributes = True` blocks across the
API schemas with `model_config = ConfigDict(from_attributes=True)`.
2026-06-03 08:08:46 +02:00
Renn F ceb4eec6ca feat(board): gate CEO Approve & Start on board-review completion
A board/coordination task stays pending throughout board review — that
pending state is what hands it to Main PM on approval — so the CEO's
Approve & Start button was live from the instant the task was created,
before the Product Owner and Head of Marketing had reviewed anything.
That let the CEO approve before the board finished.

Persist a board_review_complete flag the orchestrator sets once BOTH
board reviewers are done, and gate the button on it (the task stays
pending). The same handoff emits the formal CEO notification, so the
CEO gets an actionable signal instead of buried channel chatter.

- alembic 021: add tasks.board_review_complete (default false)
- TaskService.mark_board_review_complete: set the flag without leaving pending
- orchestrator: flag the task + notify CEO once both reviewers go idle
- panel: Approve & Start requires board_review_complete
2026-06-03 08:06:41 +02:00
Renn F 056ff41293 feat(gateway): frontend cell task waits on the UX/UI design before dispatch (cross-cell sequencing) 2026-06-03 07:08:52 +02:00
110aaa7a77 Chore: v1 removal gateway canonical (#46)
* chore(agent_sdk): remove dead /traceability/remind endpoint and reminder map

The TRACEABILITY_REMINDERS dict and its /traceability/remind endpoint were
keyed entirely on pre-gateway tool names (roboco_task_*, roboco_journal_*,
roboco_message_send, roboco_session_create_for_tasks) deleted in the gateway
cutover. The endpoint had zero callers; v2 enforces traceability server-side
in the Choreographer.

* fix(bootstrap,seeds): onboarding prompts call give_me_work(), not deleted roboco_task_scan()

The startup prompt and the seeded cell/all-hands channel onboarding messages
instructed agents to call roboco_task_scan() — a tool removed in the gateway
cutover. Point them at the live give_me_work() flow verb.

* fix: replace remaining deleted v1 tool names with gateway verbs

Spawn prompts, onboarding strings, remediation messages, and comments still
referenced pre-gateway tools deleted in the cutover (roboco_task_*,
roboco_agent_idle, roboco_notify_*, roboco_message_send,
roboco_session_create_for_tasks, roboco_journal_*, roboco_escalate). Rewrote
each to the correct role-scoped gateway verb (give_me_work/i_will_work_on for
workers, triage for PMs, i_am_done vs complete, notify/notify_ack, escalate_up,
unclaim, i_documented, open_session, note). Updated one enforcement-message
test that matched the old tool name by coincidence.

* test: guard against deleted v1 tool names reappearing in roboco/

Scans roboco/ for the deleted pre-gateway tool names; excludes the orphaned
roboco/agents/ subtree (removed in a later phase).

* chore(exceptions): drop 8 unused pre-gateway exception classes + their tests

LLMError, RAGError, AlreadyExistsError, TaskBlockedError, TaskClaimError,
AgentNotAvailableError, AgentBusyError, NotificationPermissionError were never
raised in production. SessionClosedError/DatabaseError are kept (live + tested).

* chore(models): drop unused pre-gateway notification/channel/handoff factories

Removes create_task_assignment/_blocker_escalation/_review_request/
_documentation_request/_priority_change/_alert/_broadcast, create_cell_channel/
_cross_cell_channel/_announcements_channel, create_handoff (+ HandoffParams),
ProactiveContext, and A2APartType. The gateway choreographer builds these
server-side now. Drops the matching dead-code tests.

* chore(services): drop unused pre-gateway permission/messaging/audit/optimal/remediation methods

These pre-gateway helpers (channel-permission checks, channel-membership ops,
permission-denial audit hooks, doc ingestion, two remediation hints) have no
production caller — the gateway role_config + enforcement layer replaced them.
Drops the matching dead-code tests; live methods (send_message, the SESSION_*
flow, log_task_action_denial, etc.) are untouched.

* chore(orchestrator,ws,events,config): drop unused pre-gateway lifecycle/broadcast/roster symbols

orchestrator: get_running_agents, is_agent_busy, queue_priority_work,
get_all_instances (+ their OrchestratorAccessProtocol declarations in events.py).
websocket: broadcast_new_message, broadcast_session_closed (no event type emits
them). agents_config: ALL_PMS/ALL_DEVS/ALL_QA/CELL_PMS roster constants (ALL_DOCS
stays — it gates docs-write workspace perms).

* refactor(agents): delete orphaned pre-gateway agent subtree + dead organization model

The Gateway/full cutover replaced the Python agent-class implementations with
the server-side Choreographer; the classes survived only as a self-referential
island. Removes roboco/agents/{base,mixins,factory,board,developer,documenter,
pm,qa,orchestrator}.py and roboco/agents/factories/{board,cells,developers,
documenters,pms,qa}.py, plus roboco/models/organization.py (Cell/Board/
Organization — used only by those factories). Keeps factories/_base.py
(compose_prompt — the live prompt-layer composer the orchestrator calls at
spawn) behind minimal package __init__ files.

* chore(db): drop dead tasks.execution_log + outputs columns (migration 015)

Both JSON columns had zero readers/writers in code, tests, and migrations —
execution progress is tracked via progress_updates and artifacts via
commits/documents. Removes the ORM columns, the Pydantic Task.execution_log/
outputs fields, the ExecutionLog/FileRef models (+ their __init__ exports), and
the now-invalid kwargs from test fixtures. Migration 015 (down_revision
014_drop_pm_approvals) verified live: upgrade drops, downgrade re-adds.
Apply on the NAS with 'alembic upgrade head' at next deploy.

* chore(config): drop 16 unread Settings fields

Verified unused (no settings.X, no self.X property use, no getattr-by-name):
app_name, reload, workers, openai_api_key, secret_key, access_token_expire_minutes,
algorithm, log_level, log_format, the four session_* limits, message_max_length,
commit_subject_min_chars, commit_banned_words, agent_budget_sweep_interval_seconds.
Removes the empty Logging + Sessions&Messages sections and orphaned .env.example
vars. Kept: redis_db/redis_password (redis_url property), agent_sla_* (read via
getattr in task_lifecycle), encryption_key, and all live thresholds.

NOTE: commit_banned_words/commit_subject_min_chars and
agent_budget_sweep_interval_seconds were feature-config never wired to their
consumer (commit validator / budget sweep) — removed as dead, but flagged in
case the intent was to wire them.

* test(lifecycle): give i_will_work_on calls a substantive plan (#171 contract)

The real-DB lifecycle tests called i_will_work_on with a 13-char plan and no
risks/technical_considerations, so the substantive-plan gate (#171) rejected
them with incomplete_input — failing on master. Supply a >=150-char plan plus
technical_considerations and risks (mirroring tests/unit/gateway/
test_choreographer_dev.py). All 6 now pass; gate runs with no deselect.

* feat(gateway): wire commit-validator thresholds to settings

commit_subject_min_chars and commit_banned_words were config defined but never
read — the gateway commit() gate used the validator's hardcoded module defaults.
Re-add the two Settings fields and pass them through validate_commit_message in
content_actions.commit(), so config is the source of truth (validator defaults
remain the standalone/CI fallback). Adds wiring tests that monkeypatch settings
and assert the gate honors them.

* refactor(orchestrator): retire gateway_enabled flag; trigger_filter is unconditional

The gateway_enabled Settings field gated only the trigger_filter spawn-cooldown
(never the agent tool surface). Prod ran it on; the Phase-0 'legacy dispatch
path' it guarded no longer exists. Remove the field + the early-return branch in
gateway_pre_spawn_check so the cooldown runs for every spawn, drop the now-dead
ROBOCO_GATEWAY_ENABLED from docker-compose.yml, and update the stale Phase-0
comments + cooldown test. The per-container ROBOCO_GATEWAY_ENABLED env (set by
_append_manifest_args, read by agent_sdk to load the manifest) is unaffected.

* refactor(api): relabel /api/v2 -> /api/v1 as the canonical gateway surface

The gateway is the only agent API now, so the 'v2' label (with no v1) was
misleading. Renames roboco/api/routes/v2 -> routes/v1, schemas/v2 -> schemas/v1
(+ the matching test dirs and test_v2_role_dep/test_schemas_v2_flow files),
rewrites every /api/v2 path, routes.v2/schemas.v2 import, and v2-* router tag to
v1, and refreshes the stale 'v2' comments/docstrings. The panel is untouched (it
uses the unversioned /api/* REST routes). flow_server/do_server now POST to
/api/v1/*.

* docs(scripts): reset_runtime_state header matches actual SQL behavior

The header claimed it preserves groups + journals, but the .sql wipes both
(verified live: groups 6->0, journals 5->0; only agents/projects/channels
survive). Correct the wiped/preserved lists to match.

* refactor(gateway): extract _build_rich_plan to drop i_will_work_on under the complexity gate

i_will_work_on was cyclomatic rank C (11) — one over the xenon --max-absolute B
threshold — because of the five `x or default` fallbacks in the rich_plan dict.
Move that dict into a small _build_rich_plan helper (behaviour identical); both
methods are now rank B. make quality is fully green (xenon was its last failure;
bandit already passed — its 34 findings are all LOW severity, filtered by -ll).

* feat(foundation): add canonical CELL_TEAMS set; dedupe cell-subset literals

* feat(db): add ProductTable + ProductProjectTable ORM (per-cell project map)

* feat(task): add additive nullable product_id (ORM + model + DTO + create threading)

* feat(task): thread product_id through create_subtask/route/response

* feat(db): migration 016 — products, product_projects, tasks.product_id

* fix(db): document migration 016 plan deviations (revision len, FK name)

Two values in migration 016 intentionally diverge from the Task 2.4 plan
literals; this strengthens the in-file justification so the deviations are
self-documenting and verifiable.

- revision id (plan line 623): the plan's 36-char
  "016_add_products_and_task_product_id" overflows alembic_version.version_num
  (VARCHAR(32)) — alembic upgrade head raises asyncpg
  StringDataRightTruncationError. Kept at 27 chars
  ("016_add_products_product_id") so Step 4's live round-trip stays green.
- downgrade FK name (plan line 683): roboco/db/base.py sets a metadata
  naming_convention, so the FK upgrade() creates is
  "fk_tasks_product_id_products", not the Postgres default
  "tasks_product_id_fkey". The plan literal does not exist in the DB and
  would fail the downgrade with "constraint does not exist".

Both verified via the live upgrade/downgrade round-trip on a throwaway DB.

Issue 3 note: the prior commit (b896cac) also touched
tests/unit/api/test_schemas_tasks.py (added product_id=None to the
task_to_response stub). That line is load-bearing — task_to_response reads
task.product_id (added in Task 2.3, commit 67afa6b) — and belongs to Task 2.3's
scope; it is left in place because removing it breaks 4 tests and history is
not rewritten.

* refactor(db): trim migration 016 deviation notes to plan-faithful form

Reverts the out-of-scope documentation expansion (commit 1a4f296), which
was a second undocumented commit beyond Task 2.4's single plan-specified
commit and only bloated the migration docstring/comments.

The migration file now matches the plan-specified commit (b896cac) byte for
byte: the two necessary deviations from the plan literals stay (revision id
shortened to fit alembic_version.version_num VARCHAR(32); downgrade FK name
follows db/base.py's metadata naming_convention), each kept to a concise
inline note in the plan's header style.

The Task 2.3-scoped test stub line (tests/unit/api/test_schemas_tasks.py
product_id=None) is load-bearing — task_to_response reads task.product_id —
and is left in place; history is not rewritten.

Verified: live alembic upgrade head + downgrade to 015 round-trip on a
throwaway DB drops products/product_projects/tasks.product_id cleanly, and
make quality is green.

* refactor(test): annotate db_session and drop type: ignore in migration 016 test

Annotate the test_products_tables_and_task_fk_exist param as
db_session: AsyncSession (imported under TYPE_CHECKING) and remove the
# type: ignore[no-untyped-def] suppression, matching the typed db_session
pattern used across tests/integration/.

* feat(models): Product + ProductCreate/Update + ProductCellMapping (cell-validated)

* refactor(models): minimize ProductCellMapping config override to use_enum_values

The previous override re-declared validate_assignment, populate_by_name,
and extra=forbid, which RobocoBase already supplies. Pydantic merges
model_config across inheritance, so overriding only use_enum_values=False
is sufficient to keep team as a real Team enum (required so team in
CELL_TEAMS and enum identity hold for callers) while inheriting the rest
of the base config.

* fix(models): document ProductCellMapping use_enum_values override as plan-mandated

Resolves SPEC-COMPLIANCE review notes for Task 3.1 (Product domain models).

1. The ProductCellMapping use_enum_values=False override is a deviation from a
   bare project.py mirror, but it is mandated by the plan's own Task 3.1 code:
   RobocoBase sets use_enum_values=True, which coerces team to the plain string
   "backend". The plan's Step 1 test asserts m.team is Team.BACKEND (enum
   identity) and the Step 3 validator formats its error with v.value, both of
   which require team to remain a real Team enum. The override is therefore
   necessary; this commit relabels the comment to cite the specific spec lines
   that force it instead of leaving it as an unexplained departure. Downstream
   Task 3.2 (_replace_cells / project_for) already tolerates either form and the
   ORM stores the same value regardless, so the override has no behavioral reach
   beyond the in-memory enum identity the plan's test checks.

2. test_product_model.py hoists 'from uuid import uuid4' to module level rather
   than inline (as the plan's verbatim Step 1 code shows) because the global
   Pylint PLC0415 rule (import-outside-top-level) forbids inline imports and
   there is no per-file-ignore for tests/unit/models/. The hoisted form is the
   only ruff-clean rendering of the plan's test; left unchanged here.

3. Task 3.1 landed across two commits (c616d95 create, 6ebad255 refactor) rather
   than the plan's single Step 5 commit. Earlier history is intentionally not
   rewritten; this single follow-up commit brings the model to its final
   spec-faithful, fully-documented state.

* feat(service): ProductService CRUD + project_for per-cell resolver

* feat(api): Product CRUD routes + schemas, wired into the app

* fix(api): roll back and map cell-replacement IntegrityError on product update

update_product replaced cells via ProductService._replace_cells without
any try/except, so a duplicate-team cell (uq_product_projects_product_team)
or a non-existent project_id (product_projects.project_id FK) raised an
IntegrityError at flush, poisoning the AsyncSession and surfacing an
unhandled 500 with no rollback. Wrap the update + commit in a try/except
that rolls back and maps the UNIQUE violation to 409 and the FK violation
to 422, mirroring create_product's rollback discipline. Add integration
tests covering both client-error paths.

* fix(api): map create_product cell-mapping IntegrityError to 409/422

create_product only caught the slug conflict ('already exists' in str(e))
and bare-raised everything else, so a cells entry whose project_id does not
reference any project let the product_projects.project_id FK IntegrityError
propagate out of the route as an unhandled 500. The matching update_product
path was already hardened (uq_product_projects_product_team -> 409, FK
violation -> 422); apply the same mapping in create_product so a bad
project_id (or a duplicate-team cell) is a client error, not a server error.
The slug conflict is now caught as ConflictError directly instead of via a
broad except + string match.

* feat(gateway): add optional project_id to delegate inputs/request/routes

* feat(gateway): per-cell project routing (override -> product map -> parent) + product_id inheritance

* feat(task): approve_and_start — reassign board task to Main PM (CEO gate #1)

* feat(api): POST /tasks/{id}/approve-and-start (CEO gate #1, notes-required)

* test(api): cover approve-and-start 404-before-notes-gate for missing task

* feat(panel): Product types + Task.product_id

* feat(panel): productsApi + hooks + tasksApi.approveAndStart

* feat(panel): Products management screen + sidebar nav

* feat(panel): Approve & Start button (CEO gate #1)

* fix(api): narrow delete_product to IntegrityError + cover 204/409 delete paths

* test(task): assert approve_and_start persists + appends the audit note

* refactor(db): migration 016 names the tasks.product_id FK explicitly (house style)

* fix(db): make migrations authoritative + self-heal orphan product tables

init_db() no longer silently falls back to create_all when alembic upgrade
fails. That fallback masked migration failures and, since create_all cannot
ALTER an existing table, left the schema inconsistent — turning an unapplied
migration 016 into a crash loop: 016's CREATE TABLE products failed, the
upgrade rolled back, create_all re-created an empty orphan products table, and
every later boot failed again on the now-existing table while tasks.product_id
never got added. Now a migration failure is raised so the real error surfaces.

Migration 016 additionally drops EMPTY orphan products/product_projects tables
left by the old fallback before creating them, so an already-polluted DB
self-heals on the next deploy with no manual SQL. Skipped in offline (--sql)
mode; refuses to drop a table that holds rows.

* fix(db): create_all is the schema source of truth; alembic for increments

The Alembic chain is incomplete relative to the ORM — columns/tables like
notifications.delivered_at and the RAG indexed_documents table have NO migration
and have only ever been materialized by create_all. Tests don't catch this
because the test DB is also built via create_all, so migrations are never
exercised. The prior 'migrations are authoritative' init_db (and before it, the
create_all-only-on-failure fallback) therefore left a migrate-only boot with
missing columns/tables.

init_db now reflects reality:
  - Fresh DB  -> create_all builds the full current ORM schema, then stamp
                 Alembic at head so later incremental migrations apply.
  - Existing  -> run pending migrations (a real failure is raised, not masked),
                 then create_all(checkfirst) to gap-fill any missing ORM tables.
create_all cannot add a column to an existing table, so an ORM column added
without a migration needs a fresh rebuild of that table to appear.

* fix(db): migration 017 reconciles the Alembic chain with the full ORM schema

For years the live schema was built by create_all, not migrations, so the chain
drifted — tables/columns/indexes in the ORM had no migration (the
indexed_documents table, notifications.delivered_at, ~15 indexes, plus
timestamptz/server-default metadata). With init_db no longer masking that via a
create_all fallback, a migrate-only boot was missing those objects.

017 was produced by 'alembic revision --autogenerate' against Base.metadata,
reviewed, and verified: on a fresh DB, 'alembic upgrade head' (001..017) now
reproduces the create_all schema EXACTLY — a re-run of autogenerate detects zero
changes — and the 017 upgrade/downgrade round-trips cleanly. The migration chain
is now complete: migrate-only and create_all converge.

Also updates the init_db tests to assert the new behaviour (raise on an existing
DB's migration failure; create_all + stamp head on a fresh DB) instead of the
removed silent fallback.

* feat(panel): Product picker in the New Task form (drives per-cell routing)

The Products screen and Approve & Start button shipped, but the task-creation
form had no way to attach a Product — so a human couldn't set product_id from
the UI, which is exactly what drives per-cell project routing of delegated
subtasks. Adds an optional Product dropdown (Advanced -> Git config) populated
from useProducts(); 'None' falls back to the single project.

* fix(db): seed data is preserved on a fresh DB (run migrations, not bare create_all)

The previous fresh-DB path (create_all + stamp head) built the tables but never
ran the migration chain, so migration-embedded SEED DATA was skipped — most
visibly the AI providers seeded in 004. After a DB reset that left
provider_configs empty, so PUT /api/providers/ollama-key 404'd (the handler
raises NotFoundError when the Ollama provider row is missing).

Since migration 017 made the chain reproduce the full ORM schema, init_db now
runs 'alembic upgrade head' from base on a fresh DB — building every
table/column/index AND running the seeds. Verified: a fresh upgrade head seeds
both provider rows. Existing DBs still get migrations + create_all gap-fill.
Updates the init_db fresh-DB test accordingly.

* feat(task): project_id optional when a product_id is set (board fan-out tasks)

A board task that fans out across cells via a Product has no single repo of its
own — backend/frontend/ux_ui are each wrong, because the root coordinates and
delegates. Forcing one arbitrary Project was broken design (flagged at design
time). project_id is now nullable; a task must have project_id OR product_id:
  - TaskCreate model validator + a TaskService.create() invariant (covers every
    create path).
  - ORM/DTO/schema: project_id nullable; task_to_response uses to_python_uuid.
  - Gateway: a parent with only a product can delegate (guard now needs BOTH
    project and product to be None to reject); _resolve_subtask_project resolves
    each subtask from the product map and raises a clear error if a cell has no
    mapping and no parent project.
  - Migration 018 (tasks.project_id nullable), round-trip verified; fresh
    upgrade head still seeds providers.
  - Panel: Project no longer required once a Product is selected.
  - Removed the dead, never-called a2a create_task_from_message (it could only
    ever create a repo-less task) + its two coverage-only tests.

make quality green; panel tsc/lint/build green.

* Upgrade to Minimax M3

* fix(db): seed providers on existing DBs + correct enum casing

Migration 004 created the modelprovider/assignmentscope enums and seeded
provider rows in UPPERCASE, but the ORM (_str_enum) reads/writes the
lowercase StrEnum .value — so a fresh migrate-from-base DB built an enum
the ORM cannot read. Lowercase the enum labels and seed values in 004.

Add idempotent migration 019 to (re)seed the Anthropic + Ollama Cloud
providers with ON CONFLICT (name) DO NOTHING, so an existing DB whose
provider_configs table was created by create_all (and never ran 004's
seed) gets the rows on the next `alembic upgrade head` — fixing the
/api/providers/ollama-key 404 without a volume wipe.

* fix(tasks): let board/fan-out coordination tasks flow without a repo

A coordination task (project_id NULL, product_id set) targets no repo of
its own — it fans out to cell subtasks that each resolve a real project
from the product's cell->project map. Several paths still assumed every
task does git work and blocked it:

- orchestrator: add _is_coordination_task() and exempt these tasks from
  the project/branch/git-token gates in _readiness_check_task,
  _readiness_gate, _check_stuck_conditions, _validate_task_for_spawn.
- services/task.py: _ensure_branch_for_task returns "" (no branch) for a
  coordination task instead of raising; activate requires project OR
  product. This unblocks Main PM's i_will_plan claim, which otherwise
  raised before it could delegate the fan-out.
- gateway: _pending_assignment_guard exempts advisory roles
  (product_owner/head_marketing/auditor) from the "assigned but never
  claimed" idle gate — they review without claiming, so they could not
  satisfy a claim-or-unclaim remediation.

Adds focused unit tests for each.

* fix(tasks): coordination tasks reach in_progress + team reflects Main PM

The board->cells fan-out deadlocked: a coordination/fan-out task (product set,
no project of its own) could be created and claimed, but start()'s
claimed->in_progress transition hit validate_git_requirements, which still
demanded a branch_name and raised GitRequirementError. So Main PM's i_will_plan
never completed — it looped and never delegated. c961282 exempted
_ensure_branch_for_task (branch creation) but missed this parallel git gate in
the enforcement layer.

- task_lifecycle.py: add GitContext.is_coordination; skip the
  claimed->in_progress branch_name gate when it is set.
- task.py: populate is_coordination=(project_id is None and product_id is not
  None) in _validate_and_set_status; a branchless code task is still gated.
- approve_and_start: set team=Team.MAIN_PM on hand-off so the task isn't left
  labelled team=board after it leaves the board (now assigned to main-pm).

Adds a lifecycle-gate unit test and an end-to-end integration test that
claims, plans, and starts a project-less coordination task.

* fix(hooks): remove dead traceability hook + stale deleted-verb references

The v1-removal cleanup (2cfbf39) deleted the /traceability/remind SDK endpoint
but left the PostToolUse hook that curls it, so every gateway tool call 404'd
and agents silently lost their traceability reminders. Remove the dangling hook
(registration + TRACEABILITY_TRIGGER_TOOLS + Dockerfile COPY + the script); v2
carries per-verb guidance on the Envelope. Also correct two stale pre-gateway
tool names in hook text: the budget loop-detector nudged agents toward the
deleted roboco_task_escalate() (now unclaim()/i_am_idle(), which every looping
role has), and an sdk-startup comment referenced roboco_task_scan/get.

Extends the deleted-tool-name guard to scan docker/scripts/*.sh and to assert
every $SDK_URL/<path> a hook curls is a route still served by the SDK — the
check that would have caught this class (it lives in shell, invisible to mypy
and the Python import graph).

* fix(db): backfill ORM enum values the migration chain never added

Several StrEnum values were added to the ORM over time without a matching
`ALTER TYPE ... ADD VALUE` migration; 017 was autogenerate-derived and
autogenerate does not detect added enum labels, so the drift survived. On a DB
whose enum type predates the value, binding it raises at runtime — e.g.
`invalid input value for enum notificationtype: "a2a_request"` on
GET /api/notifications (list_system_notifications), and the same class for
blockerresolvertype/handoffstatus/team.

Migration 020 adds every drifted value idempotently (ADD VALUE IF NOT EXISTS —
no-op when 009 already reconciled it). Runs on the next `alembic upgrade head`.

Detected by comparing each ORM enum's values to the labels the migration chain
produces; adds tests/unit/test_enum_migration_parity.py which renders the chain
offline and fails on any future drift — the check that would have caught both
this and the provider-enum bug.

* fix(orchestrator): stop branch auto-block, board reassign, unblock livelock, agentless claims

Cluster C1 — four coupled orchestrator/task-invariant defects:

#18: a branch is created only at claim, so a pending, never-claimed code task
legitimately has no branch_name. The stuck-detection sweep (pending-only) and
readiness gate flagged that as "Task missing branch_name" and auto-blocked the
task every tick, so it never dispatched. Centralize the gate in
_branch_is_expected (status in claimed/in_progress/verifying, never a
coordination task) and apply it in both _check_stuck_conditions and
_readiness_check_task.

#14: the main_pm -> product_owner escalation rung handed an in_progress
descendant code task to the Product Owner (a board role) and marked it BLOCKED;
the board has no verb to own code work, so the dev's finished work deadlocked.
TaskService.apply_escalation (the single write primitive — covers both the
gateway escalate verb and the HTTP escalate route) now diverts a descendant code
task targeting a board/advisory role: it releases the task to PENDING for a
role-matched cell claim instead of stranding it.

#17: a blocked task reassigned to Main PM kept respawning the ex-assignee cell
PM to unblock it, but the assignee-only pre-unblock note returned not_authorized
— a livelock. _dispatch_blocker_work now dispatches the task's CURRENT PM/board
assignee (the unblock authority), falling back to the cell PM only when no
PM/board holds it. Also: a branchless coordination parent yields no valid merge
target — resolve_parent_branch now falls back to the child's own project default
branch (e.g. master) via TaskService.project_default_branch_for_task, and
_check_parent_branch_ready no longer blocks a child on a coordination parent's
non-existent branch.

#19: a task left claimed/in_progress with an assignee but no running container
was invisibly stuck (only PENDING tasks get fresh dispatch; the heartbeat reaper
can't see a freshly-seeded claim). New _dispatch_claimed_without_agent net:
after a short grace window it respawns the assignee, or releases the claim to
pending (lifecycle-safe via unclaim_for_reaper) when the assignee is unknown.
New config ROBOCO_CLAIMED_NO_AGENT_GRACE_SECONDS (default 120).

* fix(gateway): tolerant note verb + lock evidence do-tool invariant

#15: the note verb no longer hard-rejects thin decision/reflect payloads.
List-typed fields (options, consequences, next_steps) coerce a lone scalar
into a one-element list at both the NoteRequest schema (mode=before
validator) and the service layer; missing narrative fields default to a
visible placeholder instead of returning incomplete_input. The note is
always recorded, preserving audit value, and a well-intentioned note can no
longer trip the do-server 3-strikes circuit breaker. Widen the agent-facing
do_server.note hints to accept list-or-scalar and refresh the docstrings.

#8: add regression coverage locking the invariant that every role's do_tools
carries evidence (role_config + developer spawn manifest). The current source
already registers mcp__roboco-do__evidence for developers end-to-end; the
report stemmed from a stale deployed build, and the tests prevent silent
regression.

* fix(gateway): allow UX devs to receive design tasks; surface delegation rules to cell PM

The UX/UI cell's developers (ux-dev-1/ux-dev-2, Role.DEVELOPER on
Team.UX_UI) ARE its designers, but _validate_assignee_task_type rejected
task_type='design' for every DEVELOPER, blocking the UX cell's normal
design delegation. Allow 'design' for UX-team devs only; backend/frontend
devs stay rejected (design routing belongs to the UX cell). The
orchestrator already dispatches a developer for a design task
(_dev_dispatch_role_matches returns True), so this creates no orphan like
the documentation case.

Replace the static Cell-PM 'pass planning' remediate with a per-assignee
hint so a dev/design mis-type gets a developer-class next-step instead of
an off-topic planning hint.

Surface the three delegation guardrails in the cell-PM prompt so PMs stop
probing them by trial and error: valid task_type per assignee (incl.
design for UX devs), documentation auto-creation (non-delegatable), and
the sequential single-active code-spine. Fix the delegate-row task_type
list (documentation is NOT delegatable) and update the lifecycle spec
description; regenerate the lifecycle artifacts.

* fix(orchestrator): improve agent briefings for handoff consumption, product/project model, and workspace/secret hygiene

Main PM (roles/main_pm.md):
- Require reading the upstream Product Owner / Head of Marketing handoff
  (their decision/reflect journal entries + task description) BEFORE doing
  any own research or calling i_will_plan, so the Main PM builds on the
  Board's analysis instead of duplicating it. Added a dedicated section,
  hardened workflow step 1, and added an anti-pattern.
- Add a 'Products vs Projects' section: a Product fans out to one Project
  per cell; those Projects may be the SAME repo (monorepo subtrees) or
  DIFFERENT repos (multi-repo). The Main PM coordinates across them and
  must not assume one repo or call a monorepo subtree 'a separate repo'.
  Names the Prompter monorepo case (github.com/rennf93/roboco).

Developer (roles/developer.md):
- State the exact workspace path convention
  /data/workspaces/<project-slug>/<team>/<agent-slug>/, that the cwd is
  already set there, to stay inside the own cell workspace, and to not
  probe/guess the path (ls /, find /).
- Sanctioned secret handling: env/printenv is bash-guard denied and
  reveals nothing; needed secrets arrive via the task description, else
  i_am_blocked so the PM supplies them. Added matching anti-patterns.

Tests: add tests/unit/agents/test_briefing_cluster_c4.py asserting the
composed system prompt (the text mounted into agent containers) carries
each of the above.

* fix(orchestrator): board review involves PO+HoM and notifies CEO

Cluster C5 (#2, #4): a board/coordination task was reviewed by the Product
Owner alone, and the CEO got no formal signal when the review finished —
only buried channel chatter — so the Approve & Start handoff was invisible.

#4 — Board review is now a two-reviewer gate. _handle_board_assigned_task
dispatches BOTH the Product Owner and the Head of Marketing (one-shot each),
regardless of which one holds assigned_to, and the unassigned board-routing
path delegates here instead of claiming + spawning the PO alone. Board tasks
stay pending/unassigned for the CEO's Approve & Start. The board prompt now
makes the PO+HoM pair-review model explicit (HoM owns the UX/positioning
dimension).

#2 — Once BOTH reviewers have finished (dispatched and no longer active),
the orchestrator emits exactly one formal CEO notification via
NotificationService.send_board_review_complete_notification (APPROVAL type,
ack-required, carrying related_task_id) so the handoff is an actionable
signal. One-shot per task; a notification failure clears the guard so a
later tick can retry.

To let the non-assignee board member record its review note on a task held
by the other board member, content-action ownership now exempts a board role
posting to a board/coordination task (project_id is None, product_id set).
The exemption is narrow: it does not widen ownership for any other role or
any project-backed task.

Unit tests cover both reviewers dispatched, one-shot dispatch, the CEO
notification fired exactly once when both are done (and not before), the
retry-on-failure path, the notification builder, and the board co-review
ownership exemption (allowed for board+coordination, blocked otherwise).

* fix(workspace): install dev deps post-clone + raise git commit timeout for large changesets

Cluster C6 (#10, #13, #12-investigate).

#10: per-agent workspace clones never had the project's dev dependencies
installed, so the make-quality gate (ruff/mypy/pytest for Python, the TS
toolchain for the panel) was missing and devs re-downloaded tooling per
task. WorkspaceService now runs the project's install after cloning
(`uv sync` for Python, `pnpm install`/`npm ci`/`npm install` for Node/TS,
detected by manifest/lockfile). Idempotent via a lockfile-digest marker
under .git/ so a re-entry with unchanged lockfiles is a no-op; also runs on
the healthy short-circuit so pre-existing clones get backfilled. Gated by
workspace_install_dev_deps (default on) with workspace_dep_install_timeout_seconds.

#13: the gateway commit verb timed out on the large panel changeset because
every git op used the hardcoded 30s _GIT_TIMEOUT and each call also re-walks
the tree to chown. _run_git now takes a per-call timeout override sourced
from settings (git_command_timeout_seconds default); the staging + commit
ops in commit() and create_commit() use the longer git_commit_timeout_seconds
(default 180s). httpx REST timeouts unchanged in value.

#12 (investigate only — no push, no history change): the clone base ref is
NOT hardcoded; it already comes from project.default_branch threaded through
git.get_workspace -> ensure_workspace -> _clone_repo (git clone --branch).
The stale-base problem is a deploy/process issue (GitHub master is behind the
deployed migration chain), resolvable only by pushing the chain to master.
The default_branch column is the existing configurable lever.

* fix(panel): gate Approve & Start to board coordination tasks; stop 404 storm on closed sessions

CEO gate #1 button only renders for a PENDING board coordination/fan-out
task (no project_id, has product_id) — the board-reviewed handoff that
approve_and_start accepts — instead of every PENDING board-team task.
approve_and_start requires PENDING (it re-targets to Main PM without a
status change), so the gate stays on PENDING rather than the unrelated
end-of-work awaiting_ceo_approval state.

Session/message reads now treat a 404 as terminal and never retry it: a
reaped session is gone for good, and retrying every dead session-id is
what produced the growing 404 storm on GET /api/messages. The transcript
loads once (staleTime Infinity, no focus/reconnect refetch) so closed
sessions stay viewable without re-polling.

* fix(orchestrator): role-correct respawn prompt, throttle agentless dispatch, broaden #14 guard

#19 wrong-role prompt on respawn: _get_prompt_for_agent fell through to the
developer prompt for every non-dev/doc/qa role, so a respawned PM or board
agent was told to write code and call verbs it does not own. Route by the
agent's actual role through the existing per-role prompt builders
(developer/qa/documenter/cell_pm/main_pm/product_owner/head_marketing/auditor).
Both callers benefit; _spawn_pending_dev only ever passes developer/documenter/
unknown, so its behavior is unchanged.

#19 spawn-burst: _dispatch_claimed_without_agent looped over every agentless
claimed/in_progress task and could spawn many containers in one tick. Break
after the first respawn so a restart can't trigger a burst, matching every
sibling dispatcher. The release-to-pending path spawns nothing and keeps
draining stale unknown claims.

#14 guard scope: _is_descendant_code_task only matched CODE, so a descendant
DOCUMENTATION or DESIGN task escalated to a board/advisory role was still
stranded on a role with no verb to own it. Rename to
_is_descendant_executable_task and broaden to CODE/DOCUMENTATION/DESIGN — the
cell-executed types a board role cannot own. PLANNING/RESEARCH/ADMINISTRATIVE
route to a PM, not a cell agent, and are left unchanged; root tasks are still
reviewed up the chain.

* fix(docker): add node+pnpm to orchestrator so it pre-installs frontend cell deps

* Added .github workflows

* refactor(services): extract helpers to keep install_dev_deps + developer task-type check under the xenon complexity gate

* chore(github): add launch kit — CI, GHCR release, labels, templates, funding, dependabot npm, community docs

* chore(github): bump_version — drop unused noqa, fix datetime UTC import

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-03 06:35:03 +02:00
Renn F f83f930323 chore: set CLA governing law to Italy 2026-05-31 00:56:52 +02:00
Renn F 183151baf1 chore: license under AGPL-3.0 and add Contributor License Agreement
- Add full AGPL-3.0 LICENSE (canonical GNU text)
- Switch README and pyproject.toml from MIT to AGPL-3.0
- Add CLA.md (individual + entity) granting relicensing rights
- Add CONTRIBUTING.md explaining workflow and why the CLA exists
- Add CLA Assistant GitHub workflow to enforce signing on PRs
- Document licensing stance in CLAUDE.md
2026-05-31 00:53:45 +02:00
Renn F 08b56d4680 fix(gateway): i_documented auto-records journal:reflect from submission
The reflect gate's first, by-design tracing_gap ('journal:reflect missing')
counts toward the per-verb circuit breaker (i_documented limit 3 / 60s). A
documenter that fumbled note(scope='reflect') even twice got locked out and
went idle, stranding the task in awaiting_documentation with the PR orphaned.

i_documented now synthesizes the required reflect entry from the notes + files
it already carries, but only when the submission clears the notes/files gate
thresholds and the agent did not journal one themselves (theirs is richer and
left untouched). One call, no loop.
2026-05-28 00:24:42 +02:00
Renn F c1d0eefd20 fix(orchestrator): dispatch board agents for assigned board-team tasks
No dispatcher ever spawned board roles (product-owner / head-marketing) —
_handle_pm_assigned_task gates on _PM_AGENTS and there was no board path —
so a task assigned to the Product Owner sat pending forever (surfaced by the
first board-led run). Board roles advise: triage / note / say / escalate_to_ceo
/ i_am_idle, with NO verb to claim, plan, delegate, or complete. So a respawn
cannot advance the task and would just loop.

Add _handle_board_assigned_task: spawn the assigned board agent exactly ONCE
(tracked in _board_dispatched) with a review prompt that steers it to its real
verbs (record requirements via note, discuss via say, then i_am_idle). The
board review is recorded; the CEO then reassigns the task to Main PM for
delegation (the handoff stays CEO-mediated, by design — board roles cannot
delegate). _dispatch_pm_work routes board-assigned tasks here.
2026-05-25 02:48:13 +02:00
Renn F bc5e016d6d fix(panel): align task actions to server contract + collect required audit notes
The panel's human action buttons had drifted from the server request
schemas: wrong field names (qa_notes/reason vs notes), missing bodies
(cancel/complete/submit-pm-review), and a bare-string docs-complete body —
so cancel/pass-qa/fail-qa/escalate-to-ceo 4xx'd and decisions recorded no
audit note. (Agents were unaffected — they go through the gateway.)

- tasks.ts: pass-qa/fail-qa -> {notes}; escalate-to-ceo -> {notes:reason};
  cancel -> {reason}; complete -> {justification}; docs-complete -> {notes};
  submit-pm-review -> {notes}.
- New reusable RequiredNotesDialog (generalizes CeoApproveDialog). Every
  decision action now collects a substantive note before POSTing: cancel
  (>=10), pass-qa/fail-qa/docs-complete/submit-pm-review/complete (>=20),
  matching the server gates. Wired in the task detail page, the actions
  dropdown, and the kanban board.

Verified: pnpm tsc --noEmit and eslint both clean.
2026-05-24 07:10:34 +02:00
Renn F 5120b5ce81 fix(api): require substantive audit notes on human task-decision endpoints
Audit/tracing rule: every human decision must record its rationale. These
panel-facing routes accepted empty/absent notes, leaving no trail:
- docs-complete: now requires notes (>=20) — what was documented
- submit-pm-review: now requires notes (>=20) — what is ready for review
- complete: now requires justification (>=20) — why the task is done

Mirrors the existing pass-qa / ceo-approve notes gates (checked after the
404/403 so not-found and forbidden still take precedence). submit-qa is
left as-is: it already gates on commits + PR + progress_updates +
self_verified, and the panel collects no extra note there to drop.
Tests updated to send notes; added complete-without-justification reject.
2026-05-24 07:05:33 +02:00
Renn F c093996efc fix(ceo-approve): require substantive notes; panel collects them
ceo-approve was bound to QANotes (notes required), so the panel's
one-click approve (posts {}) 422'd. The wrong fix is to waive notes —
that empties the audit record for a production merge. Instead require
substantive notes (>=20 chars, mirroring pass-qa) and make the panel
COLLECT them: a new CeoApproveDialog (mirrors the reject dialog) gates
the 'Approve & Merge' action, and the dashboard approval-queue enforces
the same before POSTing. The CEO sign-off note is now always captured.
2026-05-24 06:59:37 +02:00
Renn F d49d1cdb37 feat(gateway): developers author the full rich plan, at parity with PMs
A dev's i_will_work_on stored a flat {text} plan and routed steps to
progress, so the dev leaf's Plan tab rendered empty (no approach,
sub_tasks, technical_considerations, risks) — zero audit/tracing on the
task that does the actual work.

i_will_work_on now captures the same rich plan a PM authors via
i_will_plan: plan(>=150) doubles as approach, steps become sub_tasks,
plus technical_considerations + risks (open_questions optional). A new
_dev_plan_gate enforces them on FRESH claims only (re-entry/recovery
short-circuit before it). set_plan gains a no-downgrade guard so a
flaked-then-recovered dev can't clobber its rich plan back to flat (the
actual mechanism behind the empty leaf).
2026-05-24 06:59:25 +02:00
Renn F a8056892b6 fix(gateway): main_pm completes an in_progress root; walk it to CEO
A root resumed from paused (its subtasks all terminal) sits in
in_progress, but escalate_to_ceo requires source=awaiting_pm_review and
nothing moves the root there — submit_up is cell-PM-only. main_pm_complete
rejected the in_progress root ("expected awaiting_pm_review"), so the
chain stalled one step short of CEO.

main_pm_complete now:
- accepts in_progress (own root, subtasks terminal) in addition to
  awaiting_pm_review;
- after opening the root->master PR (which sets pr_created), walks the
  root in_progress->awaiting_pm_review via the TaskService transition
  (role-validated, no gateway team-match) so escalate_to_ceo's source
  gate passes;
- then escalates -> awaiting_ceo_approval.

The root->master PR is non-empty because the cell->root PR was already
merged into the root branch (the prior cell-completion fix). Updated
test_main_pm_complete_wrong_status (in_progress is now valid; uses paused)
and added a regression test for the in_progress->CEO path.
2026-05-23 06:15:48 +02:00
Renn F e3def6b3a2 fix(gateway): cell PM completes its own cell task; drop main-PM handoff
submit_up bubbled the cell task to Main PM (_handoff_to_main_pm), but
main_pm_complete rejects any task with a parent_task_id ("only operates
on root tasks"), so the cell->root PR had no one to merge it and the
cell task wedged at awaiting_pm_review. _maybe_advance_parent_to_pm_review
already intends the CELL PM to complete it.

Cell PM now owns cell completion:
- submit_up no longer hands off to Main PM; the cell task stays assigned
  to the cell PM, which is respawned to complete() it. Removed the
  now-unused _handoff_to_main_pm.
- cell_pm_complete resolves the merge target from the parent task's real
  branch_name (shared merge_chain.resolve_parent_branch, also used by the
  PR side-effects) so the cell->root PR merges into feature/main_pm/...,
  not the team-mis-derived feature/<cellteam>/... (same root cause as the
  prior PR-base fix).
- submit_up description + next_hint updated; lifecycle artifacts regen.

Main PM still only completes the ROOT (root->master + escalate-to-CEO).
First run to reach cell-PM bubble-up exposed this.
2026-05-23 05:16:56 +02:00
Renn F 32b6b31dd6 fix(gateway): PR base/target is the parent task's branch, not derived
submit_up opened the cell->root PR with a base computed by
merge_chain.parent_branch_for, which drops the last --segment but
REUSES the child's team prefix. Across a team boundary (cell
feature/backend/ROOT--CELL -> root feature/main_pm/ROOT) that yields a
ref that does not exist on the remote, so GitHub rejects the PR with
422 base: invalid and the cell parent wedges.

Resolve the base/target from the parent task's authoritative
branch_name (what branch creation already cuts each child from), via a
shared VerbRunner._parent_branch_for helper used by _do_create_pr and
_do_pr_merge. Falls back to parent_branch_for only when there is no
parent (root->master -> master) or it has no branch yet. The leaf->cell
path is unchanged (same team). Latent since the merge chain landed;
first run to reach cell-PM bubble-up exposed it after #180.
2026-05-23 04:14:12 +02:00
Renn F c78395f9fc fix(gateway): submit_up opens cell PR before the pm-review transition
submit_up composed submit_pm_review (atomic) then create_pr (side
effect), but VerbRunner.run_intent runs all composes before any side
effect. submit_pm_review rejects without a PR (returns None), then
create_pr deref'd the None task -> 'NoneType has no attribute
branch_name', wedging cell-PM bubble-up in a respawn loop.

Add IntentSpec.pre_side_effects, run before composes. submit_up now
opens the cell->root PR first (persisting pr_number), so submit_pm_review
re-fetches and passes its pr_created gate. Mirrors the dev open_pr ->
i_am_done split. Latent since the lifecycle spec (207aaec); first run to
reach cell-PM bubble-up exposed it.
2026-05-23 02:32:41 +02:00
Renn F 3742483e1c fix(agent): pin uv to baked /app/.venv so MCP/SDK servers start instantly (#179)
Every agent MCP server is launched as `uv run python -m roboco.mcp.<server>`
(via the orchestrator-generated mcp-config.json) and the SDK server via
`uv run python -m roboco.agent_sdk.server` (sdk-startup-hook.sh) — both with
cwd = the agent's WORKSPACE, not /app. `uv run` then resolves a cwd-relative
`.venv` (≠ the image's baked /app/.venv), ignores VIRTUAL_ENV with a warning,
and RE-SYNCS the full dependency set (torch/lancedb/pyarrow/scipy, ~350MB)
into a fresh venv on every spawn.

A warm host uv wheel cache masks this (fast re-resolve from cached wheels —
earlier runs this session opened PR #26/#28/#29 fine). On a COLD cache (first
spawn after an image rebuild — exactly when deploying new fixes) the download
takes minutes, the MCP servers never register, and the agent burns its whole
budget with "No such tool available: mcp__roboco-*" before reaping. Observed
this session: be-dev-1 never claimed; /tmp/sdk-server.log showed the live
torch/lancedb download + the `VIRTUAL_ENV ... will be ignored` warning.

Fix: set UV_PROJECT_ENVIRONMENT=/app/.venv in (1) every MCP server's env in
the generated mcp-config.json (one place — shared mcp_env dict) and (2) the
SDK startup hook. uv then reuses the pre-baked image venv instantly,
regardless of cwd or cache state. Not a regression from this session's code
(none of #172b/#175/#176/#177/#178 touched the launch/venv path — verified);
a pre-existing launch-cwd fragility that rebuilding to deploy exposed.

Test: _generate_mcp_config asserts every server env pins
UV_PROJECT_ENVIRONMENT=/app/.venv. make quality green.
2026-05-23 01:23:18 +02:00
Renn F 879b8cb991 fix(deps): bump starlette 1.0.0 → 1.0.1 (PYSEC-2026-161)
pip-audit (make quality) flagged PYSEC-2026-161 in starlette 1.0.0,
fixed in 1.0.1. Transitive via fastapi; lock-only bump
(`uv lock --upgrade-package starlette`) — sole version change in the
lockfile, no other packages touched.
2026-05-23 01:23:17 +02:00
Renn F 9e9dd55b5d fix(task): drop cell_pm→main_pm auto-escalation in complete (#178)
`service.complete()` ran a two-tier approval chain
(`_apply_complete_approval_chain`): when a Cell PM completed an
`awaiting_pm_review` task, `_handle_cell_pm_escalation` silently
reassigned `task.assigned_to = main_pm.id` (+ `claimed_by`) and kept
the task in `awaiting_pm_review` for a second-tier review by Main PM.

That model is incompatible with the gateway's `main_pm_complete`,
which explicitly rejects any non-root task
(`if t.parent_task_id is not None: return invalid_state("main_pm
complete only operates on root tasks")` —
choreographer/_impl.py:3860). Result: the leaf got handed to main-pm
with no verb that could advance it → permanent wedge.

Observed end-to-end this session (smoke run 02:25–02:35):
- 02:25:56 leaf → awaiting_pm_review (correctly assigned to be-pm via
  notify_pm_of_docs_complete).
- 02:26:42 be-pm cell_pm_complete REJECTED tracing_gap journal:reflect
  (proves leaf IS assigned to be-pm).
- ~02:27 (silent — success-path is INFO, filtered): be-pm wrote the
  reflect note + retried → choreographer cell_pm_complete → git.pr_merge
  → service.cell_pm_complete → service.complete(agent=be-pm) →
  _apply_complete_approval_chain → _handle_cell_pm_escalation →
  task.assigned_to = main_pm.id (no `task.reassigned` audit because the
  event goes to `_emit_task_event(EventType.TASK_ESCALATED_TO_MAIN_PM)`,
  not the gateway audit log).
- 02:27:30 _dispatch_pm_review_work (orchestrator) saw leaf with
  assigned_to=main-pm → spawned main-pm against the leaf (target_id
  in audit_log confirms it: `target_id=f3bdd585 agent_slug=main-pm`).
- 02:28:21+ be-pm cell_pm_complete → not_authorized "not assigned to
  you". main-pm main_pm_complete → invalid_state "only operates on
  root tasks". Closure dispatcher cycled both PMs to budget-reap.

Fix: remove the cell_pm branch from `_apply_complete_approval_chain`.
Cell PM completing a non-root awaiting_pm_review task now transitions
it to COMPLETED (the gateway model). Cell→main escalation, when
intended, uses the dedicated `submit_up` verb on the cell-level parent,
not `complete`. The main_pm → CEO branch for root parents stays.
Removed the now-dead `_handle_cell_pm_escalation` helper and the now-
unused `agent_id` parameter on `_apply_complete_approval_chain`.

Tests inverted: `test_complete_cell_pm_escalates_to_main_pm` →
`test_complete_cell_pm_does_not_escalate_to_main_pm` (asserts
status=COMPLETED, assigned_to != main_pm.id). The no-Main-PM-fallback
test trivially still passes (the path is now the only path).
Lifecycle test comment updated. make quality green.
2026-05-20 05:17:54 +02:00
Renn F 0bafbedb30 fix(orchestrator): auto-recover blocked parent at PM closure respawn (#177)
#170 made the closure dispatcher auto-resume a `paused` parent before
respawning its PM, but only `paused`. A parent that is `blocked` at
closure (every descendant already terminal) is an errant/stale block —
a child's i_am_blocked propagated, or a PM blocked it and never
unblocked — the real dependency is already done. #170 left it as-is, so
the respawned PM landed on a blocked parent it cannot submit_up /
complete and had to manually `unblock` it first (needs journal:decision)
— which models do not reliably do, wedging the whole closure chain
forever (observed end-to-end this run: leaf stuck awaiting_pm_review,
cell parent blocked, root paused, PMs cycling indefinitely).

Add `_auto_recover_blocked_parent` (mirrors `_auto_resume_paused_parent`)
and recover `blocked` symmetrically to `paused` in
`_maybe_spawn_pm_closure`. `blocked -> in_progress` is lifecycle-valid —
it is exactly what `unblock(restore=True)` performs. Scoped to the
closure-spawn point (descendants terminal) so a live dependency block is
never auto-cleared. Best-effort, like the paused path. 4 new tests
mirror the #170 suite (recovered-before-spawn, mutual exclusivity with
paused, patch shape, error-swallowing). make quality green.
2026-05-18 05:11:16 +02:00
Renn F caa4fc1969 fix(gateway): unclaim releases a pending-assigned task — escape trap (#176)
An agent assigned a `pending` task it never claimed was structurally
trapped: from pending-assigned, unclaim returned None ("cannot unclaim
from status pending"), i_am_idle rejected ("assigned but never claimed"),
i_am_blocked rejected ("block requires in_progress"). Any persistent
claim-time rejection (a gate the agent cannot satisfy, a transient
validation error) therefore looped the agent until budget-reap AND left
the task orphaned (pending, assigned, no progress). Observed in smoke-16
and smoke-17.

unclaim_for_agent now releases a pending task assigned to the caller:
no status change (already pending → no lifecycle transition, no
WorkSession to abandon since it was never claimed), just clear
assigned_to/active_claimant_id so the dispatcher can reassign. The
choreographer spec gate already permits unclaim from pending (composes=()
— role-only), so the service branch is the whole fix. Updated the now-
stale unclaim remediate string; rewrote the test that encoded the buggy
trap and added a paused-status negative case.
2026-05-18 01:30:06 +02:00
Renn F 1d02b09fe0 fix(bash-guard): deny interpreter/library HTTP to internal hosts (#175)
The internal-API rule only fired when the FIRST shell token was an HTTP
CLI (curl/wget/http/https/httpie). smoke-17 showed an agent reach the
orchestrator with hand-forged X-Agent-ID/X-Agent-Role headers via:

  python3 << 'EOF'
  import httpx
  httpx.post("http://roboco-orchestrator:8000/api/v2/flow/developer/i_will_work_on",
             headers={"X-Agent-ID": "<self>", "X-Agent-Role": "developer"})
  EOF

The binary is python3 (slips the CLI check) and it imports httpx, not
roboco.* (slips the #164 import check). Only minimax's wrong endpoint
path prevented a real gateway bypass under a forged identity.

Add a language-agnostic rule: deny when the command pairs an HTTP-client
token (httpx/requests/urllib/aiohttp/http.client/net::http/fetch(/
node-fetch/axios/...) with a forbidden internal host, consistent with
the curl/wget sibling (inspects full $low incl. heredoc body). External
HTTP (pypi/docs/github) has no internal host so it still passes. The
stale "interpreter one-liners — out of scope" KNOWN GAP comment is
corrected; the variable-expansion gap remains documented.

11 new tests incl. the exact smoke-17 heredoc, requests/urllib/aiohttp/
node-fetch/Net::HTTP variants, and allow-cases (external host, client
import w/o host, pytest runner). make quality green.
2026-05-18 00:08:04 +02:00
Renn F 251d1c36a2 fix(mcp): flow_server i_will_work_on forwards steps — completes #172
#172 (4c397e1) added IWillWorkOnRequest.steps, the flow_dev route
threading, the _dev_steps_gate, and the developer prompt — but never
updated the roboco-flow MCP tool. flow_server.i_will_work_on exposed
only (task_id, plan) and posted only those, so the agent's tool could
not transmit steps. Every fresh dev claim hit the gate's
incomplete_input missing=['steps'] with no way to satisfy it →
permanent wedge for every code task (observed in smoke-16: be-dev-1
looped ~20 times, then deadlocked — could not claim, block, or idle).

Add the steps parameter and forward it, mirroring the existing
i_will_plan/sub_tasks pattern (which is why PM i_will_plan was never
affected). Update the two body-shape tests and add a steps-passthrough
regression test.
2026-05-17 22:42:53 +02:00
Renn F 3d34fc2677 feat(progress): plan-driven progress — % derived from the plan checklist (#173)
Progress was only the synthetic milestone entry (auto-emitted at
open_pr/i_am_done); agents never deliberately reported and the % was
an ungated free-form guess.

Now the plan's sub_tasks ARE the progress skeleton:
- progress() gains optional `plan_step` (a sub_task id or its 1-based
  order). With it, that step is marked completed and the percentage is
  DERIVED as completed/total (equal weight) via new
  TaskService.record_plan_progress — the agent cannot set/game it.
- A narrative entry WITHOUT plan_step is allowed for important
  mid-step documentation and carries the current derived % (the bar
  never regresses). No hard anti-spam gate (would loop minimax) —
  prompt guidance steers "meaningful moments, not every tool call".
- `percentage` is now an optional fallback, used only for tasks with
  no sub_task checklist (back-compat). v2 ProgressRequest, the do.py
  route, and the do_server MCP tool updated accordingly.
- An unmatched plan_step returns invalid_state listing the valid step
  refs (resolve by id / order / 1-based index).
- developer + documenter prompts updated to the plan_step workflow.
- Helpers extracted (_plan_subtasks/_derive_plan_pct/_valid_step_refs/
  _mark_subtask_complete) to keep record_plan_progress within the
  cyclomatic gate.

Commit 3 of 3 for the plan/progress quality work (#171/#172/#173).
2026-05-16 11:09:41 +02:00
Renn F 4c397e1768 feat(gateway): developer i_will_work_on takes a substantive step checklist (#172)
The dev plan was a free string with only a presence gate, so the
executing dev had no checklist for plan-driven progress (#173).

- IWillWorkOnRequest gains `steps` (same SubTask shape as a PM's
  sub_tasks); flow_dev route threads it through.
- i_will_work_on layers steps onto the narrative plan via the same
  panel-shaped path PMs use, so task.plan.sub_tasks is populated
  (panel render + #173 progress).
- New _dev_steps_gate (mirrors _pm_sub_tasks_gate, runs after the spec
  gate): a developer FRESH claim must supply a non-empty steps list
  with every description >= _PM_SUBTASK_DESC_MIN_LEN. Re-entry/recovery
  short-circuit before the gate (extracted _dev_reentry +
  _fresh_dev_claim keep i_will_work_on within the
  return-count + cyclomatic gates).
- developer role prompt: steps template + "thin steps rejected" + the
  progress(plan_step=...) handoff.
- Updated every dev-fresh-claim test fixture across the suite to pass
  substantive steps; added dedicated _dev_steps_gate coverage.

Commit 2 of 3 for the plan/progress quality work (#171/#172/#173).
2026-05-16 10:48:23 +02:00
Renn F ed828a719b feat(gateway): substantive-plan gate — approach >=150 + real sub_task descriptions (#171)
Plans were vague because the gate accepted the bare minimum: PM
approach >=20 chars and title-only sub_tasks. minimax wrote exactly
the minimum.

- IWillPlanRequest.approach min_length 20 -> 150 (kept in sync with
  _PM_APPROACH_MIN_LEN; the gate enforces it at the choreographer
  layer too so direct/MCP callers can't bypass the HTTP boundary).
- New _thin_subtask_hint: every PM sub_task must have a title and a
  description >= _PM_SUBTASK_DESC_MIN_LEN (60) saying what the step
  does — each sub_task is both a delegate target AND a
  progress-checklist item, so a title alone is not a plan.
- cell_pm/main_pm role prompts: explicit "the gate REJECTS thin plans"
  framing + concrete sub_task example + the new minimums.
- Updated all affected test fixtures across the suite to use
  substantive approaches/descriptions; added thin-sub_task rejection
  coverage.

Commit 1 of 3 for the plan/progress quality work (#171/#172/#173).
2026-05-16 10:14:45 +02:00
Renn F e94159dce8 fix(orchestrator): auto-resume paused parent before PM closure respawn (#170)
A PM auto-pauses its owned parent on i_am_idle (by design — so the
closure dispatcher knows to respawn it when subtasks finish).
Pre-gateway the parent was resumed at respawn so the PM landed
actionable; the gateway refactor dropped that, so the respawned PM had
to issue resume() itself. minimax reliably failed to (called resume on
the leaf / unblock on the paused root), wedging smoke-15 — the leaf
stayed awaiting_pm_review and the chain never completed.

Restore the pre-gateway behaviour: _maybe_spawn_pm_closure now calls
new _auto_resume_paused_parent (paused -> in_progress via the same
PATCH path _auto_block_task uses) immediately before spawning the PM,
but only when the parent is actually `paused` (awaiting_pm_review /
in_progress parents untouched). Best-effort: a resume failure is
logged and swallowed so it never blocks the spawn (the PM can still
resume manually). The parent stays assigned to the PM, so it lands on
its own in_progress task able to submit_up / complete / escalate
directly — no reliance on the weak model issuing resume().

Combined with 4090397 (exact-complete remediate), this closes the
smoke-15 PM-completion wedge end to end.
2026-05-16 07:06:17 +02:00
Renn F 4090397cea fix(gateway): rejected PM is told the exact complete() call (#170, partial)
Smoke-15 wedge: leaf 1533ce56 sat at awaiting_pm_review owned by
be-pm, but the PMs looped firing complete/unblock at the wrong
(parent) task_ids — every rejection was generic ("not assigned to
you" / "not ready for completion"), so minimax never discovered it
just needed `complete(1533ce56)`.

New _own_review_hint: on a cell_pm/main_pm complete-guard rejection
(not-owner, wrong-state, or main-pm-on-non-root), if the PM owns a
DIFFERENT task that is awaiting_pm_review, append a remediate suffix
naming it and the exact `complete(task_id='<id>', notes='...')` call.
Best-effort (never raises into the rejection path), pure guidance —
no control-flow or state-machine change.

Scope: this is the bounded, low-risk slice of #170 (fix b). The
parent-state corruption + missing recovery transition (root->paused /
cell->blocked from earlier mis-targeted verbs, fix a/c) is a lifecycle
state-machine change deferred for explicit design alignment — tracked
in #170.
2026-05-16 06:37:49 +02:00
Renn F 0737cc0143 fix(git): authenticate diff-path fetches so QA's diff base is current (#168)
Smoke-15: QA's claim_review diff was `origin/master...origin/<branch>`
but origin/master in QA's clone was the STALE clone-time tip
(47c674d) — the three-dot diff spanned the whole session delta
(41 files / +2740) instead of the 1-line README change.

Two compounding causes:
- _default_branch_ref early-returns the ref NAME when origin/HEAD is
  set (it was) WITHOUT fetching it, so the base stayed stale.
- Every fetch in the diff path ran unauthenticated; the repo is
  private, so `git fetch` failed ("could not read Username for
  github.com") and could never refresh the ref. (The documenter path
  was correct only because #162 uses the PAT-injected
  workspace.fetch_branch_for_inspection.)

Fix: new best-effort _token_for_branch resolves the project PAT
(None on any failure → degrades to unauth, never raises in the
evidence path). diff()/list_changed_files() thread it into
_resolve_head_ref + _resolve_diff_base, whose fetches now pass
token= (uses the existing _run_git http.extraheader Basic-auth
injection). _resolve_diff_base additionally re-fetches the resolved
default branch so the base is current even when origin/HEAD shortcut
skipped the fetch.
2026-05-16 06:29:18 +02:00
Renn F 12672b94ed fix(docs): i_documented persists DocRef dicts, not bare strings (#169)
Smoke-15: be-doc i_documented(files=["README.md"]) → choreographer
doc.py stamped `existing.documents = files` (a list[str]) onto
Task.documents. Task.documents is list[DocRef] persisted as dicts —
list_docs does DocRef(**d), _get_existing_doc_ref does d.get("path"),
the RAG indexer does d.get("path"). A bare string 500'd GET /docs
("TypeError: DocRef() argument after ** must be a mapping, not str")
during be-pm's PR review and would AttributeError the indexer.

Fix at source: new _doc_refs_for builds proper DocRef dicts
(path, title=filename, doc_type, created_by/at, updated_by/at) at the
i_documented stamp. Defensive read: new _coerce_doc_ref tolerates
dict / DocRef / bare-string / rejects unknown — applied at
list_docs and _get_existing_doc_ref so legacy/corrupted rows can't
500. _add_doc_to_task (docs-route write path, not the gateway flow
that broke) left as-is per scope.
2026-05-16 06:20:35 +02:00
Renn F 8158eb37ef unpinning claude clode version 2026-05-16 05:28:20 +02:00
Renn F 38dba74837 fix(agents): stop instructing agents to ToolSearch built-in tools (#167)
The system-prompt directive layer and the briefing block both opened
with "FIRST ACTION REQUIRED: run ToolSearch to activate deferred
Edit/Write". That premise is false: per Claude Code 2.1.114, ToolSearch
gates only deferred MCP tools, never built-ins — and it is not even a
callable tool in the agent runtime. Built-ins are loaded at spawn via
the `--tools` flag and gated solely by the per-role permission rules
(the actual Edit/Write breakage was the global Write(*)/Edit(*) deny +
single-slash path, fixed in c0ba335). So weak models dutifully chased a
nonexistent ToolSearch, concluded Edit/Write were unavailable, and
rewrote whole files via destructive shell redirection.

Both touch points now affirm the role's built-in tools are loaded and
ready, tell the agent NOT to call ToolSearch, and (for authoring roles)
explicitly steer away from whole-file shell redirection — directly
countering the clobber behaviour. Role prompt files (developer,
cell_pm, main_pm, board) updated to match. Dead
_read_tool_load_from_role_prompt (no callers) removed. Directive tests
rewritten to lock the corrected behaviour.
2026-05-16 03:53:52 +02:00
Renn F c0ba335470 fix(runtime): agents can finally Edit/Write — drop global deny + fix abs path syntax (#167)
Smoke-10..14: every agent (developers included) got "Edit exists but is
not enabled in this context" and fell back to destructive bash
redirection (a 207-line README rewritten to a 3-line stub, which QA
correctly failed). Two coordinated defects in _generate_agent_settings /
_get_role_permissions:

1. base_deny carried a GLOBAL Write(*)/Edit(*). Claude Code evaluates
   permission rules deny -> ask -> allow, first match wins — a deny
   ALWAYS beats a more-specific allow and the glob syntax has no
   negation. So the global deny unconditionally shadowed every per-role
   workspace-scoped Write/Edit allow. Removed it; the security denies
   that legitimately rely on deny-always-wins (Bash(git:*), credential
   Read denies, curl github, env) stay. Roles that must not author
   (qa, cell_pm, main_pm, auditor) keep their OWN Write(*)/Edit(*) deny.

2. The workspace allow used a single leading slash (Write(/data/...)).
   Claude Code resolves a single / against the settings.json project
   root, not the container filesystem root, so the allow silently never
   matched even without defect #1. Emit the // absolute-filesystem form.

defaultMode stays bypassPermissions (switching to dontAsk would require
re-deriving the full allow-list and risks wedging agents elsewhere —
out of scope). Verified against Claude Code 2.1.114 permission docs.
2026-05-16 03:45:02 +02:00
Renn F 954acff911 fix(git): resolve diff HEAD ref per-workspace so QA/doc/PM see real diffs (#161 facet)
Smoke-14: QA's claim_review evidence had pr_diff_summary="" and
files_changed=[] on a PR with a real README change. Root cause: diff()
and list_changed_files() diffed against the bare local <branch_name>.
That ref only exists in the clone where the dev ran `git checkout -b`
at claim. QA / documenter / PM inspect from their OWN clones, where a
bare <branch_name> resolves refs/heads then refs/remotes/<name> but
NEVER refs/remotes/origin/<name> — so `git diff base...<branch>` had an
unresolvable HEAD and silently returned an empty diff (run with
check=False).

#161 previously fixed the BASE side (cell-PM parent never pushed → fall
back to default branch). This is the symmetric HEAD-side facet.
open_pr pushes the leaf branch, so origin/<branch> is the
workspace-independent source of truth. New _resolve_head_ref fetches the
branch and prefers the local branch (dev's own clone, unchanged
behaviour), falling back to origin/<branch> (QA/doc/PM clones), then the
bare name so the command stays well-formed. diff() and
list_changed_files() route through it; explicit base (incremental dev
path, base=HEAD~1) is preserved.
2026-05-16 03:27:23 +02:00
Renn F 1605d187f1 fix(security): bash-guard git-ops check inspects commands, not file content (#165)
The git network/auth deny rule matched its regex against the whole
command string, so heredoc bodies and echo/printf arguments that merely
documented git verbs (a README, a notes file) were treated as git
invocations and denied. This wedged smoke-13's dev: after wiping the
README via an Edit/Write fallback it could not restore it because every
`cat > README.md << EOF ... git commit ... EOF` was blocked.

The git-ops check now runs against a skeleton of the command with
heredoc bodies and echo/printf literal args stripped (both are data the
shell writes, never executed). Quoted args to a shell interpreter
(`bash -c "... && git fetch"`) ARE executed, are not echo/printf/heredoc
bodies, and so survive untouched — the hook's core purpose is preserved.
A sentinel prefix distinguishes a legitimately-empty skeleton from a
python failure (fail closed on failure). All other rules, including the
#164 import-bypass rule, still inspect the full command.
2026-05-16 02:20:08 +02:00
Renn F 81f5655d48 fix(security): block gateway-internals import + agent-id forgery (#164)
Smoke-12: be-dev-1 (minimax-m2.7) bypassed the entire MCP boundary by
running `uv run python3 -c "import os;
os.environ['ROBOCO_AGENT_ID']='...'; from roboco.mcp.flow_server
import open_pr; open_pr(...)"` from the Bash tool. This voided the
per-role tool manifest (role-scoping is meaningless if the agent can
import any server module in-process), forged agent identity via an
env-var rewrite, and ran choreographer code outside the gateway's
tracing + auth.

bash-guard-hook.sh now adds two deny rules:
  1. Any python/uv/poetry/pipenv/pdm/hatch invocation that imports or
     `-m`-runs roboco.* internals (mcp/services/runtime/foundation/
     api/enforcement). The whole command string — heredoc body
     included — is matched, so quoting/heredoc forms are covered.
  2. Any assignment or export of ROBOCO_AGENT_ID (identity forgery).

Reading roboco source for context (cat/grep) is still allowed — the
block is on *executing* internals, not viewing them. Normal python
one-liners without roboco imports still pass.

19 bash-guard tests pass (10 prior + 9 new). Note: takes effect on
agent-image rebuild (hook ships in the agent container).
2026-05-16 00:59:26 +02:00
Renn F c18ad34530 fix(gateway): reject PM-created documentation subtasks (#163)
Smoke-12: be-pm delegated TWO subtasks under one cell parent — a
code subtask (be-dev-1, spawned) and a documentation subtask
(be-dev-2). The orchestrator dev-dispatch refuses to spawn a developer
for task_type=documentation, so the doc subtask became a permanent
orphan that loops dev-dispatch forever and would deadlock submit_up
(all subtasks must be terminal). The spine-cap is per-type so
code + documentation both passed sibling-dedup — the PM never saw the
anti-pattern warning.

_delegate_static_guards now rejects task_type='documentation' with a
remediate explaining the lifecycle auto-creates the documentation
phase (awaiting_documentation → documenter spawned) after the code
subtask passes QA, and that the PM should delegate ONLY the code
subtask.
2026-05-16 00:47:59 +02:00
Renn F aa2e6bc5ed fix: panel logo (#160), diff base fallback (#161), doc branch checkout (#162)
#160 — panel /roboco-logo.png "received null":
    next/image optimizer fails for static public assets in Next.js
    standalone mode. Added `unoptimized` to the sidebar logo Image so
    it serves the static file directly (validated on panel rebuild).

#161 — QA/doc evidence pr_diff_summary empty:
    A leaf dev branch's parent_branch_for is the cell-PM branch, which
    is never pushed (only devs push their leaf branch). diff against a
    non-existent origin/<parent> returned empty. Added
    GitService._resolve_diff_base + _default_branch_ref + _ref_exists:
    diff/list_changed_files fall back to the repo default branch
    (origin/HEAD → master/main) when origin/<parent> is absent.

#162 — claim_doc_task BRANCH_MISMATCH loop:
    The documenter's clone is separate from the dev's; the task branch
    already existed (dev created it) so no checkout ran in the doc
    workspace — roboco_docs_write / commit failed BRANCH_MISMATCH and
    the doc looped. Fixes:
    (a) new GitService.checkout_branch_in_agent_workspace; claim_doc_task
        checks out the task branch into the doc clone (best-effort —
        a checkout hiccup never fails the claim).
    (b) BRANCH_MISMATCH remediate now lists all four role claim verbs
        (i_will_work_on / i_will_plan / claim_doc_task / claim_review).
    (d) give_me_work next-hint is role+status aware via _claim_verb_hint
        (doc→claim_doc_task, qa→claim_review, pm→i_will_plan, else dev).
    Facet (c) (i_am_blocked "Not Found" for doc) was only reachable via
    the stuck-without-checkout path; primary fix removes it.

Smoke-11 reached dev→QA→doc (deepest ever) and validated the prior
6 fixes (panel flood gone, #158/#159/#157 confirmed). These three
clear the doc-phase blockers found in that run.
2026-05-16 00:13:14 +02:00
Renn F 5da909d9d7 fix(gateway): cross-team planning fanout + complete tracing-gap hints
Task #157 — spine-cap allows planning fanout across cells:
    main-pm's pattern is to delegate planning to be-pm / fe-pm / ux-pm
    in parallel — each on a different team. The previous spine-cap
    rejected all planning siblings under one parent as
    over-decomposition. New helper _is_cross_team_planning skips the
    cap for planning when both teams are non-empty and distinct. Code
    / documentation stay capped regardless (single repo on one branch
    shouldn't have two simultaneous code subtasks).

Task #159 — tracing-gap remediate hints every requirement:
    journal:during_work>=1, journal:struggle, commits>=1, pr_open,
    and self_verified had no entries in _hint_for_missing_key, so when
    they were missing the agent saw the token in `missing[]` but the
    `remediate` text had no instruction for how to satisfy them.
    Smoke-10's be-dev-1 burned multiple turns retrying i_am_done not
    knowing scope='reflect' doesn't count toward during_work. Now
    every token has a hint, the during_work hint warns that reflect
    doesn't satisfy it, and multi-hint remediate uses a numbered list
    so the model treats each requirement as a distinct step instead
    of a semicolon-blob.

Also coerce convert_plan._coerce_risk formatting (ruff-format follow-up
to 9cd73d0).
2026-05-15 08:21:08 +02:00
Renn F 9cd73d0902 fix(panel): coerce risk.severity to default str on read + write
Bug:
    Smoke-10's main-pm submitted a rich plan with risks omitting
    severity. _normalize_risk persisted severity=None into the DB.
    Every panel poll of /tasks/{id} then 500'd because
    TaskPlanResponse.risks declares list[dict[str, str]] and Pydantic
    rejects None for a str field. Result: panel single-task page broken
    end-to-end every ~1-3s as panel reloads.

Fix:
    Write side (_normalize_risk): default missing/None severity to
    "medium" so new writes never persist None.
    Read side (convert_plan._coerce_risk): defensively coerce any
    existing DB row with severity=None to "medium" so old bad data
    doesn't continue bricking the read path.
2026-05-15 07:25:52 +02:00