feat: project workspace clear/update UX and creation docs
Add granular workspace management — clear local folder or repo URL independently instead of deleting the whole workspace. Fix project create route typing. Document inline workspace creation in API docs and skill references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,17 +70,30 @@ POST /api/companies/{companyId}/projects
|
||||
{
|
||||
"name": "Auth System",
|
||||
"description": "End-to-end authentication",
|
||||
"goalId": "{goalId}",
|
||||
"status": "active"
|
||||
"goalIds": ["{goalId}"],
|
||||
"status": "planned",
|
||||
"workspace": {
|
||||
"name": "auth-repo",
|
||||
"cwd": "/path/to/workspace",
|
||||
"repoUrl": "https://github.com/org/repo",
|
||||
"repoRef": "main",
|
||||
"isPrimary": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `workspace` is optional. If present, the project is created and seeded with that workspace.
|
||||
- A workspace must include at least one of `cwd` or `repoUrl`.
|
||||
- For repo-only projects, omit `cwd` and provide `repoUrl`.
|
||||
|
||||
### Update Project
|
||||
|
||||
```
|
||||
PATCH /api/projects/{projectId}
|
||||
{
|
||||
"status": "completed"
|
||||
"status": "in_progress"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -35,9 +35,11 @@ export function projectRoutes(db: Db) {
|
||||
router.post("/companies/:companyId/projects", validate(createProjectSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const { workspace, ...projectData } = req.body as Record<string, unknown> & {
|
||||
workspace?: Record<string, unknown>;
|
||||
type CreateProjectPayload = Parameters<typeof svc.create>[1] & {
|
||||
workspace?: Parameters<typeof svc.createWorkspace>[1];
|
||||
};
|
||||
|
||||
const { workspace, ...projectData } = req.body as CreateProjectPayload;
|
||||
const project = await svc.create(companyId, projectData);
|
||||
let createdWorkspaceId: string | null = null;
|
||||
if (workspace) {
|
||||
|
||||
@@ -75,6 +75,19 @@ Status values: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`,
|
||||
|
||||
**Step 9 — Delegate if needed.** Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. Set `billingCode` for cross-team work.
|
||||
|
||||
## Project Setup Workflow (CEO/Manager Common Path)
|
||||
|
||||
When asked to set up a new project with workspace config (local folder and/or GitHub repo), use:
|
||||
|
||||
1. `POST /api/companies/{companyId}/projects` with project fields.
|
||||
2. Optionally include `workspace` in that same create call, or call `POST /api/projects/{projectId}/workspaces` right after create.
|
||||
|
||||
Workspace rules:
|
||||
|
||||
- Provide at least one of `cwd` (local folder) or `repoUrl` (remote repo).
|
||||
- For repo-only setup, omit `cwd` and provide `repoUrl`.
|
||||
- Include both `cwd` + `repoUrl` when local and remote references should both be tracked.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- **Always checkout** before working. Never PATCH to `in_progress` manually.
|
||||
@@ -152,6 +165,8 @@ pls show the costs in either token or dollars on the /issues/{id} page. Make a p
|
||||
| Update task | `PATCH /api/issues/:issueId` (optional `comment` field) |
|
||||
| Add comment | `POST /api/issues/:issueId/comments` |
|
||||
| Create subtask | `POST /api/companies/:companyId/issues` |
|
||||
| Create project | `POST /api/companies/:companyId/projects` |
|
||||
| Create project workspace | `POST /api/projects/:projectId/workspaces` |
|
||||
| Release task | `POST /api/issues/:issueId/release` |
|
||||
| List agents | `GET /api/companies/:companyId/agents` |
|
||||
| Dashboard | `GET /api/companies/:companyId/dashboard` |
|
||||
|
||||
@@ -280,6 +280,58 @@ Use the dashboard for situational awareness, especially if you're a manager or C
|
||||
|
||||
---
|
||||
|
||||
## Project Setup (Create + Workspace)
|
||||
|
||||
When a CEO/manager task asks you to "set up a new project" and wire local + GitHub context, use this sequence.
|
||||
|
||||
### Option A: One-call create with workspace
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/projects
|
||||
{
|
||||
"name": "Paperclip Mobile App",
|
||||
"description": "Ship iOS + Android client",
|
||||
"status": "planned",
|
||||
"goalIds": ["{goalId}"],
|
||||
"workspace": {
|
||||
"name": "paperclip-mobile",
|
||||
"cwd": "/Users/me/paperclip-mobile",
|
||||
"repoUrl": "https://github.com/acme/paperclip-mobile",
|
||||
"repoRef": "main",
|
||||
"isPrimary": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Two calls (project first, then workspace)
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/projects
|
||||
{
|
||||
"name": "Paperclip Mobile App",
|
||||
"description": "Ship iOS + Android client",
|
||||
"status": "planned"
|
||||
}
|
||||
|
||||
POST /api/projects/{projectId}/workspaces
|
||||
{
|
||||
"cwd": "/Users/me/paperclip-mobile",
|
||||
"repoUrl": "https://github.com/acme/paperclip-mobile",
|
||||
"repoRef": "main",
|
||||
"isPrimary": true
|
||||
}
|
||||
```
|
||||
|
||||
Workspace rules:
|
||||
|
||||
- Provide at least one of `cwd` or `repoUrl`.
|
||||
- For repo-only setup, omit `cwd` and provide `repoUrl`.
|
||||
- The first workspace is primary by default.
|
||||
|
||||
Project responses include `primaryWorkspace` and `workspaces`, which agents can use for execution context resolution.
|
||||
|
||||
---
|
||||
|
||||
## Governance and Approvals
|
||||
|
||||
Some actions require board approval. You cannot bypass these gates.
|
||||
@@ -406,7 +458,7 @@ Terminal states: `done`, `cancelled`
|
||||
| GET | `/api/companies/:companyId` | Company details |
|
||||
| GET | `/api/companies/:companyId/projects` | List projects |
|
||||
| GET | `/api/projects/:projectId` | Project details |
|
||||
| POST | `/api/companies/:companyId/projects` | Create project |
|
||||
| POST | `/api/companies/:companyId/projects` | Create project (optional inline `workspace`) |
|
||||
| PATCH | `/api/projects/:projectId` | Update project |
|
||||
| GET | `/api/projects/:projectId/workspaces` | List project workspaces |
|
||||
| POST | `/api/projects/:projectId/workspaces` | Create project workspace |
|
||||
|
||||
@@ -83,6 +83,11 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
|
||||
mutationFn: (workspaceId: string) => projectsApi.removeWorkspace(project.id, workspaceId),
|
||||
onSuccess: invalidateProject,
|
||||
});
|
||||
const updateWorkspace = useMutation({
|
||||
mutationFn: ({ workspaceId, data }: { workspaceId: string; data: Record<string, unknown> }) =>
|
||||
projectsApi.updateWorkspace(project.id, workspaceId, data),
|
||||
onSuccess: invalidateProject,
|
||||
});
|
||||
|
||||
const removeGoal = (goalId: string) => {
|
||||
if (!onUpdate) return;
|
||||
@@ -167,6 +172,41 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
|
||||
});
|
||||
};
|
||||
|
||||
const clearLocalWorkspace = (workspace: Project["workspaces"][number]) => {
|
||||
const confirmed = window.confirm(
|
||||
workspace.repoUrl
|
||||
? "Clear local folder from this workspace?"
|
||||
: "Delete this workspace local folder?",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (workspace.repoUrl) {
|
||||
updateWorkspace.mutate({
|
||||
workspaceId: workspace.id,
|
||||
data: { cwd: null },
|
||||
});
|
||||
return;
|
||||
}
|
||||
removeWorkspace.mutate(workspace.id);
|
||||
};
|
||||
|
||||
const clearRepoWorkspace = (workspace: Project["workspaces"][number]) => {
|
||||
const hasLocalFolder = Boolean(workspace.cwd && workspace.cwd !== REPO_ONLY_CWD_SENTINEL);
|
||||
const confirmed = window.confirm(
|
||||
hasLocalFolder
|
||||
? "Clear GitHub repo from this workspace?"
|
||||
: "Delete this workspace repo?",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (hasLocalFolder) {
|
||||
updateWorkspace.mutate({
|
||||
workspaceId: workspace.id,
|
||||
data: { repoUrl: null, repoRef: null },
|
||||
});
|
||||
return;
|
||||
}
|
||||
removeWorkspace.mutate(workspace.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
@@ -285,13 +325,8 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => {
|
||||
const confirmed = window.confirm("Delete this workspace?");
|
||||
if (confirmed) {
|
||||
removeWorkspace.mutate(workspace.id);
|
||||
}
|
||||
}}
|
||||
aria-label="Delete workspace"
|
||||
onClick={() => clearLocalWorkspace(workspace)}
|
||||
aria-label="Delete local folder"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -312,13 +347,8 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => {
|
||||
const confirmed = window.confirm("Delete this workspace?");
|
||||
if (confirmed) {
|
||||
removeWorkspace.mutate(workspace.id);
|
||||
}
|
||||
}}
|
||||
aria-label="Delete workspace"
|
||||
onClick={() => clearRepoWorkspace(workspace)}
|
||||
aria-label="Delete workspace repo"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -427,6 +457,9 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
|
||||
{removeWorkspace.isError && (
|
||||
<p className="text-xs text-destructive">Failed to delete workspace.</p>
|
||||
)}
|
||||
{updateWorkspace.isError && (
|
||||
<p className="text-xs text-destructive">Failed to update workspace.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
Reference in New Issue
Block a user