feat: Projects backend slice — auth, primitives, backend, UI #3
@@ -18,19 +18,20 @@ import { useProjectWorkspace } from "@/hooks/use-project-workspace";
|
||||
const statusStyle: Record<Doc<"projectIssues">["status"], string> = {
|
||||
completed:
|
||||
"border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
||||
failed: "border-destructive/40 bg-destructive/10 text-destructive",
|
||||
failed:
|
||||
"border-destructive/40 bg-destructive/10 text-destructive",
|
||||
"needs-input":
|
||||
"border-orange-500/40 bg-orange-500/10 text-orange-700 dark:text-orange-300",
|
||||
open: "border-border text-muted-foreground",
|
||||
queued:
|
||||
"border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
||||
working: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
|
||||
open: "border-border bg-muted/40 text-muted-foreground",
|
||||
queued: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
|
||||
working:
|
||||
"border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
|
||||
};
|
||||
|
||||
interface RepositoryFormProps {
|
||||
readonly busy: boolean;
|
||||
readonly onChange: (value: string) => void;
|
||||
readonly onSubmit: () => Promise<void>;
|
||||
readonly onSubmit: () => void;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
@@ -41,36 +42,44 @@ const RepositoryForm = ({
|
||||
value,
|
||||
}: RepositoryFormProps) => (
|
||||
<form
|
||||
className="grid gap-3 border-b p-4"
|
||||
className="space-y-2 border-b p-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void onSubmit();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium" htmlFor="github-repository">
|
||||
Public GitHub repository
|
||||
<label className="text-xs font-medium" htmlFor="git-repository">
|
||||
Public Git repository
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect with an owner/name slug or repository URL.
|
||||
Import with a public HTTP(S) Git URL.
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="github-repository"
|
||||
id="git-repository"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="rivet-dev/rivet"
|
||||
placeholder="https://github.com/owner/repo"
|
||||
required
|
||||
value={value}
|
||||
/>
|
||||
<Button disabled={busy || value.trim().length === 0} type="submit">
|
||||
<Button className="w-full" disabled={busy} type="submit">
|
||||
<GitFork data-icon="inline-start" />
|
||||
{busy ? "Connecting" : "Connect repository"}
|
||||
{busy ? "Importing" : "Import repository"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
interface ProjectListProps {
|
||||
readonly projects: readonly Doc<"projects">[] | undefined;
|
||||
readonly projects: readonly {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly sources: readonly {
|
||||
readonly host: string;
|
||||
readonly repositoryPath: string;
|
||||
readonly url: string;
|
||||
}[];
|
||||
}[] | undefined;
|
||||
readonly selectedId: Id<"projects"> | null;
|
||||
readonly onSelect: (projectId: Id<"projects">) => void;
|
||||
}
|
||||
@@ -85,7 +94,7 @@ const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => {
|
||||
if (projects.length === 0) {
|
||||
return (
|
||||
<p className="p-3 text-xs leading-relaxed text-muted-foreground">
|
||||
Connect a repository to create its project artifacts and issue queue.
|
||||
Import a repository to create its project artifacts and issue queue.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -94,19 +103,20 @@ const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => {
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
className={`w-full border px-3 py-2 text-left transition-colors ${
|
||||
selectedId === project._id
|
||||
selectedId === project.id
|
||||
? "border-foreground/30 bg-muted"
|
||||
: "border-transparent hover:bg-muted/60"
|
||||
}`}
|
||||
key={project._id}
|
||||
onClick={() => onSelect(project._id)}
|
||||
key={project.id}
|
||||
onClick={() => onSelect(project.id as Id<"projects">)}
|
||||
type="button"
|
||||
>
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{project.repoOwner}/{project.repoName}
|
||||
{project.sources[0]?.host}
|
||||
{project.sources[0]?.repositoryPath}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -130,35 +140,26 @@ interface ArtifactGridProps {
|
||||
|
||||
const ArtifactGrid = ({ artifacts }: ArtifactGridProps) => (
|
||||
<section aria-labelledby="artifact-heading" className="space-y-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold" id="artifact-heading">
|
||||
Project artifacts
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Canonical context staged into every issue workspace.
|
||||
</p>
|
||||
</div>
|
||||
<h2 id="artifact-heading" className="text-sm font-semibold">
|
||||
Project artifacts
|
||||
</h2>
|
||||
{artifacts === undefined ? (
|
||||
<p className="text-sm text-muted-foreground">Loading artifacts...</p>
|
||||
<p className="text-xs text-muted-foreground">Loading artifacts...</p>
|
||||
) : artifacts.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No artifacts yet.</p>
|
||||
) : (
|
||||
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{artifacts.map((artifact) => (
|
||||
<article className="min-w-0 border bg-card p-3" key={artifact._id}>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<FileText className="size-4 shrink-0 text-muted-foreground" />
|
||||
<h3 className="truncate font-mono text-xs font-medium">
|
||||
{artifact.path}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">
|
||||
r{artifact.revision}
|
||||
</span>
|
||||
</div>
|
||||
<p className="line-clamp-4 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
|
||||
{artifact.content}
|
||||
<div
|
||||
className="border p-3"
|
||||
key={artifact._id}
|
||||
>
|
||||
<FileText className="mb-2 size-4 text-muted-foreground" />
|
||||
<p className="truncate text-xs font-medium">{artifact.path}</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
rev {artifact.revision}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -169,7 +170,7 @@ interface IssueComposerProps {
|
||||
readonly body: string;
|
||||
readonly busy: boolean;
|
||||
readonly onBodyChange: (value: string) => void;
|
||||
readonly onSubmit: () => Promise<void>;
|
||||
readonly onSubmit: () => void;
|
||||
readonly onTitleChange: (value: string) => void;
|
||||
readonly title: string;
|
||||
}
|
||||
@@ -183,47 +184,27 @@ const IssueComposer = ({
|
||||
title,
|
||||
}: IssueComposerProps) => (
|
||||
<form
|
||||
className="grid gap-3 border p-4"
|
||||
className="space-y-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void onSubmit();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h2 className="font-semibold">Raise an issue</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The issue becomes the identity for one Flue agent and AgentOS workspace.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium" htmlFor="issue-title">
|
||||
Title
|
||||
</label>
|
||||
<Input
|
||||
id="issue-title"
|
||||
maxLength={160}
|
||||
onChange={(event) => onTitleChange(event.target.value)}
|
||||
required
|
||||
value={title}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium" htmlFor="issue-body">
|
||||
Description
|
||||
</label>
|
||||
<Textarea
|
||||
id="issue-body"
|
||||
maxLength={10_000}
|
||||
onChange={(event) => onBodyChange(event.target.value)}
|
||||
required
|
||||
rows={5}
|
||||
value={body}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={busy || title.trim().length < 3 || body.trim().length < 10}
|
||||
type="submit"
|
||||
>
|
||||
<h2 className="text-sm font-semibold">New issue</h2>
|
||||
<Input
|
||||
onChange={(event) => onTitleChange(event.target.value)}
|
||||
placeholder="Issue title"
|
||||
required
|
||||
value={title}
|
||||
/>
|
||||
<Textarea
|
||||
onChange={(event) => onBodyChange(event.target.value)}
|
||||
placeholder="Describe the issue..."
|
||||
required
|
||||
rows={4}
|
||||
value={body}
|
||||
/>
|
||||
<Button className="w-full" disabled={busy} type="submit">
|
||||
<Plus data-icon="inline-start" />
|
||||
{busy ? "Creating" : "Create issue"}
|
||||
</Button>
|
||||
@@ -237,89 +218,55 @@ interface IssueListProps {
|
||||
issueId: Id<"projectIssues">,
|
||||
issueNumber: number,
|
||||
title: string
|
||||
) => Promise<void>;
|
||||
) => void;
|
||||
}
|
||||
|
||||
const IssueList = ({ issues, pendingAction, onStart }: IssueListProps) => {
|
||||
const renderContent = () => {
|
||||
if (issues === undefined) {
|
||||
return <p className="text-sm text-muted-foreground">Loading issues...</p>;
|
||||
}
|
||||
if (issues.length === 0) {
|
||||
return (
|
||||
<p className="border p-4 text-sm text-muted-foreground">
|
||||
No issues yet. Describe a concrete change to start the workflow.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (issues === undefined) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{issues.map((issue) => {
|
||||
const canStart = ["open", "needs-input", "failed"].includes(
|
||||
issue.status
|
||||
);
|
||||
const busy = pendingAction === `issue:${issue._id}`;
|
||||
let actionLabel: string = issue.status;
|
||||
if (canStart) {
|
||||
actionLabel = "Start agent";
|
||||
}
|
||||
if (busy) {
|
||||
actionLabel = "Dispatching";
|
||||
}
|
||||
return (
|
||||
<article
|
||||
className="grid gap-3 border bg-card p-4 md:grid-cols-[1fr_auto] md:items-center"
|
||||
key={issue._id}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
#{issue.number}
|
||||
</span>
|
||||
<span
|
||||
className={`border px-1.5 py-0.5 text-[11px] font-medium ${statusStyle[issue.status]}`}
|
||||
>
|
||||
{issue.status}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-medium">{issue.title}</h3>
|
||||
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
||||
{issue.body}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!canStart || busy}
|
||||
onClick={() =>
|
||||
void onStart(issue._id, issue.number, issue.title)
|
||||
}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={canStart ? "default" : "outline"}
|
||||
>
|
||||
{canStart ? (
|
||||
<Play data-icon="inline-start" />
|
||||
) : (
|
||||
<Bot data-icon="inline-start" />
|
||||
)}
|
||||
{actionLabel}
|
||||
</Button>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Loading issues...</p>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
if (issues.length === 0) {
|
||||
return (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No issues yet. Create one to start agent work.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section aria-labelledby="issue-heading" className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitPullRequest className="size-4 text-muted-foreground" />
|
||||
<h2 className="text-lg font-semibold" id="issue-heading">
|
||||
Issue queue
|
||||
</h2>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</section>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-sm font-semibold">Issues</h2>
|
||||
{issues.map((issue) => (
|
||||
<div className="border p-3" key={issue._id}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
#{issue.number} · {issue.title}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded border px-2 py-0.5 text-[10px] font-medium ${statusStyle[issue.status]}`}
|
||||
>
|
||||
{issue.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{issue.body}
|
||||
</p>
|
||||
{(issue.status === "open" || issue.status === "failed") && (
|
||||
<Button
|
||||
className="mt-2"
|
||||
disabled={pendingAction === `issue:${issue._id}`}
|
||||
onClick={() => onStart(issue._id, issue.number, issue.title)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Play data-icon="inline-start" />
|
||||
{pendingAction === `issue:${issue._id}` ? "Starting" : "Start"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -332,6 +279,7 @@ export const ProjectWorkspacePage = () => {
|
||||
const handleIssueSubmit = workspace.raiseIssue;
|
||||
const handleIssueTitleChange = workspace.setIssueTitle;
|
||||
const handleIssueStart = workspace.startIssue;
|
||||
const projectSource = workspace.selectedProject?.sources[0];
|
||||
|
||||
return (
|
||||
<main className="min-h-svh bg-background text-foreground">
|
||||
@@ -344,7 +292,7 @@ export const ProjectWorkspacePage = () => {
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Project manager</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
GitHub, Flue, AgentOS
|
||||
Git · Flue · AgentOS
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,21 +327,19 @@ export const ProjectWorkspacePage = () => {
|
||||
<div className="flex flex-col gap-3 border-b pb-5 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connected GitHub project
|
||||
Connected repository
|
||||
</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{workspace.selectedProject.repoOwner}/
|
||||
{workspace.selectedProject.repoName}
|
||||
{workspace.selectedProject.name}
|
||||
</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||
{workspace.selectedProject.description ??
|
||||
"No repository description is available."}
|
||||
{projectSource?.host}/{projectSource?.repositoryPath}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
workspace.selectedProject?.repoUrl,
|
||||
projectSource?.url,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
)
|
||||
@@ -402,7 +348,7 @@ export const ProjectWorkspacePage = () => {
|
||||
variant="outline"
|
||||
>
|
||||
<ExternalLink data-icon="inline-start" />
|
||||
Open GitHub
|
||||
Open repository
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -429,7 +375,7 @@ export const ProjectWorkspacePage = () => {
|
||||
<div className="max-w-sm space-y-2">
|
||||
<GitFork className="mx-auto size-7 text-muted-foreground" />
|
||||
<h1 className="text-xl font-semibold">
|
||||
Connect your first repository
|
||||
Import your first repository
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Zopu creates the project artifact set and opens an issue queue
|
||||
|
||||
@@ -17,11 +17,12 @@ export const useProjectWorkspace = () => {
|
||||
const [issueBody, setIssueBody] = useState("");
|
||||
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const connectGitHub = useAction(api.projects.connectGitHub);
|
||||
const importPublicGit = useAction(api.projects.importPublicGit);
|
||||
const createIssue = useMutation(api.projectIssues.create);
|
||||
const beginIssue = useMutation(api.projectIssues.begin);
|
||||
const markDispatchFailed = useMutation(api.projectIssues.markDispatchFailed);
|
||||
const activeProjectId = selectedProjectId ?? projects?.[0]?._id ?? null;
|
||||
const activeProjectId =
|
||||
(selectedProjectId ?? (projects?.[0]?.id as unknown as Id<"projects">)) ?? null;
|
||||
const artifacts = useQuery(
|
||||
api.projectArtifacts.list,
|
||||
activeProjectId ? { projectId: activeProjectId } : "skip"
|
||||
@@ -35,8 +36,8 @@ export const useProjectWorkspace = () => {
|
||||
setPendingAction("connect");
|
||||
setError(null);
|
||||
try {
|
||||
const projectId = await connectGitHub({ repository });
|
||||
setSelectedProjectId(projectId);
|
||||
const outcome = await importPublicGit({ repositoryUrl: repository });
|
||||
setSelectedProjectId(outcome.id as unknown as Id<"projects">);
|
||||
setRepository("");
|
||||
} catch (caughtError) {
|
||||
setError(errorMessage(caughtError));
|
||||
@@ -87,9 +88,8 @@ export const useProjectWorkspace = () => {
|
||||
setPendingAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedProject =
|
||||
projects?.find((project) => project._id === activeProjectId) ?? null;
|
||||
projects?.find((project) => project.id === (activeProjectId as unknown as string)) ?? null;
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Zopo Work OS — Product Design
|
||||
# Zopu Work OS — Product Design
|
||||
|
||||
> **Status:** canonical working interaction/interface specification · **Audience:** product design, frontend/product engineering, agent experience · **Scope:** information architecture, screens, cards, interaction patterns, states, actions, principles · **Excludes:** backend and low-level infrastructure
|
||||
|
||||
@@ -281,7 +281,7 @@ Use explicit actions, never vague “continue”: **Approve plan; answer questio
|
||||
|
||||
```text
|
||||
Deploy preview
|
||||
Project: Zopo Web · Branch: work/W-103/team-invitations
|
||||
Project: Zopu Web · Branch: work/W-103/team-invitations
|
||||
Environment: Temporary preview · External impact: None
|
||||
Expected duration: 20 minutes
|
||||
[Deploy preview]
|
||||
@@ -364,4 +364,4 @@ Within seconds, users can answer: **What should I focus on? What progressed with
|
||||
|
||||
## 25. Core experience statement
|
||||
|
||||
Zopo should feel like supervising a capable operating system, not operating a collection of agents: one workspace shows meaningful work; the user resolves the few things requiring judgment and continues conversation where needed; the system handles context, execution, progress tracking, and knowledge retention beneath a calm surface organized around persistent outcomes.
|
||||
Zopu should feel like supervising a capable operating system, not operating a collection of agents: one workspace shows meaningful work; the user resolves the few things requiring judgment and continues conversation where needed; the system handles context, execution, progress tracking, and knowledge retention beneath a calm surface organized around persistent outcomes.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Zopo Work OS — Product
|
||||
# Zopu Work OS — Product
|
||||
|
||||
> Working spec · product/design/ops/research/agent teams · concepts, behavior, boundaries, value. Excludes implementation, infra, tech choices, low-level UI.
|
||||
|
||||
## 1. Summary
|
||||
|
||||
Zopo Work OS is a persistent OS for technical founders, product engineers, and small teams managing the full lifecycle of building and growing software across products, companies, repos, customers, and functions. It replaces fragmented chats, trackers, docs, and manually coordinated AI agents with one coherent system organized around **units of work**.
|
||||
Zopu Work OS is a persistent OS for technical founders, product engineers, and small teams managing the full lifecycle of building and growing software across products, companies, repos, customers, and functions. It replaces fragmented chats, trackers, docs, and manually coordinated AI agents with one coherent system organized around **units of work**.
|
||||
|
||||
One continuous workspace discovers work, builds understanding, coordinates execution, requests human judgment when needed, tracks outcomes, and retains knowledge. Not a better chat app or just another PM tool — it continuously converts company signals into structured, persistent, executable work.
|
||||
|
||||
@@ -26,7 +26,7 @@ Technical-first because code is a clear execution domain (measurable artifacts,
|
||||
| **Across AI sessions** | Chat/thread-organized work forces repeated decisions: which conversation, what context to restate, which model/agent, where to store output, how it relates to existing work, what changed after shipping. Conversation becomes structure though conversations are temporary and work is persistent. |
|
||||
| **Founders as coordination** | Small-team founders bridge every system/function: remember decisions, connect feedback to work, check blockers, move work between tools, review AI output, decide next steps. The founder becomes the human integration bus. |
|
||||
|
||||
Zopo removes this burden by maintaining a persistent model of work, context, state, evidence, execution, and outcomes.
|
||||
Zopu removes this burden by maintaining a persistent model of work, context, state, evidence, execution, and outcomes.
|
||||
|
||||
## 4. Thesis
|
||||
|
||||
@@ -161,7 +161,7 @@ Accumulates several kinds; each retains its scope (personal prefs, project facts
|
||||
|
||||
| Boundary | Rule |
|
||||
| --- | --- |
|
||||
| **Control layer, not tool replacement** | Specialist systems (source control, support, comms, analytics, billing, deployment, docs) keep native records/actions. Zopo provides the unified model: what matters, its state, next step, whether outcome worked. |
|
||||
| **Control layer, not tool replacement** | Specialist systems (source control, support, comms, analytics, billing, deployment, docs) keep native records/actions. Zopu provides the unified model: what matters, its state, next step, whether outcome worked. |
|
||||
| **Own the work graph** | Connects organizations→projects→goals→signals→work units→dependencies→decisions→runs→artifacts→results→learnings. Durable source of coordination/context. |
|
||||
| **Execution is replaceable** | Execution is a capability beneath the work unit. Users shouldn't care which agent/model/harness ran unless inspection is useful. |
|
||||
| **No unnecessary AI config** | Users shouldn't normally choose model, thinking level, agent framework, context-window strategy, execution runtime, tool routing. System chooses by task/risk/cost/policy. |
|
||||
@@ -225,4 +225,4 @@ Not intended to be: a full external-tool replacement; a general social collabora
|
||||
|
||||
## 20. Working statement
|
||||
|
||||
Zopo Work OS is an autonomous OS for technical founders and small product teams. It observes work across the company, turns fragmented signals into persistent units of work, progresses them via agents and tools, and brings the human in only when judgment, permission, or direction is required. One continuous workspace moves from idea/problem to verified, shipped, measurable outcome while preserving context and building durable understanding over time.
|
||||
Zopu Work OS is an autonomous OS for technical founders and small product teams. It observes work across the company, turns fragmented signals into persistent units of work, progresses them via agents and tools, and brings the human in only when judgment, permission, or direction is required. One continuous workspace moves from idea/problem to verified, shipped, measurable outcome while preserving context and building durable understanding over time.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Zopo Work OS — Technical Architecture
|
||||
# Zopu Work OS — Technical Architecture
|
||||
|
||||
> Working spec · engineering/platform/infra/security/agent teams · starts from an existing Better-T-Stack monorepo (web + Expo + shared packages).
|
||||
|
||||
@@ -107,7 +107,7 @@ Known first-party templates may run in lighter env; imported arbitrary repos def
|
||||
|
||||
```
|
||||
/ README.md, AGENTS.md, product.md, design.md, tech.md
|
||||
/.zopo/ project.yaml, services.yaml, policies.yaml
|
||||
/.zopu/ project.yaml, services.yaml, policies.yaml
|
||||
lifecycle/{setup,resume,verify,preview} skills/{testing,reviewing,deploying}/SKILL.md
|
||||
/apps/
|
||||
```
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ConvexError, v } from "convex/values";
|
||||
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { requireProjectMember } from "./authz";
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ export const makeProjectMutationStore = (ctx: MutationCtx) => ({
|
||||
// Action store — calls narrowly scoped internal mutations/queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const makeProjectActionStore = (ctx: ActionCtx) => ({
|
||||
export const makeProjectActionStore = (_ctx: ActionCtx) => ({
|
||||
getCurrentOrganization: async (userId: string) => {
|
||||
return userId;
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { v } from "convex/values";
|
||||
|
||||
import {
|
||||
preparePublicGitSource,
|
||||
decodePublicGitImportResult,
|
||||
type ProjectImportOutcome,
|
||||
type ProjectView,
|
||||
type PublicGitImportResult,
|
||||
@@ -11,7 +9,6 @@ import {
|
||||
import { Effect } from "effect";
|
||||
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { internal } from "./_generated/api";
|
||||
import { action, internalMutation, query } from "./_generated/server";
|
||||
import { requireCurrentOrganization } from "./authz";
|
||||
import { createInitialArtifacts } from "./artifactModel";
|
||||
|
||||
Reference in New Issue
Block a user