diff --git a/CHANGELOG.md b/CHANGELOG.md index 013e824b..3664068c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,16 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`. - **A built-in calendar for every date field.** Due dates, sprint and initiative dates, roadmap dates, the time-log range, and snooze now open Forge's own themed calendar instead of the browser's native date box — same look on every browser and OS, with month navigation and a clear button. - **Draft or improve an issue description with AI.** Next to the Description heading there's now an AI button — **Draft with AI** writes a description from the issue title when there's none yet, and **Enhance** rewrites an existing one for clarity and structure. You see the suggestion (with a current-vs-suggested view for an enhance) and choose **Apply** or **Discard** — nothing changes until you accept it. Appears only when AI is enabled for the workspace. - **Create a label without leaving the issue.** The label picker on an issue now has a search box — type to filter, and (if you're a workspace admin) pick **Create "<name>"** to make a new label with a name and color right there and apply it in one step. No more detour to Settings → Labels first. +- **Move issues between workspaces (instance admin).** A new **Admin → Move issues** page re-homes issues into another workspace: pick source and target, paste the issue ids, and **Preview** shows exactly what will happen — the new keys (issues are renumbered into the target), how each status and label maps, which labels get dropped, and which issues are *blocked* (anything with agent runs, plans, or artifacts is refused so nothing is corrupted). Confirm to move; a record is written in both workspaces' audit logs. +- **Cap self-service workspace creation.** Operators can set `MAX_WORKSPACES_PER_USER` to limit how many workspaces a single (non-admin) user can create; the default is unlimited, so nothing changes unless you set it. +- **Approve or reject a paused agent run right where you see it.** When an agent pauses for permission (a Codex/Hermes run flagged a command or file change), the run row in Mission Control's Live tab now shows **Approve** / **Reject** inline — no detour to the Command Center. +- **See a plan's wall-clock burn.** The plan cockpit's budget meter now shows elapsed time vs. the wall-time cap alongside cost, so a plan approaching its time budget is visible at a glance (matching the goal cockpit). ### Changed +- **Plans and the review inbox update live.** The Plans list and the Review-gate inbox now refresh on their own as plans, steps, runs, and gates change — no manual **Refresh**, matching the Goals list. +- **Orchestration screens are fully themed.** Every dropdown on the Plans, Goals, and Runtime-settings screens, plus the new-plan / new-goal / edit-goal dialogs, now use Forge's own pickers and one consistent dialog instead of native browser controls. + - **Confirmations, prompts, and dropdowns use Forge's own UI, not the browser's.** "Are you sure?" confirmations (deleting a note, removing an admin, restarting a run), the GitHub App setup prompts, and a batch of dropdowns now use Forge's themed in-app dialogs and pickers instead of the operating system's native popups — consistent look, keyboard-friendly, on-brand. (More dropdowns are still being converted.) - **The label color picker is now in-app.** Choosing a label color (in Settings → Labels and the new inline create) uses themed swatches plus a hex field instead of the operating system's native color dialog — consistent with the rest of Forge. @@ -30,6 +37,12 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`. - **The Coach stops posting junk or duplicate comments.** It now discards empty or meta AI responses (e.g. "Posted the diagnostic comment…") instead of posting them, and won't comment on the same issue more than once in 24 hours — so a stuck issue gets one useful diagnostic, not an hourly pile-up. - **AI triage works again — and tells you what's wrong when it can't.** The "AI triage suggestion" card on an issue could get stuck showing a bare *"AI triage unavailable."* with only a Retry, because some AI providers reply in plain text instead of the structured format Forge expected — so every suggestion was thrown away. Forge now reads those plain-text replies too, so triage produces a real priority / label / agent suggestion. When triage genuinely can't run, the card now explains why and links straight to **Settings → Workspace → AI** to fix it, instead of leaving you guessing. +- **A goal or plan that hits its budget actually stops spending.** When a plan reaches its cost or time cap (or you abandon its goal), its remaining steps stop dispatching immediately — previously a blocked plan could keep launching agents and spend the very budget you were asked to approve. +- **Finished work stays finished.** A late or duplicate reviewer verdict can no longer knock a completed step back into "to-do" and re-run it. +- **Agent runs aren't falsely marked "stalled" after a deploy or restart.** A run whose runtime briefly can't report status — or that's quietly mid-work on a long step — now stays running, and is only marked stalled when it's genuinely idle. +- **Approving or stopping an agent run tells you if it didn't go through.** A failed Approve or Stop now surfaces an error instead of showing success while the run stays blocked at the runtime. +- **Goals and Plans tell a load error apart from an empty list.** A network hiccup now shows "couldn't load … Retry" instead of "No goals yet" / "Goal not found — may have been abandoned", which looked like your work had been deleted. +- **A freshly registered runtime is no longer a dead-end.** You can open a just-registered local daemon's settings — run its self-test, add credentials, bind a repo — straight from the global **Runtimes** page, without waiting for an agent to use it first. ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index f9a8b47f..da826fa9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -269,10 +269,15 @@ Claude adapter spawn: `FORGE_CLAUDE_BIN`. Missing binary → `[OFFLINE]` reply via `chat.finalizeDraft`, no crash. -Known v1 gaps: chat dispatch sees only `{threadId, messageId, agentId, -role}` from SSE; there's no `chat.getThread` MCP yet, so the prompt to -the local CLI is a placeholder. AGENT_ASSIGNED handler stubs a -placeholder comment. +Status (the old "v1 gaps" are closed): chat dispatch loads real thread +history via `chat.getThread`; `AGENT_ASSIGNED` runs a full agent loop +(`dispatch/issue-loop.ts`), not a stub; `runtimes.list` / `agents.list` / +`chat.getThread` MCP tools are all shipped. On (re)connect the daemon now +reconciles its unacked inbox (`agent.inbox.list`) so dispatch dropped +during a deploy/reconnect is recovered, and repeated 401/403s exit the +daemon non-zero ("run `forge login`") instead of looping. Real remaining +gaps: one linked agent per runtime (no multi-agent-per-runtime fan-out), +and ACP isn't wired for the issue-loop path. ## Attachments diff --git a/DEVLOG.md b/DEVLOG.md index 17f9ec55..381243a0 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,130 @@ > Append-only session log. Read at session start. Update at session end. +## 2026-07-06 — Agent-runtime audit: Phase 3 UI consistency (P3.2) + +Orchestration-surface polish on `worktree-audit-fixes`. Six sub-items, all +typecheck + lint + build clean. + +- **Tokens.** Swept the last ad-hoc `emerald-*` / `amber-*` (+ the dead + `destructive` class on the review page) to the semantic `--success` / + `--warning` / `--danger` tokens across plans/goals/review pages, + `orchestration-ui/status.ts`, `orchestration/{crew-roster-panel,step-node}`, + and `settings/runtimes`. The tokens already flip light/dark so the redundant + `dark:` variants collapsed. `destructive` is **not** a defined token (Tailwind + emits nothing) — it's a latent app-wide bug in ~45 files; only fixed the + review-page instance here, noted the rest for P3.4. +- **Live surfaces.** Plans index + review inbox now `useRealtime` (execution- + plan/step/agent-run and review-gate resp.), mirroring the goals index — no + more frozen list until manual Refresh. +- **Inline run approval.** `RunRow` renders the shared `RunApprovalCard` + (Approve session/once + Reject) instead of the "open Command Center" dead-end + its own doc comment already claimed it hosted. +- **Wall-time.** Plan detail budget meter now consumes the P1.7 `startedAt` + (elapsed / cap bar); `hasBudget` also trips on a wall-time-only cap. +- **Selects → Combobox.** All 7 native `` warnings — Phase 3 P3.2); orchestration suite **24 passed** +(4 new Phase-1 regression tests); `pnpm build:cli` clean. Local isolated stack on +:55432/:56379. Phase 2 (budget/lifecycle integrity, delivery idempotency, the two +security holes) + Phase 3 (cross-workspace move, UI consistency, connector parity) +still pending — see the audit memory. + ## 2026-06-28 — AXI-90 QuickCreate GitHub issue/PR import Enhanced the ⇧C QuickCreate issue overlay so a pasted GitHub issue/PR URL (or diff --git a/prisma/migrations/0091_plan_startedat_and_agentrun_sweep_indexes/migration.sql b/prisma/migrations/0091_plan_startedat_and_agentrun_sweep_indexes/migration.sql new file mode 100644 index 00000000..1a14cf83 --- /dev/null +++ b/prisma/migrations/0091_plan_startedat_and_agentrun_sweep_indexes/migration.sql @@ -0,0 +1,12 @@ +-- AlterTable: wall-time budget clock starts at execution (activatePlan), not +-- decompose, so planning + approval-wait isn't charged against the run clock. +ALTER TABLE "ExecutionPlan" ADD COLUMN "startedAt" TIMESTAMP(3); + +-- CreateIndex: status-leading index for the cross-tenant worker sweeps +-- (pollActiveRuns / ensureSubscriptions / stale watchdog) that filter on +-- status with no workspaceId and previously seq-scanned every 5s/60s. +CREATE INDEX "AgentRun_status_lastEventAt_idx" ON "AgentRun"("status", "lastEventAt"); + +-- CreateIndex: dedup lookup on the dispatch path (has this AGENT_ASSIGNED +-- event already opened a run?). +CREATE INDEX "AgentRun_assignmentEventId_idx" ON "AgentRun"("assignmentEventId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ac2b01d0..5c9445ad 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2594,6 +2594,15 @@ model AgentRun { /// Collapse stacked runs: list heads with `supersededByRunId IS NULL`, /// expand a thread by following the chain. @@index([supersededByRunId]) + /// Cross-tenant worker sweeps (pollActiveRuns / ensureSubscriptions / + /// stale watchdog) filter on `status` with NO workspaceId, so the + /// workspaceId-leading indexes above can't serve them and they seq-scan + /// every 5s/60s. A status-leading index keeps those hot sweeps off a + /// sequential scan as AgentRun history grows. + @@index([status, lastEventAt]) + /// Dedup lookup on the dispatch path: has this AGENT_ASSIGNED event + /// already opened a run for the agent? + @@index([assignmentEventId]) } /// Append-only timeline for an AgentRun. Drives the live pulse strip and @@ -3118,6 +3127,13 @@ model ExecutionPlan { maxTotalCostUsd Float? maxWallTimeMinutes Int? totalCostUsd Float @default(0) + /// When execution actually began — activatePlan flips DRAFT/APPROVED → + /// RUNNING and stamps this. Wall-time budget is measured from here, NOT + /// createdAt, so planning + approval-wait time isn't charged against the + /// execution clock (a plan approved days after decompose shouldn't BLOCK + /// on its first cost record). Null until first activation; legacy rows + /// fall back to createdAt. + startedAt DateTime? /// Marks the live decompose attempt for a goal. Prior attempts are /// flipped false when a new decompose runs. isActiveAttempt Boolean @default(true) diff --git a/src/app/(app)/admin/move-issues/page.tsx b/src/app/(app)/admin/move-issues/page.tsx new file mode 100644 index 00000000..7a43a193 --- /dev/null +++ b/src/app/(app)/admin/move-issues/page.tsx @@ -0,0 +1,21 @@ +"use client"; +import { AdminPage } from "@/components/admin-shell/admin-shell"; +import { AdminMoveIssues } from "@/components/admin-shell/admin-move-issues"; + +/** + * `/admin/move-issues` — instance-admin surface to move issues between + * workspaces (audit ask #2). Preview the full remap before executing. + */ +export default function AdminMoveIssuesPage() { + return ( + + + + ); +} diff --git a/src/app/(app)/settings/runtimes/page.tsx b/src/app/(app)/settings/runtimes/page.tsx index b0e7de97..f4902503 100644 --- a/src/app/(app)/settings/runtimes/page.tsx +++ b/src/app/(app)/settings/runtimes/page.tsx @@ -122,7 +122,11 @@ export default function RuntimesPage() { : r.health.tone === "warning" ? "border-warning/30 bg-warning/5" : "border-border bg-card/40"; - const settingsWorkspace = r.workspacesInUse[0]; + // Prefer a workspace the runtime is actively used in, but + // always fall back to its home workspace so the settings link + // is never a dead-end (a freshly registered daemon has no + // workspacesInUse yet but still needs self-test / secrets). + const settingsWorkspace = r.workspacesInUse[0] ?? r.homeWorkspace; return (
diff --git a/src/app/(app)/w/[slug]/goals/[goalId]/page.tsx b/src/app/(app)/w/[slug]/goals/[goalId]/page.tsx index a13252e7..e2343a81 100644 --- a/src/app/(app)/w/[slug]/goals/[goalId]/page.tsx +++ b/src/app/(app)/w/[slug]/goals/[goalId]/page.tsx @@ -5,7 +5,6 @@ import { useParams, useRouter } from "next/navigation"; import { AlertTriangle, Ban, - Check, ChevronLeft, Clock3, ExternalLink, @@ -20,7 +19,8 @@ import { toast } from "sonner"; import { Topbar } from "@/components/topbar"; import { Button } from "@/components/ui/button"; import { EmptyState, SkeletonList } from "@/components/ui"; -import { Confirm } from "@/components/ui/modal"; +import { Confirm, QuickForm } from "@/components/ui/modal"; +import { Combobox } from "@/components/ui/combobox"; import { trpc } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { useWorkspace } from "@/hooks/use-workspace"; @@ -72,6 +72,8 @@ export default function GoalDetailPage() { const { data: crewList } = trpc.agentCrew.list.useQuery({}); const goal = query?.data; const isLoading = available ? (query?.isLoading ?? true) : false; + // A fetch failure must not read as "Goal not found — abandoned or removed". + const isError = available ? (query?.isError ?? false) : false; // Reactivity: any orchestration event invalidates the goal so the // page repaints with fresh plan/step/budget state. The `goal` router @@ -110,7 +112,8 @@ export default function GoalDetailPage() { u.goal?.list?.invalidate?.(); setEditingGoal(false); }, - onError: (e: { message: string }) => toast.error(e.message), + // Errors surface in the GoalEditModal QuickForm banner (onSave awaits + // mutateAsync); the modal stays open for a retry. }); const activatePlanM = trpc.executionPlan.activate.useMutation({ @@ -193,6 +196,26 @@ export default function GoalDetailPage() { ); } + if (isError) { + return ( + <> + +
+
+ + +
+
+ + ); + } + if (!goal) { return ( <> @@ -467,12 +490,12 @@ export default function GoalDetailPage() { crews={crewList?.items ?? []} busy={Boolean(updateM?.isPending)} onClose={() => setEditingGoal(false)} - onSave={(payload) => - updateM?.mutate({ + onSave={async (payload) => { + await updateM?.mutateAsync({ id: goal.id, ...payload, - }) - } + }); + }} /> ) : null} @@ -518,7 +541,7 @@ function GoalEditModal({ crewId: string | null; maxTotalCostUsd: number | null; maxWallTimeMinutes: number | null; - }) => void; + }) => void | Promise; }) { const [title, setTitle] = useState(goal.title); const [description, setDescription] = useState(goal.description ?? ""); @@ -535,111 +558,92 @@ function GoalEditModal({ const invalidCost = parsedCost != null && (!Number.isFinite(parsedCost) || parsedCost < 0); const invalidTime = parsedTime != null && (!Number.isInteger(parsedTime) || parsedTime <= 0); - const canSave = title.trim().length > 0 && !invalidCost && !invalidTime; return ( -
{ + if (!o) onClose(); + }} + title="Edit goal" + description="Budgets apply to the active plan" + primaryLabel={busy ? "Saving…" : "Save"} + loading={busy} + className="max-w-lg" + onSubmit={async () => { + if (!title.trim()) return { error: "Title is required." }; + if (invalidCost || invalidTime) + return { + error: + "Cost must be zero or higher. Wall time must be a whole positive minute.", + }; + await onSave({ + title: title.trim(), + description: description.trim() || null, + crewId: crewId || null, + maxTotalCostUsd: parsedCost, + maxWallTimeMinutes: parsedTime, + }); + }} > -
e.stopPropagation()} - > -
-

Edit goal

- Budgets apply to the active plan -
- - + setTitle(e.target.value)} - className="mt-1 w-full rounded-md border border-border bg-card/40 px-3 py-2 text-sm" + className="w-full rounded-md border border-border bg-card/40 px-3 py-2 text-sm" /> - - + +