Commit Graph
1070 Commits
Author SHA1 Message Date
Renn F 037114338a fix(x): release-post drafts stop parroting changelog bullets
Highlights reorder to marketing order (Added/Changed before Fixed/Security
— the drafting model anchors on highlight #1 and the changelog opens with
Security), the prompt bans verbatim highlight copying and internal plumbing
jargon, and the deterministic fallback becomes a generic announcement that
can never quote a raw bullet.
2026-07-25 03:04:35 +02:00
roboco-app[bot]andGitHub 29243771e4 sync: master → slave 2026-07-25 01:02:49 +00:00
RoboCo Release Manager 18eaacb9ea chore(release): 0.27.0 v0.27.0 2026-07-25 00:38:57 +00:00
Renn F e5fb976069 chore(panel): prettier reflow of the usage mocks 2026-07-25 00:50:07 +02:00
Renn F 902fc9a78c chore(models): finish the opus-5 replacement — no stale opus-4 fixtures remain
Every fixture, comment example, and panel mock that presented an Opus 4.x
id as current now carries claude-opus-5 (or the current sonnet/haiku ids in
the panel usage mocks). The only deliberate claude-opus-4 survivors are the
pricing-table fragment and its tests — they price the historical usage rows,
which would otherwise re-read as $0.
2026-07-25 00:49:13 +02:00
Renn F 3c87596685 chore(models): move the opus alias to claude-opus-5
Claude Opus 5 released today — same $5/$25 sticker, 1M context; Opus 4.8
moves to legacy. The pricing table gains a dedicated claude-opus-5
fragment (the claude-opus-4 substring doesn't cover it, so without the
row the fleet's Opus usage would silently cost-track as $0 — exactly
what test_opus_is_priced now guards).
2026-07-25 00:41:13 +02:00
Renn F 9b407d764d fix(ci): docs-redirects-only commits fire a CI run so the release gate gets a verdict
The push/pull_request paths filters covered docs/panel/motion but not
docs-redirects/**, so a redirect-stub commit landing as the slave tip
produced no CI run and the fail-closed release-readiness gate read
"unknown" — blocking every release proposal until unrelated code landed.
2026-07-25 00:13:16 +02:00
Renn F 803638e8e9 chore(models): upgrade the opus alias to claude-opus-4-8
MODEL_MAP["opus"] moves off claude-opus-4-6 to the newest Opus tier at the
same price; pricing already matched via the claude-opus-4 fragment, and a
new test_opus_is_priced guard keeps the alias priced. Fixtures, panel
mocks, and docs follow.
2026-07-25 00:13:15 +02:00
Renn F f90caf6f9b docs(redirects): point the old communications-and-journals URL at its renamed target
The docs site renamed the page to journals-and-notifications when the
channel/session comms subsystem was removed; the stub still redirected
the old Pages URL to the dead path, landing visitors on a 404.
2026-07-24 22:38:47 +02:00
2a9339225c docs(release): 0.27.0 prep — changelog through Wave C, map + rag + CLAUDE.md current (#697)
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 22:23:20 +02:00
8f01446243 chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths (Wave C-2) (#695)
* feat(panel): promote project settings to a full page

The edit-project dialog carried ~30 fields across 7 concerns in one
flat scroll with a per-tab width swap — outgrown. Project settings now
live at /projects/[id]/settings as a card-per-concern grid (the
settings page's own pattern) with per-card save and Conventions as a
page-level tab at natural width; the list Edit action routes there,
and a slim quick-edit dialog (name/cell/active) replaces the
kitchen-sink.

* chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths

collapsible-section moves to ui/ as the single sectioned-disclosure
primitive (task dialogs' raw Collapsible and create-project's ad-hoc
showAdvanced converge onto it); every hand-rolled dialog footer becomes
DialogFooter; dialog widths collapse from ten ad-hoc classes to three
named sizes, with deliberate outliers annotated. No behavioral change.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 22:02:17 +02:00
646be2351e feat(panel): promote project settings to a full page (#696)
The edit-project dialog carried ~30 fields across 7 concerns in one
flat scroll with a per-tab width swap — outgrown. Project settings now
live at /projects/[id]/settings as a card-per-concern grid (the
settings page's own pattern) with per-card save and Conventions as a
page-level tab at natural width; the list Edit action routes there,
and a slim quick-edit dialog (name/cell/active) replaces the
kitchen-sink.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 22:00:16 +02:00
roboco-app[bot]GitHubroboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>Frontend Developer 1Frontend DocumenterFrontend Developer 2Renn FRenzo F
9d39005c58 [73275ff0] Panel consistency & UX wave: forms audit, command palette, kanban merge, responsiveness (#694)
* [170c9578] Frontend: Panel consistency & UX wave (forms audit, command palette, kanban merge, responsiveness) (#688)

* [f1957610] Stream1-A: Project form sync (#667)

* [f1957610] feat(panel): expose codegen_command in create-project dialog

Add the Codegen Command input to create-project-dialog.tsx, mirroring
the field already present in edit-project-dialog.tsx. All other
fields named in this task (git_provider, github_installation_id,
environments, protected_branches, video_engine_enabled,
monthly_budget_usd with gt=0 client validation, sandbox_extensions)
were already implemented on this branch's base by prior work, and the
ProjectCreate/ProjectUpdate types in types/index.ts already match the
backend ProjectCreateRequest/ProjectUpdateRequest schemas exactly --
no further changes were needed there.

* [f1957610] docs(forms): add project-fields-audit reference for future field consistency

Create a living audit of which project configuration fields are exposed in the create vs. edit dialogs, mapping to the backend ProjectCreateRequest/ProjectUpdateRequest schemas. This serves as a future reference to prevent field-sync gaps and documents the intentional asymmetry (create focuses on git setup, edit adds autonomy/maintenance toggles). Includes a checklist for adding new project fields in the future.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [515697f4] feat(panel): settings save feedback + forms-audit.md living reference (#669)

Add per-toggle confirmation toasts to the four Settings-page prefs
(notifications, sound, auto refresh, refresh interval) so an immediate
write is never indistinguishable from a silent failure. These prefs
stay on the already-shipped client-persisted useUIStore pattern
(CHANGELOG.md "Settings preferences persist as real client prefs
instead of 422-ing as theater") rather than settingsApi, since the
backend _VALIDATORS allowlist deliberately excludes them and the
parent task scoped this stream as needing no backend schema changes.

Check in docs/forms-audit.md: a living form x field x verdict table
covering Stream1-A (project dialogs), Stream1-B (task dialogs), and
this settings work, with a header note that future backend schema
changes require a row update. Fixes the project-slug help text
(now correctly says letters/numbers/hyphens, not just hyphens).

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

* [80a215a4] Stream2-A: Command palette component (#670)

* [80a215a4] feat(panel): Cmd+K command palette component

Radix Dialog + combobox pattern searching tasks/agents/projects/pages,
localStorage recents under roboco-cmd-recents, keyboard nav (arrows/
Enter/Escape), mounted globally in the dashboard layout.

* [80a215a4] fix(panel): restore fields dropped from ui-store.ts by prior merge

Stream1-C's merge stripped notificationsEnabled, soundEnabled,
autoRefresh, refreshIntervalSeconds, a2aContextOpen, quickActionIds,
productsView, and projectsView from the shared UI store, breaking
typecheck for settings/quick-actions/products/projects/a2a/notification
consumers repo-wide. Restored per already-committed tests + consumers.

* [80a215a4] docs(panel): add command palette reference guide

Documents the global Cmd+K search feature: usage (keyboard shortcuts, search categories, recents), architecture (CommandPalette component, useCommandPalette hook, fuzzy-match and recents helpers), data flow, and verification against live API data.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [cdc371d1] Stream4-A: Responsiveness audit and fix — wide-content pages (#671)

* [cdc371d1] fix(panel): bump Button sm size to 36px touch-target floor

Button's size="sm" variant was h-8 (32px), used as literal row-action
buttons on the overview page's CEO Approval/PR Review queues and other
controls across settings/metrics/agents/a2a. Bump to h-9 (36px) to meet
the touch-target floor everywhere at once, keeping the smaller
horizontal padding/gap intact for visual density.

* [cdc371d1] fix(panel): make AlertDialog scroll its body at short viewport heights

AlertDialogContent lacked the max-h-[85vh]/overflow-y-auto that the
sibling DialogContent already has, and AlertDialogFooter lacked
DialogFooter's sticky bottom-0 pinning. A tall description at a short
viewport height (mobile landscape) could clip the action buttons off
screen with no way to reach them. Affects the settings page's
GitHubAppCredentialsCard/FeatureFlagsCard confirm dialogs (and every
other AlertDialog app-wide). Ports DialogContent's already-solved
scroll pattern onto AlertDialogContent/Footer.

* [cdc371d1] fix(panel): wrap Scorecards Members table in ResponsiveTable

metrics/scorecards-tab.tsx's 9-column Members table was a bare
&lt;Table&gt; with no mobile-card fallback, unlike its sibling tables in
the same file (Rework, SpawnWaste) and sessions-table.tsx, which
already use the established ResponsiveTable wrapper. Add a MemberCard
component and wrap the table so it stacks as cards below md instead of
forcing a cramped in-card horizontal scroll on a 375px viewport.

---------

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

* [b25fca69] Stream2-B: Header integration for command palette (#675)

* [b25fca69] Wire header search into Stream2-A command palette: click trigger via useUIStore.setCommandPaletteOpen, remove disabled input and Coming Soon tooltip remnants

* [b25fca69] Wire header search into Stream2-A command palette: click trigger via useUIStore.setCommandPaletteOpen, remove disabled input and Coming Soon tooltip remnants

---------

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

* [e4ce5b9a] Stream3-A: Tasks page List|Kanban tabs + kanban embed (#674)

* [e4ce5b9a] feat(tasks): add List|Kanban tabs to tasks page sharing URL filter state

Add top-level List|Kanban Tabs above the tasks page filter bar. List
tab renders the existing TaskFilters+TaskTable unchanged; Kanban tab
embeds the existing DevKanban/QaKanban/PrReviewKanban/PmKanban views
via nested sub-tabs (dev/qa/pr-review/pm), mirroring the standalone
/kanban page's own tab styling (tooltip-wrapped triggers, pickTab
helper). Both tabs read/write `tab`/`view` query params through the
page's existing updateParams pattern, so all filters persist across
tab switches. The four kanban view wrappers gain an optional
controlled team/onTeamChange pair so the team filter is shared
bidirectionally with the List tab's team filter, while staying
backward compatible (uncontrolled, initialTeam-only) for the
standalone /kanban route. KanbanBoard's dnd-kit drag-and-drop and
mobile single-column navigation are untouched.

* [e4ce5b9a] docs(tasks): add tasks-page-tabs.md documenting List|Kanban tab structure and shared filter state

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [18c52802] feat(panel): redirect /kanban to Tasks kanban tab, remove sidebar entry, swap bottom tab bar to Agents (#679)

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

* Stream4-B: Responsiveness fixes — remaining dashboard pages (#676)

* [fee25542] fix(a11y): bump sub-36px icon-sm touch targets to 36px on remaining pages

Both kb-search-bar.tsx (Knowledge Base search clear button) and
self-hosted-section.tsx (Settings token show/hide button) used
Button size="icon-sm" (32px) for an absolutely-positioned input
adornment, below the 36px minimum touch-target size. Bumped both
to size="icon" (36px, matching the 36px input height) and adjusted
the absolute-position offset so the button still sits fully inside
each input's existing right padding reservation.

Audited every remaining dashboard page (everything Stream4-A's
wide-content/table fixes didn't already cover): no un-wrapped wide
tables remain (every <Table> already rides ResponsiveTable), and
every DialogContent across the repo already inherits or supplies
max-h-[*vh] + overflow-y-auto, so dialogs stay usable at small
viewport heights.

* [fee25542] fix(a11y): re-land sub-36px touch target and overflow fixes after sync_branch reset them again

Re-applies the fda2ac0c fix content a third time -- sync_branch's
rebase+force-push reset the branch and working tree back to the stale
f9f45d9f ref (the round-1-only state) instead of preserving the local
commits ahead of it, discarding the round-2 fix yet again.

- quick-actions-card.tsx (Overview dashboard customize dialog): reorder
  arrows drop the h-6 w-6 override, falling back to Button's 36px icon
  default
- agent-card.tsx (Agents page grid): DM / dedicated-chat / actions-menu
  icon buttons drop their h-6 w-6 override, now 36px
- product-card-grid.tsx / project-card-grid.tsx (Products/Projects card
  view): edit/external-link icon buttons drop h-6 w-6, now 36px
- environment-ladder-editor.tsx (Edit Project dialog): move-up/move-down/
  remove-rung icon buttons drop their h-6/h-8 overrides, now 36px; the
  per-rung row now scrolls horizontally within its own bordered box
  (overflow-x-auto + min-w-max) instead of overflowing at 375px now
  that the icon buttons are back to full width
- acceptance-criteria-editor.tsx / dependency-selector.tsx (task create/
  edit dialogs): remove-row icon buttons drop their h-6/h-8 overrides,
  now 36px
- tab-commits.tsx: "Linked Commits" header's fixed 3-column grid now
  stacks to one column below sm, and the branch/PR badge row scrolls
  horizontally in its own container instead of the page

panel lint + tsc --noEmit are both clean.

* [fee25542] fix(a11y): bump tab-commits.tsx delete-commit button to 36px touch target

The per-commit unlink button used className="h-7 w-7" (28px), the one
sub-36px target the prior re-land commits (fda2ac0c/8e00303f/d8408c77/
34ea8fe1/e6bee7c8) didn't cover -- their content only fixed the header
grid-stack overflow in this file, not this button. Bumped to h-9 w-9
(36px) matching the Button component's own size="icon" default used
everywhere else in this fix series, and bumped the icon from h-3 w-3
to h-3.5 w-3.5 to stay visually proportional at the larger target.

panel-gate (lint + tsc --noEmit + vitest) is green.

* [fee25542] fix(a11y): remove trailing narrative JSX comments in self-hosted-section.tsx

Removes the 9 {/* ... */} comments flagged by the conventions validator's
no_inline_comments rule (F-8db499ac) — Header, Base URL input, Auth token
input, Test Connection button, inline result badge, and the three empty-
state section markers. Each block is already self-evident from its JSX
composition (distinct Input/Button/Badge groupings and conditional guards
showNoUrlState/showErrorState/showNoModelsState/showModelList), so no
docstring/JSDoc replacement is needed. Pure deletion, no behavior change.

