mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
3516d925fe6ba64f6f82cc1c942c70188f6c0e4b
242
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
d4b7e1e7b8 | fix: post-finale completeness sweep — routing surface, provider config, budgets, compose env, interactive exemption (#661) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
f74131a122 |
fix(release): commit the approve() COMPLETED write under the release lock
approve() flushed the published proposal's COMPLETED status but left the durable commit to the background caller (_run_approve_background), which runs after approve()'s finally has already released the Redis lock. In that window a concurrent reject() could acquire the freed lock, re-read a row whose COMPLETED write was only flushed (invisible to its own session under READ COMMITTED), pass its guard, and flip the just-published proposal to CANCELLED — last writer winning the row. The bug-sweep (#638) fixed reject()'s side of this but left approve()'s, so reject()'s 'fails closed' guarantee didn't hold end to end. Commit COMPLETED while still holding the lock (mirroring XPostService._post), so it's durable before release and a racing reject sees it and refuses. A cross-session regression test proves a fresh connection sees COMPLETED the moment approve returns (it read 'pending' before the fix). |
||
|
|
17de29545a |
[6788ce7f] Silent bug sweep: concurrency, state integrity, engine edge-cases, panel data freshness (#638)
* [943d8c4d] Frontend data freshness and approval-queue reliability audit (#631)
* [233a8b0f] WebSocket reconnect message-loss audit and fix (#625)
* [233a8b0f] fix(panel): add REST catch-up to useNotificationStream on WS reconnect
connection.ts has no message buffering/replay, so a notification published
while the CEO bell's socket was down (disconnected/reconnecting) was lost
forever instead of merely delayed. Add a reconnect-triggered GET
/notifications?unread_only=true catch-up folded into the existing
notification_id dedup so a notification delivered both via catch-up and
live WS is never double-counted, and make clearMessages drop the held
catch-up batch too. use-a2a-live.ts and use-rate-limit-websocket.ts were
audited and already have working reconnect-triggered REST fallbacks
(verified via a2a/page.tsx, rate-limit-banner.tsx, usage-overview-panel.tsx
and their existing F083 tests) so no fix was needed there.
* [233a8b0f] docs(panel): add comprehensive WebSocket hooks reference and reconnect architecture guide
Add panel/docs/frontend/hooks.md with full API reference for useWebSocket, useNotificationStream (with new REST catch-up behavior), useAgentStream, useA2ALiveStream, and useConnectionStatus. Include examples, best practices, and testing guidance.
Add panel/docs/architecture/websocket-reconnect.md documenting the message-loss mitigation pattern: Strategy 1 (REST catch-up for events, used by useNotificationStream) and Strategy 2 (REST invalidation for state, used by A2A/rate-limit consumers), plus the dedup logic ensuring no notification is double-counted on reconnect.
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
* [d5315683] fix(frontend): add distinct toast feedback for silently-swallowed x-post and release-proposal statuses, plus regression tests for all 4 approval queues (#626)
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
* [cd953838] Data-hook null-guard audit and API client 429 retry-by-method fix (#630)
* [cd953838] fix(panel): gate 429 retry by HTTP method, add hook null-guard regression tests
* [cd953838] chore(conventions): waive test-fixture wrapper in hooks null-guard test
* [cd953838] docs(frontend): document API rate-limit retry behavior and null-guard audit results
Added `docs/frontend/api-rate-limiting.md` to document the 429 retry strategy: GET/PUT auto-retry, POST/PATCH/DELETE require X-Idempotency-Key header. Updated `docs/frontend/hooks.md` to confirm the data-hook null-guard audit found all hooks already have correct `enabled` guards and include a regression test suite for the board-review poll on/off behavior and enabled-guard assertions.
---------
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 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>
* [4534c71a] Backend concurrency, state-machine, and engine audit (#634)
* [41de844a] fix(lifecycle): sync CLAIM_RULES with runtime + clear stale claimant on PM hand-off (#627)
Two confirmed state-machine gaps found while auditing lifecycle.py,
task_lifecycle.py, the _ESCALATABLE_TO_BLOCKED bypass, and every
_REVIEW_QUEUE_STATES entry point:
- lifecycle.py's CLAIM_RULES/claim-ActionSpec/StatusTransition table
did not grant CELL_PM/MAIN_PM re-claim of AWAITING_PM_REVIEW even
though task.py's runtime _ROLE_CLAIM_STATUSES already granted it
and claimed the spec agreed -- the two tables had silently drifted,
breaking i_will_plan re-claim on an awaiting_pm_review task.
- docs_complete's _maybe_advance_to_pm_review pre-assigns a specific
owning PM via assigned_to but left claimed_by/active_claimant_id
pointing at the outgoing documenter, unlike every sibling transition
into a review-queue state. A stale active_claimant_id makes
content_actions.py's _active_claim_violation wrongly reject the
newly-assigned PM's own content writes before it formally claims.
Reassign claimed_by + active_claimant_id to the owning PM alongside
assigned_to.
Adds a regression test asserting the documenter's stale claim does not
survive the docs_complete -> awaiting_pm_review hand-off.
Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
* [0c46f666] Engine dedup race + sequencing.py edge-case audit (#628)
* [0c46f666] fix(sequencing): dedup race audit + collision-edge fallback bug
Audited the list-open-then-originate dedup pattern across six engines:
RoadmapEngine, XEngine.run_cycle, DepUpdateEngine, and CIWatchEngine each
run inside exactly one sequential orchestrator-loop asyncio task (no other
call site invokes run_cycle), so they cannot race with themselves; their
in-cycle dedup sets/keys are correctly built before any commit. SelfHealEngine
is the same shape. VideoEngine.open_video_task is genuinely different: it is
reachable from the release-publish hook, the feature-spotlight hook, and the
on-demand POST /video/request route, so two overlapping calls for the same
occasion can both pass the "no open task yet" check before either commits.
Fixed by wrapping the check+insert in a short-lived Redis mutex (reusing
HeartbeatMutex) keyed by occasion, mirroring XPostService's existing
lock pattern, with a regression test proving only one of two concurrent
calls creates a task.
Verified ReleaseExecutor's half-landed retry path (release_commit_sha):
apply_version_bumps and write_changelog_entry both run as uncommitted
working-tree edits before commit_and_push's single `git add -A` + commit,
so a bumped-version-without-changelog state can never reach origin (and
therefore can never be observed by a fresh retry clone) - confirmed correct
with a real-git-repo regression test, no fix needed.
Fixed sequencing.py's dev_task_collision_edges: the `if edges: return edges`
short-circuit dropped the same-assignee-lane fallback entirely whenever ANY
surfaced sibling pair produced a collision edge, even for a completely
unrelated same-assignee pair with no declared surface. Now the fallback
always runs, skipping only pairs the analyzer already ordered (so the two
mechanisms can never disagree on direction for the same pair).
Verified sequencing.py rule 3 (all-shared batch generates no edges): correct
by inspection (_shared_last_edges skips every pair when both are shared) and
confirmed with a regression test - no fix needed.
* [0c46f666] docs(reference): concurrency audit summary - engine races, fixes, verified patterns
---------
Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
* [8f7f167a] Redis mutex pre-lock write audit (#629)
* [8f7f167a] Redis mutex pre-lock write audit: add cross-session regression test for XPostService.approve
Audited x_post_service.py, video_post_service.py, release_proposal.py, and
heartbeat_mutex.py for the pre-lock DB-write anti-pattern (a session write
that happens before the SET NX / HeartbeatMutex acquire returns a token,
letting a losing racer's stale write clobber a winner's committed state).
XPostService.approve, VideoPostService.approve, and
ReleaseProposalService.approve/reject already implement the correct
validate-pure-pre-lock, apply-under-lock pattern (the XPostService fix
already shipped per CHANGELOG.md: "X edited_body write deferred into the
single-flight lock (M5)"). HeartbeatMutex holds no AsyncSession at all, so
the anti-pattern is structurally inapplicable there.
Adds a genuine cross-session concurrency regression test to
test_x_post_service.py (a real second DB connection, not an in-process
mock) mirroring VideoPostService's existing cross-session test, proving a
concurrently-committed post survives and the CEO's edited body never lands
on the just-posted row.
* [8f7f167a] Remove redundant inline comments flagged by QA in cross-session regression test
Both comments restated what the surrounding docstrings already say
explicitly, per QA findings F-dbadd8f0 (line 294) and F-27ac051e (line
631) — no behavior change, tests re-verified green against a sandbox
Postgres.
* [8f7f167a] Remove inline trailing comments flagged by QA (correct file this time)
QA findings F-e6f3e6a6 and F-24189858 cited tests/unit/services/
test_x_post_service.py:294 and :631 across 5 revision rounds, but that
file never contained the flagged comment text — a repo-wide grep for
the exact quoted strings shows both comments actually live in the
mirrored tests/unit/services/test_video_post_service.py file, in its
own cross-session concurrency regression tests (the caption-edit and
tiktok-skip tests). Removed both there:
- "# externally visible to the "concurrent" session below" on the
db_session.commit() call
- "# never attempted without credentials" on the tiktok_poster.calls
assertion
Both restated what the surrounding docstrings/test names already say;
no behavior change. Verified with the full make quality gate against a
sandbox Postgres/Redis: 13,717 passed, 94.41% coverage, clean except
one pre-existing unrelated failure in tests/unit/api/test_cloud_auth.py
::test_login_route_parses_oauth2_form_not_query_params, which connects
to the app's default localhost:5432 Postgres (not the db_session
sandbox fixture) and is unreachable in this sandboxed environment —
structurally unrelated to the auth subsystem this task never touches.
* [8f7f167a] Redis mutex pre-lock write audit (round 7): add cross-session regression tests for reject() lock protection
Round-7 QA findings F-7eb9fbcb, F-06f39a2e, and F-4d56e49b claim
XPostService.reject(), ReleaseProposalService.reject(), and
release_executor._await_proc() lack lock protection / a CancelledError
handler — but their cited line ranges (255-267, 429-454, 241-257)
describe a pre-fix, shorter version of these functions that predates
commit
|
||
|
|
a1233b2aeb |
fix(panel): V6 review gaps — honest errors, safe secretary start, real tests
CEO A2A mutations now invalidate the Mine-list query key so the list refreshes without the socket; the tg Metrics tab renders explicit error notes instead of confident zero stats when a section's fetch fails; the tg Secretary chat checks a new registry-backed /secretary/live/active route before auto-starting, showing a Take Over button instead of silently killing a session live on another device; the AI-routing card surfaces roster fetch errors instead of an empty grid; the shared acceptance-criteria editor caps at the backend's 7-item limit; the board-tab comment no longer calls the task sheet read-only. Tests: task-sheet approve/reject interactions (not just visibility), a non-demo metrics error-state test, secretary take-over branches, and dashboard-router auth-gate coverage (the e2e harness now mounts the dashboard router so the gate is actually exercised). |
||
|
|
5ca8a9c4a6 |
fix(github-app): PAT fallback covers every mint failure; video/motion/x cleanups
mint_installation_token now wraps JWT build + HTTP + parsing so raw httpx/jwt failures surface as GitHubAppError and the existing PAT fallback catches them all (a GitHub outage no longer crashes git operations for App-bound projects). The PEM is validated at credential-set time instead of first mint, and the installation-token cache is cleared when credentials are deleted. Also: the video preview root resolves symlinks like its sibling route (frames under a symlinked workspaces_root no longer false-404), the tracked dangling motion/node_modules symlink is removed and the gitignore gains a slash-less entry that actually matches symlinks, changelog caption input strips GHSA refs like PR refs, and the edit-project dialog hides the GitHub App section for auto-detected gitlab.com projects and warns before a save that would clear both auth sources. |
||
|
|
0296ec6fde |
fix(settings): ceo_name write-through renames the CEO agent row
The ceo_name setting only reached the panel header — the CEO agent row kept its seeded name, so agent selectors (create-flag, assignee pickers, kanban) still showed the old name after a rename. Setting ceo_name now updates the CEO row's name in the same transaction, resolved via the shared duplicate-tolerant get_agent_by_role lookup. The seed path never overwrites existing rows, so a custom name survives restarts. The agent-facing docs/map entries drop the personal name. Write-through tests pin ancient created_at on fixture rows so they stay deterministic against CEO rows other suite tests may have committed. |
||
|
|
161b36b563 |
fix(git): stage only codegen-produced drift and link the auto-commit
The pre-push codegen auto-commit staged with a blanket 'git add -A', so any pre-existing dirty file in the worktree (e.g. crash-orphaned edits a resume never cleans) was silently swept into the 'regenerate generated artifacts' commit. Now the porcelain state is snapshotted before and after the codegen run and only newly-dirty paths are staged with 'git add --'. The commit is also recorded via _link_commit_to_task with the pushing agent's id threaded from both push call sites, so it shows up in the task's commit history like every other commit. |
||
|
|
5f42a93b4f |
feat(git): auto-regenerate + commit codegen drift before push (#632)
A project that checks in generated artifacts (RoboCo's lifecycle renders, verb tables) drifts whenever their source changes. The agent pre-submit gate (make gate) omits foundation-check, so drift is invisible at the desk and only fails on CI's drift gate — a failure with no link back to the task, which made one live task thrash 8 revision rounds. New per-project codegen_command (migration 078): run in the task's worktree right before push, and any drift committed into the same push, so CI never sees stale artifacts. Fail-open — a broken/timeout codegen command logs and lets the push proceed (CI's drift gate is the safety net); a null command (every project without checked-in codegen) is a pure no-op. Hooked at both push_branch (open_pr's first push, the PR head CI grades) and push_task_branch (later re-pushes). RoboCo sets codegen_command='make codegen' (a new Makefile target — the write counterpart to foundation-check's read) via the panel. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
7b84162ae9 |
feat(github-app): App credentials, installation tokens, and a Select repo picker (#621)
* feat(github-app): App credentials, installation tokens, and a Select repo picker RoboCo was 100% PAT-based. A singleton Fernet-encrypted github_app_credentials row (migration 077, telegram-credentials pattern) now stores the App id + private key; github_app_auth mints RS256 app JWTs and caches installation tokens until 5 minutes before expiry. Projects can bind an installation (projects.github_installation_id): get_decrypted_token returns a minted installation token for bound projects and falls back to the stored PAT on any minting failure, so all ten token consumers work unchanged. CEO-gated routes expose credentials CRUD plus installation/repo listing, and the New Project dialog gains a Select repo picker (disabled with a HelpTip until the App is configured) that fills the git URL and binds the installation; manual URL + PAT stays the default path. * test(panel): mock the GitHub App credentials card in the settings page test --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
16fa018ace |
feat(x): real voice guide, slop ban, and caption craft for drafted content (#615)
* feat(x): real voice guide, slop ban, and caption craft for drafted content Release posts and mention replies were drafted from a one-sentence voice stub while the reasoning-backed Head-of-Marketing voice guide only reached the off-by-default spotlight path. The drafting prompts now carry the full voice rules, a banned AI-slop list, three style exemplars, hook-first structure, and an under-240-char budget so the 280 clamp never truncates mid-sentence. An empty brand_voice now nudges the CEO exactly once (durable system_settings marker) instead of silently shipping baseline voice forever. A failed reply draft skips origination instead of shipping 'Thanks for the mention!'. Video dev prompts and motion/README gain per-platform caption templates (X: hook + specifics + outro; TikTok: hook + short lines + few niche hashtags). * docs(motion): reflow the new Captions section to satisfy the prose gate --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
c8f55be904 |
fix(notifications): task titles and agent slugs replace raw UUIDs (#616)
* fix(notifications): task titles and agent slugs replace raw UUIDs
Notification producers interpolated raw task/agent UUIDs into subjects and
bodies ('Task 68e1e4db-... unblocked', 'handed back to 00000000-...-0004').
A tiny notification_text helper (task_display: title-first with a #id8
fallback; agent_display: identity-map slug first, DB lookup fallback) now
feeds every producer: all 13 NotificationService methods, the 7
delivery-service bodies whose subjects were already title-based, the
substitute-PM ad-hoc insert, and the orchestrator/choreographer callers,
which thread the task row's title one call deeper. Fixes the literal
'cell_pm' role string sent as an agent slug in the merge-conflict
notification. Tool-call examples like unblock('<uuid>') keep the raw id on
purpose — agents need it.
* test(notifications): board-review subject assertion matches the humanized format
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
e125ef08aa |
feat(settings): CEO display name is configurable, Renzo hardcode removed (#612)
The header chip and the Settings User Info card rendered a literal 'Renzo'. The name now lives in the system_settings store under ceo_name (validated: trimmed, non-empty, max 60 chars) with the same client-served default the transcript-retention card uses, editable inline from the User Info card. Agent prompts already refer to 'the CEO' generically, so no prompt rewiring; the two agent-facing RAG docs drop the name too. License/CLA copyright is untouched. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
57b9e76b12 |
fix(x): release caption uses curated CHANGELOG headlines, not commit subjects (#607)
draft_release_post was fed highlights=list(report.change_summary) — raw
per-commit subjects — so the announcement model parroted the top commit
('RoboCo API v0.26.0 is out: docs: curate the full Unreleased body #601').
New pure changelog_highlights() extracts the bold feature leads from the
curated release entry (report.drafted_changelog), stripping PR refs and
trailing periods; approve() prefers those and falls back to change_summary
only when the changelog yields nothing. The video captions were already
good because the authoring dev read the changelog — same source now.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
fc41dfa40e |
fix(security): active guard enforcement, CEO A2A target check, notification expiry (#595)
* fix(security): guard goes active; CEO A2A respects no-comms roles; ack notifications expire ROBOCO_GUARD_PASSIVE_MODE defaults to false in both compose files — the deferred post-calibration flip; fail_secure stays off and the env override remains the rollback. can_a2a_direct no longer short-circuits the CEO past the no-comms set (auditor/pr_reviewer/prompter/secretary), now canonical in foundation.policy.communications.NO_COMMS_ROLES and shared with the content-actions gate; the A2A service refuses at conversation creation instead of silently suppressing the wake. Ack-required notifications get expires_at stamped from ROBOCO_NOTIFICATION_ACK_TTL_HOURS (default 48, 0 disables), so the re-escalation sweeper's expires_at query matches rows for the first time. * refactor(notification): extract _ack_and_expiry — xenon rank back under B The expires_at stamping pushed _create_notification_with_session to rank C; the requires_ack + expiry derivation moves into a helper with the same semantics and comments. * test(conftest): dispose the global DB engine after every test Production code reaching get_db_context()/get_engine() lazily creates the process-global engine bound to the current event loop; with per-test function-scoped loops, any later test touching the global path inherits a dead-loop engine and dies with 'Future attached to a different loop' — the order-dependent class that has been wandering the suite (cloud_auth login, metrics, tasks-routes, full-lifecycle) whenever collection order shifts. An autouse fixture now close_db()s after every test, keeping the global path loop-local; no-op when untouched. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
29f7082030 |
fix(git): cancel closes the task's open PR; bulk cleanup spares live dependents (#593)
Task cancellation left the task's PR open on the forge forever: cancel() now best-effort-closes the recorded PR for the task and its cascaded descendants (close_task_pr_best_effort resolves owner/repo off git_url — no clone needed; never raises into the cancel). The bulk stale-branch sweep gains a dependents guard: a branch still recorded by a non-terminal task, or serving as a live child's resolve_parent_branch base, is excluded from the candidate window — mirroring the existing env-ladder-rung skip. Scoped to the sweep, not delete_task_branch, so the BFS cascade-cancel can't falsely block a parent's branch on its own about-to-cancel child. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
c7605b0d77 |
feat(tg): premium Mini App cockpit — spend hero, charts, avatars (#582)
* feat(tg): premium Today — spend hero, trend, quick actions, fleet avatars The cockpit home stops being flat cards and becomes a real app surface: - Spend HERO: the day's cost at 40px with a signed delta-vs-yesterday chip and a live 7-day amber area sparkline (hand-rolled inline SVG, no charting lib in the Mini App bundle). - Quick-action ring: circular Approve (amber + needs-you badge) / Board / Inbox / Chat, the wallet-style primary-verb row. - Needs-you as a rich amber gradient banner (top items + draft chips) instead of a plain section. - Fleet as live avatar tokens (stable per-name hue, pulse dot) over the working list. - "Shipped this week" day-bars (today emphasized) + week total. Backend: /telegram/today gains spend.series (7-day cost) + delta_pct and a velocity series (per-day completed tasks) — two cheap grouped-by-day queries, same DB-only ethos, degrading to zeros on error. * feat(tg): color-code approval rows by kind TgRowIcon gains a tone prop; the approvals list tints each tile per kind (amber Release / sky X post / violet Video / emerald Roadmap) so a mixed queue reads as color-coded instead of a monochrome column. * feat(tg): sender/peer avatars on Inbox + Chat Inbox notifications and Chat conversation rows adopt the fleet-avatar language: a per-name-hued initials token leads each card, unread inbox items carry a subtle primary tint, and both cards move to the rounded-2xl surface — so every tab now shares one visual system. Board keeps the shared MobileTaskBoard (already grouped/pill-styled, and reused outside the cockpit). --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
5f32d8760a |
feat(forge): Phase 4 — GitLab + Gitea repo-provisioning parity (#581)
GitLab's create_org_repo (services/forge/gitlab.py) replaces the Phase-3
synthetic 501 with a real implementation: resolves org (a group's full
path, subgroups included) to a numeric namespace id via GET
/groups/{path}, falling back to the token's own namespace on a 404
(personal-namespace projects); POSTs /projects with the
name/path/description/visibility/initialize_with_readme payload
(visibility mapped private->"private"/"internal"); reshapes the 201
onto the GitHub fields callers read (full_name/clone_url/html_url) and
GitLab's duplicate-path 400 "has already been taken" onto GitHub's 422
shape, text preserved. Gitea's create_org_repo was already real but
untested — added transport-level coverage.
GitHubProvisioningService (services/github_provisioning.py) is now
provider-aware: ROBOCO_PROVISIONING_PROVIDER (github default / gitlab /
gitea) and ROBOCO_PROVISIONING_HOST (self-hosted instance, required for
gitlab/gitea or the service stays disabled exactly like a missing
token/org) pick the target forge; the class/factory names stay
GitHub-flavored for backward compatibility (pitch.py and existing
imports untouched). A shared _is_already_exists() helper recognizes
GitHub's "already exists" (422), Gitea's (409/422 "already exists"),
and GitLab's reshaped "has already been taken" (422). The existing-repo
re-fetch now builds a provider-aware RepoRef (GitLab packs org/name
into the owner field; GitHub/Gitea keep the owner,repo pair). Default
behavior (no new env set) is byte-for-byte the Phase-1 GitHub path,
pinned by a regression test.
Gates: ruff format/check clean, mypy roboco/+tests/ clean (1235 files),
xenon A/A/B clean, targeted suite (forge + provisioning + pitch) 79/79
green.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
96401f4c10 |
feat(forge): Phases 2+2.1+3 — Gitea + GitLab providers, per-call routing, local-merge fallback (#575)
* feat(forge): Phase 2 — Gitea provider, per-call routing, host registry Gitea support lands behind the Phase-1 seam: - GiteaProvider (services/forge/gitea.py): Gitea v1 transport addressed by instance host (api base from the project's git_url). Where Gitea's wire contract diverges from GitHub's, the provider adapts responses back into the shapes GitService already classifies (ShapedResponse): `token` auth scheme, duplicate-PR 409→422 with the "already exists" text GitService keys on, commit statuses reshaped into check_runs / workflow_runs envelopes, APPROVE→APPROVED review mapping, Do-keyed POST merge, merge-method repo keys, label-color '#' prefix, client-side head/base PR filtering. Deliberate postures per the spec: zero-workflows fail-open (statuses-free repo → no_ci_configured) and merge_branch as a shaped 501 (env-sync cascade lands on missing_ref; the shared local-git fallback is Phase-2.1). - ForgeRouter (services/forge/router.py): GitService._forge now routes per call from RepoRef.host — every existing call site unchanged in shape. RepoRef gains an optional host; _parse_git_url returns the host-stamped ref and it is threaded through GitService/release executor instead of being rebuilt from strings (helpers re-signatured to take RepoRef). - Host registry (services/forge/registry.py): in-memory host→provider map, self-healing — ProjectService.get/get_by_slug re-register on every read; provider_for resolves gitea projects by git_url host. - Registration validation now accepts git_provider="gitea"; GitLab remains recognized-but-rejected. Panel: the read-only Forge badge becomes a real picker (Auto-detect / GitHub-GHE / Gitea / GitLab disabled). Plain git (clone/fetch/push) needs no changes — the Basic-auth extraheader works on Gitea unchanged. Gates: mypy 392 files, xenon A, full unit suite 6356 green, integration suite 2257 green. * feat(forge): live-Gitea contract suite + scheme support + slash-safe refs Hardening from running the provider against a real dockerized Gitea 1.22.6 (the spec's Phase-2 contract suite, now committed as the env-gated tests/e2e_smoke/test_gitea_live.py — self-seeding: creates its own repo, pushes real commits, and drives PR open → duplicate reshape → list/filter → diff → review → labels → commit-status CI reshapes → squash merge → branch delete → release, plus a live verification of the x-access-token Basic-auth git-CLI claim). Two real findings fixed: - Branch refs weren't URL-encoded — every RoboCo branch carries slashes (feature/backend/...), and Gitea's router 404s on the extra path segments. list_ci_runs + delete_branch_ref now quote the ref (regression-pinned in the unit suite). - The API base hardcoded https; a LAN instance serving plain http is a real deployment shape. GiteaProvider gains a scheme (recorded per host by the registry from the project's git_url). ShapedResponse moves to forge/shaping.py (shared by the upcoming GitLab transport, which needs its text override for diff reassembly). * feat(forge): Phase 3 GitLab provider + Phase 2.1 local-merge fallback GitLabProvider (services/forge/gitlab.py): GitLab v4 transport addressed by host+scheme, subgroup-safe (the MR project path packs into RepoRef.owner, URL-encoded per call). Adapters translate MR semantics into the GitHub shapes GitService classifies: iid→number, source/target_branch→head/base with a merged bool, per-file diffs reassembled into unified-diff text (ShapedResponse text override, 3-page cap), approve-vs-note review routing (GitLab has no request-changes verb), pipelines/statuses reshaped into workflow_runs/check_runs, merge-method repo-key mapping, duplicate-MR 409→422. Reviewer mirroring is skipped (needs numeric ids RoboCo doesn't store); provisioning stays Phase 4. gitlab.com now auto-detects at registration like github.com; self-hosted GitLab sets the provider explicitly (panel picker enabled). Phase 2.1: neither Gitea nor GitLab has GitHub's server-side merges API — their merge_branch returns a shaped 501 and GitService.sync_env_branch now runs the shared local-git fallback (_local_merge_branch: throwaway clone → ancestor check → merge → push; a conflict aborts with the remote untouched; same status vocabulary as the merges-API path). Also aligns the whole tree with the full gate's tests/-scoped mypy (provider-test responder typing, e2e_smoke's stale owner/repo shapes). Gates: mypy 1229 files clean, xenon A, unit suite 6393 green, forge suites 85 green, panel typecheck/lint clean. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
d62ae20a87 |
fix(tests): delta-based cockpit assertions — CI's one-process run leaks rows (#578)
The full-suite CI run executes every test tree in one process against one database, so other suites' committed rows are visible to the Today-brief queries and the "empty company" / absolute-count assertions failed (assert 4 == 0). The suite now snapshots a pre-seed baseline and asserts deltas, seeds at priority 0 so its rows stay inside the brief's item caps, and uses a unique agent slug (the fixed be-dev-1 could collide with leaked rows). Verified by co-running with the known-polluting integration suites in one process. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
51de1df363 |
fix(tests): satisfy the full gate's tests/-scoped mypy (#577)
CI runs `mypy roboco/ tests/`; the bridge/cockpit suites were only gated against `mypy roboco/` locally. Real credentials dataclass instead of SimpleNamespace, AsyncMock casts where mocks sit behind typed fields, a return annotation on the fake stream, a None-guard on the consumer task, and a fresh registry lookup where mypy's literal narrowing read an assert as always-false. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
baa87d584a |
feat(tg): Mini App V4 — Today brief, native approvals, live data, bot tier, chat bridges (#576)
* feat(tg): P0 — dev mock bridge + Telegram-native foundations
Mini App V4 phase 0. The (tg) shell gains the groundwork every later
phase builds on:
- Dev mock bridge: outside Telegram, a development build falls back to a
no-op WebApp object and skips the webapp-auth POST (the regular panel
session cookie authorizes API calls), so the cockpit is workable in a
plain browser. Production keeps the "Open from Telegram" wall.
- Telegram theme adoption: themeParams map onto the shadcn CSS variables
scoped to #tg-shell (desktop dashboard untouched), colorScheme drives
the dark class, themeChanged re-applies live. Non-hex values are
dropped at the trust boundary.
- Viewport/swipe correctness: shell height rides Telegram's own
--tg-viewport-stable-height (100dvh fallback), vertical swipe-to-close
disabled so list scrolling can't dismiss the app.
- Native chrome bindings: TgWebAppProvider context plus useMainButton /
useBackButton declarative hooks and a null-safe haptics helper —
consumers never touch window.Telegram directly.
* feat(tg): P1 — Today home tab + one-round-trip /telegram/today brief
Mini App V4 phase 1: the cockpit now opens on a "Today" brief answering
"does anything need me?" in one glance.
Backend: GET /api/telegram/today (CEO-gated, rate-limited) returns the
whole brief in one round trip via the new TgCockpitService — needs-you
items (awaiting-CEO + blocked tasks capped for a phone screen, held-draft
counts across release/X/video/roadmap queues), a fleet snapshot with
per-agent current-task titles, today's spend from the day rollup
(degrading to zeros on a usage hiccup, mirroring the CEO overview), and
ship state. Deliberately DB-only: no live GitHub calls, no readiness
snapshot (that path clones), no orchestrator singleton — the CI
red/green proxy is the set of open ci_watch fix tasks.
Panel: TgTodayTab is the new default tab (Gauge icon) — needs-you rows
and draft chips deep-link into the tab that acts on them (with a haptic
tap), fleet/spend/ship render as dense cards, 45s refetch until the P3
WebSocket wiring lands.
* fix(tg): dev mock engages when the CDN bridge loads outside Telegram
Live browser smoke caught it: a bare tab still loads telegram-web-app.js,
so window.Telegram.WebApp EXISTS outside Telegram — just with empty
initData. The dev fallback keyed on a null bridge only, so a dev browser
went down the real-auth path and posted empty initData instead of
mounting the mock. The fallback now treats bridge-with-no-initData the
same as no bridge (a real Telegram launch always carries initData);
production behavior is unchanged.
* feat(tg): P2 — native approvals card stack
Mini App V4 phase 2: the Approvals tab stops stacking the four desktop
queue cards and becomes a phone-native flow — one normalized list across
release proposal / X drafts / video drafts / proposed roadmap items, and
a full-context detail per item:
- Release: version/bump/gate badges, changelog draft, gaps, migration
notes, in-flight + failed-execute banners; approve runs the fail-closed
executor, reject requires a substantive change request (10 chars).
- X: editable body with the live 280 counter, replied-to mention quoted;
approve sends the edited body only when actually edited.
- Video: cut-toggled player (blob-fetched through the authed client — a
bare <video src> would 401), per-platform caption edits with 280/2200
counters; approve sends only checked-in edits.
- Roadmap: the PO's full pitch (description, rationale, ACs); approve
materializes into the backlog per item.
The detail's primary action rides Telegram's native MainButton and back
navigation rides the BackButton, with visible fallbacks outside Telegram
(dev mock, old clients). Haptics fire on outcomes. An acted-on item
vanishes from the refetched queue, popping back to the list by
construction. A failed queue source is surfaced ("list may be
incomplete" / "couldn't load") instead of masquerading as an empty
queue — caught live in the browser smoke.
* feat(tg): dev demo mode — /tg?demo=1 renders canned cockpit data
Development-only: with the flag param present, the Today brief and the
four approval queues resolve typed fixtures (dynamically imported, so
production bundles never carry them) instead of hitting the backend —
the cockpit is fully browsable with zero stack running. Mutations still
go to the real API and fail loudly; it's a showroom, not a simulator.
* feat(tg): P3 — cockpit rides /ws/system live
Chat adopts the desktop A2A invalidate-on-frame idiom over the shared
ref-counted /ws/system socket: every a2a.message frame refreshes the
conversation list and the affected thread, missed-frame gaps are healed
by a reconnect refetch, and the 10s thread poll turns off entirely while
the socket is up (it remains the fallback). The Today brief refreshes on
each USAGE_SNAPSHOT push so the spend line tracks the sweeper live, with
the 45s poll as the socket-down fallback. No new sockets, no backend
changes — the WS gate already accepts the cloud-auth session cookie.
* feat(tg): P4 — deterministic bot command tier + self-syncing menu
Mini App V4 phase 4 (deterministic half): three new bot commands beside
/status /queue /task —
- /agents: who's mid-task right now, from the same TgCockpitService
fleet snapshot the Today brief renders (now public `fleet()`).
- /usage: today's spend from the day rollup.
- /blocked: awaiting-you + blocked tasks, deep-linked into the panel,
capped per section, titles HTML-escaped.
BOT_COMMANDS is the single registry driving /help AND a once-per-process
Bot API setMyCommands sync on the first poll cycle (new client method,
best-effort), so the Telegram command menu can never drift from what the
code implements. The interactive tier (/secretary, /newtask riding a
live Intake interview in-thread) is specced but not in this commit.
* feat(tg): direction-C styling pass — Telegram palette, RoboCo voice
The cockpit stops wearing default-shadcn and gets its own visual
language on top of the P0 themeParams bridge (colors stay CSS-variable
driven, so inside Telegram everything still adopts the user's theme):
- Shared primitives (components/tg/ui.tsx): TgSection grouped cards with
tracked micro-label headers, TgRow list rows (44px targets, press
feedback, 1/2-line clamp), TgRowIcon glyph tiles, TgStat tabular-nums
figures. Every tab composes the same three, so density and rhythm are
identical across the surface.
- Shell renders a centered 430px column (sm:border-x) — the phone UI no
longer stretches across a desktop dev browser.
- Tab bar: tighter type, active stroke-weight shift, backdrop blur.
- Today: needs-you count badge, divided task rows with inline blocked
marker, fleet as mono-named rows, spend/ship as stat tiles.
- Approvals rows as icon-tile cards; detail header gains the kind glyph.
- Inbox/Chat rows aligned to the same card language.
* feat(tg): P5 — /secretary and /newtask live-chat bridges
The bot's interactive tier: both commands bridge the CEO's Telegram chat
into the same in-process runtimes the panel drives — the persistent
Secretary container and the scoped Intake interview.
There is no synchronous send→reply seam (replies land on the session's
single-consumer relay queue), so each bridged session runs one long-lived
consumer task (roboco/services/telegram_bridge.py) that drains
PrompterLiveRegistry.stream and pushes one Telegram message per completed
turn. While a session is live, plain chat text IS the conversation;
/end closes it.
/newtask resolves the intake scope (single project auto-picked, multiple
offered as a tap-to-pick keyboard holding the initial text), and the
interview happens in-thread. A draft proposal renders as a card with
Send-to-Board / Discard buttons: confirm routes through the normal
board-review path (PrompterService.confirm_live_draft, route=board) and
PARKS the session — board feedback later streams straight back into the
same thread, closing the redraft loop from the phone. MegaTask batches
still confirm in the panel only.
The consumer's open stream arms the registry's 60s keepalive, so the
bridge runs its own idle TTL (same setting, parked sessions exempt).
State is per-process in-memory by design (the _PENDING_REPLIES posture);
intake/secretary containers are process-wide singletons, so a bridged
session preempts a live panel session of the same kind by construction.
* feat(tg): cockpit skin — RoboCo dark deck with a constant amber accent
The cockpit no longer inherits the dashboard's white default outside
Telegram: #tg-shell carries its own standing skin (deep slate surfaces,
amber primary) so the Mini App looks like RoboCo everywhere. Inside
Telegram the themeParams bridge now overrides SURFACE tokens only —
background/card/text/hint/border repaint to the user's Telegram theme
while --primary/--ring stay RoboCo amber: Telegram's surfaces, RoboCo's
voice. Demo fixtures also rewritten to neutral content (they previously
depicted unbuilt forge work and already-shipped roadmap items as live).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
945ce006fd |
fix(git): hard-sync target branch to origin instead of bare pull
_sync_target_branch ran a bare `git pull` after checkout; on a workspace clone whose local target branch diverged from origin (inevitable once remote history is rewritten) modern git fatals with "Need to specify how to reconcile divergent branches". On the CEO approve-and-merge path the GitHub merge had already landed, so the failure surfaced as a spurious 400 "Merge failed" and every retry re-hit the same wedged clone. Sync is now fetch + `reset --hard origin/<branch>` — remote-authoritative, matching the helper's intent and self-healing the divergence for all call sites (CEO merge, PM 409-retry sync, post-merge best-effort sync). |
||
|
|
7e01c0cecf |
feat(marketing): project-branded drafts + project badges on the X/video queues (#570)
Item B+C of the video/X per-project targeting spec, plus the company_goals.company_name field they depend on (migration 075). - CompanyGoalsService.resolve_product_name is the single fallback chain (project name -> charter company_name -> RoboCo); XEngine and VideoEngine both call it and their prompt builders are pure functions taking product_name — release posts/videos stop hardcoding RoboCo. - The X and video queue responses carry project_slug/project_name via one shared unloaded-guard helper (api/schemas/project_fields.py); both panel queues render a shared ProjectBadge so multi-project drafts are tellable apart. - Business -> Goals editor gains the company-name input. - Fixes a pre-existing test-isolation leak: the company-goals routes test commits the charter singleton into the session-scoped test DB and polluted later suites; it now deletes the row on teardown. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
b0dcb03356 |
fix(marketing): release and spotlight drafts carry their source project
draft_release_post / draft_release_video accept a project_id and the release-proposal approve hooks pass the proposal task's own project; the spotlight companion video forwards the spotlight draft's project into open_video_task — previously it always authored against the deployment-anchor project's motion/ tree regardless of which project the spotlight was about. Omitted project_id keeps the anchor-project fallback, so single-project deployments are unchanged. |
||
|
|
fecb021eef |
fix(release): version detection accepts manifest variants, never crashes the sweep
_pyproject_version read pyproject.toml with no error handling, so a non-Python project failed the release-manager cycle every interval forever. _project_version now probes pyproject.toml, package.json, Cargo.toml, then a bare VERSION file; missing or unparseable manifests are skipped and no manifest degrades to an empty version. |
||
|
|
99224e65cd |
fix(tests): satisfy mypy on the queue-item push tests
The #568 tests unpacked AsyncMock.await_args without narrowing its Optional type and passed a SimpleNamespace where _format_task_detail is annotated TaskTable. Narrow with the repo-standard 'assert await_args is not None' and cast the fake task at the call sites. |
||
|
|
06cc986f06 | Merge branch 'slave' of https://github.com/rennf93/roboco into slave | ||
|
|
d441fb2591 |
fix(git): dedupe check-runs per name so cancelled duplicates can't mask green
_classify_check_runs counted any completed cancelled check-run as failing. The push + pull_request double-trigger leaves cancelled same-name check-runs on the same head SHA next to the surviving run's green, so the pr_pass gate saw permanent red on a genuinely green PR. Keep only the newest (highest-id) run per check name before classifying. |
||
|
|
ff78618b76 |
feat: Telegram messages get real formatting + push DMs at draft origination (#568)
* feat(telegram): HTML-styled bot messages + push DMs at held-draft origination * fix(telegram): attr-context escaping, balance-aware truncation, send observability; docs --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
9fbec78126 |
feat: Telegram V2 — inbound commands + actionable approve/reject from chat (#551)
* feat(telegram): V2 inbound — command router, actionable approve/reject keyboards, chat-gated poll loop * fix(release,x,video,telegram): terminal-state guards on approve/reject; sender-identity check * docs(map,rag): Telegram V2 inbound surfaces and terminal-state approve/reject guards --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
496c24d186 |
feat: git hygiene (branch/preview reaping + cleanup sweep) and panel charts; work sessions under Git (#548)
* feat(panel): session-start, 7d overview spend, and 30d business spend charts * feat(panel): surface work sessions as a Git page tab (route was orphaned) * feat(git): reap spent task branches and render previews at lifecycle chokepoints; guarded stale-branch sweep * feat(panel): stale-branch cleanup button on the Git page * fix(git,panel): cursor-resumable sweep, force-delete spent refs, local filter state * docs(map,rag): branch/preview reaping, cleanup sweep, git-tab work sessions, wave-2 charts --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
4c8a9fc008 |
fix(video): unconfigured platforms are skipped, not pending-forever failures (#545)
A draft targeting a platform with no credentials could never complete: the X leg posted and committed its id, the TikTok leg failed as 'transient', and the card sat pending in the CEO queue with retry semantics that structurally cannot succeed. Unconfigured platforms are now an explicit skip: the draft COMPLETES when every configured platform has posted (detail names what was skipped), an approve whose targets are all unconfigured refuses loudly instead of silently completing, and genuine post failures keep their partial/retry semantics. Re-approving a parked card clears it without re-posting (the already-posted guard is covered by a dedicated test). Verified: 32/32 test_video_post_service (3 new: skip-and-complete, all-unconfigured refusal, re-approve recovery), ruff/mypy/xenon clean. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
e9ca7d4036 |
Delegation detail-fidelity + PM-loop hardening (#541)
* feat(gateway): delegation detail-fidelity — details survive hand-off, both directions
Details thinned out at every delegation hop: a PM child task mapped to no
parent criterion was legal (coverage only surfaced at submit_up, after the
whole wave ran — a 12-subtask docs tree grew through 8 review rounds that
way, one child titled 'docs page and route wrapper' shipping only the
page), and QA could pass work on a gestalt read (a 4-scene video brief
shipped 3 scenes past every gate because the features existed only in
prose). Three chokepoint gates:
- delegate (down): every child must declare covers_parent_criteria
resolving against the parent's real acceptance criteria — no mapping or
an unresolvable ref rejects naming every offending child and the valid
criteria; the success envelope carries parent_ac_coverage
{covered, uncovered} so a wave-planning PM sees remaining gaps in the
same turn. Full coverage stays enforced at submit_up (waves stay legal).
- pass_review (up): mandatory criteria_verified — one {criterion,
evidence} entry per task AC, matched by the findings ledger's
id-or-exact-text matcher, evidence soup-checked and capped; rejects
naming the unverified criteria; entries render deterministically into
qa_notes as '[AC] <criterion> — verified: <evidence>' lines. The old
count-only ac_verdicts gate is superseded (arg kept for back-compat).
- video briefs (structured detail at origination): an enumerable feature
list (release highlights, or input_props.highlights carried onto a
reject re-author) becomes its own scene acceptance criterion, bounded to
the AC caps; a re-author without highlights carries the
feedback-addressed criterion instead.
Extracted findings.py's criterion matcher into shared unmatched_criteria /
uncovered_acceptance_criteria instead of duplicating it; criteria_verified
joins the WAF free-text exclusion set like findings/issues.
* fix(gateway): break the block/unblock wedge — four hardening fixes from the live PM loop
A cell task looped fe-pm/main-pm block/unblock for hours (10 cycles, 43
spawns): a transient GitHub API error resolving CI became an unwaivable
blocker finding whose own fix text said no code change was required, the
submit freshness guard then demanded a commit no finding called for,
escalate_up auto-blocked, and main-pm's correct recovery plan 422'd on
the approach length cap, degrading it to a bare unblock. Four fixes:
- pr_pass CI-unresolvable refusal is now explicitly transient-worded:
retry pr_pass shortly, do NOT pr_fail over a CI-status lookup error —
a platform blip is not a code finding
- submit freshness guard grants ONE unchanged-head resubmission per
head sha when the findings ledger has zero open rows (all addressed
without code changes) — stamped via the resubmit_unchanged_head
marker so the same head can never loop a second time
- unblock carries a flip breaker: block_flip_count marker, and at the
third flip a one-shot CEO notification flags the task as structurally
wedged (unblock itself still succeeds — the breaker signals, it does
not wedge recovery)
- i_will_plan's approach cap truncates at 800 chars instead of
rejecting — an over-detailed plan must never cost the PM its turn
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
aa15dc40cc |
Feature/video artifact verification (#537)
* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe get_latest_ci_conclusion defaults to the ladder's head rung, so wait_for_ci searched slave for a release commit that lives on master and timed out after 40 minutes with the run already green. The wait now passes the prod branch explicitly. Also fixes the react/no-unescaped-entities error that turned master's Panel CI red. * fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's click handler on the Tooltip root, which renders no DOM — the agents Spawn item and the KB Reindex-All / Delete-index confirms were dead. Tooltips now wrap the triggers. The video renderer accepts interior single dots in composition ids (release-0.25.0) with '..' still unrepresentable, and propose_video refuses an unrenderable id at authoring time. * fix(dispatch): restart-safe PM review turns A leaf task in awaiting_pm_review had no periodic pickup: the closure dispatcher bailed on childless tasks and skipped PR-bearing review tasks as already-promoted, assuming the submit-time PM session was still alive — an assumption every restart breaks. Proven live on the docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked its sibling dev task. Childless awaiting_pm_review tasks now flow to the PM's review turn, and the merge turn respawns its PM when none is active. * feat(video): verify the rendered artifact, not the source The 14s release-0.25.0 cut shipped with only one of four scenes visibly registering: the dev authored DOM, the smoke asserted DOM, QA read code — nobody consumed the rendered MP4 before the CEO did. Close that loop, and the reject loop behind it: - sidecar frames mode: POST /render with frames=1..32 renders the cut, ffprobes the REAL duration, extracts midpoint-sampled keyframe PNGs (timestamps in filenames), streams a tar.gz back with X-Video-Duration - request_render do-verb (developer/QA, request_sandbox's shape): renders the caller's ACTUAL composition — dev's own worktree (head_sha/dirty provenance), QA a read-only git-archive export of the assembled branch — extracts frames to the container-shared .previews/ path, stamps the render_preview marker, returns the paths as envelope evidence - gate: i_am_done on a source=video task refuses without a stamped render_preview (Requirement.RENDER_VERIFIED; canonical source string moved to foundation as markers.VIDEO_TASK_SOURCE; mirrored in the possibilities-matrix fast path so it cannot bypass the check) - QA claim_review evidence carries video_context (composition id, the dev's preview, a re-render instruction) so review checks output - dev spawn prompt block + a 4th authoring AC order Read-every-frame verification before submitting - reject -> re-author: a CEO reject with a reason opens a fresh authoring task carrying the verbatim feedback + a revise-in-place pointer at the existing composition (best-effort, never fails the reject) — rejection feedback no longer dies on the cancelled draft E2E: rendered the committed release-0.25.0 composition through the new frames mode locally — the returned keyframes show exactly the reported failure (blank frame at 5.8s, only 'Env ladder' by 12.8s), the check the fleet was missing. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
797847e379 |
[1dae04a7] Video: release 0.25.0 (revision) (#536)
* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe get_latest_ci_conclusion defaults to the ladder's head rung, so wait_for_ci searched slave for a release commit that lives on master and timed out after 40 minutes with the run already green. The wait now passes the prod branch explicitly. Also fixes the react/no-unescaped-entities error that turned master's Panel CI red. * fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's click handler on the Tooltip root, which renders no DOM — the agents Spawn item and the KB Reindex-All / Delete-index confirms were dead. Tooltips now wrap the triggers. The video renderer accepts interior single dots in composition ids (release-0.25.0) with '..' still unrepresentable, and propose_video refuses an unrenderable id at authoring time. * fix(dispatch): restart-safe PM review turns A leaf task in awaiting_pm_review had no periodic pickup: the closure dispatcher bailed on childless tasks and skipped PR-bearing review tasks as already-promoted, assuming the submit-time PM session was still alive — an assumption every restart breaks. Proven live on the docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked its sibling dev task. Childless awaiting_pm_review tasks now flow to the PM's review turn, and the merge turn respawns its PM when none is active. * [1dae04a7] Revise release-0.25.0 composition to 40s scene-based pacing with four feature cards * [1dae04a7] docs(motion): update release-0.25.0 README section for 40s four-card revision --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech> |
||
|
|
817f7f23ac |
fix(release): per-clone committer identity; signing opt-in
The fresh release clone in the orchestrator container has no git identity, so the release commit refused with 'Author identity unknown' — and the unconditional -S would have failed next on the keyless container. commit_and_push now sets a configurable bot identity on the clone and signs only when ROBOCO_RELEASE_SIGN_COMMITS is armed with a mounted key. |
||
|
|
ce5e263b79 |
fix(release): gate on the head rung's CI verdict, not an in-container test run
make quality inside the production orchestrator container fails on ~1000 clean-env assumptions (armed compose flags, live Redis, host mounts) for a tree that is green in CI — proven live on the first org-proposed release. The execute-time gate now re-verifies the head rung's CI conclusion, fail-closed on absent or red with branch, sha, and conclusion in the failure detail; the pushed release commit keeps its own CI wait before publish. |
||
|
|
bdb0dd6cdd |
feat(release): drafter prefers curated [Unreleased] notes; executor moves them instead of duplicating
The readiness drafter transcribed raw commit subjects even when [Unreleased] carried curated prose, and the executor inserted its entry below a still-populated [Unreleased] — shipping the same content twice in two qualities. The drafter now uses the curated body as the release entry when present (transcription stays the fallback; completeness gaps still police curation), and the executor empties [Unreleased] as it stamps the entry. [Unreleased] itself catches up with the feedback round, the dense tooltip passes, and the slave-CI fix. |
||
|
|
236aab18f5 |
fix(infra): mypy 2.3 compat; _ReleaseContext.prod_branch; deny bare uv sync; telegram compose flag
Renames the smoke-replay loop variable mypy 2.3's stricter narrowing rejects (the uv.lock bump made this the promotion blocker), renames _ReleaseContext.default_branch to prod_branch to match what it holds since the env ladder, extends the Makefile-gated guard to bare uv sync on both runtimes (shared-cache poisoning is the race the guard exists for), and lists ROBOCO_TELEGRAM_ENABLED in both compose files (byte-identical). |
||
|
|
f1ff149b70 |
fix(api): unconditional /git/file window cap; defer Telegram sends after commit; docs/map periodic re-index
_compute_file_range now caps any resolved window at _FILE_MAX_LINES instead of only the exact whole-file shape, closing the near-whole-file bypass. Telegram sends ride a generalized after-commit outbox (defer_after_commit over the F107 machinery) so a slow Bot API can no longer hold the caller's transaction open; TelegramClient grows an abstract close(). The KB update loop iterates AUTO_INDEX_DIRS so docs/map edits re-index without a restart. PR-label application catches all exceptions per its never-raises contract, and pr_merge's CEO-only message names the resolved branch. |