mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(gateway): reviewer/PM collision map (W5)
The collision surface (intends_to_touch / adds_migration / touches_shared)
is authored at delegate time, consumed once by SequencingService to wire
dependency edges, then never shown to a reviewer again. This surfaces it:
- Pure builder (services/gateway/choreographer/collision.py): for a task
under review, the surfaced siblings (same parent) that would collide —
file-overlap globs or a shared migration chain (both adds_migration) —
with the overlapping globs and a declared-vs-actual drift check. No
DB/IO; callers fetch siblings (one indexed get_subtasks query, mig 069)
+ actual files (git). Caps: 10 siblings, 5 globs.
- Evidence envelopes: collision_context block injected into QA
claim_review, PR-gate claim_gate_review (both carry real touched files
so drift is populated), and the PM i_will_plan briefing (no actual
files at plan time, drift omitted). Best-effort — a failure omits the
block, never breaks the verb/briefing. Empty block omitted (zero token
cost via _EVIDENCE_OMIT_WHEN_EMPTY).
- Panel: GET /api/tasks/{id}/collision-map (declared surface + sibling
overlap; no drift — the panel route resolves no workspace) + a Collision
tab on the task detail (8th tab). Mock-mode returns an empty map.
- docs/map added to the RAG auto-index dirs so the collision-map concept
is fleet-retrievable; skipped gracefully if the dir is absent.
19 new tests (15 unit on the pure builder + 4 integration on the route).
Gate green: ruff/mypy/xenon (module rank A)/pytest 13000/coverage 94.81%,
panel typecheck/lint/516 tests.
* [w6-telegram] Add Telegram notifications bridge (V1)
CEO-facing Telegram DM bridge, flag-gated off by default
(ROBOCO_TELEGRAM_ENABLED). Mirrors the X-credentials / X-client pattern:
- TelegramCredentialsTable (migration 073) — singleton Fernet-encrypted
bot_token + chat_id, all-or-nothing set/clear; API never returns plaintext.
- TelegramClient ABC / NullTelegramClient (no-op, configured->False, never
raises) / LiveTelegramClient (httpx POST sendMessage) / build_telegram_client
factory (Null when creds unset).
- /telegram/credentials CEO-only routes (write-only, guard-decorated).
- Best-effort _notify_telegram fan-out from the two CEO-notify producers
(notify_ceo_of_escalation, notify_ceo_of_completion) — guarded by the flag,
never raises into the producer, carries a panel deep-link when
panel_base_url is set.
- panel credentials card (2 fields) nested in the Telegram feature-flag row.
- panel_base_url + telegram_timeout_seconds config fields.
V1 scope only: credentials + flag + panel card + client + one-line fan-out.
Out of scope (V2): inbound commands, a TelegramEngine background loop, a
dedup ledger, a bus subscription.
* [w6-telegram] fix: slave mypy/xenon regression (product tests + helper extract)
Pre-existing on slave from prior session's merges — no PR's CI caught them
(squash merges don't re-CI the result; each branch was based on older slave).
- test_product: _product helper returned MagicMock -> list invariant error;
cast to ProductTable, move import under TYPE_CHECKING.
- test_usage: svc.session.execute (AsyncSession) has no call_args_list;
cast to MagicMock at the two call sites.
- product.progress_for_products: xenon rank C -> extract module-level
_project_to_products_map helper (repo pattern: helper-extract).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
322 lines
15 KiB
TypeScript
322 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { settingsApi } from "@/lib/api";
|
|
import type { FeatureFlag } from "@/lib/api/settings";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { useState } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { XCredentialsForm } from "@/components/settings/x-credentials-card";
|
|
import { TikTokCredentialsForm } from "@/components/settings/tiktok-credentials-card";
|
|
import { TelegramCredentialsForm } from "@/components/settings/telegram-credentials-card";
|
|
import { cn } from "@/lib/utils";
|
|
import { Flag, ChevronDown, ChevronRight } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
// One-line blurb per flag so the operator knows what each master switch gates.
|
|
const FLAG_DESCRIPTIONS: Record<string, string> = {
|
|
external_pr_enabled:
|
|
"Discover and review inbound external/fork pull requests.",
|
|
internal_pr_enabled:
|
|
"Run the read-only safety reviewer on internal branch PRs.",
|
|
research_enabled: "Let the Board and PMs run web research.",
|
|
strategy_engine_enabled: "Generate and maintain company strategy artifacts.",
|
|
self_heal_enabled: "Watch RoboCo's own CI and notify you when it regresses.",
|
|
self_heal_originate_enabled:
|
|
"Also open a PENDING fix task for a detected regression (needs Self-healing on; the task waits for your approval).",
|
|
provisioning_enabled: "Auto-provision projects from approved pitches.",
|
|
toolchain_match_enabled:
|
|
"Provision each agent workspace with the target project's Python (not RoboCo's) and block delivery gates when its test suite can't be executed.",
|
|
conventions_enabled:
|
|
"Enforce a per-project architectural standard (.roboco/conventions.yml): inject the map, attach baseline constraints, and block i_am_done / pr_pass on misplaced definitions or lint suppressions.",
|
|
possibilities_matrix_enabled:
|
|
"When a task's work is already done (commits + open PR + all acceptance criteria addressed + no open findings), submit it for QA in one i_am_done call instead of 3-6 turns — skips the retroactive plan, journal tracing, and local quality (CI-green proxy) gates. Off by default: the standard path is unchanged until you arm this.",
|
|
rag_auto_update_enabled:
|
|
"Keep the knowledge base index refreshed automatically.",
|
|
transcript_prune_enabled:
|
|
"Run the background sweep that prunes old transcripts.",
|
|
gateway_health_enabled:
|
|
"Recover an agent whose MCP gateway has broken (it can run no tools) while its container stays up — kill + respawn it instead of shielding it from the reaper forever.",
|
|
ci_watch_enabled:
|
|
"Watch every opted-in project's CI and open a fix task when its default branch goes red (per-project opt-in; never auto-merges).",
|
|
dep_update_enabled:
|
|
"Periodically probe opted-in projects for dependency updates and open an update task when a lockfile would change (per-project opt-in; never auto-merges).",
|
|
env_sync_enabled:
|
|
"Cascade each project's declared environment ladder prod→dev via GitHub's merges API so dev never falls behind prod; a conflicted rung opens a sync PR for you to merge (per-project opt-in; never pushes prod).",
|
|
release_manager_enabled:
|
|
"Run the deterministic release-readiness sweep and propose a release for you to approve or reject — it never publishes without your approval, and the executor is fail-closed on a red gate.",
|
|
org_memory_enabled:
|
|
"Close the learn→reuse loop: distill a lesson at task completion, index journal reflections, and auto-inject similar past lessons + approved playbooks into an agent's briefing on claim.",
|
|
sandbox_db_enabled:
|
|
"Provision a throwaway Postgres/Redis sibling container per agent spawn (per-project opt-in) instead of injecting RoboCo's own production DB credentials into the agent's gate.",
|
|
routing_strict:
|
|
"Fail-closed model routing: refuse to silently downgrade an agent to the legacy Anthropic path when its configured provider is disabled (raise instead). Off => graceful degradation with a warning.",
|
|
x_engine_enabled:
|
|
"Draft release-announcement posts for the X (Twitter) account — every draft is held in a queue for you to edit, approve, or reject; nothing posts automatically, and it stays inert until you set credentials in the X card below.",
|
|
x_replies_enabled:
|
|
"Also poll X mentions and draft replies (still held for your approval — nothing auto-replies). Off by default: reading mentions needs a paid X API tier, so leave this off if you only want release posts.",
|
|
x_feature_spotlight_enabled:
|
|
"Periodically spawn the Head of Marketing to investigate RoboCo's shipped features and draft a spotlight post for an under-publicized capability — held in the same X post queue below for your approval. Needs x_engine_enabled and X credentials.",
|
|
roadmap_engine_enabled:
|
|
"Weekly: the Product Owner explores the company's projects and proposes a themed cycle of 3-7 roadmap items — you approve or reject each one individually; approved items land in the backlog and nothing auto-starts.",
|
|
fable_mode_enabled:
|
|
"Compose the Fable behavioral doctrine into every agent's system prompt and install the matching turn-discipline/honesty/verification hooks at spawn (both Claude Code and grok runtimes). Off by default; spawn path is byte-for-byte unchanged.",
|
|
video_engine_enabled:
|
|
"Master switch for the video-generation engine — a UX/UI dev authors a bespoke Remotion composition per trigger, then a render pass produces the 9:16/1:1 MP4 and holds it here as a draft. Even when on, distribution needs an explicit per-clip approval below; set X / TikTok credentials to post.",
|
|
video_on_release:
|
|
"Also open a video-authoring task when a release publishes. Off by default even with video_engine_enabled on.",
|
|
video_on_spotlight:
|
|
"Also open a video-authoring task when you approve a feature-spotlight draft that requests one. Off by default even with video_engine_enabled on.",
|
|
vault_report_enabled:
|
|
"Materialize a weekly org-report note (velocity, cycle time, rework, cost) in the vault's Reports/ folder and notify you — deterministic numbers, no LLM. Needs the Obsidian vault projection on.",
|
|
vault_kb_enabled:
|
|
"Index your own vault notes (default RoboCo/Notes/) into the knowledge base so the fleet can retrieve what you write — every note is screened for injection attempts before it's indexed. Needs the Obsidian vault projection on.",
|
|
telegram_enabled:
|
|
"Best-effort Telegram DMs to you alongside in-app notifications when a task is escalated for your approval or completes. Server-side fan-out — never blocks the in-app notification. Stays inert until you set bot-token + chat-id credentials in the Telegram card below.",
|
|
};
|
|
|
|
export function FeatureFlagsCard() {
|
|
const queryClient = useQueryClient();
|
|
const [xCredsOpen, setXCredsOpen] = useState(false);
|
|
const [tiktokCredsOpen, setTiktokCredsOpen] = useState(false);
|
|
const [telegramCredsOpen, setTelegramCredsOpen] = useState(false);
|
|
// Off-transition awaiting operator confirm. Null = no dialog open.
|
|
const [confirmFlag, setConfirmFlag] = useState<FeatureFlag | null>(null);
|
|
// Every in-flight toggle key — added on mutate, removed on settle. Tracks
|
|
// concurrent toggles so each row locks independently of the others.
|
|
const [pendingKeys, setPendingKeys] = useState<Set<string>>(new Set());
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ["feature-flags"],
|
|
queryFn: settingsApi.getFeatureFlags,
|
|
});
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: ({ key, enabled }: { key: string; enabled: boolean }) =>
|
|
settingsApi.setFeatureFlag(key, enabled),
|
|
onMutate: ({ key }) => {
|
|
setPendingKeys((s) => {
|
|
const n = new Set(s);
|
|
n.add(key);
|
|
return n;
|
|
});
|
|
},
|
|
onSuccess: (_data, { enabled }) => {
|
|
queryClient.invalidateQueries({ queryKey: ["feature-flags"] });
|
|
toast.success(
|
|
`Feature ${enabled ? "enabled" : "disabled"} — takes effect on next restart`,
|
|
);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(
|
|
`Failed to update: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
},
|
|
onSettled: (_data, _err, { key }) => {
|
|
setPendingKeys((s) => {
|
|
const n = new Set(s);
|
|
n.delete(key);
|
|
return n;
|
|
});
|
|
},
|
|
});
|
|
|
|
const flags: FeatureFlag[] = data?.flags ?? [];
|
|
const note = data?.note ?? "Changes take effect on the next backend restart.";
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Flag className="h-5 w-5" />
|
|
Feature Flags
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Master switches for optional subsystems. Unset flags fall back to the
|
|
environment default. {note}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Loading feature flags…
|
|
</p>
|
|
)}
|
|
{!isLoading && flags.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">
|
|
No feature flags available.
|
|
</p>
|
|
)}
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
{flags.map((flag) => {
|
|
const isXEngine = flag.key === "x_engine_enabled";
|
|
const isVideoEngine = flag.key === "video_engine_enabled";
|
|
const isTelegram = flag.key === "telegram_enabled";
|
|
return (
|
|
<div
|
|
key={flag.key}
|
|
className={cn(
|
|
"rounded-lg border p-4",
|
|
(isXEngine || isVideoEngine || isTelegram) && "md:col-span-2",
|
|
)}
|
|
>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="min-w-0">
|
|
<Label htmlFor={`flag-${flag.key}`}>{flag.label}</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
{FLAG_DESCRIPTIONS[flag.key] ?? ""}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id={`flag-${flag.key}`}
|
|
checked={flag.enabled}
|
|
disabled={pendingKeys.has(flag.key)}
|
|
onCheckedChange={(checked) => {
|
|
// Off-transitions are destructive (a running subsystem
|
|
// stops on the next restart) — confirm before firing.
|
|
// On-transitions are low-risk and fire immediately.
|
|
if (checked) {
|
|
toggleMutation.mutate({ key: flag.key, enabled: true });
|
|
} else {
|
|
setConfirmFlag(flag);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
{isXEngine && (
|
|
<Collapsible
|
|
open={xCredsOpen}
|
|
onOpenChange={setXCredsOpen}
|
|
className="mt-3"
|
|
>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-between px-2 text-muted-foreground"
|
|
>
|
|
<span className="text-sm">X (Twitter) credentials</span>
|
|
{xCredsOpen ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="pt-3">
|
|
<XCredentialsForm />
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
{isVideoEngine && (
|
|
<Collapsible
|
|
open={tiktokCredsOpen}
|
|
onOpenChange={setTiktokCredsOpen}
|
|
className="mt-3"
|
|
>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-between px-2 text-muted-foreground"
|
|
>
|
|
<span className="text-sm">TikTok credentials</span>
|
|
{tiktokCredsOpen ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="pt-3">
|
|
<TikTokCredentialsForm />
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
{isTelegram && (
|
|
<Collapsible
|
|
open={telegramCredsOpen}
|
|
onOpenChange={setTelegramCredsOpen}
|
|
className="mt-3"
|
|
>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-between px-2 text-muted-foreground"
|
|
>
|
|
<span className="text-sm">Telegram credentials</span>
|
|
{telegramCredsOpen ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="pt-3">
|
|
<TelegramCredentialsForm />
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
<AlertDialog
|
|
open={confirmFlag !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) setConfirmFlag(null);
|
|
}}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Disable feature?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{confirmFlag
|
|
? `${confirmFlag.label} will turn off on the next backend restart. This may interrupt in-flight work depending on it.`
|
|
: ""}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => {
|
|
const flag = confirmFlag;
|
|
if (!flag) return;
|
|
setConfirmFlag(null);
|
|
toggleMutation.mutate({ key: flag.key, enabled: false });
|
|
}}
|
|
>
|
|
Disable
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</Card>
|
|
);
|
|
}
|