The two remaining open findings (F-8a39564c, F-4308219d) allege the
touch-target/overflow fixes across 8 files are missing from this branch --
re-verified via roboco_git_log(branch=<task branch>), which reads the real
ref directly, that the branch tip is c629c3e1 and already contains those
fixes (h-9 w-9 delete button + overflow-x-auto header row in tab-commits.tsx,
overflow-x-auto rung rows in environment-ladder-editor.tsx, no shrunk
icon-sm/h-6/h-7 overrides left in the other 6 files), confirmed by reading
every file on disk in this worktree. No code change needed for those two;
resolved via verification evidence instead of a 9th re-land.

pnpm lint + pnpm typecheck both clean.

---------

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

* [1ff154da] restore(panel): re-add AutoRefreshDriver and ScrollJumpButtons to dashboard layout (#691)

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

* [6ee71578] Add sequence field to task dialogs; fix pr_gate forms-audit.md findings (#693)

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

---------

Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [73275ff0] fix(panel): review follow-ups — budget help text, touch-visible unlink, docs consolidation

- The task budget help text and validation toast said empty falls back
  to a task-type default; that default table was removed (null = no
  cap) — both strings now say so, and the forms audit row documents
  the correction instead of claiming ok over a stale label.
- The per-commit unlink button was hover-revealed only, invisible on
  touch devices and to keyboard focus; it now also reveals on
  focus-visible and coarse pointers.
- The forms audit moves from the docs root into the governed
  docs/frontend/forms/ tree, cross-linked both ways with the project
  fields reference it overlapped, and both are registered in the
  frontend docs index; the tasks-page-tabs doc's standalone-reuse
  rationale now states the /kanban redirect reality.
- The two 20px tree-expand chevrons in the tasks table are left as-is
  deliberately: explicit dense-row overrides, where a 36px target
  would break table density.

---------

Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Renzo F <45401804+rennf93@users.noreply.github.com>
2026-07-24 19:05:29 +00:00
987eb09c78 fix(workspace): role-aware worktree refresh at every spawn (#692)
* fix(workspace): refresh a present per-task worktree at every respawn

ensure_worktree_self_heal treated an already-present worktree as a pure
no-op (venv-link + chown only), so a worktree created once at first claim
or first claim_review stayed frozen at that commit across every later
respawn even as new commits landed on origin — the root mechanism behind
a live multi-round QA/PR-gate bounce loop, where the reviewer kept
re-examining its own stale round-1 checkout.

_ensure_worktree_before_spawn now classifies the caller's role
(WORKTREE_AUTHOR_ROLES: developer/documenter, mirroring the gateway
commit tool's RBAC) and _refresh_present_worktree compares local HEAD
against origin/<branch>: behind-or-equal fast-forwards for every role
(never discarding an author's uncommitted edits to do it); strictly
ahead is always left alone; diverged only resets for a pure reader,
whose local history can never be anything but a stale prior-round
checkout.

conventions_check_for_task's list-vs-content gap (list from git objects,
content from the physical worktree) is closed as a side effect: the
reviewer's worktree is now current as of spawn, and the branch under
review gains no further commits while it sits in awaiting_pr_review.

* fix(workspace): refresh re-added worktrees; fail the dirty guard toward preservation

- A pruned worktree re-added from a surviving local ref now runs the same
  fetch-and-classify refresh as a present one, so an evicted reviewer
  worktree cannot resurrect a stale checkout.
- A failing git status reads as dirty, never clean: the guard that
  protects an author's uncommitted edits fails toward preservation.
- The hard reset verifies the worktree is actually on the task branch
  first; a detached or drifted worktree is left alone with a warning.
- The conventions-check docstring states the remaining second-claim
  ceiling instead of claiming full closure.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 20:29:20 +02:00
e97f46af6e fix(git): reviewer reads must prefer origin over a diverged local ref (#690)
* fix(git): reviewer reads must prefer origin over a diverged local ref

_resolve_head_ref (GitService.diff/list_changed_files/read_file_at_branch)
kept local priority on ANY divergence from origin, real or rewritten. A
reviewer's clone parked on pre-rebase history after the branch's routine
force-push sync stayed frozen there across every subsequent review round,
while origin held every fix commit — QA repeatedly bounced work that had
already landed. Every caller here is a reader, never the branch's own
author mid-write, so origin now wins whenever it carries anything the
local ref lacks; local keeps priority only when it strictly contains
origin (unpushed commits, or equal).

The read-only git MCP surface (roboco_git_log) hit the same staleness
through a separate path: /api/git/log resolved the requested branch as a
bare name straight off whatever the caller's own clone had on disk, with
no fetch at all. It now routes through the same fixed _resolve_head_ref.

* test(e2e): give the armed flow-verb timeout real headroom

The armed value is also verb-2's entire execution budget (claim + every
claim guard + set_plan + start + tracing gate), which grows as guards
land; 1s flaked on loaded CI runners while passing locally. The
cancel-and-release semantics only need the timeout far below the hang.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 17:50:13 +02:00
eb0dcb6ecb fix(orchestrator): task-scoped oscillation breaker for escalate/unblock ping-pong (#685)
* fix(orchestrator): task-scoped oscillation breaker for escalate/unblock ping-pong

An escalation ping-pong oscillates a task between two agents (cell PM
escalate_up -> BLOCKED -> main PM unblock -> restored -> respawn ->
escalate again). The per-(agent, task) respawn gate never trips on it:
the restored side is dispatched by _dispatch_claimed_without_agent,
which consults no respawn counter at all, so one side of the round trip
always has fuel regardless of the other's strikes — and even a tripped
main-PM counter only stalls the task silently at blocked instead of
surfacing the oscillation.

- Strikes are counted task-scoped at the unblock() chokepoint
  (agent-agnostic; legitimate needs_revision rework never calls
  unblock, so it structurally cannot trip this), durable in the
  existing orchestration_markers column — no migration.
- Progress between round-trips (commits / revision_count advancing)
  resets the count: real forward motion is not an oscillation.
- On trip: the task is blocked with a HUMAN resolver (the budget-breach
  posture), both dispatchers stop respawning onto it, further unblock()
  refuses until an admin override clears the marker, and the CEO
  notification names both agents and the cycle count.
- _notification_has_live_work now treats a HITL-blocked related task as
  no live work, closing the same loop for the admin-route escalation
  path.

* fix(orchestrator): wire the oscillation trip to the dispatchers and make recovery reachable

- TaskResponse serializes blocker_resolver_type: the dispatchers' HITL-blocked
  skip and the notification-path live-work check now actually fire over the
  wire instead of only against in-process rows.
- The oscillation marker clears on every human transition out of BLOCKED
  (snapshot or not), and the human unblock route treats a tripped task as
  the requested intervention: clears the marker and proceeds, while the
  agent gateway verb keeps refusing.
- The progress fingerprint includes the terminal-children count, so a
  coordination root whose children advanced between escalations resets
  instead of accruing toward a false trip.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 17:20:20 +02:00
23ae0ca217 fix(gateway): covers_parent_criteria hint that teaches the shape; CEO pause/resume (#686)
* fix(gateway): teach the delegate remediate + PM prompt the covers_parent_criteria shape; allow CEO through the plain pause route

- A child draft rejected for missing covers_parent_criteria now gets a
  copy-pasteable corrected skeleton with the parent's real criteria
  inlined, and the PM delegation guidance shows the field as part of
  every child draft — a PM no longer loops on a rejection that named
  the field but never showed the shape.
- The plain pause route now authorizes the CEO tier like its sibling
  lifecycle routes; agent-side pause restrictions are unchanged.

* fix(gateway): delegate-coverage hint heals and degrades on legacy parents

- The coverage-reject path self-heals a criteria-bearing parent whose
  ids are empty or out of length before rendering the hint, so the
  skeleton always shows real references; the renderer itself also
  falls back to quoted criterion texts for any criterion without an
  id instead of emitting a placeholder or truncating the listing.
- The remediate names both legal reference forms (id or exact text)
  again.
- Route comments state the pause/resume check as deliberately
  CEO-only instead of claiming a precedent whose role set is wider.

* test(gateway): real TaskTable rows in the remediation hint round-trips

mypy over tests/ rejects a SimpleNamespace where unknown_ac_refs takes a
TaskTable; instantiating the ORM row directly needs no session and types
cleanly.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 17:10:20 +02:00
4b2546ae19 fix(findings): path-shaped file refs + per-round collapsible findings (#687)
* fix(findings): enforce path-shaped file refs; group panel findings by round

- The findings chokepoint rejects a file that is not a repo-relative
  path shape (prose like a PR reference validated before, and the panel
  then rendered a doomed file-content fetch for it) — narrative belongs
  in evidence, the remediate says so.
- The task-detail Findings tab groups findings into per-round
  collapsible sections (newest expanded) and only attempts a code
  snippet for a path-shaped file ref, so historical prose refs render
  as plain metadata instead of a broken loader.

* fix(findings): admit client-repo path conventions; teach the file-less option

- The shape gate reviews arbitrary client projects, not just this repo:
  plus and at-sign join the character class so SvelteKit route files,
  @types dirs, and @2x assets stay citable. Spaces stay excluded — they
  are the prose signal.
- The file-rejection remediate names the file-less option for
  cross-cutting findings.
- The client mirror notes its deliberate non-ASCII divergence from the
  server gate (unicode server-pass renders snippetless, fail-open).

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 17:09:49 +02:00
21910d75ea chore(board): revive dormant board wiring — research key, pitch flow, auditor playbooks (#684)
* chore(compose): pass research key/provider + provisioning token/org through to the orchestrator

ROBOCO_RESEARCH_API_KEY / ROBOCO_RESEARCH_PROVIDER and ROBOCO_PROVISIONING_TOKEN /
ROBOCO_PROVISIONING_ORG were absent from every compose environment stanza, so .env
values never reached the container: research silently ran on the NullProvider
(empty results forever) and any approved pitch died on ProvisioningDisabledError.
.env.example also falsely claimed the provisioning creds are panel-managed.

* feat(board): pitch CEO notification + auditor playbook-draft surfacing

A proposed pitch now nudges the CEO (APPROVAL notification + Telegram link to the
Pitches tab, best-effort — a send failure never fails the verb). auditor_triage
surfaces the oldest pending playbook draft once anomalies are clear — the curation
verbs were granted but nothing ever pointed the Auditor at the review queue; the
scheduled audit prompt names the discovery path.

* docs(prompts): pitch doctrine section + auditor reply-only-dm drift fix

board.md never mentioned the pitch verb, so no board agent ever had a reason to
call it — it gets a dedicated section mirroring the roadmap/spotlight ones, plus
a roadmap-exploration escape hatch (needs-its-own-repo ideas pitch instead).
product-owner.md gains its missing propose_roadmap + pitch entries. The flat
'Auditor has no dm' claims are corrected to the real grant: never initiates,
reply-only in a CEO-opened thread. Doctrine guarded by a prompt-content test.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 14:41:14 +02:00
71f5426e40 fix(git): never discard committed local work in rebase_onto_base (#683)
The shared rebase primitive (dev sync_branch verb, PM/CEO rebase path,
submit-freshen, merge-conflict resolver) opened with fetch -> checkout ->
unconditional reset --hard origin/<head_branch>. The dirty-tree gate
protects uncommitted edits only; the reset silently rewound past every
committed-but-unpushed commit — and the commit do-verb never pushes, so
mid-rework a dev routinely has exactly that. The force-with-lease push
then republished the truncated branch as authoritative (the lease
matched the freshly-fetched, never-moved origin ref).

rebase_onto_base now classifies local vs origin/<head> post-fetch:
- behind/equal: reset --hard origin as before (origin loses nothing)
- strictly ahead: reset skipped — the rebase runs from the local tip and
  the lease'd push publishes the previously-doomed commits
- diverged: a patch-equivalence probe (rev-list --right-only
  --cherry-pick) first rescues the self-inflicted residue of a prior
  rebase whose force-push failed (treated as ahead, self-heals on
  retry); only genuine two-sided divergence returns a new
  {status: diverged, local_only, origin_only} — no reset, no rebase,
  no push, neither side silently discarded
- an absent local ref is recovered from origin (branch + checkout,
  never reset)

Callers: the sync_branch verb maps diverged to an actionable envelope
steering to i_am_blocked (stash-preserved note included); the
submit-freshen hard-rejects it like conflicts; the merge-conflict
resolver already escalates any non-rebased/superseded status to the
CEO and degrades gracefully (pinned by test, no code change).

New real-git suite (bare origin + clone, no subprocess mocking)
asserts origin-side outcomes: ahead-publishes, behind-adopts,
diverged-refuses-untouched, absent-ref recovery, superseded,
conflicts, and wedge self-heal on retry via a rejecting pre-receive
hook.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 14:34:25 +02:00
3516d925fe fix(tasks): reconcile acceptance_criteria_ids at the update chokepoint (#682)
Every post-create rewrite of acceptance_criteria (task PATCH route,
prompter update_live_draft / _patch_batch_child / update_live_batch)
routes through TaskService.update()'s generic field loop, which
overwrote the criteria without touching acceptance_criteria_ids —
leaving ids mismatched or empty, and an empty id list silently
disabled the parent-coverage gate entirely.

- New pure _reconcile_ac_ids: one id per new criterion; text-unchanged
  criteria keep their id (children and findings reference criteria by
  id or exact text — a blanket re-mint would orphan every live
  reference), new/reworded text mints fresh, dropped criteria drop
  theirs. create() now stamps through the same helper (explicitly
  supplied ids still win).
- update() derives acceptance_criteria_ids whenever acceptance_criteria
  is rewritten without an explicit id list.
- The parent-coverage gate self-heals a criteria-bearing row whose ids
  are empty/out-of-length (re-stamp in place) instead of returning
  early and silently waiving coverage for the whole subtree.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 14:18:58 +02:00
c4ba351ae0 fix(sequencing): reachability-aware claim bar + sequence_held surfacing (#681)
Three coupled claim-path bugs from the 2026-07-24 live incident, fixed at
the shared root:

- The edge-agnostic sequence bar phantom-held a task behind an unrelated,
  never-connected same-parent sibling that coincidentally shared a lower
  raw sequence (stamp_wave_sequence stamps from a partial per-task view).
  _claim_blocked_by_sequence now branches on is_batch_root_subtask: a
  MegaTask root-subtask (globally-computed Kahn wave, a deliberate
  staged-release barrier) keeps the strict rule unchanged; every other
  same-parent context routes through the pure sequence_blocker_id, which
  only blocks on a real transitive predecessor via dependency_ids UNIONED
  with completed_dependency_ids. A task with no same-parent dependency
  edge at all falls back to the raw bar unchanged (#452 preserved).
- The hold surfaced as claim()'s bare None and was misdiagnosed by the
  verb runner as a concurrent-transition invalid_state. New
  sequence_hold_reason + a proactive _sequencing_claim_guard return a
  dedicated Envelope.sequence_held naming the blocker, on both the
  PENDING and NEEDS_REVISION reclaim paths.
- give_me_work offered tasks the claim gate then rejected: both offer
  paths (list_pending_for_agent, _drop_dependency_held) now consult the
  bar via the exact claim predicate (is_pending_claim_blocked, extended
  to NEEDS_REVISION).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 13:53:29 +02:00
a3f84f3165 chore(routing): retire haiku from delivery-lifecycle roles (#680)
Haiku can't reliably emit the structured envelopes the lifecycle now
runs on — pass_review's per-AC criteria_verified, delegate's
covers_parent_criteria, the findings ledger. A haiku QA/PM claims, gets
validation-rejected, idles, respawns, and loops without advancing a task
(2026-07-24 live: fe-qa on haiku looped four awaiting_qa tasks to zero
progress). The per-token savings (~2x under the Sonnet-5 promo, 3x after)
are dwarfed by the cost of a review that never completes.

Three coordinated changes: ROLE_MODEL_MAP's qa/documenter defaults move
haiku -> sonnet (the actual source of the live incident); the cost_tiered
developer:low -> haiku seed retires to empty (the floor would upgrade it
anyway); and a structured-verb capability floor upgrades any below-floor
Anthropic assignment to sonnet at resolution — from a pin, a ROLE row,
or a future map edit — in both the assignment and legacy paths.
Non-Anthropic providers are untouched (an Anthropic-tier floor, not a
provider policy). pr_reviewer/auditor stay on opus.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 12:14:13 +02:00
roboco-app[bot]andGitHub e1ae58489c sync: master → slave 2026-07-23 23:50:12 +00:00
dependabot[bot]andGitHub c2ab5998ea chore(deps): bump actions/labeler from 6 to 7 (#677) 2026-07-24 01:39:13 +02:00
08428208f8 chore(compose): wire the agent tool-call budget caps through the composes (#672)
ROBOCO_AGENT_TOOL_CALL_HALT/_WARN were read by the in-container SDK
server and defined in config, but reached no compose environment stanza
and no .env.example — the third dead-on-arrival env var of this class.
Live consequence (2026-07-23): the 300-call default halted the
responsiveness-audit dev twice mid-task ("Agent budget exceeded;
terminating container"), releasing and respawning it in 300-call slices.
Defaults raised to halt=600/warn=200 in the build compose (registry
compose passes them through unset), matching the already-patched NAS.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 00:44:32 +02:00
f8b4a6755c fix(pr-review): fleet PRs are ours by branch ownership, not author identity (#668)
With a GitHub App bound, fleet PRs are authored by <app-slug>[bot] whose
author_association is NONE — the inbound classifier's author heuristics
read that as an outsider and ingested the org's own dev-stream PR as
external_pr for adversarial review (2026-07-23 live: PR #667). The
repo-owner author check only ever covered the PAT era.

_ingest_pr_if_reviewable now skips any same-repo PR whose head branch an
active task owns BEFORE the author-based classification, and
active_task_owns_branch widens from the single polled project to every
project sharing its git_url (the poll collapses a monorepo's
cell-projects to one canonical project, so a sibling cell's ownership
must count — the same sibling scope external_review_task_exists already
uses, now shared via _repo_sibling_project_ids). A deleted-fork head
(GitHub sends head.repo=null) now classifies as fork, failing closed to
review instead of risking a silent ownership skip on a branch-name
collision. Residual, documented: an org PR whose task went terminal with
the PR left open falls through to the author heuristics.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 21:54:45 +02:00
a036c97985 fix(tg): cockpit data correctness — real GLM pricing, display timezone, agent activity tracking (#666)
* fix(tg): cockpit data correctness — real GLM pricing, display timezone, agent activity tracking

Three root causes behind the Mini App/bot showing wrong numbers:

Pricing: glm-5.2 gets a grounded per-token rate (z.ai published pricing,
$1.40/$4.40/$0.26 per 1M, source+date in the table comment) so a GLM
fleet day stops reporting $0.00 for half a million tokens; ungrounded
Ollama-Cloud models render "subscription (untracked)" instead of a bare
zero (is_ollama_cloud_model, consumed directly by the cockpit). Side
effect, intended and documented: honestly-priced GLM now trips the
downgrade-only comparator for new qa/documenter complexity pins.

Display timezone: the cockpit bucketed days in UTC for a GMT+2 operator.
New pure foundation module display_time (resolve_zone/local_date/
trailing_dates/day_bounds_utc, DST-correct with tests for the 23h/25h
days) + ROBOCO_DISPLAY_TIMEZONE (IANA-validated, default UTC); the
cockpit's spend/velocity series bucket raw session/completion rows by
the display zone. The UTC-keyed rollup table and the main dashboard are
deliberately untouched.

Agent activity: AgentTable.status was never set to ACTIVE and
current_task_id was never written anywhere — "active: 0, working: []"
was structurally permanent. Every claim path now marks the claimant
ACTIVE with rollback symmetry (_finalize_claim for dev/PM claims,
_qa_or_doc_claim for QA/doc/PR-gate claims, pr_review_claim for external
review) and every release path clears it (pass/fail QA, pr_pass/pr_fail,
complete_review, advance-to-PM-review, reaper unclaim, voluntary
unclaim, reassign retarget, pool divert, admin transitions, unblock
restore-to-in-progress). The bot's /status shares the cockpit's fleet
derivation so the two surfaces can't disagree. Known ceiling, commented:
one current_task_id column shows a multi-root coordinator PM's most
recent claim only.

Drill: sonnet develop -> sonnet adversarial (refuted the original
chokepoint coverage claim; QA/doc/reviewer paths were unwired) ->
correction round (wired them all + restored a dropped assertion, deleted
a dead helper and the dead subscription_billed field) -> review.

* fix(db): post_update on AgentTable.current_task breaks the flush cycle

agents.current_task_id and tasks.assigned_to reference each other, so a
flush touching both rows — every claim now marks its agent ACTIVE — is
an instance-level circular dependency SQLAlchemy cannot topologically
sort. The e2e smoke's full verb paths (12 tests) hit it; the unit and
integration suites never flush both dirty rows with relationships
loaded. post_update emits the FK as a second UPDATE, the canonical fix
for mutually-referencing rows.

* fix(budgets): enforce only explicitly-set budgets — no per-TaskType defaults

The per-TaskType default cap table blocked an unbudgeted coordination
root one opus planning turn in ($1.50 PLANNING default vs. real
coordination spend) — a false positive by design the moment the fleet
runs a priced model. Budgets are now explicit-input only:
effective_task_budget_usd returns None for an unset budget_usd, the
budget sweep skips enforcement (and never prices spend) on None, and
the unblock re-check passes on None so clearing the budget field is
itself a valid resolution. The project monthly cap stays as the
explicit-input fleet-wide backstop. Panel copy tells the truth
("No cap" placeholder; empty = uncapped), and the TaskType default
table plus its resolver are deleted.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 21:09:50 +02:00
0c1d450a05 fix(intake): emit the complete reply when no text deltas streamed (#665)
The intake/secretary chat driver treated StreamEvent text deltas as the
ONLY text channel: AssistantMessage TextBlocks were always swallowed as
already-streamed (the double-render guard). The CLI's partial-message
emission turned out to be remotely gated — on 2026-07-23 the NAS
containers got zero stream_event lines from the identical binary,
flags, SDK, model, and settings that stream fine elsewhere — so the
guard became a total blackout: replies were generated, the relay
carried only init/status/turn_end, and the CEO saw nothing.

SdkIntakeSession.send now tracks whether any text delta arrived during
the turn; normalize() emits an AssistantMessage's complete text only
when none did. Streaming mode is byte-identical (deltas render live,
completes stay suppressed); gated mode delivers the reply as one block
instead of nothing. Covers Secretary (same machinery). Regression
tests: fallback emission, default suppression, and both modes
end-to-end through the session layer.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 18:22:18 +02:00
d87ce2ca11 docs(changelog): document #663 (mix-mode escape hatch + per-loop DB engine) (#664)
The 0.27.0 release proposal executes its STORED drafted changelog, and
the current proposal predates #663 — approving it as-is would ship a
release whose commit range includes both #663 fixes with no changelog
line, which the next readiness sweep would then flag as curation gaps.
Documenting them in [Unreleased] so a re-originated proposal drafts
complete; verified against the gap-check matching rules (every required
commit in v0.26.0..HEAD matches by #PR or exact summary).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 15:46:29 +02:00
Renzo FandGitHub 226e1b586a fix(routing): escape hatch out of mix mode — clear-all overrides + pins warning (#663) 2026-07-23 15:24:13 +02:00
Renzo FandGitHub dd4c3c3ed0 docs(release): prepare 0.27.0 — curated changelog, rag/map sweep, concat rebuild script (#662) 2026-07-23 11:33:02 +02:00
Renn F d8841279a8 Updated uv.lock 2026-07-23 09:56:32 +02:00
Renzo FandGitHub d4b7e1e7b8 fix: post-finale completeness sweep — routing surface, provider config, budgets, compose env, interactive exemption (#661) 2026-07-23 09:41:27 +02:00
21d6730400 feat(providers): Gemini CLI provider — ModelProvider.GEMINI (#660)
* feat(providers): Gemini CLI provider — ModelProvider.GEMINI

Mirrors the grok blueprint with source-verified divergences (all facts
pinned against google-gemini/gemini-cli @ 9681621c): no refresher
daemon — Google's refresh tokens are reusable, so the RO host mount is
COPIED to a writable container-local ~/.gemini and each container
refreshes in-process independently (the write-back crash risk on RO
never triggers); settings.json renders security.auth.selectedType
'oauth-personal', experimental.enableAgents=false (subagent ban),
autoConfigureMemory=false with a bounded heap; tool scoping rides the
tiered TOML Policy Engine (deny-only rules that yolo mode structurally
cannot beat); gemini -p with --output-format stream-json; usage parsed
from the run's own stdout stats — the adversarial pass caught the
parser reading the json-mode nested shape while the entrypoint runs
stream-json's FLAT shape (every real run would have priced $0 forever,
hidden by fixtures sharing the assumption) — now flat-primary with the
nested shape as cited fallback; rate-limit classified from structured
error.type only (model-echo immune), native exit 41 auth passthrough;
per-model pricing for the three GA models; migrations 084 (enum) + 085
(seed) complete the 082-085 finale chain. V1 excludes interactive
intake/secretary. Stack-merge required two behavior-preserving
complexity refactors in the shared park/usage plumbing (a park-pair
loop; a usage-reader dispatch dict).

* fix(providers): route gemini usage read through the containment barrier

Mirrors the codex/grok fix — _gemini_usage_json now delegates to
_read_usage_json_contained, so CodeQL's path-injection alert on the
gemini read is resolved by the same resolve-and-contain guard.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 03:53:21 +02:00
Renn F 13abb2ece0 fix(docker): install codex CLI via npm, not the CDN-denied installer
The chatgpt.com/codex/install.sh installer returns 'resource is denied'
to non-browser clients in CI, breaking the agent-codex image build.
Switch to npm install -g @openai/codex@<pinned>: the package has no
postinstall network fetch (verified) — its native binary rides an
optionalDependency served from the public npm registry, and node is
already in the base image. Verified locally: install succeeds, codex
--version runs.
2026-07-23 03:34:52 +02:00
c70ff3cf9a feat(providers): Codex CLI provider — OpenAI via ModelProvider.OPENAI (#659)
* feat(providers): Codex CLI provider — OpenAI via ModelProvider.OPENAI

Mirrors the grok blueprint end to end: CodexCliProvider (RO ~/.codex
mount, ANTHROPIC_* blanked), an orchestrator-side codex_auth.py
refresher (JWT-exp staleness, atomic rewrite, lock-serialized single-use
rotation, --check backstop; the CLI's own in-process refresh write
no-ops on the RO mount by design — margins keep the orchestrator ahead
of the CLI's 5-minute window), config.toml rendering with required=true
gateway MCP servers, execpolicy deny rules (forbidden-only), per-role
--sandbox (developer=workspace-write, review/doc roles read-only),
codex exec --json with pinned ROBOCO_CODEX_CLI_MODEL (gpt-5.3-codex),
usage summed from typed turn.completed events priced via the real
4-bucket split, dedicated image + entrypoint, registry/park/finalize/
compose/release wiring. V1 excludes interactive intake/secretary.

Per adversarial review: migration 083 seeds the openai provider row
enabled=True (without it every routing path 404'd — the whole feature
was operationally dead code; grok needed the same seed in 039), the
panel picker gained the OpenAI catalog group it silently lacked, and
exit classification is structural — only stderr and error.message
fields from error events are sniffed (word-boundaried patterns, exact
auth phrases, bare 'login' dropped), so the model echoing on-topic
words can never false-park the provider fleet-wide, proven by a
benign-transcript test. Known open risk flagged, not claimed: whether
codex's workspace-write OS sandbox excludes /app is unverified, and no
hook mechanism exists to port the bash-guard defense-in-depth.

* fix(providers): containment barrier on usage.json reads (code scanning)

CodeQL flagged the codex usage read as path injection — correctly:
os.path.basename does not neutralize '..', and the upstream segment
validator isn't in CodeQL's taint model. The grok/codex reads collapse
into one _read_usage_json_contained helper that resolves the built path
and refuses anything outside the resolved usage root — a hostile id can
never escape regardless of upstream drift. Traversal + containment
regression tests added; a stray noqa in the test file replaced with a
named constant per repo rule.

* fix(providers): use realpath+startswith containment CodeQL recognizes

The is_relative_to() guard was a real barrier but not in CodeQL's
py/path-injection sanitizer model, so the alert persisted. Switch to
the canonical os.path.realpath + startswith(root + os.sep) form, which
CodeQL recognizes as a path-traversal barrier; behavior is identical
(refuse any candidate resolving outside the usage root).

* fix(providers): regexp-allowlist the usage-id segment (CodeQL barrier)

Neither is_relative_to nor realpath+startswith was recognized by
CodeQL's py/path-injection sanitizer model across the str->Path->open
flow. Sanitize the tainted component at the source instead: the id must
fullmatch a strict slug token ([A-Za-z0-9][A-Za-z0-9._-]*, no
separators, no '..'), which CodeQL recognizes as a path-injection
barrier; the realpath+startswith containment stays as defense-in-depth.

* fix(providers): standalone regexp guard so CodeQL recognizes the barrier

The sanitizer was one disjunct of a compound 'or' condition, which
CodeQL's guard analysis does not trace as a barrier. Split the regexp
fullmatch into its own single-condition guard (the redundant '..' check
is dropped — the required alphanumeric first char already excludes it).

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 03:20:29 +02:00
165892dc62 feat(routing): cost-tiered complexity routing + saved presets (#656)
The 08-31 lever: model_assignments gains one compound rung —
AGENT_SLUG > ROLE('{role}:{complexity}') > ROLE > GLOBAL — so a
low-complexity task can route to a cheaper tier while coordinators stay
pinned. Structurally opt-in: zero rows means byte-identical routing
(pinned by a named test across every precedence case), the cost_tiered
apply-mode (seeds developer:low→haiku) is reachable only from the
explicit PM-gated endpoint — verified no startup path can apply it.
Overrides are downgrade-only (input-price comparator), allowlisted to
{developer, qa, documenter} — cell_pm excluded per the org's own
coordinator definition and its documented weak-model incidents — and
validated at write time (disabled/unconfigured provider rejected with
remediation; cross-provider-family overrides warn explicitly).

Per adversarial review: the four mode-switch applies now spare compound
rows exactly like agent pins (the 2026-07-17 unscoped-wipe class, new
victim, same fix extended via one shared wipe helper) with panel cache
invalidation + truthful confirm dialogs; preset apply validates the
entire payload BEFORE the wipe (validate-all-first), with a savepoint
crash test proving rollback.

Presets (CEO request): routing_presets table (migration 082) snapshots
the full mix — mode, per-agent overrides, complexity rows — with
save/apply/delete endpoints and a panel preset bar; applying skips
since-removed models with per-entry notes, never silently.

Task complexity threads task_id through _resolve_agent_route at both
call sites; taskless spawns unchanged. 235 backend + 23 panel tests.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 03:04:18 +02:00
10f039c36f feat(eval): golden-task eval harness + doctrine cohort stamp (#655)
* fix(notifications): exponential backoff + CAS claim for expired-unacked re-escalation

The sweep re-escalated every expired unacked ack-required notification
on every ~60s tick, forever — the live incident: 3 fresh blocker
escalations + Telegram DMs per minute from a static stale pile. Now
each notification carries reescalation_count / last_reescalated_at /
reescalation_delivered_count (migration 079): first fire at expiry,
then doubling intervals from 1h capped at 24h, hard stop after
ROBOCO_NOTIFICATION_MAX_REESCALATIONS (default 5) with one permanent
log carrying attempts-vs-delivered so 'seen and ignored' is
distinguishable from 'route never worked'. The due/wait/capped decision
is a pure function in foundation/policy/communications.py.

Per adversarial review, the attempt slot is claimed by compare-and-set
(UPDATE ... WHERE reescalation_count = :n) BEFORE delivery — the
previous draft leaned on the 60s dedup window, which never engages for
BLOCKER_ESCALATION (_LOOP_PRONE_TYPES excludes it), so concurrent
sweeps would have double-delivered. A lost claim skips delivery
outright. Legacy rows read as count=0 and keep today's first-fire
semantics. 61 tests incl. a two-session CAS race and a real alembic
upgrade/downgrade round trip.

* feat(budgets): per-task and per-project cost budgets (flag-gated)

tasks.budget_usd + projects.monthly_budget_usd (migration 080, chained
on 079; adds ix_agent_spawn_sessions_task_id since both enforcement
seams filter on bare task_id). Behind ROBOCO_TASK_BUDGETS_ENABLED
(default off, feature-flags card) — verifiably inert when off.

Claim-time: a project-month-spend guard applies to WORK-STARTING claims
only (i_will_work_on / i_will_plan) — per adversarial review, review/
doc/gate/inbound-PR claims are exempt so in-flight work can always
finish reviewing and merging at cap. Spend counts closed sessions'
estimated_cost_usd PLUS open sessions priced live from token snapshots
(the original closed-only sum read parallel long sessions as $0).

Sweep-side: the existing budget sweep also prices the active task's
spend vs budget_usd (TaskType defaults when null); on breach the task
is BLOCKED (HUMAN resolver, budget marker) BEFORE the graceful stop so
the unclaim no-ops and the dispatcher never respawns onto it, and the
CEO notification names both recovery steps. unblock on a budget-blocked
task re-checks live spend and refuses while still over — no silent
re-breach loop. Panel: budget inputs in both dialogs (0 rejected — a
zero budget silently blocks everything), spend logic consolidated in
TaskService.task_spend_usd. 42 new tests incl. a real-DB spend-query
suite and a two-tick non-refire sweep test.

* feat(eval): golden-task eval harness + doctrine cohort stamp

roboco/eval: 6 BenchTaskSpec fixtures run through the real lifecycle in
a disposable environment (the e2e_smoke harness's fake GitHub + local
git origin + throwaway DB catalog — real isolation, not convention),
scored deterministically (terminal status, revision_count, cycle time,
tokens/cost via the agent_spawn_sessions task_id join) plus a local-
model judge whose output is nested under a non_deterministic-marked
object so cohort diffs don't read judge noise as regression. CLI:
python -m roboco.eval run --role <slug> --cohort <name>. Source-
checkout-only by declared posture (deptry-scoped ignore + a hard
ImportError guard naming why; tests/ never ships in images or wheels).

agent_spawn_sessions.doctrine_version (migration 081, chained on 080)
is stamped at spawn-session finalize from the composed prompt layers —
with the session's model column it identifies a cohort durably.

Per adversarial review: bench runs patch the vault flags off (they were
writing real markdown into the operator's vault), and the real-spawn
OrchestratorStageSpawner is deliberately cut to NotImplementedError —
spawned containers' MCP wiring resolves to the production orchestrator
under real agent UUIDs, so real spawns wait for a dedicated follow-up;
the injectable scripted spawner is the working path. Full suite 13852
passed / 94% coverage in the source worktree; deptry/mypy/xenon clean.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:06:50 +02:00
7c8453e210 feat(budgets): per-task and per-project cost budgets (flag-gated) (#654)
* fix(notifications): exponential backoff + CAS claim for expired-unacked re-escalation

The sweep re-escalated every expired unacked ack-required notification
on every ~60s tick, forever — the live incident: 3 fresh blocker
escalations + Telegram DMs per minute from a static stale pile. Now
each notification carries reescalation_count / last_reescalated_at /
reescalation_delivered_count (migration 079): first fire at expiry,
then doubling intervals from 1h capped at 24h, hard stop after
ROBOCO_NOTIFICATION_MAX_REESCALATIONS (default 5) with one permanent
log carrying attempts-vs-delivered so 'seen and ignored' is
distinguishable from 'route never worked'. The due/wait/capped decision
is a pure function in foundation/policy/communications.py.

Per adversarial review, the attempt slot is claimed by compare-and-set
(UPDATE ... WHERE reescalation_count = :n) BEFORE delivery — the
previous draft leaned on the 60s dedup window, which never engages for
BLOCKER_ESCALATION (_LOOP_PRONE_TYPES excludes it), so concurrent
sweeps would have double-delivered. A lost claim skips delivery
outright. Legacy rows read as count=0 and keep today's first-fire
semantics. 61 tests incl. a two-session CAS race and a real alembic
upgrade/downgrade round trip.

* feat(budgets): per-task and per-project cost budgets (flag-gated)

tasks.budget_usd + projects.monthly_budget_usd (migration 080, chained
on 079; adds ix_agent_spawn_sessions_task_id since both enforcement
seams filter on bare task_id). Behind ROBOCO_TASK_BUDGETS_ENABLED
(default off, feature-flags card) — verifiably inert when off.

Claim-time: a project-month-spend guard applies to WORK-STARTING claims
only (i_will_work_on / i_will_plan) — per adversarial review, review/
doc/gate/inbound-PR claims are exempt so in-flight work can always
finish reviewing and merging at cap. Spend counts closed sessions'
estimated_cost_usd PLUS open sessions priced live from token snapshots
(the original closed-only sum read parallel long sessions as $0).

Sweep-side: the existing budget sweep also prices the active task's
spend vs budget_usd (TaskType defaults when null); on breach the task
is BLOCKED (HUMAN resolver, budget marker) BEFORE the graceful stop so
the unclaim no-ops and the dispatcher never respawns onto it, and the
CEO notification names both recovery steps. unblock on a budget-blocked
task re-checks live spend and refuses while still over — no silent
re-breach loop. Panel: budget inputs in both dialogs (0 rejected — a
zero budget silently blocks everything), spend logic consolidated in
TaskService.task_spend_usd. 42 new tests incl. a real-DB spend-query
suite and a two-tick non-refire sweep test.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:06:14 +02:00
1d5a8e846f fix(notifications): exponential backoff + CAS claim for expired-unacked re-escalation (#652)
The sweep re-escalated every expired unacked ack-required notification
on every ~60s tick, forever — the live incident: 3 fresh blocker
escalations + Telegram DMs per minute from a static stale pile. Now
each notification carries reescalation_count / last_reescalated_at /
reescalation_delivered_count (migration 079): first fire at expiry,
then doubling intervals from 1h capped at 24h, hard stop after
ROBOCO_NOTIFICATION_MAX_REESCALATIONS (default 5) with one permanent
log carrying attempts-vs-delivered so 'seen and ignored' is
distinguishable from 'route never worked'. The due/wait/capped decision
is a pure function in foundation/policy/communications.py.

Per adversarial review, the attempt slot is claimed by compare-and-set
(UPDATE ... WHERE reescalation_count = :n) BEFORE delivery — the
previous draft leaned on the 60s dedup window, which never engages for
BLOCKER_ESCALATION (_LOOP_PRONE_TYPES excludes it), so concurrent
sweeps would have double-delivered. A lost claim skips delivery
outright. Legacy rows read as count=0 and keep today's first-fire
semantics. 61 tests incl. a two-session CAS race and a real alembic
upgrade/downgrade round trip.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:05:41 +02:00
31a489b431 chore(packaging): make quickstart — one-command registry bring-up + pull-smoke CI (#653)
scripts/bootstrap.sh: idempotent bring-up for the pull-and-run deploy.
Fresh .env: copies .env.example and injects the three required secrets
using the documented one-liners (the panel token via the exact HMAC
formula issue_panel_token uses), with a standing-credential warning —
and a louder one when cloud auth is detected — since compose's :? guard
refuses an empty token unconditionally (verified live). Reused .env:
never touched, but the three required vars are pre-validated with
pointed remedies instead of compose's opaque interpolation error. Then
pull + up -d + a doctor-style readiness sweep grounded in the real
surfaces (root /health, /api/auth/status through nginx, the verbatim
'Alembic upgrade finished' log line, ollama list), each stage failing
loud with the command to run next. Exposed as make quickstart; README
leads with it and keeps the manual steps as 'what quickstart does'.

Also found and fixed along the way: the documented registry quickstart
was already broken — nginx's :?-required ROBOCO_PANEL_AGENT_TOKEN ships
empty in .env.example, so the 4-step path failed at compose config.

release.yml gains a pull-smoke job (fresh runner, own GHCR login,
needs publish-images): literally pulls the registry compose against the
just-published tag, guarding the missing-image regression class that
already happened once.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:05:37 +02:00
fa459998b4 feat(git): env-ladder rung protection at the shared remote-delete chokepoint (#651)
Rung protection lived only in delete_task_branch; the post-merge PR-
source cleanup (and the stale-branch sweep's shared primitive) could
still delete a branch that IS a ladder rung. _protected_branches_for_
deletion(slug) — field ∪ rung names, null-ladder shim included — now
feeds _delete_remote_branch_best_effort, so every remote deletion path
is covered; delete_task_branch's local rung check is removed as exactly
subsumed (verified byte-identical comparison semantics). Bonus closed
gap: a renamed trunk (default_branch 'trunk', null ladder) is now
delete-protected, which the hardcoded main/master floor never covered.

Per adversarial review, the deletion lookup fails CLOSED: a raised
project lookup skips the delete with a warning (a skipped best-effort
delete just retries next sweep — free safety), while a genuinely-gone
project proceeds with the hardcoded floor (its ladder is meaningless).
The rebase/sync resolver stays fail-open — a refused rebase on a DB
blip would wrongly block work, a different tradeoff, now documented.
Panel tooltip updated to the new truth. 29 tests.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:05:33 +02:00
3806317aa7 fix(guard): operator-scoped XFF hop peers + tailnet allowlist (live-incident fix) (#650)
Two coupled hardenings from the chain-peers adversarial rounds plus the
root-cause fix for the live post-deploy incident where the CEO was
blocked from the panel ('IP not allowed: 100.x.x.x').

Hop peel-set: the whole docker bridge pool leaves the XFF hop set — hops
are now loopback plus operator-named single addresses only
(ROBOCO_GUARD_TRUSTED_CHAIN_PEERS, plain IPs; CIDR entries rejected with
a warning because a range readmits sibling containers). Default-empty
closes the CGNAT-forge residual outright; a gateway-fronted Tailscale
Serve deploy sets its real gateway IP, and a rate-limited detection log
names exactly that IP when an unconfigured host-proxied tailnet chain is
seen, so the silent-regression shape is observable. The connecting-peer
gate (may nginx present XFF at all) deliberately keeps the broad bridge
pool — different check, unchanged.

Incident root cause: guard-core's whitelist is an EXCLUSIVE allowlist
(any non-member is refused), so honestly resolving the tailnet client IP
made ip_security reject the CEO. The tailnet CGNAT range joins
_guard_whitelist() deliberately: Tailscale authenticates device
membership before a packet arrives, real-IP stamping still buys correct
attribution, and any future non-tailnet exposure keeps full scrutiny.
Both compose files now pass ROBOCO_GUARD_EMERGENCY_WHITELIST through to
the orchestrator (the operator escape hatch previously did nothing in a
compose deploy).

NAS is running ROBOCO_GUARD_PASSIVE_MODE=true as interim mitigation —
flip back to false when this deploys. 66 tests.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:05:29 +02:00
da4d9b333d feat(git): protected-branches enforcement + panel editor (#649)
projects.protected_branches existed end-to-end but nothing consulted it
— the panel had no editor and the git safety checks used hardcoded sets.
Now: GitService._protected_branches_for(slug) (frozenset, stripped,
fail-open to the hardcoded floor with a warning log) is unioned — never
replacing, only tightening — into rebase()'s refusal set, the shared
_delete_remote_branch_best_effort skip set (threaded through every
caller: task cleanup, PR merge/close cleanup), and sync_task_branch,
which now refuses to force-push a protected-named head (the dev-facing
sync_branch verb path the HTTP-only fix would have missed). Matching is
exact and case-sensitive; an empty list degrades to exactly the old
hardcoded behavior, pinned by union-floor regression tests (master/main
stay refused regardless of the project list).

Panel: chips editor for the field in the edit-project dialog (add via
Enter/comma, paste-splitting on comma-separated lists, dedup, clear-to-
empty persists []) with an honest tooltip scoped to what is actually
enforced. Tests cover both the incumbent GitHub-App dialog suite and the
new Protected Branches suite in one harness.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 20:30:05 +02:00
4585a248ce feat(x): redraft loop on CEO reject — feedback re-enters the draft flow (#648)
* feat(x): redraft loop on CEO reject — feedback re-enters the draft flow

A rejected X draft's reason used to die with the cancel. reject() with a
non-blank reason now schedules a redraft after its commit
(defer_after_commit; fresh session; never blocks or fails the HTTP
response): XEngine.redraft_from_rejection re-drafts the same source kind
via the local model with the reason and rejected body folded in as
revision guidance, originating ONE fresh held draft — mirroring the
video pipeline's reauthor_from_rejection. Local-model failure or empty
output originates nothing (no degraded copies); markers carry forward
whole so a redrafted reply/spotlight stays fully functional downstream;
bodies ride the same 280 clamp; the open-posts cap holds.

Hardened per adversarial review: reject() is now idempotent on an
already-CANCELLED target at both check sites (mirroring approve's
already_rejected guard — a replayed reject schedules nothing), and the
dedup check+originate runs under a non-blocking identity-keyed Redis
lock (SET NX + compare-and-del, matching the approve/reject mutex
style) so racing rejects can't stack duplicate drafts — lock held or
Redis down skips the redraft, which is always safe. Tests pin the
fresh-session contract by session identity, the replay no-op, the
lock-skip, and clean up their own committed rows.

* fix(tests): runtime UUID import + typed task-id coercion in x cleanup helper

CI's quality gate runs mypy over tests/ (the local pass covered only
roboco/): the _delete_tasks calls handed ORM-typed ids where uuid.UUID
was expected. Coercing at the call sites then exposed that UUID was
imported under TYPE_CHECKING only — a runtime NameError. Import moved
to runtime; both call sites coerce explicitly.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 20:30:01 +02:00
d1f9d21a68 fix(tg): mini-app responsiveness — safe-area activation, truncation, touch targets (#647)
Root cause first: no viewport export existed anywhere, so viewport-fit
was never 'cover' and every env(safe-area-inset-*) resolved to 0 on
notched iPhones — content under the status bar, dock without real
home-indicator clearance. The export lives on the (tg) group layout
(server component), NOT app-wide: the dashboard shell has no safe-area
padding and must not inherit cover.

Also: min-w-0 on four truncating flex children that overflowed their
justify-between rows (chat names/previews, fleet task titles);
object-contain on the approvals video (letterbox instead of distort on
short phones); break-words on the changelog pre / task description /
quoted mention; touch targets bumped to >=36px (sheet close, segmented
controls, ack button, chips, back button, bell, jump-to-latest,
cut-toggle); overflow-x-hidden backstop on the (tg) main scroller; fleet
avatar strip sliced to 3 with a +N badge instead of silent clipping.

Verified: pnpm typecheck clean, lint 0 errors, panel suite 870/870.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 20:29:57 +02:00
583d43f213 fix(guard): resolve the tailnet client behind host-proxy hops for the WAF (#646)
fastapi-guard peels a fixed trusted_proxy_depth=1 from X-Forwarded-For
(the rightmost entry, which nginx itself recorded). That is correct for
every chain except host-proxied tailnet traffic (Tailscale Serve →
nginx), which arrives as [tailnet-client, loopback-or-bridge-gateway] —
depth-1 resolves it to a whitelisted hop IP, leaving WAF/ban/rate-limit
inert for the whole /tg surface (the documented ceiling).

ClientIpResolutionMiddleware (pure ASGI, wraps SecurityMiddleware so it
runs first) stamps guard_core's request.state.client_ip cache — its
supported pre-resolution seam — for EXACTLY that shape: peel known local
hops (loopback + docker bridge pool) from the right, stamp only when at
least one hop was peeled AND the candidate is in the tailnet CGNAT range
(100.64.0.0/10). Every other shape abstains, so direct LAN clients,
agent containers relaying through nginx (even with forged public-IP
prefixes), and all-hops operator traffic resolve byte-for-byte as
before. Documented residual: a same-bridge container forging a CGNAT
prefix only DE-privileges itself (loses its whitelist exemption). XFF is
read first-occurrence to match Starlette's own header semantics, and a
wiring test pins the middleware mount ORDER, not just presence.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 20:29:54 +02:00
98a96bcd21 chore(backup): env-gated off-disk mirror + restore drill doc (#645)
The pg_dump sidecar wrote its dumps to the same disk it protects — one
disk failure lost both. Setting ROBOCO_BACKUP_MIRROR_DIR in .env to a
path on a different disk (external/remote mount) arms a mirror step after
every successful dump: tmp+rename copy, mirror pruned to the same
BACKUP_KEEP, unwritable mirror logs-and-skips without blocking the
primary. Unset, the script never attempts a copy — no fake off-disk
copies on the same disk. Docs gain the mirror setup and a quarterly
restore drill (throwaway pgvector container, pg_restore, row-count
sanity check).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 20:29:50 +02:00
2e889c7009 feat(lifecycle): inherit advanced upstream base on work re-claims (#644)
* feat(lifecycle): inherit advanced upstream base on work re-claims

A re-claim reused a branch cut at an earlier claim, so upstream work
merged since (UX/UI landing on the root after the cell branch was cut)
never reached BE/FE branches — divergence and avoidable conflicts.
_finalize_claim now merges the advanced base into the pre-existing branch
via the dependency-lineage merge: already-ancestor is a no-op, a conflict
aborts at the cut point and leaves a transition note steering the agent
to sync_branch, a clean merge logs an audit trail; never fails the claim.

Double-gated: by role (developer/cell_pm/main_pm — QA/documenter/gate
claims review the branch as pushed and never move it) AND by pre-claim
status (pending/needs_revision only — a PM's i_will_plan re-claim of its
own awaiting_pm_review task must not move a branch that already passed
QA + the PR gate). Fresh cuts already branch from the live remote base.
Cell-PM prompt now orders reading the upstream design docs before
planning.

* fix(lifecycle): extract base-inheritance gate predicate for xenon budget

The four-condition inline gate pushed _finalize_claim to cyclomatic
rank C; the quality gate caps blocks at B. The decision moves to a pure
module-level predicate, byte-for-byte the same logic.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 20:29:46 +02:00