Projects
Project:
{
"id": 1,
"t3ProjectId": "proj_abc",
"title": "stage",
"workspaceRoot": "/home/user/workspace/stage",
"source": "app",
"archivedAt": null,
"createdAt": "2026-11-08T10:00:00Z",
"updatedAt": "2026-11-08T10:00:00Z",
"lastActivityAt": "2026-11-08T11:30:00Z"
}lastActivityAt: latest of the project's thread updatedAt / settledAt (incl. subagents) and task updatedAt; null when none.
T3 is the source of truth: title, workspaceRoot and archivedAt follow T3 on every sync (empty T3 title → workspaceRoot). Projects are read-only here; rename/archive in T3. A project deleted in T3 stays (tasks reference it) and is archived (archivedAt = T3 deletedAt unless already archived).
GET /api/projects
Query: archived = true (only archived) | false (default, only active) | all.
200 { "projects": Project[] } ordered by title.
POST /api/projects
Body: { "title": string, "workspaceRoot": string } (both required, trimmed; absolute roots are path-cleaned, e.g. trailing / dropped).
- An app project with the same
workspaceRootexists → 409. - A T3 project with the same
workspaceRootexists (already imported, or found after refreshingt3_project_list) → return it as mirrored (source: "t3"):titleand archive state stay T3's; the requestedtitleis ignored. Projects deleted in T3 do not count: their root can be created again. - Otherwise create it in T3 (
t3_project_create) →source: "app".
201 Project. 502 T3 error. 503 not connected.
GET /api/projects/:id
200 Project. 404.