Commit Graph
503 Commits
Author SHA1 Message Date
Renn F 78b22a4c1b ci(release): publish the missing roboco-agent-secretary image
The secretary agent image landed with the company layer (0.4.0) and is
built by docker-compose + referenced by the orchestrator, but the release
workflow's publish list was never updated, so it was absent from GHCR and
Docker Hub. Add it so a pull-based deploy has the full image set.
2026-06-16 18:14:24 +02:00
Renn F df5e579916 docs: add the full build-session video, count 22 agents, ground resource usage
Add the 2.5-hour 'Working with RoboCo' build session (a conversation to a
shipped feature) as a second hero thumbnail beside the 26-min intro.

Update the agent count from 20 to 22 across the README, CLAUDE.md, usage,
the base agent prompt, the how-to guide, and the org-structure RAG doc:
the standing org gains the PR Reviewer (board-level, read-only), and the
on-demand Intake and Secretary are now counted. The org-structure doc
gains the PR Reviewer in the hierarchy, count table, board team, and
communication matrix. The historical 0.1.0 changelog entry is left as-is.

Rewrite the resource-usage section: drop the unmeasured per-agent RAM
ceiling (RAM is low and agents run few-at-a-time) and lead with storage —
the image set's shared base layer — which is what docker prune reclaims.
2026-06-16 18:12:39 +02:00
Renn F a9fc870415 fix(orchestrator): harden external-PR supersede close-on-land
Scope close_pull_request repo resolution by project_id and thread the
umbrella's project into close-on-land, so a contributor PR is never
resolved (or closed) against a same-numbered PR in another project's
repo. Skip the comment + close PATCH when the PR is already closed, so a
retried sweep never re-posts the 'superseded' comment.

Require a non-cancelled descendant that actually landed a PR before
retiring the contributor PR, so an umbrella force-completed over a
cancelled code subtask leaves the contributor's still-valid PR open.

Run close-on-land from the always-on sweeper rather than the default-off
poll loop, so a supersede that lands after the feature is toggled off is
still reconciled. Serialize concurrent supersede triggers under a lock so
a double-click can't cut two branches / spawn two umbrellas. Anchor the
supersede marker checks to the marker line so appended CEO notes can't be
mistaken for the closed/dedup tokens. Make the fork-head branch cut
idempotent (forced refspec) so a commit-fail retry converges.

Also drop an importlib.reload(roboco.config) in a unit test that rebound
the settings singleton and leaked into the PM decision-window test.
2026-06-16 18:01:40 +02:00
Renn F 5511cf6e79 feat(supersede): close + link the contributor PR on land
When a supersede umbrella reaches COMPLETED (our own PR merged), close-on-land
retires the contributor's PR with a linking thank-you comment:

- TaskService.supersede_umbrellas_pending_close() finds landed umbrellas not yet
  marked closed=1; mark_supersede_pr_closed() records the close (idempotent).
- orchestrator._close_superseded_prs runs in the external-PR poll tick: parses
  the contributor PR# from the umbrella's quick_context and calls
  GitService.close_pull_request(delete_branch=False) — we never touch the
  contributor's fork branch. _parse_supersede_pr is unit-tested.

Completes the supersede flow: CEO authorizes -> fork branch -> Main PM -> cell
-> our PR -> CEO merge -> contributor PR closed + linked. ruff + mypy clean
(279); foundation + gateway suites green (5208).
2026-06-16 17:09:38 +02:00
Renn F 25e6174c04 fix(supersede): resolve adversarial-review findings (depth, dedup, gate)
A second adversarial review of the supersede flow found a feature-breaking HIGH
plus correctness gaps:

- HIGH: parenting the umbrella to the review task burned a MAX_TASK_DEPTH level
  (review->umbrella->cell-PM->dev = depth 4 > 3), so the dev code task could
  never be created and the work never reached a cell. Fix: the umbrella is now a
  ROOT task; the contributor PR# + review link ride quick_context (also
  simplifies close-on-land — no parent walk).
- MED: no dedup — a repeat CEO trigger created duplicate umbrellas / two racing
  PRs. Fix: find_supersede_umbrella() makes the trigger idempotent (returns the
  existing umbrella).
- LOW: supersede worked on un-reviewed/cancelled review tasks. Fix: require
  review.status == COMPLETED (review-first).
- LOW: a partial failure could orphan a pushed fork branch. Fix: create the
  umbrella before the push, and log the branch so any orphan is discoverable.

ruff + mypy clean (279).
2026-06-16 17:06:17 +02:00
Renn F 0fb28cfb3e feat(gateway): CEO-triggered supersede of a reviewed external PR
The org takes over a reviewed external PR and finishes it itself:

