- pricing.py: add grok-build-0.1 rates ($1/1M input, $0.20 cached, $2/1M
output), verified against xAI's published pricing. Grok is a priced
non-Anthropic model, so cost computes the moment usage is captured.
- secret-scrub.js: an opencode tool.execute.before plugin porting the
security-critical bash-guard deny rules (git network ops, credential-file
reads, /proc env, internal-host HTTP, roboco.* imports, ROBOCO_AGENT_ID
forgery, env dumps, destructive rm) to the opencode runtime — restoring the
guard the Claude Code hook can't provide there. Throwing denies the call
(confirmed by opencode's env-protection example). Wired into the generated
opencode.json plugin array + baked into the grok image.
Deny logic verified via node (9 deny + 5 allow cases). UNVALIDATED against a
live opencode runtime: confirm it fires in the live E2E spawn before a Grok
dev-agent touches a real repo; the bash permission is operator-tunable as a
second gate.
Cost CAPTURE (distinct from pricing) is intentionally NOT built yet: opencode's
plugin hooks expose model info but no token/usage object, so the capture path
is unconfirmed and needs the live spawn to settle.
CI's "Apply database migrations" failed with asyncpg
UnsafeNewEnumValueUsageError: alembic runs the whole upgrade in a single
transaction, so migration 039's INSERT used 'grok' in the same transaction
that 038 added it — which Postgres forbids. Splitting into two migration
files did not help (one transaction spans both). Wrap the ALTER TYPE ADD
VALUE in op.get_context().autocommit_block() so the value commits before 039
(and any later migration) uses it. Still renders in offline --sql, so the
enum-migration-parity test is unaffected.
Add roboco-agent-grok to the release workflow's image build/publish map so
registry deploys carry the Grok runtime image (parity with every other
agent image). Split from the feature commit because pushing a workflow
change requires a workflow-scoped token.
Complete the native Grok (xAI) path so grok-build-0.1 runs as a real
RoboCo agent, not just the provider seam.
- roboco-agent-grok image (docker/agent-grok.Dockerfile): FROM agent-base
+ opencode (the OpenAI-protocol runtime). One image serves every role;
role behaviour comes from the mounted manifest / mcp-config / system
prompt, exactly as on the Claude path.
- Entrypoint renders opencode.json at spawn from the GrokProvider env
contract + the mounted Claude Code mcp-config.json
(roboco.llm.providers.opencode_config): translates RoboCo's gateway
servers (roboco-flow / roboco-do / ...) into opencode's mcp block,
declares the xAI OpenAI-compatible provider + model, and wires
permissions + instructions. Pure, unit-tested translation.
- Orchestrator registers GrokProvider with the registry-qualified image
(_qualify_agent_image) so it resolves in local and registry deploys.
- Compose (both files + the registry compose) gain an agent-grok-image
builder service.
- Panel: a Grok (xAI) API key card on the AI Providers page, plus the
grok ModelProvider value.
KNOWN PARITY GAP (opencode runtime): the bash-guard PAT-scrub and the
transcript-based usage/cost capture are Claude Code hooks and do not
transfer to opencode. bash permission is operator-tunable
(ROBOCO_GROK_BASH_PERMISSION) so a deployment can fail closed until a
security/usage-parity opencode plugin lands. That plugin and live E2E
validation are the remaining work to finalize with xAI.
Add a roboco/llm/providers/ seam — an AgentProvider lifecycle ABC and a
ProviderRegistry keyed by ModelProvider — so the orchestrator can drive
agent backends other than Claude Code.
The first non-Claude backend is GrokProvider for xAI's grok-build-0.1.
xAI is OpenAI-compatible only (no Anthropic-Messages endpoint), so a Grok
agent runs an OpenAI-protocol runtime pointed at https://api.x.ai/v1
rather than the ANTHROPIC_BASE_URL injection the other providers use. It
reuses the orchestrator's existing mount/auth assembly, so it inherits the
same MCP gateway + tool-manifest wiring as every other agent by
construction, and passes its prompt via env (never an argv positional).
The change is purely additive: only GROK routes through the registry;
Anthropic / Ollama Cloud / self-hosted spawns run the existing
_spawn_container path unchanged.
Includes:
- ModelProvider.GROK (migration 038) + a seeded Grok provider row
(migration 039) + a grok-build-0.1 catalog entry
- GET/PUT /api/providers/grok-key to store the xAI key (Fernet-encrypted,
reusing the existing provider-key machinery)
- ClaudeCodeProvider reference adapter over the current spawn
- unit tests for the registry, GrokProvider (gateway wiring, no
ANTHROPIC_* leak, prompt-injection safety, failure paths) and routing
The dedicated roboco-agent-grok image and the exact OpenAI-protocol CLI
invocation are the remaining piece to finalise with xAI.
The Company Scorecard work (#212) added 6 test dependencies to
panel/package.json (vitest, @vitest/coverage-v8, jsdom, and three
@testing-library packages) without updating the lockfile, so every
--frozen-lockfile install (Docker panel-builder stage and CI) failed
with ERR_PNPM_OUTDATED_LOCKFILE. Regenerate the lockfile so it matches
package.json.
* [0c7a4732] feat(cockpit): add completed_30d and median_lead_time_hours to delivery summary (#207) (#210)
- Extend DeliverySummary schema with completed_30d: int = 0 and
median_lead_time_hours: float | None = None fields
- Add TaskService.get_delivery_stats_30d() that queries tasks completed
in the last 30 days and computes statistics.median of lead times
- Update CockpitService.summary() to source both new keys from
get_delivery_stats_30d() and include them in the delivery dict
- Update tests: mock new method in _patch(), assert new fields in
test_summary_aggregates, fix test_route_ok_for_ceo dict, add three
new unit tests for get_delivery_stats_30d (empty, multi, single)
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
* [d2647edf] Frontend: Build CompanyScorecard card on Goals tab (#211)
* [12569f37] Extend CockpitSummary type and build CompanyScorecardCard component (#208)
* [12569f37] feat(cockpit): extend CockpitSummary type with completed_30d and median_lead_time_hours
Add optional delivery.completed_30d (number) and top-level
median_lead_time_hours (number | null, optional) to CockpitSummary
interface in panel/src/lib/api/cockpit.ts so the API shape captures
the new backend fields without breaking existing consumers.
* [12569f37] feat(business): add CompanyScorecardCard component
Create panel/src/components/business/company-scorecard-card.tsx
exporting CompanyScorecardCard. The card fetches /cockpit/summary
via useQuery and renders five always-visible sections:
- Delivery: in_flight, blocked, awaiting_ceo, completed_30d tiles
(all from API response; no hardcoded numbers)
- Spend: 30d spend + projected monthly; muted 'No budget cap set'
when cap is null; red/destructive styling only when cap is a
non-null number AND over_budget is true
- Speed: 'X.Xh median — target: < 24h' when value present;
'No data yet' when null/undefined; '0h' never rendered
- Two stub Objectives with 'Not tracked yet' label, muted text,
and dashed-border styling — no fabricated numeric values
- Loading: three grouped Skeleton blocks
- Error: OfflineState with title 'Could not load scorecard data'
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [f1f5cded] Integrate CompanyScorecardCard into GoalsTab and pass quality gate (#209)
* [f1f5cded] feat(cockpit): extend CockpitSummary with completed_30d and median_lead_time_hours
Add optional delivery.completed_30d (number) and top-level
median_lead_time_hours (number | null, optional) to CockpitSummary
interface in panel/src/lib/api/cockpit.ts. Backward compatible.
* [f1f5cded] feat(business): add CompanyScorecardCard component
Create panel/src/components/business/company-scorecard-card.tsx
exporting CompanyScorecardCard. Fetches /cockpit/summary via
useQuery and renders five always-visible sections: Delivery (no
hardcoded numbers), Spend (muted 'No budget cap set' when null;
red only when cap set AND over_budget true), Speed (X.Xh median
or 'No data yet'), two stub Objectives with dashed border and
'Not tracked yet' label. Loading: three skeleton groups. Error:
OfflineState 'Could not load scorecard data'.
* [f1f5cded] feat(goals-tab): integrate CompanyScorecardCard into GoalsTab
Import and render CompanyScorecardCard below the charter form in
goals-tab.tsx. The scorecard fetches its own data independently
so all loading/error states are handled per-card. Both cards are
always rendered in the Goals tab.
* [f1f5cded] fix(scorecard-tests): add vitest framework and CompanyScorecardCard test suite
Install vitest + @testing-library/react + @testing-library/jest-dom +
jsdom + @vitest/coverage-v8 as devDependencies in panel/.
Add panel/vitest.config.ts (jsdom env, @/* alias, coverage on
company-scorecard-card.tsx with 80% threshold).
Add panel/src/test/setup.ts (jest-dom matchers).
Update panel/package.json: add test, test:watch, typecheck scripts.
Update panel/eslint.config.mjs: ignore coverage/ directory to keep
lint clean of generated files.
Write panel/src/components/business/__tests__/company-scorecard-card.test.tsx
with 8 tests covering all 7 AC2 scenarios:
- loading skeleton rendered
- OfflineState on error
- OfflineState when data undefined
- delivery counts from mock data
- spend 'No budget cap set' when cap null
- spend destructive styling when cap non-null and over_budget true
- speed 'No data yet' when lead time null
- speed formatted value when lead time present
pnpm lint: 0 errors pnpm typecheck: 0 errors
pnpm test: 8/8 pass coverage: stmts 95% branches 90% fns 91% lines 95%
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
The self-heal origination tests blind-inserted the fixed-uuid foundation
system agent (and a main-pm agent). In the full CI suite the app lifespan
seeds + commits those agents first, so the insert hit a duplicate-key on
pk_agents — green in isolation, red in CI. Get-or-create both (by id / by
slug) so the tests pass whether or not the agents already exist. Verified
against the real ordering (an app-lifespan integration test before this
file): 187 passed.
All three compose files must carry the same app env; the registry one had
external-PR but was missing the self-heal block. Add it (identical to the build
compose) so docker-compose.yml / .yaml / .registry.yml agree — the registry
file still differs only in image source (registry pulls) and host-env paths.
Both compose files are git-TRACKED, and Docker picks docker-compose.yaml over
.yml — so the NAS reads the .yaml. The self-heal env (and prior changes) had
only been committed to the .yml, leaving the tracked .yaml stale in the repo.
Commit the .yaml so both carry the same config. Going forward: commit BOTH.
It's a monorepo registered as three cell-projects (BE/FE/UXUI) sharing one
git_url, so the CI signal is identical whichever is named; point self-heal at
roboco-api. Overridable via ROBOCO_SELF_HEAL_PROJECT_SLUG. Mirrored into the
untracked .yaml.
Plumb the self-healing knobs into the orchestrator service so a NAS deploy is
configured without hand-editing env: both toggles default OFF (armed from
Settings -> Feature Flags), the CI signal is scoped to ci.yml (RoboCo has
several workflows, so the unscoped "latest completed run" would be
unreliable), and PROJECT_SLUG is left for the deployment to set (which
registered project IS RoboCo). Mirrored byte-identically into the untracked
docker-compose.yaml the NAS uses.
Two hardening fixes from the gap review:
- Optional self_heal_ci_workflow scopes the CI signal to one workflow file
(the workflow-scoped Actions endpoint). Without it, "latest completed run
across all workflows" could miss a red CI run masked by a later passing
workflow, or false-trigger on a non-CI workflow — unreliable on a
multi-workflow repo.
- The loop logs a warning when self-heal is armed but self_heal_project_slug
is unset, so a misconfiguration isn't mistaken for "all green".
Tests cover the workflow-scoped endpoint.
Adversarial review found the load-bearing invariant broken at the dispatch
layer: _dispatch_pm_work skipped only PR_REVIEW_SOURCES, so a PENDING
team=main_pm self_heal task (assigned_to=None, confirmed_by_human=False) was
routed to Main PM and spawned BEFORE the CEO approved it — the "never start
until you approve" promise didn't hold.
Fix: the PM dispatcher now also skips source='self_heal' while
confirmed_by_human is False (before the assigned/unassigned split, so it holds
either way); the task still shows in the panel so the CEO can see and approve
it. approve_and_start flips confirmed_by_human=True (the CEO's start IS the
human confirmation), so it dispatches normally afterward. Other sources are
unaffected.
Tests: a unit test that the dispatcher holds an unconfirmed self_heal task but
routes a confirmed one and ordinary tasks, plus a DB test that approve_and_start
flips the gate. (The readiness gate was deliberately not used — a blocker there
marks the task `blocked`; the dispatch skip leaves it cleanly PENDING.)
The CI telemetry call is the feature's only real-world I/O and was previously
exercised only through a fake source. Cover the GitHub Actions request shape
(/actions/runs, branch/status/per_page, auth) and response parsing, plus the
safe-None paths (missing token, GitHub error, no runs).
Add self_heal_enabled (detect + notify) and self_heal_originate_enabled (also
open fix tasks) to the panel feature-flags registry + card, so the loop can be
armed/disarmed from Settings instead of editing env. Effect is on the next
restart, like the other flags; the self_heal_project_slug target (which repo is
RoboCo) stays a deployment env setting.
Register _self_heal_loop alongside the other background loops (created in
start, cancelled in stop). It returns immediately unless self_heal_enabled, so
a standard deployment adds zero behaviour and makes no CI call; when on it runs
one engine cycle per interval and commits any opened fix task. A test pins the
default-off dormancy (no sleep / CI / DB when disabled).
Behind the second opt-in (self_heal_originate_enabled), a detected regression
also opens a fix task into RoboCo's own delivery lifecycle and STOPS: PENDING,
unassigned, confirmed_by_human=False, team=main_pm, source=self_heal, with
synthesized acceptance criteria and a self_heal_fp= dedupe marker. It rides the
normal dispatchers only once the CEO Approve-&-Starts it; the loop itself never
calls start / approve / merge / deploy.
- TaskService: SELF_HEAL_SOURCE, extract_self_heal_fingerprint, and
list_open_self_heal_tasks (the dedupe + open-cap basis)
- SelfHealEngine._originate: per-signal fingerprint dedupe, per-cycle and
rolling open-task caps, repo resolved to RoboCo's own project (notify-only
when it can't be resolved)
- 7 DB-backed tests including the never-start / never-approve invariant
The detect side of the self-healing loop, modeled on the strategy engine: a
pure assess() turns breaching telemetry samples into RegressionObservations
(with a stable per-signal fingerprint for later dedupe), and run_cycle() is a
no-op unless self_heal_enabled and otherwise only sends the CEO one
ack-notification per regression. Detect + notify only — it never originates,
starts, merges, or deploys; the telemetry source is injectable for testing.
6 unit tests.
First slice of the production self-healing loop: a read-only telemetry source
that watches RoboCo's OWN repo CI and normalizes the latest GitHub Actions run
conclusion into breach / no-breach samples for the regression detector. It
targets only the single project named by self_heal_project_slug — RoboCo
healing itself, never other/client repos; the org's repo-agnostic delivery flow
is untouched.
- config: self_heal_enabled / self_heal_project_slug / self_heal_originate_enabled
plus interval and open-task / per-cycle caps, all default-off
- GitService.get_latest_ci_conclusion: per-project Actions-run lookup (graceful
None on missing token / no runs / error; never raises into the loop)
- TelemetrySample + TelemetrySource contract + GitHubCITelemetrySource
- 5 unit tests
The first pass under-represented the release. Make the inbound-PR-review
scope explicit (real GitHub review, author allowlist, head-SHA re-review,
repo-aware polling, 22nd agent with its own image, migration 037); add the
PR-reviewer respawn-loop fix and supersede close-on-land hardening to
Fixed; and note the richer agent-facing RAG docs (new Prompter / Secretary
/ PR-reviewer role docs + the company layer) under Changed.
Bump the version to 0.6.0 across pyproject, the package, the config, and
the panel, and add the 0.6.0 CHANGELOG entry: inbound external/internal PR
review with a CEO decision queue and supersede, the panel feature-flags
card, the required-cells decomposition gate, the CEO-rejected
coordination-root deadlock fix, the panel UI pass, and registry-image
deploy.
Also refresh the locked dependencies, update the release-tag examples in
the README and deployment docs, and correct the package docstring's agent
count to 22.
The cell-pm role doc showed delegate with a nonexistent nested body={...}
and omitted covers_parent_criteria, so an agent following it would make a
malformed call and burn turns rediscovering the real shape. Both PM docs now
match the actual flow_server.delegate signature (flat keywords, with
covers_parent_criteria; the subtask inherits the parent's project — resolved
from the product cell→project map for coordination roots, never passed).
Also document reassign (cell PM could call it but it was undocumented) and
cross-link the two roles: cell-pm explains submit_up hands finished work to
Main PM; main-pm explains the receiving side — the integration-branch chain,
that its complete on the root opens the master PR, and that only Main PM and
the CEO act on master.
The business-workflow chapter said neither capability has a panel switch —
no longer true once the Feature Flags card shipped. Reframe both as
panel-toggleable (effect on next restart), with the env vars kept as the
same toggles at the source plus the bits the panel doesn't surface (the
research provider and its server-side API key).
Real-Postgres round-trips for the external/internal PR-review TaskService
helpers that the existing mock tests can't prove actually persist:
- ingest_external_pr — create-once, head-SHA dedup (unchanged head skips),
and re-review on a new head; internal_pr source wording;
- pr_review_claim / complete_review — the planless, branchless
pending -> in_progress -> completed lifecycle, with re-claim / re-complete
no-ops and the "complete requires in_progress" guard;
- create_supersede_umbrella / find_supersede_umbrella — created on the same
repo (not parented), idempotent lookup, non-review rejection, and the
pr=5-vs-pr=50 exact-marker disambiguation;
- list_external_pr_reviews — source isolation, the data-layer half of the
dispatcher contract (regular tasks never leak into the review queue).
Writes use flush (not commit) so the rollback-per-test fixture keeps each
case isolated from the others against the shared session-scoped test DB.
Add a Feature Flags card to the Settings page that toggles env-gated
subsystems (external/internal PR review, web research, strategy engine,
pitch provisioning, RAG auto-update, transcript pruning) directly from
the panel instead of hand-editing environment variables.
Flags persist in system_settings as 'true'/'false' and are overlaid onto
the live config singleton at startup; an unset flag keeps its
environment/config default. A toggle takes effect on the next backend
restart — no per-consumer re-routing.
Backend: FEATURE_FLAGS registry + bool validator + get_bool accessor on
SettingsService; feature_flag_effective_values and
apply_persisted_feature_flags; GET /settings/feature-flags; best-effort
startup overlay in the app lifespan.
Frontend: settingsApi.getFeatureFlags / setFeatureFlag and a
FeatureFlagsCard rendered full-width below the settings grid.
A coordination root (team=main_pm, product-linked, no repo) the CEO sends back
lands in needs_revision, but the dev dispatcher skips it (not a cell team) and
the closure path only handles paused parents — so it sat in needs_revision
forever. (NOT a foundation-spec gap: the spec already allows needs_revision ->
claimed for any role.)
- _dispatch_revision_coordination_roots: re-spawn the owning PM for a
needs_revision coordination root so it re-coordinates the revision (registered
in the dispatch loop after PM closure)
- _readiness_check_role_for_status: widen the dev-owned states (needs_revision,
verifying) to also accept cell_pm/main_pm for coordination roots — a pure
widening; normal code tasks stay dev/doc-only
- 16 unit tests (dispatcher decision + readiness widening)
Covers the inbound-PR read surface: list_open_prs normalization + fork/internal
classification (and the recent _fetch_open_prs/_normalize_open_pr refactor),
plus get_pr_diff's diff-media-type request — both with their safe-empty paths
on missing token / GitHub error. The DB-backed paths
(ingest/complete_review/pr_review_claim/supersede umbrella) are covered
separately.
Extend the inbound-PR reviewer beyond external/fork PRs to internal org-repo
PRs that bypassed the agent task-flow (a human-pushed branch). The org's own
in-flight integration PRs are skipped — a live task owns their branch and they
already pass QA + PM review — so the reviewer only flags off-process PRs.
- config: internal_pr_enabled (default OFF, like external_pr_enabled)
- PR_REVIEW_SOURCES = (external_pr, internal_pr); generalize dispatch, dedup,
the decision queue, the git-gate exemption, and supersede to both sources
- TaskService.active_task_owns_branch (skip lifecycle PRs) + ingest source param
with source-aware wording
- poll loop runs when EITHER flag is on; _ingest_pr_if_reviewable picks the
source per PR (external: flag+author-allow; internal: flag+not-task-owned)
- 11 unit tests (decision logic + branch-ownership)
- Kanban card: show the short 8-char task id (full id on hover) instead of the
full UUID, which was one unbreakable token that ran off the card edge
- PR Review queue: render an empty-state card instead of returning null when
empty, so the surface is always visible on the Command Center (matching the
CEO Approval Queue) rather than vanishing when there's nothing to decide
The companion to the prompt rule (60de3499): when the brief explicitly names
cells, the Main PM must create a subtask for each and not silently collapse one
into a neighbour. Records the named cells as a 'required_cells:' marker on the
parent's quick_context (no migration — same pattern as the other markers), and
adds a _pm_uncovered_required_cells_guard at i_am_idle that refuses to idle
while a named cell has no subtask. Inert when no parent carries the marker, so
legacy decompositions are never blocked (mirrors the AC-coverage guard).
TaskService.uncovered_required_cells + extract_required_cells + 7 unit tests.
- Settings: cards reordered to User Info / Appearance / Data & Refresh /
Transcript Retention / Notifications / Connection Info (the 3x2 grid)
- Journals: the page now fills the viewport; the agent list and the entry
detail each scroll inside their own panel — removes the page + list +
fixed-500px triple scrollbar (real layout, not bolted-on magic heights)
- Agent item: distinct per-team avatar with initials, clear selected/hover
states, truncation, focus ring (was a generic icon repeated on every row)
- Kanban: the board fills the viewport and each column's card list scrolls
inside it, so a full Done column no longer overflows down the page
- Projects: drop the misleading Workspace column — it read the legacy
per-project workspace_path (never set in the per-agent workspace model), so
it always showed 'No workspace'
The Start/Send buttons used items-stretch with a fixed-height button, pinning
a cramped button to the top of a tall textarea. Bottom-align the row, give
Start a real primary size and Send a proper square icon button, and cap the
textarea height so the composer reads as a deliberate input.
The PR-review queue only listed COMPLETED reviews and hid when empty, so while
a review was in_progress the panel showed nothing — no sign a review was
happening or where its findings go (the reviewer posts its change-request on
the PR itself). Add TaskService.list_external_pr_reviews (active reviews +
awaiting-decision, minus cancelled/decided/dismissed); the route uses it. The
panel card now shows active reviews with a 'Reviewing' badge and a link to the
PR where the change-request lands, and the Supersede/Dismiss actions only once
the review completes.
Decompose the single docs/how-to.md into an indexed 5-chapter guide under
docs/how-to/: README (index/pitch/teaser/TOC), 01 the company, 02 it starts
with you, 03 the cells build it, 04 the last call and the loop. Image/video
paths rewritten to ../, inter-chapter nav added, the Prompter-build screenshots
preserved. Adds a new chapter 05 — the business workflow: the charter, the
Cockpit, the Secretary, and the web-research / strategy-engine config toggles,
with the end-to-end 'feel it' walkthrough. README links repointed to
docs/how-to/README.md. All image links verified; reflow-check clean.
* [4c179e3a] Add git pull, fetch, and rebase backend endpoints (#190)
* [f966f772] feat(git): add pull, fetch, and rebase endpoints with integration tests (#185)
- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response schemas
- Add GitService.pull(), fetch(), and rebase() methods using _network_git_timeout()
- Add POST /api/git/pull, /api/git/fetch, /api/git/rebase route handlers
- Rebase detects conflicts via git diff --name-only --diff-filter=U and aborts cleanly
- Integration tests cover success path and GitCommandError→500 for all three endpoints
- Rebase conflict test verifies conflict=True with populated conflicted_files list
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
* [26e2b7af] test(git): add AsyncMock unit tests for rebase_onto_base conflict-state handling (#186)
New test_git_rebase.py covers three branches of rebase_onto_base:
- success path: rebase exits 0, returns rebased status, abort never called
- conflict path: non-zero exit → diff → abort → returns conflict+files
- resilience: both rebase and abort exit non-zero, still returns conflict dict without exception
All tests use AsyncMock with side_effect sequences to mock _run_git at the service-method level.
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
* [551b1dbf] Panel-wide frontend UI standardization and page fixes (#193)
* [1ec787b2] feat(panel): design-system sweep — full-width layouts, scrollbar fix, Secretary button, component audit (#188)
- Settings page: remove max-w-3xl, wrap cards in grid-cols-1 lg:grid-cols-2 two-column layout
- AI Providers page: remove max-w-5xl so AIRoutingCard fills available width
- Journals AgentList: replace ScrollArea with overflow-y-auto div to eliminate nested scrollbar
- Secretary chat input: add items-stretch to flex row so Send/Start button matches Textarea height
- Component audit: replace all raw <button>/<input>/hand-rolled badge spans outside components/ui/ with canonical Button, Checkbox, Badge variants across 15 files:
- ai-routing-card.tsx: ModeButton → Button, checkbox → Checkbox, badge spans → Badge
- self-hosted-section.tsx: eye-toggle → Button ghost icon-sm, badge spans → Badge
- journals/agent-item.tsx, communications/channel-item.tsx → Button ghost
- kb-search-bar.tsx, kb-filters.tsx → Button ghost
- kb-category-nav.tsx, git-log-panel.tsx → Button ghost
- communications/page.tsx (channel + group lists) → Button ghost
- projects/project-table.tsx, products/product-table.tsx → Button link
- git-branch-panel.tsx (local + remote lists) → Button ghost
- tasks/dependency-selector.tsx: Button ghost + Checkbox for visual indicator
- tasks/task-table.tsx: sortable header + expand toggle → Button ghost
- business/goals-tab.tsx: hidden button → Button
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [435b37b4] feat(metrics,notifications): URL-persisted tab state, semantic chart colors, humanized counts (#187)
- Notifications page: replace useState with useSearchParams/useRouter for
?tab= URL parameter (all/unread/pending, default: unread); Suspense wrapper
with skeleton fallback for SSR compatibility.
- Metrics page: split into Performance tab (Velocity + Task Status + Agent
Status + Team Health) and Token Usage tab (TokenUsageCostsSection) with
?tab= URL parameter (default: performance); Suspense wrapper; Refresh button
moved inside PerformanceTabContent; humanizeCount() helper applies K/M
suffixes to all MetricCard numeric values >= 1000.
- Chart components (usage-time-series, agent-usage, team-usage, model-donut):
replace var(--chart-N) CSS vars with explicit semantic hex colors —
#3b82f6 blue for informational, #f59e0b amber for warning/pending,
#22c55e green for success/healthy, #ef4444 red for error/blocked,
#a855f7 purple for supplemental.
pnpm lint and pnpm typecheck pass with zero new errors.
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
* [ccd256f4] Kanban mobile viewport: 375px layout, column navigation, 44px touch targets (#191)
* [ccd256f4] feat(kanban): mobile 375px layout with column navigator and 44px touch targets
- KanbanBoard: add activeColumnIndex state + mobile prev/next column
navigator (lg:hidden); existing horizontal-scroll layout hidden on
mobile (hidden lg:flex). Desktop DnD behavior unchanged.
- KanbanColumn: add optional className prop (cn-based) so mobile view
can pass w-full/sm:w-full to fill the viewport.
- KanbanCard: bump all action buttons to min-h-11 (44px) touch targets
(Assign, Pass, Fail, Move-forward).
* [ccd256f4] fix(kanban): change breakpoint from lg to sm for mobile/desktop layout switch
AC3 requires >=640px viewport to show multi-column layout (sm: breakpoint).
Previous impl used lg: (1024px), leaving 640-1023px in single-column mode.
Change:
- Mobile navigator div: lg:hidden → sm:hidden
- Desktop multi-column div: hidden lg:flex → hidden sm:flex
At <640px: single-column with prev/next navigator (375px mobile use case).
At >=640px: full horizontal-scroll multi-column layout (per AC3).
DnD behavior and all other layout unchanged.
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [23f02af4] Agents page On-Demand section + Board composition; Overview Quick Actions visibility + Team Health Intake/Secretary (#189)
* [23f02af4] feat(agents,overview): On-Demand section, Board composition fix, Intake/Secretary in Quick Actions + Team Health
- agent-definitions.ts: remove AgentRole.MAIN_PM from getBoardAgents
(Main PM has its own dedicated section; including it there was redundant).
Add getOnDemandAgents() that catches agents not in any standard team
(board/main_pm/backend/frontend/ux_ui/marketing) and not a standard cell
role — surfaces prompter/intake agents that the API may return.
- agents/page.tsx: import getOnDemandAgents; add a conditional
'On-Demand Agents' AgentGrid section (only rendered when the API returns
at least one matching agent, e.g. the Intake interviewer).
- quick-actions-bar.tsx: add 'Task Intake' button (→/prompter, Sparkles
icon) and 'Secretary' button (→/business?tab=secretary, Bot icon)
alongside existing quick actions so operators can reach on-demand agents
from the Overview in one click.
- team-health-cards.tsx: add OnDemandAgentCard sub-component (link card
with On-Demand badge) and render static cards for 'Task Intake' and
'Secretary' appended after the API-driven TeamHealthCard list, giving
them equal visual presence in the Team Health section.
pnpm lint and pnpm typecheck pass with zero new errors.
* [23f02af4] fix(agents,overview): QA revision — enum entries, QuickActions placement, On-Demand title, Board PR_REVIEWER
AC3: types/index.ts AgentRole enum adds PR_REVIEWER, PROMPTER, SECRETARY.
agent-selector.tsx ROLE_LABELS exhaustive Record updated accordingly.
AC4: command-center.tsx QuickActionsBar moved to after Team Health section,
before CEO Approval Queue and data-heavy grid rows — visible without
scrolling on a 900px viewport.
AC1: agents/page.tsx On-Demand AgentGrid title fixed to 'On-Demand'
(was 'On-Demand Agents' in prior commit).
AC2: agent-definitions.ts getBoardAgents adds explicit PR_REVIEWER inclusion
and uses inclusion-based getOnDemandAgents (PROMPTER|SECRETARY roles).
AC5: team-health-cards.tsx static OnDemandAgentCard implementation refined
with correct fallback rendering when no API team data.
AC6: pnpm lint and pnpm typecheck (src only) pass with zero new errors.
---------
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [b1c59206] Git page: Pull, Fetch, Rebase buttons wired to backend; Rebase destructive confirmation dialog (#192)
* [b1c59206] feat(git): add Pull, Fetch, Rebase operations to Git page with destructive confirmation dialog for Rebase
- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response types
- Add gitApi.pull(), gitApi.fetch(), gitApi.rebase() with mock stubs for /git/pull, /git/fetch, /git/rebase
- Add useGitPull, useGitFetch, useGitRebase mutation hooks with cache invalidation; exported via useGitOperations
- Add Pull (Download icon), Fetch (RefreshCcw icon), Rebase (GitGraph icon) buttons to GitActionsPanel
- Rebase button triggers AlertDialog with destructive confirmation before calling API
- Wire handlePull, handleFetch, handleRebase handlers in git-browser.tsx with toast feedback
* [b1c59206] fix(git): add destructive styling and branch name to Rebase AlertDialog
- Add className='border-destructive bg-destructive/5' to AlertDialogContent
so the dialog container has the required red-tinted styling (AC3)
- Update AlertDialogDescription to interpolate status?.current_branch so
the dialog body explicitly names the branch being rebased (AC3)
---------
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>
* [3f305ed9] Frontend: Fix git control contract, complete Secretary restyling, and apply polish (CEO revision) (#199)
* [72de8a65] fix(git): correct Pull/Fetch/Rebase types, API mocks, request fields, and toast handlers (#197)
- types/git.ts: GitPullResponse and GitFetchResponse now have current_branch,
has_changes, staged_files, unstaged_files, untracked_files, ahead, behind
(matching backend GitStatusResponse); removed nonexistent commits_received/
refs_updated/remote fields
- types/git.ts: GitRebaseRequest now uses target_branch: string (not onto?: string);
GitRebaseResponse now has conflict: boolean and conflicted_files: string[]
(removed branch/onto/commits_rebased); task_id made optional on all three
request types
- lib/api/git.ts: Updated mock returns for pull/fetch/rebase to match new types
- git-actions-panel.tsx: onRebase prop now (targetBranch: string) => void;
Rebase AlertDialog now contains an Input for target_branch; AlertDialogAction
disabled when targetBranch empty and passes the value to onRebase
- git-browser.tsx: handlePull and handleFetch toast references result.current_branch;
handleRebase accepts targetBranch, sends target_branch in payload, toasts
result.conflict and result.conflicted_files; no 'manual' task_id for any
pull/fetch/rebase operation
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [be6a17fc] feat(ui): design-system polish — chart tokens, KB aria-label, Kanban touch targets (#196)
- kb-search-bar.tsx: add aria-label="Clear search" to the clear (X) button
- model-usage-donut.tsx: replace hex CHART_COLORS with var(--chart-1)…var(--chart-5)
- usage-time-series-chart.tsx: replace hex stopColor/stroke with var(--chart-1)/var(--chart-2)
- agent-usage-chart.tsx: Bar fill hex → var(--chart-1)
- team-usage-chart.tsx: Bar fill hex → var(--chart-1)
- kanban-card.tsx: min-h-11 → max-sm:min-h-11 (44px touch target mobile-only, 3 buttons)
- secretary-tab.tsx: already compliant (Button + design-system tokens), no change needed
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>
* [d62036bd] Backend: Fix git endpoint schemas, add safety gates, and unit tests (CEO revision) (#200)
* [d0593fe3] feat(git): remove agent_id from schemas and add service-layer safety gates (#195)
- Remove agent_id field from all 9 git request schemas (GitCreateBranchRequest, GitCheckoutRequest, GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest, GitPullRequest, GitFetchRequest, GitRebaseRequest); agent identity comes from JWT auth context
- Make task_id Optional[UUID]=None in GitPullRequest, GitFetchRequest, GitRebaseRequest
- Add field_validator to GitRebaseRequest rejecting target_branch starting with '-' or equal to 'master'/'main'
- Add lightweight PullRequest, FetchRequest, RebaseRequest schemas for gateway layer
- Add dirty-workspace check to GitService.pull() (raises ValidationError if porcelain output)
- Switch GitService.pull() to --ff-only; raises ValidationError with diverged-branch message on non-zero exit
- Add master/main guard to GitService.rebase() for both head_branch and target_branch
- Update callers: routes/tasks.py (2x), services/task.py, tests/unit/services/test_git.py (2x)
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
* [a2f96961] Add role-gated rebase endpoint and unit tests (test_git_rebase.py) (#198)
* [a2f96961] feat(git): add role-gated rebase endpoint and unit tests
Add role-gate to POST /rebase restricting access to DEVELOPER and
CELL_PM roles; add master/main protected-branch guard to
GitService.rebase() before any git subprocess runs; add 4 unit tests
in tests/unit/services/test_git_rebase.py covering both
target-branch and head-branch REBASE_FORBIDDEN cases
* [a2f96961] fix(git): invert rebase role gate, add ownership check, schema validator, and missing tests
- _REBASE_ALLOWED_ROLES changed from {DEVELOPER, CELL_PM} to {CEO, CELL_PM, MAIN_PM}
so developers correctly receive 403 per AC1/AC2
- rebase_branch() now verifies task ownership for non-CEO PM callers: if task_id
is provided and the task is not assigned to the calling agent, returns 403/404
- GitRebaseRequest.target_branch gets a @field_validator rejecting '-' prefix
names and protected branch names (main, master, develop)
- GitService.pull() gains pre-flight safety gates: raises ValidationError
DIRTY_TREE when staged/unstaged changes exist, DIVERGED_BRANCH when
ahead > 0 and behind > 0
- test_git_rebase.py adds 9 new tests: pull() dirty-tree ValidationError,
pull() diverged-branch ValidationError, pull() success path, schema
validator for '-' prefix and protected names, and route-level tests
confirming HTTP 403 for DEVELOPER and HTTP 200 for CELL_PM on POST /rebase
* [a2f96961] fix(tests): add type annotations for tuple variables in test_git_rebase.py
mypy needs explicit tuple type annotations when assigning bare tuples
to variables used as mock side_effect return values — fixes var-annotated
error caught by the server-side quality gate
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* [94015c6d] Frontend R3: Fix legacy git taskId coercion + rebase placeholder + phantom fields (#204)
* [401ddb40] fix(git): remove phantom fields from GitPullRequest/GitFetchRequest and make task_id optional in write request interfaces; use taskId || undefined in git-browser.tsx handlers to avoid 422 errors when no task context is active (#201)
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [cca8d0c0] fix(git): fix rebase placeholder and surface backend error in toast (#202)
git-actions-panel.tsx: change rebase target_branch Input placeholder
from "e.g. main or origin/main" to "Remote ref (e.g. origin/HEAD)" so
no default branch name (main/master/develop) is suggested.
git-browser.tsx: import getErrorMessage from @/lib/api/client and use
it in handleRebase catch block instead of the hardcoded string "Failed
to rebase". getErrorMessage extracts the real detail from
AxiosError.response.data.detail and falls back to a non-empty generic
message, satisfying both the detail-surfacing and fallback criteria.
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>
* [1ea0fbcb] Backend R3: Relax legacy git schemas + fix integration tests (#206)
* [219c539b] Make task_id Optional in git request schemas and update service methods (#205)
* [219c539b] feat(git): make task_id Optional in git schemas and add None-guards in service methods
- GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest now have task_id: UUID | None = None
- commit_for_task, push_for_task, create_pr_for_task, merge_pr_for_task skip ownership/state checks when task_id is None and proceed to the git operation
- Added 16 unit tests in tests/unit/api/routes/test_git_optional_task_id.py covering schema validation and HTTP endpoint responses
- Added 4 integration tests in tests/integration/test_git_routes.py for no-422 behaviour
- All quality gates pass: ruff format, ruff check, mypy, pytest
* [219c539b] fix(tests): remove unused type-ignore comments, redundant cast, and invalid agent_id kwarg in git_optional_task_id unit tests
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
* [de95ce94] test(git): fix 3 rebase integration tests to use non-protected target_branch (#203)
- Add pm_git_client fixture (CELL_PM role) needed for the role-gated rebase endpoint
- Change target_branch from 'main' to 'develop' in test_rebase_success, test_rebase_conflict, and test_rebase_git_command_error
- Remove task_id from request bodies (optional field; random UUIDs trigger 404)
- Switch all 3 rebase tests to use pm_git_client instead of git_client
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
* chore: ruff format test_agent_image_registry.py (unblock quality gate)
---------
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-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>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
pr_reviewer was absent from GATEWAY_ENABLED_ROLES, so the spawn mounted no
tool-manifest and set ROBOCO_GATEWAY_ENABLED=false. The reviewer booted with
no flow verbs, could never claim its external-PR task, exited, and was
respawned on the same task every tick — an endless loop that burned tokens.
Add pr_reviewer to the set, plus a regression invariant asserting every
spawnable seeded role has a manifest (only the never-spawned roles —
prompter/secretary/ceo/system — may be absent) and a direct pr-reviewer-1
manifest test checking its claim_pr_review/post_pr_review verbs are present.
The panel half of the PR-review gate. A PrReviewQueue card on the Command
Center lists external PRs the org has reviewed and awaiting the CEO's call,
each with Supersede / Dismiss / View-on-GitHub. Hidden when empty.
- tasksApi.getExternalPrReviews / supersedeExternalPr / dismissExternalPr,
typed to the exact backend response shapes (GET /tasks/external-pr-reviews,
POST /tasks/{id}/supersede-external-pr, POST .../dismiss-external-pr).
- Mock mode returns [] so the queue hides and the actions are unreachable —
no mock-masked contract (the #194 trap avoided; verified FE paths/shapes
against the real routes + route ordering).
A notification can't be the gate: the reviewer is read-only and the CEO
decides what happens next. Backend for a real PR-review decision queue:
- post_pr_review now notifies the CEO (send_external_pr_reviewed_notification,
APPROVAL/HIGH, related_task_id) the moment a review lands — server-side
best-effort (the reviewer has no notify verb).
- TaskService.list_external_pr_reviews_awaiting_decision(): completed
external_pr reviews the CEO has neither superseded (confirmed_by_human) nor
dismissed (quick_context dismissed=1 marker).
- TaskService.dismiss_external_pr_review(): CEO declines → marker → leaves queue.
- GET /api/tasks/external-pr-reviews (PM+/CEO) and CEO-only
POST /api/tasks/{id}/dismiss-external-pr. Supersede already exists.
Panel queue wiring follows in the next commit.
The reviewer was one-shot: external_review_task_exists deduped on
(project, pr_number) only, so an external PR was reviewed exactly once ever —
a contributor pushing a fix never triggered a re-review (the review went
stale). Drive re-review off the PR's head commit instead:
- list_open_prs now returns head_sha (the change signal).
- ingest records the reviewed SHA as an external_pr_head=<sha> marker in the
review task's quick_context.
- external_review_task_exists is head-SHA aware: same SHA -> skip (unchanged);
new SHA -> open a fresh review (changed); no task yet -> first review;
legacy/markerless task or unknown SHA -> skip (never re-review on a guess,
so existing reviews don't re-fire after deploy).
No migration — reuses quick_context, like the supersede markers.
Multiple projects can map to ONE repo — a monorepo product's backend/
frontend/ux cells each have their own Project pointing at the same git_url.
The poll ingested per-project with a per-(project,pr) dedup, so one external
PR (e.g. #170 on github.com/rennf93/roboco) created one review task per cell
project — three identical reviews for the same PR.
Collapse active projects to one canonical project per distinct repo before
polling (_projects_one_per_repo, deterministic by slug so the pick is stable
across polls). A monorepo product now yields ONE review per external PR;
genuinely separate repos (multi-repo) each still get polled.
The build compose got ROBOCO_EXTERNAL_PR_ENABLED (default true) but the
registry compose's orchestrator env was missed, so a pull-based deploy would
have defaulted it off — inconsistent. Add the same external-PR block so both
deploy paths behave identically.
Wire the external-PR settings into the orchestrator env in both compose
files: ROBOCO_EXTERNAL_PR_ENABLED defaults to true (overridable via .env),
with require_human_confirm kept true and the poll-interval/allowlist knobs
documented inline. The config default stays off (safe for library use); the
deployment compose turns the read-only review on. The code default is
unchanged — only the shipped compose flips it.
Read-only: enabling this only discovers inbound external/fork PRs and posts
one change-request. The supersede that builds contributor code is still
CEO-triggered and human-confirm gated.
pr-reviewer-1 was the one agent with no dedicated image and no compose
builder service — it reused roboco-agent-base, which left it absent from the
compose files entirely (so it looked like the PR reviewer simply was not
there). Make it first-class for parity: add docker/agent-pr-reviewer.Dockerfile
(FROM the base — read-only reviewer, no extra toolchain), an
agent-pr-reviewer-image builder service in both compose files and the registry
compose, the image in the release workflow's publish list, and map
pr-reviewer-1 -> roboco-agent-pr-reviewer in the orchestrator plus its
lazy-build dockerfile map. Supersedes the earlier base-reuse mapping.
scripts/reflow_md.py joins hard-wrapped markdown prose to one line per
paragraph, leaving code fences, tables, headings, blockquotes, and front
matter byte-identical, and refusing to touch any file whose non-whitespace
token sequence would change (so it can only ever edit whitespace). Add
`make reflow-docs` (apply) and `make reflow-check` (fail if any in-scope doc
is hard-wrapped), and wire reflow-check into `make quality` so CI keeps the
manual ~75-char line breaks from creeping back in.
Markdown and editors soft-wrap on their own, so the manual ~75-char line
breaks across the docs added nothing but noise. Join wrapped prose, list
items, and paragraphs into single lines across 67 docs — README, CLAUDE.md,
deployment, usage, the RAG knowledge base, and the agent role prompts.
Whitespace-only: code fences, tables, and blockquote alerts are byte-identical
and the change is token-verified (no content altered). Applied with a
deterministic reflow tool (committed separately).
Also lands two doc edits that were awaiting commit: the measured under-load
resource numbers in usage.md and the pr_reviewer additions to the
org-structure RAG doc.
pr-reviewer-1 was the only role without an AGENT_IMAGES entry, relying on
the silent base-image fallback — which reads as a missing image. It is a
read-only reviewer (diff via API, grep, post one change-request; never runs
code), so the base image is the right fit. Map it explicitly to
AGENT_BASE_IMAGE so the choice is visible; no new image is built or
published. Behavior unchanged (get_agent_image still returns base).
The verb table predates both roles. Add pr_reviewer (give_me_work,
claim_pr_review, post_pr_review — read-only reviewer) and secretary
(human-only, i_am_idle only), note their content-tool restrictions, and
correct the canonical-source reference to lifecycle.intents_for_role.
A panel-wide UI task that asked for the UX/UI and Frontend cells was
decomposed into Backend + Frontend only — the Main PM judged the UI work
collapsible into Frontend and dropped UX/UI entirely. Its prompt gave full
cell discretion ('you decide which cells', 'most roots only touch one
cell') with no rule to honor cells the brief names.
Add a rule: when the brief, acceptance criteria, or PO/HoM handoff
explicitly call for a cell, the Main PM must create a subtask for each
named cell and never collapse one into a neighbour — its discretion covers
only un-named scope; a genuinely-unnecessary named cell must be confirmed
via escalate_up/dm, not silently dropped. Matching anti-pattern added.
The companion finding (cross-dev sequencing) needed no change: the Cell PM
prompt already keeps dependent units in one dev's lane and splits only
independent units, and cross-cell order is enforced by the dependency_ids
gate, not sequence.
README + deployment guide now show three ways to run RoboCo — pull the
pre-built images (docker-compose.registry.yml), build from source, or the
local-dev flow — instead of only the dev path. Add the ROBOCO_REGISTRY /
ROBOCO_VERSION knobs and the required ROBOCO_ENCRYPTION_KEY (which was
missing) to .env.example, and fix a stale piragi reference there.
usage.md drops the unmeasured per-agent RAM ceiling and records the figures
measured on the live stack: RAM is low and Ollama-dominated, storage is the
real cost (images share the agent-base layer). Idle numbers — peak under an
active task is best read live with docker stats.
The orchestrator spawned agents only by bare image names and built any
missing image from source on the host, so a deployment had to carry the
build context and a toolchain — there was no way to just pull and run the
images the release workflow publishes.
Add two settings (default empty = unchanged local-build behavior):
ROBOCO_AGENT_IMAGE_REGISTRY and ROBOCO_AGENT_IMAGE_TAG. When a registry is
set, the orchestrator spawns and ensures {registry}/roboco-agent-*[:tag] and
pulls (never builds) any image it lacks. Also adds the previously-missing
agent-secretary image to the lazy-build map.
Ship docker-compose.registry.yml: a standalone compose that pulls every
published image (GHCR or Docker Hub, pinnable version) and wires the
orchestrator to spawn the matching pre-built agent images. The existing
build compose files are unchanged.
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.
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.
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.
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).
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).
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.
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).
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.
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).
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.
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.
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).
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.
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.
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.
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.
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.
* [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>
* [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>
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.
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).
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.
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.
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.
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.
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).
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.
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.
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.
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
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.
* 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>
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.
- 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.
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].
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.
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).
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.
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.
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.