saveTask: accept requires_filesystem; opt in for the onboarding kickoff task

- saveTask now takes an optional requiresFilesystem boolean. Omitted
  → column keeps its current value on updates, defaults to 0 on
  inserts. Explicit true/false → written through.
- Onboarding kickoff (the CEO's very first task) sets
  requiresFilesystem=true. The task description already tells the
  agent to list directories and read README/package.json — that is
  filesystem work, so the task must hold the workspace exclusive.
- Other saveTask call sites (automation reassign, routes-tasks patch,
  create_task, meetings) stay implicit — update paths preserve the
  existing column value, new user-created tasks default to 0 (safe
  cognitive default) and can be opted in later by the creator.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 15:36:08 -04:00
1 parent c3dd08ec65
commit 15e3863cba
2 files changed
+59 -20

No files matched your search

+54 -20
View File
@@ -57,6 +57,16 @@ export function buildTaskCrudMethods(
workspaceId?: string | null;
priority: string;
status: string;
/**
* Per-task filesystem lock policy (v52+). When omitted the
* column defaults to 0 at the SQL layer for inserts, and the
* existing value is preserved on updates. Callers opt in with
* `true` when they know the task will read/write the working
* tree (CEO kickoff reads the repo, CTO code changes mutate
* it, etc.). Leaving it undefined is the safe default for
* purely cognitive tasks.
*/
requiresFilesystem?: boolean;
}): string {
const id = ensureId(input.id ?? null);
const ts = nowIso();
@@ -68,29 +78,52 @@ export function buildTaskCrudMethods(
.prepare("select id from tasks where id = ? and company_id = ?")
.get(id, input.companyId) as { id: string } | undefined;
if (existing) {
getDb()
.prepare(
// eslint-disable-next-line no-restricted-syntax -- B2 Phase 1 grandfathered: saveTask upsert path writes the full row (including status) as a single atomic UPDATE. The status→phase trigger keeps the shadow phase column in sync; no lease is ever held by saveTask callers (lease writes live behind checkoutTask).
"update tasks set project_id = ?, goal_id = ?, parent_id = ?, title = ?, description = ?, assignee_agent_id = ?, workspace_id = ?, priority = ?, status = ?, updated_at = ? where id = ? and company_id = ?",
)
.run(
normalizeOptionalId(input.projectId),
normalizeOptionalId(input.goalId),
normalizeOptionalId(input.parentId),
input.title,
input.description,
assigneeAgentId,
normalizeOptionalId(input.workspaceId),
input.priority,
input.status,
ts,
id,
input.companyId,
);
if (input.requiresFilesystem === undefined) {
getDb()
.prepare(
// eslint-disable-next-line no-restricted-syntax -- B2 Phase 1 grandfathered: saveTask upsert path writes the full row (including status) as a single atomic UPDATE. The status→phase trigger keeps the shadow phase column in sync; no lease is ever held by saveTask callers (lease writes live behind checkoutTask).
"update tasks set project_id = ?, goal_id = ?, parent_id = ?, title = ?, description = ?, assignee_agent_id = ?, workspace_id = ?, priority = ?, status = ?, updated_at = ? where id = ? and company_id = ?",
)
.run(
normalizeOptionalId(input.projectId),
normalizeOptionalId(input.goalId),
normalizeOptionalId(input.parentId),
input.title,
input.description,
assigneeAgentId,
normalizeOptionalId(input.workspaceId),
input.priority,
input.status,
ts,
id,
input.companyId,
);
} else {
getDb()
.prepare(
// eslint-disable-next-line no-restricted-syntax -- B2 Phase 1 grandfathered: saveTask upsert path writes the full row (including status) as a single atomic UPDATE. The status→phase trigger keeps the shadow phase column in sync; no lease is ever held by saveTask callers (lease writes live behind checkoutTask).
"update tasks set project_id = ?, goal_id = ?, parent_id = ?, title = ?, description = ?, assignee_agent_id = ?, workspace_id = ?, priority = ?, status = ?, requires_filesystem = ?, updated_at = ? where id = ? and company_id = ?",
)
.run(
normalizeOptionalId(input.projectId),
normalizeOptionalId(input.goalId),
normalizeOptionalId(input.parentId),
input.title,
input.description,
assigneeAgentId,
normalizeOptionalId(input.workspaceId),
input.priority,
input.status,
input.requiresFilesystem ? 1 : 0,
ts,
id,
input.companyId,
);
}
} else {
getDb()
.prepare(
"insert into tasks (id, company_id, project_id, goal_id, parent_id, title, description, assignee_agent_id, workspace_id, status, priority, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"insert into tasks (id, company_id, project_id, goal_id, parent_id, title, description, assignee_agent_id, workspace_id, status, priority, requires_filesystem, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.run(
id,
@@ -104,6 +137,7 @@ export function buildTaskCrudMethods(
normalizeOptionalId(input.workspaceId),
input.status,
input.priority,
input.requiresFilesystem ? 1 : 0,
ts,
ts,
);