From 2a0487aa6e4fa1bedf0cafbf1f6eece0518e477a Mon Sep 17 00:00:00 2001 From: sai karthik Date: Sun, 26 Jul 2026 00:50:11 +0530 Subject: [PATCH] feat: integrate mobile work chat and Gitea delivery --- apps/daemon/package.json | 2 +- apps/daemon/src/runtime.ts | 67 +-- .../mobile-workspace/mobile-flow-page.tsx | 71 ++- .../src/hooks/use-mobile-project-workspace.ts | 66 ++- apps/web/src/hooks/use-mobile-work-stack.ts | 66 +++ .../mobile-status-message.test.ts | 23 + .../mobile-workspace/mobile-status-message.ts | 25 + .../src/lib/users/get-user-initials.test.ts | 13 + apps/web/src/lib/users/get-user-initials.ts | 10 + apps/web/src/routes.ts | 1 + apps/web/src/routes/app/mobile/chat/page.tsx | 2 +- .../src/routes/app/mobile/chat/work/page.tsx | 15 + bun.lock | 159 +----- .../src/actions/finalize-gitea-lifecycle.ts | 113 +++- packages/agents/src/agents/project-manager.ts | 2 +- packages/agents/src/git/gitea.test.ts | 2 +- packages/agents/src/git/gitea.ts | 4 +- packages/agents/src/sandboxes/agent-os.ts | 33 +- .../sandboxes/host-repository-bridge.test.ts | 297 ++++++++++ .../src/sandboxes/host-repository-bridge.ts | 506 ++++++++++++++++++ .../src/agent-os-git-software.test.ts | 85 +++ .../primitives/src/agent-os-git-software.ts | 194 +++++++ packages/primitives/src/agent-os.ts | 7 +- .../primitives/src/project-workspace.test.ts | 10 + packages/primitives/src/project-workspace.ts | 11 +- packages/ui/src/components/mobile-product.tsx | 4 + .../src/components/mobile-workspace/index.ts | 6 + .../mobile-settings-panel.tsx | 83 +++ .../mobile-work-chat-screen.tsx | 206 +++++++ .../mobile-work-stack-screen.tsx | 458 ++++++++++++++++ packages/ui/src/hooks/use-swipe-card.ts | 156 ++++++ 31 files changed, 2477 insertions(+), 220 deletions(-) create mode 100644 apps/web/src/hooks/use-mobile-work-stack.ts create mode 100644 apps/web/src/lib/mobile-workspace/mobile-status-message.test.ts create mode 100644 apps/web/src/lib/mobile-workspace/mobile-status-message.ts create mode 100644 apps/web/src/lib/users/get-user-initials.test.ts create mode 100644 apps/web/src/lib/users/get-user-initials.ts create mode 100644 apps/web/src/routes/app/mobile/chat/work/page.tsx create mode 100644 packages/agents/src/sandboxes/host-repository-bridge.test.ts create mode 100644 packages/agents/src/sandboxes/host-repository-bridge.ts create mode 100644 packages/primitives/src/agent-os-git-software.test.ts create mode 100644 packages/primitives/src/agent-os-git-software.ts create mode 100644 packages/ui/src/components/mobile-workspace/mobile-settings-panel.tsx create mode 100644 packages/ui/src/components/mobile-workspace/mobile-work-chat-screen.tsx create mode 100644 packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx create mode 100644 packages/ui/src/hooks/use-swipe-card.ts diff --git a/apps/daemon/package.json b/apps/daemon/package.json index 8c2852d..5470e7a 100644 --- a/apps/daemon/package.json +++ b/apps/daemon/package.json @@ -18,7 +18,7 @@ "@rivet-dev/agentos": "catalog:", "convex": "catalog:", "effect": "catalog:", - "rivetkit": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0" + "rivetkit": "2.3.7" }, "devDependencies": { "@code/config": "workspace:*", diff --git a/apps/daemon/src/runtime.ts b/apps/daemon/src/runtime.ts index 8128e64..acf8e44 100644 --- a/apps/daemon/src/runtime.ts +++ b/apps/daemon/src/runtime.ts @@ -2,26 +2,27 @@ import { env } from "@code/env/server"; import { Console, Effect, FiberSet, Result, Stream } from "effect"; import { makeAgentOsRuntime } from "./agent-os"; -import { ConvexControlPlane, type DaemonCommand } from "./convex"; +import { ConvexControlPlane } from "./convex"; +import type { DaemonCommand } from "./convex"; const errorMessage = (cause: unknown) => cause instanceof Error ? cause.message : String(cause); export const runDaemon = Effect.scoped( - Effect.gen(function* () { + Effect.gen(function* runDaemon() { const controlPlane = yield* ConvexControlPlane; const agentOs = yield* makeAgentOsRuntime; const sessionId = crypto.randomUUID(); const hostname = yield* Effect.promise(() => import("node:os")).pipe( Effect.map((os) => os.hostname()) ); - const fibers = yield* FiberSet.make(); + const fibers = yield* FiberSet.make(); yield* controlPlane.connect({ - sessionId, + architecture: process.arch, hostname, platform: process.platform, - architecture: process.arch, + sessionId, }); yield* Console.log(`daemon ${env.DAEMON_ID} connected as ${sessionId}`); @@ -40,33 +41,37 @@ export const runDaemon = Effect.scoped( Effect.forkScoped ); - const processCommand = Effect.fn("Daemon.processCommand")(function* ( - candidate: DaemonCommand - ) { - const command = yield* controlPlane.claim(candidate._id, sessionId); - if (!command) { - return; + const processCommand = Effect.fn("Daemon.processCommand")( + function* processCommand(candidate: DaemonCommand) { + const command = yield* controlPlane.claim(candidate._id, sessionId); + if (!command) { + return; + } + const started = yield* controlPlane.start(command._id, sessionId); + if (!started) { + return; + } + yield* controlPlane.recordEvent({ + commandId: command._id, + data: { actorKey: command.actorKey, method: command.method }, + kind: "command.started", + }); + const result = yield* agentOs.execute(command).pipe(Effect.result); + if (Result.isSuccess(result)) { + yield* controlPlane.succeed( + command._id, + sessionId, + result.success ?? null + ); + return; + } + yield* controlPlane.fail( + command._id, + sessionId, + errorMessage(result.failure) + ); } - const started = yield* controlPlane.start(command._id, sessionId); - if (!started) { - return; - } - yield* controlPlane.recordEvent({ - commandId: command._id, - kind: "command.started", - data: { method: command.method, actorKey: command.actorKey }, - }); - const result = yield* agentOs.execute(command).pipe(Effect.result); - if (Result.isSuccess(result)) { - yield* controlPlane.succeed(command._id, sessionId, result.success); - return; - } - yield* controlPlane.fail( - command._id, - sessionId, - errorMessage(result.failure) - ); - }); + ); yield* controlPlane.commands.pipe( Stream.runForEach((commands) => diff --git a/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx b/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx index a654ab4..59c3bed 100644 --- a/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx +++ b/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx @@ -1,21 +1,29 @@ +import { signOutWeb, useWebAuth } from "@code/auth/web"; import type { Id } from "@code/backend/convex/_generated/dataModel"; import { MobileAssistantChatScreen, MobileExpandedWorkScreen, MobileHomeScreen, + MobileWorkChatScreen, MobileWorkListScreen, + MobileWorkStackScreen, MobileWorkUnitDetailScreen, } from "@code/ui/components/mobile-product"; import type { FormEvent } from "react"; import { useNavigate, useParams } from "react-router"; import { useMobileProjectWorkspace } from "@/hooks/use-mobile-project-workspace"; +import { useMobileWorkStack } from "@/hooks/use-mobile-work-stack"; import { useMobileWorkspace } from "@/hooks/use-mobile-workspace"; import { MODEL_ID, MODEL_LABEL } from "@/lib/chat/constants"; +import { getMobileStatusMessage } from "@/lib/mobile-workspace/mobile-status-message"; +import { getUserInitials } from "@/lib/users/get-user-initials"; export type MobileFlowScreen = | "assistant-chat" | "home" + | "stack-home" + | "work-chat" | "work-list" | "work-unit-detail"; @@ -26,6 +34,7 @@ interface MobileFlowPageProps { export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => { const navigate = useNavigate(); const { workUnitId } = useParams(); + const auth = useWebAuth(); const projectWorkspace = useMobileProjectWorkspace({ selectedWorkUnitId: workUnitId, }); @@ -34,8 +43,21 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => { projectWorkspace.raiseIssue({ body, title }), onSend: projectWorkspace.sendMessage, }); + const workStack = useMobileWorkStack(projectWorkspace.data.workUnits); const workPath = "/work"; const chatPath = "/chat"; + const handleRestoreChecked = workStack.restoreChecked; + const handleWorkUnitChecked = workStack.markChecked; + const handleWorkUnitSentBack = workStack.sendToBack; + const user = auth.status === "authenticated" ? auth.user : undefined; + const handleSignOut = async () => { + await signOutWeb(); + navigate("/login"); + }; + const statusMessage = getMobileStatusMessage({ + assistantError: projectWorkspace.error?.message, + localStatus: workspace.statusMessage, + }); const handleOpenUnit = (selectedWorkUnitId?: string) => { const targetId = @@ -48,7 +70,7 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => { workspace.setExpanded(true); return; } - navigate(`/work/${targetId}`); + navigate(`/chat/${targetId}`); }; const screenProps = { @@ -58,7 +80,7 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => { data: projectWorkspace.data, modelId: MODEL_ID, modelLabel: MODEL_LABEL, - onBack: () => navigate(workPath), + onBack: () => navigate(chatPath), onComposerChange: workspace.handleComposerChange, onComposerMessageSubmit: workspace.handleComposerMessageSubmit, onComposerSubmit: workspace.handleComposerSubmit, @@ -99,12 +121,55 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => { projectManagerOpen: workspace.projectManagerOpen, repositoryValue: projectWorkspace.projectWorkspace.repository, settingsOpen: workspace.settingsOpen, - statusMessage: workspace.statusMessage ?? projectWorkspace.error?.message, + statusMessage, }; if (screen === "assistant-chat") { return ; } + if (screen === "stack-home") { + return ( + + navigate(`/chat/${selectedWorkUnitId}`) + } + onRestoreChecked={handleRestoreChecked} + onSettingsClose={() => workspace.setSettingsOpen(false)} + onSettingsOpen={() => workspace.setSettingsOpen(true)} + onSignOut={() => { + void handleSignOut(); + }} + onWorkUnitChecked={handleWorkUnitChecked} + onWorkUnitSentBack={handleWorkUnitSentBack} + settingsOpen={workspace.settingsOpen} + statusMessage={statusMessage} + userEmail={user?.email} + userInitials={getUserInitials(user?.name)} + userName={user?.name} + workUnits={workStack.visibleWorkUnits} + /> + ); + } + if (screen === "work-chat") { + return ( + navigate(chatPath)} + onComposerChange={workspace.handleComposerChange} + onComposerMessageSubmit={workspace.handleComposerMessageSubmit} + onComposerSubmit={workspace.handleComposerSubmit} + statusMessage={statusMessage} + /> + ); + } if (screen === "home") { return ; } diff --git a/apps/web/src/hooks/use-mobile-project-workspace.ts b/apps/web/src/hooks/use-mobile-project-workspace.ts index 81114b9..4f50d8b 100644 --- a/apps/web/src/hooks/use-mobile-project-workspace.ts +++ b/apps/web/src/hooks/use-mobile-project-workspace.ts @@ -13,24 +13,45 @@ import { STATUS_COPY } from "@/lib/chat/constants"; import { getMessageText } from "@/lib/chat/transforms"; import { buildMobileWorkspaceView } from "@/lib/mobile-workspace/build-mobile-workspace-view"; +const WORK_UNIT_SCOPE_PATTERN = + /^[\s\S]*?(?[\s\S]*?)<\/message>\s*<\/zopu-work-unit>$/u; + const toAssistantMessages = ( - messages: ReturnType["messages"] -): MobileAssistantMessageView[] => - messages.flatMap((message) => { + messages: ReturnType["messages"], + selectedWorkUnitId?: string +): MobileAssistantMessageView[] => { + const result: MobileAssistantMessageView[] = []; + for (const message of messages) { if (message.role !== "assistant" && message.role !== "user") { - return []; + continue; } - const text = getMessageText(message); - return text - ? [ - { - id: message.id, - role: message.role, - text, - }, - ] - : []; - }); + const rawText = getMessageText(message); + if (!rawText) { + continue; + } + if (!selectedWorkUnitId) { + result.push({ id: message.id, role: message.role, text: rawText }); + continue; + } + if (message.role === "user") { + const match = WORK_UNIT_SCOPE_PATTERN.exec(rawText); + if (match?.groups?.workUnitId !== selectedWorkUnitId) { + result.length = 0; + continue; + } + result.push({ + id: message.id, + role: message.role, + text: match.groups.message?.trim() ?? rawText, + }); + continue; + } + if (result.length > 0) { + result.push({ id: message.id, role: message.role, text: rawText }); + } + } + return result; +}; const getStatusTone = ( status: ReturnType["status"] @@ -57,12 +78,23 @@ export const useMobileProjectWorkspace = ({ ? { organizationId: organization.organizationId } : "skip" ); + const selectedIssue = projectWorkspace.issues?.find( + (issue) => String(issue._id) === selectedWorkUnitId + ); + const sendMessage = (message: string) => { + if (!selectedIssue) { + return chatAgent.sendMessage(message); + } + return chatAgent.sendMessage( + `\nIssue #${selectedIssue.number}: ${selectedIssue.title}\n${message}\n` + ); + }; const assistant: MobileAssistantView = { isBusy: chatAgent.status === "connecting" || chatAgent.status === "streaming" || chatAgent.status === "submitted", - messages: toAssistantMessages(chatAgent.messages), + messages: toAssistantMessages(chatAgent.messages, selectedWorkUnitId), statusLabel: STATUS_COPY[chatAgent.status], statusTone: getStatusTone(chatAgent.status), }; @@ -88,7 +120,7 @@ export const useMobileProjectWorkspace = ({ raiseIssue: projectWorkspace.raiseIssue, raiseIssueFromSignal: (signalId: string) => projectWorkspace.raiseIssueFromSignal(signalId as Id<"signals">), - sendMessage: chatAgent.sendMessage, + sendMessage, startWorkUnit: projectWorkspace.startWorkUnit, } as const; }; diff --git a/apps/web/src/hooks/use-mobile-work-stack.ts b/apps/web/src/hooks/use-mobile-work-stack.ts new file mode 100644 index 0000000..49dbc17 --- /dev/null +++ b/apps/web/src/hooks/use-mobile-work-stack.ts @@ -0,0 +1,66 @@ +import type { MobileWorkUnitView } from "@code/ui/components/mobile-product"; +import { useCallback, useMemo, useState } from "react"; + +export const useMobileWorkStack = ( + workUnits: readonly MobileWorkUnitView[] +) => { + const [order, setOrder] = useState([]); + const [checkedIds, setCheckedIds] = useState>( + () => new Set() + ); + + const workUnitsById = useMemo( + () => new Map(workUnits.map((workUnit) => [workUnit.id, workUnit])), + [workUnits] + ); + const availableIds = useMemo( + () => new Set(workUnitsById.keys()), + [workUnitsById] + ); + const visibleWorkUnits = useMemo(() => { + const retainedOrder = order.filter( + (workUnitId) => + availableIds.has(workUnitId) && !checkedIds.has(workUnitId) + ); + const retainedIds = new Set(retainedOrder); + const addedIds = workUnits + .map((workUnit) => workUnit.id) + .filter( + (workUnitId) => + !retainedIds.has(workUnitId) && !checkedIds.has(workUnitId) + ); + return [...retainedOrder, ...addedIds].flatMap((workUnitId) => { + const workUnit = workUnitsById.get(workUnitId); + return workUnit ? [workUnit] : []; + }); + }, [availableIds, checkedIds, order, workUnits, workUnitsById]); + + const sendToBack = useCallback( + (workUnitId: string) => { + if (visibleWorkUnits[0]?.id !== workUnitId) { + return; + } + setOrder([...visibleWorkUnits.slice(1).map(({ id }) => id), workUnitId]); + }, + [visibleWorkUnits] + ); + + const markChecked = useCallback((workUnitId: string) => { + setCheckedIds((current) => new Set(current).add(workUnitId)); + }, []); + + const restoreChecked = useCallback(() => { + setCheckedIds(new Set()); + setOrder([]); + }, []); + + return { + checkedCount: [...checkedIds].filter((workUnitId) => + availableIds.has(workUnitId) + ).length, + markChecked, + restoreChecked, + sendToBack, + visibleWorkUnits, + } as const; +}; diff --git a/apps/web/src/lib/mobile-workspace/mobile-status-message.test.ts b/apps/web/src/lib/mobile-workspace/mobile-status-message.test.ts new file mode 100644 index 0000000..7d2678f --- /dev/null +++ b/apps/web/src/lib/mobile-workspace/mobile-status-message.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { getMobileStatusMessage } from "./mobile-status-message"; + +describe("getMobileStatusMessage", () => { + it("keeps local composer feedback", () => { + expect( + getMobileStatusMessage({ + assistantError: "Flue API error 401", + localStatus: "Sent to Zopu", + }) + ).toBe("Sent to Zopu"); + }); + + it("does not expose Flue authorization internals", () => { + expect( + getMobileStatusMessage({ + assistantError: + 'Flue API error 401: GET /agents/zopu/example: {"error":"Unauthorized"}', + }) + ).toBe("Zopu is reconnecting…"); + }); +}); diff --git a/apps/web/src/lib/mobile-workspace/mobile-status-message.ts b/apps/web/src/lib/mobile-workspace/mobile-status-message.ts new file mode 100644 index 0000000..e274b7f --- /dev/null +++ b/apps/web/src/lib/mobile-workspace/mobile-status-message.ts @@ -0,0 +1,25 @@ +interface MobileStatusMessageInput { + readonly assistantError?: string; + readonly localStatus?: string; +} + +const isAgentAuthorizationError = (message: string) => + message.includes("Flue API error 401") || + message.includes('"Unauthorized"') || + message.includes("GET /agents/"); + +export const getMobileStatusMessage = ({ + assistantError, + localStatus, +}: MobileStatusMessageInput): string | undefined => { + if (localStatus) { + return localStatus; + } + if (!assistantError) { + return undefined; + } + if (isAgentAuthorizationError(assistantError)) { + return "Zopu is reconnecting…"; + } + return "Zopu needs attention"; +}; diff --git a/apps/web/src/lib/users/get-user-initials.test.ts b/apps/web/src/lib/users/get-user-initials.test.ts new file mode 100644 index 0000000..e34618b --- /dev/null +++ b/apps/web/src/lib/users/get-user-initials.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { getUserInitials } from "./get-user-initials"; + +describe("getUserInitials", () => { + it("uses the first two name parts", () => { + expect(getUserInitials("Sai Karthik")).toBe("SK"); + }); + + it("uses a neutral fallback while auth loads", () => { + expect(getUserInitials()).toBe("U"); + }); +}); diff --git a/apps/web/src/lib/users/get-user-initials.ts b/apps/web/src/lib/users/get-user-initials.ts new file mode 100644 index 0000000..9eba292 --- /dev/null +++ b/apps/web/src/lib/users/get-user-initials.ts @@ -0,0 +1,10 @@ +export const getUserInitials = (name?: string): string => { + const parts = name?.trim().split(/\s+/u).filter(Boolean) ?? []; + if (parts.length === 0) { + return "U"; + } + return parts + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() ?? "") + .join(""); +}; diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts index 0898540..d4e3976 100644 --- a/apps/web/src/routes.ts +++ b/apps/web/src/routes.ts @@ -10,6 +10,7 @@ export default [ layout("./routes/app/mobile/layout.tsx", [ index("./routes/app/mobile/page.tsx"), route("chat", "./routes/app/mobile/chat/page.tsx"), + route("chat/:workUnitId", "./routes/app/mobile/chat/work/page.tsx"), route("work", "./routes/app/mobile/work-list/page.tsx"), route("work/:workUnitId", "./routes/app/mobile/work/page.tsx"), ]), diff --git a/apps/web/src/routes/app/mobile/chat/page.tsx b/apps/web/src/routes/app/mobile/chat/page.tsx index 1326aab..d9a348d 100644 --- a/apps/web/src/routes/app/mobile/chat/page.tsx +++ b/apps/web/src/routes/app/mobile/chat/page.tsx @@ -8,5 +8,5 @@ export const meta = (_args: Route.MetaArgs) => [ ]; export default function MobileChatPage() { - return ; + return ; } diff --git a/apps/web/src/routes/app/mobile/chat/work/page.tsx b/apps/web/src/routes/app/mobile/chat/work/page.tsx new file mode 100644 index 0000000..03a411b --- /dev/null +++ b/apps/web/src/routes/app/mobile/chat/work/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Work chat | Zopu" }, + { + content: "Chat with Zopu in the context of a selected work unit", + name: "description", + }, +]; + +export default function MobileWorkChatPage() { + return ; +} diff --git a/bun.lock b/bun.lock index f8063d6..ae23790 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "code", @@ -34,7 +35,7 @@ "@rivet-dev/agentos": "catalog:", "convex": "catalog:", "effect": "catalog:", - "rivetkit": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", + "rivetkit": "2.3.7", }, "devDependencies": { "@code/config": "workspace:*", @@ -1287,19 +1288,19 @@ "@rivetkit/bare-ts": ["@rivetkit/bare-ts@0.6.2", "", {}, "sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg=="], - "@rivetkit/engine-cli": ["@rivetkit/engine-cli@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "optionalDependencies": { "@rivetkit/engine-cli-darwin-arm64": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/engine-cli-darwin-x64": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/engine-cli-linux-arm64-musl": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/engine-cli-linux-x64-musl": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0" } }, "sha512-7fq+rJt5+bujGV+Q0esDY3OJrzRUhEpYd2EV0oVUI+pcIRyYJMx2y7KrMUucqTFOr77O/NDlmSGTfD4cStRfrg=="], + "@rivetkit/engine-cli": ["@rivetkit/engine-cli@2.3.7", "", { "optionalDependencies": { "@rivetkit/engine-cli-darwin-arm64": "2.3.7", "@rivetkit/engine-cli-darwin-x64": "2.3.7", "@rivetkit/engine-cli-linux-arm64-musl": "2.3.7", "@rivetkit/engine-cli-linux-x64-musl": "2.3.7", "@rivetkit/engine-cli-win32-x64": "2.3.7" } }, "sha512-CezLwJ0B7dWDbA7qM6Aq04mwnrJAdrDActRrrcb4NBa20h7wO9KPAYBwyJz/dRnKm9EUUNcFZ6hrSDpp6T3+Rg=="], - "@rivetkit/engine-cli-darwin-arm64": ["@rivetkit/engine-cli-darwin-arm64@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kPzGTfdIIR5qAZpZDDRXC3OtGd8Jh/dZb/Pbmd03k/2DT12ILUHmI2DZg829icylExSSmhfRVjUSYsjRw8rmug=="], + "@rivetkit/engine-cli-darwin-arm64": ["@rivetkit/engine-cli-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-F3yGSDIaW94BZzuAm3erQRT/Ki55wHJ/b6tOaE7d9tRmTAZw/XqHmjNdUYjOvFUQKLb+SSsOittM4dINrAANQQ=="], - "@rivetkit/engine-cli-darwin-x64": ["@rivetkit/engine-cli-darwin-x64@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0rJcsIttFzycpR4J/ciECjPk+KzlVFeJwTPobhG9anWHycq8GpCuQx9ipgBA++01tZYo/x7kEfSU+dNdrxqsEA=="], + "@rivetkit/engine-cli-darwin-x64": ["@rivetkit/engine-cli-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8BbQsamIvv8U/cEoJUFr+uAzwjjl9cG+ZZxjDX9OJxbuD11ESpHvDLgge54ijynO3IjoP8cIzjIXat6mr2Gkw=="], - "@rivetkit/engine-cli-linux-arm64-musl": ["@rivetkit/engine-cli-linux-arm64-musl@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jXh2SgMHm/JPwVGOlFSO1NGMy7/2t4lNwU/iSwX4CN1px9Nrr4Ud8n0FY58VHcU4Ppv1tWAdWEWsOlF5EZlr8Q=="], + "@rivetkit/engine-cli-linux-arm64-musl": ["@rivetkit/engine-cli-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-G3s76BHfIs6R9rm6JYI13sOjPLy08+C8WvVoFwSdMqsibEaqlfo36wVkp/x1g0sEZUek/dBxVrKqMMR7c4/gug=="], - "@rivetkit/engine-cli-linux-x64-musl": ["@rivetkit/engine-cli-linux-x64-musl@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "linux", "cpu": "x64" }, "sha512-pDBkdi5la//sp7Uq1KXDR4eXbwO0l/HmQCnosbgWHWz8OBL62RcueeYWQt181Gl7VIsINm1QB7mihUcbcECVxw=="], + "@rivetkit/engine-cli-linux-x64-musl": ["@rivetkit/engine-cli-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-LWhrUVzULcsZ4ufCLsx23YfrdMW2INd9g0ANBac9+PzGwL8qYfVIXnDpR3587lapu68EbSEPZcconf1tBlvJxQ=="], "@rivetkit/engine-cli-win32-x64": ["@rivetkit/engine-cli-win32-x64@2.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-jyA5PlHivHXyyq2ehnuXwxd5TUSbB4emomnph9EovsJo7qIdmSNaaCO13NYBT483fd0uXiNI0MK/lkJa9ePY5A=="], - "@rivetkit/engine-envoy-protocol": ["@rivetkit/engine-envoy-protocol@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2" } }, "sha512-2u0MPsYLMSuwaX0amz52kZX4wsh3PmbczjPb4AoSk1hCbycf1IN3Vkj3RR87ZwMTlLYKkcpgq8fHPlxoPSkftQ=="], + "@rivetkit/engine-envoy-protocol": ["@rivetkit/engine-envoy-protocol@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2" } }, "sha512-lsJM3ERwozQCebaOMKJzqIQlcLbbN3LXurRB0+7LsM6FUUL2l6hMoq5I2De/G35DJ6OWDva9AmzfvzI+5f1GPg=="], "@rivetkit/framework-base": ["@rivetkit/framework-base@2.3.7", "", { "dependencies": { "@tanstack/store": "^0.7.1", "fast-deep-equal": "^3.1.3", "rivetkit": "2.3.7" } }, "sha512-zxmdAzFogA3PeT3ZgFIoYlEAJVi3iCwbPBXczZKf2x2QMCl2cTZ2jswFCAgHUUMQWj+AWiX65usxq73a9Ls/qQ=="], @@ -1307,29 +1308,29 @@ "@rivetkit/react": ["@rivetkit/react@2.3.7", "", { "dependencies": { "@rivetkit/framework-base": "2.3.7", "@tanstack/react-store": "^0.7.1", "rivetkit": "2.3.7" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-Y3UoUYhkLKDQrJdloCmYK+fhJMlPw5BPHbRb5VutFh5L8csIRZLoCoMitzV7mjNUxuDE1+k10iGuUHYPZUP4aA=="], - "@rivetkit/rivetkit-napi": ["@rivetkit/rivetkit-napi@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "dependencies": { "@napi-rs/cli": "^2.18.4", "@rivetkit/engine-envoy-protocol": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0" }, "optionalDependencies": { "@rivetkit/rivetkit-napi-darwin-arm64": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/rivetkit-napi-darwin-x64": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/rivetkit-napi-linux-arm64-gnu": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/rivetkit-napi-linux-arm64-musl": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/rivetkit-napi-linux-x64-gnu": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/rivetkit-napi-linux-x64-musl": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0" } }, "sha512-uSBQFnTSuh+YMjdRR34KAS7QJQpkVXNleNdzLoCic1XdRyIh13KvuX3+JR3bbk3S32euM7ZeScy3kve8c3nuyA=="], + "@rivetkit/rivetkit-napi": ["@rivetkit/rivetkit-napi@2.3.7", "", { "dependencies": { "@napi-rs/cli": "^2.18.4", "@rivetkit/engine-envoy-protocol": "2.3.7" }, "optionalDependencies": { "@rivetkit/rivetkit-napi-darwin-arm64": "2.3.7", "@rivetkit/rivetkit-napi-darwin-x64": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-musl": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-musl": "2.3.7", "@rivetkit/rivetkit-napi-win32-x64-msvc": "2.3.7" } }, "sha512-1d3HtNKzJwdkznWDWSpCwg1rBn+nVwUfcKrApINTfiEhfDyVeznwbVFkfHWqOBHsjU17vvWFfjjTtf8QsjLwUQ=="], - "@rivetkit/rivetkit-napi-darwin-arm64": ["@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TQPCk1U2Z7otH9ehV7NEEksRT/hzEsLyMaA7ZpkMKxOr8bBNWkES7U+YY5fc7mLFdWR/N15iN7SU0koxJE7PjA=="], + "@rivetkit/rivetkit-napi-darwin-arm64": ["@rivetkit/rivetkit-napi-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gI1wMe6z85VDeR4K/wnzevVakXTg5UCfvjlDOp1+23nyU3NlInhN156dEWO8EsfRMrs+1bYoGdc4rcoN+4PUrw=="], - "@rivetkit/rivetkit-napi-darwin-x64": ["@rivetkit/rivetkit-napi-darwin-x64@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "darwin", "cpu": "x64" }, "sha512-wAuJIR0efIcpaQQvWgniZ0qOy6x5+/V+/agACJhQVSjj7P29kY0piEBKLKDtP0M4gTd6GGzBAe5oNY0t785/0w=="], + "@rivetkit/rivetkit-napi-darwin-x64": ["@rivetkit/rivetkit-napi-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-H5tUwvCbKwTIBiyQG01ikK5mdc9ISg+bv8gPUw41mYOKq8rb9nBLkcl2TKcreFHac8KaGxtnBZzYcjbUV3NcwQ=="], - "@rivetkit/rivetkit-napi-linux-arm64-gnu": ["@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "linux", "cpu": "arm64" }, "sha512-A+kuGvc9RJjUflPVHbHPkQSH4xyjasbVMNRfq2ISdaJ/CnNcmrexJ4F+5Mm2VPz61gxNjkSoyk8tPupz07CZnw=="], + "@rivetkit/rivetkit-napi-linux-arm64-gnu": ["@rivetkit/rivetkit-napi-linux-arm64-gnu@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-MpOeiO5M737e93hM/FjcH+zv8xBEX1tgBd/59K/Uw7qlRbWW7JGz8WGjavdxv211LRqxsn8EUlGRxcADDsXnBA=="], - "@rivetkit/rivetkit-napi-linux-arm64-musl": ["@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jo7WcmJ7hjWpi/8DUlWu1x2JtHfEAiY21QdNy5VZ23Dtg/pzKzDAn4m6myPB/s8MS+nAOiJcruWTH16qMHAeWw=="], + "@rivetkit/rivetkit-napi-linux-arm64-musl": ["@rivetkit/rivetkit-napi-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-0GsUgd/HHvIq6v92kp0H175mGm2Exvs4krBLih0MIpVAkqJk8EIcKI6wJhCwY04Fr0MS8sHkUhjuHXa7QcmlYA=="], - "@rivetkit/rivetkit-napi-linux-x64-gnu": ["@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "linux", "cpu": "x64" }, "sha512-hcHO0FkMQITQ3taSJ0084DVDRysOep3eBzv8cwgA0qJhIzzd5EF+4qo4QO4woEdnDYCGMBJZLByYyEPFY3WSOQ=="], + "@rivetkit/rivetkit-napi-linux-x64-gnu": ["@rivetkit/rivetkit-napi-linux-x64-gnu@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8wCD1FuDpM+yiKRG8ro1K8ILVpJXzCNKViJ1axq1b4Nyqgod/nyEvvKDaT4qhA5+Om+W5i6YRO1uddjC6QjJSQ=="], - "@rivetkit/rivetkit-napi-linux-x64-musl": ["@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "os": "linux", "cpu": "x64" }, "sha512-Xx8DWZwdWBZdkqSbR7UdPR1nrZH53tK4r0kSM9Y670GGwgBHg3mesaYshPs/B0XzEFVybF5vS/0ToIi97T/LIw=="], + "@rivetkit/rivetkit-napi-linux-x64-musl": ["@rivetkit/rivetkit-napi-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8RGzXD33GH1/DWufFzCZRI+npF5IL/9z7Ja3pthsuYgrL+axGG0iqfAjbwj5HHnB/ceUGQVT+uCKzoLnG+odtw=="], "@rivetkit/rivetkit-napi-win32-x64-msvc": ["@rivetkit/rivetkit-napi-win32-x64-msvc@2.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-n1by/coKxWOb2FZDGLM2qmULxcLSqx8hxJuk7Hsb1gl4uNHGviUzYr+r7WAiOzonK/Dgy477u3OKf5yBie743A=="], - "@rivetkit/rivetkit-wasm": ["@rivetkit/rivetkit-wasm@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", {}, "sha512-8bTqxP4WihKbKifZHZT1umnINvlv1NU+GfciQ+pciiIxnaPVT7dX++IczTB2g3njEibYL44wrEzicNNXNsf6lg=="], + "@rivetkit/rivetkit-wasm": ["@rivetkit/rivetkit-wasm@2.3.7", "", {}, "sha512-o7QBtFOJrajyPVBIDQ32k8sDNFsUc8v2ETfNpr+Ch374ydmZMzCv3XKJU9vubc9bZa7WpCHSzf5ztc8b8i/Xpg=="], - "@rivetkit/traces": ["@rivetkit/traces@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "vbare": "^0.0.4" } }, "sha512-4yXOCC2rPncF/aNBhW4KU6CF56Bo9Sslidj311eY0iGqCxRBpQhI57Y5eX8Ho5DNXmVTobjP6USxeWp6Oymmvg=="], + "@rivetkit/traces": ["@rivetkit/traces@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "vbare": "^0.0.4" } }, "sha512-B7QYjFP2HPxyfPsVjfcNIV7FPLWgGWGypmlT/dZM1i1Cf1WRdeuBumPLffgs3YceiY7Pm4D/gZZVt5p0m4Cx+A=="], - "@rivetkit/virtual-websocket": ["@rivetkit/virtual-websocket@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", {}, "sha512-Rd3MdziVAS4Ut/dqcLfN9pMCRLOSqK7n/AunWtI6Xr72y+0jh7gwFVYmZRJGyGamnfJFTKTQnbnhg+KGkyu+wg=="], + "@rivetkit/virtual-websocket": ["@rivetkit/virtual-websocket@2.3.7", "", {}, "sha512-jwxDvYbr3YB6vFuxwRdB/AqUUMwdtC1rIAc4tNN6VQR3weHbE+PrDoHKbe85zrtlBtcnG89/p8tlovrj8qyQWA=="], - "@rivetkit/workflow-engine": ["@rivetkit/workflow-engine@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "pino": "^9.6.0", "vbare": "^0.0.4" } }, "sha512-Nh+tlQT9FoBsznHfRujeT/hlgj/4k4d3M7+0ogZpCmPhuwim0cjiog6fL18bfehr7BPobvSH0SInqLvF3kPPWA=="], + "@rivetkit/workflow-engine": ["@rivetkit/workflow-engine@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "pino": "^9.6.0", "vbare": "^0.0.4" } }, "sha512-+C4kGuSNysrw2zs/LQoYxouOxufKHHMxiIXKF8Ab9GU2BCJ32RUST9dYU/gaQE/w9uM+hqAHUz0DTlOBYU6p6g=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="], @@ -3299,7 +3300,7 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], @@ -3467,7 +3468,7 @@ "ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="], - "rivetkit": ["rivetkit@0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "", { "dependencies": { "@hono/zod-openapi": "^1.1.5", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-cli": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/engine-envoy-protocol": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/on-change": "6.0.1", "@rivetkit/rivetkit-napi": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/rivetkit-wasm": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/traces": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/virtual-websocket": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "@rivetkit/workflow-engine": "0.0.0-stack-feat-rivetkit-forward-inspector-tabs-to-native-plugin-actors-qkwmksyy.d2859b0", "cbor-x": "^1.6.0", "drizzle-orm": "^0.44.2", "hono": "^4.7.0", "invariant": "^2.2.4", "p-retry": "^6.2.1", "pino": "^9.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", "zod": "^4.1.0" }, "peerDependencies": { "drizzle-kit": "^0.31.2", "eventsource": "^4.0.0", "ws": "^8.0.0" }, "optionalPeers": ["drizzle-kit", "eventsource", "ws"] }, "sha512-3kXHTXMv6u/P29xul+8YnqCmkhyj3TYJxl1f40yyjWqbrxhAPR7qoW+mDY3NTIMX6xpXuiqnR9gp9pSPiM4s0A=="], + "rivetkit": ["rivetkit@2.3.7", "", { "dependencies": { "@hono/zod-openapi": "^1.1.5", "@rivet-dev/agent-os-core": "^0.1.1", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-cli": "2.3.7", "@rivetkit/engine-envoy-protocol": "2.3.7", "@rivetkit/on-change": "6.0.1", "@rivetkit/rivetkit-napi": "2.3.7", "@rivetkit/rivetkit-wasm": "2.3.7", "@rivetkit/traces": "2.3.7", "@rivetkit/virtual-websocket": "2.3.7", "@rivetkit/workflow-engine": "2.3.7", "cbor-x": "^1.6.0", "drizzle-orm": "^0.44.2", "hono": "^4.7.0", "invariant": "^2.2.4", "p-retry": "^6.2.1", "pino": "^9.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", "zod": "^4.1.0" }, "peerDependencies": { "drizzle-kit": "^0.31.2", "eventsource": "^4.0.0", "ws": "^8.0.0" }, "optionalPeers": ["drizzle-kit", "eventsource", "ws"] }, "sha512-V2IWlXq9SRLJWrcYVa8WUftELCpbdsBVWZIqz2WQjhhzRK9Aan65RfMoZVx62A9k8HuIO6MH1nAPLhOuk1xePQ=="], "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], @@ -4073,16 +4074,10 @@ "@react-router/dev/react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "@rivet-dev/agentos/rivetkit": ["rivetkit@2.3.7", "", { "dependencies": { "@hono/zod-openapi": "^1.1.5", "@rivet-dev/agent-os-core": "^0.1.1", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-cli": "2.3.7", "@rivetkit/engine-envoy-protocol": "2.3.7", "@rivetkit/on-change": "6.0.1", "@rivetkit/rivetkit-napi": "2.3.7", "@rivetkit/rivetkit-wasm": "2.3.7", "@rivetkit/traces": "2.3.7", "@rivetkit/virtual-websocket": "2.3.7", "@rivetkit/workflow-engine": "2.3.7", "cbor-x": "^1.6.0", "drizzle-orm": "^0.44.2", "hono": "^4.7.0", "invariant": "^2.2.4", "p-retry": "^6.2.1", "pino": "^9.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", "zod": "^4.1.0" }, "peerDependencies": { "drizzle-kit": "^0.31.2", "eventsource": "^4.0.0", "ws": "^8.0.0" }, "optionalPeers": ["drizzle-kit", "eventsource", "ws"] }, "sha512-V2IWlXq9SRLJWrcYVa8WUftELCpbdsBVWZIqz2WQjhhzRK9Aan65RfMoZVx62A9k8HuIO6MH1nAPLhOuk1xePQ=="], - "@rivetkit/framework-base/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], - "@rivetkit/framework-base/rivetkit": ["rivetkit@2.3.7", "", { "dependencies": { "@hono/zod-openapi": "^1.1.5", "@rivet-dev/agent-os-core": "^0.1.1", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-cli": "2.3.7", "@rivetkit/engine-envoy-protocol": "2.3.7", "@rivetkit/on-change": "6.0.1", "@rivetkit/rivetkit-napi": "2.3.7", "@rivetkit/rivetkit-wasm": "2.3.7", "@rivetkit/traces": "2.3.7", "@rivetkit/virtual-websocket": "2.3.7", "@rivetkit/workflow-engine": "2.3.7", "cbor-x": "^1.6.0", "drizzle-orm": "^0.44.2", "hono": "^4.7.0", "invariant": "^2.2.4", "p-retry": "^6.2.1", "pino": "^9.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", "zod": "^4.1.0" }, "peerDependencies": { "drizzle-kit": "^0.31.2", "eventsource": "^4.0.0", "ws": "^8.0.0" }, "optionalPeers": ["drizzle-kit", "eventsource", "ws"] }, "sha512-V2IWlXq9SRLJWrcYVa8WUftELCpbdsBVWZIqz2WQjhhzRK9Aan65RfMoZVx62A9k8HuIO6MH1nAPLhOuk1xePQ=="], - "@rivetkit/react/@tanstack/react-store": ["@tanstack/react-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg=="], - "@rivetkit/react/rivetkit": ["rivetkit@2.3.7", "", { "dependencies": { "@hono/zod-openapi": "^1.1.5", "@rivet-dev/agent-os-core": "^0.1.1", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-cli": "2.3.7", "@rivetkit/engine-envoy-protocol": "2.3.7", "@rivetkit/on-change": "6.0.1", "@rivetkit/rivetkit-napi": "2.3.7", "@rivetkit/rivetkit-wasm": "2.3.7", "@rivetkit/traces": "2.3.7", "@rivetkit/virtual-websocket": "2.3.7", "@rivetkit/workflow-engine": "2.3.7", "cbor-x": "^1.6.0", "drizzle-orm": "^0.44.2", "hono": "^4.7.0", "invariant": "^2.2.4", "p-retry": "^6.2.1", "pino": "^9.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", "zod": "^4.1.0" }, "peerDependencies": { "drizzle-kit": "^0.31.2", "eventsource": "^4.0.0", "ws": "^8.0.0" }, "optionalPeers": ["drizzle-kit", "eventsource", "ws"] }, "sha512-V2IWlXq9SRLJWrcYVa8WUftELCpbdsBVWZIqz2WQjhhzRK9Aan65RfMoZVx62A9k8HuIO6MH1nAPLhOuk1xePQ=="], - "@secure-exec/nodejs/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "@secure-exec/nodejs/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], @@ -4297,8 +4292,6 @@ "native/react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], - "node-stdlib-browser/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], - "npm-package-arg/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], "npm-package-arg/validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], @@ -4403,7 +4396,7 @@ "uniwind/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], - "url/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], + "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "vite-plus/oxfmt": ["oxfmt@0.57.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.57.0", "@oxfmt/binding-android-arm64": "0.57.0", "@oxfmt/binding-darwin-arm64": "0.57.0", "@oxfmt/binding-darwin-x64": "0.57.0", "@oxfmt/binding-freebsd-x64": "0.57.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.57.0", "@oxfmt/binding-linux-arm-musleabihf": "0.57.0", "@oxfmt/binding-linux-arm64-gnu": "0.57.0", "@oxfmt/binding-linux-arm64-musl": "0.57.0", "@oxfmt/binding-linux-ppc64-gnu": "0.57.0", "@oxfmt/binding-linux-riscv64-gnu": "0.57.0", "@oxfmt/binding-linux-riscv64-musl": "0.57.0", "@oxfmt/binding-linux-s390x-gnu": "0.57.0", "@oxfmt/binding-linux-x64-gnu": "0.57.0", "@oxfmt/binding-linux-x64-musl": "0.57.0", "@oxfmt/binding-openharmony-arm64": "0.57.0", "@oxfmt/binding-win32-arm64-msvc": "0.57.0", "@oxfmt/binding-win32-ia32-msvc": "0.57.0", "@oxfmt/binding-win32-x64-msvc": "0.57.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA=="], @@ -4537,56 +4530,8 @@ "@mariozechner/pi-coding-agent/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@rivet-dev/agentos/rivetkit/@rivetkit/engine-cli": ["@rivetkit/engine-cli@2.3.7", "", { "optionalDependencies": { "@rivetkit/engine-cli-darwin-arm64": "2.3.7", "@rivetkit/engine-cli-darwin-x64": "2.3.7", "@rivetkit/engine-cli-linux-arm64-musl": "2.3.7", "@rivetkit/engine-cli-linux-x64-musl": "2.3.7", "@rivetkit/engine-cli-win32-x64": "2.3.7" } }, "sha512-CezLwJ0B7dWDbA7qM6Aq04mwnrJAdrDActRrrcb4NBa20h7wO9KPAYBwyJz/dRnKm9EUUNcFZ6hrSDpp6T3+Rg=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/engine-envoy-protocol": ["@rivetkit/engine-envoy-protocol@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2" } }, "sha512-lsJM3ERwozQCebaOMKJzqIQlcLbbN3LXurRB0+7LsM6FUUL2l6hMoq5I2De/G35DJ6OWDva9AmzfvzI+5f1GPg=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi": ["@rivetkit/rivetkit-napi@2.3.7", "", { "dependencies": { "@napi-rs/cli": "^2.18.4", "@rivetkit/engine-envoy-protocol": "2.3.7" }, "optionalDependencies": { "@rivetkit/rivetkit-napi-darwin-arm64": "2.3.7", "@rivetkit/rivetkit-napi-darwin-x64": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-musl": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-musl": "2.3.7", "@rivetkit/rivetkit-napi-win32-x64-msvc": "2.3.7" } }, "sha512-1d3HtNKzJwdkznWDWSpCwg1rBn+nVwUfcKrApINTfiEhfDyVeznwbVFkfHWqOBHsjU17vvWFfjjTtf8QsjLwUQ=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-wasm": ["@rivetkit/rivetkit-wasm@2.3.7", "", {}, "sha512-o7QBtFOJrajyPVBIDQ32k8sDNFsUc8v2ETfNpr+Ch374ydmZMzCv3XKJU9vubc9bZa7WpCHSzf5ztc8b8i/Xpg=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/traces": ["@rivetkit/traces@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "vbare": "^0.0.4" } }, "sha512-B7QYjFP2HPxyfPsVjfcNIV7FPLWgGWGypmlT/dZM1i1Cf1WRdeuBumPLffgs3YceiY7Pm4D/gZZVt5p0m4Cx+A=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/virtual-websocket": ["@rivetkit/virtual-websocket@2.3.7", "", {}, "sha512-jwxDvYbr3YB6vFuxwRdB/AqUUMwdtC1rIAc4tNN6VQR3weHbE+PrDoHKbe85zrtlBtcnG89/p8tlovrj8qyQWA=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/workflow-engine": ["@rivetkit/workflow-engine@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "pino": "^9.6.0", "vbare": "^0.0.4" } }, "sha512-+C4kGuSNysrw2zs/LQoYxouOxufKHHMxiIXKF8Ab9GU2BCJ32RUST9dYU/gaQE/w9uM+hqAHUz0DTlOBYU6p6g=="], - - "@rivet-dev/agentos/rivetkit/uuid": ["uuid@12.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/engine-cli": ["@rivetkit/engine-cli@2.3.7", "", { "optionalDependencies": { "@rivetkit/engine-cli-darwin-arm64": "2.3.7", "@rivetkit/engine-cli-darwin-x64": "2.3.7", "@rivetkit/engine-cli-linux-arm64-musl": "2.3.7", "@rivetkit/engine-cli-linux-x64-musl": "2.3.7", "@rivetkit/engine-cli-win32-x64": "2.3.7" } }, "sha512-CezLwJ0B7dWDbA7qM6Aq04mwnrJAdrDActRrrcb4NBa20h7wO9KPAYBwyJz/dRnKm9EUUNcFZ6hrSDpp6T3+Rg=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/engine-envoy-protocol": ["@rivetkit/engine-envoy-protocol@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2" } }, "sha512-lsJM3ERwozQCebaOMKJzqIQlcLbbN3LXurRB0+7LsM6FUUL2l6hMoq5I2De/G35DJ6OWDva9AmzfvzI+5f1GPg=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi": ["@rivetkit/rivetkit-napi@2.3.7", "", { "dependencies": { "@napi-rs/cli": "^2.18.4", "@rivetkit/engine-envoy-protocol": "2.3.7" }, "optionalDependencies": { "@rivetkit/rivetkit-napi-darwin-arm64": "2.3.7", "@rivetkit/rivetkit-napi-darwin-x64": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-musl": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-musl": "2.3.7", "@rivetkit/rivetkit-napi-win32-x64-msvc": "2.3.7" } }, "sha512-1d3HtNKzJwdkznWDWSpCwg1rBn+nVwUfcKrApINTfiEhfDyVeznwbVFkfHWqOBHsjU17vvWFfjjTtf8QsjLwUQ=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-wasm": ["@rivetkit/rivetkit-wasm@2.3.7", "", {}, "sha512-o7QBtFOJrajyPVBIDQ32k8sDNFsUc8v2ETfNpr+Ch374ydmZMzCv3XKJU9vubc9bZa7WpCHSzf5ztc8b8i/Xpg=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/traces": ["@rivetkit/traces@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "vbare": "^0.0.4" } }, "sha512-B7QYjFP2HPxyfPsVjfcNIV7FPLWgGWGypmlT/dZM1i1Cf1WRdeuBumPLffgs3YceiY7Pm4D/gZZVt5p0m4Cx+A=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/virtual-websocket": ["@rivetkit/virtual-websocket@2.3.7", "", {}, "sha512-jwxDvYbr3YB6vFuxwRdB/AqUUMwdtC1rIAc4tNN6VQR3weHbE+PrDoHKbe85zrtlBtcnG89/p8tlovrj8qyQWA=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/workflow-engine": ["@rivetkit/workflow-engine@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "pino": "^9.6.0", "vbare": "^0.0.4" } }, "sha512-+C4kGuSNysrw2zs/LQoYxouOxufKHHMxiIXKF8Ab9GU2BCJ32RUST9dYU/gaQE/w9uM+hqAHUz0DTlOBYU6p6g=="], - - "@rivetkit/framework-base/rivetkit/uuid": ["uuid@12.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="], - "@rivetkit/react/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], - "@rivetkit/react/rivetkit/@rivetkit/engine-cli": ["@rivetkit/engine-cli@2.3.7", "", { "optionalDependencies": { "@rivetkit/engine-cli-darwin-arm64": "2.3.7", "@rivetkit/engine-cli-darwin-x64": "2.3.7", "@rivetkit/engine-cli-linux-arm64-musl": "2.3.7", "@rivetkit/engine-cli-linux-x64-musl": "2.3.7", "@rivetkit/engine-cli-win32-x64": "2.3.7" } }, "sha512-CezLwJ0B7dWDbA7qM6Aq04mwnrJAdrDActRrrcb4NBa20h7wO9KPAYBwyJz/dRnKm9EUUNcFZ6hrSDpp6T3+Rg=="], - - "@rivetkit/react/rivetkit/@rivetkit/engine-envoy-protocol": ["@rivetkit/engine-envoy-protocol@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2" } }, "sha512-lsJM3ERwozQCebaOMKJzqIQlcLbbN3LXurRB0+7LsM6FUUL2l6hMoq5I2De/G35DJ6OWDva9AmzfvzI+5f1GPg=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi": ["@rivetkit/rivetkit-napi@2.3.7", "", { "dependencies": { "@napi-rs/cli": "^2.18.4", "@rivetkit/engine-envoy-protocol": "2.3.7" }, "optionalDependencies": { "@rivetkit/rivetkit-napi-darwin-arm64": "2.3.7", "@rivetkit/rivetkit-napi-darwin-x64": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-arm64-musl": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-gnu": "2.3.7", "@rivetkit/rivetkit-napi-linux-x64-musl": "2.3.7", "@rivetkit/rivetkit-napi-win32-x64-msvc": "2.3.7" } }, "sha512-1d3HtNKzJwdkznWDWSpCwg1rBn+nVwUfcKrApINTfiEhfDyVeznwbVFkfHWqOBHsjU17vvWFfjjTtf8QsjLwUQ=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-wasm": ["@rivetkit/rivetkit-wasm@2.3.7", "", {}, "sha512-o7QBtFOJrajyPVBIDQ32k8sDNFsUc8v2ETfNpr+Ch374ydmZMzCv3XKJU9vubc9bZa7WpCHSzf5ztc8b8i/Xpg=="], - - "@rivetkit/react/rivetkit/@rivetkit/traces": ["@rivetkit/traces@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "vbare": "^0.0.4" } }, "sha512-B7QYjFP2HPxyfPsVjfcNIV7FPLWgGWGypmlT/dZM1i1Cf1WRdeuBumPLffgs3YceiY7Pm4D/gZZVt5p0m4Cx+A=="], - - "@rivetkit/react/rivetkit/@rivetkit/virtual-websocket": ["@rivetkit/virtual-websocket@2.3.7", "", {}, "sha512-jwxDvYbr3YB6vFuxwRdB/AqUUMwdtC1rIAc4tNN6VQR3weHbE+PrDoHKbe85zrtlBtcnG89/p8tlovrj8qyQWA=="], - - "@rivetkit/react/rivetkit/@rivetkit/workflow-engine": ["@rivetkit/workflow-engine@2.3.7", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "pino": "^9.6.0", "vbare": "^0.0.4" } }, "sha512-+C4kGuSNysrw2zs/LQoYxouOxufKHHMxiIXKF8Ab9GU2BCJ32RUST9dYU/gaQE/w9uM+hqAHUz0DTlOBYU6p6g=="], - - "@rivetkit/react/rivetkit/uuid": ["uuid@12.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="], - "@secure-exec/nodejs/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "@secure-exec/nodejs/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], @@ -4981,66 +4926,6 @@ "@google/genai/google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "@rivet-dev/agentos/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-darwin-arm64": ["@rivetkit/engine-cli-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-F3yGSDIaW94BZzuAm3erQRT/Ki55wHJ/b6tOaE7d9tRmTAZw/XqHmjNdUYjOvFUQKLb+SSsOittM4dINrAANQQ=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-darwin-x64": ["@rivetkit/engine-cli-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8BbQsamIvv8U/cEoJUFr+uAzwjjl9cG+ZZxjDX9OJxbuD11ESpHvDLgge54ijynO3IjoP8cIzjIXat6mr2Gkw=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-linux-arm64-musl": ["@rivetkit/engine-cli-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-G3s76BHfIs6R9rm6JYI13sOjPLy08+C8WvVoFwSdMqsibEaqlfo36wVkp/x1g0sEZUek/dBxVrKqMMR7c4/gug=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-linux-x64-musl": ["@rivetkit/engine-cli-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-LWhrUVzULcsZ4ufCLsx23YfrdMW2INd9g0ANBac9+PzGwL8qYfVIXnDpR3587lapu68EbSEPZcconf1tBlvJxQ=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-darwin-arm64": ["@rivetkit/rivetkit-napi-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gI1wMe6z85VDeR4K/wnzevVakXTg5UCfvjlDOp1+23nyU3NlInhN156dEWO8EsfRMrs+1bYoGdc4rcoN+4PUrw=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-darwin-x64": ["@rivetkit/rivetkit-napi-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-H5tUwvCbKwTIBiyQG01ikK5mdc9ISg+bv8gPUw41mYOKq8rb9nBLkcl2TKcreFHac8KaGxtnBZzYcjbUV3NcwQ=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-arm64-gnu": ["@rivetkit/rivetkit-napi-linux-arm64-gnu@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-MpOeiO5M737e93hM/FjcH+zv8xBEX1tgBd/59K/Uw7qlRbWW7JGz8WGjavdxv211LRqxsn8EUlGRxcADDsXnBA=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-arm64-musl": ["@rivetkit/rivetkit-napi-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-0GsUgd/HHvIq6v92kp0H175mGm2Exvs4krBLih0MIpVAkqJk8EIcKI6wJhCwY04Fr0MS8sHkUhjuHXa7QcmlYA=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-x64-gnu": ["@rivetkit/rivetkit-napi-linux-x64-gnu@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8wCD1FuDpM+yiKRG8ro1K8ILVpJXzCNKViJ1axq1b4Nyqgod/nyEvvKDaT4qhA5+Om+W5i6YRO1uddjC6QjJSQ=="], - - "@rivet-dev/agentos/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-x64-musl": ["@rivetkit/rivetkit-napi-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8RGzXD33GH1/DWufFzCZRI+npF5IL/9z7Ja3pthsuYgrL+axGG0iqfAjbwj5HHnB/ceUGQVT+uCKzoLnG+odtw=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-darwin-arm64": ["@rivetkit/engine-cli-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-F3yGSDIaW94BZzuAm3erQRT/Ki55wHJ/b6tOaE7d9tRmTAZw/XqHmjNdUYjOvFUQKLb+SSsOittM4dINrAANQQ=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-darwin-x64": ["@rivetkit/engine-cli-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8BbQsamIvv8U/cEoJUFr+uAzwjjl9cG+ZZxjDX9OJxbuD11ESpHvDLgge54ijynO3IjoP8cIzjIXat6mr2Gkw=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-linux-arm64-musl": ["@rivetkit/engine-cli-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-G3s76BHfIs6R9rm6JYI13sOjPLy08+C8WvVoFwSdMqsibEaqlfo36wVkp/x1g0sEZUek/dBxVrKqMMR7c4/gug=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-linux-x64-musl": ["@rivetkit/engine-cli-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-LWhrUVzULcsZ4ufCLsx23YfrdMW2INd9g0ANBac9+PzGwL8qYfVIXnDpR3587lapu68EbSEPZcconf1tBlvJxQ=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-darwin-arm64": ["@rivetkit/rivetkit-napi-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gI1wMe6z85VDeR4K/wnzevVakXTg5UCfvjlDOp1+23nyU3NlInhN156dEWO8EsfRMrs+1bYoGdc4rcoN+4PUrw=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-darwin-x64": ["@rivetkit/rivetkit-napi-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-H5tUwvCbKwTIBiyQG01ikK5mdc9ISg+bv8gPUw41mYOKq8rb9nBLkcl2TKcreFHac8KaGxtnBZzYcjbUV3NcwQ=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-arm64-gnu": ["@rivetkit/rivetkit-napi-linux-arm64-gnu@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-MpOeiO5M737e93hM/FjcH+zv8xBEX1tgBd/59K/Uw7qlRbWW7JGz8WGjavdxv211LRqxsn8EUlGRxcADDsXnBA=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-arm64-musl": ["@rivetkit/rivetkit-napi-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-0GsUgd/HHvIq6v92kp0H175mGm2Exvs4krBLih0MIpVAkqJk8EIcKI6wJhCwY04Fr0MS8sHkUhjuHXa7QcmlYA=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-x64-gnu": ["@rivetkit/rivetkit-napi-linux-x64-gnu@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8wCD1FuDpM+yiKRG8ro1K8ILVpJXzCNKViJ1axq1b4Nyqgod/nyEvvKDaT4qhA5+Om+W5i6YRO1uddjC6QjJSQ=="], - - "@rivetkit/framework-base/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-x64-musl": ["@rivetkit/rivetkit-napi-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8RGzXD33GH1/DWufFzCZRI+npF5IL/9z7Ja3pthsuYgrL+axGG0iqfAjbwj5HHnB/ceUGQVT+uCKzoLnG+odtw=="], - - "@rivetkit/react/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-darwin-arm64": ["@rivetkit/engine-cli-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-F3yGSDIaW94BZzuAm3erQRT/Ki55wHJ/b6tOaE7d9tRmTAZw/XqHmjNdUYjOvFUQKLb+SSsOittM4dINrAANQQ=="], - - "@rivetkit/react/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-darwin-x64": ["@rivetkit/engine-cli-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8BbQsamIvv8U/cEoJUFr+uAzwjjl9cG+ZZxjDX9OJxbuD11ESpHvDLgge54ijynO3IjoP8cIzjIXat6mr2Gkw=="], - - "@rivetkit/react/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-linux-arm64-musl": ["@rivetkit/engine-cli-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-G3s76BHfIs6R9rm6JYI13sOjPLy08+C8WvVoFwSdMqsibEaqlfo36wVkp/x1g0sEZUek/dBxVrKqMMR7c4/gug=="], - - "@rivetkit/react/rivetkit/@rivetkit/engine-cli/@rivetkit/engine-cli-linux-x64-musl": ["@rivetkit/engine-cli-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-LWhrUVzULcsZ4ufCLsx23YfrdMW2INd9g0ANBac9+PzGwL8qYfVIXnDpR3587lapu68EbSEPZcconf1tBlvJxQ=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-darwin-arm64": ["@rivetkit/rivetkit-napi-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gI1wMe6z85VDeR4K/wnzevVakXTg5UCfvjlDOp1+23nyU3NlInhN156dEWO8EsfRMrs+1bYoGdc4rcoN+4PUrw=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-darwin-x64": ["@rivetkit/rivetkit-napi-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-H5tUwvCbKwTIBiyQG01ikK5mdc9ISg+bv8gPUw41mYOKq8rb9nBLkcl2TKcreFHac8KaGxtnBZzYcjbUV3NcwQ=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-arm64-gnu": ["@rivetkit/rivetkit-napi-linux-arm64-gnu@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-MpOeiO5M737e93hM/FjcH+zv8xBEX1tgBd/59K/Uw7qlRbWW7JGz8WGjavdxv211LRqxsn8EUlGRxcADDsXnBA=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-arm64-musl": ["@rivetkit/rivetkit-napi-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-0GsUgd/HHvIq6v92kp0H175mGm2Exvs4krBLih0MIpVAkqJk8EIcKI6wJhCwY04Fr0MS8sHkUhjuHXa7QcmlYA=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-x64-gnu": ["@rivetkit/rivetkit-napi-linux-x64-gnu@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8wCD1FuDpM+yiKRG8ro1K8ILVpJXzCNKViJ1axq1b4Nyqgod/nyEvvKDaT4qhA5+Om+W5i6YRO1uddjC6QjJSQ=="], - - "@rivetkit/react/rivetkit/@rivetkit/rivetkit-napi/@rivetkit/rivetkit-napi-linux-x64-musl": ["@rivetkit/rivetkit-napi-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8RGzXD33GH1/DWufFzCZRI+npF5IL/9z7Ja3pthsuYgrL+axGG0iqfAjbwj5HHnB/ceUGQVT+uCKzoLnG+odtw=="], - "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/packages/agents/src/actions/finalize-gitea-lifecycle.ts b/packages/agents/src/actions/finalize-gitea-lifecycle.ts index 68154e1..59dfd1d 100644 --- a/packages/agents/src/actions/finalize-gitea-lifecycle.ts +++ b/packages/agents/src/actions/finalize-gitea-lifecycle.ts @@ -1,3 +1,6 @@ +import { exec, execFile } from "node:child_process"; +import { promisify } from "node:util"; + import { api } from "@code/backend/convex/_generated/api"; import type { Id } from "@code/backend/convex/_generated/dataModel"; import { parseAgentEnv } from "@code/env/agent"; @@ -9,6 +12,64 @@ import { createGiteaHttpTransport, runPostRunGiteaLifecycle, } from "../git/gitea"; +import { AgentOsSandboxApi } from "../sandboxes/agent-os"; +import { + clonePublicRepository, + mirrorSandboxToHostCheckout, +} from "../sandboxes/host-repository-bridge"; + +const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); +const GIT_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024; + +const createHostGitRunner = () => ({ + async run( + command: string, + options?: { cwd?: string; env?: Record } + ) { + try { + const result = await execAsync(command, { + cwd: options?.cwd, + env: options?.env, + maxBuffer: GIT_OUTPUT_LIMIT_BYTES, + }); + return { + exitCode: 0, + stderr: String(result.stderr), + stdout: String(result.stdout), + }; + } catch (error) { + const failure = error as Error & { + readonly code?: number; + readonly stderr?: string; + readonly stdout?: string; + }; + return { + exitCode: + typeof failure.code === "number" && failure.code > 0 + ? failure.code + : 1, + stderr: failure.stderr ?? failure.message, + stdout: failure.stdout ?? "", + }; + } + }, +}); + +const makeAuthenticatedRepositoryUrl = ( + repositoryUrl: string, + repositoryPath: string, + token: string +): string => { + const [owner] = repositoryPath.split("/"); + if (!owner) { + throw new Error("Gitea repository path has no owner"); + } + const url = new URL(repositoryUrl); + url.username = owner; + url.password = token; + return url.toString(); +}; const pullRequestOutput = v.object({ baseBranch: v.string(), @@ -49,7 +110,7 @@ export const createFinalizeGiteaLifecycle = ( }), name: "finalize_gitea_lifecycle", output, - async run({ harness, input, log }) { + async run({ input, log }) { const context = await client.query(api.agentWorkspace.get, { issueId, token: env.FLUE_DB_TOKEN, @@ -65,21 +126,49 @@ export const createFinalizeGiteaLifecycle = ( "GITEA_TOKEN is required for Gitea pull request creation" ); } + if (!context.run) { + throw new Error("Project work run is not initialized"); + } - const runner = { - async run( - command: string, - options?: { cwd?: string; env?: Record } - ) { - return await harness.shell(command, options); - }, - }; + const sandbox = new AgentOsSandboxApi( + client, + env.DAEMON_ID, + context.run.actorKey + ); + const checkout = await clonePublicRepository({ + branchName: + context.run.branchName ?? `work/issue-${context.issue.number}`, + repositoryUrl: context.source.url, + }); + const runner = createHostGitRunner(); const transport = createGiteaHttpTransport({ baseUrl: env.GITEA_URL, token: env.GITEA_TOKEN, }); try { + await mirrorSandboxToHostCheckout({ + checkoutDirectory: checkout.checkoutDirectory, + sandbox, + sandboxDirectory: context.run.checkoutPath ?? "/workspace/repository", + }); + await execFileAsync( + "git", + [ + "remote", + "set-url", + "origin", + makeAuthenticatedRepositoryUrl( + context.source.url, + context.source.repositoryPath, + env.GITEA_TOKEN + ), + ], + { + cwd: checkout.checkoutDirectory, + maxBuffer: GIT_OUTPUT_LIMIT_BYTES, + } + ); const result = await runPostRunGiteaLifecycle({ baseBranch: context.source.defaultBranch, body: `Verified changes for project issue #${context.issue.number}. Merge remains a manual review action.`, @@ -91,7 +180,7 @@ export const createFinalizeGiteaLifecycle = ( title: `Issue #${context.issue.number}: ${context.issue.title}`, transport, verification: input.verified ? "passed" : "failed", - workspace: "/workspace/repository", + workspace: checkout.checkoutDirectory, }); await client.mutation(api.agentWorkspace.recordGiteaLifecycle, { baseBranch: result.baseBranch, @@ -113,7 +202,7 @@ export const createFinalizeGiteaLifecycle = ( return result; } catch (error) { const branchResult = await runner.run("git branch --show-current", { - cwd: "/workspace/repository", + cwd: checkout.checkoutDirectory, }); const branch = branchResult.stdout.trim() || "unknown"; const message = error instanceof Error ? error.message : String(error); @@ -126,6 +215,8 @@ export const createFinalizeGiteaLifecycle = ( token: env.FLUE_DB_TOKEN, }); throw error; + } finally { + await checkout.cleanup(); } }, }); diff --git a/packages/agents/src/agents/project-manager.ts b/packages/agents/src/agents/project-manager.ts index 65e7c22..e27008c 100644 --- a/packages/agents/src/agents/project-manager.ts +++ b/packages/agents/src/agents/project-manager.ts @@ -22,7 +22,7 @@ export default defineAgent(({ env, id }) => { Start every run by calling lookup_issue_context, reading /workspace/control/issue.md, the canonical context files under /workspace/control/context, and the operational artifacts under /workspace/control/artifacts. Call report_work_status with working before editing. -Work only on the bound issue. The repository checkout is the current working directory; inspect existing source files before changing them. If a missing product decision makes safe progress impossible, publish the current work.md and steps.md, report needs-input, then ask one focused question. Otherwise implement the complete issue, run the relevant install, edit, test, or scenario command in AgentOS, and preserve command evidence. +Work only on the bound issue. The repository checkout is the current working directory; inspect existing source files before changing them. Git metadata stays on the trusted host bridge, so do not run git commands inside AgentOS—the final lifecycle action mirrors verified files into the host checkout before committing and pushing. If a missing product decision makes safe progress impossible, publish the current work.md and steps.md, report needs-input, then ask one focused question. Otherwise implement the complete issue, run the relevant install, edit, test, or scenario command in AgentOS, and preserve command evidence. Before finishing, update the operational files under /workspace/control/artifacts and publish each changed canonical artifact with publish_project_artifact. After verification, call finalize_gitea_lifecycle with verified=true; it owns the post-run Git/Gitea lifecycle and never merges. Report completed only after that action succeeds or reports no_changes. On an unrecoverable error, report failed with the exact blocker. Never claim a repository change or command result you did not observe.`, model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`, diff --git a/packages/agents/src/git/gitea.test.ts b/packages/agents/src/git/gitea.test.ts index b49e32e..d27bb64 100644 --- a/packages/agents/src/git/gitea.test.ts +++ b/packages/agents/src/git/gitea.test.ts @@ -105,7 +105,7 @@ describe("Gitea lifecycle adapter", () => { "git add --all && git commit -m 'feat(issue-5): Publish verified changes'", "git rev-parse HEAD", "git status --porcelain=v1", - "git push 'ssh://git@git.openputer.com:2222/puter/zopu-code.git' HEAD:'work/issue-5'", + "git push origin HEAD:'work/issue-5'", ]); expect(pullRequests).toHaveLength(1); }); diff --git a/packages/agents/src/git/gitea.ts b/packages/agents/src/git/gitea.ts index 18eefaf..dea96fb 100644 --- a/packages/agents/src/git/gitea.ts +++ b/packages/agents/src/git/gitea.ts @@ -248,7 +248,7 @@ export const runPostRunGiteaLifecycle = async ( }; } - const repository = await input.transport.getRepository(input.repositoryPath); + await input.transport.getRepository(input.repositoryPath); await runChecked( input.runner, @@ -276,7 +276,7 @@ export const runPostRunGiteaLifecycle = async ( await runChecked( input.runner, - `git push ${shellQuote(repository.sshUrl)} HEAD:${shellQuote(inspection.branch)}`, + `git push origin HEAD:${shellQuote(inspection.branch)}`, { cwd: input.workspace } ); diff --git a/packages/agents/src/sandboxes/agent-os.ts b/packages/agents/src/sandboxes/agent-os.ts index a3a973f..3d59b79 100644 --- a/packages/agents/src/sandboxes/agent-os.ts +++ b/packages/agents/src/sandboxes/agent-os.ts @@ -11,6 +11,11 @@ import { ConvexHttpClient } from "convex/browser"; import { Effect } from "effect"; import * as v from "valibot"; +import { + clonePublicRepository, + mirrorHostCheckoutToSandbox, +} from "./host-repository-bridge"; + const execResultSchema = v.object({ exitCode: v.number(), stderr: v.string(), @@ -29,7 +34,7 @@ interface ExecuteOptions { readonly timeoutMs?: number; } -class AgentOsSandboxApi implements SandboxApi { +export class AgentOsSandboxApi implements SandboxApi { readonly #actorKey: string[]; readonly #client: ConvexHttpClient; readonly #daemonId: string; @@ -258,13 +263,27 @@ export const agentOs = ( if (!checkoutOperation || checkoutOperation._tag !== "Exec") { throw new Error("Issue workspace plan must include a checkout command"); } - const checkoutCommandResult = await sandbox.exec( - checkoutOperation.command, - { - cwd: checkoutOperation.cwd, - timeoutMs: checkoutOperation.timeoutMs, + const hostCheckout = await clonePublicRepository({ + branchName: plan.branchName, + repositoryUrl: plan.sourceUrl, + timeoutMs: checkoutOperation.timeoutMs, + }); + try { + if (!(await sandbox.exists(plan.checkoutPath))) { + await mirrorHostCheckoutToSandbox({ + checkoutDirectory: hostCheckout.checkoutDirectory, + sandbox, + sandboxDirectory: plan.checkoutPath, + }); } - ); + } finally { + await hostCheckout.cleanup(); + } + const checkoutCommandResult = { + exitCode: 0, + stderr: "", + stdout: `${plan.branchName} ${hostCheckout.headSha}\n`, + }; await Promise.all( stagingOperations diff --git a/packages/agents/src/sandboxes/host-repository-bridge.test.ts b/packages/agents/src/sandboxes/host-repository-bridge.test.ts new file mode 100644 index 0000000..98214d5 --- /dev/null +++ b/packages/agents/src/sandboxes/host-repository-bridge.test.ts @@ -0,0 +1,297 @@ +import { + lstat, + mkdir, + mkdtemp, + readFile, + rm, + stat, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import nodePath from "node:path"; + +import type { FileStat, SandboxApi } from "@flue/runtime"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + mirrorHostCheckoutToSandbox, + mirrorSandboxToHostCheckout, + parseGitCommitSha, + parsePublicGitUrl, +} from "./host-repository-bridge"; + +class FakeSandbox implements SandboxApi { + readonly files = new Map(); + readonly directories = new Set(["/"]); + + async readFile(path: string): Promise { + return new TextDecoder().decode(await this.readFileBuffer(path)); + } + + readFileBuffer(path: string): Promise { + const content = this.files.get(path); + if (!content) { + throw new Error(`Missing fake sandbox file: ${path}`); + } + return Promise.resolve(Uint8Array.from(content)); + } + + writeFile(path: string, content: string | Uint8Array): Promise { + this.files.set( + path, + typeof content === "string" + ? new TextEncoder().encode(content) + : Uint8Array.from(content) + ); + return Promise.resolve(); + } + + stat(path: string): Promise { + const content = this.files.get(path); + if (content) { + return Promise.resolve({ + isDirectory: false, + isFile: true, + size: content.byteLength, + }); + } + if (this.directories.has(path)) { + return Promise.resolve({ isDirectory: true, isFile: false }); + } + throw new Error(`Missing fake sandbox path: ${path}`); + } + + readdir(path: string): Promise { + const prefix = path === "/" ? "/" : `${path}/`; + const entries = new Set(); + for (const directory of this.directories) { + if (directory.startsWith(prefix)) { + const [entry] = directory.slice(prefix.length).split("/"); + if (entry) { + entries.add(entry); + } + } + } + for (const file of this.files.keys()) { + if (file.startsWith(prefix)) { + const [entry] = file.slice(prefix.length).split("/"); + if (entry) { + entries.add(entry); + } + } + } + return Promise.resolve([...entries]); + } + + exists(path: string): Promise { + return Promise.resolve(this.files.has(path) || this.directories.has(path)); + } + + mkdir( + path: string, + options?: { readonly recursive?: boolean } + ): Promise { + if (!options?.recursive) { + this.directories.add(path); + return Promise.resolve(); + } + const parts = path.split("/").filter(Boolean); + let current = ""; + for (const part of parts) { + current += `/${part}`; + this.directories.add(current); + } + return Promise.resolve(); + } + + rm( + path: string, + options?: { readonly force?: boolean; readonly recursive?: boolean } + ): Promise { + const prefix = `${path}/`; + if (options?.recursive) { + for (const file of this.files.keys()) { + if (file === path || file.startsWith(prefix)) { + this.files.delete(file); + } + } + for (const directory of this.directories) { + if (directory === path || directory.startsWith(prefix)) { + this.directories.delete(directory); + } + } + return Promise.resolve(); + } + this.files.delete(path); + this.directories.delete(path); + return Promise.resolve(); + } + + exec(): Promise<{ + exitCode: number; + stderr: string; + stdout: string; + }> { + void this.directories; + return Promise.resolve({ exitCode: 0, stderr: "", stdout: "" }); + } +} + +const temporaryDirectories: string[] = []; + +const makeCheckout = async (): Promise => { + const directory = await mkdtemp(nodePath.join(tmpdir(), "host-bridge-test-")); + temporaryDirectories.push(directory); + await mkdir(nodePath.join(directory, ".git"), { recursive: true }); + await writeFile(nodePath.join(directory, ".git", "keep"), "git metadata"); + return directory; +}; + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { force: true, recursive: true })) + ); +}); + +describe("parsePublicGitUrl", () => { + it("accepts public HTTP(S) repositories", () => { + expect( + parsePublicGitUrl("https://git.openputer.com/team/repository.git") + .hostname + ).toBe("git.openputer.com"); + }); + + it.each([ + "file:///tmp/repository", + "ssh://git@example.com/team/repository.git", + "https://token@example.com/team/repository.git", + "http://127.0.0.1/repository.git", + "http://192.168.1.10/repository.git", + ])("rejects non-public repository URL %s", (repositoryUrl) => { + expect(() => parsePublicGitUrl(repositoryUrl)).toThrow(); + }); +}); + +describe("parseGitCommitSha", () => { + it("normalizes a full SHA returned by git", () => { + const sha = "a".repeat(40); + expect(parseGitCommitSha(` ${sha}\n`)).toBe(sha); + }); + + it.each(["", "abc123", "g".repeat(40), "a".repeat(41)])( + "rejects invalid commit SHA %s", + (sha) => { + expect(() => parseGitCommitSha(sha)).toThrow("invalid HEAD commit SHA"); + } + ); +}); + +describe("repository mirroring", () => { + it("copies host files into a sandbox without copying .git", async () => { + const checkoutDirectory = await makeCheckout(); + await mkdir(nodePath.join(checkoutDirectory, "src"), { + recursive: true, + }); + await writeFile(nodePath.join(checkoutDirectory, "README.md"), "hello"); + await writeFile( + nodePath.join(checkoutDirectory, "src", "binary.bin"), + Uint8Array.from([0, 255, 1]) + ); + const sandbox = new FakeSandbox(); + + await mirrorHostCheckoutToSandbox({ + checkoutDirectory, + sandbox, + sandboxDirectory: "/workspace/repository", + }); + + expect( + new TextDecoder().decode( + sandbox.files.get("/workspace/repository/README.md") + ) + ).toBe("hello"); + expect(sandbox.files.get("/workspace/repository/src/binary.bin")).toEqual( + Uint8Array.from([0, 255, 1]) + ); + expect( + [...sandbox.files.keys()].some((filePath) => filePath.includes("/.git/")) + ).toBe(false); + }); + + it("stages sandbox files, removes stale files, and preserves .git", async () => { + const checkoutDirectory = await makeCheckout(); + await writeFile(nodePath.join(checkoutDirectory, "stale.txt"), "delete me"); + const executablePath = nodePath.join(checkoutDirectory, "run.sh"); + await writeFile(executablePath, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + const sandbox = new FakeSandbox(); + await sandbox.mkdir("/workspace/repository/src", { recursive: true }); + await sandbox.writeFile( + "/workspace/repository/run.sh", + "#!/bin/sh\necho changed\n" + ); + await sandbox.writeFile( + "/workspace/repository/src/index.ts", + "export const value = 1;\n" + ); + + await mirrorSandboxToHostCheckout({ + checkoutDirectory, + sandbox, + sandboxDirectory: "/workspace/repository", + }); + + await expect( + readFile(nodePath.join(checkoutDirectory, "stale.txt")) + ).rejects.toThrow(); + await expect( + readFile(nodePath.join(checkoutDirectory, ".git", "keep"), "utf-8") + ).resolves.toBe("git metadata"); + await expect( + readFile(nodePath.join(checkoutDirectory, "src", "index.ts"), "utf-8") + ).resolves.toBe("export const value = 1;\n"); + const executableMetadata = await stat(executablePath); + expect(executableMetadata.mode % 0o1000).toBe(0o755); + }); + + it("enforces transfer limits before modifying the sandbox", async () => { + const checkoutDirectory = await makeCheckout(); + await writeFile(nodePath.join(checkoutDirectory, "one.txt"), "1"); + await writeFile(nodePath.join(checkoutDirectory, "two.txt"), "2"); + const sandbox = new FakeSandbox(); + + await expect( + mirrorHostCheckoutToSandbox({ + checkoutDirectory, + limits: { maxFiles: 1 }, + sandbox, + sandboxDirectory: "/workspace/repository", + }) + ).rejects.toThrow("limit is 1"); + expect(sandbox.files.size).toBe(0); + }); + + it("rejects host symbolic links", async () => { + const checkoutDirectory = await makeCheckout(); + await writeFile(nodePath.join(checkoutDirectory, "outside.txt"), "outside"); + await symlink( + "outside.txt", + nodePath.join(checkoutDirectory, "linked.txt") + ); + const sandbox = new FakeSandbox(); + + await expect( + mirrorHostCheckoutToSandbox({ + checkoutDirectory, + sandbox, + sandboxDirectory: "/workspace/repository", + }) + ).rejects.toThrow("Symbolic links are not supported"); + const linkedMetadata = await lstat( + nodePath.join(checkoutDirectory, "linked.txt") + ); + expect(linkedMetadata.isSymbolicLink()).toBe(true); + }); +}); diff --git a/packages/agents/src/sandboxes/host-repository-bridge.ts b/packages/agents/src/sandboxes/host-repository-bridge.ts new file mode 100644 index 0000000..07bfdb4 --- /dev/null +++ b/packages/agents/src/sandboxes/host-repository-bridge.ts @@ -0,0 +1,506 @@ +import { execFile } from "node:child_process"; +import { + chmod, + lstat, + mkdir, + mkdtemp, + readdir, + readFile, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import { isIP } from "node:net"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; + +import type { SandboxApi } from "@flue/runtime"; + +const DEFAULT_GIT_TIMEOUT_MS = 60_000; +const GIT_OUTPUT_LIMIT_BYTES = 1_048_576; + +export interface RepositoryMirrorLimits { + readonly maxDepth: number; + readonly maxFileBytes: number; + readonly maxFiles: number; + readonly maxTotalBytes: number; +} + +export const DEFAULT_REPOSITORY_MIRROR_LIMITS: RepositoryMirrorLimits = { + maxDepth: 64, + maxFileBytes: 10 * 1024 * 1024, + maxFiles: 10_000, + maxTotalBytes: 100 * 1024 * 1024, +}; + +export interface HostRepositoryCheckout { + readonly branchName: string; + readonly checkoutDirectory: string; + readonly headSha: string; + readonly temporaryDirectory: string; + readonly cleanup: () => Promise; +} + +interface MirrorFile { + readonly mode?: number; + readonly path: string; + readonly size: number; +} + +interface GitResult { + readonly stderr: string; + readonly stdout: string; +} + +const gitError = ( + args: readonly string[], + error: Error & { readonly stderr?: string } +): Error => { + const detail = error.stderr?.trim() || error.message; + return new Error(`git ${args.join(" ")} failed: ${detail}`, { + cause: error, + }); +}; + +const execFileAsync = promisify(execFile); + +const runGit = async ( + args: readonly string[], + options: { readonly cwd?: string; readonly timeoutMs: number } +): Promise => { + try { + const result = await execFileAsync("git", [...args], { + cwd: options.cwd, + maxBuffer: GIT_OUTPUT_LIMIT_BYTES, + timeout: options.timeoutMs, + }); + return { + stderr: String(result.stderr), + stdout: String(result.stdout), + }; + } catch (error) { + if (!(error instanceof Error)) { + throw error; + } + throw gitError( + args, + error as Error & { + readonly stderr?: string; + } + ); + } +}; + +const isPrivateIpv4 = (hostname: string): boolean => { + const octets = hostname.split(".").map(Number); + const [first = -1, second = -1] = octets; + return ( + first === 0 || + first === 10 || + first === 127 || + (first === 169 && second === 254) || + (first === 172 && second >= 16 && second <= 31) || + (first === 192 && second === 168) + ); +}; + +const isPrivateIpv6 = (hostname: string): boolean => { + const normalized = hostname + .toLowerCase() + .replaceAll("[", "") + .replaceAll("]", ""); + return ( + normalized === "::" || + normalized === "::1" || + normalized.startsWith("fc") || + normalized.startsWith("fd") || + normalized.startsWith("fe8") || + normalized.startsWith("fe9") || + normalized.startsWith("fea") || + normalized.startsWith("feb") + ); +}; + +export const parsePublicGitUrl = (repositoryUrl: string): URL => { + const url = new URL(repositoryUrl); + if (url.protocol !== "https:" && url.protocol !== "http:") { + throw new Error("Public Git repositories must use an HTTP(S) URL"); + } + if (url.username || url.password) { + throw new Error("Public Git repository URLs must not contain credentials"); + } + + const hostname = url.hostname.toLowerCase(); + const addressKind = isIP(hostname); + const isPrivateAddress = + (addressKind === 4 && isPrivateIpv4(hostname)) || + (addressKind === 6 && isPrivateIpv6(hostname)); + if ( + hostname === "localhost" || + hostname.endsWith(".localhost") || + hostname.endsWith(".local") || + isPrivateAddress + ) { + throw new Error("Public Git repository URLs must use a public host"); + } + return url; +}; + +export const parseGitCommitSha = (value: string): string => { + const sha = value.trim(); + if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(sha)) { + throw new Error("Git returned an invalid HEAD commit SHA"); + } + return sha; +}; + +export const clonePublicRepository = async (options: { + readonly branchName: string; + readonly repositoryUrl: string; + readonly timeoutMs?: number; +}): Promise => { + const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS; + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) { + throw new Error("Git timeout must be a positive integer"); + } + + const repositoryUrl = parsePublicGitUrl(options.repositoryUrl).toString(); + await runGit(["check-ref-format", "--branch", options.branchName], { + timeoutMs, + }); + + const temporaryDirectory = await mkdtemp( + path.join(tmpdir(), "zopu-host-repository-") + ); + const checkoutDirectory = path.join(temporaryDirectory, "repository"); + let headSha = ""; + + try { + await runGit( + [ + "clone", + "--depth", + "1", + "--single-branch", + "--no-tags", + "--", + repositoryUrl, + checkoutDirectory, + ], + { timeoutMs } + ); + await runGit(["switch", "-c", options.branchName], { + cwd: checkoutDirectory, + timeoutMs, + }); + const headResult = await runGit(["rev-parse", "HEAD"], { + cwd: checkoutDirectory, + timeoutMs, + }); + headSha = parseGitCommitSha(headResult.stdout); + } catch (error) { + await rm(temporaryDirectory, { force: true, recursive: true }); + throw error; + } + + return { + branchName: options.branchName, + checkoutDirectory, + cleanup: () => + rm(temporaryDirectory, { + force: true, + recursive: true, + }), + headSha, + temporaryDirectory, + }; +}; + +const resolveLimits = ( + limits: Partial | undefined +): RepositoryMirrorLimits => { + const resolvedLimits = { + ...DEFAULT_REPOSITORY_MIRROR_LIMITS, + ...limits, + }; + for (const [name, value] of Object.entries(resolvedLimits)) { + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error(`${name} must be a positive integer`); + } + } + return resolvedLimits; +}; + +const assertSafeEntryName = (name: string): void => { + if ( + !name || + name === "." || + name === ".." || + name.includes("/") || + name.includes("\\") + ) { + throw new Error(`Sandbox returned an unsafe directory entry: ${name}`); + } +}; + +const assertWithinCheckout = (checkoutDirectory: string): string => { + const absoluteCheckout = path.resolve(checkoutDirectory); + if (absoluteCheckout === path.resolve(path.sep)) { + throw new Error("Repository checkout cannot be the filesystem root"); + } + return absoluteCheckout; +}; + +const assertAbsoluteSandboxDirectory = (sandboxDirectory: string): string => { + if (!path.posix.isAbsolute(sandboxDirectory)) { + throw new Error("Sandbox directory must be absolute"); + } + return path.posix.normalize(sandboxDirectory); +}; + +const enforceFileBounds = ( + files: readonly MirrorFile[], + limits: RepositoryMirrorLimits +): void => { + if (files.length > limits.maxFiles) { + throw new Error( + `Repository contains ${files.length} files; limit is ${limits.maxFiles}` + ); + } + let totalBytes = 0; + for (const file of files) { + if (file.size > limits.maxFileBytes) { + throw new Error( + `${file.path} is ${file.size} bytes; per-file limit is ${limits.maxFileBytes}` + ); + } + totalBytes += file.size; + if (totalBytes > limits.maxTotalBytes) { + throw new Error( + `Repository files exceed the ${limits.maxTotalBytes}-byte total limit` + ); + } + } +}; + +const listHostFiles = async ( + checkoutDirectory: string, + limits: RepositoryMirrorLimits +): Promise => { + const files: MirrorFile[] = []; + + const visit = async ( + directory: string, + relativeDirectory: string + ): Promise => { + const depth = relativeDirectory + ? relativeDirectory.split(path.sep).length + : 0; + if (depth > limits.maxDepth) { + throw new Error(`Repository directory depth exceeds ${limits.maxDepth}`); + } + + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + if (!relativeDirectory && entry.name === ".git") { + continue; + } + const relativePath = path.join(relativeDirectory, entry.name); + const absolutePath = path.join(directory, entry.name); + if (entry.isSymbolicLink()) { + throw new Error(`Symbolic links are not supported: ${relativePath}`); + } + if (entry.isDirectory()) { + // oxlint-disable-next-line no-await-in-loop -- deterministic bounded traversal. + await visit(absolutePath, relativePath); + continue; + } + if (!entry.isFile()) { + throw new Error(`Unsupported repository entry: ${relativePath}`); + } + // oxlint-disable-next-line no-await-in-loop -- metadata is bounded before file transfer. + const metadata = await lstat(absolutePath); + files.push({ + mode: metadata.mode % 0o1000, + path: relativePath.split(path.sep).join(path.posix.sep), + size: metadata.size, + }); + enforceFileBounds(files, limits); + } + }; + + await visit(checkoutDirectory, ""); + return files; +}; + +const listSandboxFiles = async ( + sandbox: SandboxApi, + sandboxDirectory: string, + limits: RepositoryMirrorLimits +): Promise => { + const files: MirrorFile[] = []; + + const visit = async ( + directory: string, + relativeDirectory: string + ): Promise => { + const depth = relativeDirectory + ? relativeDirectory.split(path.posix.sep).length + : 0; + if (depth > limits.maxDepth) { + throw new Error(`Sandbox directory depth exceeds ${limits.maxDepth}`); + } + + const entries = [...(await sandbox.readdir(directory))].toSorted( + (left, right) => left.localeCompare(right) + ); + for (const entry of entries) { + assertSafeEntryName(entry); + if (!relativeDirectory && entry === ".git") { + continue; + } + const relativePath = path.posix.join(relativeDirectory, entry); + const absolutePath = path.posix.join(directory, entry); + // oxlint-disable-next-line no-await-in-loop -- sandbox metadata is read sequentially and bounded. + const metadata = await sandbox.stat(absolutePath); + if (metadata.isSymbolicLink) { + throw new Error(`Symbolic links are not supported: ${relativePath}`); + } + if (metadata.isDirectory) { + // oxlint-disable-next-line no-await-in-loop -- deterministic bounded traversal. + await visit(absolutePath, relativePath); + continue; + } + if (!metadata.isFile) { + throw new Error(`Unsupported sandbox entry: ${relativePath}`); + } + if (metadata.size === undefined) { + throw new Error(`Sandbox did not report a size for ${relativePath}`); + } + files.push({ path: relativePath, size: metadata.size }); + enforceFileBounds(files, limits); + } + }; + + await visit(sandboxDirectory, ""); + return files; +}; + +export const mirrorHostCheckoutToSandbox = async (options: { + readonly checkoutDirectory: string; + readonly limits?: Partial; + readonly sandbox: SandboxApi; + readonly sandboxDirectory: string; +}): Promise => { + const checkoutDirectory = assertWithinCheckout(options.checkoutDirectory); + const sandboxDirectory = assertAbsoluteSandboxDirectory( + options.sandboxDirectory + ); + const limits = resolveLimits(options.limits); + const files = await listHostFiles(checkoutDirectory, limits); + + await options.sandbox.mkdir(sandboxDirectory, { recursive: true }); + for (const file of files) { + const hostPath = path.join( + checkoutDirectory, + ...file.path.split(path.posix.sep) + ); + const sandboxPath = path.posix.join(sandboxDirectory, file.path); + // oxlint-disable-next-line no-await-in-loop -- bounded sequential writes avoid overwhelming remote sandboxes. + await options.sandbox.mkdir(path.posix.dirname(sandboxPath), { + recursive: true, + }); + // oxlint-disable-next-line no-await-in-loop -- bounded sequential transfer limits peak memory. + const content = await readFile(hostPath); + if (content.byteLength !== file.size) { + throw new Error(`${file.path} changed while it was being mirrored`); + } + // oxlint-disable-next-line no-await-in-loop -- bounded sequential writes avoid overwhelming remote sandboxes. + await options.sandbox.writeFile(sandboxPath, content); + } +}; + +const clearCheckoutFiles = async (checkoutDirectory: string): Promise => { + const entries = await readdir(checkoutDirectory); + for (const entry of entries) { + if (entry === ".git") { + continue; + } + // oxlint-disable-next-line no-await-in-loop -- deletion is deliberately scoped to checkout children. + await rm(path.join(checkoutDirectory, entry), { + force: true, + recursive: true, + }); + } +}; + +export const mirrorSandboxToHostCheckout = async (options: { + readonly checkoutDirectory: string; + readonly limits?: Partial; + readonly sandbox: SandboxApi; + readonly sandboxDirectory: string; +}): Promise => { + const checkoutDirectory = assertWithinCheckout(options.checkoutDirectory); + const sandboxDirectory = assertAbsoluteSandboxDirectory( + options.sandboxDirectory + ); + const limits = resolveLimits(options.limits); + const files = await listSandboxFiles( + options.sandbox, + sandboxDirectory, + limits + ); + const originalFiles = await listHostFiles(checkoutDirectory, limits); + const originalModes = new Map( + originalFiles.map((file) => [file.path, file.mode]) + ); + const stagingDirectory = await mkdtemp( + path.join(path.dirname(checkoutDirectory), ".zopu-sandbox-mirror-") + ); + + try { + let transferredBytes = 0; + for (const file of files) { + const sandboxPath = path.posix.join(sandboxDirectory, file.path); + // oxlint-disable-next-line no-await-in-loop -- bounded sequential reads limit peak memory. + const content = await options.sandbox.readFileBuffer(sandboxPath); + if (content.byteLength !== file.size) { + throw new Error(`${file.path} changed while it was being mirrored`); + } + transferredBytes += content.byteLength; + if (transferredBytes > limits.maxTotalBytes) { + throw new Error( + `Sandbox files exceed the ${limits.maxTotalBytes}-byte total limit` + ); + } + + const stagedPath = path.join( + stagingDirectory, + ...file.path.split(path.posix.sep) + ); + // oxlint-disable-next-line no-await-in-loop -- staged writes keep the checkout intact until transfer succeeds. + await mkdir(path.dirname(stagedPath), { recursive: true }); + // oxlint-disable-next-line no-await-in-loop -- bounded sequential writes limit peak memory. + await writeFile(stagedPath, content); + const originalMode = originalModes.get(file.path); + if (originalMode !== undefined) { + // oxlint-disable-next-line no-await-in-loop -- preserve executable bits for existing files. + await chmod(stagedPath, originalMode); + } + } + + await clearCheckoutFiles(checkoutDirectory); + const stagedEntries = await readdir(stagingDirectory); + for (const entry of stagedEntries) { + // oxlint-disable-next-line no-await-in-loop -- each top-level entry is moved exactly once. + await rename( + path.join(stagingDirectory, entry), + path.join(checkoutDirectory, entry) + ); + } + } finally { + await rm(stagingDirectory, { force: true, recursive: true }); + } +}; diff --git a/packages/primitives/src/agent-os-git-software.test.ts b/packages/primitives/src/agent-os-git-software.test.ts new file mode 100644 index 0000000..484ff07 --- /dev/null +++ b/packages/primitives/src/agent-os-git-software.test.ts @@ -0,0 +1,85 @@ +import { readFileSync } from "node:fs"; + +import git from "@agentos-software/git"; +import { describe, expect, it } from "vitest"; + +import { + getExecutableGitSoftware, + makeGitExecutablesRunnable, +} from "./agent-os-git-software"; + +const TAR_BLOCK_SIZE = 512; +const READ_ONLY_EXECUTABLE_MODE = Buffer.from([0xa4, 0x81, 0x00, 0x00]); +const RUNNABLE_EXECUTABLE_MODE = Buffer.from([0xed, 0x81, 0x00, 0x00]); + +const readTarModes = (packageBytes: Buffer): Map => { + const tarOffset = + 16 + packageBytes.readUInt32LE(8) + packageBytes.readUInt32LE(12); + const modes = new Map(); + let offset = tarOffset; + + while ( + offset + TAR_BLOCK_SIZE <= packageBytes.length && + packageBytes + .subarray(offset, offset + TAR_BLOCK_SIZE) + .some((byte) => byte !== 0) + ) { + const [name = ""] = packageBytes + .subarray(offset, offset + 100) + .toString("utf-8") + .split("\0"); + const mode = Number.parseInt( + packageBytes + .subarray(offset + 100, offset + 108) + .toString("ascii") + .replaceAll("\0", "") + .trim(), + 8 + ); + const size = Number.parseInt( + packageBytes + .subarray(offset + 124, offset + 136) + .toString("ascii") + .replaceAll("\0", "") + .trim() || "0", + 8 + ); + + modes.set(name, mode); + offset += + TAR_BLOCK_SIZE + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; + } + + return modes; +}; + +describe("AgentOS Git software", () => { + it("marks the packaged Git commands executable", () => { + const repaired = makeGitExecutablesRunnable(readFileSync(git.packagePath)); + const modes = readTarModes(repaired); + + expect(modes.get("./bin/git")).toBe(0o755); + expect(modes.get("./bin/git-remote-http")).toBe(0o755); + expect(modes.get("./bin/git-remote-https")).toBe(0o755); + expect(repaired.includes(Buffer.from("0.3.0-zp.1", "ascii"))).toBe(true); + + const mountIndexOffset = 16 + repaired.readUInt32LE(8); + const tarOffset = mountIndexOffset + repaired.readUInt32LE(12); + const mountIndex = repaired.subarray(mountIndexOffset, tarOffset); + expect(mountIndex.includes(READ_ONLY_EXECUTABLE_MODE)).toBe(false); + expect( + mountIndex.toString("hex").split(RUNNABLE_EXECUTABLE_MODE.toString("hex")) + .length - 1 + ).toBe(3); + }); + + it("writes a stable repaired package for the AgentOS registry", () => { + const first = getExecutableGitSoftware(); + const second = getExecutableGitSoftware(); + + expect(second).toEqual(first); + expect(readTarModes(readFileSync(first.packagePath)).get("./bin/git")).toBe( + 0o755 + ); + }); +}); diff --git a/packages/primitives/src/agent-os-git-software.ts b/packages/primitives/src/agent-os-git-software.ts new file mode 100644 index 0000000..a92ad39 --- /dev/null +++ b/packages/primitives/src/agent-os-git-software.ts @@ -0,0 +1,194 @@ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import git from "@agentos-software/git"; + +const AOS_PACKAGE_MAGIC = Buffer.from([0x89, 0x41, 0x4f, 0x53]); +const TAR_BLOCK_SIZE = 512; +const TAR_CHECKSUM_OFFSET = 148; +const TAR_CHECKSUM_SIZE = 8; +const TAR_MODE_OFFSET = 100; +const TAR_MODE_SIZE = 8; +const TAR_NAME_SIZE = 100; +const TAR_SIZE_OFFSET = 124; +const TAR_SIZE_SIZE = 12; +const GIT_EXECUTABLES = new Set([ + "./bin/git", + "./bin/git-remote-http", + "./bin/git-remote-https", +]); +const UPSTREAM_GIT_PACKAGE_VERSION = Buffer.from("0.3.0-rc.2", "ascii"); +const REPAIRED_GIT_PACKAGE_VERSION = Buffer.from("0.3.0-zp.1", "ascii"); +const READ_ONLY_EXECUTABLE_MODE = Buffer.from([0xa4, 0x81, 0x00, 0x00]); +const RUNNABLE_EXECUTABLE_MODE = Buffer.from([0xed, 0x81, 0x00, 0x00]); + +const parseTarOctal = ( + packageBytes: Buffer, + offset: number, + length: number +): number => { + const value = packageBytes + .subarray(offset, offset + length) + .toString("ascii") + .replaceAll("\0", "") + .trim(); + + return value.length === 0 ? 0 : Number.parseInt(value, 8); +}; + +const readTarName = (packageBytes: Buffer, headerOffset: number): string => { + const [name = ""] = packageBytes + .subarray(headerOffset, headerOffset + TAR_NAME_SIZE) + .toString("utf-8") + .split("\0"); + return name; +}; + +const isEmptyTarHeader = ( + packageBytes: Buffer, + headerOffset: number +): boolean => + packageBytes + .subarray(headerOffset, headerOffset + TAR_BLOCK_SIZE) + .every((byte) => byte === 0); + +const writeTarChecksum = (packageBytes: Buffer, headerOffset: number): void => { + packageBytes.fill( + 0x20, + headerOffset + TAR_CHECKSUM_OFFSET, + headerOffset + TAR_CHECKSUM_OFFSET + TAR_CHECKSUM_SIZE + ); + + let checksum = 0; + for ( + let index = headerOffset; + index < headerOffset + TAR_BLOCK_SIZE; + index += 1 + ) { + checksum += packageBytes[index] ?? 0; + } + + const encodedChecksum = `${checksum.toString(8).padStart(6, "0")}\0 `; + packageBytes.write( + encodedChecksum, + headerOffset + TAR_CHECKSUM_OFFSET, + TAR_CHECKSUM_SIZE, + "ascii" + ); +}; + +const getTarOffset = (packageBytes: Buffer): number => { + if ( + packageBytes.length < 16 || + !packageBytes + .subarray(0, AOS_PACKAGE_MAGIC.length) + .equals(AOS_PACKAGE_MAGIC) + ) { + throw new Error("Invalid AgentOS package header"); + } + + return 16 + packageBytes.readUInt32LE(8) + packageBytes.readUInt32LE(12); +}; + +export const makeGitExecutablesRunnable = (source: Uint8Array): Buffer => { + const packageBytes = Buffer.from(source); + const mountIndexOffset = 16 + packageBytes.readUInt32LE(8); + const tarOffset = getTarOffset(packageBytes); + const versionOffset = packageBytes.indexOf( + UPSTREAM_GIT_PACKAGE_VERSION, + 0, + "ascii" + ); + if (versionOffset === -1 || versionOffset >= tarOffset) { + throw new Error("Expected AgentOS Git package version was not found"); + } + REPAIRED_GIT_PACKAGE_VERSION.copy(packageBytes, versionOffset); + + let mountModeOffset = mountIndexOffset; + let repairedMountEntries = 0; + while (mountModeOffset < tarOffset) { + mountModeOffset = packageBytes.indexOf( + READ_ONLY_EXECUTABLE_MODE, + mountModeOffset + ); + if (mountModeOffset < 0 || mountModeOffset >= tarOffset) { + break; + } + + RUNNABLE_EXECUTABLE_MODE.copy(packageBytes, mountModeOffset); + repairedMountEntries += 1; + mountModeOffset += RUNNABLE_EXECUTABLE_MODE.length; + } + + if (repairedMountEntries !== GIT_EXECUTABLES.size) { + throw new Error( + `Expected ${GIT_EXECUTABLES.size} Git mount entries, repaired ${repairedMountEntries}` + ); + } + + let headerOffset = tarOffset; + let repairedExecutables = 0; + + while ( + headerOffset + TAR_BLOCK_SIZE <= packageBytes.length && + !isEmptyTarHeader(packageBytes, headerOffset) + ) { + const name = readTarName(packageBytes, headerOffset); + const size = parseTarOctal( + packageBytes, + headerOffset + TAR_SIZE_OFFSET, + TAR_SIZE_SIZE + ); + + if (GIT_EXECUTABLES.has(name)) { + packageBytes.write( + "0000755\0", + headerOffset + TAR_MODE_OFFSET, + TAR_MODE_SIZE, + "ascii" + ); + writeTarChecksum(packageBytes, headerOffset); + repairedExecutables += 1; + } + + headerOffset += + TAR_BLOCK_SIZE + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; + } + + if (repairedExecutables !== GIT_EXECUTABLES.size) { + throw new Error( + `Expected ${GIT_EXECUTABLES.size} Git executables, repaired ${repairedExecutables}` + ); + } + + return packageBytes; +}; + +let cachedGitSoftware: { readonly packagePath: string } | undefined; + +export const getExecutableGitSoftware = (): { + readonly packagePath: string; +} => { + if (cachedGitSoftware) { + return cachedGitSoftware; + } + + const source = readFileSync(git.packagePath); + const repaired = makeGitExecutablesRunnable(source); + const digest = createHash("sha256") + .update(repaired) + .digest("hex") + .slice(0, 16); + const cacheDirectory = path.join(tmpdir(), "zopu-agentos-software"); + const packagePath = path.join(cacheDirectory, `git-${digest}.aospkg`); + + mkdirSync(cacheDirectory, { recursive: true }); + if (!existsSync(packagePath)) { + writeFileSync(packagePath, repaired); + } + + cachedGitSoftware = { packagePath }; + return cachedGitSoftware; +}; diff --git a/packages/primitives/src/agent-os.ts b/packages/primitives/src/agent-os.ts index 055476d..cc6473a 100644 --- a/packages/primitives/src/agent-os.ts +++ b/packages/primitives/src/agent-os.ts @@ -1,15 +1,16 @@ /* eslint-disable max-classes-per-file -- the AgentOS service and its tagged error form one adapter contract. */ import codex from "@agentos-software/codex-cli"; -import git from "@agentos-software/git"; import { agentOS as createAgentOsActor } from "@rivet-dev/agentos"; import type { AgentOSConfigInput } from "@rivet-dev/agentos"; import { Context, Effect, Layer, Schema } from "effect"; +import { getExecutableGitSoftware } from "./agent-os-git-software"; + const withGitSoftware = ( config: AgentOSConfigInput ): AgentOSConfigInput => ({ ...config, - software: [git, ...(config.software ?? [])], + software: [getExecutableGitSoftware(), ...(config.software ?? [])], }); export class AgentOsError extends Schema.TaggedErrorClass()( @@ -83,7 +84,7 @@ export const createAgentOsActorEffect = Effect.fn("createAgentOsActorEffect")( // --------------------------------------------------------------------------- /** Software bundle for a Codex-capable VM: the Codex harness plus git. */ -export const codexSoftware = [codex, git] as const; +export const codexSoftware = [codex, getExecutableGitSoftware()] as const; /** Model-provider env that Codex reads inside the VM. Pass this to the session's * `env` when opening a Codex session (`agent: "codex"`). */ diff --git a/packages/primitives/src/project-workspace.test.ts b/packages/primitives/src/project-workspace.test.ts index a1d5723..c2a3116 100644 --- a/packages/primitives/src/project-workspace.test.ts +++ b/packages/primitives/src/project-workspace.test.ts @@ -73,6 +73,16 @@ describe("project workspace primitives", () => { operation._tag === "Exec" && operation.command.includes("git clone") ) ).toBe(true); + const checkoutOperation = plan.operations.find( + (operation) => operation._tag === "Exec" + ); + expect(checkoutOperation?._tag).toBe("Exec"); + if (checkoutOperation?._tag === "Exec") { + expect(checkoutOperation.command).not.toContain("--single-branch"); + expect(checkoutOperation.command).toContain( + "checkout -b 'work/issue-7-abc123'" + ); + } }); it("rejects unsafe sources and control-file paths", () => { diff --git a/packages/primitives/src/project-workspace.ts b/packages/primitives/src/project-workspace.ts index 19581d4..aa1775e 100644 --- a/packages/primitives/src/project-workspace.ts +++ b/packages/primitives/src/project-workspace.ts @@ -260,17 +260,18 @@ const makeCheckoutCommand = (input: { readonly sourceUrl: string; }): string => { const checkout = shellQuote(input.checkoutPath); - const baseBranch = shellQuote(input.baseBranch); const branch = shellQuote(input.branchName); - const originBaseBranch = shellQuote(`origin/${input.baseBranch}`); + const branchRef = shellQuote( + `${input.checkoutPath}/.git/refs/heads/${input.branchName}` + ); const source = shellQuote(input.sourceUrl); return [ "set -eu", `mkdir -p ${shellQuote(WORKSPACE_PATH)}`, - `if [ ! -d ${checkout}/.git ]; then git clone --branch ${baseBranch} --single-branch ${source} ${checkout}; fi`, - `if [ "$(git -C ${checkout} branch --show-current)" != ${branch} ]; then git -C ${checkout} show-ref --verify --quiet refs/heads/${branch} && git -C ${checkout} checkout ${branch} || git -C ${checkout} checkout -b ${branch} ${originBaseBranch}; fi`, - `printf '%s\\n' "$(git -C ${checkout} branch --show-current)" "$(git -C ${checkout} rev-parse HEAD)"`, + `if [ ! -d ${checkout}/.git ]; then git clone ${source} ${checkout}; fi`, + `if [ -f ${branchRef} ]; then git -C ${checkout} checkout ${branch}; else git -C ${checkout} checkout -b ${branch}; fi`, + `printf '%s\\n' ${branch} "$(git -C ${checkout} rev-parse HEAD)"`, ].join(" && "); }; diff --git a/packages/ui/src/components/mobile-product.tsx b/packages/ui/src/components/mobile-product.tsx index 012d211..6fa2480 100644 --- a/packages/ui/src/components/mobile-product.tsx +++ b/packages/ui/src/components/mobile-product.tsx @@ -2,7 +2,9 @@ export { MobileAssistantChatScreen, MobileExpandedWorkScreen, MobileHomeScreen, + MobileWorkChatScreen, MobileWorkListScreen, + MobileWorkStackScreen, MobileWorkUnitDetailScreen, } from "./mobile-workspace/index"; export type { @@ -12,6 +14,8 @@ export type { MobileSignalView, MobileWorkspaceScreenProps, MobileWorkspaceView, + MobileWorkChatScreenProps, + MobileWorkStackScreenProps, MobileWorkUnitTone, MobileWorkUnitView, } from "./mobile-workspace/index"; diff --git a/packages/ui/src/components/mobile-workspace/index.ts b/packages/ui/src/components/mobile-workspace/index.ts index c565131..7721e09 100644 --- a/packages/ui/src/components/mobile-workspace/index.ts +++ b/packages/ui/src/components/mobile-workspace/index.ts @@ -1,7 +1,13 @@ export { MobileAssistantChatScreen } from "./mobile-assistant-chat-screen"; export { MobileExpandedWorkScreen } from "./mobile-expanded-work-screen"; export { MobileHomeScreen } from "./mobile-home-screen"; +export { MobileSettingsPanel } from "./mobile-settings-panel"; +export type { MobileSettingsPanelProps } from "./mobile-settings-panel"; +export { MobileWorkChatScreen } from "./mobile-work-chat-screen"; +export type { MobileWorkChatScreenProps } from "./mobile-work-chat-screen"; export { MobileWorkListScreen } from "./mobile-work-list-screen"; +export { MobileWorkStackScreen } from "./mobile-work-stack-screen"; +export type { MobileWorkStackScreenProps } from "./mobile-work-stack-screen"; export { MobileWorkUnitDetailScreen } from "./mobile-work-unit-detail-screen"; export type { MobileAssistantMessageView, diff --git a/packages/ui/src/components/mobile-workspace/mobile-settings-panel.tsx b/packages/ui/src/components/mobile-workspace/mobile-settings-panel.tsx new file mode 100644 index 0000000..a80eeac --- /dev/null +++ b/packages/ui/src/components/mobile-workspace/mobile-settings-panel.tsx @@ -0,0 +1,83 @@ +import { X } from "lucide-react"; + +export interface MobileSettingsPanelProps { + readonly modelId: string; + readonly modelLabel: string; + readonly onClose: () => void; + readonly onSignOut?: () => void; + readonly userEmail?: string; + readonly userName?: string; +} + +export const MobileSettingsPanel = ({ + modelId, + modelLabel, + onClose, + onSignOut, + userEmail, + userName, +}: MobileSettingsPanelProps) => ( +
+
+
+

Settings

+

+ Your account and Zopu runtime. +

+
+ +
+
+
+

+ Account +

+

+ {userName ?? "Signed-in user"} +

+ {userEmail ? ( +

{userEmail}

+ ) : null} +
+
+ + +

+ The configured OpenRouter model is used for Zopu conversations and + project work. +

+
+ {onSignOut ? ( + + ) : null} +
+
+); diff --git a/packages/ui/src/components/mobile-workspace/mobile-work-chat-screen.tsx b/packages/ui/src/components/mobile-workspace/mobile-work-chat-screen.tsx new file mode 100644 index 0000000..528aae9 --- /dev/null +++ b/packages/ui/src/components/mobile-workspace/mobile-work-chat-screen.tsx @@ -0,0 +1,206 @@ +import { cn } from "@code/ui/lib/utils"; +import { ArrowLeft, ArrowUp, ExternalLink, Sparkles } from "lucide-react"; +import type { FormEvent } from "react"; + +import { + Conversation, + ConversationContent, + ConversationScrollButton, +} from "../ai-elements/conversation"; +import { + Message, + MessageContent, + MessageResponse, +} from "../ai-elements/message"; +import { + PromptInput, + PromptInputBody, + PromptInputSubmit, + PromptInputTextarea, +} from "../ai-elements/prompt-input"; +import type { MobileWorkspaceView } from "./models"; + +export interface MobileWorkChatScreenProps { + readonly composerValue: string; + readonly data: MobileWorkspaceView; + readonly onBack: () => void; + readonly onComposerChange: (value: string) => void; + readonly onComposerMessageSubmit?: (message: string) => Promise; + readonly onComposerSubmit: (event: FormEvent) => void; + readonly statusMessage?: string; +} + +export const MobileWorkChatScreen = ({ + composerValue, + data, + onBack, + onComposerChange, + onComposerMessageSubmit, + onComposerSubmit, + statusMessage, +}: MobileWorkChatScreenProps) => { + const workUnit = data.selectedWorkUnit; + + return ( +
+
+ +
+

+ {workUnit?.title ?? "Work chat"} +

+

+ {workUnit + ? `${workUnit.code} · ${workUnit.statusLabel}` + : "Loading work unit"} +

+
+ {workUnit?.reviewUrl ? ( + + + + ) : ( + + )} +
+
+ + + {workUnit ? ( +
+
+ {workUnit.statusLabel.toUpperCase()} + {workUnit.progress}% +
+

+ {workUnit.nextAction} +

+

+ {workUnit.summary} +

+ {workUnit.reviewUrl ? ( + + Review PR #{workUnit.pullRequestNumber} + + + ) : null} +
+ ) : null} + + +
+ + + + Zopu + + WORK PARTNER + +
+

+ This chat is scoped to {workUnit?.title ?? "this work unit"}. + Add context, change direction, or ask what happens next. +

+
+
+ {data.assistant.messages.map((message) => ( + + + {message.role === "assistant" ? ( + <> +
+ + + + Zopu +
+ + {message.text} + + + ) : ( + {message.text} + )} +
+
+ ))} + {data.assistant.isBusy ? ( +

Zopu is working…

+ ) : null} +
+ +
+
+
+ { + if (onComposerMessageSubmit) { + await onComposerMessageSubmit(message.text); + return; + } + onComposerChange(message.text); + }} + > + + onComposerChange(event.currentTarget.value)} + placeholder="Ask about this work unit..." + value={composerValue} + /> + + + + + +

+ {statusMessage ?? "Messages stay with this work unit"} +

+
+
+
+ ); +}; diff --git a/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx b/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx new file mode 100644 index 0000000..1e8c6ec --- /dev/null +++ b/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx @@ -0,0 +1,458 @@ +import { useSwipeCard } from "@code/ui/hooks/use-swipe-card"; +import { cn } from "@code/ui/lib/utils"; +import { + ArrowUp, + Check, + ChevronRight, + MoreHorizontal, + Plus, + RotateCcw, +} from "lucide-react"; +import type { FormEvent, MouseEvent } from "react"; + +import { MobileSettingsPanel } from "./mobile-settings-panel"; +import type { + MobileWorkspaceView, + MobileWorkUnitTone, + MobileWorkUnitView, +} from "./models"; + +const toneClass: Readonly> = { + blue: "bg-[#e8efff] text-[#14265f]", + green: "bg-[#e8f7ef] text-[#105c35]", + orange: "bg-[#fff0e8] text-[#8a2e13]", + purple: "bg-[#f1e8ff] text-[#3e0b74]", +}; + +const statusClass: Readonly> = { + blue: "bg-[#d5e1ff] text-[#315dc0]", + green: "bg-[#d8f1e4] text-[#238050]", + orange: "bg-[#ffe0d0] text-[#c53c08]", + purple: "bg-[#e6d6ff] text-[#7137ef]", +}; + +const dateLabel = new Intl.DateTimeFormat("en", { + day: "numeric", + weekday: "short", +}) + .format(new Date()) + .toUpperCase(); + +interface WorkStackHeaderProps { + readonly activeCount: number; + readonly onOpenSettings?: () => void; + readonly userInitials: string; +} + +const WorkStackHeader = ({ + activeCount, + onOpenSettings, + userInitials, +}: WorkStackHeaderProps) => ( +
+
+ Z +
+
+

+ Zopu +

+

+ + {activeCount} active work units +

+
+ + +
+); + +interface SwipeWorkCardProps { + readonly data: MobileWorkspaceView; + readonly onChecked: (workUnitId: string) => void; + readonly onOpen: (workUnitId: string) => void; + readonly onSendBack: (workUnitId: string) => void; + readonly workUnit: MobileWorkUnitView; +} + +const SwipeWorkCard = ({ + data, + onChecked, + onOpen, + onSendBack, + workUnit, +}: SwipeWorkCardProps) => { + const swipeCard = useSwipeCard({ + itemKey: workUnit.id, + onSwipe: (direction) => { + if (direction === "right") { + onChecked(workUnit.id); + return; + } + onSendBack(workUnit.id); + }, + }); + const { + backLabelRef, + cardRef, + checkedLabelRef, + dragged, + pointerHandlers, + swipe, + transitionHandlers, + } = swipeCard; + + const handleOpen = (event: MouseEvent) => { + if (dragged()) { + event.preventDefault(); + return; + } + onOpen(workUnit.id); + }; + + return ( +
+ + SEND TO BACK + + + CHECKED + +
+
+ + {workUnit.statusLabel.toUpperCase()} + + {workUnit.code} + +
+ +
+ +
+

NEXT ACTION

+

+ {workUnit.nextAction} +

+

+ {data.artifactCount} artifacts available +

+
+ +
+
+
+ ); +}; + +interface WorkStackComposerProps { + readonly onChange: (value: string) => void; + readonly onSubmit: (event: FormEvent) => void; + readonly statusMessage?: string; + readonly value: string; +} + +const WorkStackComposer = ({ + onChange, + onSubmit, + statusMessage, + value, +}: WorkStackComposerProps) => ( +