feat: consolidate agent execution stack

This commit is contained in:
-Puter
2026-07-31 18:26:41 +05:30
parent c644ec8d01
commit ce1f33aa69
68 changed files with 2196 additions and 3827 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ AGENT_WORKSPACE_ROOT=/absolute/path/to/zopu-agent-workspaces
# Flue persistence adapter # Flue persistence adapter
FLUE_DB_TOKEN=replace-with-a-long-random-token FLUE_DB_TOKEN=replace-with-a-long-random-token
FLUE_URL=http://localhost:3583 FLUE_URL=http://127.0.0.1:3585
# Agent model provider # Agent model provider
AGENT_MODEL_PROVIDER=xiaomi AGENT_MODEL_PROVIDER=xiaomi
+18 -70
View File
@@ -1,7 +1,6 @@
import { Button } from "@code/ui/components/button"; import { Button } from "@code/ui/components/button";
import { import {
AlertTriangle, AlertTriangle,
Check,
ChevronRight, ChevronRight,
FileCode2, FileCode2,
Hammer, Hammer,
@@ -32,16 +31,13 @@ const starterDefinition = (work: WorkRecord) => ({
outOfScope: ["Unrelated product changes"], outOfScope: ["Unrelated product changes"],
problem: work.objective, problem: work.objective,
questions: [], questions: [],
requiredArtifacts: ["Simulation activity and terminal outcome"], requiredArtifacts: ["Implementation diff and terminal outcome"],
risk: "medium", risk: "medium",
}); });
const starterDesign = (work: WorkRecord) => ({ const starterDesign = (work: WorkRecord) => ({
architectureSummary: architectureSummary: "Implement the requested outcome in focused changes.",
"Validate the approved Definition, then exercise one deterministic fake slice.", callFlowDelta: ["Work -> Run -> Attempt -> AgentOS -> candidate revision"],
callFlowDelta: [
"Work -> Run -> Attempt -> normalized events -> terminal outcome",
],
concerns: [], concerns: [],
evidenceRequirements: ["Terminal Run classification"], evidenceRequirements: ["Terminal Run classification"],
fileTreeDelta: [], fileTreeDelta: [],
@@ -49,30 +45,30 @@ const starterDesign = (work: WorkRecord) => ({
files: [], files: [],
modules: [], modules: [],
risks: [], risks: [],
summary: "Compact vertical-slice simulation", summary: "Focused implementation",
}, },
invariants: [ invariants: [
"Simulation never claims implementation", "A successful AgentOS response alone never marks Work complete",
"Every Attempt reaches a terminal classification", "Every Attempt reaches a terminal classification",
], ],
keyInterfaces: ["HarnessRuntime", "AttemptOutcome"], keyInterfaces: ["WorkExecution", "AttemptResult"],
slices: [ slices: [
{ {
codeBoundaries: ["workExecution"], codeBoundaries: ["workExecution"],
dependsOn: [], dependsOn: [],
evidenceRequirements: ["Normalized activity events"], evidenceRequirements: ["Normalized activity events"],
id: "deterministic-simulation", id: "implementation",
objective: work.objective, objective: work.objective,
observableBehavior: "A terminal fake Run is visible", observableBehavior: "Implementation changes are visible in the diff",
reviewRequired: false, reviewRequired: false,
title: "Deterministic simulation", title: "Implementation",
verification: ["Run completes with a terminal classification"], verification: ["Run completes with a terminal classification"],
}, },
], ],
tradeoffs: ["Fake runtime proves contract before sandbox integration"], tradeoffs: [],
}); });
// oxlint-disable-next-line complexity -- this card intentionally coordinates review actions and run evidence. // oxlint-disable-next-line complexity -- this card coordinates planning and run evidence.
export const WorkCard = ({ export const WorkCard = ({
onSourceSelect, onSourceSelect,
work, work,
@@ -95,7 +91,7 @@ export const WorkCard = ({
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-[10px] font-semibold uppercase text-[#65713a]"> <p className="text-[10px] font-semibold uppercase text-[#65713a]">
Proposed Work {work.status === "proposed" ? "Proposed Work" : "Work"}
</p> </p>
<h2 className="mt-1 text-[15px] font-semibold leading-5"> <h2 className="mt-1 text-[15px] font-semibold leading-5">
{work.title} {work.title}
@@ -149,7 +145,8 @@ export const WorkCard = ({
Outcome Outcome
</p> </p>
<p className="mt-1 leading-5 text-[#626057]">{work.objective}</p> <p className="mt-1 leading-5 text-[#626057]">{work.objective}</p>
<p className="mt-2 text-[#747168]"> <p className="mt-2 text-[#747168]">Status: {work.status}</p>
<p className="mt-1 text-[#747168]">
Risk: {work.definition?.risk ?? "not defined"} Risk: {work.definition?.risk ?? "not defined"}
</p> </p>
{openQuestions > 0 ? ( {openQuestions > 0 ? (
@@ -180,20 +177,6 @@ export const WorkCard = ({
<Hammer className="size-3.5" /> Save definition <Hammer className="size-3.5" /> Save definition
</Button> </Button>
) : null} ) : null}
{work.status === "awaiting-definition-approval" &&
work.definitionVersion ? (
<Button
size="sm"
onClick={() =>
void workspace.approveDefinition(
work._id,
work.definitionVersion as number
)
}
>
<Check className="size-3.5" /> Approve
</Button>
) : null}
</div> </div>
</section> </section>
<section> <section>
@@ -220,22 +203,6 @@ export const WorkCard = ({
<Hammer className="size-3.5" /> Save design <Hammer className="size-3.5" /> Save design
</Button> </Button>
) : null} ) : null}
{work.status === "awaiting-design-approval" &&
work.definitionApprovalVersion &&
work.designVersion ? (
<Button
size="sm"
onClick={() =>
void workspace.approveDesign(
work._id,
work.definitionApprovalVersion as number,
work.designVersion as number
)
}
>
<Check className="size-3.5" /> Approve design
</Button>
) : null}
</div> </div>
</section> </section>
<section> <section>
@@ -261,10 +228,7 @@ export const WorkCard = ({
{latestRun ? ( {latestRun ? (
<div className="mt-1 space-y-2 text-[#626057]"> <div className="mt-1 space-y-2 text-[#626057]">
<p className="leading-5"> <p className="leading-5">
{latestRun.executionKind === "real" Run {latestRun.status}:{" "}
? "AgentOS"
: "Simulation"}{" "}
run {latestRun.status}:{" "}
{latestRun.terminalSummary ?? {latestRun.terminalSummary ??
latestRun.terminalClassification ?? latestRun.terminalClassification ??
"activity is still arriving"} "activity is still arriving"}
@@ -343,7 +307,6 @@ export const WorkCard = ({
)} )}
<div className="mt-2 flex flex-wrap gap-2"> <div className="mt-2 flex flex-wrap gap-2">
{work.status === "ready" ? ( {work.status === "ready" ? (
<>
<Button <Button
size="sm" size="sm"
disabled={!workspace.projectGitConnection} disabled={!workspace.projectGitConnection}
@@ -351,37 +314,22 @@ export const WorkCard = ({
> >
<Play className="size-3.5" /> Run <Play className="size-3.5" /> Run
</Button> </Button>
<Button
size="sm"
variant="outline"
onClick={() =>
void workspace.startSimulation(work._id, "success")
}
>
<Play className="size-3.5" /> Simulate
</Button>
</>
) : null} ) : null}
{latestRun?.status === "running" ? ( {latestRun?.status === "running" ? (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => onClick={() => void workspace.cancelExecution(latestRun._id)}
void (latestRun.executionKind === "real"
? workspace.cancelExecution(latestRun._id)
: workspace.cancelSimulation(latestRun._id))
}
> >
<X className="size-3.5" /> Cancel <X className="size-3.5" /> Cancel
</Button> </Button>
) : null} ) : null}
{latestRun?.executionKind !== "real" && {latestRun?.status === "terminal" &&
latestRun?.status === "terminal" &&
latestRun.terminalClassification === "RetryableFailure" ? ( latestRun.terminalClassification === "RetryableFailure" ? (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => void workspace.retrySimulation(latestRun._id)} onClick={() => void workspace.retryExecution(latestRun._id)}
> >
<RotateCcw className="size-3.5" /> Retry <RotateCcw className="size-3.5" /> Retry
</Button> </Button>
@@ -13,13 +13,6 @@ import type { WorkRecord, WorkspaceState } from "@/lib/workspace/types";
const toError = (error: unknown) => const toError = (error: unknown) =>
error instanceof Error ? error : new Error(String(error)); error instanceof Error ? error : new Error(String(error));
type ExecutionScenario =
| "success"
| "transient-failure-then-success"
| "needs-input"
| "permanent-failure"
| "cancelled";
const workListRef = makeFunctionReference< const workListRef = makeFunctionReference<
"query", "query",
{ projectId: Id<"projects"> }, { projectId: Id<"projects"> },
@@ -35,46 +28,26 @@ const saveDefinitionRef = makeFunctionReference<
{ workId: Id<"works">; payloadJson: string }, { workId: Id<"works">; payloadJson: string },
unknown unknown
>("workPlanning:saveDefinitionProposal"); >("workPlanning:saveDefinitionProposal");
const approveDefinitionRef = makeFunctionReference<
"mutation",
{ workId: Id<"works">; version: number },
unknown
>("workPlanning:approveDefinition");
const saveDesignRef = makeFunctionReference< const saveDesignRef = makeFunctionReference<
"mutation", "mutation",
{ workId: Id<"works">; payloadJson: string }, { workId: Id<"works">; payloadJson: string },
unknown unknown
>("workPlanning:saveDesignProposal"); >("workPlanning:saveDesignProposal");
const approveDesignRef = makeFunctionReference<
"mutation",
{ workId: Id<"works">; definitionVersion: number; designVersion: number },
unknown
>("workPlanning:approveDesign");
const startSimulationRef = makeFunctionReference<
"mutation",
{ workId: Id<"works">; scenario: ExecutionScenario; sliceId?: string },
unknown
>("workExecution:startSimulatedExecution");
const cancelSimulationRef = makeFunctionReference<
"mutation",
{ runId: Id<"workRuns"> },
unknown
>("workExecution:cancelSimulatedExecution");
const retrySimulationRef = makeFunctionReference<
"mutation",
{ runId: Id<"workRuns"> },
unknown
>("workExecution:retrySimulatedExecution");
const startExecutionRef = makeFunctionReference< const startExecutionRef = makeFunctionReference<
"mutation", "mutation",
{ workId: Id<"works">; sliceId?: string }, { sliceId?: string; workId: Id<"works"> },
unknown unknown
>("workExecutionWorkflow:startExecution"); >("workExecution:start");
const cancelExecutionRef = makeFunctionReference< const cancelExecutionRef = makeFunctionReference<
"mutation", "mutation",
{ runId: Id<"workRuns"> }, { runId: Id<"workRuns"> },
unknown unknown
>("workExecutionWorkflow:cancelExecution"); >("workExecution:cancel");
const retryExecutionRef = makeFunctionReference<
"mutation",
{ runId: Id<"workRuns"> },
unknown
>("workExecution:retry");
const listGitConnectionsRef = makeFunctionReference< const listGitConnectionsRef = makeFunctionReference<
"query", "query",
Record<string, never>, Record<string, never>,
@@ -153,14 +126,10 @@ export const useProjectWorkspace = (): WorkspaceState => {
); );
const requestDefinitionMutation = useMutation(requestDefinitionRef); const requestDefinitionMutation = useMutation(requestDefinitionRef);
const saveDefinitionMutation = useMutation(saveDefinitionRef); const saveDefinitionMutation = useMutation(saveDefinitionRef);
const approveDefinitionMutation = useMutation(approveDefinitionRef);
const saveDesignMutation = useMutation(saveDesignRef); const saveDesignMutation = useMutation(saveDesignRef);
const approveDesignMutation = useMutation(approveDesignRef);
const startSimulationMutation = useMutation(startSimulationRef);
const cancelSimulationMutation = useMutation(cancelSimulationRef);
const retrySimulationMutation = useMutation(retrySimulationRef);
const startExecutionMutation = useMutation(startExecutionRef); const startExecutionMutation = useMutation(startExecutionRef);
const cancelExecutionMutation = useMutation(cancelExecutionRef); const cancelExecutionMutation = useMutation(cancelExecutionRef);
const retryExecutionMutation = useMutation(retryExecutionRef);
const attachGitConnectionMutation = useMutation(attachGitConnectionRef); const attachGitConnectionMutation = useMutation(attachGitConnectionRef);
const connectGiteaAction = useAction(connectGiteaRef); const connectGiteaAction = useAction(connectGiteaRef);
const connectGithubAction = useAction(connectGithubRef); const connectGithubAction = useAction(connectGithubRef);
@@ -224,13 +193,6 @@ export const useProjectWorkspace = (): WorkspaceState => {
return { return {
agent, agent,
approveDefinition: (workId: Id<"works">, version: number) =>
approveDefinitionMutation({ version, workId }),
approveDesign: (
workId: Id<"works">,
definitionVersion: number,
designVersion: number
) => approveDesignMutation({ definitionVersion, designVersion, workId }),
authorizeGithub: () => authorizeGithub: () =>
authClient.linkSocial({ authClient.linkSocial({
callbackURL: `${window.location.origin}/projects?resume=github`, callbackURL: `${window.location.origin}/projects?resume=github`,
@@ -238,8 +200,6 @@ export const useProjectWorkspace = (): WorkspaceState => {
}), }),
cancelExecution: (runId: Id<"workRuns">) => cancelExecution: (runId: Id<"workRuns">) =>
runOperation(() => cancelExecutionMutation({ runId })), runOperation(() => cancelExecutionMutation({ runId })),
cancelSimulation: (runId: Id<"workRuns">) =>
runOperation(() => cancelSimulationMutation({ runId })),
clearOperationError: () => setOperationError(undefined), clearOperationError: () => setOperationError(undefined),
connectGitea, connectGitea,
connectLinkedGithub, connectLinkedGithub,
@@ -253,8 +213,8 @@ export const useProjectWorkspace = (): WorkspaceState => {
repository, repository,
requestDefinition: (workId: Id<"works">) => requestDefinition: (workId: Id<"works">) =>
requestDefinitionMutation({ workId }), requestDefinitionMutation({ workId }),
retrySimulation: (runId: Id<"workRuns">) => retryExecution: (runId: Id<"workRuns">) =>
runOperation(() => retrySimulationMutation({ runId })), runOperation(() => retryExecutionMutation({ runId })),
saveDefinition: (workId: Id<"works">, payload: unknown) => saveDefinition: (workId: Id<"works">, payload: unknown) =>
saveDefinitionMutation({ payloadJson: JSON.stringify(payload), workId }), saveDefinitionMutation({ payloadJson: JSON.stringify(payload), workId }),
saveDesign: (workId: Id<"works">, payload: unknown) => saveDesign: (workId: Id<"works">, payload: unknown) =>
@@ -267,14 +227,6 @@ export const useProjectWorkspace = (): WorkspaceState => {
setRepository, setRepository,
startExecution: (workId: Id<"works">, sliceId?: string) => startExecution: (workId: Id<"works">, sliceId?: string) =>
runOperation(() => startExecutionMutation({ sliceId, workId })), runOperation(() => startExecutionMutation({ sliceId, workId })),
startSimulation: (
workId: Id<"works">,
scenario: ExecutionScenario,
sliceId?: string
) =>
runOperation(() =>
startSimulationMutation({ scenario, sliceId, workId })
),
works, works,
}; };
}; };
+1 -23
View File
@@ -9,7 +9,6 @@ export interface WorkRecord {
readonly objective: string; readonly objective: string;
readonly status: string; readonly status: string;
readonly definitionVersion?: number; readonly definitionVersion?: number;
readonly definitionApprovalVersion?: number;
readonly designVersion?: number; readonly designVersion?: number;
readonly signals: readonly { readonly signals: readonly {
readonly sources: readonly { messageId: string; rawText: string }[]; readonly sources: readonly { messageId: string; rawText: string }[];
@@ -34,7 +33,6 @@ export interface WorkRun {
readonly attemptEvents?: readonly WorkAttemptEvent[]; readonly attemptEvents?: readonly WorkAttemptEvent[];
readonly baseRevision?: string; readonly baseRevision?: string;
readonly candidateRevision?: string; readonly candidateRevision?: string;
readonly executionKind?: string;
readonly _id: Id<"workRuns">; readonly _id: Id<"workRuns">;
readonly status: string; readonly status: string;
readonly terminalClassification?: string; readonly terminalClassification?: string;
@@ -81,18 +79,8 @@ export interface WorkspaceState {
| "streaming" | "streaming"
| "submitted"; | "submitted";
}; };
readonly approveDefinition: (
workId: Id<"works">,
version: number
) => Promise<unknown>;
readonly approveDesign: (
workId: Id<"works">,
definitionVersion: number,
designVersion: number
) => Promise<unknown>;
readonly authorizeGithub: () => Promise<unknown>; readonly authorizeGithub: () => Promise<unknown>;
readonly cancelExecution: (runId: Id<"workRuns">) => Promise<unknown>; readonly cancelExecution: (runId: Id<"workRuns">) => Promise<unknown>;
readonly cancelSimulation: (runId: Id<"workRuns">) => Promise<unknown>;
readonly clearOperationError: () => void; readonly clearOperationError: () => void;
readonly connectGitea: (input: { readonly connectGitea: (input: {
readonly token: string; readonly token: string;
@@ -108,7 +96,7 @@ export interface WorkspaceState {
readonly projects: readonly ProjectListItem[] | undefined; readonly projects: readonly ProjectListItem[] | undefined;
readonly repository: string; readonly repository: string;
readonly requestDefinition: (workId: Id<"works">) => Promise<unknown>; readonly requestDefinition: (workId: Id<"works">) => Promise<unknown>;
readonly retrySimulation: (runId: Id<"workRuns">) => Promise<unknown>; readonly retryExecution: (runId: Id<"workRuns">) => Promise<unknown>;
readonly saveDefinition: ( readonly saveDefinition: (
workId: Id<"works">, workId: Id<"works">,
payload: unknown payload: unknown
@@ -124,16 +112,6 @@ export interface WorkspaceState {
workId: Id<"works">, workId: Id<"works">,
sliceId?: string sliceId?: string
) => Promise<unknown>; ) => Promise<unknown>;
readonly startSimulation: (
workId: Id<"works">,
scenario:
| "success"
| "transient-failure-then-success"
| "needs-input"
| "permanent-failure"
| "cancelled",
sliceId?: string
) => Promise<unknown>;
readonly works: readonly WorkRecord[] | undefined; readonly works: readonly WorkRecord[] | undefined;
} }
+28 -11
View File
@@ -148,17 +148,32 @@ bun run dev:server
This watches and publishes functions to the configured Convex cloud development deployment. This watches and publishes functions to the configured Convex cloud development deployment.
### 2. Start Rivet Engine ### 2. Start the whole stack
The installed RivetKit 2.3.9 package supplies the platform-specific `rivet-engine` binary. Start it with: The root `dev` script starts Rivet Engine, the AgentOS registry runner, Convex, Flue agents, and the web app in one terminal:
```bash ```bash
./node_modules/.pnpm/@rivetkit+engine-cli-*/node_modules/@rivetkit/engine-cli-*/rivet-engine start pnpm dev
```
For phone or Tailscale-device access, start the stack with the web server bound to all interfaces:
```bash
pnpm dev:tailscale
```
### 3. Start Rivet Engine
If you prefer separate terminals, start the engine from the agents package:
```bash
cd packages/agents
bun run dev:engine
``` ```
There must be exactly one local engine listening on `127.0.0.1:6420`. Do not start a second engine, and do not use `npx rivetkit dev`; that command is unavailable in the installed version. There must be exactly one local engine listening on `127.0.0.1:6420`. Do not start a second engine, and do not use `npx rivetkit dev`; that command is unavailable in the installed version.
### 3. Start the AgentOS registry runner ### 4. Start the AgentOS registry runner
```bash ```bash
cd packages/agents cd packages/agents
@@ -167,21 +182,21 @@ bun run runner
The runner registers the AgentOS workspace actor with Rivet Engine. Keep it running whenever a coding attempt or issue-to-PR request may execute. The runner registers the AgentOS workspace actor with Rivet Engine. Keep it running whenever a coding attempt or issue-to-PR request may execute.
### 4. Start Flue agents ### 5. Start Flue agents
For laptop-only access: For laptop-only access:
```bash ```bash
bun run dev:agents -- --port 3585 pnpm dev:agents
``` ```
For Convex callbacks or access from another device, bind Flue to all interfaces: For Convex callbacks or access from another device, bind Flue to all interfaces:
```bash ```bash
bun run dev:tailscale:agents -- --port 3585 pnpm dev:tailscale:agents
``` ```
Always pass port `3585`; the Flue CLI defaults to `3583`, but the current Zopu configuration and Convex environment use `3585`. The dev scripts already use port `3585`; the Flue CLI defaults to `3583`, but the current Zopu configuration and Convex environment use `3585`.
If the shell already exports remote Rivet values, shell values override `.env`. Start Flue with explicit local values: If the shell already exports remote Rivet values, shell values override `.env`. Start Flue with explicit local values:
@@ -191,18 +206,20 @@ RIVET_PUBLIC_ENDPOINT=http://127.0.0.1:6420 \
bun run dev:tailscale:agents -- --port 3585 bun run dev:tailscale:agents -- --port 3585
``` ```
### 5. Start the web application ### 6. Start the web application
If you are not using the root `dev` script, start the web app separately.
For laptop-only access: For laptop-only access:
```bash ```bash
bun run dev:web pnpm dev:web
``` ```
For phone or other Tailscale-device access: For phone or other Tailscale-device access:
```bash ```bash
bun run dev:tailscale:web pnpm dev:tailscale:web
``` ```
Open `http://localhost:5173`, or `http://<tailscale-ip>:5173` when using the Tailscale command. Open `http://localhost:5173`, or `http://<tailscale-ip>:5173` when using the Tailscale command.
+8 -8
View File
@@ -5,7 +5,7 @@
> **Status:** Working source of truth > **Status:** Working source of truth
> **Audience:** product, design, engineering, agents > **Audience:** product, design, engineering, agents
> **Normative words:** **MUST** = required, **SHOULD** = default, **MAY** = optional > **Normative words:** **MUST** = required, **SHOULD** = default, **MAY** = optional
> **Product boundary:** this file defines *what and why*, not implementation details. > **Product boundary:** this file defines _what and why_, not implementation details.
## 1. Thesis ## 1. Thesis
@@ -35,7 +35,7 @@ Technical founders and small engineering teams working in one Git repository.
```text ```text
Project chat Project chat
→ actionable Signal → actionable Signal
→ approved Work Definition → ready Work Definition
→ lightweight Design Packet → lightweight Design Packet
→ vertical slices → vertical slices
→ isolated coding run → isolated coding run
@@ -90,7 +90,7 @@ A Work card is the durable, inspectable representation of one desired outcome. I
All decisions requiring a human are first-class objects: All decisions requiring a human are first-class objects:
- clarify intent; - clarify intent;
- approve definition/design; - definition/design persist automatically;
- grant permission; - grant permission;
- select an alternative; - select an alternative;
- review a slice; - review a slice;
@@ -250,7 +250,7 @@ Canonical knowledge updates require review.
High-quality software is evaluated across: High-quality software is evaluated across:
| Dimension | Required question | | Dimension | Required question |
|---|---| | ----------------- | -------------------------------------------------------- |
| Correctness | Does behavior satisfy the accepted criteria? | | Correctness | Does behavior satisfy the accepted criteria? |
| Regression safety | Did existing behavior remain valid? | | Regression safety | Did existing behavior remain valid? |
| Maintainability | Does the change preserve understandable boundaries? | | Maintainability | Does the change preserve understandable boundaries? |
@@ -283,7 +283,7 @@ The implementation worker MUST NOT be the sole authority on completion.
Builder → candidate output Builder → candidate output
Verifier → independent evidence Verifier → independent evidence
Resolver → completion decision Resolver → completion decision
Human → policy-defined approvals Human → merge/release authorization
``` ```
### Risk lanes ### Risk lanes
@@ -309,7 +309,7 @@ definition → combined design → 1–3 slices → verify → review → PR
Auth, billing, permissions, migrations, shared infrastructure, destructive operations. Auth, billing, permissions, migrations, shared infrastructure, destructive operations.
```text ```text
definition approval → architecture approval → program-design approval definition → design → ready → merge/release
→ one slice at a time → staged release → observation/rollback gate → one slice at a time → staged release → observation/rollback gate
``` ```
@@ -356,8 +356,8 @@ Humans should spend attention on high-leverage decisions, not supervise tool cal
Preferred gates: Preferred gates:
- Work Definition approval; - Work Definition (no approval gate);
- Design Packet approval for standard/critical work; - Design Packet (no approval gate);
- slice review when risk requires it; - slice review when risk requires it;
- merge/release approval; - merge/release approval;
- resolution of ambiguity or policy exceptions. - resolution of ambiguity or policy exceptions.
+5 -7
View File
@@ -198,9 +198,7 @@ All external/model payloads MUST be decoded with schemas at boundaries.
```text ```text
Proposed Proposed
→ Defining → Defining
→ AwaitingDefinitionApproval
→ Designing → Designing
→ AwaitingDesignApproval
→ Ready → Ready
→ ExecutingSlice → ExecutingSlice
→ VerifyingSlice → VerifyingSlice
@@ -242,7 +240,7 @@ Initial central aggregate:
- Design Packet and versions; - Design Packet and versions;
- slice/step graph; - slice/step graph;
- Resolver state; - Resolver state;
- approvals/questions; - questions;
- artifact references; - artifact references;
- result. - result.
@@ -425,7 +423,7 @@ HarnessRuntime
v0 selects one harness. Prefer mature coding harnesses for repository exploration/edit/test loops; use custom FLUE agents for product-specific reasoning. v0 selects one harness. Prefer mature coding harnesses for repository exploration/edit/test loops; use custom FLUE agents for product-specific reasoning.
Zopu owns Work, branches, worktrees, budgets, approvals, artifacts, and completion. Harness owns one bounded implementation loop. Zopu owns Work, branches, worktrees, budgets, artifacts, and completion. Harness owns one bounded implementation loop.
## 10. Runtime strategy ## 10. Runtime strategy
@@ -580,7 +578,7 @@ Never create a “verified PR” from a different SHA than the verified candidat
Review package: Review package:
- original intent; - original intent;
- approved definition/design; - current definition/design;
- slice narrative; - slice narrative;
- meaningful diffs; - meaningful diffs;
- screenshots/video; - screenshots/video;
@@ -608,7 +606,7 @@ Production release requires explicit policy, rollout plan, health signals, and r
- no long-lived model/Git secrets in workspace files; - no long-lived model/Git secrets in workspace files;
- network egress policy; - network egress policy;
- artifact access authorization; - artifact access authorization;
- immutable audit trail for approvals; - immutable audit trail for all Work events;
- tool allowlist per Kit; - tool allowlist per Kit;
- destructive tools denied by default; - destructive tools denied by default;
- merge/deploy human-gated initially; - merge/deploy human-gated initially;
@@ -663,7 +661,7 @@ Keep Cube control APIs private; expose only authorized preview paths. Rivet and
The architecture is proven when one real repository supports: The architecture is proven when one real repository supports:
```text ```text
message → Signal → approved Work → approved Design Packet message → Signal → proposed Work → ready Work
→ one slice → isolated harness run → independent verification → one slice → isolated harness run → independent verification
→ verified commit → real PR → human response/resume → verified commit → real PR → human response/resume
``` ```
+12 -8
View File
@@ -50,7 +50,8 @@
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vp run -r dev", "dev": "concurrently --names engine,runner,server,agents,web --prefix-colors yellow,blue,magenta,green,cyan --handle-input --kill-others-on-fail \"pnpm run dev:engine\" \"pnpm run dev:runner\" \"pnpm run dev:server\" \"pnpm run dev:agents\" \"pnpm run dev:web\"",
"dev:tailscale": "concurrently --names engine,runner,server,agents,web --prefix-colors yellow,blue,magenta,green,cyan --handle-input --kill-others-on-fail \"pnpm run dev:engine\" \"pnpm run dev:runner\" \"pnpm run dev:server\" \"pnpm run dev:agents\" \"pnpm run dev:tailscale:web\"",
"build": "vp run -r build", "build": "vp run -r build",
"check-types": "vp run -r check-types", "check-types": "vp run -r check-types",
"check": "ultracite check package.json vite.config.ts apps/web/package.json apps/web/src/root.tsx apps/web/src/index.css apps/web/src/components/chat apps/web/src/components/workspace apps/web/src/hooks/chat apps/web/src/hooks/workspace apps/web/src/lib/chat apps/web/src/lib/workspace apps/web/src/routes.ts apps/web/src/routes/app/dashboard/page.tsx apps/web/src/routes/app/workspace/page.tsx packages/agents/package.json packages/agents/src/agents/zopu.ts packages/agents/src/app.ts packages/agents/src/auth.ts packages/agents/src/tools/slice-one.ts packages/backend/package.json packages/backend/convex/conversationMessages.ts packages/backend/convex/conversationMessages.test.ts packages/backend/convex/fluePersistence.test.ts packages/backend/convex/projects.ts", "check": "ultracite check package.json vite.config.ts apps/web/package.json apps/web/src/root.tsx apps/web/src/index.css apps/web/src/components/chat apps/web/src/components/workspace apps/web/src/hooks/chat apps/web/src/hooks/workspace apps/web/src/lib/chat apps/web/src/lib/workspace apps/web/src/routes.ts apps/web/src/routes/app/dashboard/page.tsx apps/web/src/routes/app/workspace/page.tsx packages/agents/package.json packages/agents/src/agents/zopu.ts packages/agents/src/app.ts packages/agents/src/auth.ts packages/agents/src/tools/slice-one.ts packages/backend/package.json packages/backend/convex/conversationMessages.ts packages/backend/convex/conversationMessages.test.ts packages/backend/convex/fluePersistence.test.ts packages/backend/convex/projects.ts",
@@ -58,12 +59,14 @@
"format": "vp fmt", "format": "vp fmt",
"staged": "vp staged", "staged": "vp staged",
"hooks:setup": "vp config", "hooks:setup": "vp config",
"dev:web": "vp run --filter web dev", "dev:web": "pnpm --filter web run dev",
"dev:agents": "vp run --filter @code/agents dev", "dev:agents": "pnpm --filter @code/agents run dev",
"dev:tailscale:web": "vp run --filter web dev:tailscale", "dev:tailscale:web": "pnpm --filter web run dev:tailscale",
"dev:tailscale:agents": "vp run --filter @code/agents dev:tailscale", "dev:tailscale:agents": "pnpm --filter @code/agents run dev:tailscale",
"dev:server": "vp run --filter @code/backend dev", "dev:server": "pnpm --filter @code/backend run dev",
"dev:setup": "vp run --filter @code/backend dev:setup", "dev:setup": "pnpm --filter @code/backend run dev:setup",
"dev:engine": "pnpm --filter @code/agents run dev:engine",
"dev:runner": "node scripts/wait-for-port.mjs 127.0.0.1 6420 && pnpm --filter @code/agents run runner",
"build:agents": "vp run --filter @code/agents build", "build:agents": "vp run --filter @code/agents build",
"docs:update": "node scripts/update-docs.ts", "docs:update": "node scripts/update-docs.ts",
"subtree": "node scripts/subtree.ts", "subtree": "node scripts/subtree.ts",
@@ -74,9 +77,10 @@
"@code/config": "workspace:*", "@code/config": "workspace:*",
"@code/env": "workspace:*", "@code/env": "workspace:*",
"@code/primitives": "workspace:*", "@code/primitives": "workspace:*",
"@flue/sdk": "1.0.0-beta.9", "@flue/sdk": "npm:@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"concurrently": "^9.1.2",
"convex": "catalog:", "convex": "catalog:",
"convex-test": "catalog:", "convex-test": "catalog:",
"effect": "catalog:", "effect": "catalog:",
+8 -11
View File
@@ -7,29 +7,26 @@
"build": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs build", "build": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs build",
"start": "node --env-file=../../.env dist/server.mjs", "start": "node --env-file=../../.env dist/server.mjs",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"dev": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs dev", "dev": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs dev --port 3585",
"dev:tailscale": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs dev", "dev:engine": "node --env-file=../../.env scripts/start-engine.mjs",
"run": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run", "run": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run",
"runner": "bun --env-file=../../.env src/runner.ts", "runner": "bun --env-file=../../.env src/runner.ts",
"run:zopu": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run zopu", "run:zopu": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run zopu"
"run:work-planner": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run work-planner"
}, },
"dependencies": { "dependencies": {
"@agentos-software/git": "0.3.3",
"@code/backend": "workspace:*",
"@code/env": "workspace:*", "@code/env": "workspace:*",
"@code/primitives": "workspace:*", "@code/primitives": "workspace:*",
"@flue/runtime": "latest", "@flue/runtime": "npm:@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2",
"@rivet-dev/agentos": "0.2.14", "@rivet-dev/agentos": "catalog:",
"@rivet-dev/agentos-core": "0.2.14", "@rivet-dev/agentos-core": "catalog:",
"convex": "catalog:", "convex": "catalog:",
"hono": "catalog:", "hono": "catalog:",
"rivetkit": "2.3.9",
"valibot": "catalog:" "valibot": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@code/config": "workspace:*", "@code/config": "workspace:*",
"@flue/cli": "latest", "@flue/cli": "npm:@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2",
"@rivetkit/engine-cli": "2.3.9",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
+13
View File
@@ -0,0 +1,13 @@
import { spawn } from "node:child_process";
import { getEnginePath } from "@rivetkit/engine-cli";
const enginePath = getEnginePath();
const proc = spawn(enginePath, ["start"], { stdio: "inherit" });
process.on("SIGINT", () => proc.kill("SIGINT"));
process.on("SIGTERM", () => proc.kill("SIGTERM"));
proc.on("exit", (code) => {
process.exit(code ?? 0);
});
@@ -0,0 +1,18 @@
import { parseAgentEnv } from "@code/env/agent";
import { ConvexHttpClient } from "convex/browser";
export interface ConvexServiceClient {
readonly client: ConvexHttpClient;
readonly token: string;
}
/** Shared factory for service-authenticated ConvexHttpClient used by agent tools. */
export const createConvexServiceClient = (
runtimeEnv: Record<string, string | undefined>
): ConvexServiceClient => {
const env = parseAgentEnv(runtimeEnv);
return {
client: new ConvexHttpClient(env.CONVEX_URL),
token: env.FLUE_DB_TOKEN,
};
};
@@ -8,11 +8,9 @@
* build typed references against explicit DTO shapes, so the file typechecks * build typed references against explicit DTO shapes, so the file typechecks
* against the installed runtime package without depending on codegen output. * against the installed runtime package without depending on codegen output.
* *
* DTO conventions shared with the backend: * - Submissions persist a shared input payload plus `chunksJson` for
* - Submissions persist `inputJson` (the dispatch input or the direct input * attachments stripped by `prepareSubmissionAttachments` and restored by
* with image data stripped via `prepareDirectSubmission`) plus `chunksJson` * `hydratePersistedSubmissionAttachments`.
* for direct submissions (a `PersistedImageChunk[]`). Direct inputs are
* rehydrated with `hydratePersistedDirectSubmission`.
* - Conversation batches persist a numeric `offset` and `recordsJson`. * - Conversation batches persist a numeric `offset` and `recordsJson`.
* - Event entries persist a numeric `offset` and `dataJson`. * - Event entries persist a numeric `offset` and `dataJson`.
* - Attachments carry `attachmentJson` and raw `bytes` as `ArrayBuffer`. * - Attachments carry `attachmentJson` and raw `bytes` as `ArrayBuffer`.
@@ -36,13 +34,14 @@ import {
assertSupportedFlueSchemaVersion, assertSupportedFlueSchemaVersion,
clampLimit, clampLimit,
copyAttachmentBytes, copyAttachmentBytes,
createDispatchAgentSubmissionInput,
createSessionStorageKey, createSessionStorageKey,
decodeRunCursor, decodeRunCursor,
encodeRunCursor, encodeRunCursor,
formatOffset, formatOffset,
hydratePersistedDirectSubmission,
parseOffset, parseOffset,
prepareDirectSubmission, hydratePersistedSubmissionAttachments,
prepareSubmissionAttachments,
verifyAttachmentBytes, verifyAttachmentBytes,
} from "@flue/runtime/adapter"; } from "@flue/runtime/adapter";
import type { import type {
@@ -61,9 +60,8 @@ import type {
ConversationStreamMeta, ConversationStreamMeta,
ConversationStreamReadResult, ConversationStreamReadResult,
ConversationStreamStore, ConversationStreamStore,
AgentSubmissionInput,
CreateRunInput, CreateRunInput,
DirectAgentSubmissionInput,
DispatchAgentSubmissionInput,
DispatchInput, DispatchInput,
EndRunInput, EndRunInput,
EventStreamMeta, EventStreamMeta,
@@ -113,7 +111,7 @@ interface TokenArgs {
const TOKEN = (): string => env.FLUE_DB_TOKEN; const TOKEN = (): string => env.FLUE_DB_TOKEN;
/** Trace carrier shape (W3C tracecontext). Not exported by Flue's adapter. */ /** Trace carrier shape (W3C tracecontext). Not exported by Flue's adapter. */
type TraceCarrier = NonNullable<DirectAgentSubmissionInput["traceCarrier"]>; type TraceCarrier = NonNullable<AgentSubmissionInput["traceCarrier"]>;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared DTO shapes (wire types returned by the Convex backend) // Shared DTO shapes (wire types returned by the Convex backend)
@@ -651,9 +649,7 @@ const attachmentRefToWire = (ref: AttachmentRef): AttachmentRefWire => ({
/** /**
* Rehydrate a {@link SubmissionRow} into the {@link AgentSubmission} contract. * Rehydrate a {@link SubmissionRow} into the {@link AgentSubmission} contract.
* `dispatch` inputs round-trip through `inputJson`; `direct` inputs are split * Submission attachments are stored separately from the shared persisted payload.
* into `inputJson` (images stripped) + `chunksJson` and rehydrated with Flue's
* `hydratePersistedDirectSubmission`.
*/ */
const hydrateSubmission = (row: SubmissionRow): AgentSubmission => { const hydrateSubmission = (row: SubmissionRow): AgentSubmission => {
const baseSubmission: Omit<AgentSubmission, "input"> = { const baseSubmission: Omit<AgentSubmission, "input"> = {
@@ -682,29 +678,15 @@ const hydrateSubmission = (row: SubmissionRow): AgentSubmission => {
...(row.error === null ? {} : { error: row.error }), ...(row.error === null ? {} : { error: row.error }),
...(row.ownerId === null ? {} : { ownerId: row.ownerId }), ...(row.ownerId === null ? {} : { ownerId: row.ownerId }),
}; };
const persisted = safeJsonParse<AgentSubmissionInput>(row.inputJson);
const traceCarrier = safeJsonParse<TraceCarrier>(row.traceCarrierJson); if (persisted === undefined) {
if (row.kind === "dispatch") {
const input = safeJsonParse<DispatchAgentSubmissionInput>(row.inputJson);
if (input === undefined) {
throw new Error( throw new Error(
`[flue] persisted dispatch submission ${row.submissionId} has empty inputJson` `[flue] persisted submission ${row.submissionId} has empty inputJson`
);
}
const inputWithCarrier =
traceCarrier === undefined ? input : { ...input, traceCarrier };
return { ...baseSubmission, input: inputWithCarrier };
}
const stripped = safeJsonParse<DirectAgentSubmissionInput>(row.inputJson);
if (stripped === undefined) {
throw new Error(
`[flue] persisted direct submission ${row.submissionId} has empty inputJson`
); );
} }
const chunks = safeJsonParse<PersistedChunkRow[]>(row.chunksJson) ?? []; const chunks = safeJsonParse<PersistedChunkRow[]>(row.chunksJson) ?? [];
const hydrated = hydratePersistedDirectSubmission(stripped, chunks); const hydrated = hydratePersistedSubmissionAttachments(persisted, chunks);
const traceCarrier = safeJsonParse<TraceCarrier>(row.traceCarrierJson);
const input = const input =
traceCarrier === undefined ? hydrated : { ...hydrated, traceCarrier }; traceCarrier === undefined ? hydrated : { ...hydrated, traceCarrier };
return { ...baseSubmission, input }; return { ...baseSubmission, input };
@@ -849,25 +831,14 @@ class ConvexAgentSubmissionStore implements AgentSubmissionStore {
} }
async admitDispatch(input: DispatchInput): Promise<AgentDispatchAdmission> { async admitDispatch(input: DispatchInput): Promise<AgentDispatchAdmission> {
const submissionInput = createDispatchAgentSubmissionInput(input);
const sessionKey = createSessionStorageKey(input.id, "default", "default"); const sessionKey = createSessionStorageKey(input.id, "default", "default");
const submissionInput: Omit< const extracted = prepareSubmissionAttachments(submissionInput);
DispatchAgentSubmissionInput,
"kind" | "submissionId"
> = {
acceptedAt: input.acceptedAt,
agent: input.agent,
dispatchId: input.dispatchId,
id: input.id,
input: input.input,
};
const res = await this.convex.mutation(admitSubmission, { const res = await this.convex.mutation(admitSubmission, {
input: { input: {
acceptedAt: parseAcceptedAt(input.acceptedAt, "admitDispatch"), acceptedAt: parseAcceptedAt(input.acceptedAt, "admitDispatch"),
inputJson: jsonStringify({ chunksJson: jsonStringify(extracted.chunks),
kind: "dispatch", inputJson: jsonStringify(extracted.value),
submissionId: input.dispatchId,
...submissionInput,
}),
kind: "dispatch", kind: "dispatch",
sessionKey, sessionKey,
submissionId: input.dispatchId, submissionId: input.dispatchId,
@@ -886,11 +857,9 @@ class ConvexAgentSubmissionStore implements AgentSubmissionStore {
return { kind: "conflict" }; return { kind: "conflict" };
} }
async admitDirect( async admitDirect(input: AgentSubmissionInput): Promise<AgentSubmission> {
input: DirectAgentSubmissionInput
): Promise<AgentSubmission> {
const sessionKey = createSessionStorageKey(input.id, "default", "default"); const sessionKey = createSessionStorageKey(input.id, "default", "default");
const extracted = prepareDirectSubmission(input); const extracted = prepareSubmissionAttachments(input);
const admission = currentTurnAdmission(); const admission = currentTurnAdmission();
const res = await this.convex.mutation(admitSubmission, { const res = await this.convex.mutation(admitSubmission, {
input: { input: {
@@ -1,30 +0,0 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineAgent } from "@flue/runtime";
import { createWorkPlannerTools } from "../tools/work-planner";
const INSTRUCTIONS = `You are Zopu's private Work Planner.
You may only submit typed proposals through tools:
- a Work Definition proposal;
- a Design Packet proposal containing 1-4 observable, independently verifiable slices;
- structured questions.
You must never approve a Definition or Design, change Work status, start execution, claim implementation, create Git changes, or claim that simulation implemented the Work. Convex validates and stores every proposal. Ask a focused structured question when a high-impact decision is unresolved.`;
export {
convexAgentRoute as attachments,
convexAgentRoute as route,
} from "../auth";
export default defineAgent(({ env }) => {
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
return {
description:
"Proposes Work Definitions, Design Packets, and structured questions.",
instructions: INSTRUCTIONS,
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
thinkingLevel: "medium",
tools: createWorkPlannerTools(env),
};
});
+10 -15
View File
@@ -1,31 +1,26 @@
import { parseAgentEnv } from "@code/env/agent"; import { parseAgentEnv } from "@code/env/agent";
import { defineAgent } from "@flue/runtime"; import { defineAgent } from "@flue/runtime";
import { local } from "@flue/runtime/node";
import INSTRUCTIONS from "../prompts/zopu-instructions.md" with { type: "markdown" }; import INSTRUCTIONS from "../prompts/zopu-instructions.md" with { type: "markdown" };
import { createCreatePrForIssueTool } from "../tools/create-pr-for-issue"; import { createSignalRoutingTools } from "../tools/signal-routing";
import { createSliceOneTools } from "../tools/slice-one"; import { createWorkCommandTools } from "../tools/work-commands";
export { export {
convexAgentRoute as attachments, conversationRoute as attachments,
convexAgentRoute as route, conversationRoute as route,
} from "../auth"; } from "../auth/conversation-route";
// The host checkout the chat agent explores. The `local()` sandbox reuses the
// machine's existing shell, Git, and Tea install unchanged; its cwd becomes the
// default working directory for every command the agent runs.
const ZOPU_CODE_PATH = "/Users/puter/Workspace/zopu/code";
export default defineAgent(({ env, id }) => { export default defineAgent(({ env, id }) => {
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env); const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
return { return {
description: description:
"Turns actionable conversation into provenanced Signals and proposed Work.", "Turns actionable conversation into provenanced Signals, proposed Work, and execution requests.",
instructions: INSTRUCTIONS, instructions: INSTRUCTIONS,
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`, model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
sandbox: local({ cwd: ZOPU_CODE_PATH }),
thinkingLevel: "high", thinkingLevel: "high",
tools: [...createSliceOneTools(id, env), createCreatePrForIssueTool(env)], tools: [
...createSignalRoutingTools(id, env),
...createWorkCommandTools(id, env),
],
}; };
}); });
+14 -16
View File
@@ -5,11 +5,12 @@ import type { Fetchable } from "@flue/runtime/routing";
import { flue } from "@flue/runtime/routing"; import { flue } from "@flue/runtime/routing";
import { Hono } from "hono"; import { Hono } from "hono";
import { internalRoute } from "./auth/internal-route";
import { runtimeRegistry } from "./runtime/agent-os-registry";
import { import {
cancelAgentOsAttempt, cancelAgentOsAttempt,
executeAgentOsAttempt, executeAgentOsAttempt,
runtimeRegistry, } from "./runtime/attempt-runner";
} from "./runtime/agent-os";
const agentEnv = parseAgentEnv(process.env); const agentEnv = parseAgentEnv(process.env);
@@ -34,12 +35,7 @@ registerProvider(agentEnv.AGENT_MODEL_PROVIDER, {
const app = new Hono(); const app = new Hono();
app.post("/internal/work-attempts/execute", async (context) => { app.post("/internal/work-attempts/execute", internalRoute, async (context) => {
if (
context.req.header("authorization") !== `Bearer ${agentEnv.FLUE_DB_TOKEN}`
) {
return context.json({ error: "Unauthorized" }, 401);
}
try { try {
return context.json(await executeAgentOsAttempt(await context.req.json())); return context.json(await executeAgentOsAttempt(await context.req.json()));
} catch (error) { } catch (error) {
@@ -65,19 +61,21 @@ app.post("/internal/work-attempts/execute", async (context) => {
} }
}); });
app.post("/internal/work-attempts/:workspaceKey/cancel", async (context) => { app.post(
if ( "/internal/work-attempts/:workspaceKey/cancel",
context.req.header("authorization") !== `Bearer ${agentEnv.FLUE_DB_TOKEN}` internalRoute,
) { async (context) => {
return context.json({ error: "Unauthorized" }, 401);
}
const body = (await context.req.json()) as { attemptId?: unknown }; const body = (await context.req.json()) as { attemptId?: unknown };
if (typeof body.attemptId !== "string" || body.attemptId.length === 0) { if (typeof body.attemptId !== "string" || body.attemptId.length === 0) {
return context.json({ error: "attemptId is required" }, 400); return context.json({ error: "attemptId is required" }, 400);
} }
await cancelAgentOsAttempt(context.req.param("workspaceKey"), body.attemptId); await cancelAgentOsAttempt(
context.req.param("workspaceKey"),
body.attemptId
);
return context.json({ cancelled: true }); return context.json({ cancelled: true });
}); }
);
app.all("/api/rivet/*", (context) => runtimeRegistry.handler(context.req.raw)); app.all("/api/rivet/*", (context) => runtimeRegistry.handler(context.req.raw));
@@ -1,10 +1,10 @@
import { parseAgentEnv } from "@code/env/agent"; import { parseAgentEnv } from "@code/env/agent";
import type { AgentRouteHandler } from "@flue/runtime"; import type { AgentRouteHandler } from "@flue/runtime";
import { withTurnAdmission } from "./admission-context"; import { withTurnAdmission } from "../adapters/admission-context";
/** Only Convex may invoke or observe the organization-scoped Flue agent. */ /** Only Convex may invoke or observe the organization-scoped Flue agent. */
export const convexAgentRoute: AgentRouteHandler = async (context, next) => { export const conversationRoute: AgentRouteHandler = async (context, next) => {
const env = parseAgentEnv(process.env); const env = parseAgentEnv(process.env);
const authorization = context.req.header("authorization"); const authorization = context.req.header("authorization");
if (authorization !== `Bearer ${env.FLUE_DB_TOKEN}`) { if (authorization !== `Bearer ${env.FLUE_DB_TOKEN}`) {
@@ -0,0 +1,12 @@
import { parseAgentEnv } from "@code/env/agent";
import type { MiddlewareHandler } from "hono";
/** Internal service authorization for execution/cancel routes. Not client-facing. */
export const internalRoute: MiddlewareHandler = async (context, next) => {
const env = parseAgentEnv(process.env);
const authorization = context.req.header("authorization");
if (authorization !== `Bearer ${env.FLUE_DB_TOKEN}`) {
return context.json({ error: "Unauthorized" }, 401);
}
return await next();
};
@@ -0,0 +1,39 @@
import { parseAgentEnv } from "@code/env/agent";
import type { WorkflowRouteHandler } from "@flue/runtime";
import * as v from "valibot";
const WorkflowInputEnvelope = v.object({
input: v.object({
organizationId: v.string(),
requestId: v.string(),
}),
});
/** Service authorization for finite Flue workflows. No turn headers required. */
export const workflowRoute: WorkflowRouteHandler = async (context, next) => {
const env = parseAgentEnv(process.env);
const authorization = context.req.header("authorization");
if (authorization !== `Bearer ${env.FLUE_DB_TOKEN}`) {
return context.json({ error: "Unauthorized" }, 401);
}
if (context.req.method !== "POST") {
return await next();
}
const body = (await context.req.json().catch(() => null)) as unknown;
const parsed = v.safeParse(WorkflowInputEnvelope, body);
if (!parsed.success) {
return context.json({ error: "Invalid workflow input envelope" }, 400);
}
const organizationId = context.req.header("x-zopu-organization-id");
if (
!organizationId ||
organizationId !== parsed.output.input.organizationId
) {
return context.json({ error: "Forbidden" }, 403);
}
return await next();
};
@@ -1,4 +1,4 @@
You are Zopu for product Slice 1 and the Work planning handoff. You are Zopu, the conversational agent that turns user messages into provenanced Signals, proposed Work, and execution requests.
## Your role ## Your role
@@ -12,15 +12,18 @@ When a user sends a message, follow this decision flow:
Direct questions, casual conversation, and image-reading requests must be answered immediately without calling any tools. Direct questions, casual conversation, and image-reading requests must be answered immediately without calling any tools.
2. **Identify project context.** Call list_projects. If there is one project, use it. If there are several and the request is ambiguous, ask one focused question. 2. **Identify project context.** Call `list_projects`. If there is one project, use it. If there are several and the request is ambiguous, ask one focused question.
3. **Create a Signal (only when actionable).** When the message is actionable: a. Call list_signal_evidence to see the exact admitted user messages. b. Select the message IDs that compose the problem statement. c. Call create_signal with a structured problem statement (title, summary, desiredOutcome, constraints). The problem statement must faithfully represent the user's own intent. Do not invent scope they did not mention. d. Include the projectId when the project is known. 3. **Create a Signal (only when actionable).** a. Call `list_signal_evidence` to see the exact admitted user messages. b. Select the message IDs that compose the problem statement. c. Call `create_signal` with a structured problem statement (title, summary, desiredOutcome, constraints). The problem statement must faithfully represent the user's own intent. Do not invent scope they did not mention. d. Include the `projectId` when the project is known.
4. **Route the Signal.** After creating the Signal: a. Call list_proposed_work for the project. b. If the Signal clearly describes the same desired outcome as existing Work, call attach_signal_to_work. c. Otherwise call create_work_from_signal. e. If genuinely uncertain whether to attach or create, ask one focused question. 4. **Route the Signal.** a. Call `list_proposed_work` for the project. b. If the Signal clearly describes the same desired outcome as existing Work, call `attach_signal_to_work`. c. Otherwise call `create_work_from_signal`. d. If genuinely uncertain whether to attach or create, ask one focused question.
5. **Explain the outcome.** Tell the user clearly what happened: 5. **Execution requests.** When a user explicitly asks to implement or start work on a ready Work unit, call `start_work` with the work ID. The system handles planning, execution, verification, and delivery. Do not claim implementation has started until the tool returns a runId.
6. **Explain the outcome.** Tell the user clearly what happened:
- "Captured [Signal title] and linked it to [Work title]." - "Captured [Signal title] and linked it to [Work title]."
- "Captured [Signal title] and proposed [Work title]." - "Captured [Signal title] and proposed [Work title]."
- "Started execution of [Work title]."
- Keep the response brief; the product renders the durable Work card separately. - Keep the response brief; the product renders the durable Work card separately.
## Rules ## Rules
@@ -31,27 +34,7 @@ When a user sends a message, follow this decision flow:
- Ask at most one focused clarification when genuinely ambiguous. - Ask at most one focused clarification when genuinely ambiguous.
- Preserve project and organization scope at all times. - Preserve project and organization scope at all times.
- Repeated delivery of the same message must not create duplicate Signals or attachments. The backend is idempotent. - Repeated delivery of the same message must not create duplicate Signals or attachments. The backend is idempotent.
- Never claim you created a Signal until the tool call returns successfully. - Never claim you created a Signal, started Work, or produced a result until the tool call returns successfully.
- Do not start implementation, planning, sandboxes, verification, or delivery. Those are explicitly outside Slice 1. - Do not perform implementation, planning, design, verification, or delivery yourself. Those are handled by dedicated workflows and the execution runtime.
- After creating proposed Work, the system may invoke the private work-planner. Never claim that a Definition, Design, approval, or implementation exists unless Convex reports it. - You have no repository access. You cannot run shell commands, inspect code, manage Git, or create pull requests. Focus on conversation, Signal capture, and Work orchestration.
- Proposed Work is the only Work status you directly create. - Proposed Work is the only Work status you directly create. The system advances Work through planning (Definition, Design) automatically. Work becomes ready for execution when planning completes.
## Issue resolution with create_pr_for_issue
When a user wants an open Gitea issue implemented and turned into a pull request, call `create_pr_for_issue` with the issue number or URL.
- The tool accepts the request immediately and runs the AgentOS implementation, branch push, and PR creation in the background.
- Tell the user the issue was accepted for background implementation; do not claim the PR exists yet and do not wait for completion in the chat turn.
- Do not retry the tool in a tight loop. Background success and failure are written to the agent server logs with the `[create_pr_for_issue]` prefix.
- Do not run repository mutations yourself; the tool owns the implementation and PR pipeline.
## Repository access
Your shell runs inside the `puter/zopu-code` checkout at `/Users/puter/Workspace/zopu/code`, so you can answer questions about this repository and manage its Gitea issues.
- Explore the repository with read-only Git: `git log`, `git status`, `git diff`, `git branch`, `git show`, and reading files. Use this to ground answers about the codebase.
- List open issues with `tea issues list` (defaults to open issues).
- Create a Gitea issue in this repo with `tea issues create --title "<title>" [--description "<details>"]`.
- Never mutate repository state: do not run `git push`, `git merge`, `git rebase`, `git reset`, `git commit`, `git add`, force operations, or any history rewrite.
- Never close, reopen, or edit existing issues (`tea issues close|reopen|edit`); only list and create.
- Never expose credentials, tokens, or the contents of Tea/Git config files.
+1 -1
View File
@@ -1,3 +1,3 @@
import { runtimeRegistry } from "./runtime/agent-os"; import { runtimeRegistry } from "./runtime/agent-os-registry";
await runtimeRegistry.startAndWait(); await runtimeRegistry.startAndWait();
@@ -0,0 +1,26 @@
import { makePiAgentOsConfig } from "@code/primitives";
import { agentOS, setup } from "@rivet-dev/agentos";
const MAX_ACP_COMPLETED_MESSAGE_BYTES = 128 * 1024 * 1024;
const piConfig = makePiAgentOsConfig();
/** AgentOS workspace actor. Only the registry runner serves this actor type. */
export const workspace = agentOS<undefined, { token: string }>({
limits: {
acp: {
maxCompletedMessageBytes: MAX_ACP_COMPLETED_MESSAGE_BYTES,
},
},
onBeforeConnect: (_context, params) => {
if (params.token !== process.env.RIVET_WORKSPACE_TOKEN) {
throw new Error("Unauthorized workspace connection");
}
},
options: {
actionTimeout: 10 * 60 * 1000,
},
permissions: piConfig.permissions,
software: piConfig.software,
});
export const runtimeRegistry = setup({ use: { workspace } });
@@ -1,43 +1,20 @@
import { parseAgentEnv } from "@code/env/agent"; import { parseAgentEnv } from "@code/env/agent";
import { import {
makePiAgentOsConfig,
makePiHomeFiles,
decodeWorkAttemptExecutionInput, decodeWorkAttemptExecutionInput,
WorkAttemptExecutionError, makePiHomeFiles,
piSessionEnv, piSessionEnv,
} from "@code/primitives"; } from "@code/primitives";
import type { import type {
ExecutionEvent, ExecutionEvent,
WorkAttemptExecutionResult, WorkAttemptExecutionResult,
} from "@code/primitives"; } from "@code/primitives";
import { agentOS, setup } from "@rivet-dev/agentos";
import { createHostDirBackend } from "@rivet-dev/agentos-core"; import { createHostDirBackend } from "@rivet-dev/agentos-core";
import { createClient } from "@rivet-dev/agentos/client"; import { createClient } from "@rivet-dev/agentos/client";
import { Effect } from "effect"; import { Effect } from "effect";
import { HostRepositoryWorkspace } from "./host-repository"; import type { runtimeRegistry } from "./agent-os-registry";
import { classifyRuntimeFailure, executionError } from "./execution-errors";
const MAX_ACP_COMPLETED_MESSAGE_BYTES = 128 * 1024 * 1024; import { RepositoryWorkspace } from "./repository-workspace";
const piConfig = makePiAgentOsConfig();
const workspace = agentOS<undefined, { token: string }>({
limits: {
acp: {
maxCompletedMessageBytes: MAX_ACP_COMPLETED_MESSAGE_BYTES,
},
},
onBeforeConnect: (_context, params) => {
if (params.token !== process.env.RIVET_WORKSPACE_TOKEN) {
throw new Error("Unauthorized workspace connection");
}
},
options: {
actionTimeout: 10 * 60 * 1000,
},
permissions: piConfig.permissions,
software: piConfig.software,
});
export const runtimeRegistry = setup({ use: { workspace } });
const event = ( const event = (
sequence: number, sequence: number,
@@ -52,32 +29,6 @@ const event = (
sequence, sequence,
}); });
const executionError = (
message: string,
reason: WorkAttemptExecutionError["reason"],
retryable: boolean
) => new WorkAttemptExecutionError({ message, reason, retryable });
const classifyRuntimeFailure = (cause: unknown): WorkAttemptExecutionError => {
if (cause instanceof WorkAttemptExecutionError) {
return cause;
}
const message = cause instanceof Error ? cause.message : "Execution failed";
if (/auth|credential|401|403/iu.test(message)) {
return executionError(message, "Authentication", false);
}
if (/clone|checkout|repository|git /iu.test(message)) {
return executionError(message, "RepositoryFailed", false);
}
if (/timeout|timed out/iu.test(message)) {
return executionError(message, "Timeout", true);
}
if (/connect|network|unavailable|502|503|504/iu.test(message)) {
return executionError(message, "ProviderUnavailable", true);
}
return executionError(message, "HarnessFailed", false);
};
export const executeAgentOsAttempt = async ( export const executeAgentOsAttempt = async (
rawInput: unknown rawInput: unknown
): Promise<WorkAttemptExecutionResult> => { ): Promise<WorkAttemptExecutionResult> => {
@@ -100,9 +51,10 @@ export const executeAgentOsAttempt = async (
workspaceKey: input.workspaceKey, workspaceKey: input.workspaceKey,
}), }),
]; ];
const hostRepository = new HostRepositoryWorkspace(); const repository = new RepositoryWorkspace();
const prepared = await hostRepository.prepare({ const prepared = await repository.prepare({
attemptId: input.attemptId, attemptId: input.attemptId,
baseBranch: input.baseBranch,
cloneUrl: input.repositoryUrl, cloneUrl: input.repositoryUrl,
credential: input.auth.credential, credential: input.auth.credential,
piHomeFiles: makePiHomeFiles({ piHomeFiles: makePiHomeFiles({
@@ -122,8 +74,8 @@ export const executeAgentOsAttempt = async (
1, 1,
"runtime.preparing", "runtime.preparing",
prepared.created prepared.created
? "Repository cloned on the execution host" ? "Isolated repository clone created on the execution host"
: "Repository re-cloned" : "Isolated repository clone recreated"
) )
); );
const mounts = [ const mounts = [
@@ -177,18 +129,15 @@ export const executeAgentOsAttempt = async (
const sessionId = `pi-${input.attemptId}`; const sessionId = `pi-${input.attemptId}`;
await vm.openSession({ await vm.openSession({
additionalDirectories: [`${prepared.checkoutPath}/.git`],
additionalInstructions: additionalInstructions:
"Work only inside /workspace/repository. This is an isolated checkout of the project repository. Read AGENTS.md and the relevant product specifications before changing code. Never reveal credentials, push, or open a pull request. Implement the requested change and run focused verification.", "Work only inside /workspace/repository. This is an isolated clone of the project repository. Read the project documentation and conventions before changing code. Never reveal credentials, push, create branches, or open a pull request. Implement the requested change and run focused verification.",
agent: "pi", agent: "pi",
cwd: "/workspace/repository", cwd: "/workspace/repository",
env: piSessionEnv(env.AGENT_MODEL_API_KEY), env: piSessionEnv(env.AGENT_MODEL_API_KEY),
permissionPolicy: "allow_all", permissionPolicy: "allow_all",
sessionId, sessionId,
}); });
events.push( events.push(event(3, "harness.started", "Implementation session started"));
event(3, "harness.started", "Pi implementation session started")
);
const promptResult = await vm.prompt({ const promptResult = await vm.prompt({
content: [{ text: input.prompt, type: "text" }], content: [{ text: input.prompt, type: "text" }],
idempotencyKey: input.attemptId, idempotencyKey: input.attemptId,
@@ -198,19 +147,19 @@ export const executeAgentOsAttempt = async (
const reason = const reason =
promptResult.stopReason === "cancelled" ? "Cancelled" : "HarnessFailed"; promptResult.stopReason === "cancelled" ? "Cancelled" : "HarnessFailed";
throw executionError( throw executionError(
`Pi stopped with ${promptResult.stopReason}`, `Agent stopped with ${promptResult.stopReason}`,
reason, reason,
promptResult.stopReason === "max_tokens" || promptResult.stopReason === "max_tokens" ||
promptResult.stopReason === "max_turn_requests" promptResult.stopReason === "max_turn_requests"
); );
} }
events.push( events.push(
event(4, "harness.progress", "Pi implementation turn completed", { event(4, "harness.progress", "Implementation turn completed", {
stopReason: promptResult.stopReason, stopReason: promptResult.stopReason,
}) })
); );
const collected = await HostRepositoryWorkspace.collect({ const collected = await RepositoryWorkspace.collect({
attemptId: input.attemptId, attemptId: input.attemptId,
baseRevision, baseRevision,
checkoutPath: prepared.checkoutPath, checkoutPath: prepared.checkoutPath,
@@ -221,9 +170,7 @@ export const executeAgentOsAttempt = async (
5, 5,
"repository.changed", "repository.changed",
`${changedFiles.length} changed file(s) collected`, `${changedFiles.length} changed file(s) collected`,
{ { candidateRevision }
candidateRevision,
}
), ),
event(6, "runtime.completed", "AgentOS execution completed") event(6, "runtime.completed", "AgentOS execution completed")
); );
@@ -237,8 +184,8 @@ export const executeAgentOsAttempt = async (
events, events,
summary: summary:
changedFiles.length > 0 changedFiles.length > 0
? `Pi changed ${changedFiles.length} file(s)` ? `Agent changed ${changedFiles.length} file(s)`
: "Pi completed without repository changes", : "Agent completed without repository changes",
}; };
} catch (error) { } catch (error) {
throw classifyRuntimeFailure(error); throw classifyRuntimeFailure(error);
@@ -0,0 +1,29 @@
import { WorkAttemptExecutionError } from "@code/primitives/execution-runtime";
export const executionError = (
message: string,
reason: WorkAttemptExecutionError["reason"],
retryable: boolean
) => new WorkAttemptExecutionError({ message, reason, retryable });
export const classifyRuntimeFailure = (
cause: unknown
): WorkAttemptExecutionError => {
if (cause instanceof WorkAttemptExecutionError) {
return cause;
}
const message = cause instanceof Error ? cause.message : "Execution failed";
if (/auth|credential|401|403/iu.test(message)) {
return executionError(message, "Authentication", false);
}
if (/clone|checkout|repository|git /iu.test(message)) {
return executionError(message, "RepositoryFailed", false);
}
if (/timeout|timed out/iu.test(message)) {
return executionError(message, "Timeout", true);
}
if (/connect|network|unavailable|502|503|504/iu.test(message)) {
return executionError(message, "ProviderUnavailable", true);
}
return executionError(message, "HarnessFailed", false);
};
@@ -15,6 +15,7 @@ import type { PiHomeFiles } from "@code/primitives";
interface PrepareRepositoryInput { interface PrepareRepositoryInput {
attemptId: string; attemptId: string;
baseBranch?: string;
cloneUrl: string; cloneUrl: string;
/** Provider credential for the initial clone, stripped after cloning. */ /** Provider credential for the initial clone, stripped after cloning. */
credential?: string; credential?: string;
@@ -68,20 +69,40 @@ const changedFilePath = (line: string): string => {
: filePath.slice(renameIndex + renameSeparator.length); : filePath.slice(renameIndex + renameSeparator.length);
}; };
// Minimal allow-list so untrusted repo code (postinstall scripts, hooks)
// can never read host secrets like tokens or API keys from the inherited env.
// PATH is needed for git/bun; locale vars prevent encoding issues.
// HOME is intentionally excluded — the runner gets its own /home/zopu mount,
// and forwarding the host HOME would expose host-level dotfiles and credentials.
const SAFE_ENV_KEYS = [
"PATH",
"LANG",
"LC_ALL",
"LC_CTYPE",
"TERM",
"TZ",
] as const;
const safeBaseEnv = (): Record<string, string> => {
const env: Record<string, string> = {};
for (const key of SAFE_ENV_KEYS) {
const value = process.env[key];
if (value !== undefined) {
env[key] = value;
}
}
return env;
};
const runProcess = async ( const runProcess = async (
command: string, command: string,
args: readonly string[], args: readonly string[],
cwd: string, cwd: string,
env: Record<string, string> = {} env: Record<string, string> = {}
): Promise<ProcessResult> => { ): Promise<ProcessResult> => {
const environment = Object.fromEntries(
Object.entries(process.env).filter(
(entry): entry is [string, string] => entry[1] !== undefined
)
);
const child = spawn(command, args, { const child = spawn(command, args, {
cwd, cwd,
env: { ...environment, ...env }, env: { ...safeBaseEnv(), ...env },
}); });
const stderr: Uint8Array[] = []; const stderr: Uint8Array[] = [];
const stdout: Uint8Array[] = []; const stdout: Uint8Array[] = [];
@@ -107,7 +128,7 @@ const pathExists = async (target: string): Promise<boolean> => {
} }
}; };
export class HostRepositoryWorkspace { export class RepositoryWorkspace {
readonly #root: string; readonly #root: string;
readonly #installDependencies: boolean; readonly #installDependencies: boolean;
@@ -169,12 +190,17 @@ export class HostRepositoryWorkspace {
"Repository clone" "Repository clone"
); );
} finally { } finally {
// The credential env var is never written to disk or git config. // The credential only existed on the local cloneEnv object, never on
// The askpass script is cleaned up with the workspace. // process.env. The askpass script is cleaned up with the workspace.
cloneEnv.ZOPU_GIT_TOKEN = ""; cloneEnv.ZOPU_GIT_TOKEN = "";
delete process.env.ZOPU_GIT_TOKEN;
} }
if (input.baseBranch) {
requireSuccess(
await runGit(checkoutPath, ["checkout", input.baseBranch]),
"Base branch checkout"
);
}
const bunExecutable = const bunExecutable =
process.env.BUN_EXECUTABLE ?? process.env.BUN_EXECUTABLE ??
execFileSync("which", ["bun"], { encoding: "utf-8" }).trim(); execFileSync("which", ["bun"], { encoding: "utf-8" }).trim();
@@ -1,572 +0,0 @@
import { spawnSync } from "node:child_process";
import type { SpawnSyncOptions } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import path from "node:path";
import { parseAgentEnv } from "@code/env/agent";
import { defineTool } from "@flue/runtime";
import type { JsonValue } from "@flue/runtime";
import * as v from "valibot";
import { executeAgentOsAttempt } from "../runtime/agent-os";
/**
* Direct Zopu chat tool that converts a Gitea issue into an AgentOS-implemented
* pull request. It owns the entire issue → harness → push → PR pipeline so the
* chat agent never needs (and must not use) shell mutation for this workflow.
*/
// The host checkout the chat agent explores. Tea and Git resolve this repo's
// Gitea connection; all Tea/Git commands run with this fixed cwd.
const TRUSTED_REPO_PATH = "/Users/puter/Workspace/zopu/code";
const REPO_SLUG = "puter/zopu-code";
// Bounded output so a verbose harness/CLI can never blow up a tool result.
const MAX_OUTPUT = 4000;
const MAX_ISSUE_BODY = 8000;
const MAX_CHANGED_FILES = 200;
const COMMAND_TIMEOUT_MS = 60_000;
const PUSH_TIMEOUT_MS = 120_000;
interface CommandResult {
readonly status: number | null;
readonly stdout: string;
readonly stderr: string;
}
interface IssueDetails {
readonly body: string;
readonly title: string;
readonly url: string;
}
interface ToolFailure {
readonly [key: string]: JsonValue;
readonly error: string;
readonly issue: string;
readonly stage: string;
}
interface ToolSuccess {
readonly [key: string]: JsonValue;
readonly baseBranch: string;
readonly changedFileCount: number;
readonly changedFiles: string[];
readonly headBranch: string;
readonly issue: string;
readonly issueNumber: number;
readonly ok: true;
readonly pullRequestUrl: string;
readonly summary: string;
}
interface ToolAccepted {
readonly [key: string]: JsonValue;
readonly accepted: true;
readonly backgroundId: string;
readonly issue: string;
readonly issueNumber: number;
readonly message: string;
readonly ok: true;
}
const cap = (value: string, limit = MAX_OUTPUT): string =>
value.length <= limit ? value : `${value.slice(0, limit)}…[truncated]`;
// Stable prefix for every background pipeline log line so operators can grep
// for issue→PR outcomes independently of chat traffic.
const LOG_PREFIX = "[create_pr_for_issue]";
/** Writes one stable, prefixed server log line for a background outcome. */
const logBackground = (level: "info" | "error", message: string): void => {
const line = `${LOG_PREFIX} ${message}`;
if (level === "error") {
console.error(line);
} else {
console.log(line);
}
};
/** Logs a structured terminal pipeline result under the stable prefix. */
const logPipelineResult = (
backgroundId: string,
result: ToolFailure | ToolSuccess
): void => {
if ("ok" in result && result.ok === true) {
logBackground(
"info",
`background=${backgroundId} completed issue=#${result.issueNumber} stage=pr url=${result.pullRequestUrl} changedFiles=${result.changedFileCount}`
);
return;
}
const detail =
"reason" in result && typeof result.reason === "string"
? ` reason=${result.reason}`
: "";
logBackground(
"error",
`background=${backgroundId} failed issue=${result.issue} issueNumber=${result.issueNumber} stage=${result.stage}: ${result.error}${detail}`
);
};
/** Last-resort logger for an unexpected throw outside the pipeline's catch. */
const logPipelineError = (backgroundId: string, error: unknown): void => {
const message = error instanceof Error ? error.message : String(error);
logBackground("error", `background=${backgroundId} unexpected: ${message}`);
};
/**
* Runs one command as a direct child process. Arguments are passed to the
* child verbatim — never through a shell — so issue titles, bodies, and
* revisions cannot inject shell metacharacters.
*/
const run = (
command: string,
args: readonly string[],
options: {
cwd?: string;
env?: Record<string, string>;
timeoutMs?: number;
} = {}
): CommandResult => {
const spawnOptions: SpawnSyncOptions = {
cwd: options.cwd ?? TRUSTED_REPO_PATH,
env: { ...process.env, ...options.env },
maxBuffer: 1024 * 1024,
timeout: options.timeoutMs ?? COMMAND_TIMEOUT_MS,
};
const result = spawnSync(command, args, spawnOptions);
return {
status: result.status,
stderr: cap((result.stderr ?? "").toString()),
stdout: cap((result.stdout ?? "").toString()),
};
};
/** Extracts the issue number from an issue number, "#23", or a full issue URL. */
const parseIssueNumber = (input: string): number | null => {
// Prefer a /issues/<n> URL path segment so anchors like #comment-45 do not
// hijack the number.
const pathMatch = input.match(/\/issues\/(?<issueNumber>\d+)(?:[^/\d]|$)/iu);
if (pathMatch?.groups?.issueNumber) {
return Math.trunc(Number(pathMatch.groups.issueNumber));
}
const hashMatch = input.match(/#(?<issueNumber>\d+)\b/u);
if (hashMatch?.groups?.issueNumber) {
return Math.trunc(Number(hashMatch.groups.issueNumber));
}
const standalone = input.trim().match(/^\d+$/u);
if (standalone) {
return Math.trunc(Number(standalone[0]));
}
// Last resort: the trailing integer anywhere in the string.
const value = Math.trunc(Number(input.match(/\d+/gu)?.at(-1)));
if (Number.isFinite(value) && value > 0) {
return value;
}
return null;
};
const readIssue = (issueNumber: number): IssueDetails | ToolFailure => {
const result = run("tea", [
"issues",
String(issueNumber),
"--fields",
"index,title,body,url,state",
"--output",
"json",
]);
if (result.status !== 0) {
return {
error: `tea could not read issue #${issueNumber}.`,
issue: String(issueNumber),
issueNumber,
stage: "read-issue",
stderr: result.stderr,
};
}
let data: {
body?: string;
state?: string;
title?: string;
url?: string;
};
try {
data = JSON.parse(result.stdout) as typeof data;
} catch (error) {
return {
error: `Could not parse the issue payload from tea: ${
error instanceof Error ? error.message : String(error)
}`,
issue: String(issueNumber),
issueNumber,
stage: "read-issue",
stderr: result.stdout,
};
}
const title = (data.title ?? "").trim();
if (title.length === 0) {
return {
error: `Issue #${issueNumber} has no title.`,
issue: String(issueNumber),
issueNumber,
stage: "read-issue",
};
}
return {
body: (data.body ?? "").trim(),
title,
url: (data.url ?? "").trim(),
};
};
const buildPrompt = (issueNumber: number, issue: IssueDetails): string => {
const body =
issue.body.length > MAX_ISSUE_BODY
? `${issue.body.slice(0, MAX_ISSUE_BODY)}…[issue body truncated]`
: issue.body;
return [
`Resolve Gitea issue #${issueNumber} in this repository and ship a complete implementation.`,
"",
`## Issue #${issueNumber}: ${issue.title}`,
"",
body || "_(The issue provided no additional description.)_",
"",
"## Instructions",
"- Read AGENTS.md and the relevant product/architecture specifications before changing code.",
"- Implement exactly the change described by the issue with focused, correct edits.",
"- Reuse existing conventions; do not introduce unrelated scope.",
"- Run focused verification for your change before finishing.",
"- Do NOT push, open a pull request, rewrite history, or modify the read-only base checkout.",
" The orchestrator collects your working-tree changes, pushes a candidate branch, and opens the pull request after you finish.",
].join("\n");
};
/** Mirrors HostRepositoryWorkspace's worktree layout to locate the harness checkout. */
const harnessCheckoutPath = (attemptId: string): string => {
const root = process.env.AGENT_WORKSPACE_ROOT ?? "/var/lib/zopu/workspaces";
const identity = createHash("sha256")
.update(attemptId)
.digest("hex")
.slice(0, 24);
return path.join(root, identity, "repository");
};
const detectBaseBranch = (): string => {
const result = run("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], {
cwd: TRUSTED_REPO_PATH,
});
if (result.status === 0) {
const ref = result.stdout.trim();
const branch = ref.startsWith("origin/")
? ref.slice("origin/".length)
: ref;
if (branch.length > 0) {
return branch;
}
}
return "master";
};
const pushCandidate = (
checkoutPath: string,
candidateRevision: string,
pushBranch: string
): true | ToolFailure => {
const result = run(
"git",
[
"-C",
checkoutPath,
"push",
"origin",
`${candidateRevision}:refs/heads/${pushBranch}`,
],
{ cwd: checkoutPath, timeoutMs: PUSH_TIMEOUT_MS }
);
if (result.status !== 0) {
return {
error: `Could not push candidate branch "${pushBranch}" from the harness worktree.`,
issue: "",
stage: "push",
stderr: result.stderr,
};
}
return true;
};
const findPullUrl = (head: string): string => {
const result = run("tea", [
"pulls",
"list",
"--state",
"open",
"--fields",
"index,title,head,url",
"--output",
"json",
]);
if (result.status !== 0) {
return "";
}
try {
const pulls = JSON.parse(result.stdout) as {
head?: string;
url?: string;
}[];
const match = pulls.find(
(pull) => pull.head === head || (pull.head ?? "").endsWith(`:${head}`)
);
return (match?.url ?? "").trim();
} catch {
return "";
}
};
const createPullRequest = (
issueNumber: number,
title: string,
head: string,
base: string
): ToolFailure | { url: string } => {
const prTitle = `Fix #${issueNumber}: ${title}`;
const result = run("tea", [
"pulls",
"create",
"--head",
head,
"--base",
base,
"--title",
prTitle,
"--description",
`Fixes #${issueNumber}`,
]);
if (result.status !== 0) {
return {
error: `tea could not create the pull request for branch "${head}".`,
issue: "",
stage: "create-pr",
stderr: result.stderr,
stdout: result.stdout,
};
}
const combined = `${result.stdout}\n${result.stderr}`;
const urlMatch = combined.match(/https?:\/\/[^\s)]+\/pulls\/\d+/u);
if (urlMatch) {
return { url: urlMatch[0] };
}
// Fall back to listing open pulls and matching the head branch.
const fallback = findPullUrl(head);
if (fallback.length > 0) {
return { url: fallback };
}
return {
error:
"The pull request was created, but its URL could not be captured from tea. Inspect open pulls in Gitea.",
issue: "",
stage: "create-pr",
stdout: result.stdout,
};
};
/**
* Runs the full issue → AgentOS → push → PR pipeline and returns a structured
* terminal result. Used both by the background scheduler (chat path) and any
* direct/internal caller that wants the structured outcome. Never rejects: every
* failure is caught and returned as a {@link ToolFailure}.
*/
const runIssuePrPipeline = async (params: {
readonly giteaUrl: string;
readonly giteaToken: string | undefined;
readonly issue: string;
readonly issueNumber: number;
readonly identifiers: {
readonly attemptId: string;
readonly headBranch: string;
readonly runId: string;
readonly workspaceKey: string;
};
}): Promise<ToolFailure | ToolSuccess> => {
const { giteaUrl, giteaToken, issue, issueNumber, identifiers } = params;
try {
const issueOrFailure = readIssue(issueNumber);
if ("error" in issueOrFailure) {
return issueOrFailure;
}
const issueDetails = issueOrFailure;
const baseBranch = detectBaseBranch();
const prompt = buildPrompt(issueNumber, issueDetails);
// The fixed-checkout harness path ignores `auth`/`repositoryUrl` for
// cloning (it always uses the mounted Zopu source worktree); they are
// still supplied as valid schema values. GITEA_TOKEN is optional.
const result = await executeAgentOsAttempt({
attemptId: identifiers.attemptId,
auth: {
credential: giteaToken ?? "unused-by-fixed-checkout",
provider: "gitea",
serverUrl: giteaUrl,
},
baseBranch,
prompt,
repositoryUrl: `${giteaUrl}/${REPO_SLUG}`,
runId: identifiers.runId,
workId: `issue-${issueNumber}`,
workspaceKey: identifiers.workspaceKey,
});
const changedFiles = result.changedFiles.slice(0, MAX_CHANGED_FILES);
if (
result.changedFiles.length === 0 ||
result.candidateRevision === result.baseRevision
) {
return {
error:
"The AgentOS session completed without producing any repository changes, so there is nothing to turn into a pull request.",
issue,
issueNumber,
stage: "harness",
summary: result.summary,
};
}
const checkoutPath = harnessCheckoutPath(identifiers.attemptId);
const pushed = pushCandidate(
checkoutPath,
result.candidateRevision,
identifiers.headBranch
);
if (pushed !== true) {
return {
...pushed,
attemptId: identifiers.attemptId,
baseRevision: result.baseRevision,
candidateRevision: result.candidateRevision,
headBranch: identifiers.headBranch,
issue,
issueNumber,
workspaceKey: identifiers.workspaceKey,
};
}
const pr = createPullRequest(
issueNumber,
issueDetails.title,
identifiers.headBranch,
baseBranch
);
if ("error" in pr) {
return {
...pr,
attemptId: identifiers.attemptId,
baseRevision: result.baseRevision,
candidateRevision: result.candidateRevision,
headBranch: identifiers.headBranch,
issue,
issueNumber,
workspaceKey: identifiers.workspaceKey,
};
}
return {
baseBranch,
changedFileCount: result.changedFiles.length,
changedFiles,
headBranch: identifiers.headBranch,
issue,
issueNumber,
ok: true,
pullRequestUrl: pr.url,
summary: result.summary,
};
} catch (error) {
const message =
error instanceof Error ? error.message : "Unexpected failure";
const reason =
error !== null &&
typeof error === "object" &&
"reason" in error &&
typeof (error as { reason: unknown }).reason === "string"
? (error as { reason: string }).reason
: undefined;
return {
error: message,
issue,
issueNumber,
...(reason ? { reason } : {}),
stage: "harness",
};
}
};
export const createCreatePrForIssueTool = (
runtimeEnv: Record<string, string | undefined>
) => {
const agentEnv = parseAgentEnv(runtimeEnv);
const giteaUrl = agentEnv.GITEA_URL;
return defineTool({
description:
'Validate a Gitea issue number or URL immediately, then return an accepted acknowledgement right away while an AgentOS coding session implements the issue, pushes a candidate branch, and opens a PR titled "Fix #N: <title>" with body "Fixes #N" in the background. Pass a Gitea issue number or full issue URL. The created PR URL and any failure are logged to the server log with the "[create_pr_for_issue]" prefix rather than returned to chat; malformed input returns a parse failure immediately.',
input: v.object({
issue: v.pipe(
v.string(),
v.description(
"A Gitea issue number like 23, or a full issue URL such as https://git.openputer.com/puter/zopu-code/issues/23."
)
),
}),
name: "create_pr_for_issue",
run({ input }): ToolFailure | ToolAccepted {
const issueRef = input.issue.trim();
const issueNumber = parseIssueNumber(issueRef);
if (issueNumber === null) {
return {
error: `Could not parse an issue number from "${input.issue}". Provide a Gitea issue number or URL.`,
issue: input.issue,
stage: "parse",
};
}
// Background identifiers are generated up front so the accepted
// response can reference them and the pipeline can reuse them.
const backgroundId = randomUUID();
const attemptId = `create-pr-${issueNumber}-${randomUUID()}`;
const workspaceKey = `issue-${issueNumber}-pr-${randomUUID()}`;
const runId = `issue-pr-${randomUUID()}`;
const shortId = randomUUID().slice(0, 8);
const headBranch = `zopu/issue-${issueNumber}-${shortId}`;
// Schedule the full issue → AgentOS → push → PR pipeline on the next
// microtask boundary so this turn returns immediately. The pipeline
// catches its own failures and logs a stable prefixed line; the IIFE
// never rejects, so the originating chat turn cannot fail.
queueMicrotask(() => {
void (async () => {
try {
const result = await runIssuePrPipeline({
giteaToken: runtimeEnv.GITEA_TOKEN,
giteaUrl,
identifiers: { attemptId, headBranch, runId, workspaceKey },
issue: input.issue,
issueNumber,
});
logPipelineResult(backgroundId, result);
} catch (error) {
logPipelineError(backgroundId, error);
}
})();
});
return {
accepted: true,
backgroundId,
issue: input.issue,
issueNumber,
message: `Issue #${issueNumber} accepted. An AgentOS session will implement it, push branch "${headBranch}", and open a pull request in the background. The created PR URL and any failure are logged with the "${LOG_PREFIX}" prefix.`,
ok: true,
};
},
});
};
@@ -1,9 +1,9 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineTool } from "@flue/runtime"; import { defineTool } from "@flue/runtime";
import { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server"; import { makeFunctionReference } from "convex/server";
import * as v from "valibot"; import * as v from "valibot";
import { createConvexServiceClient } from "../adapters/convex-client";
const listProjectsRef = makeFunctionReference< const listProjectsRef = makeFunctionReference<
"query", "query",
{ organizationId: string; token: string }, { organizationId: string; token: string },
@@ -52,13 +52,11 @@ const attachSignalRef = makeFunctionReference<
{ attached: boolean; workId: string } { attached: boolean; workId: string }
>("works:attachSignalToWork"); >("works:attachSignalToWork");
export const createSliceOneTools = ( export const createSignalRoutingTools = (
organizationId: string, organizationId: string,
runtimeEnv: Record<string, string | undefined> runtimeEnv: Record<string, string | undefined>
) => { ) => {
const env = parseAgentEnv(runtimeEnv); const { client, token } = createConvexServiceClient(runtimeEnv);
const client = new ConvexHttpClient(env.CONVEX_URL);
const token = env.FLUE_DB_TOKEN;
return [ return [
defineTool({ defineTool({
@@ -0,0 +1,79 @@
import { defineTool } from "@flue/runtime";
import { makeFunctionReference } from "convex/server";
import * as v from "valibot";
import { createConvexServiceClient } from "../adapters/convex-client";
const startWorkRef = makeFunctionReference<
"mutation",
{
organizationId: string;
sliceId?: string;
token: string;
workId: string;
},
{ runId: string }
>("workExecution:start");
const cancelWorkRef = makeFunctionReference<
"mutation",
{ organizationId: string; runId: string; token: string },
{ cancelled: boolean }
>("workExecution:cancel");
const retryWorkRef = makeFunctionReference<
"mutation",
{ organizationId: string; runId: string; token: string },
{ runId: string }
>("workExecution:retry");
export const createWorkCommandTools = (
organizationId: string,
runtimeEnv: Record<string, string | undefined>
) => {
const { client, token } = createConvexServiceClient(runtimeEnv);
return [
defineTool({
description:
"Start execution of a ready Work unit. Returns the runId. Use only when the user explicitly requests implementation.",
input: v.object({
sliceId: v.optional(v.string()),
workId: v.string(),
}),
name: "start_work",
async run({ input }) {
return await client.mutation(startWorkRef, {
organizationId,
sliceId: input.sliceId,
token,
workId: input.workId,
});
},
}),
defineTool({
description: "Cancel a running Work execution by runId.",
input: v.object({ runId: v.string() }),
name: "cancel_work",
async run({ input }) {
return await client.mutation(cancelWorkRef, {
organizationId,
runId: input.runId,
token,
});
},
}),
defineTool({
description: "Retry a failed or cancelled Work execution by runId.",
input: v.object({ runId: v.string() }),
name: "retry_work",
async run({ input }) {
return await client.mutation(retryWorkRef, {
organizationId,
runId: input.runId,
token,
});
},
}),
];
};
-69
View File
@@ -1,69 +0,0 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineTool } from "@flue/runtime";
import { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server";
import * as v from "valibot";
const submitDefinitionRef = makeFunctionReference<
"mutation",
{ workId: string; payloadJson: string; token: string },
{ version: number }
>("workPlanning:submitDefinitionProposal");
const submitDesignRef = makeFunctionReference<
"mutation",
{ workId: string; payloadJson: string; token: string },
{ version: number }
>("workPlanning:submitDesignProposal");
const submitQuestionRef = makeFunctionReference<
"mutation",
{ workId: string; questionJson: string; token: string },
{ accepted: boolean }
>("workPlanning:submitQuestion");
export const createWorkPlannerTools = (
runtimeEnv: Record<string, string | undefined>
) => {
const env = parseAgentEnv(runtimeEnv);
const client = new ConvexHttpClient(env.CONVEX_URL);
const token = env.FLUE_DB_TOKEN;
return [
defineTool({
description:
"Submit a versioned Work Definition proposal. This never approves the Definition.",
input: v.object({ definitionJson: v.string(), workId: v.string() }),
name: "submit_definition_proposal",
async run({ input }) {
return await client.mutation(submitDefinitionRef, {
payloadJson: input.definitionJson,
token,
workId: input.workId,
});
},
}),
defineTool({
description:
"Submit a Design Packet with observable independently verifiable slices. This never approves the Design.",
input: v.object({ designJson: v.string(), workId: v.string() }),
name: "submit_design_proposal",
async run({ input }) {
return await client.mutation(submitDesignRef, {
payloadJson: input.designJson,
token,
workId: input.workId,
});
},
}),
defineTool({
description: "Submit one structured question for human attention.",
input: v.object({ questionJson: v.string(), workId: v.string() }),
name: "submit_question",
async run({ input }) {
return await client.mutation(submitQuestionRef, {
questionJson: input.questionJson,
token,
workId: input.workId,
});
},
}),
];
};
+121
View File
@@ -0,0 +1,121 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineAgent, defineWorkflow } from "@flue/runtime";
import * as v from "valibot";
export { workflowRoute as route } from "../auth/workflow-route";
// --- Input ---
const questionSchema = v.object({
alternatives: v.array(v.string()),
id: v.string(),
impact: v.picklist(["low", "medium", "high"]),
prompt: v.string(),
recommendation: v.optional(v.string()),
});
const planWorkInput = v.object({
context: v.object({
existingDefinition: v.optional(v.string()),
problemStatement: v.string(),
}),
organizationId: v.string(),
phase: v.picklist(["definition", "design"]),
requestId: v.string(),
workId: v.string(),
});
// --- Output ---
const definitionSchema = v.object({
acceptanceCriteria: v.array(v.string()),
affectedUsers: v.array(v.string()),
assumptions: v.array(v.string()),
constraints: v.array(v.string()),
desiredOutcome: v.string(),
inScope: v.array(v.string()),
outOfScope: v.array(v.string()),
problem: v.string(),
requiredArtifacts: v.array(v.string()),
risk: v.picklist(["low", "medium", "high"]),
});
const sliceSchema = v.object({
codeBoundaries: v.array(v.string()),
dependsOn: v.array(v.string()),
evidenceRequirements: v.array(v.string()),
id: v.string(),
objective: v.string(),
observableBehavior: v.string(),
reviewRequired: v.boolean(),
title: v.string(),
verification: v.array(v.string()),
});
const designSchema = v.object({
architectureSummary: v.string(),
callFlowDelta: v.array(v.string()),
concerns: v.array(v.string()),
evidenceRequirements: v.array(v.string()),
fileTreeDelta: v.array(v.string()),
invariants: v.array(v.string()),
keyInterfaces: v.array(v.string()),
slices: v.array(sliceSchema),
tradeoffs: v.array(v.string()),
});
const planWorkOutput = v.union([
v.object({
definition: definitionSchema,
phase: v.literal("definition"),
questions: v.array(questionSchema),
}),
v.object({
design: designSchema,
phase: v.literal("design"),
questions: v.array(questionSchema),
}),
]);
// --- Agent ---
const planWorkAgent = defineAgent(({ env }) => {
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
return {
description:
"Produces a Work Definition or Design Packet with structured questions.",
instructions: `You are Zopu's Work Planner. You receive a Work problem statement and produce either a Definition or a Design Packet depending on the requested phase.
For the definition phase, produce a precise problem statement, desired outcome, acceptance criteria, scope boundaries, assumptions, constraints, and risk level. Ask focused high-impact questions only when a decision genuinely blocks the work.
For the design phase, produce an architecture summary, file tree delta, call flow delta, key interfaces, invariants, evidence requirements, 1-4 observable and independently verifiable slices, tradeoffs, and concerns. Each slice must have a clear objective, observable behavior, verification steps, and code boundaries. Dependencies must reference earlier slices.
Never approve, execute, or claim implementation. Your output is a proposal that Convex validates and persists.`,
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
thinkingLevel: "high",
};
});
// --- Workflow ---
export default defineWorkflow({
agent: planWorkAgent,
input: planWorkInput,
output: planWorkOutput,
async run({ input, harness }) {
const session = await harness.session();
const instruction =
input.phase === "definition"
? `Produce a Work Definition for the following problem:\n\n${input.context.problemStatement}`
: `Produce a Design Packet based on this approved Definition:\n\n${input.context.existingDefinition ?? input.context.problemStatement}`;
const response = await session.prompt(instruction, {
result: planWorkOutput,
});
if (response.data.phase !== input.phase) {
throw new Error(
`Planner returned phase "${response.data.phase}" but "${input.phase}" was requested`
);
}
return response.data;
},
});
+3 -3
View File
@@ -1,6 +1,6 @@
# Convex Backend # Convex Backend
Convex is the only application API used by web, desktop, and mobile clients. The active backend covers the durable control plane through Slice 4: tenancy, projects, conversation turns, Signals, Work planning, approved Design slices, simulated Runs and Attempts, artifacts/delivery metadata, and Flue persistence. Convex is the only application API used by web, desktop, and mobile clients. The active backend covers the durable control plane: tenancy, projects, conversation turns, Signals, Work planning (Definition/Design without approval gates), real Runs and Attempts through AgentOS, artifacts/delivery metadata, and Flue persistence.
## Domain relations ## Domain relations
@@ -9,9 +9,9 @@ Convex is the only application API used by web, desktop, and mobile clients. The
- `conversations`, `conversationTurns`, `conversationMessages`, and `conversationAttachments` own chat admission and reactive history. - `conversations`, `conversationTurns`, `conversationMessages`, and `conversationAttachments` own chat admission and reactive history.
- `signals`, `signalConstraints`, and `signalSources` preserve structured intent and exact evidence. - `signals`, `signalConstraints`, and `signalSources` preserve structured intent and exact evidence.
- `works`, `signalWorkAttachments`, and `workEvents` own proposed Work and provenance. - `works`, `signalWorkAttachments`, and `workEvents` own proposed Work and provenance.
- `workDefinitions`, `workQuestions`, and `workApprovals` own versioned outcome contracts and approvals. - `workDefinitions` and `workQuestions` own versioned outcome contracts and structured questions.
- `designPackets` and `workSlices` preserve versioned implementation intent and executable slice order. - `designPackets` and `workSlices` preserve versioned implementation intent and executable slice order.
- `workRuns`, `workAttempts`, `workAttemptEvents`, and `resolverDecisions` own bounded simulated execution and recovery. - `workRuns`, `workAttempts`, `workAttemptEvents`, and `resolverDecisions` own bounded real execution and recovery.
- `workArtifacts` and `workDeliveries` preserve evidence and delivery metadata without performing Git or sandbox work. - `workArtifacts` and `workDeliveries` preserve evidence and delivery metadata without performing Git or sandbox work.
The `flue*` tables are infrastructure tables required by Flue's persistence contract. They are deliberately separate from the product relations. The `flue*` tables are infrastructure tables required by Flue's persistence contract. They are deliberately separate from the product relations.
+8 -4
View File
@@ -8,6 +8,7 @@
* @module * @module
*/ */
import type * as agentBackend from "../agentBackend.js";
import type * as auth from "../auth.js"; import type * as auth from "../auth.js";
import type * as authz from "../authz.js"; import type * as authz from "../authz.js";
import type * as conversationMessages from "../conversationMessages.js"; import type * as conversationMessages from "../conversationMessages.js";
@@ -15,7 +16,10 @@ import type * as conversationProjections from "../conversationProjections.js";
import type * as crons from "../crons.js"; import type * as crons from "../crons.js";
import type * as fluePersistence from "../fluePersistence.js"; import type * as fluePersistence from "../fluePersistence.js";
import type * as gitConnectionData from "../gitConnectionData.js"; import type * as gitConnectionData from "../gitConnectionData.js";
import type * as gitConnectionHealth from "../gitConnectionHealth.js";
import type * as gitConnections from "../gitConnections.js"; import type * as gitConnections from "../gitConnections.js";
import type * as gitProvisioning from "../gitProvisioning.js";
import type * as gitWebhooks from "../gitWebhooks.js";
import type * as healthCheck from "../healthCheck.js"; import type * as healthCheck from "../healthCheck.js";
import type * as http from "../http.js"; import type * as http from "../http.js";
import type * as organizations from "../organizations.js"; import type * as organizations from "../organizations.js";
@@ -25,8 +29,6 @@ import type * as publicGit from "../publicGit.js";
import type * as signalRouting from "../signalRouting.js"; import type * as signalRouting from "../signalRouting.js";
import type * as workArtifacts from "../workArtifacts.js"; import type * as workArtifacts from "../workArtifacts.js";
import type * as workExecution from "../workExecution.js"; import type * as workExecution from "../workExecution.js";
import type * as workExecutionAgent from "../workExecutionAgent.js";
import type * as workExecutionWorkflow from "../workExecutionWorkflow.js";
import type * as workPlanning from "../workPlanning.js"; import type * as workPlanning from "../workPlanning.js";
import type * as works from "../works.js"; import type * as works from "../works.js";
@@ -37,6 +39,7 @@ import type {
} from "convex/server"; } from "convex/server";
declare const fullApi: ApiFromModules<{ declare const fullApi: ApiFromModules<{
agentBackend: typeof agentBackend;
auth: typeof auth; auth: typeof auth;
authz: typeof authz; authz: typeof authz;
conversationMessages: typeof conversationMessages; conversationMessages: typeof conversationMessages;
@@ -44,7 +47,10 @@ declare const fullApi: ApiFromModules<{
crons: typeof crons; crons: typeof crons;
fluePersistence: typeof fluePersistence; fluePersistence: typeof fluePersistence;
gitConnectionData: typeof gitConnectionData; gitConnectionData: typeof gitConnectionData;
gitConnectionHealth: typeof gitConnectionHealth;
gitConnections: typeof gitConnections; gitConnections: typeof gitConnections;
gitProvisioning: typeof gitProvisioning;
gitWebhooks: typeof gitWebhooks;
healthCheck: typeof healthCheck; healthCheck: typeof healthCheck;
http: typeof http; http: typeof http;
organizations: typeof organizations; organizations: typeof organizations;
@@ -54,8 +60,6 @@ declare const fullApi: ApiFromModules<{
signalRouting: typeof signalRouting; signalRouting: typeof signalRouting;
workArtifacts: typeof workArtifacts; workArtifacts: typeof workArtifacts;
workExecution: typeof workExecution; workExecution: typeof workExecution;
workExecutionAgent: typeof workExecutionAgent;
workExecutionWorkflow: typeof workExecutionWorkflow;
workPlanning: typeof workPlanning; workPlanning: typeof workPlanning;
works: typeof works; works: typeof works;
}>; }>;
+3
View File
@@ -30,10 +30,13 @@ type Env = {
readonly FLUE_URL: string | undefined; readonly FLUE_URL: string | undefined;
readonly GITEA_TOKEN: string | undefined; readonly GITEA_TOKEN: string | undefined;
readonly GITEA_URL: string | undefined; readonly GITEA_URL: string | undefined;
readonly GITEA_WEBHOOK_SECRET: string | undefined;
readonly GITHUB_CLIENT_ID: string | undefined; readonly GITHUB_CLIENT_ID: string | undefined;
readonly GITHUB_CLIENT_SECRET: string | undefined; readonly GITHUB_CLIENT_SECRET: string | undefined;
readonly GITHUB_WEBHOOK_SECRET: string | undefined;
readonly GIT_CREDENTIAL_ENCRYPTION_KEY: string | undefined; readonly GIT_CREDENTIAL_ENCRYPTION_KEY: string | undefined;
readonly NATIVE_APP_URL: string | undefined; readonly NATIVE_APP_URL: string | undefined;
readonly PUTER_GIT_ADMIN_TOKEN: string | undefined;
readonly SITE_URL: string; readonly SITE_URL: string;
}; };
@@ -30,14 +30,14 @@ export const executeAttempt = internalAction({
args: { attemptId: v.id("workAttempts") }, args: { attemptId: v.id("workAttempts") },
handler: async (ctx, args) => { handler: async (ctx, args) => {
const running = await ctx.runMutation( const running = await ctx.runMutation(
internal.workExecutionWorkflow.markAttemptRunning, internal.workExecution.markAttemptRunning,
args args
); );
if (!running) { if (!running) {
throw new ConvexError("Attempt is no longer runnable"); throw new ConvexError("Attempt is no longer runnable");
} }
const context = await ctx.runQuery( const context = await ctx.runQuery(
internal.workExecutionWorkflow.executionContext, internal.workExecution.executionContext,
args args
); );
const credential = await decryptCredential( const credential = await decryptCredential(
+46 -3
View File
@@ -1,5 +1,4 @@
"use node"; import { env } from "@code/env/convex";
import { import {
CREDENTIAL_FRESHNESS_MS, CREDENTIAL_FRESHNESS_MS,
isCredentialFresh, isCredentialFresh,
@@ -15,13 +14,57 @@ import {
query, query,
} from "./_generated/server"; } from "./_generated/server";
import { requireCurrentOrganization } from "./authz"; import { requireCurrentOrganization } from "./authz";
import { decryptCredential } from "./gitConnections";
interface VerifyResult { interface VerifyResult {
readonly lastError?: string; readonly lastError?: string;
readonly state: "active" | "reauth-required" | "unavailable"; readonly state: "active" | "reauth-required" | "unavailable";
} }
const decodeBase64url = (value: string): Uint8Array<ArrayBuffer> => {
const padded = value
.replaceAll("-", "+")
.replaceAll("_", "/")
.padEnd(Math.ceil(value.length / 4) * 4, "=");
const decoded = atob(padded);
const bytes = new Uint8Array(decoded.length);
for (let index = 0; index < decoded.length; index += 1) {
const codePoint = decoded.codePointAt(index);
if (codePoint === undefined) {
throw new ConvexError("Invalid credential encoding");
}
bytes[index] = codePoint;
}
return bytes;
};
const credentialEncryptionKey = async (): Promise<CryptoKey> => {
if (!env.GIT_CREDENTIAL_ENCRYPTION_KEY) {
throw new ConvexError("Git credential encryption is not configured");
}
const bytes = decodeBase64url(env.GIT_CREDENTIAL_ENCRYPTION_KEY);
if (bytes.byteLength !== 32) {
throw new ConvexError("Git credential encryption key must be 32 bytes");
}
return await crypto.subtle.importKey("raw", bytes, "AES-GCM", false, [
"decrypt",
]);
};
const decryptCredential = async (
credentialCiphertext: string,
credentialIv: string
): Promise<string> => {
const decrypted = await crypto.subtle.decrypt(
{
iv: decodeBase64url(credentialIv),
name: "AES-GCM",
},
await credentialEncryptionKey(),
decodeBase64url(credentialCiphertext)
);
return new TextDecoder().decode(decrypted);
};
const verifyGiteaCredential = async ( const verifyGiteaCredential = async (
serverUrl: string, serverUrl: string,
token: string token: string
@@ -1,5 +1,3 @@
"use node";
import { env } from "@code/env/convex"; import { env } from "@code/env/convex";
import { import {
CREDENTIAL_FRESHNESS_MS, CREDENTIAL_FRESHNESS_MS,
-2
View File
@@ -1,5 +1,3 @@
"use node";
import { env } from "@code/env/convex"; import { env } from "@code/env/convex";
import { import {
isProcessedEvent, isProcessedEvent,
+7 -33
View File
@@ -21,6 +21,7 @@ const workStatus = v.union(
v.literal("awaiting-design-approval"), v.literal("awaiting-design-approval"),
v.literal("ready"), v.literal("ready"),
v.literal("executing"), v.literal("executing"),
v.literal("verifying"),
v.literal("needs-input"), v.literal("needs-input"),
v.literal("blocked"), v.literal("blocked"),
v.literal("completed"), v.literal("completed"),
@@ -315,7 +316,6 @@ export default defineSchema({
createdAt: v.number(), createdAt: v.number(),
definitionApprovalVersion: v.optional(v.number()), definitionApprovalVersion: v.optional(v.number()),
definitionVersion: v.optional(v.number()), definitionVersion: v.optional(v.number()),
designApprovalVersion: v.optional(v.number()),
designVersion: v.optional(v.number()), designVersion: v.optional(v.number()),
objective: v.string(), objective: v.string(),
organizationId: v.id("organizations"), organizationId: v.id("organizations"),
@@ -343,18 +343,15 @@ export default defineSchema({
v.literal("work.proposed"), v.literal("work.proposed"),
v.literal("signal.attached"), v.literal("signal.attached"),
v.literal("definition.requested"), v.literal("definition.requested"),
v.literal("definition.approved"),
v.literal("definition.saved"), v.literal("definition.saved"),
v.literal("definition.revised"), v.literal("definition.revised"),
v.literal("definition.approved"),
v.literal("definition.invalidated"),
v.literal("question.created"), v.literal("question.created"),
v.literal("question.answered"), v.literal("question.answered"),
v.literal("question.withdrawn"), v.literal("question.withdrawn"),
v.literal("design.requested"),
v.literal("design.saved"), v.literal("design.saved"),
v.literal("design.revised"),
v.literal("design.approved"), v.literal("design.approved"),
v.literal("design.invalidated"), v.literal("design.revised"),
v.literal("planner.failed"), v.literal("planner.failed"),
v.literal("slice.started"), v.literal("slice.started"),
v.literal("slice.completed"), v.literal("slice.completed"),
@@ -367,6 +364,9 @@ export default defineSchema({
v.literal("attempt.completed"), v.literal("attempt.completed"),
v.literal("attempt.reconciled"), v.literal("attempt.reconciled"),
v.literal("resolver.decided"), v.literal("resolver.decided"),
v.literal("verification.started"),
v.literal("verification.completed"),
v.literal("verification.failed"),
v.literal("artifact.recorded"), v.literal("artifact.recorded"),
v.literal("delivery.recorded"), v.literal("delivery.recorded"),
v.literal("delivery.updated") v.literal("delivery.updated")
@@ -418,23 +418,6 @@ export default defineSchema({
"questionId", "questionId",
]), ]),
workApprovals: defineTable({
approvedAt: v.number(),
approvedBy: v.string(),
definitionVersion: v.number(),
designVersion: v.optional(v.number()),
kind: v.union(v.literal("definition"), v.literal("design")),
status: v.union(v.literal("active"), v.literal("invalidated")),
workId: v.id("works"),
})
.index("by_workId_and_kind", ["workId", "kind"])
.index("by_workId_and_kind_and_definitionVersion_and_designVersion", [
"workId",
"kind",
"definitionVersion",
"designVersion",
]),
designPackets: defineTable({ designPackets: defineTable({
createdAt: v.number(), createdAt: v.number(),
createdBy: v.string(), createdBy: v.string(),
@@ -482,17 +465,8 @@ export default defineSchema({
endedAt: v.optional(v.number()), endedAt: v.optional(v.number()),
kitId: v.string(), kitId: v.string(),
kitVersion: v.string(), kitVersion: v.string(),
scenario: v.optional(v.string()),
environmentId: v.optional(v.string()), environmentId: v.optional(v.string()),
executionKind: v.optional(
v.union(v.literal("simulated"), v.literal("real"))
),
scenario: v.union(
v.literal("success"),
v.literal("transient-failure-then-success"),
v.literal("needs-input"),
v.literal("permanent-failure"),
v.literal("cancelled")
),
sliceId: v.optional(v.string()), sliceId: v.optional(v.string()),
sliceRowId: v.optional(v.id("workSlices")), sliceRowId: v.optional(v.id("workSlices")),
startedAt: v.optional(v.number()), startedAt: v.optional(v.number()),
+104 -334
View File
@@ -1,39 +1,23 @@
import { convexTest } from "convex-test"; import { convexTest } from "convex-test";
import { anyApi, makeFunctionReference } from "convex/server"; import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import type { Id } from "./_generated/dataModel";
import schema from "./schema"; import schema from "./schema";
// Execution-only module set: avoids loading workPlanning (and its env read) so declare global {
// these tests run without Convex env vars. interface ImportMeta {
const modules = { readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
"./_generated/api.ts": () => import("./_generated/api"), }
"./_generated/server.ts": () => import("./_generated/server"), }
"./authz.ts": () => import("./authz"),
"./workExecution.ts": () => import("./workExecution"), const modules = import.meta.glob("./**/*.ts");
};
const identity = { tokenIdentifier: "https://convex.test|exec-user" };
const api = anyApi; const api = anyApi;
const claimAttemptRef = makeFunctionReference< const identity = { tokenIdentifier: "https://convex.test|exec-user" };
"mutation",
{ attemptId: Id<"workAttempts">; leaseMs: number; owner: string },
{ number: number } | null
>("workExecution:claimAttempt");
const makeTest = () => convexTest({ modules, schema }); describe("Work execution without simulation", () => {
type TestT = ReturnType<typeof makeTest>; test("rejects execution when Work is not ready", async () => {
type Scenario =
| "success"
| "transient-failure-then-success"
| "needs-input"
| "permanent-failure"
| "cancelled";
const seedReadyWork = async (options: { secondSlice?: boolean } = {}) => {
const t = convexTest({ modules, schema }); const t = convexTest({ modules, schema });
const { workId } = await t.withIdentity(identity).run(async (ctx) => { await t.withIdentity(identity).run(async (ctx) => {
const organizationId = await ctx.db.insert("organizations", { const organizationId = await ctx.db.insert("organizations", {
createdAt: 1, createdAt: 1,
createdBy: identity.tokenIdentifier, createdBy: identity.tokenIdentifier,
@@ -47,327 +31,113 @@ const seedReadyWork = async (options: { secondSlice?: boolean } = {}) => {
userId: identity.tokenIdentifier, userId: identity.tokenIdentifier,
}); });
const projectId = await ctx.db.insert("projects", { const projectId = await ctx.db.insert("projects", {
createdAt: 1, createdAt: 2,
name: "Zopu", name: "Project",
normalizedSourceUrl: "https://example.com/zopu", normalizedSourceUrl: "https://git.example.com/test/repo",
organizationId, organizationId,
repositoryPath: "puter/zopu", repositoryPath: "test/repo",
sourceHost: "example.com", sourceHost: "gitea",
sourceUrl: "https://example.com/zopu", sourceUrl: "https://git.example.com/test/repo.git",
updatedAt: 1, updatedAt: 2,
}); });
const id = await ctx.db.insert("works", { const workId = await ctx.db.insert("works", {
createdAt: 3,
objective: "Test",
organizationId,
projectId,
status: "proposed",
title: "Proposed Work",
updatedAt: 3,
});
await expect(
t.withIdentity(identity).mutation(api.workExecution.start, { workId })
).rejects.toThrow(/Ready/u);
});
});
test("rejects execution without Definition or Design", async () => {
const t = convexTest({ modules, schema });
await t.withIdentity(identity).run(async (ctx) => {
const organizationId = await ctx.db.insert("organizations", {
createdAt: 1, createdAt: 1,
definitionApprovalVersion: 1, createdBy: identity.tokenIdentifier,
definitionVersion: 1, kind: "personal",
designApprovalVersion: 1, name: "Personal",
designVersion: 1, });
objective: "Prove execution", await ctx.db.insert("organizationMembers", {
createdAt: 1,
organizationId,
role: "owner",
userId: identity.tokenIdentifier,
});
const projectId = await ctx.db.insert("projects", {
createdAt: 2,
name: "Project",
normalizedSourceUrl: "https://git.example.com/test/repo",
organizationId,
repositoryPath: "test/repo",
sourceHost: "gitea",
sourceUrl: "https://git.example.com/test/repo.git",
updatedAt: 2,
});
const workId = await ctx.db.insert("works", {
createdAt: 3,
objective: "Test",
organizationId, organizationId,
projectId, projectId,
status: "ready", status: "ready",
title: "Executable work", title: "Ready but unplanned",
updatedAt: 1, updatedAt: 3,
}); });
await ctx.db.insert("designPackets", { await expect(
t.withIdentity(identity).mutation(api.workExecution.start, { workId })
).rejects.toThrow(/Definition and Design/u);
});
});
test("rejects invalid service token", async () => {
const t = convexTest({ modules, schema });
await t.withIdentity(identity).run(async (ctx) => {
const organizationId = await ctx.db.insert("organizations", {
createdAt: 1, createdAt: 1,
createdBy: "test", createdBy: identity.tokenIdentifier,
kind: "personal",
name: "Personal",
});
await ctx.db.insert("organizationMembers", {
createdAt: 1,
organizationId,
role: "owner",
userId: identity.tokenIdentifier,
});
const projectId = await ctx.db.insert("projects", {
createdAt: 2,
name: "Project",
normalizedSourceUrl: "https://git.example.com/test/repo",
organizationId,
repositoryPath: "test/repo",
sourceHost: "gitea",
sourceUrl: "https://git.example.com/test/repo.git",
updatedAt: 2,
});
const workId = await ctx.db.insert("works", {
createdAt: 3,
definitionVersion: 1, definitionVersion: 1,
payloadJson: "{}",
status: "current",
version: 1,
workId: id,
});
await ctx.db.insert("workSlices", {
createdAt: 1,
designVersion: 1, designVersion: 1,
objective: "execute the slice", objective: "Test",
observableBehavior: "terminal run", organizationId,
ordinal: 0, projectId,
payloadJson: "{}",
sliceId: "slice-1",
status: "ready", status: "ready",
title: "first", title: "Ready Work",
workId: id, updatedAt: 3,
}); });
if (options.secondSlice) {
await ctx.db.insert("workSlices", {
createdAt: 1,
designVersion: 1,
objective: "execute the second slice",
observableBehavior: "second terminal run",
ordinal: 1,
payloadJson: "{}",
sliceId: "slice-2",
status: "planned",
title: "second",
workId: id,
});
}
return { workId: id };
});
return { t, workId };
};
const runAttempt = async (
t: TestT,
attemptId: Id<"workAttempts">,
scenario: Scenario
) => t.action(api.workExecution.executeFakeAttempt, { attemptId, scenario });
const attemptNumber = async (
t: TestT,
runId: Id<"workRuns">,
number: number
): Promise<Id<"workAttempts">> => {
const attempt = await t.run(async (ctx) =>
ctx.db
.query("workAttempts")
.withIndex("by_runId_and_number", (q) =>
q.eq("runId", runId).eq("number", number)
)
.unique()
);
if (!attempt) {
throw new Error(`attempt ${number} not found`);
}
return attempt._id;
};
const snapshot = async (t: TestT, workId: Id<"works">, runId: Id<"workRuns">) =>
t.run(async (ctx) => ({
attempts: await ctx.db
.query("workAttempts")
.withIndex("by_runId_and_number", (q) => q.eq("runId", runId))
.collect(),
run: await ctx.db.get(runId),
slice: await ctx.db
.query("workSlices")
.withIndex("by_workId_and_designVersion", (q) =>
q.eq("workId", workId).eq("designVersion", 1)
)
.first(),
slices: await ctx.db
.query("workSlices")
.withIndex("by_workId_and_designVersion", (q) =>
q.eq("workId", workId).eq("designVersion", 1)
)
.collect(),
work: await ctx.db.get(workId),
}));
describe("simulated execution resolver", () => {
test("success settles Work as completed and marks the slice completed", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
sliceId: "slice-1",
workId,
});
await runAttempt(t, started.attemptId, "success");
const state = await snapshot(t, workId, started.runId);
expect(state.work?.status).toBe("completed");
expect(state.run?.terminalClassification).toBe("Succeeded");
expect(state.attempts).toHaveLength(1);
expect(state.slice?.status).toBe("completed");
const decisions = await t.run(async (ctx) =>
ctx.db.query("resolverDecisions").collect()
);
expect(decisions).toHaveLength(1);
expect(decisions[0]).toMatchObject({
classification: "Succeeded",
decision: "terminal",
resultingWorkStatus: "completed",
});
});
test("completes Work only after every approved slice succeeds", async () => {
const { t, workId } = await seedReadyWork({ secondSlice: true });
const first = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
workId,
});
await runAttempt(t, first.attemptId, "success");
const afterFirst = await snapshot(t, workId, first.runId);
expect(afterFirst.work?.status).toBe("ready");
expect(
afterFirst.slices.find((slice) => slice.sliceId === "slice-1")?.status
).toBe("completed");
expect(
afterFirst.slices.find((slice) => slice.sliceId === "slice-2")?.status
).toBe("ready");
await expect( await expect(
t t.mutation(api.workExecution.start, {
.withIdentity(identity) organizationId,
.mutation(api.workExecution.retrySimulatedExecution, { token: "wrong-token",
runId: first.runId,
})
).rejects.toThrow(/retryable state/u);
const second = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
sliceId: "slice-2",
workId,
});
await runAttempt(t, second.attemptId, "success");
const completed = await snapshot(t, workId, second.runId);
expect(completed.work?.status).toBe("completed");
expect(
completed.slices.every((slice) => slice.status === "completed")
).toBe(true);
});
test("allows only one owner to claim a queued attempt", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
workId,
});
const first = await t.mutation(claimAttemptRef, {
attemptId: started.attemptId,
leaseMs: 60_000,
owner: "worker-a",
});
const second = await t.mutation(claimAttemptRef, {
attemptId: started.attemptId,
leaseMs: 60_000,
owner: "worker-b",
});
expect(first?.number).toBe(1);
expect(second).toBeNull();
});
test("transient failure retries within the kit budget and then succeeds", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "transient-failure-then-success",
workId,
});
await runAttempt(t, started.attemptId, "transient-failure-then-success");
// resolver auto-scheduled attempt 2; drive it manually in the test runtime
const second = await attemptNumber(t, started.runId, 2);
await runAttempt(t, second, "transient-failure-then-success");
const state = await snapshot(t, workId, started.runId);
expect(state.work?.status).toBe("completed");
expect(state.run?.terminalClassification).toBe("Succeeded");
expect(state.attempts).toHaveLength(2);
expect(state.attempts[0]?.classification).toBe("RetryableFailure");
});
test("permanent failure settles Work as failed, not silently runnable", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "permanent-failure",
workId,
});
await runAttempt(t, started.attemptId, "permanent-failure");
const state = await snapshot(t, workId, started.runId);
expect(state.work?.status).toBe("failed");
expect(state.run?.terminalClassification).toBe("PermanentFailure");
});
test("needs-input surfaces a blocker instead of looping", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "needs-input",
workId,
});
await runAttempt(t, started.attemptId, "needs-input");
const state = await snapshot(t, workId, started.runId);
expect(state.work?.status).toBe("needs-input");
expect(state.run?.terminalClassification).toBe("NeedsInput");
});
test("cancellation terminates the attempt and returns Work to ready", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
workId,
});
await t
.withIdentity(identity)
.mutation(api.workExecution.cancelSimulatedExecution, {
runId: started.runId,
});
const state = await snapshot(t, workId, started.runId);
expect(state.work?.status).toBe("ready");
expect(state.run?.status).toBe("cancelled");
expect(state.slice?.status).toBe("ready");
});
test("a slice that is not part of the current approved Design is rejected", async () => {
const { t, workId } = await seedReadyWork();
await expect(
t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
sliceId: "not-a-real-slice",
workId, workId,
}) })
).rejects.toThrow(/current approved Design/u); ).rejects.toThrow(/agent control token/u);
}); });
test("manual retry restarts a failed Run", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "permanent-failure",
workId,
});
await runAttempt(t, started.attemptId, "permanent-failure");
expect((await snapshot(t, workId, started.runId)).work?.status).toBe(
"failed"
);
const retried = await t
.withIdentity(identity)
.mutation(api.workExecution.retrySimulatedExecution, {
runId: started.runId,
});
await runAttempt(t, retried.attemptId as Id<"workAttempts">, "success");
const state = await snapshot(t, workId, started.runId);
expect(state.work?.status).toBe("completed");
expect(retried.number).toBe(2);
});
test("reconciling an expired lease resumes within budget", async () => {
const { t, workId } = await seedReadyWork();
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
workId,
});
// simulate a worker that claimed but died before finishing
await t.run(async (ctx) => {
await ctx.db.patch(started.attemptId, {
leaseExpiresAt: Date.now() - 1000,
leaseOwner: "dead-worker",
status: "claimed",
});
});
await t.mutation(api.workExecution.reconcileExpiredAttempts, {});
const after = await snapshot(t, workId, started.runId);
expect(after.attempts.find((a) => a.number === 1)?.status).toBe("terminal");
expect(
after.attempts.find((a) => a.number === 2 && a.status === "queued")
).toBeTruthy();
}); });
}); });
File diff suppressed because it is too large Load Diff
@@ -1,396 +0,0 @@
import { convexTest } from "convex-test";
import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest";
import schema from "./schema";
const modules = import.meta.glob("./**/*.ts");
const api = anyApi;
type TestContext = ReturnType<typeof convexTest>;
interface Seeded {
attemptId: string;
runId: string;
t: TestContext;
workId: string;
}
// Seed an in-flight real attempt so each mutation under test starts from a
// realistic running state: Work "executing", Run "running", Attempt "running".
const seedRunningAttempt = async (
overrides: {
attemptStatus?: "queued" | "claimed" | "running" | "terminal";
runStatus?: "ready" | "running" | "terminal" | "cancelled";
} = {}
): Promise<Seeded> => {
const t = convexTest({ modules, schema });
const seeded = await t.run(async (ctx) => {
const organizationId = await ctx.db.insert("organizations", {
createdAt: 1,
createdBy: "user",
kind: "personal",
name: "Personal",
});
const projectId = await ctx.db.insert("projects", {
createdAt: 1,
name: "Zopu",
normalizedSourceUrl: "https://github.com/puter/zopu",
organizationId,
repositoryPath: "puter/zopu",
sourceHost: "github.com",
sourceUrl: "https://github.com/puter/zopu",
updatedAt: 1,
});
const workId = await ctx.db.insert("works", {
createdAt: 1,
objective: "Implement a real slice",
organizationId,
projectId,
status: "executing",
title: "Real execution",
updatedAt: 1,
});
const sliceRowId = await ctx.db.insert("workSlices", {
designVersion: 1,
objective: "Change the repo",
observableBehavior: "A diff exists",
ordinal: 0,
payloadJson: "{}",
sliceId: "slice-1",
status: "running",
title: "Implementation",
workId,
});
const runId = await ctx.db.insert("workRuns", {
createdAt: 1,
designVersion: 1,
executionKind: "real",
kitId: "coding-v0",
kitVersion: "1",
scenario: "success",
sliceId: "slice-1",
sliceRowId,
status: overrides.runStatus ?? "running",
workId,
});
const attemptId = await ctx.db.insert("workAttempts", {
number: 1,
runId,
status: overrides.attemptStatus ?? "running",
workId,
workspaceKey: "workspace-1",
});
return { attemptId, runId, workId };
});
return { ...seeded, t };
};
const successResult = {
baseRevision: "base123",
candidateRevision: "candidate456",
changedFiles: ["src/index.ts"],
diff: "+export const ready = true;",
environmentId: "workspace-1",
events: [
{
kind: "runtime.completed",
message: "completed",
metadata: {},
occurredAt: 2,
sequence: 0,
},
],
summary: "Changed one file",
};
describe("real work execution persistence", () => {
test("records revisions, activity, diff, and terminal state atomically", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.mutation(api.workExecutionWorkflow.completeAttempt, {
attemptId: seeded.attemptId,
result: successResult,
});
const state = await t.run(async (ctx) => ({
artifacts: await ctx.db.query("workArtifacts").collect(),
attempt: await ctx.db.get(seeded.attemptId as any),
run: await ctx.db.get(seeded.runId as any),
work: await ctx.db.get(seeded.workId as any),
}));
expect(state.attempt?.classification).toBe("Succeeded");
expect(state.run).toMatchObject({
baseRevision: "base123",
candidateRevision: "candidate456",
terminalClassification: "Succeeded",
});
expect(state.work?.status).toBe("completed");
expect(state.artifacts[0]).toMatchObject({
kind: "diff",
sourceRevision: "candidate456",
});
});
});
describe("classified failure mapping", () => {
test("maps a transient runtime reason to a retryable classification", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.mutation(api.workExecutionWorkflow.failAttempt, {
attemptId: seeded.attemptId,
reason: "ProviderUnavailable",
retryable: true,
summary: "model provider timed out",
});
const attempt = await t.run((ctx) => ctx.db.get(seeded.attemptId as any));
expect(attempt?.classification).toBe("RetryableFailure");
expect(attempt?.failureReason).toBe("ProviderUnavailable");
});
test("queues a new attempt without terminalizing a retryable run", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.mutation(api.workExecutionWorkflow.failAttempt, {
attemptId: seeded.attemptId,
reason: "ProviderUnavailable",
retryable: true,
summary: "provider unavailable",
});
const state = await t.run(async (ctx) => ({
attempts: await ctx.db.query("workAttempts").collect(),
run: await ctx.db.get(seeded.runId as any),
work: await ctx.db.get(seeded.workId as any),
}));
expect(state.attempts).toHaveLength(2);
expect(state.attempts[1]).toMatchObject({
number: 2,
status: "queued",
workspaceKey: "workspace-1",
});
expect(state.run?.status).toBe("running");
expect(state.work?.status).toBe("executing");
});
test("maps an authentication reason to a permanent failure", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.mutation(api.workExecutionWorkflow.failAttempt, {
attemptId: seeded.attemptId,
reason: "Authentication",
retryable: false,
summary: "token rejected",
});
const state = await t.run(async (ctx) => ({
attempt: await ctx.db.get(seeded.attemptId as any),
run: await ctx.db.get(seeded.runId as any),
work: await ctx.db.get(seeded.workId as any),
}));
expect(state.attempt?.classification).toBe("PermanentFailure");
expect(state.attempt?.failureReason).toBe("Authentication");
expect(state.run?.terminalClassification).toBe("PermanentFailure");
expect(state.work?.status).toBe("failed");
});
});
describe("cancellation fencing", () => {
test("a cancelled attempt cannot later settle success", async () => {
const { t, ...seeded } = await seedRunningAttempt();
// Simulate cancellation marking the attempt/run terminal as Cancelled.
await t.run(async (ctx) => {
await ctx.db.patch(seeded.attemptId as any, {
classification: "Cancelled",
endedAt: 5,
status: "terminal",
summary: "Execution cancelled",
});
await ctx.db.patch(seeded.runId as any, {
endedAt: 5,
status: "cancelled",
terminalClassification: "Cancelled",
terminalSummary: "Execution cancelled",
});
});
// A late-arriving completion must be ignored.
await t.mutation(api.workExecutionWorkflow.completeAttempt, {
attemptId: seeded.attemptId,
result: successResult,
});
const attempt = await t.run((ctx) => ctx.db.get(seeded.attemptId as any));
expect(attempt?.classification).toBe("Cancelled");
expect(attempt?.status).toBe("terminal");
});
test("a late failure does not overwrite a cancelled run", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.run(async (ctx) => {
await ctx.db.patch(seeded.attemptId as any, {
classification: "Cancelled",
endedAt: 5,
status: "terminal",
});
await ctx.db.patch(seeded.runId as any, { status: "cancelled" });
});
await t.mutation(api.workExecutionWorkflow.failAttempt, {
attemptId: seeded.attemptId,
reason: "HarnessFailed",
retryable: true,
summary: "late failure",
});
const attempt = await t.run((ctx) => ctx.db.get(seeded.attemptId as any));
expect(attempt?.classification).toBe("Cancelled");
});
});
describe("empty-change rejection", () => {
test("a no-op result with no changed files fails as permanent", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.mutation(api.workExecutionWorkflow.completeAttempt, {
attemptId: seeded.attemptId,
result: {
...successResult,
changedFiles: [],
candidateRevision: "base123",
},
});
const state = await t.run(async (ctx) => ({
attempt: await ctx.db.get(seeded.attemptId as any),
work: await ctx.db.get(seeded.workId as any),
}));
expect(state.attempt?.classification).toBe("PermanentFailure");
expect(state.attempt?.failureReason).toBe("InvalidInput");
expect(state.work?.status).toBe("failed");
});
test("identical base and candidate revisions are rejected", async () => {
const { t, ...seeded } = await seedRunningAttempt();
await t.mutation(api.workExecutionWorkflow.completeAttempt, {
attemptId: seeded.attemptId,
result: {
...successResult,
baseRevision: "same",
candidateRevision: "same",
},
});
const attempt = await t.run((ctx) => ctx.db.get(seeded.attemptId as any));
expect(attempt?.classification).toBe("PermanentFailure");
});
});
describe("attempt re-entry", () => {
test("markAttemptRunning re-claims an already-running attempt", async () => {
const { t, ...seeded } = await seedRunningAttempt();
// First claim (already "running" from seed) must still succeed so a
// workflow replay can re-enter the same live attempt.
const first = await t.mutation(
api.workExecutionWorkflow.markAttemptRunning,
{ attemptId: seeded.attemptId }
);
expect(first).toBe(true);
const attempt = await t.run((ctx) => ctx.db.get(seeded.attemptId as any));
expect(attempt?.status).toBe("running");
expect(attempt?.leaseExpiresAt).toBeGreaterThan(0);
});
test("a terminal attempt is not re-runnable", async () => {
const { t, ...seeded } = await seedRunningAttempt({
attemptStatus: "terminal",
});
const result = await t.mutation(
api.workExecutionWorkflow.markAttemptRunning,
{ attemptId: seeded.attemptId }
);
expect(result).toBe(false);
});
test("a cancelled run cannot re-enter a queued attempt", async () => {
const { t, ...seeded } = await seedRunningAttempt({
attemptStatus: "queued",
runStatus: "cancelled",
});
const result = await t.mutation(
api.workExecutionWorkflow.markAttemptRunning,
{ attemptId: seeded.attemptId }
);
expect(result).toBe(false);
const attempt = await t.run((ctx) => ctx.db.get(seeded.attemptId as any));
expect(attempt?.status).toBe("queued");
});
});
describe("forge credential validation", () => {
const identity = { tokenIdentifier: "https://convex.test|forge-user" };
const seedForgableProject = async (
provider: "github" | "gitea",
sourceHost: string
) => {
const t = convexTest({ modules, schema }).withIdentity(identity);
const ids = await t.run(async (ctx) => {
const organizationId = await ctx.db.insert("organizations", {
createdAt: 1,
createdBy: identity.tokenIdentifier,
kind: "personal",
name: "Personal",
});
await ctx.db.insert("organizationMembers", {
createdAt: 1,
organizationId,
role: "owner",
userId: identity.tokenIdentifier,
});
const projectId = await ctx.db.insert("projects", {
createdAt: 1,
name: "Zopu",
normalizedSourceUrl: `https://${sourceHost}/puter/zopu`,
organizationId,
repositoryPath: "puter/zopu",
sourceHost,
sourceUrl: `https://${sourceHost}/puter/zopu`,
updatedAt: 1,
});
const providerAccountId = await ctx.db.insert("gitProviderAccounts", {
createdAt: 1,
externalAccountId: "ext-1",
externalUsername: "zopu",
organizationId,
provider,
serverUrl: `https://${sourceHost}`,
status: "active",
updatedAt: 1,
userId: identity.tokenIdentifier,
});
const connectionId = await ctx.db.insert("gitConnections", {
connectedAt: 1,
creatorUserId: identity.tokenIdentifier,
credentialCiphertext: "x",
credentialIv: "y",
credentialKind: provider === "github" ? "oauth" : "token",
gitProviderAccountId: providerAccountId,
lastVerifiedAt: Date.now(),
organizationId,
provider,
serverUrl: `https://${sourceHost}`,
state: "active",
updatedAt: 1,
username: "zopu",
});
return { connectionId, projectId };
});
return { t, ids };
};
test("rejects a git connection whose provider mismatches the project forge", async () => {
// Gitea token attached to a GitHub-hosted project.
const { t, ids } = await seedForgableProject("gitea", "github.com");
await expect(
t.mutation(api.gitConnectionData.attachToProject, {
connectionId: ids.connectionId,
projectId: ids.projectId,
})
).rejects.toThrow(/does not match this project.s provider/u);
});
test("accepts a matching provider for the project forge", async () => {
const { t, ids } = await seedForgableProject("github", "github.com");
const result = await t.mutation(api.gitConnectionData.attachToProject, {
connectionId: ids.connectionId,
projectId: ids.projectId,
});
expect(result).toMatchObject({ attached: true });
});
});
@@ -1,654 +0,0 @@
import { WorkAttemptExecutionError } from "@code/primitives/execution-runtime";
import type { WorkAttemptExecutionErrorReason } from "@code/primitives/execution-runtime";
import {
CREDENTIAL_FRESHNESS_MS,
providerForHost as forgeForHost,
} from "@code/primitives/git-provider";
import { defaultCodingKitV0, resolveOutcome } from "@code/primitives/resolver";
import type { AttemptClassification } from "@code/primitives/resolver";
import { WorkflowManager } from "@convex-dev/workflow";
import type { WorkflowId } from "@convex-dev/workflow";
import { ConvexError, v } from "convex/values";
import { components, internal } from "./_generated/api";
import type { Doc, Id } from "./_generated/dataModel";
import { internalMutation, internalQuery, mutation } from "./_generated/server";
import type { MutationCtx } from "./_generated/server";
import { requireProjectMember } from "./authz";
export const workflow = new WorkflowManager(components.workflow);
// Lease window for a running real attempt. Long enough to outlast a single
// agent turn; the workflow's own retry/lease reconciliation covers crashes.
const LEASE_MS = 5 * 60_000;
// Runtime failure reason -> durable attempt classification. The Convex
// workflow stores classifications, not provider-specific reasons, so the
// resolver and Work lifecycle stay forge-agnostic. `retryable` from the
// runtime is carried alongside and consulted by the kit retry policy.
const FAILURE_REASON_CLASSIFICATION = {
Authentication: "PermanentFailure",
Cancelled: "Cancelled",
HarnessFailed: "RetryableFailure",
InvalidInput: "PermanentFailure",
ProviderUnavailable: "RetryableFailure",
RepositoryFailed: "PermanentFailure",
Timeout: "RetryableFailure",
} as const satisfies Record<
WorkAttemptExecutionErrorReason,
AttemptClassification
>;
export const classifyFailure = (
reason: WorkAttemptExecutionErrorReason
): AttemptClassification => FAILURE_REASON_CLASSIFICATION[reason];
// Normalize an error thrown from the agent action into the durable failure
// triple (reason/retryable/summary). WorkAttemptExecutionError is the
// classified runtime failure; anything else is a Convex/infrastructure error
// treated as a transient HarnessFailed so the workflow retry policy decides.
const toExecutionFailure = (
error: unknown
): {
message: string;
reason: WorkAttemptExecutionErrorReason;
retryable: boolean;
} =>
error instanceof WorkAttemptExecutionError
? {
message: error.message,
reason: error.reason,
retryable: error.retryable,
}
: {
message: error instanceof Error ? error.message : "Execution failed",
reason: "HarnessFailed",
retryable: true,
};
const failureReasonValues = v.union(
v.literal("Authentication"),
v.literal("Cancelled"),
v.literal("HarnessFailed"),
v.literal("InvalidInput"),
v.literal("ProviderUnavailable"),
v.literal("RepositoryFailed"),
v.literal("Timeout")
);
const resolveReadySlice = async (
ctx: MutationCtx,
work: Doc<"works">,
sliceId?: string
) => {
if (work.designVersion === undefined) {
throw new ConvexError("Work has no approved Design to execute");
}
const slices = await ctx.db
.query("workSlices")
.withIndex("by_workId_and_designVersion", (q) =>
q.eq("workId", work._id).eq("designVersion", work.designVersion!)
)
.collect();
const slice = sliceId
? slices.find((candidate) => candidate.sliceId === sliceId)
: slices
.sort((left, right) => left.ordinal - right.ordinal)
.find((candidate) => candidate.status === "ready");
if (!slice || slice.status !== "ready") {
throw new ConvexError("Only the next ready slice can be executed");
}
return slice;
};
// Deployment prerequisite gate for real execution. Rejects before any
// workflow/sandbox starts when the project lacks an attached, forge-matched
// Git connection with a cloneable source URL and default branch. Returns the
// validated connection so the caller can pass it to the agent unchanged.
const validateProjectDeployment = async (
ctx: MutationCtx,
work: Doc<"works">
): Promise<Doc<"gitConnections">> => {
const project = await ctx.db.get(work.projectId);
if (!project) {
throw new ConvexError("Project not found");
}
if (!project.gitConnectionId) {
throw new ConvexError("Connect Git credentials to this project first");
}
const connection = await ctx.db.get(project.gitConnectionId);
if (!connection) {
throw new ConvexError("Git connection not found");
}
const expected = forgeForHost(project.sourceHost);
if (expected && connection.provider !== expected) {
throw new ConvexError(
`Git credential provider (${connection.provider}) does not match this project's provider (${expected})`
);
}
if (connection.state !== undefined && connection.state !== "active") {
throw new ConvexError(
`Git connection is ${connection.state}; verify or reconnect credentials before execution`
);
}
const now = Date.now();
if (
connection.lastVerifiedAt === undefined ||
now - connection.lastVerifiedAt > CREDENTIAL_FRESHNESS_MS
) {
throw new ConvexError(
"Git credentials have not been verified recently; run a connection health check before execution"
);
}
return connection;
};
export const execute = workflow
.define({ args: { attemptId: v.id("workAttempts") } })
.handler(async (step, args): Promise<void> => {
try {
const result = await step.runAction(
internal.workExecutionAgent.executeAttempt,
args,
{ retry: true }
);
await step.runMutation(internal.workExecutionWorkflow.completeAttempt, {
attemptId: args.attemptId,
result: {
...result,
changedFiles: [...result.changedFiles],
events: result.events.map((item) => ({
...item,
metadata: { ...item.metadata },
})),
},
});
} catch (error) {
const failure = toExecutionFailure(error);
await step.runMutation(internal.workExecutionWorkflow.failAttempt, {
attemptId: args.attemptId,
reason: failure.reason,
retryable: failure.retryable,
summary: failure.message,
});
}
});
export const startExecution = mutation({
args: {
sliceId: v.optional(v.string()),
workId: v.id("works"),
},
handler: async (
ctx,
args
): Promise<{
attemptId: Id<"workAttempts">;
runId: Id<"workRuns">;
workflowId: WorkflowId;
}> => {
const work = await ctx.db.get(args.workId);
if (!work) {
throw new ConvexError("Work not found");
}
await requireProjectMember(ctx, work.projectId);
if (work.status !== "ready") {
throw new ConvexError("Work must be Ready before execution");
}
if (
work.definitionApprovalVersion !== work.definitionVersion ||
work.designApprovalVersion !== work.designVersion
) {
throw new ConvexError("Execution requires exact approved versions");
}
await validateProjectDeployment(ctx, work);
const slice = await resolveReadySlice(ctx, work, args.sliceId);
const createdAt = Date.now();
const runId = await ctx.db.insert("workRuns", {
createdAt,
designVersion: slice.designVersion,
executionKind: "real",
kitId: defaultCodingKitV0.id,
kitVersion: defaultCodingKitV0.version,
scenario: "success",
sliceId: slice.sliceId,
sliceRowId: slice._id,
status: "running",
workId: work._id,
});
const workspaceKey = `work-${work._id}-run-${runId}`;
const attemptId = await ctx.db.insert("workAttempts", {
number: 1,
runId,
status: "queued",
workId: work._id,
workspaceKey,
});
const workflowId: WorkflowId = await workflow.start(
ctx,
internal.workExecutionWorkflow.execute,
{ attemptId }
);
await ctx.db.patch(runId, { startedAt: createdAt, workflowId });
await ctx.db.patch(slice._id, { status: "running" });
await ctx.db.patch(work._id, { status: "executing", updatedAt: createdAt });
await ctx.db.insert("workEvents", {
createdAt,
idempotencyKey: `real-run-started:${runId}`,
kind: "run.started",
referenceId: String(runId),
workId: work._id,
});
return { attemptId, runId, workflowId };
},
});
export const executionContext = internalQuery({
args: { attemptId: v.id("workAttempts") },
handler: async (ctx, args) => {
const attempt = await ctx.db.get(args.attemptId);
if (!attempt) {
throw new ConvexError("Attempt not found");
}
const run = await ctx.db.get(attempt.runId);
const work = await ctx.db.get(attempt.workId);
if (!run || !work || !run.sliceRowId) {
throw new ConvexError("Execution records are incomplete");
}
const [slice, project] = await Promise.all([
ctx.db.get(run.sliceRowId),
ctx.db.get(work.projectId),
]);
if (!slice || !project?.gitConnectionId) {
throw new ConvexError("Project execution configuration is incomplete");
}
const connection = await ctx.db.get(project.gitConnectionId);
if (!connection) {
throw new ConvexError("Git connection not found");
}
const expectedForge = forgeForHost(project.sourceHost);
if (expectedForge && connection.provider !== expectedForge) {
throw new ConvexError(
`Git credential provider (${connection.provider}) does not match this project's provider (${expectedForge})`
);
}
if (connection.state !== undefined && connection.state !== "active") {
throw new ConvexError(
`Git connection is ${connection.state}; execution is blocked`
);
}
return {
attempt,
connection,
project,
prompt: [
`Implement this approved Zopu slice: ${slice.title}`,
`Objective: ${slice.objective}`,
`Observable behavior: ${slice.observableBehavior}`,
"Inspect the repository instructions first. Make focused changes and run relevant checks.",
].join("\n\n"),
run,
work,
};
},
});
export const markAttemptRunning = internalMutation({
args: { attemptId: v.id("workAttempts") },
handler: async (ctx, args) => {
const attempt = await ctx.db.get(args.attemptId);
if (!attempt || attempt.status === "terminal") {
return false;
}
const run = await ctx.db.get(attempt.runId);
if (!run || run.status !== "running") {
return false;
}
await ctx.db.patch(attempt._id, {
leaseExpiresAt: Date.now() + LEASE_MS,
startedAt: attempt.startedAt ?? Date.now(),
status: "running",
});
return true;
},
});
const settleSliceAndWork = async (
ctx: MutationCtx,
run: Doc<"workRuns">,
succeeded: boolean,
workStatus: Doc<"works">["status"]
) => {
if (!run.sliceRowId) {
return;
}
const slice = await ctx.db.get(run.sliceRowId);
if (!slice) {
return;
}
let sliceStatus: "blocked" | "completed" | "ready" = "ready";
if (succeeded) {
sliceStatus = "completed";
} else if (workStatus === "blocked") {
sliceStatus = "blocked";
}
await ctx.db.patch(slice._id, { status: sliceStatus });
const work = await ctx.db.get(run.workId);
if (!work) {
return;
}
let status = workStatus;
if (succeeded) {
const slices = await ctx.db
.query("workSlices")
.withIndex("by_workId_and_designVersion", (q) =>
q.eq("workId", work._id).eq("designVersion", slice.designVersion)
)
.collect();
const next = slices
.sort((left, right) => left.ordinal - right.ordinal)
.find((candidate) => candidate.status === "planned");
if (next) {
await ctx.db.patch(next._id, { status: "ready" });
status = "ready";
}
}
await ctx.db.patch(work._id, { status, updatedAt: Date.now() });
};
export const completeAttempt = internalMutation({
args: {
attemptId: v.id("workAttempts"),
result: v.object({
baseRevision: v.string(),
candidateRevision: v.string(),
changedFiles: v.array(v.string()),
diff: v.string(),
environmentId: v.string(),
events: v.array(
v.object({
kind: v.string(),
message: v.string(),
metadata: v.record(v.string(), v.string()),
occurredAt: v.number(),
sequence: v.number(),
})
),
summary: v.string(),
}),
},
handler: async (ctx, args) => {
const attempt = await ctx.db.get(args.attemptId);
// Cancellation fencing: a terminal or cancelled attempt/run must never
// later settle success, even if the agent action raced to completion.
if (!attempt || attempt.status === "terminal") {
return;
}
const run = await ctx.db.get(attempt.runId);
const work = await ctx.db.get(attempt.workId);
if (!run || !work) {
throw new ConvexError("Execution records not found");
}
if (run.status === "terminal" || run.status === "cancelled") {
return;
}
// Empty-change rejection: a no-op result (no changed files, or identical
// base/candidate revisions) is not a successful implementation. Fail it
// as a permanent InvalidInput so the resolver does not mark Work done.
const noOp =
args.result.changedFiles.length === 0 ||
args.result.baseRevision === args.result.candidateRevision;
if (noOp) {
await ctx.db.patch(attempt._id, {
classification: "PermanentFailure",
endedAt: Date.now(),
failureReason: "InvalidInput",
leaseExpiresAt: undefined,
status: "terminal",
summary: "Execution produced no repository changes",
});
await ctx.db.patch(run._id, {
endedAt: Date.now(),
status: "terminal",
terminalClassification: "PermanentFailure",
terminalSummary: "Execution produced no repository changes",
});
await settleSliceAndWork(ctx, run, false, "failed");
return;
}
for (const item of args.result.events) {
await ctx.db.insert("workAttemptEvents", {
attemptId: attempt._id,
kind: item.kind,
message: item.message,
metadataJson: JSON.stringify(item.metadata),
occurredAt: item.occurredAt,
sequence: item.sequence,
});
}
const endedAt = Date.now();
await ctx.db.patch(attempt._id, {
classification: "Succeeded",
endedAt,
leaseExpiresAt: undefined,
status: "terminal",
summary: args.result.summary,
});
await ctx.db.patch(run._id, {
baseRevision: args.result.baseRevision,
candidateRevision: args.result.candidateRevision,
endedAt,
environmentId: args.result.environmentId,
status: "terminal",
terminalClassification: "Succeeded",
terminalSummary: args.result.summary,
});
await ctx.db.insert("workArtifacts", {
attemptId: attempt._id,
createdAt: endedAt,
designVersion: run.designVersion,
environmentId: args.result.environmentId,
idempotencyKey: `real-diff:${attempt._id}`,
kind: "diff",
metadataJson: JSON.stringify({ changedFiles: args.result.changedFiles }),
organizationId: work.organizationId,
producer: "agentos-pi",
projectId: work.projectId,
provenanceJson: JSON.stringify({
baseRevision: args.result.baseRevision,
}),
runId: run._id,
sliceId: run.sliceId,
sourceRevision: args.result.candidateRevision,
title: "Implementation diff",
verificationStatus: "unverified",
workId: work._id,
...(args.result.diff.length > 0
? {
uri: `data:text/plain;charset=utf-8,${encodeURIComponent(args.result.diff.slice(0, 50_000))}`,
}
: {}),
});
await settleSliceAndWork(ctx, run, true, "completed");
await ctx.db.insert("workEvents", {
createdAt: endedAt,
idempotencyKey: `real-run-completed:${run._id}`,
kind: "run.completed",
payloadJson: JSON.stringify({ classification: "Succeeded" }),
referenceId: String(run._id),
workId: work._id,
});
},
});
export const launchAttemptWorkflow = internalMutation({
args: { attemptId: v.id("workAttempts") },
handler: async (ctx, args) => {
const attempt = await ctx.db.get(args.attemptId);
if (!attempt || attempt.status === "terminal") {
return;
}
const run = await ctx.db.get(attempt.runId);
if (!run || run.status !== "running") {
return;
}
const workflowId: WorkflowId = await workflow.start(
ctx,
internal.workExecutionWorkflow.execute,
args
);
await ctx.db.patch(run._id, { workflowId });
},
});
export const failAttempt = internalMutation({
args: {
attemptId: v.id("workAttempts"),
reason: failureReasonValues,
retryable: v.boolean(),
summary: v.string(),
},
handler: async (ctx, args) => {
const attempt = await ctx.db.get(args.attemptId);
// Cancellation fencing: once an attempt or run is terminal/cancelled, a
// late failure from the workflow catch block must not overwrite the
// settled state. This also prevents a cancelled attempt from settling
// as a different classification after cancelExecution ran.
if (!attempt || attempt.status === "terminal") {
return;
}
const run = await ctx.db.get(attempt.runId);
if (!run || run.status === "terminal" || run.status === "cancelled") {
return;
}
const classification = classifyFailure(args.reason);
const endedAt = Date.now();
await ctx.db.patch(attempt._id, {
classification,
endedAt,
failureReason: args.reason,
leaseExpiresAt: undefined,
status: "terminal",
summary: args.summary,
});
const outcome = resolveOutcome(
{ classification, retryable: args.retryable, summary: args.summary },
attempt.number,
defaultCodingKitV0.retryPolicy
);
await ctx.db.insert("resolverDecisions", {
attemptId: attempt._id,
attemptNumber: attempt.number,
classification,
createdAt: endedAt,
decision: outcome.kind,
...(outcome.kind === "terminal"
? { resultingWorkStatus: outcome.workStatus }
: {}),
runId: run._id,
summary: args.summary,
workId: attempt.workId,
});
if (outcome.kind === "retry") {
const nextAttemptId = await ctx.db.insert("workAttempts", {
number: attempt.number + 1,
runId: run._id,
status: "queued",
workId: attempt.workId,
workspaceKey: attempt.workspaceKey,
});
await ctx.scheduler.runAfter(
0,
internal.workExecutionWorkflow.launchAttemptWorkflow,
{ attemptId: nextAttemptId }
);
return;
}
await ctx.db.patch(run._id, {
endedAt,
status: "terminal",
terminalClassification: classification,
terminalSummary: args.summary,
});
await settleSliceAndWork(ctx, run, false, outcome.workStatus);
const work = await ctx.db.get(run.workId);
if (work) {
await ctx.db.insert("workEvents", {
createdAt: endedAt,
idempotencyKey: `real-run-failed:${run._id}`,
kind: "run.completed",
payloadJson: JSON.stringify({ classification }),
referenceId: String(run._id),
workId: work._id,
});
}
},
});
export const cancelExecution = mutation({
args: { runId: v.id("workRuns") },
handler: async (ctx, args) => {
const run = await ctx.db.get(args.runId);
if (!run) {
throw new ConvexError("Run not found");
}
await requireProjectMember(ctx, (await ctx.db.get(run.workId))!.projectId);
if (run.status !== "running") {
return { cancelled: false };
}
if (run.workflowId) {
await workflow.cancel(ctx, run.workflowId as WorkflowId);
}
const attempts = await ctx.db
.query("workAttempts")
.withIndex("by_runId_and_number", (q) => q.eq("runId", run._id))
.collect();
const attempt = attempts.find(
(candidate) => candidate.status !== "terminal"
);
if (attempt?.workspaceKey) {
await ctx.scheduler.runAfter(
0,
internal.workExecutionAgent.cancelAttempt,
{
attemptId: String(attempt._id),
workspaceKey: attempt.workspaceKey,
}
);
const cancelledAt = Date.now();
await ctx.db.patch(attempt._id, {
classification: "Cancelled",
endedAt: cancelledAt,
failureReason: "Cancelled",
leaseExpiresAt: undefined,
status: "terminal",
summary: "Execution cancelled",
});
await ctx.db.insert("resolverDecisions", {
attemptId: attempt._id,
attemptNumber: attempt.number,
classification: "Cancelled",
createdAt: cancelledAt,
decision: "terminal",
resultingWorkStatus: "ready",
runId: run._id,
summary: "Execution cancelled",
workId: run.workId,
});
}
await ctx.db.patch(run._id, {
endedAt: Date.now(),
status: "cancelled",
terminalClassification: "Cancelled",
terminalSummary: "Execution cancelled",
});
if (run.sliceRowId) {
await ctx.db.patch(run.sliceRowId, { status: "ready" });
}
const work = await ctx.db.get(run.workId);
if (work) {
await ctx.db.patch(work._id, { status: "ready", updatedAt: Date.now() });
}
return { cancelled: true };
},
});
+317 -334
View File
@@ -1,8 +1,7 @@
import { convexTest } from "convex-test"; import { convexTest } from "convex-test";
import { anyApi, makeFunctionReference } from "convex/server"; import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import type { Id } from "./_generated/dataModel";
import schema from "./schema"; import schema from "./schema";
declare global { declare global {
@@ -13,16 +12,10 @@ declare global {
const modules = import.meta.glob("./**/*.ts"); const modules = import.meta.glob("./**/*.ts");
const api = anyApi; const api = anyApi;
const executeFakeAttemptRef = makeFunctionReference<
"action",
{ attemptId: string; scenario: "success" }
>("workExecution:executeFakeAttempt");
const identity = { tokenIdentifier: "https://convex.test|planner-user" }; const identity = { tokenIdentifier: "https://convex.test|planner-user" };
describe("Work planning and execution commands", () => { const seedProject = async (t: ReturnType<typeof convexTest>) =>
test("binds approvals to exact versions and admits a fake Run only from Ready", async () => { await t.withIdentity(identity).run(async (ctx) => {
const t = convexTest({ modules, schema });
const fixture = await t.withIdentity(identity).run(async (ctx) => {
const organizationId = await ctx.db.insert("organizations", { const organizationId = await ctx.db.insert("organizations", {
createdAt: 1, createdAt: 1,
createdBy: identity.tokenIdentifier, createdBy: identity.tokenIdentifier,
@@ -36,379 +29,369 @@ describe("Work planning and execution commands", () => {
userId: identity.tokenIdentifier, userId: identity.tokenIdentifier,
}); });
const projectId = await ctx.db.insert("projects", { const projectId = await ctx.db.insert("projects", {
createdAt: 1, createdAt: 2,
name: "Zopu", defaultBranch: "main",
normalizedSourceUrl: "https://example.com/zopu", name: "Test Project",
normalizedSourceUrl: "https://git.example.com/test/repo",
organizationId, organizationId,
repositoryPath: "puter/zopu", repositoryPath: "test/repo",
sourceHost: "example.com", sourceHost: "gitea",
sourceUrl: "https://example.com/zopu", sourceUrl: "https://git.example.com/test/repo.git",
updatedAt: 1, updatedAt: 2,
}); });
const workId = await ctx.db.insert("works", { return { organizationId, projectId };
createdAt: 1, });
objective: "Prove the flow",
const seedReadyWork = async (
t: ReturnType<typeof convexTest>,
organizationId: string,
projectId: string,
workIdOverride?: string
) =>
await t.withIdentity(identity).run(async (ctx) => {
const workId =
workIdOverride ??
(await ctx.db.insert("works", {
createdAt: 3,
definitionVersion: 1,
designVersion: 1,
objective: "Build it",
organizationId,
projectId,
status: "ready",
title: "Ready Work",
updatedAt: 3,
}));
await ctx.db.insert("workDefinitions", {
createdAt: 3,
createdBy: "test",
payloadJson: JSON.stringify({
acceptanceCriteria: ["Works"],
assumptions: [],
constraints: [],
desiredOutcome: "Ship",
inScope: [],
outOfScope: [],
problem: "Missing",
questions: [],
requiredArtifacts: [],
risk: "low",
version: 1,
}),
risk: "low",
status: "current",
version: 1,
workId,
});
await ctx.db.insert("designPackets", {
createdAt: 3,
createdBy: "test",
definitionVersion: 1,
payloadJson: JSON.stringify({
architectureSummary: "Simple",
callFlowDelta: [],
concerns: [],
definitionVersion: 1,
evidenceRequirements: ["Tests pass"],
fileTreeDelta: [],
impactMap: {
files: [],
modules: [],
risks: [],
summary: "Low",
},
invariants: ["No breaking changes"],
keyInterfaces: [],
slices: [
{
codeBoundaries: ["src/index.ts"],
dependsOn: [],
evidenceRequirements: ["Build succeeds"],
id: "s1",
objective: "Implement",
observableBehavior: "Feature works",
reviewRequired: false,
title: "Slice 1",
verification: ["Run tests"],
},
],
tradeoffs: [],
version: 1,
}),
status: "current",
version: 1,
workId,
});
await ctx.db.insert("workSlices", {
createdAt: 3,
designVersion: 1,
objective: "Implement",
observableBehavior: "It works",
ordinal: 0,
payloadJson: "{}",
sliceId: "s1",
status: "ready",
title: "Slice 1",
workId,
});
return workId;
});
describe("Work planning without approvals", () => {
test("creates proposed Work and starts Definition planning on request", async () => {
const t = convexTest({ modules, schema });
const { organizationId, projectId } = await seedProject(t);
const workId = await t.run(
async (ctx) =>
await ctx.db.insert("works", {
createdAt: 4,
objective: "Implement the feature",
organizationId, organizationId,
projectId, projectId,
status: "proposed", status: "proposed",
title: "Executable work", title: "Feature Work",
updatedAt: 1, updatedAt: 4,
}); })
return { projectId, workId }; );
});
await t await t
.withIdentity(identity) .withIdentity(identity)
.mutation(api.workPlanning.requestDefinition, { workId: fixture.workId }); .mutation(api.workPlanning.requestDefinition, {
const definition = await t workId,
.withIdentity(identity)
.mutation(api.workPlanning.saveDefinitionProposal, {
payloadJson: JSON.stringify({
acceptanceCriteria: ["terminal"],
affectedUsers: ["user"],
assumptions: [],
constraints: [],
desiredOutcome: "A terminal simulation",
inScope: ["fake"],
outOfScope: [],
problem: "Need a proof",
questions: [],
requiredArtifacts: ["events"],
risk: "low",
}),
workId: fixture.workId,
}); });
await t const work = await t.run(async (ctx) => await ctx.db.get(workId));
.withIdentity(identity) expect(work?.status).toBe("defining");
.mutation(api.workPlanning.approveDefinition, {
version: definition.version,
workId: fixture.workId,
}); });
const design = await t
.withIdentity(identity)
.mutation(api.workPlanning.saveDesignProposal, {
payloadJson: JSON.stringify({
architectureSummary: "fake",
callFlowDelta: [],
concerns: [],
evidenceRequirements: ["event"],
fileTreeDelta: [],
impactMap: { files: [], modules: [], risks: [], summary: "small" },
invariants: ["terminal"],
keyInterfaces: [],
slices: [
{
codeBoundaries: ["runtime"],
dependsOn: [],
evidenceRequirements: ["event"],
id: "slice-1",
objective: "fake",
observableBehavior: "event",
reviewRequired: false,
title: "fake",
verification: ["assert"],
},
],
tradeoffs: [],
}),
workId: fixture.workId,
});
await t.withIdentity(identity).mutation(api.workPlanning.approveDesign, {
definitionVersion: definition.version,
designVersion: design.version,
workId: fixture.workId,
});
const started = await t
.withIdentity(identity)
.mutation(api.workExecution.startSimulatedExecution, {
scenario: "success",
sliceId: "slice-1",
workId: fixture.workId,
});
expect(started.runId).toBeTruthy();
const state = await t.run(async (ctx) => ctx.db.get(fixture.workId));
expect(state?.status).toBe("executing");
expect(state?.definitionApprovalVersion).toBe(definition.version);
expect(state?.designApprovalVersion).toBe(design.version);
await t.action(executeFakeAttemptRef, {
attemptId: String(started.attemptId),
scenario: "success",
});
const completed = await t.run(async (ctx) => ({
attempt: await ctx.db
.query("workAttempts")
.withIndex("by_runId_and_number", (q) => q.eq("runId", started.runId))
.unique(),
events: await ctx.db.query("workAttemptEvents").collect(),
run: await ctx.db.get(started.runId as Id<"workRuns">),
work: await ctx.db.get(fixture.workId),
}));
expect(completed.work?.status).toBe("completed");
expect(completed.run?.terminalClassification).toBe("Succeeded");
expect(completed.attempt?.status).toBe("terminal");
expect(completed.events.length).toBeGreaterThan(0);
});
});
const validDefinitionPayload = () => ({ test("Definition persistence auto-advances to Design phase", async () => {
acceptanceCriteria: ["terminal outcome"],
affectedUsers: ["user"],
assumptions: [],
constraints: [],
desiredOutcome: "A terminal simulation",
inScope: ["fake"],
outOfScope: [],
problem: "Need a proof",
questions: [],
requiredArtifacts: ["events"],
risk: "low",
});
const validDesignPayload = (sliceId = "slice-1") => ({
architectureSummary: "fake",
callFlowDelta: [],
concerns: [],
evidenceRequirements: ["event"],
fileTreeDelta: [],
impactMap: { files: [], modules: [], risks: [], summary: "small" },
invariants: ["terminal"],
keyInterfaces: [],
slices: [
{
codeBoundaries: ["runtime"],
dependsOn: [],
evidenceRequirements: ["event"],
id: sliceId,
objective: "fake",
observableBehavior: "event",
reviewRequired: false,
title: "fake",
verification: ["assert"],
},
],
tradeoffs: [],
});
describe("Work planning guards", () => {
test("persists high-impact questions but blocks approval", async () => {
const t = convexTest({ modules, schema }); const t = convexTest({ modules, schema });
const { workId } = await t.withIdentity(identity).run(async (ctx) => { const { organizationId, projectId } = await seedProject(t);
const organizationId = await ctx.db.insert("organizations", { const workId = await t.run(
async (ctx) =>
await ctx.db.insert("works", {
createdAt: 1, createdAt: 1,
createdBy: identity.tokenIdentifier, objective: "Build it",
kind: "personal",
name: "Personal",
});
await ctx.db.insert("organizationMembers", {
createdAt: 1,
organizationId,
role: "owner",
userId: identity.tokenIdentifier,
});
const projectId = await ctx.db.insert("projects", {
createdAt: 1,
name: "Zopu",
normalizedSourceUrl: "https://example.com/zopu",
organizationId,
repositoryPath: "puter/zopu",
sourceHost: "example.com",
sourceUrl: "https://example.com/zopu",
updatedAt: 1,
});
const id = await ctx.db.insert("works", {
createdAt: 1,
objective: "Resolve a critical question",
organizationId, organizationId,
projectId, projectId,
status: "defining", status: "defining",
title: "Question gate", title: "Auto-advance test",
updatedAt: 1, updatedAt: 1,
}); })
return { workId: id }; );
}); const definition = {
const saved = await t acceptanceCriteria: ["Works end to end"],
assumptions: [],
constraints: [],
desiredOutcome: "Feature is live",
inScope: ["Backend"],
outOfScope: ["Mobile"],
problem: "Missing feature",
questions: [],
requiredArtifacts: ["PR"],
risk: "low",
};
await t
.withIdentity(identity) .withIdentity(identity)
.mutation(api.workPlanning.saveDefinitionProposal, { .mutation(api.workPlanning.saveDefinitionProposal, {
payloadJson: JSON.stringify({ payloadJson: JSON.stringify(definition),
...validDefinitionPayload(),
questions: [
{
alternatives: ["public", "private"],
id: "visibility",
impact: "high",
prompt: "Should the endpoint be public?",
status: "open",
},
],
}),
workId, workId,
}); });
await expect( const work = await t.run(async (ctx) => await ctx.db.get(workId));
t.withIdentity(identity).mutation(api.workPlanning.approveDefinition, { expect(work?.status).toBe("designing");
version: saved.version, expect(work?.definitionVersion).toBe(1);
workId,
})
).rejects.toThrow(/High-impact open questions/u);
const stored = await t.run(async (ctx) =>
ctx.db.query("workQuestions").collect()
);
expect(stored).toHaveLength(1);
expect(stored[0]?.status).toBe("open");
}); });
test("keeps prior Design slices as versioned history", async () => { test("Design persistence makes Work ready for execution", async () => {
const t = convexTest({ modules, schema }); const t = convexTest({ modules, schema });
const { workId } = await t.withIdentity(identity).run(async (ctx) => { const { organizationId, projectId } = await seedProject(t);
const organizationId = await ctx.db.insert("organizations", { const workId = await t.run(
async (ctx) =>
await ctx.db.insert("works", {
createdAt: 1, createdAt: 1,
createdBy: identity.tokenIdentifier,
kind: "personal",
name: "Personal",
});
await ctx.db.insert("organizationMembers", {
createdAt: 1,
organizationId,
role: "owner",
userId: identity.tokenIdentifier,
});
const projectId = await ctx.db.insert("projects", {
createdAt: 1,
name: "Zopu",
normalizedSourceUrl: "https://example.com/zopu",
organizationId,
repositoryPath: "puter/zopu",
sourceHost: "example.com",
sourceUrl: "https://example.com/zopu",
updatedAt: 1,
});
const id = await ctx.db.insert("works", {
createdAt: 1,
definitionApprovalVersion: 1,
definitionVersion: 1, definitionVersion: 1,
objective: "Preserve Design history", objective: "Build it",
organizationId, organizationId,
projectId, projectId,
status: "designing", status: "designing",
title: "Versioned Design", title: "Ready test",
updatedAt: 1, updatedAt: 1,
}); })
return { workId: id };
});
await t
.withIdentity(identity)
.mutation(api.workPlanning.saveDesignProposal, {
payloadJson: JSON.stringify(validDesignPayload("slice-v1")),
workId,
});
await t
.withIdentity(identity)
.mutation(api.workPlanning.saveDesignProposal, {
payloadJson: JSON.stringify(validDesignPayload("slice-v2")),
workId,
});
const slices = await t.run(async (ctx) =>
ctx.db.query("workSlices").collect()
); );
expect(slices.map((slice) => slice.designVersion).sort()).toEqual([1, 2]); await t.run(async (ctx) => {
await ctx.db.insert("workDefinitions", {
createdAt: 2,
createdBy: "test",
payloadJson: JSON.stringify({
acceptanceCriteria: [],
assumptions: [],
constraints: [],
desiredOutcome: "Ship",
inScope: [],
outOfScope: [],
problem: "Missing",
questions: [],
requiredArtifacts: [],
risk: "low",
version: 1,
}),
risk: "low",
status: "current",
version: 1,
workId,
});
});
const design = {
architectureSummary: "Simple",
callFlowDelta: [],
concerns: [],
evidenceRequirements: ["Tests pass"],
fileTreeDelta: [],
impactMap: {
files: [],
modules: [],
risks: [],
summary: "Low",
},
invariants: ["No breaking changes"],
keyInterfaces: [],
slices: [
{
codeBoundaries: ["src/index.ts"],
dependsOn: [],
evidenceRequirements: ["Build succeeds"],
id: "s1",
objective: "Implement",
observableBehavior: "Feature works",
reviewRequired: false,
title: "Slice 1",
verification: ["Run tests"],
},
],
tradeoffs: [],
};
await t
.withIdentity(identity)
.mutation(api.workPlanning.saveDesignProposal, {
payloadJson: JSON.stringify(design),
workId,
});
const work = await t.run(async (ctx) => await ctx.db.get(workId));
expect(work?.status).toBe("ready");
expect(work?.designVersion).toBe(1);
}); });
test("a Definition cannot be revised while a Run is executing", async () => { test("revising a Definition invalidates existing Design and restarts planning", async () => {
const t = convexTest({ modules, schema }); const t = convexTest({ modules, schema });
const { workId } = await t.withIdentity(identity).run(async (ctx) => { const { organizationId, projectId } = await seedProject(t);
const organizationId = await ctx.db.insert("organizations", { const workId = await t.run(
async (ctx) =>
await ctx.db.insert("works", {
createdAt: 1, createdAt: 1,
createdBy: identity.tokenIdentifier,
kind: "personal",
name: "Personal",
});
await ctx.db.insert("organizationMembers", {
createdAt: 1,
organizationId,
role: "owner",
userId: identity.tokenIdentifier,
});
const projectId = await ctx.db.insert("projects", {
createdAt: 1,
name: "Zopu",
normalizedSourceUrl: "https://example.com/zopu",
organizationId,
repositoryPath: "puter/zopu",
sourceHost: "example.com",
sourceUrl: "https://example.com/zopu",
updatedAt: 1,
});
const id = await ctx.db.insert("works", {
createdAt: 1,
definitionApprovalVersion: 1,
definitionVersion: 1, definitionVersion: 1,
designApprovalVersion: 1,
designVersion: 1, designVersion: 1,
objective: "Prove the guard", objective: "Build it",
organizationId, organizationId,
projectId, projectId,
status: "executing", status: "designing",
title: "Guarded work", title: "Revision test",
updatedAt: 1, updatedAt: 1,
})
);
await t.run(async (ctx) => {
await ctx.db.insert("designPackets", {
createdAt: 2,
createdBy: "test",
definitionVersion: 1,
payloadJson: "{}",
status: "current",
version: 1,
workId,
}); });
return { workId: id };
}); });
await expect( const revisedDefinition = {
t acceptanceCriteria: ["Works"],
assumptions: [],
constraints: [],
desiredOutcome: "Ship",
inScope: ["All"],
outOfScope: [],
problem: "Missing",
questions: [],
requiredArtifacts: [],
risk: "medium",
};
await t
.withIdentity(identity) .withIdentity(identity)
.mutation(api.workPlanning.saveDefinitionProposal, { .mutation(api.workPlanning.saveDefinitionProposal, {
payloadJson: JSON.stringify(validDefinitionPayload()), payloadJson: JSON.stringify(revisedDefinition),
workId, workId,
}) });
).rejects.toThrow(/while a Run is executing/u); const work = await t.run(async (ctx) => await ctx.db.get(workId));
expect(work?.status).toBe("designing");
expect(work?.definitionVersion).toBe(2);
expect(work?.designVersion).toBeUndefined();
const designs = await t.run(
async (ctx) =>
await ctx.db
.query("designPackets")
.withIndex("by_work_and_version", (q) => q.eq("workId", workId))
.collect()
);
expect(designs.every((d) => d.status === "superseded")).toBe(true);
}); });
test("planner questions are persisted as durable Work questions", async () => { test("rejects execution when Work is not ready", async () => {
const t = convexTest({ modules, schema }); const t = convexTest({ modules, schema });
const { workId } = await t.withIdentity(identity).run(async (ctx) => { const { organizationId, projectId } = await seedProject(t);
const organizationId = await ctx.db.insert("organizations", { const workId = await t.run(
createdAt: 1, async (ctx) =>
createdBy: identity.tokenIdentifier, await ctx.db.insert("works", {
kind: "personal", createdAt: 3,
name: "Personal", objective: "Test",
});
const projectId = await ctx.db.insert("projects", {
createdAt: 1,
name: "Zopu",
normalizedSourceUrl: "https://example.com/zopu",
organizationId,
repositoryPath: "puter/zopu",
sourceHost: "example.com",
sourceUrl: "https://example.com/zopu",
updatedAt: 1,
});
const id = await ctx.db.insert("works", {
createdAt: 1,
definitionVersion: 1,
objective: "Prove questions",
organizationId, organizationId,
projectId, projectId,
status: "awaiting-definition-approval", status: "proposed",
title: "Questionable work", title: "Proposed Work",
updatedAt: 1, updatedAt: 3,
}); })
return { workId: id };
});
await t.mutation(api.workPlanning.submitQuestion, {
questionJson: JSON.stringify({
alternatives: [],
id: "q1",
impact: "medium",
prompt: "Which runtime?",
status: "open",
}),
token: "test-token",
workId,
});
const questions = await t.run(async (ctx) =>
ctx.db.query("workQuestions").collect()
); );
expect(questions).toHaveLength(1); await expect(
expect(questions[0]?.questionId).toBe("q1"); t.withIdentity(identity).mutation(api.workExecution.start, { workId })
expect(questions[0]?.definitionVersion).toBe(1); ).rejects.toThrow(/Ready/u);
});
test("rejects execution when Work lacks Definition or Design", async () => {
const t = convexTest({ modules, schema });
const { organizationId, projectId } = await seedProject(t);
const workId = await t.run(
async (ctx) =>
await ctx.db.insert("works", {
createdAt: 3,
objective: "Test",
organizationId,
projectId,
status: "ready",
title: "Ready but unplanned",
updatedAt: 3,
})
);
await expect(
t.withIdentity(identity).mutation(api.workExecution.start, { workId })
).rejects.toThrow(/Definition and Design/u);
});
test("execution mutations accept service token for agent calls", async () => {
const t = convexTest({ modules, schema });
const { organizationId, projectId } = await seedProject(t);
const workId = await seedReadyWork(t, organizationId, projectId);
// Service-auth path: token + organizationId, no user identity needed.
// This should not throw an auth error (it may fail later for missing
// git connection, but auth itself must pass).
await expect(
t.mutation(api.workExecution.start, {
organizationId,
token: "invalid-token",
workId,
})
).rejects.toThrow(/agent control token/u);
}); });
}); });
+138 -241
View File
@@ -1,10 +1,8 @@
import type { WorkEventKind } from "@code/primitives/work"; import type { WorkEventKind } from "@code/primitives/work";
import { import {
decodeDefinition, decodeDefinition,
validateDefinition,
WorkQuestion, WorkQuestion,
} from "@code/primitives/work-definition"; } from "@code/primitives/work-definition";
import type { WorkDefinition } from "@code/primitives/work-definition";
import { validateDesignPacket } from "@code/primitives/work-design"; import { validateDesignPacket } from "@code/primitives/work-design";
import { makeFunctionReference } from "convex/server"; import { makeFunctionReference } from "convex/server";
import { ConvexError, v } from "convex/values"; import { ConvexError, v } from "convex/values";
@@ -26,10 +24,26 @@ const plannerRef = makeFunctionReference<
"action", "action",
{ organizationId: Id<"organizations">; workId: Id<"works"> } { organizationId: Id<"organizations">; workId: Id<"works"> }
>("workPlanning:runPlanner"); >("workPlanning:runPlanner");
const submitDefinitionRef = makeFunctionReference<
"mutation",
{ payloadJson: string; token: string; workId: Id<"works"> },
{ definitionId: Id<"workDefinitions">; version: number }
>("workPlanning:submitDefinitionProposal");
const submitDesignRef = makeFunctionReference<
"mutation",
{ payloadJson: string; token: string; workId: Id<"works"> },
{ designId: Id<"designPackets">; version: number }
>("workPlanning:submitDesignProposal");
const plannerContextRef = makeFunctionReference< const plannerContextRef = makeFunctionReference<
"query", "query",
{ workId: Id<"works"> }, { workId: Id<"works"> },
{ objective: string; status: string; title: string } | null {
existingDefinition?: string;
objective: string;
phase: "definition" | "design";
status: string;
title: string;
} | null
>("workPlanning:getPlannerContext"); >("workPlanning:getPlannerContext");
const plannerFailureRef = makeFunctionReference< const plannerFailureRef = makeFunctionReference<
"mutation", "mutation",
@@ -91,43 +105,43 @@ const appendEvent = async (
}); });
}; };
const invalidateApprovalsAndDesign = async ( const supersedePriorDefinitions = async (
ctx: MutationCtx, ctx: MutationCtx,
workId: Id<"works"> workId: Id<"works">
) => { ) => {
const approvals = await ctx.db const previous = await ctx.db
.query("workApprovals") .query("workDefinitions")
.withIndex("by_workId_and_kind", (q) => q.eq("workId", workId))
.collect();
for (const approval of approvals) {
if (approval.status === "active") {
await ctx.db.patch(approval._id, { status: "invalidated" });
}
}
const designs = await ctx.db
.query("designPackets")
.withIndex("by_work_and_version", (q) => q.eq("workId", workId)) .withIndex("by_work_and_version", (q) => q.eq("workId", workId))
.collect(); .collect();
for (const design of designs) { for (const row of previous) {
if (design.status === "current") { if (row.status === "current") {
await ctx.db.patch(design._id, { status: "superseded" }); await ctx.db.patch(row._id, { status: "superseded" });
} }
} }
}; };
const supersedePriorDesigns = async (ctx: MutationCtx, workId: Id<"works">) => {
const prior = await ctx.db
.query("designPackets")
.withIndex("by_work_and_version", (q) => q.eq("workId", workId))
.collect();
for (const row of prior) {
if (row.status === "current") {
await ctx.db.patch(row._id, { status: "superseded" });
}
}
};
/**
* Starts Definition planning for a proposed Work. No approval is needed.
* The planner workflow produces a Definition; persistence auto-advances to Design.
*/
export const requestDefinition = mutation({ export const requestDefinition = mutation({
args: { workId: v.id("works") }, args: { workId: v.id("works") },
handler: async (ctx, args) => { handler: async (ctx, args) => {
const work = await requireWork(ctx, args.workId); const work = await requireWork(ctx, args.workId);
await requireProjectMember(ctx, work.projectId); await requireProjectMember(ctx, work.projectId);
if ( if (work.status !== "proposed" && work.status !== "defining") {
work.status !== "proposed" &&
work.status !== "defining" &&
!(
work.status === "blocked" &&
work.definitionApprovalVersion !== work.definitionVersion
)
) {
throw new ConvexError("Work is not available for definition"); throw new ConvexError("Work is not available for definition");
} }
const now = Date.now(); const now = Date.now();
@@ -152,8 +166,10 @@ const saveDefinition = async (
payloadJson: string, payloadJson: string,
createdBy: string createdBy: string
) => { ) => {
if (work.status === "executing") { if (work.status === "executing" || work.status === "verifying") {
throw new ConvexError("Cannot revise Work while a Run is executing"); throw new ConvexError(
"Cannot revise Work during execution or verification"
);
} }
const decoded = await Effect.runPromise( const decoded = await Effect.runPromise(
decodeDefinition({ decodeDefinition({
@@ -166,16 +182,9 @@ const saveDefinition = async (
); );
}); });
const { version } = decoded; const { version } = decoded;
await invalidateApprovalsAndDesign(ctx, work._id); await supersedePriorDefinitions(ctx, work._id);
const previous = await ctx.db // Revising a Definition invalidates any existing Design so planning restarts.
.query("workDefinitions") await supersedePriorDesigns(ctx, work._id);
.withIndex("by_work_and_version", (q) => q.eq("workId", work._id))
.collect();
for (const row of previous) {
if (row.status === "current") {
await ctx.db.patch(row._id, { status: "superseded" });
}
}
const definitionId = await ctx.db.insert("workDefinitions", { const definitionId = await ctx.db.insert("workDefinitions", {
createdAt: Date.now(), createdAt: Date.now(),
createdBy, createdBy,
@@ -199,12 +208,11 @@ const saveDefinition = async (
workId: work._id, workId: work._id,
}); });
} }
// Auto-advance: Definition persistence starts the Design phase immediately.
await ctx.db.patch(work._id, { await ctx.db.patch(work._id, {
definitionApprovalVersion: undefined,
definitionVersion: version, definitionVersion: version,
designApprovalVersion: undefined,
designVersion: undefined, designVersion: undefined,
status: "awaiting-definition-approval", status: "designing",
updatedAt: Date.now(), updatedAt: Date.now(),
}); });
await appendEvent( await appendEvent(
@@ -215,6 +223,11 @@ const saveDefinition = async (
String(definitionId), String(definitionId),
JSON.stringify(decoded) JSON.stringify(decoded)
); );
// Kick off Design planning automatically.
await ctx.scheduler.runAfter(0, plannerRef, {
organizationId: work.organizationId,
workId: work._id,
});
return { definitionId, version }; return { definitionId, version };
}; };
@@ -229,82 +242,6 @@ export const saveDefinitionProposal = mutation({
export const reviseDefinition = saveDefinitionProposal; export const reviseDefinition = saveDefinitionProposal;
export const approveDefinition = mutation({
args: { version: v.number(), workId: v.id("works") },
handler: async (ctx, args) => {
const work = await requireWork(ctx, args.workId);
await requireProjectMember(ctx, work.projectId);
if (
work.definitionVersion !== args.version ||
work.status !== "awaiting-definition-approval"
) {
throw new ConvexError("Definition version is not current or approvable");
}
const row = await ctx.db
.query("workDefinitions")
.withIndex("by_work_and_version", (q) =>
q.eq("workId", work._id).eq("version", args.version)
)
.unique();
if (!row) {
throw new ConvexError("Definition not found");
}
const definition = JSON.parse(row.payloadJson) as WorkDefinition;
const valid = await Effect.runPromise(validateDefinition(definition)).catch(
(error: unknown) => {
throw new ConvexError(
error instanceof Error
? error.message
: "Definition cannot be approved"
);
}
);
const questions = await ctx.db
.query("workQuestions")
.withIndex("by_workId_and_definitionVersion", (q) =>
q.eq("workId", work._id).eq("definitionVersion", args.version)
)
.collect();
if (
questions.some(
(question) => question.status === "open" && question.impact === "high"
)
) {
throw new ConvexError(
"High-impact open questions must be resolved before approval"
);
}
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Authentication required");
}
await ctx.db.insert("workApprovals", {
approvedAt: Date.now(),
approvedBy: identity.tokenIdentifier,
definitionVersion: args.version,
kind: "definition",
status: "active",
workId: work._id,
});
await ctx.db.patch(work._id, {
definitionApprovalVersion: valid.version,
status: "designing",
updatedAt: Date.now(),
});
await appendEvent(
ctx,
work._id,
"definition.approved",
`definition-approved:${args.version}`
);
await ctx.scheduler.runAfter(0, plannerRef, {
organizationId: work.organizationId,
workId: work._id,
});
return { status: "designing" as const, version: args.version };
},
});
const reviseQuestion = async ( const reviseQuestion = async (
ctx: MutationCtx, ctx: MutationCtx,
work: Doc<"works">, work: Doc<"works">,
@@ -320,7 +257,7 @@ const reviseQuestion = async (
if (!current) { if (!current) {
throw new ConvexError("Current Definition not found"); throw new ConvexError("Current Definition not found");
} }
const definition = JSON.parse(current.payloadJson) as WorkDefinition; const definition = JSON.parse(current.payloadJson);
const persistedQuestions = await ctx.db const persistedQuestions = await ctx.db
.query("workQuestions") .query("workQuestions")
.withIndex("by_workId_and_definitionVersion", (q) => .withIndex("by_workId_and_definitionVersion", (q) =>
@@ -386,48 +323,19 @@ export const withdrawQuestion = mutation({
}, },
}); });
export const requestDesign = mutation({
args: { workId: v.id("works") },
handler: async (ctx, args) => {
const work = await requireWork(ctx, args.workId);
await requireProjectMember(ctx, work.projectId);
if (
(work.status !== "designing" && work.status !== "blocked") ||
work.definitionApprovalVersion !== work.definitionVersion
) {
throw new ConvexError("Definition must be approved before Design");
}
if (work.status === "blocked") {
await ctx.db.patch(work._id, {
status: "designing",
updatedAt: Date.now(),
});
}
await appendEvent(
ctx,
work._id,
"design.requested",
`design-requested:${work.definitionVersion}`
);
await ctx.scheduler.runAfter(0, plannerRef, {
organizationId: work.organizationId,
workId: work._id,
});
return { status: "designing" as const };
},
});
const saveDesign = async ( const saveDesign = async (
ctx: MutationCtx, ctx: MutationCtx,
work: Doc<"works">, work: Doc<"works">,
payloadJson: string, payloadJson: string,
createdBy: string createdBy: string
) => { ) => {
if (work.status === "executing") { if (work.status === "executing" || work.status === "verifying") {
throw new ConvexError("Cannot revise Work while a Run is executing"); throw new ConvexError(
"Cannot revise Work during execution or verification"
);
} }
if (work.definitionApprovalVersion !== work.definitionVersion) { if (!work.definitionVersion) {
throw new ConvexError("Design must bind an approved Definition version"); throw new ConvexError("Design requires a current Definition");
} }
const decoded = await Effect.runPromise( const decoded = await Effect.runPromise(
validateDesignPacket({ validateDesignPacket({
@@ -440,26 +348,7 @@ const saveDesign = async (
error instanceof Error ? error.message : "Invalid Design Packet" error instanceof Error ? error.message : "Invalid Design Packet"
); );
}); });
const prior = await ctx.db await supersedePriorDesigns(ctx, work._id);
.query("designPackets")
.withIndex("by_work_and_version", (q) => q.eq("workId", work._id))
.collect();
for (const row of prior) {
if (row.status === "current") {
await ctx.db.patch(row._id, { status: "superseded" });
}
}
const designApprovals = await ctx.db
.query("workApprovals")
.withIndex("by_workId_and_kind", (q) =>
q.eq("workId", work._id).eq("kind", "design")
)
.collect();
for (const approval of designApprovals) {
if (approval.status === "active") {
await ctx.db.patch(approval._id, { status: "invalidated" });
}
}
const designId = await ctx.db.insert("designPackets", { const designId = await ctx.db.insert("designPackets", {
createdAt: Date.now(), createdAt: Date.now(),
createdBy, createdBy,
@@ -483,10 +372,10 @@ const saveDesign = async (
workId: work._id, workId: work._id,
}); });
} }
// Auto-advance: Design persistence makes the Work ready for execution.
await ctx.db.patch(work._id, { await ctx.db.patch(work._id, {
designApprovalVersion: undefined,
designVersion: decoded.version, designVersion: decoded.version,
status: "awaiting-design-approval", status: "ready",
updatedAt: Date.now(), updatedAt: Date.now(),
}); });
await appendEvent( await appendEvent(
@@ -511,52 +400,6 @@ export const saveDesignProposal = mutation({
export const reviseDesign = saveDesignProposal; export const reviseDesign = saveDesignProposal;
export const approveDesign = mutation({
args: {
definitionVersion: v.number(),
designVersion: v.number(),
workId: v.id("works"),
},
handler: async (ctx, args) => {
const work = await requireWork(ctx, args.workId);
await requireProjectMember(ctx, work.projectId);
if (
work.definitionApprovalVersion !== args.definitionVersion ||
work.designVersion !== args.designVersion ||
work.status !== "awaiting-design-approval"
) {
throw new ConvexError(
"Design approval must bind current Definition and Design versions"
);
}
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Authentication required");
}
await ctx.db.insert("workApprovals", {
approvedAt: Date.now(),
approvedBy: identity.tokenIdentifier,
definitionVersion: args.definitionVersion,
designVersion: args.designVersion,
kind: "design",
status: "active",
workId: work._id,
});
await ctx.db.patch(work._id, {
designApprovalVersion: args.designVersion,
status: "ready",
updatedAt: Date.now(),
});
await appendEvent(
ctx,
work._id,
"design.approved",
`design-approved:${args.definitionVersion}:${args.designVersion}`
);
return { status: "ready" as const };
},
});
export const submitDefinitionProposal = mutation({ export const submitDefinitionProposal = mutation({
args: { payloadJson: v.string(), token: v.string(), workId: v.id("works") }, args: { payloadJson: v.string(), token: v.string(), workId: v.id("works") },
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -589,13 +432,8 @@ export const submitQuestion = mutation({
if (work.definitionVersion === undefined) { if (work.definitionVersion === undefined) {
throw new ConvexError("Work has no Definition to attach a question to"); throw new ConvexError("Work has no Definition to attach a question to");
} }
if ( if (work.status !== "defining" && work.status !== "designing") {
work.status !== "defining" && throw new ConvexError("Questions can only be submitted during planning");
work.status !== "awaiting-definition-approval"
) {
throw new ConvexError(
"Questions can only be submitted while the Work is being defined"
);
} }
const question = await Effect.runPromise( const question = await Effect.runPromise(
Schema.decodeUnknownEffect(WorkQuestion)(parsePayload(args.questionJson)) Schema.decodeUnknownEffect(WorkQuestion)(parsePayload(args.questionJson))
@@ -671,6 +509,11 @@ export const recordPlannerFailure = internalMutation({
}, },
}); });
/**
* Calls the private Flue `plan-work` workflow for the current planning phase.
* The workflow returns a structured Definition or Design Packet; the planner
* worker (Flue side) submits it through the authenticated Convex mutations.
*/
export const runPlanner = internalAction({ export const runPlanner = internalAction({
args: { organizationId: v.id("organizations"), workId: v.id("works") }, args: { organizationId: v.id("organizations"), workId: v.id("works") },
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -690,38 +533,71 @@ export const runPlanner = internalAction({
return null; return null;
} }
const endpoint = new URL( const endpoint = new URL(
`agents/work-planner/${encodeURIComponent(String(args.organizationId))}`, "workflows/plan-work",
`${flueUrl.replace(/\/+$/u, "")}/` `${flueUrl.replace(/\/+/u, "")}/`
); );
endpoint.searchParams.set("wait", "result"); endpoint.searchParams.set("wait", "result");
try { try {
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
body: JSON.stringify({ body: JSON.stringify({
message: `Plan Work ${String(args.workId)} titled "${context.title}" with objective "${context.objective}". Current state is ${context.status}. If the Work is defining, submit only a Definition proposal and questions. If the Work is designing, submit only a Design Packet proposal bound to the approved Definition. Never approve or execute.`, input: {
context: {
...(context.existingDefinition
? { existingDefinition: context.existingDefinition }
: {}),
problemStatement: context.objective,
},
organizationId: String(args.organizationId),
phase: context.phase,
requestId: `planner:${args.workId}:${context.phase}`,
workId: String(args.workId),
},
}), }),
headers: { headers: {
authorization: `Bearer ${env.FLUE_DB_TOKEN}`, authorization: `Bearer ${env.FLUE_DB_TOKEN}`,
"content-type": "application/json", "content-type": "application/json",
"x-zopu-organization-id": String(args.organizationId), "x-zopu-organization-id": String(args.organizationId),
"x-zopu-request-id": `work-planner:${args.workId}`,
}, },
method: "POST", method: "POST",
}); });
if (response.ok) { if (!response.ok) {
return null;
}
await ctx.runMutation(plannerFailureRef, { await ctx.runMutation(plannerFailureRef, {
error: `Work Planner request failed (${response.status})`, error: `Plan-work workflow request failed (${response.status})`,
workId: args.workId, workId: args.workId,
}); });
return null;
}
const raw = (await response.json()) as Record<string, unknown>;
const data = (raw.data as Record<string, unknown> | undefined) ?? raw;
const phase = data.phase as string | undefined;
if (phase === "definition") {
const payload = {
...(data.definition as Record<string, unknown>),
questions: data.questions ?? [],
};
await ctx.runMutation(submitDefinitionRef, {
payloadJson: JSON.stringify(payload),
token: env.FLUE_DB_TOKEN,
workId: args.workId,
});
} else if (phase === "design") {
await ctx.runMutation(submitDesignRef, {
payloadJson: JSON.stringify(data.design),
token: env.FLUE_DB_TOKEN,
workId: args.workId,
});
} else {
await ctx.runMutation(plannerFailureRef, {
error: `Unexpected planner phase: ${String(phase)}`,
workId: args.workId,
});
}
} catch (error) { } catch (error) {
await ctx.runMutation(plannerFailureRef, { await ctx.runMutation(plannerFailureRef, {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
workId: args.workId, workId: args.workId,
}); });
} }
// The private worker submits typed proposals through Convex mutations; it
// never approves or advances Work itself.
return null; return null;
}, },
}); });
@@ -730,9 +606,30 @@ export const getPlannerContext = internalQuery({
args: { workId: v.id("works") }, args: { workId: v.id("works") },
handler: async (ctx, args) => { handler: async (ctx, args) => {
const work = await ctx.db.get(args.workId); const work = await ctx.db.get(args.workId);
return work if (!work) {
? { objective: work.objective, status: work.status, title: work.title } return null;
: null; }
const phase: "definition" | "design" =
work.status === "designing" ? "design" : "definition";
let existingDefinition: string | undefined;
if (phase === "design") {
const current = await ctx.db
.query("workDefinitions")
.withIndex("by_work_and_version", (q) =>
q.eq("workId", work._id).eq("version", work.definitionVersion ?? 0)
)
.unique();
if (current) {
existingDefinition = current.payloadJson;
}
}
return {
objective: work.objective,
phase,
status: work.status,
title: work.title,
...(existingDefinition ? { existingDefinition } : {}),
};
}, },
}); });
+11
View File
@@ -4,6 +4,7 @@ import {
workDraftFromSignal, workDraftFromSignal,
workProposedEventFromSignal, workProposedEventFromSignal,
} from "@code/primitives/work"; } from "@code/primitives/work";
import { makeFunctionReference } from "convex/server";
import { ConvexError, v } from "convex/values"; import { ConvexError, v } from "convex/values";
import { Effect } from "effect"; import { Effect } from "effect";
@@ -18,6 +19,11 @@ const requireAgent = (token: string) => {
} }
}; };
const plannerRef = makeFunctionReference<
"action",
{ organizationId: Id<"organizations">; workId: Id<"works"> }
>("workPlanning:runPlanner");
const requireSignal = async ( const requireSignal = async (
ctx: MutationCtx, ctx: MutationCtx,
organizationId: Id<"organizations">, organizationId: Id<"organizations">,
@@ -187,6 +193,11 @@ export const createFromSignal = mutation({
signalId: signal._id, signalId: signal._id,
workId, workId,
}); });
// Auto-start Definition planning for newly proposed Work.
await ctx.scheduler.runAfter(0, plannerRef, {
organizationId: signal.organizationId,
workId,
});
return { created: true, workId }; return { created: true, workId };
}, },
}); });
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
import { import {
getExecutableGitSoftware, getExecutableGitSoftware,
makeGitExecutablesRunnable, makeGitExecutablesRunnable,
} from "./agent-os-git-software"; } from "./agent-os-git-software.ts";
const TAR_BLOCK_SIZE = 512; const TAR_BLOCK_SIZE = 512;
const READ_ONLY_EXECUTABLE_MODE = Buffer.from([0xa4, 0x81, 0x00, 0x00]); const READ_ONLY_EXECUTABLE_MODE = Buffer.from([0xa4, 0x81, 0x00, 0x00]);
@@ -4,7 +4,7 @@ import { describe, expect, test } from "vitest";
import { import {
decodeGitConnectionInput, decodeGitConnectionInput,
decodeWorkAttemptExecutionResult, decodeWorkAttemptExecutionResult,
} from "./execution-runtime"; } from "./execution-runtime.ts";
describe("execution runtime contracts", () => { describe("execution runtime contracts", () => {
test("accepts one authenticated Gitea connection", async () => { test("accepts one authenticated Gitea connection", async () => {
+2 -2
View File
@@ -1,9 +1,9 @@
/* eslint-disable max-classes-per-file -- execution boundary errors live with their schemas. */ /* eslint-disable max-classes-per-file -- execution boundary errors live with their schemas. */
import { Effect, Schema } from "effect"; import { Effect, Schema } from "effect";
import { GitProvider } from "./git-provider"; import { GitProvider } from "./git-provider.ts";
export type { GitProvider } from "./git-provider"; export type { GitProvider } from "./git-provider.ts";
const Text = Schema.String.check( const Text = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, { Schema.makeFilter((value) => value.trim().length > 0, {
@@ -12,7 +12,7 @@ import {
currentBranch, currentBranch,
nodeShell, nodeShell,
push, push,
} from "./git-local-runtime"; } from "./git-local-runtime.ts";
const run = <A, E>(eff: Effect.Effect<A, E>) => Effect.runPromise(eff); const run = <A, E>(eff: Effect.Effect<A, E>) => Effect.runPromise(eff);
+3 -3
View File
@@ -9,7 +9,7 @@ import {
isProviderHostCompatible, isProviderHostCompatible,
normalizeRepositoryUrl, normalizeRepositoryUrl,
providerForHost, providerForHost,
} from "./git-provider"; } from "./git-provider.ts";
import { import {
canSafelyReplaceRepository, canSafelyReplaceRepository,
idempotencyKeyFor, idempotencyKeyFor,
@@ -18,8 +18,8 @@ import {
validatePuterOrgName, validatePuterOrgName,
validatePuterUsername, validatePuterUsername,
validateRepositoryName, validateRepositoryName,
} from "./git-provisioning"; } from "./git-provisioning.ts";
import { isProcessedEvent } from "./git-webhook"; import { isProcessedEvent } from "./git-webhook.ts";
describe("git-provider normalization", () => { describe("git-provider normalization", () => {
test("normalizes a standard HTTPS GitHub URL", async () => { test("normalizes a standard HTTPS GitHub URL", async () => {
+2 -2
View File
@@ -1,12 +1,12 @@
/* eslint-disable max-classes-per-file -- provisioning commands, results, and errors form one validated-command module. */ /* eslint-disable max-classes-per-file -- provisioning commands, results, and errors form one validated-command module. */
import { Effect, Schema } from "effect"; import { Effect, Schema } from "effect";
import type { GitProvider, ProviderAccount } from "./git-provider"; import type { GitProvider, ProviderAccount } from "./git-provider.ts";
import { import {
GitProviderError, GitProviderError,
RepositoryPrivacy, RepositoryPrivacy,
PUTER_GIT_SERVER_URL, PUTER_GIT_SERVER_URL,
} from "./git-provider"; } from "./git-provider.ts";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared filters // Shared filters
@@ -7,8 +7,8 @@ import {
createPullRequest, createPullRequest,
getIssue, getIssue,
listIssues, listIssues,
} from "./git-remote-runtime"; } from "./git-remote-runtime.ts";
import type { GitRemoteConfig, GitRemoteTransport } from "./git-remote-runtime"; import type { GitRemoteConfig, GitRemoteTransport } from "./git-remote-runtime.ts";
const config: GitRemoteConfig = { const config: GitRemoteConfig = {
baseUrl: "https://git.example.com", baseUrl: "https://git.example.com",
+1 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable max-classes-per-file -- webhook event envelopes and errors form one normalized module. */ /* eslint-disable max-classes-per-file -- webhook event envelopes and errors form one normalized module. */
import { Effect, Schema } from "effect"; import { Effect, Schema } from "effect";
import { GitProvider } from "./git-provider"; import { GitProvider } from "./git-provider.ts";
const Text = Schema.String.check( const Text = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, { Schema.makeFilter((value) => value.trim().length > 0, {
+1 -1
View File
@@ -6,7 +6,7 @@ import {
inspectGitWorkspace, inspectGitWorkspace,
makeCommitMessage, makeCommitMessage,
validatePullRequestMetadata, validatePullRequestMetadata,
} from "./git"; } from "./git.ts";
const inspectionInput = { const inspectionInput = {
branch: "work/issue-5", branch: "work/issue-5",
@@ -8,7 +8,7 @@ import {
projectIssueDraftFromSignal, projectIssueDraftFromSignal,
queueProjectIssue, queueProjectIssue,
validateProjectIssueDraft, validateProjectIssueDraft,
} from "./project-issue"; } from "./project-issue.ts";
describe("project issue primitives", () => { describe("project issue primitives", () => {
it("normalizes an explicit project request", () => { it("normalizes an explicit project request", () => {
+1 -1
View File
@@ -4,7 +4,7 @@ import {
getProjectWorkNextAction, getProjectWorkNextAction,
getProjectWorkProgress, getProjectWorkProgress,
getProjectWorkStatus, getProjectWorkStatus,
} from "./project-work"; } from "./project-work.ts";
describe("project work state mapping", () => { describe("project work state mapping", () => {
test("maps issue and run statuses to the same product language", () => { test("maps issue and run statuses to the same product language", () => {
@@ -8,7 +8,7 @@ import {
WorkspaceExecutionError, WorkspaceExecutionError,
WorkspaceStateError, WorkspaceStateError,
WorkspaceValidationError, WorkspaceValidationError,
} from "./project-workspace"; } from "./project-workspace.ts";
const makeInput = () => ({ const makeInput = () => ({
artifacts: [ artifacts: [
+2 -2
View File
@@ -1,9 +1,9 @@
/* eslint-disable max-classes-per-file -- deep Effect v4 domain module: branded schemas, tagged errors, and context services */ /* eslint-disable max-classes-per-file -- deep Effect v4 domain module: branded schemas, tagged errors, and context services */
import { Context, Effect, Layer, Schema } from "effect"; import { Context, Effect, Layer, Schema } from "effect";
import { OrganizationId, ProjectId, TimestampMs } from "./signal"; import { OrganizationId, ProjectId, TimestampMs } from "./signal.ts";
export type { OrganizationId, ProjectId } from "./signal"; export type { OrganizationId, ProjectId } from "./signal.ts";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Domain constants // Domain constants
+2 -2
View File
@@ -1,8 +1,8 @@
import { Effect } from "effect"; import { Effect } from "effect";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { composeSignal, SignalValidationError } from "./signal"; import { composeSignal, SignalValidationError } from "./signal.ts";
import type { SignalValidationReason } from "./signal"; import type { SignalValidationReason } from "./signal.ts";
type TestSignalScope = type TestSignalScope =
| { | {
+1 -1
View File
@@ -13,7 +13,7 @@ import {
redactSmokeText, redactSmokeText,
selectSmokeModelRoles, selectSmokeModelRoles,
validateSmokeCapabilities, validateSmokeCapabilities,
} from "./smoke"; } from "./smoke.ts";
const validRoleSelection = () => ({ const validRoleSelection = () => ({
plannerReviewerModel: "glm-5.2", plannerReviewerModel: "glm-5.2",
@@ -4,7 +4,7 @@ import { describe, expect, test } from "vitest";
import { import {
decodeWorkArtifactDraft, decodeWorkArtifactDraft,
decodeWorkDeliveryDraft, decodeWorkDeliveryDraft,
} from "./work-artifact"; } from "./work-artifact.ts";
describe("Work artifact contracts", () => { describe("Work artifact contracts", () => {
test("accepts traceable artifact and delivery metadata", async () => { test("accepts traceable artifact and delivery metadata", async () => {
+15 -17
View File
@@ -8,6 +8,7 @@ export const WorkStatus = Schema.Literals([
"awaiting-design-approval", "awaiting-design-approval",
"ready", "ready",
"executing", "executing",
"verifying",
"needs-input", "needs-input",
"blocked", "blocked",
"completed", "completed",
@@ -19,15 +20,16 @@ export type WorkStatus = typeof WorkStatus.Type;
export const WORK_TRANSITIONS: Readonly< export const WORK_TRANSITIONS: Readonly<
Record<WorkStatus, readonly WorkStatus[]> Record<WorkStatus, readonly WorkStatus[]>
> = { > = {
"awaiting-definition-approval": ["designing", "defining"], "awaiting-definition-approval": ["defining", "cancelled"],
"awaiting-design-approval": ["ready", "designing"], "awaiting-design-approval": ["designing", "cancelled"],
blocked: ["ready", "executing", "cancelled"], blocked: ["ready", "defining", "designing", "executing", "cancelled"],
cancelled: ["ready"], cancelled: ["ready"],
completed: [], completed: [],
defining: ["awaiting-definition-approval", "proposed"], defining: ["designing", "proposed"],
designing: ["awaiting-design-approval", "awaiting-definition-approval"], designing: ["ready", "defining"],
executing: [ executing: [
"ready", "ready",
"verifying",
"needs-input", "needs-input",
"blocked", "blocked",
"completed", "completed",
@@ -38,6 +40,14 @@ export const WORK_TRANSITIONS: Readonly<
"needs-input": ["ready", "executing", "cancelled"], "needs-input": ["ready", "executing", "cancelled"],
proposed: ["defining"], proposed: ["defining"],
ready: ["executing", "designing", "cancelled"], ready: ["executing", "designing", "cancelled"],
verifying: [
"ready",
"executing",
"completed",
"blocked",
"failed",
"cancelled",
],
}; };
export const canTransitionWork = (from: WorkStatus, to: WorkStatus): boolean => export const canTransitionWork = (from: WorkStatus, to: WorkStatus): boolean =>
@@ -51,15 +61,3 @@ export const assertWorkTransition = (
throw new Error(`Invalid Work transition: ${from} -> ${to}`); throw new Error(`Invalid Work transition: ${from} -> ${to}`);
} }
}; };
export const definitionRevisionInvalidation = {
definitionApproval: "invalidate" as const,
dependentDesign: "invalidate" as const,
designApproval: "invalidate" as const,
workStatus: "defining" as const,
};
export const designRevisionInvalidation = {
designApproval: "invalidate" as const,
workStatus: "designing" as const,
};
@@ -1,11 +1,11 @@
import { Effect } from "effect"; import { Effect } from "effect";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { FakeHarnessLive } from "./harness-runtime"; import { FakeHarnessLive } from "./harness-runtime.ts";
import { defaultCodingKitV0, resolveOutcome, shouldRetry } from "./resolver"; import { defaultCodingKitV0, resolveOutcome, shouldRetry } from "./resolver.ts";
import { decodeDefinition, validateDefinition } from "./work-definition"; import { decodeDefinition, validateDefinition } from "./work-definition.ts";
import { validateDesignPacket } from "./work-design"; import { validateDesignPacket } from "./work-design.ts";
import { canTransitionWork } from "./work-lifecycle"; import { canTransitionWork } from "./work-lifecycle.ts";
const definition = { const definition = {
acceptanceCriteria: ["terminal outcome"], acceptanceCriteria: ["terminal outcome"],
+1 -1
View File
@@ -6,7 +6,7 @@ import {
signalAttachedEvent, signalAttachedEvent,
workDraftFromSignal, workDraftFromSignal,
workProposedEventFromSignal, workProposedEventFromSignal,
} from "./work"; } from "./work.ts";
describe("workDraftFromSignal", () => { describe("workDraftFromSignal", () => {
test("creates a proposed Work draft from Signal meaning", async () => { test("creates a proposed Work draft from Signal meaning", async () => {
+7 -7
View File
@@ -1,7 +1,7 @@
import { Array as EffectArray, Effect, Order, Schema } from "effect"; import { Array as EffectArray, Effect, Order, Schema } from "effect";
export { WorkStatus } from "./work-lifecycle"; export { WorkStatus } from "./work-lifecycle.ts";
export type { WorkStatus as WorkLifecycleStatus } from "./work-lifecycle"; export type { WorkStatus as WorkLifecycleStatus } from "./work-lifecycle.ts";
const MeaningfulString = Schema.String.check( const MeaningfulString = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, { Schema.makeFilter((value) => value.trim().length > 0, {
@@ -92,18 +92,15 @@ export const WorkEventKind = Schema.Literals([
"work.proposed", "work.proposed",
"signal.attached", "signal.attached",
"definition.requested", "definition.requested",
"definition.approved",
"definition.saved", "definition.saved",
"definition.revised", "definition.revised",
"definition.approved",
"definition.invalidated",
"question.created", "question.created",
"question.answered", "question.answered",
"question.withdrawn", "question.withdrawn",
"design.requested",
"design.saved", "design.saved",
"design.revised",
"design.approved", "design.approved",
"design.invalidated", "design.revised",
"planner.failed", "planner.failed",
"slice.started", "slice.started",
"slice.completed", "slice.completed",
@@ -116,6 +113,9 @@ export const WorkEventKind = Schema.Literals([
"attempt.completed", "attempt.completed",
"attempt.reconciled", "attempt.reconciled",
"resolver.decided", "resolver.decided",
"verification.started",
"verification.completed",
"verification.failed",
"artifact.recorded", "artifact.recorded",
"delivery.recorded", "delivery.recorded",
"delivery.updated", "delivery.updated",
+237 -153
View File
@@ -12,6 +12,12 @@ catalogs:
'@convex-dev/better-auth': '@convex-dev/better-auth':
specifier: 0.12.5 specifier: 0.12.5
version: 0.12.5 version: 0.12.5
'@rivet-dev/agentos':
specifier: 0.2.14
version: 0.2.14
'@rivet-dev/agentos-core':
specifier: 0.2.14
version: 0.2.14
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: 4.3.3 specifier: 4.3.3
version: 4.3.3 version: 4.3.3
@@ -107,14 +113,17 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:packages/primitives version: link:packages/primitives
'@flue/sdk': '@flue/sdk':
specifier: 1.0.0-beta.9 specifier: npm:@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2
version: 1.0.0-beta.9 version: '@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2'
'@types/bun': '@types/bun':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.3.14 version: 1.3.14
'@types/node': '@types/node':
specifier: 'catalog:' specifier: 'catalog:'
version: 22.20.1 version: 22.20.1
concurrently:
specifier: ^9.1.2
version: 9.2.4
convex: convex:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.42.3(react@19.2.8) version: 1.42.3(react@19.2.8)
@@ -239,12 +248,6 @@ importers:
packages/agents: packages/agents:
dependencies: dependencies:
'@agentos-software/git':
specifier: 0.3.3
version: 0.3.3
'@code/backend':
specifier: workspace:*
version: link:../backend
'@code/env': '@code/env':
specifier: workspace:* specifier: workspace:*
version: link:../env version: link:../env
@@ -252,23 +255,20 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../primitives version: link:../primitives
'@flue/runtime': '@flue/runtime':
specifier: latest specifier: npm:@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2
version: 1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(effect@4.0.0-beta.99)(supports-color@10.2.2)(typebox@1.1.38)(typescript@7.0.2)(ws@8.21.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) version: '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(effect@4.0.0-beta.99)(supports-color@10.2.2)(typebox@1.1.38)(typescript@7.0.2)(ws@8.21.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)'
'@rivet-dev/agentos': '@rivet-dev/agentos':
specifier: 0.2.14 specifier: 'catalog:'
version: 0.2.14(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(supports-color@10.2.2)(ws@8.21.1) version: 0.2.14(@cfworker/json-schema@4.1.1)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(supports-color@10.2.2)(ws@8.21.1)
'@rivet-dev/agentos-core': '@rivet-dev/agentos-core':
specifier: 0.2.14 specifier: 'catalog:'
version: 0.2.14(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1) version: 0.2.14(@cfworker/json-schema@4.1.1)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)
convex: convex:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.42.3(react@19.2.8) version: 1.42.3(react@19.2.8)
hono: hono:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.12.32 version: 4.12.32
rivetkit:
specifier: 2.3.9
version: 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(sql.js@1.14.1)(ws@8.21.1)
valibot: valibot:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.4.2(typescript@7.0.2) version: 1.4.2(typescript@7.0.2)
@@ -277,8 +277,11 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../config version: link:../config
'@flue/cli': '@flue/cli':
specifier: latest specifier: npm:@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2
version: 1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@22.20.1)(effect@4.0.0-beta.99)(esbuild@0.28.1)(hono@4.12.32)(jiti@2.7.0)(supports-color@10.2.2)(terser@5.49.0)(typebox@1.1.38)(typescript@7.0.2)(wrangler@4.114.0)(ws@8.21.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) version: '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@22.20.1)(effect@4.0.0-beta.99)(esbuild@0.28.1)(hono@4.12.32)(jiti@2.7.0)(supports-color@10.2.2)(terser@5.49.0)(typebox@1.1.38)(typescript@7.0.2)(wrangler@4.114.0)(ws@8.21.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)'
'@rivetkit/engine-cli':
specifier: 2.3.9
version: 2.3.9
'@types/bun': '@types/bun':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.3.14 version: 1.3.14
@@ -427,10 +430,10 @@ importers:
version: 0.3.3 version: 0.3.3
'@agentos-software/pi': '@agentos-software/pi':
specifier: 0.2.7 specifier: 0.2.7
version: 0.2.7(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) version: 0.2.7(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@rivet-dev/agentos': '@rivet-dev/agentos':
specifier: 0.2.14 specifier: 0.2.14
version: 0.2.14(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(supports-color@10.2.2)(ws@8.21.1) version: 0.2.14(@cfworker/json-schema@4.1.1)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(supports-color@10.2.2)(ws@8.21.1)
effect: effect:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.0.0-beta.99 version: 4.0.0-beta.99
@@ -491,7 +494,7 @@ importers:
version: 19.2.8(react@19.2.8) version: 19.2.8(react@19.2.8)
shadcn: shadcn:
specifier: ^4.12.0 specifier: ^4.12.0
version: 4.16.0(supports-color@10.2.2)(typescript@7.0.2) version: 4.16.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(typescript@7.0.2)
shiki: shiki:
specifier: ^4.3.1 specifier: ^4.3.1
version: 4.3.1 version: 4.3.1
@@ -1323,6 +1326,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@chevrotain/types@11.1.2': '@chevrotain/types@11.1.2':
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
@@ -1429,6 +1435,11 @@ packages:
engines: {node: '>=22.19.0'} engines: {node: '>=22.19.0'}
hasBin: true hasBin: true
'@earendil-works/pi-ai@0.81.1':
resolution: {integrity: sha512-hzHE7Z8l5mgJk+ke67Lge0rwS2+wbKJrFKl9o5M1R1rh33+cCT7D1AHz1OAtX5wFs90E1/BTGhyJRTUHaMxGvQ==}
engines: {node: '>=22.19.0'}
hasBin: true
'@egjs/hammerjs@2.0.17': '@egjs/hammerjs@2.0.17':
resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
@@ -2114,18 +2125,6 @@ packages:
'@floating-ui/utils@0.2.12': '@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@flue/cli@1.0.0-beta.9':
resolution: {integrity: sha512-RPv28ecD7lQ2XBZbuohte3rXDqHHrdXTpCGC5mSxYQsEkOeDN2LOmImQZbJqCrkIAFmMgntzOhaF2+14R61C4Q==}
engines: {node: '>=22.19.0'}
hasBin: true
'@flue/runtime@1.0.0-beta.9':
resolution: {integrity: sha512-ksh0ZkTVyqQnGvU3OnbVX6luAJwe6tt8q7O0vn99b7Cx6XcPTXzY/YEkXrOtCHzV6ZwfSdO9ZfaWbhTD1tdQuQ==}
engines: {node: '>=22.19.0'}
'@flue/sdk@1.0.0-beta.9':
resolution: {integrity: sha512-EkGWF1Yw6baM4ekmp+3xFkc+LmDKcIAA0KQSZSpaSqQRYyixXB+IwEbhePS83HCgrR0/jup9l8rgld4Q2IGNOw==}
'@google/genai@1.52.0': '@google/genai@1.52.0':
resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -3657,6 +3656,18 @@ packages:
react-dom: react-dom:
optional: true optional: true
'@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2':
resolution: {integrity: sha512-5nQEh0Pj4koF/lOyNQ/aTZGLPmXAtiHJmFuevfg6oP9J8G65YPlx64SJAMdc37frUSNlIoYx/yNKDXcBE46RtQ==}
engines: {node: '>=22.19.0'}
hasBin: true
'@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2':
resolution: {integrity: sha512-sIbIrYXGnCZ3S80NU+9/4qYEGzhT5RBCjHKkrBckiYlIrEW0Y0mohR+hcqt1bSDuk8Jd6EUGF0FCOVSA0CVTxA==}
engines: {node: '>=22.19.0'}
'@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2':
resolution: {integrity: sha512-Hts14CWUNUSB9FMTg21maatBEoqbpX80UDmTuMp8N1HNdkEow1pBhIg122ZnfaUyNdzx6vzrQaNhKCfDb7YkeA==}
'@rivetkit/bare-ts@0.6.2': '@rivetkit/bare-ts@0.6.2':
resolution: {integrity: sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg==} resolution: {integrity: sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@@ -5636,6 +5647,11 @@ packages:
resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
concurrently@9.2.4:
resolution: {integrity: sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==}
engines: {node: '>=18'}
hasBin: true
conf@10.2.0: conf@10.2.0:
resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -9014,6 +9030,9 @@ packages:
rw@1.3.3: rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-buffer@5.1.2: safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -9123,6 +9142,10 @@ packages:
resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
shell-quote@1.9.0:
resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==}
engines: {node: '>= 0.4'}
shiki@3.23.0: shiki@3.23.0:
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
@@ -9488,6 +9511,10 @@ packages:
tr46@0.0.3: tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
trim-lines@3.0.1: trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -10068,6 +10095,10 @@ packages:
resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==}
engines: {node: '>=10'} engines: {node: '>=10'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yargs@17.7.3: yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -10113,10 +10144,10 @@ snapshots:
dependencies: dependencies:
zod: 4.4.3 zod: 4.4.3
'@agentos-software/claude-code@0.2.7(supports-color@10.2.2)': '@agentos-software/claude-code@0.2.7(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)':
dependencies: dependencies:
'@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
'@anthropic-ai/claude-agent-sdk': 0.2.87(supports-color@10.2.2)(zod@4.4.3) '@anthropic-ai/claude-agent-sdk': 0.2.87(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3)
zod: 4.4.3 zod: 4.4.3
transitivePeerDependencies: transitivePeerDependencies:
- '@cfworker/json-schema' - '@cfworker/json-schema'
@@ -10153,11 +10184,11 @@ snapshots:
'@agentos-software/opencode@0.2.7': {} '@agentos-software/opencode@0.2.7': {}
'@agentos-software/pi@0.2.7(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)': '@agentos-software/pi@0.2.7(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies: dependencies:
'@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
'@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
transitivePeerDependencies: transitivePeerDependencies:
- '@modelcontextprotocol/sdk' - '@modelcontextprotocol/sdk'
- bufferutil - bufferutil
@@ -10196,10 +10227,10 @@ snapshots:
package-manager-detector: 1.8.0 package-manager-detector: 1.8.0
tinyexec: 1.2.4 tinyexec: 1.2.4
'@anthropic-ai/claude-agent-sdk@0.2.87(supports-color@10.2.2)(zod@4.4.3)': '@anthropic-ai/claude-agent-sdk@0.2.87(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3)':
dependencies: dependencies:
'@anthropic-ai/sdk': 0.74.0(zod@4.4.3) '@anthropic-ai/sdk': 0.74.0(zod@4.4.3)
'@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@4.4.3) '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3)
zod: 4.4.3 zod: 4.4.3
optionalDependencies: optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-arm64': 0.34.5
@@ -10285,7 +10316,7 @@ snapshots:
'@aws-sdk/types': 3.974.2 '@aws-sdk/types': 3.974.2
'@smithy/core': 3.30.0 '@smithy/core': 3.30.0
'@smithy/fetch-http-handler': 5.6.11 '@smithy/fetch-http-handler': 5.6.11
'@smithy/node-http-handler': 4.7.3 '@smithy/node-http-handler': 4.9.11
'@smithy/types': 4.16.1 '@smithy/types': 4.16.1
tslib: 2.8.1 tslib: 2.8.1
@@ -11118,6 +11149,8 @@ snapshots:
'@cbor-extract/cbor-extract-win32-x64@2.2.2': '@cbor-extract/cbor-extract-win32-x64@2.2.2':
optional: true optional: true
'@cfworker/json-schema@4.1.1': {}
'@chevrotain/types@11.1.2': {} '@chevrotain/types@11.1.2': {}
'@clack/core@1.4.3': '@clack/core@1.4.3':
@@ -11246,9 +11279,9 @@ snapshots:
'@microsoft/fetch-event-source': 2.0.1 '@microsoft/fetch-event-source': 2.0.1
fastq: 1.20.1 fastq: 1.20.1
'@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)': '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies: dependencies:
'@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
ignore: 7.0.5 ignore: 7.0.5
typebox: 1.1.38 typebox: 1.1.38
yaml: 2.9.0 yaml: 2.9.0
@@ -11260,11 +11293,32 @@ snapshots:
- ws - ws
- zod - zod
'@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)': '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies: dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
'@aws-sdk/client-bedrock-runtime': 3.1048.0 '@aws-sdk/client-bedrock-runtime': 3.1048.0
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2) '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
'@opentelemetry/api': 1.9.0
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2(supports-color@10.2.2)
https-proxy-agent: 7.0.6(supports-color@10.2.2)
openai: 6.26.0(ws@8.21.1)(zod@4.4.3)
partial-json: 0.1.7
typebox: 1.1.38
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-ai@0.81.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
'@smithy/node-http-handler': 4.7.3 '@smithy/node-http-handler': 4.7.3
@@ -12051,102 +12105,14 @@ snapshots:
'@floating-ui/utils@0.2.12': {} '@floating-ui/utils@0.2.12': {}
'@flue/cli@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@22.20.1)(effect@4.0.0-beta.99)(esbuild@0.28.1)(hono@4.12.32)(jiti@2.7.0)(supports-color@10.2.2)(terser@5.49.0)(typebox@1.1.38)(typescript@7.0.2)(wrangler@4.114.0)(ws@8.21.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)':
dependencies:
'@cloudflare/vite-plugin': 1.47.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0))(wrangler@4.114.0)
'@flue/runtime': 1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(effect@4.0.0-beta.99)(supports-color@10.2.2)(typebox@1.1.38)(typescript@7.0.2)(ws@8.21.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
'@flue/sdk': 1.0.0-beta.9
'@hono/node-server': 2.0.12(hono@4.12.32)
'@vercel/detect-agent': 1.2.3
debug: 4.4.3(supports-color@10.2.2)
minisearch: 7.2.0
package-up: 5.0.0
picocolors: 1.1.1
ulidx: 2.4.1
valibot: 1.4.2(typescript@7.0.2)
vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0)'
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@cfworker/json-schema'
- '@standard-schema/spec'
- '@types/json-schema'
- '@types/node'
- '@vitejs/devtools'
- arktype
- bufferutil
- effect
- esbuild
- hono
- jiti
- less
- publint
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- sury
- terser
- tsx
- typebox
- typescript
- unplugin-unused
- unrun
- utf-8-validate
- wrangler
- ws
- yaml
- zod
- zod-openapi
- zod-to-json-schema
'@flue/runtime@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(effect@4.0.0-beta.99)(supports-color@10.2.2)(typebox@1.1.38)(typescript@7.0.2)(ws@8.21.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)':
dependencies:
'@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@hono/node-server': 2.0.12(hono@4.12.32)
'@hono/standard-validator': 0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.32)
'@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@4.4.3)
'@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
'@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(effect@4.0.0-beta.99)(openapi-types@12.1.3)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod@4.4.3)
'@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@7.0.2))
hono: 4.12.32
hono-openapi: 1.3.1(@hono/standard-validator@0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.32))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(effect@4.0.0-beta.99)(openapi-types@12.1.3)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod@4.4.3))(@types/json-schema@7.0.15)(hono@4.12.32)(openapi-types@12.1.3)
js-yaml: 4.3.0
just-bash: 3.2.0(supports-color@10.2.2)
openapi-types: 12.1.3
quansync: 0.2.11
ulidx: 2.4.1
valibot: 1.4.2(typescript@7.0.2)
transitivePeerDependencies:
- '@cfworker/json-schema'
- '@standard-schema/spec'
- '@types/json-schema'
- arktype
- bufferutil
- effect
- supports-color
- sury
- typebox
- typescript
- utf-8-validate
- ws
- zod
- zod-openapi
- zod-to-json-schema
'@flue/sdk@1.0.0-beta.9':
dependencies:
'@durable-streams/client': 0.2.6
'@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)':
dependencies: dependencies:
google-auth-library: 10.9.1(supports-color@10.2.2) google-auth-library: 10.9.1(supports-color@10.2.2)
p-retry: 4.6.2 p-retry: 4.6.2
protobufjs: 7.6.5 protobufjs: 7.6.5
ws: 8.21.1 ws: 8.21.1
optionalDependencies: optionalDependencies:
'@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@4.4.3) '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3)
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- supports-color - supports-color
@@ -12477,9 +12443,9 @@ snapshots:
std-env: 3.10.0 std-env: 3.10.0
yoctocolors: 2.2.0 yoctocolors: 2.2.0
'@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)': '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies: dependencies:
'@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
transitivePeerDependencies: transitivePeerDependencies:
- '@modelcontextprotocol/sdk' - '@modelcontextprotocol/sdk'
- bufferutil - bufferutil
@@ -12488,11 +12454,11 @@ snapshots:
- ws - ws
- zod - zod
'@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)': '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies: dependencies:
'@anthropic-ai/sdk': 0.73.0(zod@4.4.3) '@anthropic-ai/sdk': 0.73.0(zod@4.4.3)
'@aws-sdk/client-bedrock-runtime': 3.1096.0 '@aws-sdk/client-bedrock-runtime': 3.1096.0
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2) '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)
'@mistralai/mistralai': 1.14.1 '@mistralai/mistralai': 1.14.1
'@sinclair/typebox': 0.34.52 '@sinclair/typebox': 0.34.52
ajv: 8.20.0 ajv: 8.20.0
@@ -12511,11 +12477,11 @@ snapshots:
- ws - ws
- zod - zod
'@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)': '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)':
dependencies: dependencies:
'@mariozechner/jiti': 2.6.5 '@mariozechner/jiti': 2.6.5
'@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@mariozechner/pi-tui': 0.60.0 '@mariozechner/pi-tui': 0.60.0
'@silvia-odwyer/photon-node': 0.3.4 '@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2 chalk: 5.6.2
@@ -12581,7 +12547,7 @@ snapshots:
'@mixmark-io/domino@2.2.0': {} '@mixmark-io/domino@2.2.0': {}
'@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@3.25.76)': '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@3.25.76)':
dependencies: dependencies:
'@hono/node-server': 2.0.12(hono@4.12.32) '@hono/node-server': 2.0.12(hono@4.12.32)
ajv: 8.20.0 ajv: 8.20.0
@@ -12600,10 +12566,12 @@ snapshots:
raw-body: 3.0.2 raw-body: 3.0.2
zod: 3.25.76 zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76) zod-to-json-schema: 3.25.2(zod@3.25.76)
optionalDependencies:
'@cfworker/json-schema': 4.1.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3)': '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3)':
dependencies: dependencies:
'@hono/node-server': 2.0.12(hono@4.12.32) '@hono/node-server': 2.0.12(hono@4.12.32)
ajv: 8.20.0 ajv: 8.20.0
@@ -12622,6 +12590,8 @@ snapshots:
raw-body: 3.0.2 raw-body: 3.0.2
zod: 4.4.3 zod: 4.4.3
zod-to-json-schema: 3.25.2(zod@4.4.3) zod-to-json-schema: 3.25.2(zod@4.4.3)
optionalDependencies:
'@cfworker/json-schema': 4.1.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -13355,15 +13325,15 @@ snapshots:
dependencies: dependencies:
'@secure-exec/core': 0.2.1 '@secure-exec/core': 0.2.1
'@rivet-dev/agentos-core@0.2.14(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)': '@rivet-dev/agentos-core@0.2.14(@cfworker/json-schema@4.1.1)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)':
dependencies: dependencies:
'@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
'@agentos-software/claude-code': 0.2.7(supports-color@10.2.2) '@agentos-software/claude-code': 0.2.7(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)
'@agentos-software/codex-cli': 0.3.4 '@agentos-software/codex-cli': 0.3.4
'@agentos-software/common': 0.2.14 '@agentos-software/common': 0.2.14
'@agentos-software/manifest': 0.2.14 '@agentos-software/manifest': 0.2.14
'@agentos-software/opencode': 0.2.7 '@agentos-software/opencode': 0.2.7
'@agentos-software/pi': 0.2.7(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3) '@agentos-software/pi': 0.2.7(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@aws-sdk/client-s3': 3.1096.0 '@aws-sdk/client-s3': 3.1096.0
'@rivet-dev/agentos-runtime-core': 0.2.14 '@rivet-dev/agentos-runtime-core': 0.2.14
'@rivet-dev/agentos-sidecar': 0.2.14 '@rivet-dev/agentos-sidecar': 0.2.14
@@ -13430,11 +13400,11 @@ snapshots:
'@rivet-dev/agentos-sidecar-linux-arm64-gnu': 0.2.14 '@rivet-dev/agentos-sidecar-linux-arm64-gnu': 0.2.14
'@rivet-dev/agentos-sidecar-linux-x64-gnu': 0.2.14 '@rivet-dev/agentos-sidecar-linux-x64-gnu': 0.2.14
'@rivet-dev/agentos@0.2.14(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(supports-color@10.2.2)(ws@8.21.1)': '@rivet-dev/agentos@0.2.14(@cfworker/json-schema@4.1.1)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(supports-color@10.2.2)(ws@8.21.1)':
dependencies: dependencies:
'@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
'@agentos-software/common': 0.2.14 '@agentos-software/common': 0.2.14
'@rivet-dev/agentos-core': 0.2.14(@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1) '@rivet-dev/agentos-core': 0.2.14(@cfworker/json-schema@4.1.1)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)
'@rivetkit/react': 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(ws@8.21.1) '@rivetkit/react': 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sql.js@1.14.1)(ws@8.21.1)
rivetkit: 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(sql.js@1.14.1)(ws@8.21.1) rivetkit: 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.4)(sql.js@1.14.1)(ws@8.21.1)
zod: 4.4.3 zod: 4.4.3
@@ -13482,6 +13452,93 @@ snapshots:
- utf-8-validate - utf-8-validate
- ws - ws
'@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@22.20.1)(effect@4.0.0-beta.99)(esbuild@0.28.1)(hono@4.12.32)(jiti@2.7.0)(supports-color@10.2.2)(terser@5.49.0)(typebox@1.1.38)(typescript@7.0.2)(wrangler@4.114.0)(ws@8.21.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)':
dependencies:
'@cloudflare/vite-plugin': 1.47.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0))(wrangler@4.114.0)
'@flue/runtime': '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(effect@4.0.0-beta.99)(supports-color@10.2.2)(typebox@1.1.38)(typescript@7.0.2)(ws@8.21.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)'
'@flue/sdk': '@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2'
'@hono/node-server': 2.0.12(hono@4.12.32)
'@vercel/detect-agent': 1.2.3
debug: 4.4.3(supports-color@10.2.2)
minisearch: 7.2.0
package-up: 5.0.0
picocolors: 1.1.1
ulidx: 2.4.1
valibot: 1.4.2(typescript@7.0.2)
vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0)'
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@standard-schema/spec'
- '@types/json-schema'
- '@types/node'
- '@vitejs/devtools'
- arktype
- bufferutil
- effect
- esbuild
- hono
- jiti
- less
- publint
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- sury
- terser
- tsx
- typebox
- typescript
- unplugin-unused
- unrun
- utf-8-validate
- wrangler
- ws
- yaml
- zod
- zod-openapi
- zod-to-json-schema
'@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(effect@4.0.0-beta.99)(supports-color@10.2.2)(typebox@1.1.38)(typescript@7.0.2)(ws@8.21.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)':
dependencies:
'@cfworker/json-schema': 4.1.1
'@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@earendil-works/pi-ai': 0.81.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.1)(zod@4.4.3)
'@hono/node-server': 2.0.12(hono@4.12.32)
'@hono/standard-validator': 0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.32)
'@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.4.3)
'@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
'@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(effect@4.0.0-beta.99)(openapi-types@12.1.3)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod@4.4.3)
'@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@7.0.2))
hono: 4.12.32
hono-openapi: 1.3.1(@hono/standard-validator@0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.32))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@7.0.2)))(effect@4.0.0-beta.99)(quansync@0.2.11)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(effect@4.0.0-beta.99)(openapi-types@12.1.3)(typebox@1.1.38)(valibot@1.4.2(typescript@7.0.2))(zod@4.4.3))(@types/json-schema@7.0.15)(hono@4.12.32)(openapi-types@12.1.3)
js-yaml: 4.3.0
just-bash: 3.2.0(supports-color@10.2.2)
openapi-types: 12.1.3
quansync: 0.2.11
ulidx: 2.4.1
valibot: 1.4.2(typescript@7.0.2)
transitivePeerDependencies:
- '@standard-schema/spec'
- '@types/json-schema'
- arktype
- bufferutil
- effect
- supports-color
- sury
- typebox
- typescript
- utf-8-validate
- ws
- zod
- zod-openapi
- zod-to-json-schema
'@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2':
dependencies:
'@durable-streams/client': 0.2.6
'@rivetkit/bare-ts@0.6.2': {} '@rivetkit/bare-ts@0.6.2': {}
'@rivetkit/engine-cli-darwin-arm64@2.3.9': '@rivetkit/engine-cli-darwin-arm64@2.3.9':
@@ -15450,6 +15507,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
concurrently@9.2.4:
dependencies:
chalk: 4.1.2
rxjs: 7.8.2
shell-quote: 1.9.0
supports-color: 8.1.1
tree-kill: 1.2.2
yargs: 17.7.2
conf@10.2.0: conf@10.2.0:
dependencies: dependencies:
ajv: 8.20.0 ajv: 8.20.0
@@ -17630,7 +17696,7 @@ snapshots:
md5.js@1.3.5: md5.js@1.3.5:
dependencies: dependencies:
hash-base: 3.0.5 hash-base: 3.1.2
inherits: 2.0.4 inherits: 2.0.4
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@@ -19704,6 +19770,10 @@ snapshots:
rw@1.3.3: {} rw@1.3.3: {}
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
safe-buffer@5.1.2: {} safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {} safe-buffer@5.2.1: {}
@@ -19816,14 +19886,14 @@ snapshots:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
to-buffer: 1.2.2 to-buffer: 1.2.2
shadcn@4.16.0(supports-color@10.2.2)(typescript@7.0.2): shadcn@4.16.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(typescript@7.0.2):
dependencies: dependencies:
'@babel/core': 7.29.7(supports-color@10.2.2) '@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/parser': 7.29.7 '@babel/parser': 7.29.7
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@dotenvx/dotenvx': 1.75.1 '@dotenvx/dotenvx': 1.75.1
'@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@3.25.76) '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@3.25.76)
'@types/validate-npm-package-name': 4.0.2 '@types/validate-npm-package-name': 4.0.2
browserslist: 4.28.7 browserslist: 4.28.7
commander: 14.0.3 commander: 14.0.3
@@ -19896,6 +19966,8 @@ snapshots:
shell-quote@1.10.0: {} shell-quote@1.10.0: {}
shell-quote@1.9.0: {}
shiki@3.23.0: shiki@3.23.0:
dependencies: dependencies:
'@shikijs/core': 3.23.0 '@shikijs/core': 3.23.0
@@ -20277,6 +20349,8 @@ snapshots:
tr46@0.0.3: {} tr46@0.0.3: {}
tree-kill@1.2.2: {}
trim-lines@3.0.1: {} trim-lines@3.0.1: {}
trough@2.2.0: {} trough@2.2.0: {}
@@ -20877,6 +20951,16 @@ snapshots:
y18n: 5.0.8 y18n: 5.0.8
yargs-parser: 20.2.9 yargs-parser: 20.2.9
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yargs@17.7.3: yargs@17.7.3:
dependencies: dependencies:
cliui: 8.0.1 cliui: 8.0.1
+40
View File
@@ -0,0 +1,40 @@
import net from "node:net";
const [host, portStr, timeoutStr] = process.argv.slice(2);
const port = Number.parseInt(portStr ?? "", 10);
const timeoutMs = Number.parseInt(timeoutStr ?? "30000", 10);
if (!host || Number.isNaN(port)) {
console.error("Usage: node wait-for-port.mjs <host> <port> [timeoutMs]");
process.exit(1);
}
const start = Date.now();
function tryConnect() {
const socket = new net.Socket();
socket.setTimeout(1000);
socket.once("connect", () => {
socket.destroy();
process.exit(0);
});
socket.once("error", () => {
socket.destroy();
if (Date.now() - start > timeoutMs) {
console.error(`Timed out waiting for ${host}:${port}`);
process.exit(1);
}
setTimeout(tryConnect, 500);
});
socket.once("timeout", () => {
socket.destroy();
if (Date.now() - start > timeoutMs) {
console.error(`Timed out waiting for ${host}:${port}`);
process.exit(1);
}
setTimeout(tryConnect, 500);
});
socket.connect(port, host);
}
tryConnect();
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://t3.codes/schema/t3.json",
"scripts": [
{
"name": "Bootstrap worktree (portable)",
"icon": "configure",
"command": "set -e\nworktree=\"${T3CODE_WORKTREE_PATH:-$PWD}\"\nroot=\"${T3CODE_PROJECT_ROOT:-}\"\nif [ -z \"$root\" ] || [ ! -d \"$root\" ]; then\n root=$(git -C \"$worktree\" worktree list --porcelain | sed -n '1{s/^worktree //;p;}')\nfi\nif [ -z \"$root\" ] || [ ! -d \"$root\" ]; then\n printf '%s\\n' 'Could not find the source checkout for worktree bootstrap.' >&2\n exit 1\nfi\ncd \"$worktree\"\nfor relative in .env packages/backend/.env.local packages/backend/.env.staging; do\n source_path=\"$root/$relative\"\n target_path=\"$worktree/$relative\"\n if [ -f \"$source_path\" ]; then\n mkdir -p \"$(dirname \"$target_path\")\"\n command cp -f \"$source_path\" \"$target_path\"\n fi\ndone\nprintf '%s\\n' 'Copied project environment files.'\ncommand pnpm install --frozen-lockfile",
"runOnWorktreeCreate": true
},
{
"name": "Start local stack (Herdr script)",
"icon": "play",
"command": "bash scripts/start-local-stack.sh",
"previewUrl": "http://localhost:5173",
"autoOpenPreview": true
}
]
}