- POST /api/tasks/{id}/supersede-external-pr (CEO-only) -> orchestrator
  .supersede_external_pr: confirms the review task (this CEO action authorizes
  running the contributor's code), cuts a roboco-owned branch off the fork head
  (create_branch_from_pr_head — the only point untrusted code enters a roboco
  branch), and creates the supersede umbrella.
- TaskService.create_supersede_umbrella: a planning task on the same repo,
  parented to the review task (contributor PR# stays reachable for close-on-land),
  carrying the pre-cut fork branch, handed to Main PM to delegate to a cell.
  confirmed_by_human=True.

From there the work rides the normal lifecycle (Main PM -> cell -> our PR ->
QA/doc/PM -> CEO). ruff + mypy clean (279); app builds with the new route.
Follow-up: close-on-land of the contributor PR + an adversarial review pass.
2026-06-16 16:49:19 +02:00
Renn F 5bea82dbbc fix(gateway): resolve adversarial-review findings on the pr_reviewer flow
An adversarial review of the feature found two blocking defects (both would
surface the moment external_pr_enabled is turned on) plus hardening gaps:

- HIGH: the enforcement legacy role-gate overlay OVERWROTE spec-derived roles,
  so pr_reviewer was erased from the (in_progress->completed) edge it shares
  with the PM self-complete gate — the review task could never complete. Fix:
  UNION legacy + spec roles instead of overwriting (also preserves the legacy
  'add roles' intent on every shared edge).
- HIGH: claim_pr_review routed claim+start through the verb runner, which hit
  start()'s plan gate (planless review task -> None -> crash/respawn loop) and
  auto-created+pushed a stray branch (violating the read-only/branchless
  invariant). Fix: mirror QA's claim_review — a verb-body TaskService.pr_review_claim
  does pending->in_progress with no plan and no branch.
- MED: add the pr_reviewer Write(*)/Edit(*) deny at the permission layer (it
  ingests untrusted PR diffs — make read-only explicit, not implicit).
- MED: regenerate the verb-table artifacts (the schemas existed but the
  generator had not been re-run; the agent prompt showed 'unknown' signatures).

ruff + mypy clean (279 files); foundation + gateway suites green (5205 passed).
2026-06-16 11:48:24 +02:00
Renn F 831321f436 feat(git): create_branch_from_pr_head — branch off a fork PR's commits
The safe core of supersede: fetch a contributor PR's head via the GitHub
special ref refs/pull/{n}/head into a roboco-owned local branch and push it to
origin, so a dev cell can finish the work on a branch WE own and merge. We never
push to the contributor's fork. First point untrusted code enters a roboco
branch — callers invoke it only for a human-confirmed supersede.
2026-06-16 11:24:33 +02:00
Renn F f41f9548a8 feat(orchestrator): author allowlist for inbound external-PR review
At ingest, a non-empty external_pr_author_allowlist restricts which external
PRs are reviewed to those GitHub logins (case-insensitive). An empty allowlist
(default) reviews every external PR — safe because the review is read-only; the
confirmed_by_human gate still guards any later supersede that runs fork code.
Unit-tested (_pr_author_allowed).
2026-06-16 11:21:16 +02:00
Renn F a77ce453a2 feat(gateway): wire pr_reviewer verbs end-to-end + dispatch
Make the reviewer verbs reachable and dispatched:

- flow_server: claim_pr_review / post_pr_review MCP tools + registry entries.
- flow API: a flow_pr_reviewer router (/api/v1/flow/pr_reviewer/{verb}) with
  give_me_work / claim_pr_review / post_pr_review / i_am_idle, gated by a new
  require_pr_reviewer dependency; request schemas ClaimPrReviewRequest /
  PostPrReviewRequest; router registered in the app.
- orchestrator: _dispatch_pr_review_work routes PENDING source='external_pr'
  tasks to the single global pr-reviewer-1 (no pre-claim — the reviewer claims
  via claim_pr_review, which needs the task PENDING; guarded on is_agent_active)
  + _build_pr_review_prompt (read-only, trust-boundary framing) + the builder
  map entry. _dispatch_pm_work and _dispatch_dev_work now skip
  source='external_pr' so only the reviewer ever handles review tasks.

ruff + mypy clean (279 files); app builds with all four pr_reviewer flow routes;
foundation + flow-mapping suites green.
2026-06-16 11:17:39 +02:00
Renn F 347cf7b028 feat(gateway): pr_reviewer review verbs (claim_pr_review/post_pr_review)
Implement the choreographer + service layer for inbound external-PR review:

- PRReviewerMixin (claim_pr_review, post_pr_review) composed into the
  Choreographer MRO. claim_pr_review runs claim+start (pending->in_progress) and
  returns the PR's unified diff INLINE; post_pr_review runs pr_review_done
  (in_progress->completed) then posts ONE change-request to GitHub from the verb
  body (a2a.send pattern), gated on a journal:learning entry.
- VerbRunner: _do_pr_review_done atomic handler -> TaskService.complete_review.
- TaskService.complete_review: validated in_progress->completed for the review
  task, attributed to the reviewer.
- GitService.get_pr_diff: read-only unified diff via the GitHub API (the fork
  code is never checked out or run).
- Enforcement: review tasks (source='external_pr') are branch-gate exempt for
  claimed->in_progress (they do no git of their own, like coordination tasks).

ruff + mypy clean (278 files); composed choreographer imports with both verbs.
2026-06-16 11:06:56 +02:00
Renn F c69900ee9c feat(git): post_pr_review — post one change-request to a PR
GitService.post_pr_review posts a single review via POST /pulls/{n}/reviews
(REQUEST_CHANGES by default; APPROVE/COMMENT supported) — the first /reviews
call in the codebase. Resolves owner/repo/token from the project slug,
authenticates as the PAT owner (Bearer), and raises GitError on any token or
GitHub failure so the calling side-effect can surface it. This is the capability
the pr_reviewer's post_pr_review verb invokes after its DB commit. httpx fully
mocked in tests (request shape, auth, error paths).
2026-06-16 10:41:09 +02:00
Renn F 5902c0fe38 feat(roles): add the read-only pr_reviewer role end-to-end
A global, read-only PR reviewer agent (pr-reviewer-1) that reviews inbound
external/fork PRs and posts one change-request. Wired end-to-end:

- identity: Role.PR_REVIEWER + agent + ROLE_LEVEL (QA-peer) + REVIEWER_ROLES
- lifecycle: CLAIM_RULES + ROLE_TEAM_RULES + a dedicated claim_pr_review /
  post_pr_review verb pair (distinct from QA's) + the pr_review_done action and
  its in_progress->completed transition; give_me_work / i_am_idle gain the role
- role_config: a read-only RoleConfig (allows_write=False)
- journaling: ALL_CELLS read tier so it can read internal intent like QA
- tracing: post_pr_review requires a learning entry; claim_pr_review is waived
- seeds presentation + factory prompt layer + builtin tools + the agentrole
  enum migration (037) + regenerated verb/lifecycle artifacts

Read-only at /app like QA/auditor; default-off — nothing dispatches review work
until external_pr_enabled. Foundation + role-config + enum suites green; ruff +
mypy clean; orchestrator boots.
2026-06-16 10:37:06 +02:00
Renn F beb2287316 feat(orchestrator): inbound external-PR discovery + review-task ingestion
Add the dormant inbound path for external-PR review (gated by external_pr_enabled,
off by default):

- GitService.list_open_prs lists a project's open PRs, normalized with fork /
  author-association classification (the inbound counterpart to the org's
  outbound, head-filtered PR calls).
- TaskService.ingest_external_pr + external_review_task_exists create one
  de-duped review task per newly-seen external PR (source='external_pr',
  confirmed_by_human=False) — a gate so no agent fetches or runs contributor
  code until a human confirms the PR.
- A poll loop in the orchestrator, mirroring the strategy-engine loop: only when
  enabled it lists each active project's open PRs, ingests the external ones, and
  wakes the dispatcher.

The trust-critical author/fork classifier is unit-tested; the GitHub-list and
DB-ingest paths are exercised by the integration gate.
2026-06-16 09:59:56 +02:00
Renn F f9f2adb0aa feat(config): add dormant external-PR review settings (default-off)
Introduce the configuration contract for inbound external-PR review, mirroring
the strategy-engine block: external_pr_enabled (master switch, off),
external_pr_poll_interval_seconds (>=60), external_pr_author_allowlist, and
external_pr_require_human_confirm (default true). All inert by default — no
inbound GitHub call and no untrusted-code execution until the CEO opts in and a
human confirms an ingested PR.
2026-06-16 09:41:44 +02:00
Renn F cdef7549fe docs(rag): document the 0.4.0 company layer for agents
Add an architecture doc for the company-in-a-box layer (charter, pitches,
strategy engine, cockpit signals, feature toggles) — none of it was in the KB
the agents query — and note the on-demand human-facing roles (prompter,
secretary) in org-structure so they are no longer invisible.
2026-06-16 09:14:20 +02:00
Renn F 568ae7a25d docs(rag): add prompter/secretary role docs + refresh guardrails surface
The agents' runtime KB had drifted three releases behind the gateway. Add the
two missing role docs (prompter, secretary — both live-session SDK chat roles,
human-only) and fold the AC/decomposition guardrails and per-dev code queues
into the high-traffic PM docs:

- task-model: acceptance_criteria_ids + parent_ac_refs fields and how the
  child->parent AC link works.
- task-planning: covers_parent_criteria on delegate, the parent_ac_coverage /
  unclaimed_parent_acs briefing fields, the decomposition-floor and roll-up
  gates (safe-by-construction), and per-dev sequenced code queues.
- task-tools: same coverage note + correct stale verbs — QA is
  pass_review/fail_review (not pass/fail), cell_pm gains reassign, and main_pm
  no longer claims submit_up/reassign it does not have.
2026-06-16 09:11:27 +02:00
Renn F cb7cbb69fa Update uv.lock v0.5.0 2026-06-16 08:52:18 +02:00
Renn F 99c2ac5c62 docs(changelog): cut RoboCo 0.5.0
Promote the Unreleased section to [0.5.0] - 2026-06-16 — AC/decomposition
guardrails, per-dev sequenced code queues, the unified Business page, the 26
panel UI fixes, and the spawn/PR/ownership firefight fixes — and add the 0.5.0
compare link.

Correct the Removed note: the /cockpit, /company-goals, /secretary, and
/pitches panel routes are deleted (404), not redirected; the relocated strategy
signals are served by the new GET /api/cockpit/signals endpoint.

Bump the version 0.2.0 -> 0.5.0 across pyproject, __init__, config app_version,
panel package.json, and the uv.lock self-entry — these had drifted unbumped
since 0.3.0.
2026-06-16 08:48:19 +02:00
1757659754 [27208d92] Consolidate Cockpit/Goals/Secretary/Pitches into a Business page (#184)
* [0c66b856] Frontend: Build tabbed Business page consolidating Goals/Secretary/Pitches (#183)

* [c9f00d0d] feat(business): add /business tabbed page consolidating Goals, Secretary, Pitches (#182)

- Create src/app/(dashboard)/business/page.tsx with URL-driven Tabs (goals|secretary|pitches), reading ?tab= via useSearchParams; defaults to 'goals'
- Create src/components/business/goals-tab.tsx: key-introspected form fields for objectives items and operating_policy (no raw JSON textareas), updated_at/updated_by metadata, skeleton loading, OfflineState on error
- Create src/components/business/secretary-tab.tsx: ReactMarkdown (GFM) chat bubbles, structured directive cards with labeled key-value rows, RequiredNotesDialog for reject, skeleton loading, OfflineState on error
- Create src/components/business/pitches-tab.tsx: sub-header Refresh button, PitchCard skeleton loading, OfflineState on error (not empty-state text), RequiredNotesDialog for both Approve and Reject
- Create src/components/ui/required-notes-dialog.tsx: Submit disabled on empty/whitespace, Cancel closes without action, state resets on each open via key pattern
- Update sidebar.tsx: remove Cockpit/Company Goals/Secretary/Pitches entries, add single Business entry (Building2 icon, /business)
- Replace company-goals/page.tsx, secretary/page.tsx, pitches/page.tsx with server-side redirect() to /business?tab=X
- Replace cockpit/page.tsx with notFound() (404)
- All tabs: shadcn Card + Skeleton, sonner toast for success/error

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [e3e5ff9b] feat(dashboard): add StrategySignalsPanel next to CeoApprovalQueue in a 2-column grid layout (#181)

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* refactor(panel): delete the consolidated old routes instead of stubbing them

cockpit/company-goals/secretary/pitches are fully consolidated into /business,
so the old route pages are dead code. Remove the four page.tsx files outright
rather than keep redirect/404 stubs — the clean move is to delete, not add.
The sidebar already points only at /business; no internal links reference the
old routes (the remaining /company-goals|/secretary|/pitches|/cockpit strings
are backend API paths the API clients call, unaffected). Old bookmarks now
resolve to Next's default 404, which is correct for a removed route.

* perf(cockpit): light /cockpit/signals endpoint for the Dashboard panel

The relocated Strategy Signals panel was calling /cockpit/summary, which runs
the whole fan-out (company goals + usage/spend + task-counts + pitches +
strategy assess) just to read the signals. Add CockpitService.signals() +
GET /api/cockpit/signals (CockpitSignals schema, same _COCKPIT_ROLES gate) that
runs only StrategyEngine.assess(), and repoint the panel (+ cockpitApi.signals()
client method, CockpitSignal type). Now the Dashboard fetches only what it
shows. Backend gated: ruff + full mypy + 6 cockpit tests green (live DB).

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-16 08:40:51 +02:00
a89d3cc885 [a1e2bfb4] Fix 26 verified UI bugs across the panel dashboard (#175)
* [41219301] Fix 26 verified UI bugs in the panel dashboard (#174)

* [115788ef] API/state bugs batch 1 — PATCH fix, WebSocket reconnect, agent roster, sessions export, timestamp (#173)

* fix(orchestrator): launch agent MCP servers with uv run --no-sync

Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.

* fix(agent): launch agent uv-run subprocesses with --no-sync

Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync

* [115788ef] fix(api): use PATCH not PUT in tasksApi.update(), remove WS double-increment, fix staleTime/roster id, remove sessions groupsApi dup, add < 1h ago label

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [db2c341f] UI/visual bugs batch 1 — priority labels, QA columns, DnD prompt, CEO dialog, dark mode (#172)

* fix(orchestrator): launch agent MCP servers with uv run --no-sync

Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.

* fix(agent): launch agent uv-run subprocesses with --no-sync

Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync

* [db2c341f] fix(ui): priority badges, QA columns, DnD dialog, CEO label, dark mode

- priority-indicator.tsx: update labels P0→P0-Highest etc, add text-xs to all color strings, fix className operator precedence bug
- task-table.tsx: match priority label format and add text-xs to badge className
- kanban-column.tsx: show QA Pass/Fail buttons in VERIFYING column alongside AWAITING_QA
- kanban-board.tsx: intercept DnD drops onto NEEDS_REVISION/AWAITING_DOCUMENTATION to show notes dialog when showQaActions is true
- task-action-dialogs.tsx: change CeoApproveDialog Label from 'Approval notes' to 'Notes required'; fix all Cancel buttons to call handleOpenChange(false) so state is cleared on dismiss
- create-task-dialog.tsx: reset form when dialog is closed without submitting
- active-blockers-panel.tsx: add dark:border-red-900 dark:bg-red-950 dark:hover:bg-red-900 to blocker items
- notifications/page.tsx: add dark: Tailwind variants for NORMAL, HIGH, URGENT priority badge colors

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [123e2ec2] Fix remaining 15 UI bugs — revision pass after CEO rejection (#178)

* [cdb9b22a] fix(ui): task-header BACKLOG/NEEDS_REVISION actions, P0 priority label, chat-composer safe clear, and inline-edit double-mutation guards (#176)

- task-header.tsx: add BACKLOG ('Activate Task') and NEEDS_REVISION ('Start Revision') cases to getAvailableActions() switch so the Actions dropdown is never empty for those statuses
- draft-proposal-card.tsx: PRIORITY_LABELS[0] changed from 'Urgent' to 'Highest' to match backend contract
- chat-composer.tsx: move setValue('') inside try-block after onSend resolves; a failed send now preserves the textarea text
- acceptance-criteria.tsx: onMouseDown={(e)=>e.preventDefault()} on inline-edit save button to prevent onBlur+onClick double API mutation
- tab-dependencies.tsx: same onMouseDown guard on parent-task inline-edit save button
- tab-plan.tsx: onMouseDown guards on all inline-edit/add save buttons (ApproachSection, SubTasks, TechConsiderations, Risks, OpenQuestions)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [01852d69] fix(dashboard): wire real agent status, refetch all 4 queries, error indicator, Coming Soon tooltip on search, sentinel div auto-scroll in message-list and mentor-chat, and New Report / Generate Report button mutations in reports-panel and auditor-dashboard — all 9 files fixed (#177)

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [3f35f502] feat(tasks): add case activate and case start-revision to handleAction switch in task detail page (#179) (#180)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* fix(tasks): route Start Revision through the operator status override

The new "Start Revision" action on a NEEDS_REVISION task called
lifecycle.start (POST /tasks/{id}/start), which is assignee-only — so an
operator/CEO clicking it from the task detail page got a 403 ("Only the
assigned agent can start this task") instead of a transition.

Route it through useUpdateTask (PATCH /tasks/{id} with status=in_progress)
instead. The backend treats status as an audited admin override applied via
admin_set_status and gated on elevated (ASSIGN) permissions — the same
god-mode path the kanban board uses for operator status changes — so the
operator can nudge a needs_revision task back into progress for its assignee
to rework. Mirrors the existing kanban updateTask.mutateAsync shape.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
2026-06-16 06:57:00 +02:00
Renn F de1336c74d fix(gateway): let a dev idle past lane-held code-queue siblings
Per-dev sequenced queues (the prior commit) have a PM delegate a dev's whole
code queue up front, so a dev owns several pending, assigned-but-unclaimed code
leaves at once (seq0 + seq2). The orchestrator's lane barrier holds the seq2
SPAWN while seq0 is non-terminal — but _pending_assignment_guard rejected
i_am_idle for ANY pending assigned task, with no lane awareness. So a dev whose
current leaf just moved to QA (awaiting_qa) could neither idle (guard rejects)
nor proceed cleanly: it was steered to claim seq2 early (the claim path has no
lane/sequence check, since delegate sets `sequence` not `dependency_ids`),
jumping its own queue order, or it looped on the rejection. An adversarial
review of the queue work surfaced this; it is latent until PMs actually
delegate multi-item per-dev queues, so the green suite hid it.

Fix: TaskService.has_earlier_incomplete_code_sibling mirrors the orchestrator's
lane barrier in the service layer; _pending_assignment_guard now drops a dev's
lane-held pending code leaves (via _pending_blocking_idle / _pending_not_lane_held)
so the dev idles cleanly and the orchestrator spawns the next queue item when
the lane clears — preserving one-leaf-at-a-time, in order. `is not True` keeps
it inert under partial test mocks. Tests cover the service primitive (live /
terminal / higher-seq / non-code / missing-field) and the guard (dev idles when
lane-held; still blocks a non-lane-held pending leaf). Full mypy + xenon green.
2026-06-16 05:13:00 +02:00
Renn F e66a79a8ec fix(gateway): align ChoreographerHelpers._briefing_for stub with impl
The spec-2 change added `include_ac_coverage` to Choreographer._briefing_for
(_impl.py) but not to the typed stub in _protocol.py that the role mixins
inherit for static analysis. The composed Choreographer (board/doc/qa mixins +
_LegacyChoreographer) then had two incompatible _briefing_for signatures in its
MRO — caught by full `mypy roboco/ tests/` (not by a per-file check). Add the
same keyword to the stub. mypy clean at CI scope (648 files).
2026-06-16 04:55:04 +02:00
Renn F 92543ad593 chore(prompts): stop generating verb tables for driver-based roles
regenerate_verb_tables.py looped every role in ROLE_CONFIGS, emitting a
_generated/<role>.md for prompter and secretary too. Both intentionally keep
only note+evidence in role_config — their real tools live in their agent_sdk
drivers (intake: propose_draft; secretary: read_state/read_task/
submit_directive, the last gated through the backend /directives), and neither
uses the _generated/<role>.md prompt-composition path. So the generated tables
understated those roles and showed up as perpetually-untracked noise.

Skip the driver-based roles (_DRIVER_BASED_ROLES) in both the aggregate verbs.md
and the per-role file output, with a comment pointing at the real surfaces.
Regenerated verbs.md drops the two misleading sections.
2026-06-16 04:50:44 +02:00
Renn F e209e285b8 feat(dispatch): per-dev sequenced queues for code subtasks (guardrails spec 3)
True two-dev parallelism: a cell PM delegates the FULL set of code units up
front — each dev gets its own queue, both build at the same time, each works
its queue one task at a time in order. Replaces the old ceiling (≤2 code
subtasks per parent, one per dev) which structurally forced under-decomposition.

- Cap: `code` removed from `_SPINE_TYPE_CAPS` — no per-parent code cap (total
  fan-out still bounded by `_SUBTASK_HARD_CAP=12`); `planning`/`documentation`
  stay sequential at 1. `_same_assignee_rejection` exempts `code` so a dev may
  own a queue, but still rejects an exact same-title duplicate (the accidental
  re-delegation bug). `_spine_type_dup_envelope` simplified to the sequential
  spine it now only serves.
- Dispatch barrier: `_blocked_by_earlier_lane_sibling` holds a dev's
  higher-sequence pending code leaf while it still has an earlier non-terminal
  code sibling under the same parent (keyed on assignee, gates only code) — the
  dev works its queue in order. Wired into `_spawn_pending_dev`. Loop-free
  (skip the tick, no reject/respawn) and best-effort (lookup failure → dispatch),
  mirroring the existing merge barrier. The merge barrier is unchanged: leaf
  PRs still merge serially in sequence order into the shared cell branch, so the
  independent build lanes never wedge it.
- Prompt: cell_pm role guidance rewritten from the two-subtask-cap model to the
  per-dev-queue model (delegate all units now; dependent units go in one dev's
  queue, upstream first).

Independent per-dev queues (each lane advances at its own pace) rather than
strict cross-dev wave-sync, by design — more parallel and leaves the
wedge-prone merge barrier untouched. Pairs with the spec-2 idle coverage gate:
removing the code cap lets a PM claim every criterion up front, so that gate is
always satisfiable.
2026-06-16 04:10:05 +02:00
Renn F 1fb723174a feat(gateway): decomposition coverage gate + AC visibility (guardrails spec 2)
The decomposition floor that pairs with the roll-up gate (spec 4): a PM
cannot finish decomposing a parent while one of its acceptance criteria has
no subtask responsible for it — the "two leaves, half the ACs silently
dropped" pattern. Three parts:

- Gate: i_am_idle is rejected for a cell_pm/main_pm whose owned parent still
  has criteria in unclaimed_parent_acceptance_criteria (claimed = referenced
  by any live, non-cancelled child). Distinct from the roll-up gate, which
  fires at submit_up/complete and demands a *completed* child; this fires
  earlier and asks only that every criterion be *claimed*. Safe-by-
  construction: inert until a PM declares coverage, so legacy / not-yet-
  adopted decompositions are never blocked.

- Visibility: PM-facing briefings (give_me_work, i_will_plan, submit_up) and
  every delegate response now carry parent_ac_coverage ({id,text,claimed,
  verified} per criterion) + unclaimed_parent_acs, so a PM can map subtasks
  to criterion ids via covers_parent_criteria and see what is still
  uncovered after each delegate. Off for leaf roles, so a developer's own
  criteria never surface as bogus "unclaimed" noise.

- Prompts: cell_pm / main_pm role prompts document covers_parent_criteria and
  the new idle enforcement in the existing Coverage discipline.

TaskService.{parent_ac_coverage,unclaimed_parent_acceptance_criteria} added
beside uncovered_parent_acceptance_criteria; all three refactored onto a
shared _parent_ac_ref_sets helper (keeps each under the xenon B ceiling,
preserves the committed roll-up behavior). Verb tables regenerated for the
new delegate param — the regen also syncs pre-existing table drift that was
never regenerated after earlier merges (read_messages, pass_review
ac_verdicts, board pitch). Two brand-new generated tables (prompter,
secretary) are left untracked pending a separate decision.
2026-06-16 03:49:00 +02:00
Renn F 5ce4570c85 feat(gateway): expose covers_parent_criteria on the delegate verb (guardrails spec 2)
The AC coverage + roll-up gates (specs 1, 4) are inert until PMs declare which
parent criteria each subtask covers. Thread covers_parent_criteria end-to-end so
they can: MCP delegate tool -> DelegateRequest schema -> cell_pm + main_pm routes
-> DelegateInputs -> child.parent_ac_refs. Additive/optional — no behavior change
until a PM populates it; the tool docstring instructs splitting the parent's
criteria across subtasks so their union covers all of them.

ruff + mypy clean; 107 delegate/flow_server tests green.
2026-06-16 03:22:50 +02:00
Renn F 0fd9aee88d feat(gateway): roll-up AC-verification gate (guardrails spec 4/4)
A parent could complete / submit_up / escalate_to_ceo once its subtasks were
merely terminal — never checking whether the parent's acceptance criteria were
actually satisfied. That's how PR #175's half-built umbrella sailed to CEO
approval (escalate_to_ceo had no subtask/AC check at all).

- TaskService.uncovered_parent_acceptance_criteria(parent): parent ACs not
  covered by a COMPLETED child (via parent_ac_refs). Safe-by-construction —
  returns [] unless a child declares coverage, so it is INERT for tasks
  decomposed before coverage tracking and activates only once a PM maps
  children to parent criteria. Cancelled children do not count.
- _parent_acs_covered_envelope wired into all four roll-up gates: cell_pm_complete,
  main_pm_complete, submit_up, and escalate_to_ceo (the weakest — previously
  only journal:decision). isinstance guard keeps it inert under partial mocks.
- 4 new tests; 57 task + 89 gateway tests green.

Pairs with spec 2 (coverage at decompose-time forces the linkage this enforces).
2026-06-16 03:18:17 +02:00
Renn F 87ca142f4e feat(tasks): AC identity + child->parent AC linkage (guardrails spec 1/4)
Foundation for the decomposition-coverage and roll-up AC-verification gates.
Acceptance criteria were a flat list[str] with no per-criterion identity, so
nothing could relate a child task's criteria to the parent's — letting a PM drop
half a parent's ACs unnoticed (PR #175).

- migration 036: additive acceptance_criteria_ids + parent_ac_refs array columns;
  backfills stable md5(task_id:index) ids for existing rows.
- Task model + TaskCreateRequest + db table: the two fields.
- TaskService.create generates one stable id per criterion (1:1) when absent.
- DelegateInputs.covers_parent_criteria -> child.parent_ac_refs (the linkage),
  propagated through create_subtask.
- regression-safe (53 task tests green) + 1 new test.

Coverage gate (spec 2), roll-up AC gate (spec 4), per-dev sequenced queues
(spec 3) build on this. Design: docs/SPEC_AC_GUARDRAILS_2026-06-16.md.
2026-06-16 03:02:54 +02:00
Renn F 55ff05e6ec fix(task): restore pre-block owner when an admin override leaves blocked
A developer that hits a wall calls i_am_blocked, which escalates the code task
to its cell PM (assigned_to=PM, BLOCKED) and snapshots the dev as
pre_block_assignee — the intended dev->cell-PM triage handoff. The in-band
recovery (unblock(restore=True)) hands ownership back to the dev. But the
OUT-OF-BAND paths — the operator PATCH /tasks/{id} status override and the
orchestrator's own _auto_recover_blocked_parent / _auto_resume_paused_parent —
go through admin_set_status, which set only status and never restored the owner.
The task re-entered pending/in_progress still owned by the PM, and the dispatcher
then execute-spawned the PM on a code task it cannot do ('break this down and
delegate' against the task itself) -> respawn loop.

admin_set_status now, when taking a task out of 'blocked' into pending/in_progress
with a pre-block snapshot present, routes through the existing
_apply_pre_block_restore primitive (the same one unblock(restore=True) uses) to
hand ownership back to the executor. Every other override is unchanged, and the
escalate/apply_escalation/block-down path is untouched, so the dev->cell-PM
handoff still works. + 2 regression tests.
2026-06-16 00:14:01 +02:00
Renn F 3d8c0e1c54 fix(git): create_pr auto-creates a missing PR base branch on origin
open_pr -> GitService.create_pr posted "base": parent straight to GitHub, so
when the parent (an ancestor task's integration branch) was never pushed — a PM
paused before its first push, or the workspace was wiped — GitHub 422'd "base
field invalid" and stranded every child PR. The base-existence fallback added
in 3d9dd298 lived only in create_pull_request, which open_pr never calls.

Add _ensure_base_on_remote and call it in create_pr: if the base branch is
absent on origin, create it off the default branch's tip (preserving the
integration hierarchy) instead of failing; fall back to the default branch only
if that create push itself fails. Covered by 3 new tests.
2026-06-15 23:34:43 +02:00
Renn F a4d84992bd Merge branch 'master' of https://github.com/rennf93/roboco 2026-06-15 22:45:25 +02:00
Renn F 25aed51c04 fix(agent): launch agent uv-run subprocesses with --no-sync
Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync
2026-06-15 22:43:05 +02:00
Renn F 5df7f78508 fix(orchestrator): launch agent MCP servers with uv run --no-sync
Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.
2026-06-15 22:29:33 +02:00
Renzo FandGitHub f443a60d29 Update CHANGELOG.md v0.4.0 2026-06-15 20:56:11 +02:00
46d89b58fe feat: company-in-a-box — goal-aware company layer (0.4.0) (#171)
* feat(goals): company charter singleton — data layer (Business Goals slice 1)

First slice of the company-in-a-box "Business Goals" phase: a single CEO-owned
charter row (north star + objectives + constraints + operating policy) that
will be injected into every agent's context_briefing so all work is goal-aware.

- CompanyGoalsTable: singleton table (all-zeros id), JSON objectives /
  constraints / operating_policy, updated_at / updated_by.
- migration 032: create + seed the singleton row (offline-renderable; column
  server-defaults fill an INSERT of just the id).
- CompanyGoalsService: get() (empty defaults when unset) + upsert() (singleton,
  partial update, caller commits).
- tests: empty defaults, roundtrip, singleton + partial-update preservation.

Next slices (mapped, not yet built): briefing injection (BriefingInputs +
build_context_briefing + EvidenceRepo), API route (GET any / PUT CEO-only),
panel /goals page, and base/Board/PM prompt mentions.

* feat(goals): inject the company charter into every agent briefing (slice 2)

The charter is now goal-aware context for every agent:
- BriefingInputs gains company_goals; build_context_briefing surfaces it.
- EvidenceRepo.company_goals(): single-row lookup returning a COMPACT charter
  (north star + objectives + constraints + operating policy; audit columns
  dropped, lists capped) or None when unset, so an empty charter never bloats
  the per-verb briefing.
- _briefing_for wires it into every context_briefing.

Tests: briefing surfaces company_goals (defaults None); repo returns None for an
absent/empty charter and the compact dict when set.

* feat(goals): company charter API — GET any agent, PUT CEO-only (slice 3)

- routes/company_goals.py: GET returns the charter (any authenticated agent —
  it drives every briefing); PUT is CEO-only (403 otherwise), partial update via
  model_dump(exclude_unset=True), explicit commit.
- schemas/company_goals.py: response + partial-update models.
- registered at /api/company-goals.
- tests: GET open to any role, CEO update persists + is readable, non-CEO 403.

* feat(goals): make the company charter actionable in agent prompts (slice 5)

Agents already receive company_goals in the briefing (slice 2); now tell them to
act on it:
- base.md: universal "Align with the company charter" section — favour work and
  trade-offs that advance the objectives, honour the constraints, flag conflicts;
  never a license to leave your role.
- board / main_pm / cell_pm: role-specific lines tying triage / cell-routing /
  subtask decomposition to the charter.

Prompts are composed at spawn from base.md + roles/*.md directly (compose_prompt),
so no _generated regeneration is needed.

* feat(goals): company charter panel page (slice 4)

CEO-facing editor for the charter at /company-goals:
- lib/api/company-goals.ts: get / update (PUT) client.
- company-goals-card.tsx: edit north star + constraints (one per line) +
  objectives / operating_policy (JSON, parsed + validated with toast errors);
  display derives from server state (no set-state-in-effect).
- (dashboard)/company-goals/page.tsx + a "Company Goals" sidebar nav link.

tsc --noEmit + eslint clean. Completes Phase 1 (Business Goals): data, briefing
injection, API, prompts, panel.

* fix(test): make test_app route assertions robust to FastAPI 0.137 _IncludedRouter

FastAPI 0.137 stopped flattening include_router into app.routes — each include is
now an _IncludedRouter (a BaseRoute with no .path), so `{r.path for r in
app.routes}` raised AttributeError and the two router-registration tests failed
(the bump arrived via the claude-agent-sdk update in uv.lock). Add
_registered_paths(): OpenAPI schema paths (the stable public contract) plus each
included router's prefix, which also covers the websocket /ws mount (never in the
schema). Drops the now-incorrect type: ignore[attr-defined].

* feat(research): pluggable web search/fetch for Board + PM agents

Add a provider-agnostic web-research capability so the Board and PMs can
ground decisions in current external evidence the knowledge base can't
answer.

- ResearchService selects a provider adapter from config: Tavily, Brave,
  and Exa adapters plus a NullProvider that degrades gracefully when no
  key is set. Result count and fetched-content size are clamped to caps.
- /api/research/search and /api/research/fetch: role-gated to Board + PMs
  (and the CEO), with a per-agent/day Redis quota that fails open.
- roboco-search MCP server (web_search / web_fetch) calls those routes;
  the provider key stays server-side and agent containers never egress.
  Mounted per role by the orchestrator, behind a master switch.
- Charter-aware prompt guidance for Board, Main PM, and Cell PM.

Additive: with no key configured it is a no-op and the existing delivery
lifecycle is unchanged.

* feat(pitch): Board pitch -> CEO approve -> auto-provision repos

Add an additive origination path so a product can be proposed, approved,
and stood up without manual repo/Project setup.

- Pitch entity + migration (pitches table); PitchService create/list/
  reject/approve.
- GitHubProvisioningService: the one place that creates repos (POST
  /orgs/{org}/repos). Server-side token/org; when unconfigured the whole
  approve path is inert and nothing is created.
- On approval: provision one repo per target cell, register a Project per
  repo, create a Product when multi-cell, and seed one Main-PM delivery
  task — all reusing the existing Product / coordination-task machinery.
- /api/pitches: Board authors (PO/HoM), CEO approves/rejects, Board+PM+CEO
  view. Errors mapped via a single translator.

Additive: the delivery lifecycle is untouched; with no provisioning token
the capability is a no-op. Agent-facing pitch tool + panel are follow-ups.

* feat(strategy): dormant autonomous strategy engine (engine 2)

Add a second, optional engine that watches the company against its
standing goals and surfaces what needs the CEO — without touching the
delivery lifecycle (engine 1).

- StrategyEngine.assess() reports observations: the company is idle while
  goals stand, and tasks stranded in 'blocked' past a threshold.
- run_cycle() notifies the CEO (notify-only; it never spends, builds, or
  auto-approves — originating work stays a CEO decision).
- Orchestrator runs it on its own interval, started/stopped with the other
  background loops; the loop returns immediately unless enabled.

DORMANT by default (strategy_engine_enabled=False): the loop never runs and
a standard deployment is unchanged. Auto-origination is a further opt-in.

* docs(changelog): record Business Goals, Web Research, Pitch->Provision, and the dormant strategy engine under Unreleased

* feat(secretary): wire the Secretary role end-to-end (foundation)

Add SECRETARY as a distinct role — the CEO's conversational chief-of-staff,
governed separately from the Prompter (which stays read-only/human-only).
This is the role foundation only; authority, the live agent, and the panel
land in following commits.

- foundation/identity: Role.SECRETARY (board level), seeded secretary-1 agent,
  role-level mapping.
- journaling read tier (ALL — it advises the CEO), role_config entry,
  per-role model (opus), prompt-layer mapping + roles/secretary.md.
- i_am_idle gains SECRETARY so the role has a verb surface.
- migration 034: add 'secretary' to the agentrole enum (mirrors 025).
- Role-registry tests updated for the new role.

Inert by itself (nothing spawns it yet); additive — existing roles unchanged.

* feat(secretary): directives + gate-list authority (backend)

The Secretary acts only under CEO command. Low-risk directives (relay a
dictated message) execute immediately; high-impact ones — charter edits,
task start/cancel/override, pitch approval, announcements — are recorded
pending and run only after the CEO confirms (the gate list).

- secretary_directives table (migration 035) as the command audit + queue.
- SecretaryService: read company state; submit (direct->run, gated->queue +
  notify CEO); confirm/reject; execution runs with the CEO as actor through
  the existing services (the Secretary never holds CEO authority itself).
- /api/secretary: submit + state/task reads (Secretary or CEO); list/confirm/
  reject (CEO only). Writes commit explicitly.

* feat(secretary): live conversational agent (container + bridge)

Stand up the Secretary as a persistent Claude-SDK container the CEO chats
with, mirroring the Intake agent and reusing its driver/session machinery.

- secretary_driver: build_secretary_options exposes read_company_state /
  read_task / submit_directive as SDK tools that call /api/secretary/* with
  the agent's HMAC token; backend-call logic is module-level + tested.
- secretary_main: container entrypoint (receiver + relay) reusing IntakeDriver.
- orchestrator: start/spawn/reap secretary session + run-cmd builder; no
  workspace clone (reads state via API), mints a role=secretary token.
- secretary_live routes: panel <-> container bridge over the live registry.
- agent-secretary image (Dockerfile + compose build service).

Inert until a session is started; additive — intake and all agents unchanged.

* feat(secretary): panel chat + directive confirmation queue

The CEO's Secretary surface: a live chat (SSE) to talk to the Secretary, and
a 'Needs your confirmation' queue listing gated directives the Secretary
proposed — each with Confirm / Reject. Adds the sidebar nav entry.

- lib/api/secretary.ts: live (start/stream/status/send/stop) + directive
  (list/confirm/reject) + state clients (all as the CEO).
- hooks/use-secretary.ts: drives one chat, accumulating SSE token deltas.
- secretary page: chat pane + pending-directive cards.

Completes the Secretary end-to-end (role + authority + live agent + panel).

* feat(pitch): agent-facing pitch tool + pitches panel

Complete the pitch path: the Board can now author pitches through the gateway,
and the CEO reviews/approves them in the panel.

- content_actions.pitch (Board-only) -> PitchService.create, returning an
  Envelope; wired as a do-tool (do_server + /api/v1/do/pitch + schema) and
  added to the Board's do-tools.
- Panel /pitches page: lists pitches with CEO Approve & provision / Reject;
  sidebar nav entry.

Pitch (Phase 4) is now end-to-end: author -> CEO approve -> auto-provision.

* feat(cockpit): read-only 'is the business winning?' summary

A pure aggregation for the CEO over existing data — no new state, no writes.

- CockpitService.summary(): charter north-star/objectives, delivery counts
  (in-flight/blocked/awaiting-CEO), 30-day spend vs the charter's budget cap,
  pending pitches, and the strategy engine's signals (what needs you). Stamped
  basis='proxy' — performance is a proxy until real launches.
- GET /api/cockpit/summary (CEO / Board / Main PM / Secretary).
- Panel /cockpit page + sidebar nav.

Reuses goals + usage + StrategyEngine.assess(); reads only.

* docs(changelog): add the Secretary and Cockpit to Unreleased

* fix(test): isolate the company-goals empty-defaults test from committed state

The shared test DB persists committed writes across tests; a route test
commits a charter, so the unit test's 'unset' assertion must establish its
own clean precondition rather than assume global emptiness.

* fix(gateway): lower evidence_repo complexity to rank A (xenon gate)

company_goals()'s 4-way `or` emptiness check tipped the module average to
rank B; `any(...)` is equivalent and keeps the module under the gate's A bar.

* chore(compose): mirror agent-secretary-image build into docker-compose.yaml

Both compose files are byte-identical and tracked; .yaml carries the same
agent-secretary-image build service already present in docker-compose.yml.

* chore(lifecycle): regenerate artifacts for secretary i_am_idle

The secretary role gained i_am_idle in the lifecycle spec; regenerate the
generated prompt/doc/json artifacts so foundation-check stays green.

* docs(changelog): cut the company-in-a-box phases to 0.4.0

Label the six additive phases (business goals, web research, pitch-provision,
strategy engine, secretary, cockpit) as 0.4.0; tag v0.4.0 is held until the
branch merges to master so it points at the release commit.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-15 20:47:41 +02:00
Renzo FandGitHub 9f668b2a50 Update CHANGELOG.md v0.3.0 2026-06-15 20:27:06 +02:00
Renn F 5fc77353a8 docs(changelog): catalog everything merged since 0.2.0 under Unreleased 2026-06-15 13:53:28 +02:00
Renn F 892fd20aac docs(security): record that WebSocket auth is REST-only and /ws/system is unauthenticated
Token enforcement currently covers the REST API only; the /ws/* endpoints —
including the read-only operator stream /ws/system, which carries rate-limit and
token-usage telemetry — do not validate X-Agent-Token even in secure mode (nginx
injects it for the panel, but a direct WS connection is not rejected). Recorded
under the existing trusted-network disclaimer; the streams are read-only with no
control surface.
2026-06-15 08:26:00 +02:00
Renn F 77771c280c fix: align auditor channel perms, extend desk gate to tests, drop stale usage-event doc
- permissions: the Auditor is a silent, read-only observer with no say/dm in
  its verb surface, so can_write_channel now returns False for it — matching
  the role's real capabilities instead of granting an unreachable channel
  write (test updated to assert read-only).
- Makefile: make lint and make gate now type-check mypy roboco/ tests/, matching
  make quality / make quality-fast, so the developer-desk gate also catches test
  type errors before submit (tests/ is already mypy-clean).
- docs: CLAUDE.md no longer lists USAGE_UPDATE — only USAGE_SNAPSHOT is published
  to /ws/system.
2026-06-15 08:13:53 +02:00
Renn F ba74eb4fd2 fix(test): make test_app route assertions robust to FastAPI 0.137 _IncludedRouter
FastAPI 0.137 stopped flattening include_router into app.routes — each include is
now an _IncludedRouter (a BaseRoute with no .path), so `{r.path for r in
app.routes}` raised AttributeError and the two router-registration tests failed
(the bump arrived via the claude-agent-sdk update in uv.lock). Add
_registered_paths(): OpenAPI schema paths (the stable public contract) plus each
included router's prefix, which also covers the websocket /ws mount (never in the
schema). Drops the now-incorrect type: ignore[attr-defined].
2026-06-15 07:54:34 +02:00
Renn F 3d9dd29848 fix(git): fall back on merge-method and PR-base when the repo/remote refuses
Two completion-stranding fixes, reimplemented on current master from
CoreyRDean's #120 and #121:

- merge_pull_request: on a 405 (repo disallows the requested merge method — e.g.
  squash merges turned off in repo settings), look up a permitted method
  (_first_allowed_merge_method, preferring squash > merge > rebase) and retry
  once. A repo's merge-button config can no longer permanently wedge the PM on
  an open, mergeable PR. No behavior change when the requested method is allowed.
- create_pull_request: if the resolved PR base branch is missing on origin (an
  ancestor task claimed but never pushed -> GitHub 422 "base field invalid"),
  ls-remote the base and retarget to the project default branch
  (_pr_base_on_remote), mirroring the existing create_branch fallback. No
  behavior change when the base exists.

Both funnel through the central git paths so every caller benefits. Adds unit
tests for both fallbacks.
2026-06-15 07:03:59 +02:00
Renn F 128c3cf908 updated uv.lock 2026-06-15 06:42:26 +02:00
Renn F de82e06b3c feat(rag): hybrid retrieval (vector + full-text), retire HyDE
Recall no longer depends on a per-query HyDE LLM call — it comes from the index.
Each chunks_<type> table gets a generated `tsv` column + GIN index (migration
031; the engine CREATE TABLE matches so fresh tables get it too).
VectorStore.hybrid_search fuses pgvector cosine with Postgres full-text in one
query: score = min(1, cosine + 0.3 * normalized_ts_rank). A vector-only match
keeps its cosine score (so decisions/reviewer thresholds are unchanged), a
keyword match adds a bounded boost (the recall win), and a keyword-only match
stays low. Empty/garbage query text degrades to pure vector.

HyDE is removed from the search hot path: _compute_query_embedding now embeds
the query directly, and _generate_hyde_passage / rag_use_hyde /
IndexConfig.use_hyde are deleted. So a search is one local embed + one indexed
SQL — no LLM round-trip. The raw query text is threaded through the
embed-once + concurrent fan-out (search_with_embedding(embedding, query_text)).

Verified live via a real pgvector round-trip: vector ranking + keyword boost +
[0,1] scores + empty-query fallback all correct. Adds wiring + fan-out unit
tests; the fusion SQL itself is verified live (needs pgvector, not gated in CI).
2026-06-15 06:37:27 +02:00
Renn F d7aee91b39 perf(rag): embed the query once and search indexes concurrently
OptimalService.search / query (via _aggregate_citations) ran each index's
plugin.search() sequentially, and every plugin.search re-ran HyDE + embed — so an
N-index query made N LLM+embed round-trips in series (~28s across all indexes,
even though the SQL is fast). Embed the query ONCE
(BaseIndexPlugin.compute_query_embedding) and run every index's vector search
concurrently against that single embedding (search_with_embedding +
asyncio.gather). The search/query signatures and return contract are unchanged;
behavior is identical, just ~Nx fewer embed calls and parallel fetch.

Adds a regression test asserting one embed + per-index fan-out.
2026-06-15 06:12:00 +02:00
Renn F dfd0d1f188 fix(rag): decode jsonb metadata returned as a string by asyncpg
VectorStore.search / list_docs called dict(row["metadata"]), but asyncpg
returns jsonb as a JSON *string* (no codec on the pool), so dict() iterated
characters and raised 'dictionary update sequence element #0 has length 1; 2 is
required' — making every KB search fail at the row-mapping step once migration
030 let the query reach rows (it was masked before by the missing content
column). Add _as_dict(): json.loads a string, pass dicts through, null/non-object
-> {}. Caught by live end-to-end verification on the NAS.
2026-06-15 05:56:15 +02:00
Renn F 6422f77bb9 fix(rag): close audit gaps in the in-house engine
An adversarial audit of the piragi -> in-house swap surfaced nine confirmed
issues; this fixes all of them.

- Re-ingest now REPLACES a source's chunks instead of appending. Add
  VectorStore.delete_by_source and BaseIndexPlugin.replace_on_reingest (default
  True), called before add_chunks in both ingest paths. Without it every
  startup / periodic / manual reindex appended a fresh copy of each doc's
  chunks, growing the tables unbounded and crowding out distinct results.
  Conversations opt OUT (replace_on_reingest=False): their many messages share
  one source URI, so delete-by-source would wipe history.
- index_* now honor the plugin IngestResult. The explicit record endpoints
  (error / standard / decision / review / learning) raise on failure instead of
  writing a green tracking row for content that never persisted;
  conversation / journal indexing stays best-effort but skips the tracking row
  when the embed fails. index_message / index_entry return IngestResult.
- A deprecated index type (code) now returns 404 instead of a 500 leaked from
  _get_plugin's missing-plugin error: add OptimalService.is_index_registered
  and guard the stats / clear / refresh routes. The panel drops the dead 'Code'
  category, filter, badge, label, and mock data.
- Panel: getContext reads 'results' (matches SearchResponse) instead of a
  non-existent 'context' field; the reindex toast no longer reports phantom
  '0 code files'; the stats 'Updated' label uses the max timestamp across
  indexes rather than indexes[0]; ProactiveContextItem matches the wire shape.
- Drop the always-zero per-document chunk_count from the documents API.
- Remove dead RAG settings (hybrid_search, cross_encoder) the engine never
  consumed, and correct stale piragi / BM25 references in code, README, and
  CLAUDE.md. Delete the unused duplicate roboco/kb embedder package the swap
  shipped.

Adds tests for replace-on-reingest (incl. the conversations carve-out) and the
deprecated-index 404.
2026-06-15 04:55:16 +02:00
Renn F e53eb5b7ee fix(rag): make migration 030 offline-renderable (plpgsql DO block)
The initial 030 used sa.inspect(op.get_bind()) for its conditional ALTERs, which
raises NoInspectionAvailable under `alembic upgrade --sql` (offline mode) —
breaking test_enum_migration_parity and any offline SQL generation. Reimplement
the same idempotent rename (text->content) + add (created_at) as a self-contained
plpgsql DO block that guards on information_schema at execution time, so it
renders offline and runs online identically. Round-trip verified.
2026-06-15 04:54:51 +02:00
Renn F 996ef56ac3 fix(rag): migrate chunks_* tables to in-house vector-store schema
The in-house RAG engine (which replaced piragi) reads/writes a `content`
column and a `created_at` column on every chunks_<index_type> table and
provisions them at runtime via CREATE TABLE IF NOT EXISTS. On databases that
already carried the piragi-era tables (column `text`, no `created_at`) that
DDL is a silent no-op, so the engine never reshapes them and every
ingest/search/list fails with `column "content" ... does not exist`.

Migration 030 ALTERs each existing chunk table in place — renames
text -> content and adds created_at — preserving the non-rebuildable agent
knowledge (journals, decisions, errors, learnings, reviews, conversations)
that a docs reindex cannot regenerate. It guards on actual column presence,
so it is idempotent and safe on piragi-shaped, already-correct, or absent
tables (e.g. chunks_code).

Adds a guard test pinning the migration's table list to the IndexType enum so
a new index type cannot silently escape the schema alignment.
2026-06-15 03:43:05 +02:00
Renn F 0039e2a7ee test(manifest): guard board roles keep read_messages in do_tools
The PO read_messages gap (board agent soft-blocked on i_am_idle, unable to clear
unread A2A) was deploy-staleness: a PO spawned from an old manifest predating the
read_messages grant in _BOARD_DO. Current code is correct (verified: live
product-owner/head-marketing manifests carry it). Add a regression guard so the
grant can't silently drop from _BOARD_DO for board roles.
2026-06-15 02:56:49 +02:00