Notification bell: the websocket stream was rendered with a plain filter, so
a stream replay after a reconnect surfaced — and counted — the same
notification twice, inflating the unread badge. De-duplicate by
notification_id (newest copy wins, arrival order preserved) and key the list
on the id instead of array index.
PM kanban: the board had no columns for the recovery states (paused,
needs-revision, awaiting-CEO-approval, cancelled), so a human had no way to
drive a wedged task into them from the UI. Add those columns; dragging a card
issues an admin status override, giving the CEO direct recovery from the board.
The dispatcher's respawn guard already detects an agent repeatedly spawned
on the same task without advancing it, logs a warning, and skips further
spawns. But nothing surfaced that to a human, so a wedged agent could sit
silently with its task stalled.
When the guard trips, send a one-shot high-priority notification to the CEO
(tracked per (agent, task) so it fires once, not on every subsequent skipped
spawn, and resets if the agent later makes progress). Delivery is best-effort
so a notification failure never wedges dispatch.
Adds send_stuck_agent_notification to NotificationService and a coverage test
asserting the alert fires exactly once across multiple over-threshold spawns.
A board/advisory role (product owner, head of marketing, auditor) has no
verb to build, document, or complete a cell task. The escalate path already
diverts such a hand-off to the pool; extend the same backstop to the two
remaining write-sites:
- reassign / reassign_active_claim: a refused board/advisory target is
diverted to the pool for a role-matched claim instead of being planted as
a non-workable owner. Normal handoff targets (qa, documenter, cell PM) are
unaffected.
- dependency revival: when a blocked task's last dependency clears, resume
in place only under a workable owner; re-home a board/advisory-or-absent
owner on a cell task to the pool so it does not immediately re-deadlock.
All three sites share one audited pool-divert primitive so no direct status
set skips the transition audit.
Also reduce cyclomatic complexity below the project threshold for
unclaim_for_agent, the dependency-revival path, and the docs index source
expansion by extracting helpers (behavior-preserving), and add coverage for
the doc source-expansion paths.
* Fix: human surface lifecycle hardening (#81)
* fix: harden agent-idle, redis loop, git errors, escalation audit
- i_am_idle no longer 500s when auto-pausing a task whose commits are
stored as dicts: tolerate dict-or-object commit refs and run the
synthetic-checkpoint computation inside the swallowing try block.
- The stream event loop no longer logs an idle redis read-timeout as an
ERROR every cycle; the blocking-read timeout is treated as a normal idle.
- Git command failures surface git's own (secret-scrubbed) stderr in the
error message instead of a bare 'Command failed', so push/fetch
rejections are diagnosable; the injected PAT is redacted.
- The escalate-to-pool redirect emits the task.pending audit event,
closing a status mutation that previously skipped the audit log.
* fix: let privileged operators set task status via an audited override
The task update route silently dropped a 'status' field in the request body,
so a CEO/admin could not transition a task wedged in a state with no valid
in-band move (e.g. a blocked task whose work merged out-of-band) — the panel
returned 200 while nothing changed. Add 'status' to the update schema and
apply it through a new audited 'admin_set_status' that bypasses the strict
transition validator but always records the audit event. The override
requires elevated permissions; ordinary field updates are unchanged.
* fix: stop human chat sessions from expiring between messages
Messaging sessions fell back to a hardcoded 300s idle timeout, shorter than a
normal pause in a human conversation: the sweeper closed the session and the
next message opened a new one, so a person could not hold a continuous chat.
Make the idle timeout configurable (session_idle_timeout_seconds, default
3600) and resolve an unset timeout to it at every session-creation path
instead of the 300s column fallback.
* fix: resolve doubled doc paths and stop the indexer warning flood
The doc-path resolver returned absolute paths verbatim, so a documenter path
that doubled the base segment (/app/docs/docs/...) never resolved on disk and
the docs never indexed into RAG. Reduce an absolute path under the docs base
to a relative one before normalizing, leaving truly-external absolute paths
for the indexer to skip. The indexer now skips non-markdown source files and
logs a missing/non-doc source at debug instead of warning on every pass.
* fix: reject project repo URLs that point at a protected repository
Add a configurable denylist (protected_git_urls) enforced in the project
create and update paths, so a project cannot be registered against a
repository that must not receive agent commits or merges (e.g. the roboco
source repo during a smoke run). Empty by default (no behavior change);
operators set it to sandbox smoke-test projects.
* fix: let an agent release a blocked task back to the pool
A developer (or QA/doc) trapped on a 'blocked' task had no legal forward
move — every verb rejected from that state — so the dispatcher kept
respawning it with nothing to do. Allow 'unclaim' to release a blocked task
the agent owns back to pending (assignment cleared, work session abandoned,
audited), so the cell PM can re-delegate it instead of the agent churning.
* fix: keep blocked-dev churn out and cell tasks out of board hands
- The dispatcher no longer respawns the owner of a blocked task: from blocked
the owner has no legal move, so respawning only churns; it is revived on
unblock or released via unclaim.
- Escalation no longer hands a cell (backend/frontend/ux_ui) coordination task
to a board/advisory role — such an escalation is diverted to the cell pool,
matching the existing executable-task guard. main_pm targets are unaffected.
* chore: add an opt-in full clean-slate to the reset script
FULL_RESET=1 wipes everything under the roboco data root except the
persistent service stores (ollama/postgres/redis) and clears the persisted
agent Claude session dirs (ROBOCO_CLAUDE_STATE_DIRS), which otherwise replay
across runs. Default off — the existing DB/Redis wipe + workspace git-reset
is unchanged.
* ++
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* fix: read the real commit key (hash) and audit the restore-unblock path
- The auto-pause checkpoint and _extract_first_commit_sha read commit dicts by
key 'sha', but persisted commits are keyed 'hash' (CommitRef.hash) — the prior
change stopped the crash but silently dropped every ref. Read 'hash' (sha
fallback) at both sites; the test now uses the production dict shape so the
regression can't hide.
- unblock_with_restore set status directly and skipped the audit log; emit the
status-transition audit there too, like the other direct-set paths.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
- The auto-pause checkpoint and _extract_first_commit_sha read commit dicts by
key 'sha', but persisted commits are keyed 'hash' (CommitRef.hash) — the prior
change stopped the crash but silently dropped every ref. Read 'hash' (sha
fallback) at both sites; the test now uses the production dict shape so the
regression can't hide.
- unblock_with_restore set status directly and skipped the audit log; emit the
status-transition audit there too, like the other direct-set paths.
* fix: harden agent-idle, redis loop, git errors, escalation audit
- i_am_idle no longer 500s when auto-pausing a task whose commits are
stored as dicts: tolerate dict-or-object commit refs and run the
synthetic-checkpoint computation inside the swallowing try block.
- The stream event loop no longer logs an idle redis read-timeout as an
ERROR every cycle; the blocking-read timeout is treated as a normal idle.
- Git command failures surface git's own (secret-scrubbed) stderr in the
error message instead of a bare 'Command failed', so push/fetch
rejections are diagnosable; the injected PAT is redacted.
- The escalate-to-pool redirect emits the task.pending audit event,
closing a status mutation that previously skipped the audit log.
* fix: let privileged operators set task status via an audited override
The task update route silently dropped a 'status' field in the request body,
so a CEO/admin could not transition a task wedged in a state with no valid
in-band move (e.g. a blocked task whose work merged out-of-band) — the panel
returned 200 while nothing changed. Add 'status' to the update schema and
apply it through a new audited 'admin_set_status' that bypasses the strict
transition validator but always records the audit event. The override
requires elevated permissions; ordinary field updates are unchanged.
* fix: stop human chat sessions from expiring between messages
Messaging sessions fell back to a hardcoded 300s idle timeout, shorter than a
normal pause in a human conversation: the sweeper closed the session and the
next message opened a new one, so a person could not hold a continuous chat.
Make the idle timeout configurable (session_idle_timeout_seconds, default
3600) and resolve an unset timeout to it at every session-creation path
instead of the 300s column fallback.
* fix: resolve doubled doc paths and stop the indexer warning flood
The doc-path resolver returned absolute paths verbatim, so a documenter path
that doubled the base segment (/app/docs/docs/...) never resolved on disk and
the docs never indexed into RAG. Reduce an absolute path under the docs base
to a relative one before normalizing, leaving truly-external absolute paths
for the indexer to skip. The indexer now skips non-markdown source files and
logs a missing/non-doc source at debug instead of warning on every pass.
* fix: reject project repo URLs that point at a protected repository
Add a configurable denylist (protected_git_urls) enforced in the project
create and update paths, so a project cannot be registered against a
repository that must not receive agent commits or merges (e.g. the roboco
source repo during a smoke run). Empty by default (no behavior change);
operators set it to sandbox smoke-test projects.
* fix: let an agent release a blocked task back to the pool
A developer (or QA/doc) trapped on a 'blocked' task had no legal forward
move — every verb rejected from that state — so the dispatcher kept
respawning it with nothing to do. Allow 'unclaim' to release a blocked task
the agent owns back to pending (assignment cleared, work session abandoned,
audited), so the cell PM can re-delegate it instead of the agent churning.
* fix: keep blocked-dev churn out and cell tasks out of board hands
- The dispatcher no longer respawns the owner of a blocked task: from blocked
the owner has no legal move, so respawning only churns; it is revived on
unblock or released via unclaim.
- Escalation no longer hands a cell (backend/frontend/ux_ui) coordination task
to a board/advisory role — such an escalation is diverted to the cell pool,
matching the existing executable-task guard. main_pm targets are unaffected.
* chore: add an opt-in full clean-slate to the reset script
FULL_RESET=1 wipes everything under the roboco data root except the
persistent service stores (ollama/postgres/redis) and clears the persisted
agent Claude session dirs (ROBOCO_CLAUDE_STATE_DIRS), which otherwise replay
across runs. Default off — the existing DB/Redis wipe + workspace git-reset
is unchanged.
* ++
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* [529f579a] feat(prompter): add PrompterService with chat and draft generation endpoints
* [529f579a] feat(prompter): add PrompterService, schemas, routes, and integration tests
* [529f579a] feat(prompter): implement session-based prompter chat endpoints with DB persistence
Add full session-based Prompter chat system with:
- Alembic migration 024 creating prompter_sessions, prompter_messages, and task_drafts tables with proper foreign keys, indexes, and enum columns
- Three new SQLAlchemy ORM table classes in roboco/db/tables.py
- Pydantic schemas: PrompterSessionCreateRequest, PrompterMessageRequest, PrompterSessionResponse, PrompterMessageResponse, TaskDraftResponse, TaskConfirmRequest
- Four new session-based FastAPI routes: POST /sessions, POST /sessions/{id}/messages, GET /sessions/{id}/draft, POST /sessions/{id}/confirm
- PrompterService with DB-backed session, message, and draft persistence; LLM-driven draft generation; ConfirmOverrides dataclass to stay under PLR0913
- Legacy stateless /chat and /draft endpoints retained for backward compatibility
- Unit tests for schemas (test_schemas_prompter.py), service pure functions and DB logic (test_prompter.py) with mocked LLM calls
- Integration tests for full happy path and legacy endpoints (test_prompter_routes.py)
- All ruff format, ruff check, mypy (changed files), and pytest checks passing
* [529f579a] fix(prompter): correct test assertion for confirmed_at field nesting
The test test_get_draft_generates_from_conversation incorrectly
accessed body['draft']['confirmed_at'] but confirmed_at is a field
on the outer TaskDraftResponse, not on the nested PrompterDraftTask.
Fixed to body['confirmed_at'].
---------
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
* Cleanup + Missing greenlet error
* fix(messaging): persist a group's active-session pointer so posts reuse it
create_session and create_session_with_access_check set group.active_session_id
from session.id BEFORE the flush that materializes it — the id is a flush-time
uuid4 default, so the pointer was written as NULL and every post opened a fresh
session, fragmenting one conversation across many. Flush first, then link, the
same ordering the seed path already uses.
Two tests fabricated "two distinct sessions" by calling create_session twice on
one group, which only differed because of this bug; switch them to two groups so
they keep testing their real intent. Add a regression guard that the pointer is
actually persisted and a second create reuses the live session.
* fix(orchestrator): gate spawns on dependencies and keep cell tasks in their cell
The cross-task dependency check ran only on the dev dispatch path, so cell-PM,
Main-PM and board agents were spawned onto dependency-blocked tasks and flailed
unblock / escalate / notify against an unfinished upstream — climbing ownership
of cell work up to the board, which cannot drive it, and deadlocking the task.
- Move the dependency gate into the shared spawn readiness check so it covers
every role, and auto-block the task so it leaves the pending pool until the
upstream reaches a terminal state (then the existing auto-unblock revives it).
- Cell-ownership invariant: a backend/frontend/ux_ui task may only be worked or
owned by its own cell. The readiness gate refuses a board or Main-PM spawn
onto a cell task; reassign refuses and clears such an owner; and on
dependency-clear a mis-owned cell task is re-homed to its cell's pending pool
instead of reviving under an owner that cannot progress it.
- A dependency block is never a CEO signal: notify(target=ceo) is refused while
the task is waiting on an unfinished upstream, with a remediate to idle and
wait — the block clears on its own.
* Uploading images + Fixing pyproject.toml
* ++
* revert(orchestrator): drop the cell-ownership block pending a tooling audit
The cell-ownership invariant added earlier — a board / Main-PM role may never be
spawned onto or reassigned to a cell task, plus re-homing a mis-owned cell task
on dependency-clear — was too absolute. It forbids a higher role from stepping
in when something genuinely deeper is going on, and contradicts the existing
rule that main_pm may hold a task at awaiting_pm_review. The dependency spawn
gate already prevents the cascade that handed the board cell tasks; the deadlock
it guarded against will be addressed with a return-path approach after auditing
what tools the cell PMs actually need. Keeps the dependency gate and the CEO
dependency-block notify guard.
* docs(prompts): a dependency wait is wait-and-idle, not escalate
The cell-PM and Main-PM prompts told agents to escalate_up / retry unblock on a
blocked task without distinguishing a dependency wait (which auto-clears the
moment the upstream completes) from a real wedge — the source of the
escalate/unblock flail and the CEO-notification spam. Split the blocked-state
guidance: a cross-cell dependency wait = note + i_am_idle (do not escalate,
unblock, or notify the CEO); escalate only a genuinely broken upstream. Fix two
stale references to i_am_blocked, a developer-only verb the PMs do not have,
to escalate_up.
Correct the CLAUDE.md verb-surface table, which understated every role: it
listed 4 cell_pm verbs while the flow manifest derives the full set (11,
including unclaim and i_am_idle) from lifecycle.spec.intents_for_role.
* feat(gateway): cell_pm reassign verb — intra-cell developer hand-off
A cell PM can now hand a claimed/in_progress task to another developer in its
own cell without unclaim (which drops the work back to the pool and loses the
assignee). The branch is keyed to the task, so the work-in-progress is
preserved; the new dev is respawned to continue. Intra-cell only: the task must
be in the caller's cell and new_assignee must be a developer of that same cell.
Wired through every layer: the reassign IntentSpec (composes=(), cell_pm-only),
the choreographer verb + intra-cell guard, a reaper-safe
TaskService.reassign_active_claim (reseeds the claim heartbeat so the new dev
is not immediately reaped), the ReassignRequest schema, the cell_pm flow route,
and the MCP flow-server tool. Tracing-waived like unclaim (mechanical hand-off).
Regenerated lifecycle/verb artifacts; prompt + CLAUDE.md updated.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
usage.md described a pre-gateway operating model — a roboco_task_scan
tool, /api/v1/<group> endpoints, an awaiting_docs status, and an 18-agent
roster. Update it to the real surface: give_me_work, /api/<group> routes,
awaiting_documentation, the estimated_complexity/task_type/nature task
fields, and 19 agents.
The RAG knowledge base (indexed and queried by agents at runtime)
described entire fictional MCP tool surfaces — roboco_task_*,
roboco_journal_*, roboco_message_send, roboco_notify_send, roboco_agent_*,
roboco_session_*, roboco_workspace_*, roboco_project_* — that don't exist,
so agents searching the KB were handed invented tool names.
Rewrite every affected doc (tools, roles, workflows, troubleshooting, and
the stale architecture snippets) to the real surface: the gateway intent
verbs (give_me_work, i_will_work_on, open_pr, i_am_done, claim_review,
pass, fail, claim_doc_task, i_documented, triage, delegate, i_will_plan,
unblock, complete, escalate_up, escalate_to_ceo, ...) and content tools
(commit, note(scope=...), say, dm, evidence, notify*, open_session,
channels). Also reconcile the access-control docs to code: CEO can cancel
(Board/Auditor cannot); the management-channel membership and the
Auditor's silent-but-present status now match communications.py.
config-reference.md (indexed into the KB agents trust at runtime)
documented a non-existent ROBOCO_SECRET_KEY/JWT and token-expiry setting
and omitted the real one. Replace the Security table with the actual env
vars — ROBOCO_ENCRYPTION_KEY (Fernet), ROBOCO_AGENT_AUTH_SECRET,
ROBOCO_AGENT_AUTH_REQUIRED, ROBOCO_PANEL_AGENT_TOKEN — and drop the
phantom Logging/Sessions sections (no backing config fields).
regenerate_verb_tables.py imported roboco.api.schemas.v2, which no longer
exists (schemas moved to v1), so it raised on import and the generated
verb/tool tables could never be refreshed — leaving _generated/verbs.md
and the per-role prompts stale (e.g. listing submit_for_qa, omitting the
notify_*/channels/progress/pr_update content tools). Repoint the imports
to v1, fix the renamed schema (OpenPrRequest), and regenerate.
A documentation audit against the code surfaced several stale claims:
- Agent count: the roster is 19 AI agents (the UX/UI cell has two devs,
ux-dev-1 + ux-dev-2), not 18 / a single UX dev. Fixed in README,
CLAUDE.md, base.md, and docs/ux_ui.
- API: domain routes are mounted under /api, not /api/v1 (the /api/v1
prefix is the agent gateway only); dropped the non-existent /api/v1/test
group; fixed the orchestrator-status path in deployment.md.
- Quick Start uvicorn target is roboco.api.app:app (the api package
deliberately does not export app).
- Verb table: the developer PR verb is open_pr (renamed from
submit_for_qa); the lifecycle's canonical module is
foundation/policy/lifecycle.py (enforcement/task_lifecycle.py is a shim).
- Backend team stack: vector store is PostgreSQL + pgvector (via piragi),
not Qdrant; mypy targets roboco/, not src/.
- .env.example: replaced the phantom Qdrant/OpenAI blocks with the real
Ollama/RAG settings.
A step-by-step guide to the workflow — give the Board a task, approve,
the cells build/review/document, the Main PM opens the final PR — each
step illustrated with a panel screenshot. The screenshots live under the
tracked docs/images/ (the source logos/ tree is gitignored, so embedding
from there would render broken on the public repo).
docs/internal/ held internal scratch and business-strategy material
(architecture dumps under old/, plus drafts) that should not live on the
public repo. Remove the whole tree from tracking — local copies are kept
— and gitignore docs/internal/ so it can't be re-added by accident. Also
gitignore the internal smoke-findings tracker.
(History scrub of the already-pushed copies is a separate follow-up.)
* fix(gateway): push the branch before QA handoff so reviewers see the latest commits
The commit content tool commits locally without pushing; only open_pr pushed
the branch. On the first submission that was fine, but a fix committed while
addressing needs_revision never reached origin (open_pr is skipped once the PR
exists), so QA — which reviews the remote PR branch — re-reviewed the stale
remote and re-failed the task on every cycle, a loop that never converged.
i_am_done now pushes the task branch (idempotent; a no-op when nothing is
unpushed) as part of the shared submit gate, covering both the normal and
resume-from-verifying paths. A push failure blocks the handoff with a clear
remediation rather than parking the task in awaiting_qa with commits that exist
only in the developer's local workspace.
* fix(orchestrator): don't reap a stale claim while the agent's container is alive
The stale-claim reaper released any claimed/in_progress task whose
last_heartbeat_at exceeded the TTL. The heartbeat only updates on certain
gateway calls, so a developer deep in a long edit/test cycle outran the TTL and
had its claim reaped mid-work — churning the task and risking a double spawn
against the still-running container.
The reaper now skips a task whose assignee still holds a live (ACTIVE) agent
instance, trusting container liveness — the ground truth — over the heartbeat
proxy. The check is defensive on missing fields so a heartbeat-only caller (and
the reaper's existing unit tests) behave exactly as before.
* fix(gateway): refuse to unblock a task while a dependency is unfinished
A PM unblock on a dependency-gated task moved it straight to in_progress,
overriding the dependency — letting a dependent proceed without its upstream's
work (e.g. a frontend task built before its UX design lands). A dependency
block is meant to clear on its own via _unblock_dependents the moment the
upstream reaches a terminal state.
unblock now refuses while any dependency is still non-terminal, returning a
clear remediation that the block resolves automatically. Manual unblock remains
available for genuine, non-dependency blockers.
* fix(gateway): release a dependency-blocked claim to pending instead of looping
A task that reached claimed/in_progress with an unfinished dependency was left
in that state when the claim guard rejected, so the orchestrator's respawn loop
kept reviving its assignee — which could make no progress — burning work for
nothing.
The claim guard now releases such a task back to pending. claimed -> blocked is
not a legal transition, so pending — held by the dispatch dependency filter — is
the lifecycle-correct resting state: the respawn loop ignores pending tasks, and
_unblock_dependents re-dispatches it once the upstream reaches a terminal state.
release_dependency_blocked_claim shares a _force_unclaim_to_pending core with
unclaim_for_reaper so both record a truthful work-session abandon reason.
* feat(security): warn at startup in header-trust mode + document the auth posture
When ROBOCO_AGENT_AUTH_REQUIRED is not enabled the API accepts the X-Agent-Id /
X-Agent-Role headers without a signed token, so any client that can reach it may
act as any role (including 'ceo'). The API now logs a clear warning at startup
in this mode, and the README gains a Security section documenting the auth
posture and how to harden it. Acceptable only on a trusted private network — do
not expose the API to untrusted networks.
* fix(workspace): scope the refresh fetch to current + default branch
ensure_workspace's healthy short-circuit ran an all-refs 'git fetch origin' to
keep every origin/<branch> ref current. On a monorepo with many accumulated
feature/* branches that exceeds the refresh timeout, the fetch silently fails,
and the workspace keeps a stale base — so an agent builds on an out-of-date
branch.
The refresh now fetches only the workspace's current branch and the repo's
default branch (resolved via origin/HEAD), with --no-tags --prune: it transfers
near-nothing and can't time out. Readers need their own branch and the default;
the integration branch is refreshed at branch-creation time.
* fix(git): refresh a dependency-blocked task's branch off the current integration tip
A cross-cell dependent (e.g. a frontend task waiting on the UX design) was
branched off a base captured before its upstream merged into the integration
branch, and the branch was never re-synced — so the agent built on a stale
snapshot with none of the upstream's work.
Two changes close the gap:
- release_dependency_blocked_claim now clears branch_name, so the re-claim
(after the dependency clears) re-runs branch creation.
- create_branch, when the branch is already on disk with no commits of its own,
resets it onto the freshly-pulled base — the dependent now builds on the
current integration tip. A branch carrying real commits is left untouched, so
no work is discarded; the cell->leaf cascade carries the upstream down to the
dev branch automatically.
* refactor(gateway): drop the sibling-sequence claim guard
Sibling sequence no longer gates a claim. Cross-cell ordering is
enforced by task dependencies — a cell task that depends on another is
held until its upstream reaches a terminal state, a stronger,
status-aware gate than the sequence-number check. That check was
dormant in practice anyway: every fan-out child carries sequence 0, on
which the guard short-circuited. `sequence` stays a sibling-ordering /
dispatch-priority field (list_pending ordering and the panel).
Removes sibling_sequence_guard and its _earlier_blocking_sibling
helper, the now-unused skip_sequence parameter threaded through the
claim verbs, and the sibling fetch that fed it.
* feat(gateway): sort a cross-cell dependent after its upstream
When the frontend cell task is wired to depend on its UX/UI sibling, set
its sequence to the upstream's sequence + 1 so it sorts after the design
it waits on — list_pending ordering and the panel now show UX ahead of
the implementation it gates, in either delegation order.
Adds TaskService.set_sequence (the sibling-ordering field is a service
write; it carries no claim-gating semantics — dependencies gate claims).
* feat(gateway): make the backend cell depend on UX too
UX/UI design defines the screens and API contracts both implementation
cells build against, so the backend cell — not just the frontend — waits
on the UX/UI cell task in a product fan-out and sorts after it. Wires in
either delegation order: a backend task delegated after UX gets the
dependency directly; a UX task delegated after a still-pending backend
sibling retro-wires it.
Mirrors the existing frontend wiring (_depend_backend_on_ux and
_depend_pending_backends_on_ux). Backend is held by the same dependency
gate, so it costs no extra dispatch churn.
* fix(websocket): forward notification acks instead of logging them incomplete
The bridge handler serves both notification.sent and notification.acked,
but acked events carry `agent_id` (the acking agent) rather than
`recipient_id`, so every acknowledgement tripped the missing-field guard
and logged "Incomplete notification event" instead of reaching the panel.
Accept either field as the recipient.
* feat(api): hint the full UUID when a truncated task id fails validation
Agents copy the 8-character task prefix the system shows them (the commit
prefix, task summaries) and send it as task_id, which fails UUID
validation with an opaque "invalid length" 422 and wastes a call. The
request-validation handler now detects a task_id UUID error and attaches
a `remediate` hint telling the agent to retry with the full 36-character
UUID from its task envelope.
* fix(audit): record the blocked transition when a task is escalated
Escalation sets a task to blocked by writing task.status directly, which
bypassed the validated transition helper and so never emitted a
task.blocked audit row — the lifecycle moved but the Auditor saw nothing.
Extract the audit emit from the central transition helper into
_emit_status_transition_audit and call it from the escalate path,
capturing the prior status and outgoing owner before reassignment so the
row is attributed correctly.
* fix(docs): stop doubling the docs path so design specs index into RAG
The documenter sometimes hands a doc path already rooted at docs/, and
joining it onto DOCS_BASE_PATH (/app/docs) produced /app/docs/docs/...,
so the file was never found and the spec never indexed — the frontend
cell could not retrieve the UX design over RAG. Normalize the path
before joining: trust an absolute path, otherwise strip a single
redundant leading docs/ segment.
* feat(security): let the control panel authenticate in secure mode
With ROBOCO_AGENT_AUTH_REQUIRED=true every request must carry a valid
HMAC token, which locked the human control panel out — it sends role
headers but no token. nginx, the only trusted hop between the browser
and the API, now injects the CEO token on /api and /ws, so the browser
never holds the signing secret. The injected value is just the existing
per-agent token issued for the CEO identity (issue_panel_token), so the
token-verification path is unchanged. An empty value (dev/header-trust
mode) renders to no header.
`make panel-token` prints the value; set it as ROBOCO_PANEL_AGENT_TOKEN
in .env before enabling secure mode. .env.example and the README
Security section document the flow.
* chore(compose): consolidate the two compose files into one
docker-compose.yml and docker-compose.yaml had diverged: .yml — the file
Docker actually uses — carried ROBOCO_PUBLIC_BASE_URL but was missing the
/app/manifests bind-mount, while .yaml had the manifests mount but not
the base URL. Merge the union into docker-compose.yml and delete the
duplicate so there is one source of truth and no "multiple config files"
warning.
This activates the manifests mount in the deployed file: without it the
orchestrator writes per-agent tool manifests to its ephemeral container
fs, they never reach the host for the daemon to bind-mount, and agents
fall back to all-verbs registration. Drop the stale .yaml reference from
the config.py docstring, the labeler, and the CI path filters.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
addNotification prepended every delivery and re-incremented unreadCount /
pendingAckCount, so a re-fetched or replayed notification stacked duplicates
and re-inflated the counts — already-acknowledged notifications re-surfaced to
the CEO and the pending badge kept climbing. Dedupe by id: update in place on
re-delivery; only add + count a genuinely new notification.
create_session and create_session_with_access_check closed the group's active
session and opened a new one on every call. A group is meant to have ONE live
session (groups.active_session_id) that all participants post into, so this
churned a single conversation across many sessions — the smoke run showed ~one
session per message, and the CEO could not hold a conversation in a channel.
Both now reuse the live session when one is active and only open a fresh one
when none is (closed via timeout / boundary / merge), matching the existing
get_or_create_active_session contract.
merge_pr_for_task merged using the client-provided project_slug, which a
coordination root (no project of its own) cannot supply. For a root, resolve
the repo from its product server-side so the CEO's approve-&-merge of the
root->master PR works without the panel knowing the repo. Non-root tasks keep
the client slug. Completes the root->master->CEO chain for the monorepo case
(multi-repo N-PR fan-out remains a follow-up).
main_pm_complete opens the root->master PR via create_pr(t.branch_name).
create_pr resolved the repo from task.project_id (null for a root) and would
raise. Route it through _project_for_task so the root's PR resolves the
product's repo. Additive — non-root tasks unchanged.
Branch->project resolution (_project_slug_for_branch, _workspace_for_branch)
read task.project_id, which is null for a coordination root — so root-level
git ops (the root->master PR, the CEO merge) could not resolve a workspace.
A new _project_for_task falls through to the product's first distinct repo
(monorepo => the single repo) when project_id is null. Purely additive: a
task with a project_id resolves exactly as before; only the previously-
unresolvable root case changes.
Every push/fetch to a private monorepo from a self-hosted runner takes
~1-2s, so a 1s threshold tagged routine ops as 'slow git op' on every
operation — pure noise. 5s only fires on genuinely slow ops.
The learning singleton was created on demand but initialize(optimal_service)
was never called, so record_learning() always raised "not initialized" and
every task completion logged "Failed to extract learnings". The lifespan now
wires it to OptimalService once RAG is up (skipped when RAG is disabled).
tracing_gap and incomplete_input rejections set missing and remediate but
left message null. The audit log records message (not remediate) and agents
keyed on message, so a rejected agent saw a null reason and retried the same
verb until it burned out instead of reading remediate and self-correcting.
Both builders (and from_decision) now derive a non-null message that folds
the missing tokens and the actionable remediate into one line, so the agent
and the audit trail always see what was missing and how to fix it.
pr_merge (the gateway path a cell PM uses to merge a leaf/cell PR up the
hierarchy) accepted any target, including a repo's default branch — the
hole that let cell completion land on master. It now refuses any target
equal to the project's default branch with a CEO_ONLY error: a root→master
PR is merged solely by the CEO via approve-&-merge (merge_pr_for_task,
already CEO-gated from awaiting_ceo_approval). Agents open the master PR
and escalate; they never merge it.
Belt-and-suspenders to the integration-branch routing: even if a target
ever resolved to master, this blocks the merge at the GitHub-API boundary.
The coordination/fan-out root carries a product (cell->repo map) but no
project of its own, and was forced branchless — so a cell's parent-branch
resolution fell back to the project default (master), and cell completion
merged each cell straight to master, bypassing the Main-PM integration
point and the CEO merge gate.
Per the locked branch model (master <- feature/main_pm/{root} <- cell <-
dev), the root is now the Main-PM integration point: on claim it cuts
feature/main_pm/{root} off master in EACH distinct repo the product spans
(monorepo => 1, multi-repo => N). Cells then branch off it via the existing
ancestor-branch resolution, so cell work never targets master.
- ProductService.distinct_project_ids: enumerate the repos a product spans
- TaskService._create_branch_in_project: project-parameterized branch
creation split out of _auto_create_branch
- TaskService._ensure_coordination_root_branches: cut the integration
branch in each repo; graceful empty when the product has no cell map yet
- _ensure_branch_for_task routes a product-backed root here, not to no-op
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Extract per-entry chown+chmod into _own_and_grant_rw and the pruned
workspace walk into _iter_ownable_entries. The single function carried a
no-op guard, an explicit-root chown, an os.walk with in-place pruning, two
path comprehensions, an inner entry loop, and a failure tally — cyclomatic
rank C. Behaviour is identical (root + every non-pruned entry chowned and
granted owner/group rw); the main function is now a guard, a sum() over the
entry iterator, and the warning, all rank A.
The lifecycle fix routes a never-claimed (no-branch) blocked task to pending on
unblock; these two tests asserted in_progress on a no-branch task. Give them a
branch so they exercise the claimed-task resume path they intend (no-branch ->
pending is covered by new unit tests).