@hasna/todos 0.15.7 → 0.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.15.7",
2126
+ version: "0.15.9",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -5273,12 +5273,16 @@ function protectRemoteClient(client) {
5273
5273
  function remoteAuthorityBase(client) {
5274
5274
  return client.baseUrl.replace(/\/v1\/?$/, "");
5275
5275
  }
5276
- async function requiredRemoteRoute(client, route, request) {
5276
+ async function requiredRemoteRoute(client, route, request, recognized404Codes = []) {
5277
5277
  try {
5278
5278
  return await request();
5279
5279
  } catch (error) {
5280
5280
  const status = error && typeof error === "object" ? error.status : undefined;
5281
5281
  if (status === 404) {
5282
+ const body = error && typeof error === "object" ? error.body : undefined;
5283
+ const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
5284
+ if (typeof code === "string" && recognized404Codes.includes(code))
5285
+ throw error;
5282
5286
  throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} does not expose ${route}; ` + "deploy the @hasna/todos /v1 server contract before retrying; local SQLite fallback is disabled", { cause: error });
5283
5287
  }
5284
5288
  throw error;
@@ -5693,6 +5697,15 @@ async function cloudDeleteProject(client, id) {
5693
5697
  }
5694
5698
  return true;
5695
5699
  }
5700
+ async function cloudPlanProjectTaskListEnsure(client, projectId) {
5701
+ return requiredRemoteRoute(client, "/v1/projects/:id/task-list/ensure", () => client.transport.get(`/projects/${encodeURIComponent(projectId)}/task-list/ensure`), ["PROJECT_NOT_FOUND"]);
5702
+ }
5703
+ async function cloudApplyProjectTaskListEnsure(client, projectId, input) {
5704
+ return requiredRemoteRoute(client, "/v1/projects/:id/task-list/ensure", () => client.transport.post(`/projects/${encodeURIComponent(projectId)}/task-list/ensure`, input), ["PROJECT_NOT_FOUND"]);
5705
+ }
5706
+ async function cloudRollbackProjectTaskListEnsure(client, projectId, input) {
5707
+ return requiredRemoteRoute(client, "/v1/projects/:id/task-list/rollback", () => client.transport.post(`/projects/${encodeURIComponent(projectId)}/task-list/rollback`, input), ["PROJECT_NOT_FOUND", "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND"]);
5708
+ }
5696
5709
  async function cloudListPlans(client, projectId) {
5697
5710
  const query = projectId ? { project_id: projectId } : {};
5698
5711
  const res = await requiredRemoteRoute(client, "/v1/plans", () => client.list("plans", { query }));
@@ -13716,6 +13729,40 @@ function deleteTaskList(id, db) {
13716
13729
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
13717
13730
  })();
13718
13731
  }
13732
+ function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
13733
+ const d = db || getDatabase();
13734
+ return d.transaction(() => {
13735
+ const current = getTaskList(id, d);
13736
+ if (!current) {
13737
+ return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
13738
+ }
13739
+ const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
13740
+ if (changed) {
13741
+ return { status: "changed", task_dependents: 0, plan_dependents: 0 };
13742
+ }
13743
+ const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
13744
+ const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
13745
+ if (taskDependents > 0 || planDependents > 0) {
13746
+ return {
13747
+ status: "has_dependents",
13748
+ task_dependents: taskDependents,
13749
+ plan_dependents: planDependents
13750
+ };
13751
+ }
13752
+ recordStorageTombstone({
13753
+ object_type: "task_lists",
13754
+ object_id: id,
13755
+ payload: current
13756
+ }, d);
13757
+ releaseCanonicalSlugClaims("task_list", id, d);
13758
+ const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
13759
+ return {
13760
+ status: deleted ? "deleted" : "not_found",
13761
+ task_dependents: 0,
13762
+ plan_dependents: 0
13763
+ };
13764
+ })();
13765
+ }
13719
13766
  function ensureTaskList(name, slug, projectId, db) {
13720
13767
  const d = db || getDatabase();
13721
13768
  const existing = getTaskListBySlug(slug, projectId, d);
@@ -19673,6 +19720,23 @@ function normalizeAgentNameInput(name) {
19673
19720
  import { readFileSync as readFileSync4 } from "fs";
19674
19721
  import { homedir as homedir3 } from "os";
19675
19722
  import { join as join8 } from "path";
19723
+ function describeAssigneeFilter(input, ctx) {
19724
+ const raw = input.trim();
19725
+ const normalized = normalizeAgentNameInput(raw);
19726
+ if (!normalized)
19727
+ return { kind: "ok" };
19728
+ if (ctx.agents.some((a) => a.id.toLowerCase() === normalized))
19729
+ return { kind: "ok" };
19730
+ const byName = ctx.agents.filter((a) => normalizeAgentNameInput(a.name) === normalized);
19731
+ if (byName.length <= 1)
19732
+ return { kind: "ok" };
19733
+ const ids = byName.map((a) => a.id).sort();
19734
+ return {
19735
+ kind: "ambiguous",
19736
+ candidates: byName,
19737
+ message: `'${raw}' names ${byName.length} registered agents (${ids.join(", ")}), so this result is INCOMPLETE \u2014 ` + `rows whose assignee was stored as one of the other ids are missing from it. ` + `Re-run with an agent ID instead to get that agent's full queue.`
19738
+ };
19739
+ }
19676
19740
  function defaultSeatRosterPath() {
19677
19741
  return process.env["TODOS_SEAT_ROSTER_PATH"] || join8(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
19678
19742
  }
@@ -21337,6 +21401,7 @@ function registerTaskCommands(program2) {
21337
21401
  }
21338
21402
  if (assignedFilter !== undefined)
21339
21403
  filter["assigned_to"] = assignedFilter;
21404
+ const rosterPromise = assignedWasTyped && assignedFilter ? loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), true).catch(() => ({ agents: [], seats: new Set, allowSeat: true, degraded: true })) : undefined;
21340
21405
  if (opts.recurring)
21341
21406
  filter["has_recurrence"] = true;
21342
21407
  if (opts.limit !== undefined) {
@@ -21407,15 +21472,22 @@ function registerTaskCommands(program2) {
21407
21472
  }
21408
21473
  if (withholdLimit && requestedLimit !== undefined)
21409
21474
  tasks = tasks.slice(0, requestedLimit);
21410
- if (assignedWasTyped && assignedFilter && tasks.length === 0) {
21475
+ if (rosterPromise && assignedFilter) {
21411
21476
  try {
21412
- const roster = await loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), true);
21477
+ const roster = await rosterPromise;
21413
21478
  if (!roster.degraded) {
21414
- const target = canonicalAgentRef(assignedFilter);
21415
- const known = roster.agents.some((a) => canonicalAgentRef(a.name) === target || canonicalAgentRef(a.id) === target);
21416
- if (!known) {
21417
- console.error(chalk3.yellow(`Warning: no agent named '${assignedFilter}' is registered, so this empty result may be a
21479
+ if (tasks.length === 0) {
21480
+ const target = canonicalAgentRef(assignedFilter);
21481
+ const known = roster.agents.some((a) => canonicalAgentRef(a.name) === target || canonicalAgentRef(a.id) === target);
21482
+ if (!known) {
21483
+ console.error(chalk3.yellow(`Warning: no agent named '${assignedFilter}' is registered, so this empty result may be a
21418
21484
  ` + ` mistyped name rather than an empty queue. Check with 'todos agents'.`));
21485
+ }
21486
+ } else {
21487
+ const notice = describeAssigneeFilter(assignedFilter, { agents: roster.agents });
21488
+ if (notice.kind === "ambiguous") {
21489
+ console.error(chalk3.yellow(`Warning: ${notice.message}`));
21490
+ }
21419
21491
  }
21420
21492
  }
21421
21493
  } catch {}
@@ -22333,6 +22405,7 @@ var init_task_commands = __esm(() => {
22333
22405
  init_claim_guard();
22334
22406
  init_assignee_guard();
22335
22407
  init_assignee_context();
22408
+ init_assignee_validation();
22336
22409
  init_agents();
22337
22410
  init_helpers();
22338
22411
  init_output_redaction();
@@ -25036,267 +25109,1365 @@ var init_sync = __esm(() => {
25036
25109
  init_config();
25037
25110
  });
25038
25111
 
25039
- // src/lib/project-bootstrap.ts
25040
- var exports_project_bootstrap = {};
25041
- __export(exports_project_bootstrap, {
25042
- discoverProjectWorkspace: () => discoverProjectWorkspace,
25043
- bootstrapProject: () => bootstrapProject
25044
- });
25045
- import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
25046
- import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
25047
- function safeStat(path) {
25048
- try {
25049
- return statSync4(path);
25050
- } catch {
25051
- return null;
25052
- }
25053
- }
25054
- function canonicalPath(input) {
25055
- const resolved = resolve11(input);
25056
- const stats = safeStat(resolved);
25057
- if (stats?.isFile())
25058
- return dirname6(resolved);
25059
- return resolved;
25060
- }
25061
- function findUp(start, marker) {
25062
- let current = canonicalPath(start);
25063
- while (true) {
25064
- if (existsSync13(resolve11(current, marker)))
25065
- return current;
25066
- const parent = dirname6(current);
25067
- if (parent === current)
25068
- return null;
25069
- current = parent;
25070
- }
25112
+ // src/lib/integrity.ts
25113
+ function resolveIntegritySeverity(spec, measurement) {
25114
+ if (spec.kind === "dangling")
25115
+ return "error";
25116
+ if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
25117
+ return "error";
25118
+ return spec.base_severity;
25071
25119
  }
25072
- function readPackageJson(path) {
25073
- if (!path)
25074
- return null;
25075
- const file = resolve11(path, "package.json");
25076
- if (!existsSync13(file))
25077
- return null;
25078
- try {
25079
- const parsed = JSON.parse(readFileSync7(file, "utf-8"));
25080
- return parsed && typeof parsed === "object" ? parsed : null;
25081
- } catch {
25082
- return null;
25083
- }
25120
+ function plural(entity, count) {
25121
+ const [one, many] = ENTITY_LABEL[entity];
25122
+ return count === 1 ? one : many;
25084
25123
  }
25085
- function packageDisplayName(name, fallbackPath) {
25086
- if (!name)
25087
- return basename4(fallbackPath);
25088
- const withoutScope = name.startsWith("@") ? name.split("/")[1] : name;
25089
- return withoutScope || basename4(fallbackPath);
25124
+ function formatIntegrityMessage(spec, measurement) {
25125
+ const noun = plural(spec.entity, measurement.count);
25126
+ const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
25127
+ const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
25128
+ return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
25090
25129
  }
25091
- function workspaceMarker(root, rootPackage) {
25092
- if (!root)
25093
- return { kind: null, markers: [] };
25094
- const markers = [];
25095
- if (rootPackage?.workspaces)
25096
- markers.push("package.json#workspaces");
25097
- for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
25098
- if (existsSync13(resolve11(root, marker)))
25099
- markers.push(marker);
25100
- }
25101
- const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
25102
- return { kind, markers };
25130
+ function measuredCondition(spec, measurement, source) {
25131
+ return {
25132
+ id: spec.id,
25133
+ entity: spec.entity,
25134
+ field: spec.field,
25135
+ kind: spec.kind,
25136
+ count: measurement.count,
25137
+ open_count: measurement.open_count,
25138
+ severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
25139
+ verified: true,
25140
+ source,
25141
+ message: formatIntegrityMessage(spec, measurement),
25142
+ impact: spec.impact
25143
+ };
25103
25144
  }
25104
- function discoverProjectWorkspace(inputPath = process.cwd()) {
25105
- const input = canonicalPath(inputPath);
25106
- const gitRoot = findUp(input, ".git");
25107
- const packageRoot = findUp(input, "package.json");
25108
- const rootPackage = readPackageJson(gitRoot);
25109
- const packageMeta = readPackageJson(packageRoot);
25110
- const workspace = workspaceMarker(gitRoot, rootPackage);
25111
- const monorepo = Boolean(gitRoot && packageRoot && packageRoot !== gitRoot && workspace.kind);
25112
- const projectPath = monorepo ? packageRoot : gitRoot ?? packageRoot ?? input;
25113
- const projectName = packageDisplayName(packageMeta?.name ?? rootPackage?.name ?? null, projectPath);
25145
+ function unverifiedCondition(spec, reason) {
25114
25146
  return {
25115
- inputPath: input,
25116
- projectPath,
25117
- projectName,
25118
- gitRoot,
25119
- packageRoot,
25120
- packageName: packageMeta?.name ?? null,
25121
- workspaceRoot: workspace.kind ? gitRoot : null,
25122
- workspaceKind: workspace.kind,
25123
- monorepo,
25124
- markers: workspace.markers
25147
+ id: spec.id,
25148
+ entity: spec.entity,
25149
+ field: spec.field,
25150
+ kind: spec.kind,
25151
+ count: null,
25152
+ open_count: null,
25153
+ severity: null,
25154
+ verified: false,
25155
+ source: "unverified",
25156
+ unverified_reason: reason,
25157
+ message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
25158
+ impact: spec.impact
25125
25159
  };
25126
25160
  }
25127
- function sourceExists(projectId, type, uri, db) {
25128
- return listProjectSources(projectId, db).some((source) => source.type === type && source.uri === uri);
25161
+ function summarizeIntegrity(conditions) {
25162
+ const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
25163
+ const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
25164
+ const unverified = conditions.length - measured.length;
25165
+ return {
25166
+ ok: unverified === 0 && findings.length === 0,
25167
+ findings: findings.length,
25168
+ rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
25169
+ errors: findings.filter((condition) => condition.severity === "error").length,
25170
+ warnings: findings.filter((condition) => condition.severity === "warn").length,
25171
+ unverified,
25172
+ complete: unverified === 0
25173
+ };
25129
25174
  }
25130
- function addSourceOnce(projectId, type, name, uri, metadata, db) {
25131
- if (!uri || sourceExists(projectId, type, uri, db))
25132
- return null;
25133
- return addProjectSource({ project_id: projectId, type, name, uri, metadata }, db);
25175
+ function buildIntegrityReport(conditions, generatedAt) {
25176
+ const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
25177
+ return {
25178
+ schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
25179
+ generated_at: generatedAt,
25180
+ source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
25181
+ conditions,
25182
+ summary: summarizeIntegrity(conditions)
25183
+ };
25134
25184
  }
25135
- function bootstrapProject(options = {}, db) {
25136
- const d = db || getDatabase();
25137
- const discovery = discoverProjectWorkspace(options.path);
25138
- const taskListSlug = options.taskListSlug || `todos-${slugify(options.name || discovery.projectName)}`;
25139
- if (options.dryRun) {
25140
- return {
25141
- dryRun: true,
25142
- discovery: { ...discovery, projectName: options.name || discovery.projectName },
25143
- project: null,
25144
- taskList: null,
25145
- sources: [],
25146
- created: { project: false, taskList: false, sources: [] }
25147
- };
25148
- }
25149
- const beforeProject = getProjectByCanonicalPath(discovery.projectPath, d);
25150
- let project = ensureProject(options.name || discovery.projectName, discovery.projectPath, d);
25151
- const createdProject = !beforeProject;
25152
- if (project.task_list_id !== taskListSlug || options.name && project.name !== options.name) {
25153
- project = renameProject(project.id, {
25154
- name: options.name ?? project.name,
25155
- new_slug: taskListSlug
25156
- }, d).project;
25157
- }
25158
- setMachineLocalPath(project.id, discovery.projectPath, d);
25159
- const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
25160
- let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
25161
- if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
25162
- taskList = updateTaskList(taskList.id, {
25163
- metadata: {
25164
- ...taskList.metadata,
25165
- route_enabled: true,
25166
- automation: {
25167
- ...taskList.metadata.automation && typeof taskList.metadata.automation === "object" && !Array.isArray(taskList.metadata.automation) ? taskList.metadata.automation : {},
25168
- no_auto: false
25169
- }
25170
- }
25171
- }, d);
25172
- }
25173
- const createdSources = [];
25174
- for (const source of [
25175
- addSourceOnce(project.id, "local", "Project root", discovery.projectPath, { role: "project-root" }, d),
25176
- addSourceOnce(project.id, "git", "Git root", discovery.gitRoot, { role: "git-root" }, d),
25177
- addSourceOnce(project.id, "workspace", "Workspace root", discovery.workspaceRoot, {
25178
- role: "workspace-root",
25179
- kind: discovery.workspaceKind,
25180
- markers: discovery.markers,
25181
- monorepo: discovery.monorepo
25182
- }, d)
25183
- ]) {
25184
- if (source) {
25185
- createdSources.push(source.type);
25185
+ function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
25186
+ const received = new Map;
25187
+ if (Array.isArray(raw.conditions)) {
25188
+ for (const entry of raw.conditions) {
25189
+ if (entry && typeof entry["id"] === "string")
25190
+ received.set(entry["id"], entry);
25186
25191
  }
25187
25192
  }
25188
- return {
25189
- dryRun: false,
25190
- discovery: { ...discovery, projectName: options.name || discovery.projectName },
25191
- project,
25192
- taskList,
25193
- sources: listProjectSources(project.id, d),
25194
- created: {
25195
- project: createdProject,
25196
- taskList: !beforeTaskList,
25197
- sources: createdSources
25193
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
25194
+ const entry = received.get(spec.id);
25195
+ const count = entry?.["count"];
25196
+ const verified = entry?.["verified"];
25197
+ if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
25198
+ return unverifiedCondition(spec, entry ? typeof entry["unverified_reason"] === "string" ? String(entry["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
25198
25199
  }
25199
- };
25200
+ const openRaw = entry["open_count"];
25201
+ return measuredCondition(spec, {
25202
+ count,
25203
+ open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
25204
+ }, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
25205
+ });
25206
+ const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
25207
+ return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
25200
25208
  }
25201
- function getProjectByCanonicalPath(path, db) {
25202
- return getProjectByExactPath(path, db) ?? null;
25209
+ function sqliteOpenStatusList() {
25210
+ return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
25203
25211
  }
25204
- function getProjectByExactPath(path, db) {
25205
- return getProjectByPathForBootstrap(path, db);
25212
+ function buildSqliteIntegritySql(spec) {
25213
+ const table = SQLITE_TABLE[spec.entity];
25214
+ const target = SQLITE_TABLE[spec.target];
25215
+ const column = `t."${spec.field}"`;
25216
+ const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
25217
+ const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
25218
+ return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
25206
25219
  }
25207
- function getProjectByPathForBootstrap(path, db) {
25208
- const row = db.query("SELECT * FROM projects WHERE path = ?").get(path);
25209
- if (row)
25210
- return row;
25211
- const machineRow = db.query(`SELECT p.* FROM projects p
25212
- JOIN project_machine_paths pmp ON pmp.project_id = p.id
25213
- WHERE pmp.path = ?`).get(path);
25214
- return machineRow ?? null;
25220
+ function buildPostgresIntegritySql(spec, options) {
25221
+ const params = [options.service, RECORD_TYPE[spec.entity]];
25222
+ const p = (value) => {
25223
+ params.push(value);
25224
+ return `$${params.length}`;
25225
+ };
25226
+ const column = `t.payload->>'${spec.field}'`;
25227
+ const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
25228
+ const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
25229
+ const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
25230
+ return { sql, params };
25215
25231
  }
25216
- var init_project_bootstrap = __esm(() => {
25217
- init_database();
25218
- init_projects();
25219
- init_task_lists();
25220
- });
25221
-
25222
- // src/lib/project-panel.ts
25223
- var exports_project_panel = {};
25224
- __export(exports_project_panel, {
25225
- createTodosProjectPanel: () => createTodosProjectPanel
25226
- });
25227
- import {
25228
- parseContract,
25229
- SCHEMA_IDS
25230
- } from "@hasna/contracts";
25231
- function clampLimit(limit) {
25232
- if (!Number.isFinite(limit ?? 0))
25233
- return 20;
25234
- return Math.max(1, Math.min(100, Math.trunc(limit ?? 20)));
25232
+ function referenceOf(row, field) {
25233
+ const raw = row[field];
25234
+ if (typeof raw !== "string")
25235
+ return null;
25236
+ const trimmed = raw.trim();
25237
+ return trimmed === "" ? null : trimmed;
25235
25238
  }
25236
- function taskUri(id) {
25237
- return `todo://tasks/${id}`;
25239
+ function measureIntegrityRows(spec, sets) {
25240
+ const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
25241
+ if (!rows)
25242
+ return null;
25243
+ const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
25244
+ if (spec.kind === "dangling" && !registered)
25245
+ return null;
25246
+ let count = 0;
25247
+ let open = 0;
25248
+ for (const row of rows) {
25249
+ const reference = referenceOf(row, spec.field);
25250
+ const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
25251
+ if (!matches)
25252
+ continue;
25253
+ count++;
25254
+ if (spec.entity === "task") {
25255
+ const status = row.status;
25256
+ if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
25257
+ open++;
25258
+ }
25259
+ }
25260
+ return { count, open_count: spec.entity === "task" ? open : null };
25238
25261
  }
25239
- function taskResource(task) {
25240
- return {
25241
- kind: "task",
25242
- id: task.id,
25243
- name: task.title,
25244
- uri: taskUri(task.id),
25245
- externalId: task.id,
25246
- sourcePackage: SOURCE_PACKAGE,
25247
- tags: task.tags
25262
+ var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
25263
+ var init_integrity = __esm(() => {
25264
+ init_types();
25265
+ TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
25266
+ OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
25267
+ INTEGRITY_CONDITIONS = [
25268
+ {
25269
+ id: "tasks_without_project",
25270
+ entity: "task",
25271
+ field: "project_id",
25272
+ target: "project",
25273
+ kind: "missing",
25274
+ base_severity: "warn",
25275
+ escalate_when_open: true,
25276
+ impact: "invisible to every project-scoped read (list, status, next, claim)"
25277
+ },
25278
+ {
25279
+ id: "tasks_without_task_list",
25280
+ entity: "task",
25281
+ field: "task_list_id",
25282
+ target: "task_list",
25283
+ kind: "missing",
25284
+ base_severity: "warn",
25285
+ escalate_when_open: true,
25286
+ impact: "invisible to every task-list read \u2014 the list reports zero open work"
25287
+ },
25288
+ {
25289
+ id: "tasks_with_unregistered_project",
25290
+ entity: "task",
25291
+ field: "project_id",
25292
+ target: "project",
25293
+ kind: "dangling",
25294
+ base_severity: "error",
25295
+ escalate_when_open: false,
25296
+ impact: "points at a project id that does not exist; the reference can never resolve"
25297
+ },
25298
+ {
25299
+ id: "tasks_with_unregistered_task_list",
25300
+ entity: "task",
25301
+ field: "task_list_id",
25302
+ target: "task_list",
25303
+ kind: "dangling",
25304
+ base_severity: "error",
25305
+ escalate_when_open: false,
25306
+ impact: "points at a task-list id that does not exist; the reference can never resolve"
25307
+ },
25308
+ {
25309
+ id: "task_lists_without_project",
25310
+ entity: "task_list",
25311
+ field: "project_id",
25312
+ target: "project",
25313
+ kind: "missing",
25314
+ base_severity: "warn",
25315
+ escalate_when_open: false,
25316
+ impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
25317
+ },
25318
+ {
25319
+ id: "task_lists_with_unregistered_project",
25320
+ entity: "task_list",
25321
+ field: "project_id",
25322
+ target: "project",
25323
+ kind: "dangling",
25324
+ base_severity: "error",
25325
+ escalate_when_open: false,
25326
+ impact: "points at a project id that does not exist; the list can never be reached"
25327
+ }
25328
+ ];
25329
+ ENTITY_LABEL = {
25330
+ task: ["task", "tasks"],
25331
+ task_list: ["task list", "task lists"]
25248
25332
  };
25249
- }
25250
- function planResource(id, name) {
25251
- return {
25252
- kind: "workflow",
25253
- id,
25254
- name: name ?? undefined,
25255
- externalId: id,
25256
- sourcePackage: SOURCE_PACKAGE
25333
+ SQLITE_TABLE = {
25334
+ task: "tasks",
25335
+ task_list: "task_lists",
25336
+ project: "projects"
25257
25337
  };
25258
- }
25259
- function projectResource(project) {
25260
- const projectSlug = projectSlugForPanel(project);
25261
- return {
25262
- kind: "project",
25263
- id: projectSlug,
25264
- name: project.name,
25265
- uri: `project://${projectSlug}`,
25266
- externalId: project.id,
25267
- sourcePackage: SOURCE_PACKAGE
25338
+ RECORD_TYPE = {
25339
+ task: "tasks",
25340
+ task_list: "task_lists",
25341
+ project: "projects"
25268
25342
  };
25343
+ });
25344
+
25345
+ // src/db/integrity.ts
25346
+ function tableExists(db, table) {
25347
+ return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
25269
25348
  }
25270
- function projectSlugForPanel(project) {
25271
- const taskListSlug = project.task_list_id?.replace(/^todos-/, "");
25272
- return taskListSlug && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(taskListSlug) ? taskListSlug : slugify(project.name) || project.id.toLowerCase();
25349
+ function scanSqliteIntegrity(db = getDatabase()) {
25350
+ const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
25351
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
25352
+ if (missing.length > 0) {
25353
+ return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
25354
+ }
25355
+ try {
25356
+ const row = db.query(buildSqliteIntegritySql(spec)).get();
25357
+ return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
25358
+ } catch (error) {
25359
+ return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
25360
+ }
25361
+ });
25362
+ return buildIntegrityReport(conditions, now());
25273
25363
  }
25274
- function countByStatus(tasks) {
25364
+ var REQUIRED_TABLES;
25365
+ var init_integrity2 = __esm(() => {
25366
+ init_database();
25367
+ init_integrity();
25368
+ REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
25369
+ });
25370
+
25371
+ // src/storage/sqlite-snapshot.ts
25372
+ function exportSqliteTodosStorageSnapshot(db) {
25373
+ const d = db ?? getDatabase();
25275
25374
  return {
25276
- pending: tasks.filter((task) => task.status === "pending").length,
25277
- in_progress: tasks.filter((task) => task.status === "in_progress").length,
25278
- completed: tasks.filter((task) => task.status === "completed").length,
25279
- failed: tasks.filter((task) => task.status === "failed").length,
25280
- cancelled: tasks.filter((task) => task.status === "cancelled").length
25375
+ exportedAt: new Date().toISOString(),
25376
+ source: "sqlite",
25377
+ tasks: listTasks({ include_archived: true }, d),
25378
+ projects: listProjects(d),
25379
+ projectMachinePaths: listProjectMachinePaths(d),
25380
+ plans: listPlans(undefined, d),
25381
+ agents: listAgents({ include_archived: true }, d),
25382
+ taskLists: listTaskLists(undefined, d),
25383
+ templates: listTemplates(d),
25384
+ templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
25385
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
25386
+ tombstones: listStorageTombstones(d)
25281
25387
  };
25282
25388
  }
25283
- function countByPriority(tasks) {
25284
- return {
25285
- critical: tasks.filter((task) => task.priority === "critical").length,
25286
- high: tasks.filter((task) => task.priority === "high").length,
25287
- medium: tasks.filter((task) => task.priority === "medium").length,
25288
- low: tasks.filter((task) => task.priority === "low").length
25389
+ function importSqliteTodosStorageSnapshot(snapshot, db) {
25390
+ const d = db ?? getDatabase();
25391
+ const result = {
25392
+ inserted: 0,
25393
+ updated: 0,
25394
+ deleted: 0,
25395
+ skipped: 0,
25396
+ errors: []
25289
25397
  };
25290
- }
25291
- function isOverdue(task, generatedAt) {
25292
- if (!task.due_at || TERMINAL_STATUSES.has(task.status))
25293
- return false;
25294
- const due = Date.parse(task.due_at);
25295
- return Number.isFinite(due) && due < Date.parse(generatedAt);
25296
- }
25297
- function taskSummary(task, blockers) {
25298
- if (blockers.length > 0) {
25299
- return `Blocked by ${blockers.map((blocker) => blocker.id.slice(0, 8)).join(", ")}`;
25398
+ result.errors.push(...validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists));
25399
+ if (result.errors.length === 0) {
25400
+ const existingProjects = d.query("SELECT id, task_list_id FROM projects").all();
25401
+ const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
25402
+ result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
25403
+ }
25404
+ if (result.errors.length > 0)
25405
+ return result;
25406
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
25407
+ for (const row of rows) {
25408
+ try {
25409
+ const record = asRecord(row);
25410
+ const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType, record["id"], d) : null;
25411
+ if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
25412
+ result.skipped += 1;
25413
+ continue;
25414
+ }
25415
+ const state = upsertById(d, table, columns, record, updateClockColumn);
25416
+ if (state === "inserted")
25417
+ result.inserted += 1;
25418
+ else if (state === "updated")
25419
+ result.updated += 1;
25420
+ else
25421
+ result.skipped += 1;
25422
+ afterUpsert?.(record, state !== "skipped");
25423
+ } catch (error) {
25424
+ result.errors.push(error instanceof Error ? error.message : String(error));
25425
+ }
25426
+ }
25427
+ };
25428
+ applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
25429
+ applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
25430
+ applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
25431
+ applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
25432
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
25433
+ applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
25434
+ applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
25435
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
25436
+ if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
25437
+ replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
25438
+ }
25439
+ });
25440
+ applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
25441
+ applyTombstones(d, snapshot.tombstones ?? [], result);
25442
+ return result;
25443
+ }
25444
+ function upsertById(db, table, columns, row, updateClockColumn) {
25445
+ const id = row["id"];
25446
+ if (typeof id !== "string" || !id)
25447
+ throw new Error(`${table} row is missing id`);
25448
+ const presentColumns = columns.filter((column) => (column in row));
25449
+ if (!presentColumns.includes("id"))
25450
+ presentColumns.unshift("id");
25451
+ const existing = existsById(db, table, id);
25452
+ const placeholders = presentColumns.map(() => "?").join(", ");
25453
+ const values = presentColumns.map((column) => valueForColumn(column, row[column]));
25454
+ const updateColumns = presentColumns.filter((column) => column !== "id");
25455
+ const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
25456
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
25457
+ const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
25458
+ ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
25459
+ const changes = db.run(sql, values).changes;
25460
+ if (changes === 0)
25461
+ return "skipped";
25462
+ return existing ? "updated" : "inserted";
25463
+ }
25464
+ function existsById(db, table, id) {
25465
+ return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
25466
+ }
25467
+ function valueForColumn(column, value) {
25468
+ if (BOOLEAN_COLUMNS.has(column))
25469
+ return value ? 1 : 0;
25470
+ if (JSON_COLUMNS.has(column))
25471
+ return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
25472
+ return value === undefined ? null : value;
25473
+ }
25474
+ function asRecord(value) {
25475
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
25476
+ throw new Error("snapshot rows must be objects");
25477
+ }
25478
+ return value;
25479
+ }
25480
+ function sortedTasks(tasks) {
25481
+ const byId = new Map(tasks.map((task) => [task.id, task]));
25482
+ const seen = new Set;
25483
+ const result = [];
25484
+ const visit = (task) => {
25485
+ if (seen.has(task.id))
25486
+ return;
25487
+ if (task.parent_id && byId.has(task.parent_id))
25488
+ visit(byId.get(task.parent_id));
25489
+ seen.add(task.id);
25490
+ result.push(task);
25491
+ };
25492
+ for (const task of tasks)
25493
+ visit(task);
25494
+ return result;
25495
+ }
25496
+ function applyTombstones(db, tombstones, result) {
25497
+ for (const tombstone of tombstones) {
25498
+ try {
25499
+ recordStorageTombstone({
25500
+ object_type: tombstone.object_type,
25501
+ object_id: tombstone.object_id,
25502
+ deleted_at: tombstone.deleted_at,
25503
+ source_machine_id: tombstone.source_machine_id ?? null,
25504
+ payload: tombstone.payload ?? null,
25505
+ version: tombstone.version ?? null
25506
+ }, db);
25507
+ const table = tableForTombstone(tombstone.object_type);
25508
+ const existing = existingClock(db, table, tombstone.object_id);
25509
+ if (!shouldApplyStorageTombstone(tombstone, existing)) {
25510
+ result.skipped += 1;
25511
+ continue;
25512
+ }
25513
+ const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
25514
+ const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
25515
+ if (deleted > 0 || deletedTags > 0)
25516
+ result.deleted = (result.deleted ?? 0) + 1;
25517
+ else
25518
+ result.skipped += 1;
25519
+ } catch (error) {
25520
+ result.errors.push(error instanceof Error ? error.message : String(error));
25521
+ }
25522
+ }
25523
+ }
25524
+ function tableForTombstone(objectType) {
25525
+ if (objectType === "tasks")
25526
+ return "tasks";
25527
+ if (objectType === "projects")
25528
+ return "projects";
25529
+ if (objectType === "project_machine_paths")
25530
+ return "project_machine_paths";
25531
+ if (objectType === "plans")
25532
+ return "plans";
25533
+ if (objectType === "agents")
25534
+ return "agents";
25535
+ if (objectType === "task_lists")
25536
+ return "task_lists";
25537
+ if (objectType === "templates")
25538
+ return "task_templates";
25539
+ if (objectType === "template_tasks")
25540
+ return "template_tasks";
25541
+ return "task_history";
25542
+ }
25543
+ function listRows(db, table, columns) {
25544
+ return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
25545
+ }
25546
+ function listProjectMachinePaths(db) {
25547
+ return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
25548
+ id: String(row.id),
25549
+ project_id: String(row.project_id),
25550
+ machine_id: String(row.machine_id),
25551
+ path: String(row.path),
25552
+ created_at: String(row.created_at),
25553
+ updated_at: String(row.updated_at)
25554
+ }));
25555
+ }
25556
+ function existingClock(db, table, id) {
25557
+ const clockColumns = clockColumnsForTable(table);
25558
+ const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
25559
+ return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
25560
+ }
25561
+ function rowClock(row, updateClockColumn) {
25562
+ const value = updateClockColumn ? row[updateClockColumn] : null;
25563
+ return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
25564
+ }
25565
+ function stringClock(value) {
25566
+ return typeof value === "string" && value ? value : null;
25567
+ }
25568
+ function clockColumnsForTable(table) {
25569
+ if (table === "agents")
25570
+ return ["last_seen_at", "created_at"];
25571
+ if (table === "task_templates")
25572
+ return ["created_at"];
25573
+ if (table === "task_history")
25574
+ return ["created_at"];
25575
+ return ["updated_at", "created_at"];
25576
+ }
25577
+ var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TEMPLATE_TASK_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
25578
+ var init_sqlite_snapshot = __esm(() => {
25579
+ init_database();
25580
+ init_agents();
25581
+ init_audit();
25582
+ init_plans();
25583
+ init_projects();
25584
+ init_task_lists();
25585
+ init_tasks();
25586
+ init_templates();
25587
+ init_storage_tombstones();
25588
+ PROJECT_COLUMNS = [
25589
+ "id",
25590
+ "name",
25591
+ "path",
25592
+ "description",
25593
+ "task_list_id",
25594
+ "task_prefix",
25595
+ "task_counter",
25596
+ "created_at",
25597
+ "updated_at",
25598
+ "machine_id",
25599
+ "synced_at"
25600
+ ];
25601
+ PROJECT_MACHINE_PATH_COLUMNS = [
25602
+ "id",
25603
+ "project_id",
25604
+ "machine_id",
25605
+ "path",
25606
+ "created_at",
25607
+ "updated_at"
25608
+ ];
25609
+ TASK_LIST_COLUMNS = [
25610
+ "id",
25611
+ "project_id",
25612
+ "slug",
25613
+ "name",
25614
+ "description",
25615
+ "metadata",
25616
+ "created_at",
25617
+ "updated_at",
25618
+ "machine_id",
25619
+ "synced_at"
25620
+ ];
25621
+ PLAN_COLUMNS = [
25622
+ "id",
25623
+ "project_id",
25624
+ "task_list_id",
25625
+ "agent_id",
25626
+ "name",
25627
+ "description",
25628
+ "status",
25629
+ "created_at",
25630
+ "updated_at",
25631
+ "machine_id",
25632
+ "synced_at"
25633
+ ];
25634
+ AGENT_COLUMNS = [
25635
+ "id",
25636
+ "name",
25637
+ "description",
25638
+ "role",
25639
+ "title",
25640
+ "level",
25641
+ "permissions",
25642
+ "capabilities",
25643
+ "reports_to",
25644
+ "org_id",
25645
+ "metadata",
25646
+ "status",
25647
+ "created_at",
25648
+ "last_seen_at",
25649
+ "session_id",
25650
+ "working_dir",
25651
+ "active_project_id",
25652
+ "machine_id",
25653
+ "synced_at"
25654
+ ];
25655
+ TEMPLATE_COLUMNS = [
25656
+ "id",
25657
+ "name",
25658
+ "title_pattern",
25659
+ "description",
25660
+ "priority",
25661
+ "tags",
25662
+ "variables",
25663
+ "project_id",
25664
+ "plan_id",
25665
+ "metadata",
25666
+ "version",
25667
+ "created_at",
25668
+ "machine_id",
25669
+ "synced_at"
25670
+ ];
25671
+ TEMPLATE_TASK_COLUMNS = [
25672
+ "id",
25673
+ "template_id",
25674
+ "position",
25675
+ "title_pattern",
25676
+ "description",
25677
+ "priority",
25678
+ "tags",
25679
+ "task_type",
25680
+ "condition",
25681
+ "include_template_id",
25682
+ "depends_on_positions",
25683
+ "metadata",
25684
+ "created_at"
25685
+ ];
25686
+ TASK_COLUMNS = [
25687
+ "id",
25688
+ "short_id",
25689
+ "project_id",
25690
+ "parent_id",
25691
+ "plan_id",
25692
+ "task_list_id",
25693
+ "title",
25694
+ "description",
25695
+ "status",
25696
+ "priority",
25697
+ "agent_id",
25698
+ "assigned_to",
25699
+ "session_id",
25700
+ "working_dir",
25701
+ "tags",
25702
+ "metadata",
25703
+ "version",
25704
+ "locked_by",
25705
+ "locked_at",
25706
+ "created_at",
25707
+ "updated_at",
25708
+ "started_at",
25709
+ "completed_at",
25710
+ "due_at",
25711
+ "estimated_minutes",
25712
+ "actual_minutes",
25713
+ "requires_approval",
25714
+ "approved_by",
25715
+ "approved_at",
25716
+ "recurrence_rule",
25717
+ "recurrence_parent_id",
25718
+ "spawns_template_id",
25719
+ "confidence",
25720
+ "reason",
25721
+ "spawned_from_session",
25722
+ "assigned_by",
25723
+ "assigned_from_project",
25724
+ "task_type",
25725
+ "cost_tokens",
25726
+ "cost_usd",
25727
+ "delegated_from",
25728
+ "delegation_depth",
25729
+ "retry_count",
25730
+ "max_retries",
25731
+ "retry_after",
25732
+ "sla_minutes",
25733
+ "runner_id",
25734
+ "runner_started_at",
25735
+ "runner_completed_at",
25736
+ "current_step",
25737
+ "total_steps",
25738
+ "cycle_id",
25739
+ "machine_id",
25740
+ "synced_at",
25741
+ "archived_at"
25742
+ ];
25743
+ AUDIT_COLUMNS = [
25744
+ "id",
25745
+ "task_id",
25746
+ "action",
25747
+ "field",
25748
+ "old_value",
25749
+ "new_value",
25750
+ "agent_id",
25751
+ "created_at",
25752
+ "machine_id"
25753
+ ];
25754
+ JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
25755
+ BOOLEAN_COLUMNS = new Set(["requires_approval"]);
25756
+ });
25757
+
25758
+ // src/storage/local-sqlite.ts
25759
+ function resolveTaskRefLocal(db, ref) {
25760
+ const raw = ref.trim().toLowerCase();
25761
+ if (!raw)
25762
+ return null;
25763
+ if (TASK_UUID_RE2.test(raw))
25764
+ return getTask(raw, db);
25765
+ const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
25766
+ if (prefixRows.length > 1) {
25767
+ throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
25768
+ }
25769
+ if (prefixRows.length === 1)
25770
+ return getTask(prefixRows[0].id, db);
25771
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
25772
+ if (shortIdRows.length > 1) {
25773
+ throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
25774
+ }
25775
+ if (shortIdRows.length === 1)
25776
+ return getTask(shortIdRows[0].id, db);
25777
+ return null;
25778
+ }
25779
+ function isSearchQuery(filter) {
25780
+ const q = filter.query?.trim();
25781
+ return !!q && q !== "*";
25782
+ }
25783
+ function matchesExtraFilters(task, filter) {
25784
+ if (filter.ids && !filter.ids.includes(task.id))
25785
+ return false;
25786
+ if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
25787
+ return false;
25788
+ if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
25789
+ return false;
25790
+ if (filter.session_id !== undefined && task.session_id !== filter.session_id)
25791
+ return false;
25792
+ if (filter.has_recurrence !== undefined && Boolean(task.recurrence_rule) !== filter.has_recurrence)
25793
+ return false;
25794
+ if (filter.task_type !== undefined) {
25795
+ const allowed = Array.isArray(filter.task_type) ? filter.task_type : [filter.task_type];
25796
+ if (!allowed.includes(task.task_type ?? ""))
25797
+ return false;
25798
+ }
25799
+ if (filter.tags?.length) {
25800
+ const taskTags = new Set(task.tags ?? []);
25801
+ if (!filter.tags.every((tag) => taskTags.has(tag)))
25802
+ return false;
25803
+ }
25804
+ if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
25805
+ return false;
25806
+ return true;
25807
+ }
25808
+ function listTasksMaybeSearch(filter, db) {
25809
+ if (!isSearchQuery(filter))
25810
+ return listTasks(filter, db);
25811
+ const matched = searchTasks({
25812
+ query: filter.query,
25813
+ project_id: filter.project_id,
25814
+ task_list_id: filter.task_list_id,
25815
+ status: filter.status,
25816
+ priority: filter.priority,
25817
+ assigned_to: filter.assigned_to,
25818
+ agent_id: filter.agent_id
25819
+ }, undefined, undefined, db).filter((task) => matchesExtraFilters(task, filter));
25820
+ const offset = filter.offset && filter.offset > 0 ? Math.trunc(filter.offset) : 0;
25821
+ if (filter.limit !== undefined && filter.limit >= 0)
25822
+ return matched.slice(offset, offset + filter.limit);
25823
+ return offset ? matched.slice(offset) : matched;
25824
+ }
25825
+ function createLocalSqliteTodosStorageAdapter(options = {}) {
25826
+ const database = () => options.db ?? getDatabase();
25827
+ let adapter;
25828
+ adapter = {
25829
+ kind: "sqlite",
25830
+ capabilities: {
25831
+ localPersistence: true,
25832
+ remotePersistence: false,
25833
+ transactions: true,
25834
+ auditLog: true,
25835
+ sync: true
25836
+ },
25837
+ tasks: {
25838
+ create: (input, context) => createTask({
25839
+ ...input,
25840
+ agent_id: input.agent_id ?? context?.agentId,
25841
+ created_by: input.created_by ?? input.agent_id ?? context?.agentId
25842
+ }, database()),
25843
+ get: (id) => getTask(id, database()),
25844
+ resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
25845
+ list: (filter = {}) => listTasksMaybeSearch(filter, database()),
25846
+ count: (filter = {}) => isSearchQuery(filter) ? listTasksMaybeSearch({ ...filter, limit: undefined, offset: undefined }, database()).length : countTasks(filter, database()),
25847
+ update: (id, input) => updateTask(id, input, database()),
25848
+ unlock: (id, agentId) => {
25849
+ unlockTask(id, agentId, database());
25850
+ return true;
25851
+ },
25852
+ delete: (id) => deleteTask(id, database()),
25853
+ start: (id, agentId) => startTask(id, agentId, database()),
25854
+ complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
25855
+ fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
25856
+ claimNext: (agentId, filters) => claimNextTask(agentId, filters, database()),
25857
+ getNext: (agentId, filters) => getNextTask(agentId, filters, database()),
25858
+ getActiveWork: (filters) => getActiveWork(filters, database()),
25859
+ getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
25860
+ },
25861
+ projects: {
25862
+ create: (input) => createProject(input, database()),
25863
+ get: (id) => getProject(id, database()),
25864
+ getByPath: (path) => getProjectByPath(path, database()),
25865
+ list: () => listProjects(database()),
25866
+ update: (id, input) => updateProject(id, input, database()),
25867
+ rename: (id, input) => renameProject(id, input, database()),
25868
+ delete: (id) => deleteProject(id, database())
25869
+ },
25870
+ plans: {
25871
+ create: (input) => createPlan(input, database()),
25872
+ get: (id) => getPlan(id, database()),
25873
+ list: (projectId) => listPlans(projectId, database()),
25874
+ update: (id, input) => updatePlan(id, input, database()),
25875
+ delete: (id) => deletePlan(id, database())
25876
+ },
25877
+ agents: {
25878
+ register: (input) => registerAgent(input, database()),
25879
+ get: (id) => getAgent(id, database()),
25880
+ getByName: (name) => getAgentByName(name, database()),
25881
+ list: (options2) => listAgents(options2, database()),
25882
+ update: (id, input) => updateAgent(id, input, database())
25883
+ },
25884
+ taskLists: {
25885
+ create: (input) => createTaskList(input, database()),
25886
+ get: (id) => getTaskList(id, database()),
25887
+ getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
25888
+ list: (projectId) => listTaskLists(projectId, database()),
25889
+ update: (id, input) => updateTaskList(id, input, database()),
25890
+ delete: (id) => deleteTaskList(id, database()),
25891
+ deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
25892
+ },
25893
+ templates: {
25894
+ create: (input) => createTemplate(input, database()),
25895
+ get: (id) => getTemplate(id, database()),
25896
+ list: () => listTemplates(database()),
25897
+ update: (id, input) => updateTemplate(id, input, database()),
25898
+ delete: (id) => deleteTemplate(id, database()),
25899
+ getWithTasks: (id) => getTemplateWithTasks(id, database())
25900
+ },
25901
+ audit: {
25902
+ logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
25903
+ addComment: (input) => addComment(input, database()),
25904
+ getComments: (taskId) => listComments(taskId, database()),
25905
+ getCommentsPage: (taskId, options2) => {
25906
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
25907
+ throw new Error("Comment limit must be an integer between 1 and 1001");
25908
+ }
25909
+ let comments = listComments(taskId, database());
25910
+ comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
25911
+ if (options2?.before) {
25912
+ const before = options2.before;
25913
+ comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
25914
+ }
25915
+ if (options2?.limit !== undefined)
25916
+ comments = comments.slice(-options2.limit);
25917
+ return comments;
25918
+ },
25919
+ getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
25920
+ getRecentActivity: (limit) => getRecentActivity(limit, database())
25921
+ },
25922
+ sync: {
25923
+ getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database()),
25924
+ exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
25925
+ importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
25926
+ },
25927
+ integrity: {
25928
+ report: () => scanSqliteIntegrity(database())
25929
+ },
25930
+ transaction: (fn) => {
25931
+ const tx = database().transaction(() => fn(adapter));
25932
+ return tx();
25933
+ }
25934
+ };
25935
+ return adapter;
25936
+ }
25937
+ var TASK_UUID_RE2;
25938
+ var init_local_sqlite = __esm(() => {
25939
+ init_types();
25940
+ init_search();
25941
+ init_tasks();
25942
+ init_projects();
25943
+ init_plans();
25944
+ init_agents();
25945
+ init_task_lists();
25946
+ init_templates();
25947
+ init_audit();
25948
+ init_comments();
25949
+ init_database();
25950
+ init_integrity2();
25951
+ init_sqlite_snapshot();
25952
+ TASK_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25953
+ });
25954
+
25955
+ // src/lib/project-task-list-ensure.ts
25956
+ import { createHash as createHash5 } from "crypto";
25957
+ function canonicalJson(value) {
25958
+ if (value === null || typeof value !== "object")
25959
+ return JSON.stringify(value);
25960
+ if (Array.isArray(value))
25961
+ return `[${value.map(canonicalJson).join(",")}]`;
25962
+ return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
25963
+ }
25964
+ function digest(value) {
25965
+ return createHash5("sha256").update(canonicalJson(value)).digest("hex");
25966
+ }
25967
+ function deriveIdempotencyKey(projectId, slug) {
25968
+ return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
25969
+ }
25970
+ function normalizeIdempotencyKey(value, projectId, slug) {
25971
+ const key = value?.trim() || deriveIdempotencyKey(projectId, slug);
25972
+ if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
25973
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
25974
+ }
25975
+ return key;
25976
+ }
25977
+ function receiptId(projectId, slug, idempotencyKey) {
25978
+ return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
25979
+ }
25980
+ function semanticListDigest(list) {
25981
+ const metadata = { ...list.metadata ?? {} };
25982
+ delete metadata[RECEIPT_METADATA_KEY];
25983
+ return digest({
25984
+ project_id: list.project_id,
25985
+ slug: list.slug,
25986
+ name: list.name,
25987
+ description: list.description,
25988
+ metadata
25989
+ });
25990
+ }
25991
+ function storedMarker(list) {
25992
+ const value = list.metadata?.[RECEIPT_METADATA_KEY];
25993
+ if (!value || typeof value !== "object" || Array.isArray(value))
25994
+ return null;
25995
+ const marker = value;
25996
+ if (marker.schema_version !== PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION || typeof marker.receipt_id !== "string" || typeof marker.idempotency_key !== "string" || typeof marker.project_id !== "string" || typeof marker.slug !== "string" || typeof marker.result_digest !== "string" || typeof marker.created_at !== "string")
25997
+ return null;
25998
+ return marker;
25999
+ }
26000
+ function receiptFor(store, project, list, idempotencyKey) {
26001
+ const marker = storedMarker(list);
26002
+ const owned = marker?.project_id === project.id && marker.slug === list.slug;
26003
+ if (owned && marker.idempotency_key !== idempotencyKey) {
26004
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
26005
+ project_id: project.id,
26006
+ task_list_id: list.id,
26007
+ receipt_id: marker.receipt_id
26008
+ });
26009
+ }
26010
+ return {
26011
+ schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
26012
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
26013
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
26014
+ project_id: project.id,
26015
+ task_list_id: list.id,
26016
+ slug: list.slug,
26017
+ created_by_operation: owned,
26018
+ result_revision: list.updated_at,
26019
+ result_digest: owned ? marker.result_digest : semanticListDigest(list),
26020
+ rollback_supported: Boolean(owned && semanticListDigest(list) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused),
26021
+ created_at: owned ? marker.created_at : list.created_at
26022
+ };
26023
+ }
26024
+ async function exactProjectState(store, projectId) {
26025
+ const project = await store.projects.get(projectId);
26026
+ if (!project) {
26027
+ throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, { project_id: projectId });
26028
+ }
26029
+ const slug = project.task_list_id?.trim();
26030
+ if (!slug) {
26031
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_NOT_DECLARED", "Project does not declare a canonical task_list_id slug", { project_id: project.id });
26032
+ }
26033
+ const all = await store.taskLists.list();
26034
+ const scopedMatches = all.filter((list) => list.project_id === project.id && list.slug === slug);
26035
+ if (scopedMatches.length > 1) {
26036
+ throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "More than one task list matches the project's exact id and declared slug", { project_id: project.id, slug, task_list_ids: scopedMatches.map((list) => list.id) });
26037
+ }
26038
+ const globalMatches = all.filter((list) => list.project_id === null && list.slug === slug);
26039
+ if (globalMatches.length > 0 && scopedMatches.length === 0) {
26040
+ throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "A legacy global task list already owns the declared slug; refusing to create a second locator", { project_id: project.id, slug, task_list_ids: globalMatches.map((list) => list.id) });
26041
+ }
26042
+ return { project, scoped: scopedMatches[0] ?? null, globalCollision: globalMatches[0] ?? null };
26043
+ }
26044
+ async function planProjectTaskListEnsure(store, projectId) {
26045
+ const { project, scoped } = await exactProjectState(store, projectId);
26046
+ return {
26047
+ mode: "plan",
26048
+ action: scoped ? "already_present" : "would_create",
26049
+ project,
26050
+ task_list: scoped,
26051
+ receipt: null
26052
+ };
26053
+ }
26054
+ async function applyProjectTaskListEnsure(store, projectId, options) {
26055
+ const state = await exactProjectState(store, projectId);
26056
+ const { project } = state;
26057
+ if (project.updated_at !== options.expected_project_revision) {
26058
+ throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed after the ensure plan; fetch a fresh plan before applying", {
26059
+ project_id: project.id,
26060
+ expected_project_revision: options.expected_project_revision,
26061
+ current_project_revision: project.updated_at
26062
+ });
26063
+ }
26064
+ const slug = project.task_list_id;
26065
+ const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
26066
+ if (state.scoped) {
26067
+ return {
26068
+ mode: "apply",
26069
+ action: "already_present",
26070
+ project,
26071
+ task_list: state.scoped,
26072
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey)
26073
+ };
26074
+ }
26075
+ const marker = {
26076
+ schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
26077
+ receipt_id: receiptId(project.id, slug, idempotencyKey),
26078
+ idempotency_key: idempotencyKey,
26079
+ project_id: project.id,
26080
+ slug,
26081
+ result_digest: semanticListDigest({
26082
+ project_id: project.id,
26083
+ slug,
26084
+ name: project.name,
26085
+ description: null,
26086
+ metadata: {}
26087
+ }),
26088
+ created_at: new Date().toISOString()
26089
+ };
26090
+ let list;
26091
+ try {
26092
+ list = await store.taskLists.create({
26093
+ name: project.name,
26094
+ slug,
26095
+ project_id: project.id,
26096
+ metadata: { [RECEIPT_METADATA_KEY]: marker }
26097
+ });
26098
+ } catch (error) {
26099
+ if (!(error instanceof ResourceConflictError))
26100
+ throw error;
26101
+ const raced = await exactProjectState(store, projectId);
26102
+ if (!raced.scoped)
26103
+ throw error;
26104
+ if (raced.project.updated_at !== options.expected_project_revision || raced.project.task_list_id !== slug) {
26105
+ throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed while the task list was being created; fetch a fresh plan before retrying", {
26106
+ project_id: raced.project.id,
26107
+ expected_project_revision: options.expected_project_revision,
26108
+ current_project_revision: raced.project.updated_at
26109
+ });
26110
+ }
26111
+ return {
26112
+ mode: "apply",
26113
+ action: "already_present",
26114
+ project: raced.project,
26115
+ task_list: raced.scoped,
26116
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
26117
+ };
26118
+ }
26119
+ const projectReadback = await store.projects.get(project.id);
26120
+ if (!projectReadback || projectReadback.updated_at !== options.expected_project_revision || projectReadback.task_list_id !== slug) {
26121
+ let compensated = false;
26122
+ const unchanged = await store.taskLists.get(list.id);
26123
+ const unchangedMarker = unchanged ? storedMarker(unchanged) : null;
26124
+ if (unchanged && unchangedMarker?.receipt_id === marker.receipt_id && semanticListDigest(unchanged) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused) {
26125
+ const deletion = await store.taskLists.deleteIfUnchangedAndUnused(list.id, {
26126
+ project_id: unchanged.project_id,
26127
+ slug: unchanged.slug,
26128
+ name: unchanged.name,
26129
+ description: unchanged.description,
26130
+ metadata: unchanged.metadata,
26131
+ updated_at: unchanged.updated_at
26132
+ });
26133
+ compensated = deletion.status === "deleted";
26134
+ }
26135
+ throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", compensated ? "Project changed while the task list was being created; the new list was rolled back" : "Project changed while the task list was being created; the new list was retained because safe conditional rollback could not be proven", { project_id: project.id, task_list_id: list.id, compensated });
26136
+ }
26137
+ const readback = await store.taskLists.get(list.id);
26138
+ if (!readback || readback.project_id !== project.id || readback.slug !== slug) {
26139
+ throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "Task-list create did not preserve the exact project id and declared slug", { project_id: project.id, task_list_id: list.id, slug });
26140
+ }
26141
+ return {
26142
+ mode: "apply",
26143
+ action: "created",
26144
+ project: projectReadback,
26145
+ task_list: readback,
26146
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
26147
+ };
26148
+ }
26149
+ async function rollbackProjectTaskListEnsure(store, projectId, options) {
26150
+ const conditionalDelete = store.taskLists.deleteIfUnchangedAndUnused;
26151
+ if (!conditionalDelete) {
26152
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "This storage backend cannot guarantee atomic conditional rollback; refusing to delete", { project_id: projectId, receipt_id: options.receipt_id });
26153
+ }
26154
+ const project = await store.projects.get(projectId);
26155
+ if (!project) {
26156
+ throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`);
26157
+ }
26158
+ const candidates = (await store.taskLists.list(project.id)).filter((list2) => storedMarker(list2)?.receipt_id === options.receipt_id);
26159
+ if (candidates.length !== 1) {
26160
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_RECEIPT_NOT_FOUND", "No exact operation-owned task list matches this rollback receipt", { project_id: project.id, receipt_id: options.receipt_id });
26161
+ }
26162
+ const list = candidates[0];
26163
+ const marker = storedMarker(list);
26164
+ if (marker.project_id !== project.id || marker.slug !== list.slug || list.project_id !== project.id || list.updated_at !== options.expected_task_list_revision || semanticListDigest(list) !== marker.result_digest) {
26165
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "The operation-owned task list drifted; refusing conditional rollback", { project_id: project.id, task_list_id: list.id, receipt_id: options.receipt_id });
26166
+ }
26167
+ const deletion = await conditionalDelete.call(store.taskLists, list.id, {
26168
+ project_id: list.project_id,
26169
+ slug: list.slug,
26170
+ name: list.name,
26171
+ description: list.description,
26172
+ metadata: list.metadata,
26173
+ updated_at: list.updated_at
26174
+ });
26175
+ if (deletion.status === "has_dependents") {
26176
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_HAS_DEPENDENTS", "The operation-owned task list has dependents; refusing conditional rollback", {
26177
+ task_list_id: list.id,
26178
+ task_dependents: deletion.task_dependents,
26179
+ plan_dependents: deletion.plan_dependents
26180
+ });
26181
+ }
26182
+ if (deletion.status !== "deleted" || await store.taskLists.get(list.id)) {
26183
+ throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "Conditional rollback did not remove the exact task list", { task_list_id: list.id });
26184
+ }
26185
+ return {
26186
+ schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
26187
+ action: "removed",
26188
+ project_id: project.id,
26189
+ task_list_id: list.id,
26190
+ accepted_receipt_id: options.receipt_id,
26191
+ rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
26192
+ removed_at: new Date().toISOString()
26193
+ };
26194
+ }
26195
+ var PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION = "todos.project-task-list-ensure.v1", RECEIPT_METADATA_KEY = "todos_project_task_list_ensure", ProjectTaskListEnsureError;
26196
+ var init_project_task_list_ensure = __esm(() => {
26197
+ init_types();
26198
+ ProjectTaskListEnsureError = class ProjectTaskListEnsureError extends Error {
26199
+ code;
26200
+ details;
26201
+ constructor(code, message, details = {}) {
26202
+ super(message);
26203
+ this.code = code;
26204
+ this.details = details;
26205
+ this.name = "ProjectTaskListEnsureError";
26206
+ }
26207
+ };
26208
+ });
26209
+
26210
+ // src/lib/project-bootstrap.ts
26211
+ var exports_project_bootstrap = {};
26212
+ __export(exports_project_bootstrap, {
26213
+ discoverProjectWorkspace: () => discoverProjectWorkspace,
26214
+ bootstrapProject: () => bootstrapProject
26215
+ });
26216
+ import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
26217
+ import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
26218
+ function safeStat(path) {
26219
+ try {
26220
+ return statSync4(path);
26221
+ } catch {
26222
+ return null;
26223
+ }
26224
+ }
26225
+ function canonicalPath(input) {
26226
+ const resolved = resolve11(input);
26227
+ const stats = safeStat(resolved);
26228
+ if (stats?.isFile())
26229
+ return dirname6(resolved);
26230
+ return resolved;
26231
+ }
26232
+ function findUp(start, marker) {
26233
+ let current = canonicalPath(start);
26234
+ while (true) {
26235
+ if (existsSync13(resolve11(current, marker)))
26236
+ return current;
26237
+ const parent = dirname6(current);
26238
+ if (parent === current)
26239
+ return null;
26240
+ current = parent;
26241
+ }
26242
+ }
26243
+ function readPackageJson(path) {
26244
+ if (!path)
26245
+ return null;
26246
+ const file = resolve11(path, "package.json");
26247
+ if (!existsSync13(file))
26248
+ return null;
26249
+ try {
26250
+ const parsed = JSON.parse(readFileSync7(file, "utf-8"));
26251
+ return parsed && typeof parsed === "object" ? parsed : null;
26252
+ } catch {
26253
+ return null;
26254
+ }
26255
+ }
26256
+ function packageDisplayName(name, fallbackPath) {
26257
+ if (!name)
26258
+ return basename4(fallbackPath);
26259
+ const withoutScope = name.startsWith("@") ? name.split("/")[1] : name;
26260
+ return withoutScope || basename4(fallbackPath);
26261
+ }
26262
+ function workspaceMarker(root, rootPackage) {
26263
+ if (!root)
26264
+ return { kind: null, markers: [] };
26265
+ const markers = [];
26266
+ if (rootPackage?.workspaces)
26267
+ markers.push("package.json#workspaces");
26268
+ for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
26269
+ if (existsSync13(resolve11(root, marker)))
26270
+ markers.push(marker);
26271
+ }
26272
+ const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
26273
+ return { kind, markers };
26274
+ }
26275
+ function discoverProjectWorkspace(inputPath = process.cwd()) {
26276
+ const input = canonicalPath(inputPath);
26277
+ const gitRoot = findUp(input, ".git");
26278
+ const packageRoot = findUp(input, "package.json");
26279
+ const rootPackage = readPackageJson(gitRoot);
26280
+ const packageMeta = readPackageJson(packageRoot);
26281
+ const workspace = workspaceMarker(gitRoot, rootPackage);
26282
+ const monorepo = Boolean(gitRoot && packageRoot && packageRoot !== gitRoot && workspace.kind);
26283
+ const projectPath = monorepo ? packageRoot : gitRoot ?? packageRoot ?? input;
26284
+ const projectName = packageDisplayName(packageMeta?.name ?? rootPackage?.name ?? null, projectPath);
26285
+ return {
26286
+ inputPath: input,
26287
+ projectPath,
26288
+ projectName,
26289
+ gitRoot,
26290
+ packageRoot,
26291
+ packageName: packageMeta?.name ?? null,
26292
+ workspaceRoot: workspace.kind ? gitRoot : null,
26293
+ workspaceKind: workspace.kind,
26294
+ monorepo,
26295
+ markers: workspace.markers
26296
+ };
26297
+ }
26298
+ function sourceExists(projectId, type, uri, db) {
26299
+ return listProjectSources(projectId, db).some((source) => source.type === type && source.uri === uri);
26300
+ }
26301
+ function addSourceOnce(projectId, type, name, uri, metadata, db) {
26302
+ if (!uri || sourceExists(projectId, type, uri, db))
26303
+ return null;
26304
+ return addProjectSource({ project_id: projectId, type, name, uri, metadata }, db);
26305
+ }
26306
+ function bootstrapProject(options = {}, db) {
26307
+ const d = db || getDatabase();
26308
+ const discovery = discoverProjectWorkspace(options.path);
26309
+ const taskListSlug = options.taskListSlug || `todos-${slugify(options.name || discovery.projectName)}`;
26310
+ if (options.dryRun) {
26311
+ return {
26312
+ dryRun: true,
26313
+ discovery: { ...discovery, projectName: options.name || discovery.projectName },
26314
+ project: null,
26315
+ taskList: null,
26316
+ sources: [],
26317
+ created: { project: false, taskList: false, sources: [] }
26318
+ };
26319
+ }
26320
+ const beforeProject = getProjectByCanonicalPath(discovery.projectPath, d);
26321
+ let project = ensureProject(options.name || discovery.projectName, discovery.projectPath, d);
26322
+ const createdProject = !beforeProject;
26323
+ if (project.task_list_id !== taskListSlug || options.name && project.name !== options.name) {
26324
+ project = renameProject(project.id, {
26325
+ name: options.name ?? project.name,
26326
+ new_slug: taskListSlug
26327
+ }, d).project;
26328
+ }
26329
+ setMachineLocalPath(project.id, discovery.projectPath, d);
26330
+ const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
26331
+ let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug, project.id, d);
26332
+ if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
26333
+ taskList = updateTaskList(taskList.id, {
26334
+ metadata: {
26335
+ ...taskList.metadata,
26336
+ route_enabled: true,
26337
+ automation: {
26338
+ ...taskList.metadata.automation && typeof taskList.metadata.automation === "object" && !Array.isArray(taskList.metadata.automation) ? taskList.metadata.automation : {},
26339
+ no_auto: false
26340
+ }
26341
+ }
26342
+ }, d);
26343
+ }
26344
+ const createdSources = [];
26345
+ for (const source of [
26346
+ addSourceOnce(project.id, "local", "Project root", discovery.projectPath, { role: "project-root" }, d),
26347
+ addSourceOnce(project.id, "git", "Git root", discovery.gitRoot, { role: "git-root" }, d),
26348
+ addSourceOnce(project.id, "workspace", "Workspace root", discovery.workspaceRoot, {
26349
+ role: "workspace-root",
26350
+ kind: discovery.workspaceKind,
26351
+ markers: discovery.markers,
26352
+ monorepo: discovery.monorepo
26353
+ }, d)
26354
+ ]) {
26355
+ if (source) {
26356
+ createdSources.push(source.type);
26357
+ }
26358
+ }
26359
+ return {
26360
+ dryRun: false,
26361
+ discovery: { ...discovery, projectName: options.name || discovery.projectName },
26362
+ project,
26363
+ taskList,
26364
+ sources: listProjectSources(project.id, d),
26365
+ created: {
26366
+ project: createdProject,
26367
+ taskList: !beforeTaskList,
26368
+ sources: createdSources
26369
+ }
26370
+ };
26371
+ }
26372
+ function getProjectByCanonicalPath(path, db) {
26373
+ return getProjectByExactPath(path, db) ?? null;
26374
+ }
26375
+ function getProjectByExactPath(path, db) {
26376
+ return getProjectByPathForBootstrap(path, db);
26377
+ }
26378
+ function getProjectByPathForBootstrap(path, db) {
26379
+ const row = db.query("SELECT * FROM projects WHERE path = ?").get(path);
26380
+ if (row)
26381
+ return row;
26382
+ const machineRow = db.query(`SELECT p.* FROM projects p
26383
+ JOIN project_machine_paths pmp ON pmp.project_id = p.id
26384
+ WHERE pmp.path = ?`).get(path);
26385
+ return machineRow ?? null;
26386
+ }
26387
+ var init_project_bootstrap = __esm(() => {
26388
+ init_database();
26389
+ init_projects();
26390
+ init_task_lists();
26391
+ });
26392
+
26393
+ // src/lib/project-panel.ts
26394
+ var exports_project_panel = {};
26395
+ __export(exports_project_panel, {
26396
+ createTodosProjectPanel: () => createTodosProjectPanel
26397
+ });
26398
+ import {
26399
+ parseContract,
26400
+ SCHEMA_IDS
26401
+ } from "@hasna/contracts";
26402
+ function clampLimit(limit) {
26403
+ if (!Number.isFinite(limit ?? 0))
26404
+ return 20;
26405
+ return Math.max(1, Math.min(100, Math.trunc(limit ?? 20)));
26406
+ }
26407
+ function taskUri(id) {
26408
+ return `todo://tasks/${id}`;
26409
+ }
26410
+ function taskResource(task) {
26411
+ return {
26412
+ kind: "task",
26413
+ id: task.id,
26414
+ name: task.title,
26415
+ uri: taskUri(task.id),
26416
+ externalId: task.id,
26417
+ sourcePackage: SOURCE_PACKAGE,
26418
+ tags: task.tags
26419
+ };
26420
+ }
26421
+ function planResource(id, name) {
26422
+ return {
26423
+ kind: "workflow",
26424
+ id,
26425
+ name: name ?? undefined,
26426
+ externalId: id,
26427
+ sourcePackage: SOURCE_PACKAGE
26428
+ };
26429
+ }
26430
+ function projectResource(project) {
26431
+ const projectSlug = projectSlugForPanel(project);
26432
+ return {
26433
+ kind: "project",
26434
+ id: projectSlug,
26435
+ name: project.name,
26436
+ uri: `project://${projectSlug}`,
26437
+ externalId: project.id,
26438
+ sourcePackage: SOURCE_PACKAGE
26439
+ };
26440
+ }
26441
+ function projectSlugForPanel(project) {
26442
+ const taskListSlug = project.task_list_id?.replace(/^todos-/, "");
26443
+ return taskListSlug && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(taskListSlug) ? taskListSlug : slugify(project.name) || project.id.toLowerCase();
26444
+ }
26445
+ function countByStatus(tasks) {
26446
+ return {
26447
+ pending: tasks.filter((task) => task.status === "pending").length,
26448
+ in_progress: tasks.filter((task) => task.status === "in_progress").length,
26449
+ completed: tasks.filter((task) => task.status === "completed").length,
26450
+ failed: tasks.filter((task) => task.status === "failed").length,
26451
+ cancelled: tasks.filter((task) => task.status === "cancelled").length
26452
+ };
26453
+ }
26454
+ function countByPriority(tasks) {
26455
+ return {
26456
+ critical: tasks.filter((task) => task.priority === "critical").length,
26457
+ high: tasks.filter((task) => task.priority === "high").length,
26458
+ medium: tasks.filter((task) => task.priority === "medium").length,
26459
+ low: tasks.filter((task) => task.priority === "low").length
26460
+ };
26461
+ }
26462
+ function isOverdue(task, generatedAt) {
26463
+ if (!task.due_at || TERMINAL_STATUSES.has(task.status))
26464
+ return false;
26465
+ const due = Date.parse(task.due_at);
26466
+ return Number.isFinite(due) && due < Date.parse(generatedAt);
26467
+ }
26468
+ function taskSummary(task, blockers) {
26469
+ if (blockers.length > 0) {
26470
+ return `Blocked by ${blockers.map((blocker) => blocker.id.slice(0, 8)).join(", ")}`;
25300
26471
  }
25301
26472
  const firstLine = task.description?.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
25302
26473
  return firstLine || undefined;
@@ -25430,10 +26601,10 @@ __export(exports_extract, {
25430
26601
  EXTRACT_TAGS: () => EXTRACT_TAGS
25431
26602
  });
25432
26603
  import { existsSync as existsSync14, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
25433
- import { createHash as createHash5 } from "crypto";
26604
+ import { createHash as createHash6 } from "crypto";
25434
26605
  import { relative as relative3, resolve as resolve12, join as join13 } from "path";
25435
26606
  function stableHash(value) {
25436
- return createHash5("sha256").update(value).digest("hex");
26607
+ return createHash6("sha256").update(value).digest("hex");
25437
26608
  }
25438
26609
  function normalizePathForMatch(value) {
25439
26610
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -25974,7 +27145,7 @@ function validateLocalBridgeBundle(value) {
25974
27145
  }
25975
27146
  return { ok: issues.length === 0, issues };
25976
27147
  }
25977
- function existsById(db, table, id) {
27148
+ function existsById2(db, table, id) {
25978
27149
  return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
25979
27150
  }
25980
27151
  function dependencyExists(db, row) {
@@ -25990,7 +27161,7 @@ function plannedIdSets(data) {
25990
27161
  return result;
25991
27162
  }
25992
27163
  function hasExistingOrPlannedId(db, tableKey, id, plannedIds) {
25993
- return existsById(db, tableByKey[tableKey], id) || Boolean(plannedIds.get(tableKey)?.has(id));
27164
+ return existsById2(db, tableByKey[tableKey], id) || Boolean(plannedIds.get(tableKey)?.has(id));
25994
27165
  }
25995
27166
  function missingDependency(db, tableKey, row, plannedIds) {
25996
27167
  const taskId = typeof row.task_id === "string" ? row.task_id : null;
@@ -26067,7 +27238,7 @@ function insertRecord(db, tableKey, row) {
26067
27238
  }
26068
27239
  return result.changes > 0;
26069
27240
  }
26070
- function sortedTasks(tasks) {
27241
+ function sortedTasks2(tasks) {
26071
27242
  const byId = new Map(tasks.map((task) => [task.id, task]));
26072
27243
  const visited = new Set;
26073
27244
  const result = [];
@@ -26188,7 +27359,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
26188
27359
  const data = {
26189
27360
  ...bundle.data,
26190
27361
  plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
26191
- tasks: sortedTasks(bundle.data.tasks),
27362
+ tasks: sortedTasks2(bundle.data.tasks),
26192
27363
  saved_views: bundle.data.saved_views ?? [],
26193
27364
  task_boards: bundle.data.task_boards ?? [],
26194
27365
  local_calendar_items: bundle.data.local_calendar_items ?? []
@@ -26198,7 +27369,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
26198
27369
  for (const row of data[key]) {
26199
27370
  const table = tableByKey[key];
26200
27371
  const id = key === "task_dependencies" ? `${row.task_id}->${row.depends_on}` : String(row.id);
26201
- const exists = key === "task_dependencies" ? dependencyExists(d, row) : existsById(d, table, id);
27372
+ const exists = key === "task_dependencies" ? dependencyExists(d, row) : existsById2(d, table, id);
26202
27373
  if (exists) {
26203
27374
  if (key === "tasks" && conflictStrategy === "safe_merge") {
26204
27375
  const merge = safeMergeTask(d, row, { dryRun });
@@ -26413,12 +27584,12 @@ __export(exports_local_encryption, {
26413
27584
  DEFAULT_ENCRYPTION_PROFILE: () => DEFAULT_ENCRYPTION_PROFILE,
26414
27585
  DEFAULT_ENCRYPTION_KEY_ENV: () => DEFAULT_ENCRYPTION_KEY_ENV
26415
27586
  });
26416
- import { createCipheriv, createDecipheriv, createHash as createHash6, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
27587
+ import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
26417
27588
  function now3() {
26418
27589
  return new Date().toISOString();
26419
27590
  }
26420
27591
  function sha2563(value) {
26421
- return createHash6("sha256").update(value).digest("hex");
27592
+ return createHash7("sha256").update(value).digest("hex");
26422
27593
  }
26423
27594
  function normalizeProfileName(value) {
26424
27595
  const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
@@ -27474,9 +28645,69 @@ function registerProjectCommands(program2) {
27474
28645
  }
27475
28646
  }
27476
28647
  });
27477
- program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").action(async (opts) => {
28648
+ program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").option("--ensure-task-list <project>", "Plan or apply creation of an existing project's declared task list").option("--rollback-task-list <project>", "Conditionally roll back a task list created by --ensure-task-list").option("--apply", "Apply --ensure-task-list or --rollback-task-list; ensure plans by default").option("--idempotency-key <key>", "Stable idempotency key for --ensure-task-list --apply").option("--receipt <id>", "Accepted ensure receipt for --rollback-task-list --apply").action(async (opts) => {
27478
28649
  const globalOpts = program2.opts();
27479
28650
  const cloud = getTodosCloudClient();
28651
+ if (opts.ensureTaskList && opts.rollbackTaskList) {
28652
+ handleError(new Error("Choose either --ensure-task-list or --rollback-task-list, not both"));
28653
+ }
28654
+ if (opts.apply && !opts.ensureTaskList && !opts.rollbackTaskList) {
28655
+ handleError(new Error("projects --apply requires --ensure-task-list or --rollback-task-list"));
28656
+ }
28657
+ if (opts.dryRun && opts.apply && (opts.ensureTaskList || opts.rollbackTaskList)) {
28658
+ handleError(new Error("Choose either --dry-run or --apply, not both"));
28659
+ }
28660
+ if (opts.ensureTaskList) {
28661
+ const project = cloud ? await cloudResolveProject(cloud, opts.ensureTaskList) : resolveExplicitProject(opts.ensureTaskList);
28662
+ const store = cloud ? null : createLocalSqliteTodosStorageAdapter({ db: getDatabase() });
28663
+ const plan = cloud ? await cloudPlanProjectTaskListEnsure(cloud, project.id) : await planProjectTaskListEnsure(store, project.id);
28664
+ const result = opts.apply ? cloud ? await cloudApplyProjectTaskListEnsure(cloud, project.id, {
28665
+ expected_project_revision: plan.project.updated_at,
28666
+ ...opts.idempotencyKey ? { idempotency_key: opts.idempotencyKey } : {}
28667
+ }) : await applyProjectTaskListEnsure(store, project.id, {
28668
+ expected_project_revision: plan.project.updated_at,
28669
+ ...opts.idempotencyKey ? { idempotency_key: opts.idempotencyKey } : {}
28670
+ }) : plan;
28671
+ if (globalOpts.json) {
28672
+ output(result, true);
28673
+ } else {
28674
+ const list = result.task_list ? `${result.task_list.slug} (${result.task_list.id})` : result.project.task_list_id;
28675
+ console.log(chalk5.green(`${result.mode === "plan" ? "Task-list plan" : "Task-list ensure"}: ${result.action}`));
28676
+ console.log(chalk5.dim(` Project: ${result.project.name} (${result.project.id})`));
28677
+ console.log(chalk5.dim(` Task list: ${list}`));
28678
+ if (result.receipt)
28679
+ console.log(chalk5.dim(` Receipt: ${result.receipt.receipt_id}`));
28680
+ }
28681
+ return;
28682
+ }
28683
+ if (opts.rollbackTaskList) {
28684
+ if (!opts.apply) {
28685
+ handleError(new Error("projects --rollback-task-list requires --apply"));
28686
+ }
28687
+ if (!opts.receipt) {
28688
+ handleError(new Error("projects --rollback-task-list requires --receipt"));
28689
+ }
28690
+ const project = cloud ? await cloudResolveProject(cloud, opts.rollbackTaskList) : resolveExplicitProject(opts.rollbackTaskList);
28691
+ const store = cloud ? null : createLocalSqliteTodosStorageAdapter({ db: getDatabase() });
28692
+ const plan = cloud ? await cloudPlanProjectTaskListEnsure(cloud, project.id) : await planProjectTaskListEnsure(store, project.id);
28693
+ if (!plan.task_list) {
28694
+ handleError(new Error("The project has no exact declared task list to roll back"));
28695
+ }
28696
+ const input = {
28697
+ receipt_id: opts.receipt,
28698
+ expected_task_list_revision: plan.task_list.updated_at
28699
+ };
28700
+ const result = cloud ? await cloudRollbackProjectTaskListEnsure(cloud, project.id, input) : await rollbackProjectTaskListEnsure(store, project.id, input);
28701
+ if (globalOpts.json) {
28702
+ output(result, true);
28703
+ } else {
28704
+ console.log(chalk5.green(`Task-list rollback: ${result.action}`));
28705
+ console.log(chalk5.dim(` Project: ${result.project_id}`));
28706
+ console.log(chalk5.dim(` Removed task list: ${result.task_list_id}`));
28707
+ console.log(chalk5.dim(` Rollback receipt: ${result.rollback_receipt_id}`));
28708
+ }
28709
+ return;
28710
+ }
27480
28711
  if (opts.show) {
27481
28712
  const project = cloud ? await cloudResolveProject(cloud, opts.show) : resolveExplicitProject(opts.show);
27482
28713
  outputRecord(project, Boolean(globalOpts.json), "Project:");
@@ -27976,6 +29207,8 @@ var init_project_commands = __esm(() => {
27976
29207
  init_config();
27977
29208
  init_helpers();
27978
29209
  init_output_redaction();
29210
+ init_local_sqlite();
29211
+ init_project_task_list_ensure();
27979
29212
  SEARCH_SCOPE_FLAG = {
27980
29213
  name: "--scope",
27981
29214
  vocabulary: SAVED_SEARCH_SCOPES,
@@ -28662,12 +29895,12 @@ function unique4(values) {
28662
29895
  function placeholders(values) {
28663
29896
  return values.map(() => "?").join(", ");
28664
29897
  }
28665
- function tableExists(db, table) {
29898
+ function tableExists2(db, table) {
28666
29899
  const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
28667
29900
  return Boolean(row);
28668
29901
  }
28669
29902
  function tableColumns(db, table) {
28670
- if (!tableExists(db, table))
29903
+ if (!tableExists2(db, table))
28671
29904
  return new Set;
28672
29905
  const rows = db.query(`PRAGMA table_info(${table})`).all();
28673
29906
  return new Set(rows.map((row) => row.name));
@@ -28683,7 +29916,7 @@ function normalizeScope2(options) {
28683
29916
  };
28684
29917
  }
28685
29918
  function scopedCommentTaskIds(db, commentIds) {
28686
- if (commentIds.length === 0 || !tableExists(db, "task_comments"))
29919
+ if (commentIds.length === 0 || !tableExists2(db, "task_comments"))
28687
29920
  return [];
28688
29921
  const rows = db.query(`SELECT DISTINCT task_id FROM task_comments WHERE id IN (${placeholders(commentIds)})`).all(...commentIds);
28689
29922
  return rows.map((row) => row.task_id).filter(Boolean);
@@ -29896,7 +31129,7 @@ __export(exports_local_extensions, {
29896
31129
  getLocalExtension: () => getLocalExtension,
29897
31130
  discoverLocalExtensions: () => discoverLocalExtensions
29898
31131
  });
29899
- import { createHash as createHash7, createVerify } from "crypto";
31132
+ import { createHash as createHash8, createVerify } from "crypto";
29900
31133
  import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
29901
31134
  import { basename as basename6, join as join16, resolve as resolve15 } from "path";
29902
31135
  function isObject(value) {
@@ -29982,7 +31215,7 @@ function parseJson(path) {
29982
31215
  return JSON.parse(readFileSync10(path, "utf8"));
29983
31216
  }
29984
31217
  function sha2564(bytes) {
29985
- return `sha256:${createHash7("sha256").update(bytes).digest("hex")}`;
31218
+ return `sha256:${createHash8("sha256").update(bytes).digest("hex")}`;
29986
31219
  }
29987
31220
  function compareVersions(a, b) {
29988
31221
  const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -31339,7 +32572,7 @@ var init_terminal_notifications = __esm(() => {
31339
32572
  });
31340
32573
 
31341
32574
  // src/db/api-keys.ts
31342
- import { createHash as createHash8, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
32575
+ import { createHash as createHash9, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
31343
32576
  function rowToRecord(row) {
31344
32577
  return {
31345
32578
  id: row.id,
@@ -31353,7 +32586,7 @@ function rowToRecord(row) {
31353
32586
  };
31354
32587
  }
31355
32588
  function hashApiKey(key) {
31356
- return createHash8("sha256").update(key).digest("hex");
32589
+ return createHash9("sha256").update(key).digest("hex");
31357
32590
  }
31358
32591
  function safeEqualHex(a, b) {
31359
32592
  if (a.length !== b.length)
@@ -31361,8 +32594,8 @@ function safeEqualHex(a, b) {
31361
32594
  return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
31362
32595
  }
31363
32596
  function safeEqualStrings(a, b) {
31364
- const ah = createHash8("sha256").update(a, "utf8").digest();
31365
- const bh = createHash8("sha256").update(b, "utf8").digest();
32597
+ const ah = createHash9("sha256").update(a, "utf8").digest();
32598
+ const bh = createHash9("sha256").update(b, "utf8").digest();
31366
32599
  return timingSafeEqual3(ah, bh);
31367
32600
  }
31368
32601
  function generatePlaintextKey() {
@@ -31889,239 +33122,6 @@ var init_postgres_sync = __esm(() => {
31889
33122
  };
31890
33123
  });
31891
33124
 
31892
- // src/lib/integrity.ts
31893
- function resolveIntegritySeverity(spec, measurement) {
31894
- if (spec.kind === "dangling")
31895
- return "error";
31896
- if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
31897
- return "error";
31898
- return spec.base_severity;
31899
- }
31900
- function plural(entity, count) {
31901
- const [one, many] = ENTITY_LABEL[entity];
31902
- return count === 1 ? one : many;
31903
- }
31904
- function formatIntegrityMessage(spec, measurement) {
31905
- const noun = plural(spec.entity, measurement.count);
31906
- const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
31907
- const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
31908
- return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
31909
- }
31910
- function measuredCondition(spec, measurement, source2) {
31911
- return {
31912
- id: spec.id,
31913
- entity: spec.entity,
31914
- field: spec.field,
31915
- kind: spec.kind,
31916
- count: measurement.count,
31917
- open_count: measurement.open_count,
31918
- severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
31919
- verified: true,
31920
- source: source2,
31921
- message: formatIntegrityMessage(spec, measurement),
31922
- impact: spec.impact
31923
- };
31924
- }
31925
- function unverifiedCondition(spec, reason) {
31926
- return {
31927
- id: spec.id,
31928
- entity: spec.entity,
31929
- field: spec.field,
31930
- kind: spec.kind,
31931
- count: null,
31932
- open_count: null,
31933
- severity: null,
31934
- verified: false,
31935
- source: "unverified",
31936
- unverified_reason: reason,
31937
- message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
31938
- impact: spec.impact
31939
- };
31940
- }
31941
- function summarizeIntegrity(conditions) {
31942
- const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
31943
- const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
31944
- const unverified = conditions.length - measured.length;
31945
- return {
31946
- ok: unverified === 0 && findings.length === 0,
31947
- findings: findings.length,
31948
- rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
31949
- errors: findings.filter((condition) => condition.severity === "error").length,
31950
- warnings: findings.filter((condition) => condition.severity === "warn").length,
31951
- unverified,
31952
- complete: unverified === 0
31953
- };
31954
- }
31955
- function buildIntegrityReport(conditions, generatedAt) {
31956
- const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source2) => source2 !== "unverified");
31957
- return {
31958
- schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
31959
- generated_at: generatedAt,
31960
- source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
31961
- conditions,
31962
- summary: summarizeIntegrity(conditions)
31963
- };
31964
- }
31965
- function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
31966
- const received = new Map;
31967
- if (Array.isArray(raw.conditions)) {
31968
- for (const entry2 of raw.conditions) {
31969
- if (entry2 && typeof entry2["id"] === "string")
31970
- received.set(entry2["id"], entry2);
31971
- }
31972
- }
31973
- const conditions = INTEGRITY_CONDITIONS.map((spec) => {
31974
- const entry2 = received.get(spec.id);
31975
- const count = entry2?.["count"];
31976
- const verified = entry2?.["verified"];
31977
- if (!entry2 || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
31978
- return unverifiedCondition(spec, entry2 ? typeof entry2["unverified_reason"] === "string" ? String(entry2["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
31979
- }
31980
- const openRaw = entry2["open_count"];
31981
- return measuredCondition(spec, {
31982
- count,
31983
- open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
31984
- }, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
31985
- });
31986
- const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
31987
- return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
31988
- }
31989
- function sqliteOpenStatusList() {
31990
- return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
31991
- }
31992
- function buildSqliteIntegritySql(spec) {
31993
- const table = SQLITE_TABLE[spec.entity];
31994
- const target = SQLITE_TABLE[spec.target];
31995
- const column = `t."${spec.field}"`;
31996
- const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
31997
- const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
31998
- return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
31999
- }
32000
- function buildPostgresIntegritySql(spec, options) {
32001
- const params = [options.service, RECORD_TYPE[spec.entity]];
32002
- const p = (value) => {
32003
- params.push(value);
32004
- return `$${params.length}`;
32005
- };
32006
- const column = `t.payload->>'${spec.field}'`;
32007
- const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
32008
- const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
32009
- const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
32010
- return { sql, params };
32011
- }
32012
- function referenceOf(row, field) {
32013
- const raw = row[field];
32014
- if (typeof raw !== "string")
32015
- return null;
32016
- const trimmed = raw.trim();
32017
- return trimmed === "" ? null : trimmed;
32018
- }
32019
- function measureIntegrityRows(spec, sets) {
32020
- const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
32021
- if (!rows)
32022
- return null;
32023
- const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
32024
- if (spec.kind === "dangling" && !registered)
32025
- return null;
32026
- let count = 0;
32027
- let open = 0;
32028
- for (const row of rows) {
32029
- const reference = referenceOf(row, spec.field);
32030
- const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
32031
- if (!matches)
32032
- continue;
32033
- count++;
32034
- if (spec.entity === "task") {
32035
- const status = row.status;
32036
- if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
32037
- open++;
32038
- }
32039
- }
32040
- return { count, open_count: spec.entity === "task" ? open : null };
32041
- }
32042
- var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
32043
- var init_integrity = __esm(() => {
32044
- init_types();
32045
- TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
32046
- OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
32047
- INTEGRITY_CONDITIONS = [
32048
- {
32049
- id: "tasks_without_project",
32050
- entity: "task",
32051
- field: "project_id",
32052
- target: "project",
32053
- kind: "missing",
32054
- base_severity: "warn",
32055
- escalate_when_open: true,
32056
- impact: "invisible to every project-scoped read (list, status, next, claim)"
32057
- },
32058
- {
32059
- id: "tasks_without_task_list",
32060
- entity: "task",
32061
- field: "task_list_id",
32062
- target: "task_list",
32063
- kind: "missing",
32064
- base_severity: "warn",
32065
- escalate_when_open: true,
32066
- impact: "invisible to every task-list read \u2014 the list reports zero open work"
32067
- },
32068
- {
32069
- id: "tasks_with_unregistered_project",
32070
- entity: "task",
32071
- field: "project_id",
32072
- target: "project",
32073
- kind: "dangling",
32074
- base_severity: "error",
32075
- escalate_when_open: false,
32076
- impact: "points at a project id that does not exist; the reference can never resolve"
32077
- },
32078
- {
32079
- id: "tasks_with_unregistered_task_list",
32080
- entity: "task",
32081
- field: "task_list_id",
32082
- target: "task_list",
32083
- kind: "dangling",
32084
- base_severity: "error",
32085
- escalate_when_open: false,
32086
- impact: "points at a task-list id that does not exist; the reference can never resolve"
32087
- },
32088
- {
32089
- id: "task_lists_without_project",
32090
- entity: "task_list",
32091
- field: "project_id",
32092
- target: "project",
32093
- kind: "missing",
32094
- base_severity: "warn",
32095
- escalate_when_open: false,
32096
- impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
32097
- },
32098
- {
32099
- id: "task_lists_with_unregistered_project",
32100
- entity: "task_list",
32101
- field: "project_id",
32102
- target: "project",
32103
- kind: "dangling",
32104
- base_severity: "error",
32105
- escalate_when_open: false,
32106
- impact: "points at a project id that does not exist; the list can never be reached"
32107
- }
32108
- ];
32109
- ENTITY_LABEL = {
32110
- task: ["task", "tasks"],
32111
- task_list: ["task list", "task lists"]
32112
- };
32113
- SQLITE_TABLE = {
32114
- task: "tasks",
32115
- task_list: "task_lists",
32116
- project: "projects"
32117
- };
32118
- RECORD_TYPE = {
32119
- task: "tasks",
32120
- task_list: "task_lists",
32121
- project: "projects"
32122
- };
32123
- });
32124
-
32125
33125
  // src/storage/postgres-adapter.ts
32126
33126
  import { randomUUID as randomUUID3 } from "crypto";
32127
33127
  function createPostgresTodosStorageAdapter(options) {
@@ -34271,8 +35271,8 @@ class PostgresTodosProjectRegistrationTransaction {
34271
35271
  ]);
34272
35272
  return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
34273
35273
  }
34274
- async getReceiptById(receiptId) {
34275
- const result = await this.client.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = $1 LIMIT 1", [receiptId]);
35274
+ async getReceiptById(receiptId2) {
35275
+ const result = await this.client.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = $1 LIMIT 1", [receiptId2]);
34276
35276
  return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
34277
35277
  }
34278
35278
  async getAcceptedReceiptForStep(identity) {
@@ -34552,8 +35552,8 @@ class PostgresTodosProjectRegistrationBackend {
34552
35552
  async getReceiptForLookup(identity) {
34553
35553
  return (await this.direct()).getReceiptForLookup(identity);
34554
35554
  }
34555
- async getReceiptById(receiptId) {
34556
- return (await this.direct()).getReceiptById(receiptId);
35555
+ async getReceiptById(receiptId2) {
35556
+ return (await this.direct()).getReceiptById(receiptId2);
34557
35557
  }
34558
35558
  async getBinding(scope, resourceKind, targetSelector) {
34559
35559
  return (await this.direct()).getBinding(scope, resourceKind, targetSelector);
@@ -34571,618 +35571,593 @@ var init_postgres2 = __esm(() => {
34571
35571
  init_types3();
34572
35572
  });
34573
35573
 
34574
- // src/db/integrity.ts
34575
- function tableExists2(db, table) {
34576
- return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
34577
- }
34578
- function scanSqliteIntegrity(db = getDatabase()) {
34579
- const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists2(db, table));
34580
- const conditions = INTEGRITY_CONDITIONS.map((spec) => {
34581
- if (missing.length > 0) {
34582
- return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
34583
- }
34584
- try {
34585
- const row = db.query(buildSqliteIntegritySql(spec)).get();
34586
- return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
34587
- } catch (error) {
34588
- return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
34589
- }
34590
- });
34591
- return buildIntegrityReport(conditions, now());
35574
+ // src/project-registration/sqlite.ts
35575
+ function sameSqliteValue(left, right) {
35576
+ return JSON.stringify(left) === JSON.stringify(right);
34592
35577
  }
34593
- var REQUIRED_TABLES;
34594
- var init_integrity2 = __esm(() => {
34595
- init_database();
34596
- init_integrity();
34597
- REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
34598
- });
34599
-
34600
- // src/storage/sqlite-snapshot.ts
34601
- function exportSqliteTodosStorageSnapshot(db) {
34602
- const d = db ?? getDatabase();
35578
+ function taskListFromRow(row) {
34603
35579
  return {
34604
- exportedAt: new Date().toISOString(),
34605
- source: "sqlite",
34606
- tasks: listTasks({ include_archived: true }, d),
34607
- projects: listProjects(d),
34608
- projectMachinePaths: listProjectMachinePaths(d),
34609
- plans: listPlans(undefined, d),
34610
- agents: listAgents({ include_archived: true }, d),
34611
- taskLists: listTaskLists(undefined, d),
34612
- templates: listTemplates(d),
34613
- templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
34614
- auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
34615
- tombstones: listStorageTombstones(d)
34616
- };
34617
- }
34618
- function importSqliteTodosStorageSnapshot(snapshot, db) {
34619
- const d = db ?? getDatabase();
34620
- const result = {
34621
- inserted: 0,
34622
- updated: 0,
34623
- deleted: 0,
34624
- skipped: 0,
34625
- errors: []
34626
- };
34627
- result.errors.push(...validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists));
34628
- if (result.errors.length === 0) {
34629
- const existingProjects = d.query("SELECT id, task_list_id FROM projects").all();
34630
- const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
34631
- result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
34632
- }
34633
- if (result.errors.length > 0)
34634
- return result;
34635
- const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
34636
- for (const row of rows) {
34637
- try {
34638
- const record = asRecord(row);
34639
- const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType, record["id"], d) : null;
34640
- if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
34641
- result.skipped += 1;
34642
- continue;
34643
- }
34644
- const state = upsertById(d, table, columns, record, updateClockColumn);
34645
- if (state === "inserted")
34646
- result.inserted += 1;
34647
- else if (state === "updated")
34648
- result.updated += 1;
34649
- else
34650
- result.skipped += 1;
34651
- afterUpsert?.(record, state !== "skipped");
34652
- } catch (error) {
34653
- result.errors.push(error instanceof Error ? error.message : String(error));
34654
- }
34655
- }
34656
- };
34657
- applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
34658
- applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
34659
- applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
34660
- applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
34661
- applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
34662
- applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
34663
- applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
34664
- applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
34665
- if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
34666
- replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
34667
- }
34668
- });
34669
- applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
34670
- applyTombstones(d, snapshot.tombstones ?? [], result);
34671
- return result;
34672
- }
34673
- function upsertById(db, table, columns, row, updateClockColumn) {
34674
- const id = row["id"];
34675
- if (typeof id !== "string" || !id)
34676
- throw new Error(`${table} row is missing id`);
34677
- const presentColumns = columns.filter((column) => (column in row));
34678
- if (!presentColumns.includes("id"))
34679
- presentColumns.unshift("id");
34680
- const existing = existsById2(db, table, id);
34681
- const placeholders3 = presentColumns.map(() => "?").join(", ");
34682
- const values = presentColumns.map((column) => valueForColumn(column, row[column]));
34683
- const updateColumns = presentColumns.filter((column) => column !== "id");
34684
- const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
34685
- const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
34686
- const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders3})
34687
- ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders3})`;
34688
- const changes = db.run(sql, values).changes;
34689
- if (changes === 0)
34690
- return "skipped";
34691
- return existing ? "updated" : "inserted";
34692
- }
34693
- function existsById2(db, table, id) {
34694
- return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
34695
- }
34696
- function valueForColumn(column, value) {
34697
- if (BOOLEAN_COLUMNS.has(column))
34698
- return value ? 1 : 0;
34699
- if (JSON_COLUMNS.has(column))
34700
- return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
34701
- return value === undefined ? null : value;
34702
- }
34703
- function asRecord(value) {
34704
- if (!value || typeof value !== "object" || Array.isArray(value)) {
34705
- throw new Error("snapshot rows must be objects");
34706
- }
34707
- return value;
34708
- }
34709
- function sortedTasks2(tasks) {
34710
- const byId = new Map(tasks.map((task) => [task.id, task]));
34711
- const seen = new Set;
34712
- const result = [];
34713
- const visit = (task) => {
34714
- if (seen.has(task.id))
34715
- return;
34716
- if (task.parent_id && byId.has(task.parent_id))
34717
- visit(byId.get(task.parent_id));
34718
- seen.add(task.id);
34719
- result.push(task);
34720
- };
34721
- for (const task of tasks)
34722
- visit(task);
34723
- return result;
34724
- }
34725
- function applyTombstones(db, tombstones, result) {
34726
- for (const tombstone of tombstones) {
34727
- try {
34728
- recordStorageTombstone({
34729
- object_type: tombstone.object_type,
34730
- object_id: tombstone.object_id,
34731
- deleted_at: tombstone.deleted_at,
34732
- source_machine_id: tombstone.source_machine_id ?? null,
34733
- payload: tombstone.payload ?? null,
34734
- version: tombstone.version ?? null
34735
- }, db);
34736
- const table = tableForTombstone(tombstone.object_type);
34737
- const existing = existingClock(db, table, tombstone.object_id);
34738
- if (!shouldApplyStorageTombstone(tombstone, existing)) {
34739
- result.skipped += 1;
34740
- continue;
34741
- }
34742
- const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
34743
- const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
34744
- if (deleted > 0 || deletedTags > 0)
34745
- result.deleted = (result.deleted ?? 0) + 1;
34746
- else
34747
- result.skipped += 1;
34748
- } catch (error) {
34749
- result.errors.push(error instanceof Error ? error.message : String(error));
34750
- }
34751
- }
34752
- }
34753
- function tableForTombstone(objectType) {
34754
- if (objectType === "tasks")
34755
- return "tasks";
34756
- if (objectType === "projects")
34757
- return "projects";
34758
- if (objectType === "project_machine_paths")
34759
- return "project_machine_paths";
34760
- if (objectType === "plans")
34761
- return "plans";
34762
- if (objectType === "agents")
34763
- return "agents";
34764
- if (objectType === "task_lists")
34765
- return "task_lists";
34766
- if (objectType === "templates")
34767
- return "task_templates";
34768
- if (objectType === "template_tasks")
34769
- return "template_tasks";
34770
- return "task_history";
34771
- }
34772
- function listRows(db, table, columns) {
34773
- return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
34774
- }
34775
- function listProjectMachinePaths(db) {
34776
- return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
34777
- id: String(row.id),
34778
- project_id: String(row.project_id),
34779
- machine_id: String(row.machine_id),
34780
- path: String(row.path),
34781
- created_at: String(row.created_at),
34782
- updated_at: String(row.updated_at)
34783
- }));
34784
- }
34785
- function existingClock(db, table, id) {
34786
- const clockColumns = clockColumnsForTable(table);
34787
- const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
34788
- return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
34789
- }
34790
- function rowClock(row, updateClockColumn) {
34791
- const value = updateClockColumn ? row[updateClockColumn] : null;
34792
- return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
34793
- }
34794
- function stringClock(value) {
34795
- return typeof value === "string" && value ? value : null;
34796
- }
34797
- function clockColumnsForTable(table) {
34798
- if (table === "agents")
34799
- return ["last_seen_at", "created_at"];
34800
- if (table === "task_templates")
34801
- return ["created_at"];
34802
- if (table === "task_history")
34803
- return ["created_at"];
34804
- return ["updated_at", "created_at"];
34805
- }
34806
- var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TEMPLATE_TASK_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
34807
- var init_sqlite_snapshot = __esm(() => {
34808
- init_database();
34809
- init_agents();
34810
- init_audit();
34811
- init_plans();
34812
- init_projects();
34813
- init_task_lists();
34814
- init_tasks();
34815
- init_templates();
34816
- init_storage_tombstones();
34817
- PROJECT_COLUMNS = [
34818
- "id",
34819
- "name",
34820
- "path",
34821
- "description",
34822
- "task_list_id",
34823
- "task_prefix",
34824
- "task_counter",
34825
- "created_at",
34826
- "updated_at",
34827
- "machine_id",
34828
- "synced_at"
34829
- ];
34830
- PROJECT_MACHINE_PATH_COLUMNS = [
34831
- "id",
34832
- "project_id",
34833
- "machine_id",
34834
- "path",
34835
- "created_at",
34836
- "updated_at"
34837
- ];
34838
- TASK_LIST_COLUMNS = [
34839
- "id",
34840
- "project_id",
34841
- "slug",
34842
- "name",
34843
- "description",
34844
- "metadata",
34845
- "created_at",
34846
- "updated_at",
34847
- "machine_id",
34848
- "synced_at"
34849
- ];
34850
- PLAN_COLUMNS = [
34851
- "id",
34852
- "project_id",
34853
- "task_list_id",
34854
- "agent_id",
34855
- "name",
34856
- "description",
34857
- "status",
34858
- "created_at",
34859
- "updated_at",
34860
- "machine_id",
34861
- "synced_at"
34862
- ];
34863
- AGENT_COLUMNS = [
34864
- "id",
34865
- "name",
34866
- "description",
34867
- "role",
34868
- "title",
34869
- "level",
34870
- "permissions",
34871
- "capabilities",
34872
- "reports_to",
34873
- "org_id",
34874
- "metadata",
34875
- "status",
34876
- "created_at",
34877
- "last_seen_at",
34878
- "session_id",
34879
- "working_dir",
34880
- "active_project_id",
34881
- "machine_id",
34882
- "synced_at"
34883
- ];
34884
- TEMPLATE_COLUMNS = [
34885
- "id",
34886
- "name",
34887
- "title_pattern",
34888
- "description",
34889
- "priority",
34890
- "tags",
34891
- "variables",
34892
- "project_id",
34893
- "plan_id",
34894
- "metadata",
34895
- "version",
34896
- "created_at",
34897
- "machine_id",
34898
- "synced_at"
34899
- ];
34900
- TEMPLATE_TASK_COLUMNS = [
34901
- "id",
34902
- "template_id",
34903
- "position",
34904
- "title_pattern",
34905
- "description",
34906
- "priority",
34907
- "tags",
34908
- "task_type",
34909
- "condition",
34910
- "include_template_id",
34911
- "depends_on_positions",
34912
- "metadata",
34913
- "created_at"
34914
- ];
34915
- TASK_COLUMNS = [
34916
- "id",
34917
- "short_id",
34918
- "project_id",
34919
- "parent_id",
34920
- "plan_id",
34921
- "task_list_id",
34922
- "title",
34923
- "description",
34924
- "status",
34925
- "priority",
34926
- "agent_id",
34927
- "assigned_to",
34928
- "session_id",
34929
- "working_dir",
34930
- "tags",
34931
- "metadata",
34932
- "version",
34933
- "locked_by",
34934
- "locked_at",
34935
- "created_at",
34936
- "updated_at",
34937
- "started_at",
34938
- "completed_at",
34939
- "due_at",
34940
- "estimated_minutes",
34941
- "actual_minutes",
34942
- "requires_approval",
34943
- "approved_by",
34944
- "approved_at",
34945
- "recurrence_rule",
34946
- "recurrence_parent_id",
34947
- "spawns_template_id",
34948
- "confidence",
34949
- "reason",
34950
- "spawned_from_session",
34951
- "assigned_by",
34952
- "assigned_from_project",
34953
- "task_type",
34954
- "cost_tokens",
34955
- "cost_usd",
34956
- "delegated_from",
34957
- "delegation_depth",
34958
- "retry_count",
34959
- "max_retries",
34960
- "retry_after",
34961
- "sla_minutes",
34962
- "runner_id",
34963
- "runner_started_at",
34964
- "runner_completed_at",
34965
- "current_step",
34966
- "total_steps",
34967
- "cycle_id",
34968
- "machine_id",
34969
- "synced_at",
34970
- "archived_at"
34971
- ];
34972
- AUDIT_COLUMNS = [
34973
- "id",
34974
- "task_id",
34975
- "action",
34976
- "field",
34977
- "old_value",
34978
- "new_value",
34979
- "agent_id",
34980
- "created_at",
34981
- "machine_id"
34982
- ];
34983
- JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
34984
- BOOLEAN_COLUMNS = new Set(["requires_approval"]);
34985
- });
35580
+ ...row,
35581
+ metadata: JSON.parse(row.metadata || "{}")
35582
+ };
35583
+ }
35584
+ function selectProject(db, id) {
35585
+ return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
35586
+ }
35587
+ function selectTaskList(db, id) {
35588
+ const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
35589
+ return row ? taskListFromRow(row) : null;
35590
+ }
35591
+ function selectProjectConflict(db, path, taskListSlug) {
35592
+ return db.query(`
35593
+ SELECT * FROM projects
35594
+ WHERE path = ? OR task_list_id = ?
35595
+ ORDER BY created_at ASC, id ASC
35596
+ LIMIT 1
35597
+ `).get(path, taskListSlug);
35598
+ }
35599
+ function selectTaskListConflict(db, projectId, slug) {
35600
+ const row = db.query(`
35601
+ SELECT * FROM task_lists
35602
+ WHERE project_id = ? AND slug = ?
35603
+ LIMIT 1
35604
+ `).get(projectId, slug);
35605
+ return row ? taskListFromRow(row) : null;
35606
+ }
35607
+ function quoteSqliteIdentifier(value) {
35608
+ return `"${value.replaceAll('"', '""')}"`;
35609
+ }
35610
+ function hasSqliteDependents(db, resourceKind, targetId) {
35611
+ const targetTable = resourceKind === "project" ? "projects" : "task_lists";
35612
+ const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
35613
+ const tables = db.query(`
35614
+ SELECT name FROM sqlite_schema
35615
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
35616
+ ORDER BY name
35617
+ `).all();
35618
+ for (const { name: tableName } of tables) {
35619
+ const quotedTable = quoteSqliteIdentifier(tableName);
35620
+ const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
35621
+ const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
35622
+ const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
35623
+ for (const columnName of referenceColumns) {
35624
+ const row = db.query(`
35625
+ SELECT 1 AS found
35626
+ FROM ${quotedTable}
35627
+ WHERE ${quoteSqliteIdentifier(columnName)} = ?
35628
+ LIMIT 1
35629
+ `).get(targetId);
35630
+ if (row)
35631
+ return true;
35632
+ }
35633
+ }
35634
+ return false;
35635
+ }
35636
+ function receiptFromRow2(row) {
35637
+ return {
35638
+ ...row,
35639
+ authority: "todos",
35640
+ created_by_operation: Number(row["created_by_operation"]) === 1
35641
+ };
35642
+ }
35643
+ function bindingFromRow2(row) {
35644
+ return row;
35645
+ }
34986
35646
 
34987
- // src/storage/local-sqlite.ts
34988
- function resolveTaskRefLocal(db, ref) {
34989
- const raw = ref.trim().toLowerCase();
34990
- if (!raw)
34991
- return null;
34992
- if (TASK_UUID_RE2.test(raw))
34993
- return getTask(raw, db);
34994
- const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
34995
- if (prefixRows.length > 1) {
34996
- throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
35647
+ class SqliteTodosProjectRegistrationTransaction {
35648
+ db;
35649
+ storage;
35650
+ constructor(db) {
35651
+ this.db = db;
35652
+ this.storage = createLocalSqliteTodosStorageAdapter({ db });
34997
35653
  }
34998
- if (prefixRows.length === 1)
34999
- return getTask(prefixRows[0].id, db);
35000
- const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
35001
- if (shortIdRows.length > 1) {
35002
- throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
35654
+ async lockStep(_identity) {}
35655
+ async getReceiptForLookup(identity) {
35656
+ const row = this.db.query(`
35657
+ SELECT * FROM todos_project_registration_receipts
35658
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35659
+ AND operation_id = ? AND step_id = ? AND resource_kind = ?
35660
+ AND direction = ? AND idempotency_key = ? AND target_selector = ?
35661
+ ORDER BY CASE outcome
35662
+ WHEN 'terminal_nonacceptance' THEN 0
35663
+ WHEN 'duplicate_of_accepted' THEN 1
35664
+ ELSE 2
35665
+ END, created_at DESC, receipt_id DESC
35666
+ LIMIT 1
35667
+ `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
35668
+ return row ? receiptFromRow2(row) : null;
35669
+ }
35670
+ async getReceiptById(receiptId2) {
35671
+ const row = this.db.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = ? LIMIT 1").get(receiptId2);
35672
+ return row ? receiptFromRow2(row) : null;
35673
+ }
35674
+ async getAcceptedReceiptForStep(identity) {
35675
+ const row = this.db.query(`
35676
+ SELECT * FROM todos_project_registration_receipts
35677
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35678
+ AND operation_id = ? AND step_id = ? AND resource_kind = ?
35679
+ AND direction = ? AND outcome = 'accepted'
35680
+ ORDER BY created_at ASC, receipt_id ASC
35681
+ LIMIT 1
35682
+ `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction);
35683
+ return row ? receiptFromRow2(row) : null;
35684
+ }
35685
+ async insertReceipt(receipt) {
35686
+ const result = this.db.query(`
35687
+ INSERT OR IGNORE INTO todos_project_registration_receipts (
35688
+ receipt_id, authority, route, package_version, authority_id, tenant_id,
35689
+ corpus_id, operation_id, step_id, resource_kind, direction,
35690
+ target_selector, idempotency_key, request_digest, precondition_digest,
35691
+ normalized_call_digest, outcome, reason, target_id, result_revision,
35692
+ result_digest, duplicate_of_receipt_id, accepted_receipt_id,
35693
+ created_by_operation, created_at
35694
+ ) VALUES (
35695
+ ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
35696
+ )
35697
+ `).run(receipt.receipt_id, receipt.authority, receipt.route, receipt.package_version, receipt.authority_id, receipt.tenant_id, receipt.corpus_id, receipt.operation_id, receipt.step_id, receipt.resource_kind, receipt.direction, receipt.target_selector, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.normalized_call_digest, receipt.outcome, receipt.reason, receipt.target_id, receipt.result_revision, receipt.result_digest, receipt.duplicate_of_receipt_id, receipt.accepted_receipt_id, receipt.created_by_operation ? 1 : 0, receipt.created_at);
35698
+ return result.changes === 1;
35699
+ }
35700
+ async getBinding(scope, resourceKind, targetSelector) {
35701
+ const row = this.db.query(`
35702
+ SELECT * FROM todos_project_registration_bindings
35703
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35704
+ AND resource_kind = ? AND target_selector = ?
35705
+ LIMIT 1
35706
+ `).get(scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35707
+ return row ? bindingFromRow2(row) : null;
35708
+ }
35709
+ async claimBinding(binding) {
35710
+ const result = this.db.query(`
35711
+ INSERT OR IGNORE INTO todos_project_registration_bindings (
35712
+ authority_id, tenant_id, corpus_id, resource_kind, target_selector,
35713
+ operation_id, step_id, direction, idempotency_key, request_digest,
35714
+ precondition_digest, normalized_call_digest, state, target_id,
35715
+ accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
35716
+ created_at, updated_at
35717
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
35718
+ `).run(binding.authority_id, binding.tenant_id, binding.corpus_id, binding.resource_kind, binding.target_selector, binding.operation_id, binding.step_id, binding.direction, binding.idempotency_key, binding.request_digest, binding.precondition_digest, binding.normalized_call_digest, binding.state, binding.target_id, binding.accepted_receipt_id, binding.result_revision, binding.result_digest, binding.removed_receipt_id, binding.created_at, binding.updated_at);
35719
+ return result.changes === 1;
35720
+ }
35721
+ async setBindingAccepted(scope, resourceKind, targetSelector, update) {
35722
+ const result = this.db.query(`
35723
+ UPDATE todos_project_registration_bindings
35724
+ SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
35725
+ result_revision = ?, result_digest = ?, updated_at = ?
35726
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35727
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
35728
+ `).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35729
+ if (result.changes !== 1) {
35730
+ throw new Error("Todos project registration binding was not pending at acceptance");
35731
+ }
35732
+ }
35733
+ async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
35734
+ this.db.query(`
35735
+ UPDATE todos_project_registration_bindings
35736
+ SET state = 'terminal_nonacceptance', updated_at = ?
35737
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35738
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
35739
+ `).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35740
+ }
35741
+ async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
35742
+ const result = this.db.query(`
35743
+ UPDATE todos_project_registration_bindings
35744
+ SET state = 'removed', removed_receipt_id = ?, updated_at = ?
35745
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35746
+ AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
35747
+ `).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35748
+ if (result.changes !== 1) {
35749
+ throw new Error("Todos project registration binding was not accepted at removal");
35750
+ }
35751
+ }
35752
+ async findProjectConflict(path, taskListSlug) {
35753
+ const row = this.db.query(`
35754
+ SELECT * FROM projects
35755
+ WHERE path = ? OR task_list_id = ?
35756
+ ORDER BY created_at ASC, id ASC
35757
+ LIMIT 1
35758
+ `).get(path, taskListSlug);
35759
+ return row ?? null;
35760
+ }
35761
+ async findTaskListConflict(projectId, slug) {
35762
+ return await this.storage.taskLists.getBySlug(slug, projectId);
35763
+ }
35764
+ async createProject(input) {
35765
+ return await this.storage.projects.create(input);
35766
+ }
35767
+ async createTaskList(input) {
35768
+ return await this.storage.taskLists.create(input);
35769
+ }
35770
+ async getProject(id) {
35771
+ return await this.storage.projects.get(id);
35772
+ }
35773
+ async getTaskList(id) {
35774
+ return await this.storage.taskLists.get(id);
35775
+ }
35776
+ async lockCompensationWrites() {}
35777
+ async hasDependents(resourceKind, targetId) {
35778
+ return hasSqliteDependents(this.db, resourceKind, targetId);
35779
+ }
35780
+ async deleteProject(id) {
35781
+ return await this.storage.projects.delete(id);
35782
+ }
35783
+ async deleteTaskList(id) {
35784
+ return await this.storage.taskLists.delete(id);
35003
35785
  }
35004
- if (shortIdRows.length === 1)
35005
- return getTask(shortIdRows[0].id, db);
35006
- return null;
35007
- }
35008
- function isSearchQuery(filter) {
35009
- const q = filter.query?.trim();
35010
- return !!q && q !== "*";
35011
35786
  }
35012
- function matchesExtraFilters(task, filter) {
35013
- if (filter.ids && !filter.ids.includes(task.id))
35014
- return false;
35015
- if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
35016
- return false;
35017
- if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
35018
- return false;
35019
- if (filter.session_id !== undefined && task.session_id !== filter.session_id)
35020
- return false;
35021
- if (filter.has_recurrence !== undefined && Boolean(task.recurrence_rule) !== filter.has_recurrence)
35022
- return false;
35023
- if (filter.task_type !== undefined) {
35024
- const allowed = Array.isArray(filter.task_type) ? filter.task_type : [filter.task_type];
35025
- if (!allowed.includes(task.task_type ?? ""))
35787
+
35788
+ class StagedSqliteTodosProjectRegistrationTransaction {
35789
+ db;
35790
+ direct;
35791
+ validators = [];
35792
+ mutations = [];
35793
+ receipts = new Map;
35794
+ bindings = new Map;
35795
+ projects = new Map;
35796
+ taskLists = new Map;
35797
+ constructor(db) {
35798
+ this.db = db;
35799
+ this.direct = new SqliteTodosProjectRegistrationTransaction(db);
35800
+ }
35801
+ commit() {
35802
+ this.db.exec("BEGIN IMMEDIATE");
35803
+ try {
35804
+ for (const validate of this.validators) {
35805
+ if (!validate()) {
35806
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
35807
+ }
35808
+ }
35809
+ for (const mutate of this.mutations)
35810
+ mutate();
35811
+ this.db.exec("COMMIT");
35812
+ } catch (error) {
35813
+ try {
35814
+ this.db.exec("ROLLBACK");
35815
+ } catch {}
35816
+ throw error;
35817
+ }
35818
+ }
35819
+ async lockStep(_identity) {}
35820
+ async getReceiptForLookup(identity) {
35821
+ const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
35822
+ const stored = await this.direct.getReceiptForLookup(identity);
35823
+ if (stored)
35824
+ staged.push(stored);
35825
+ const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
35826
+ return staged.sort((left, right) => outcomeRank(left) - outcomeRank(right) || right.created_at.localeCompare(left.created_at) || right.receipt_id.localeCompare(left.receipt_id))[0] ?? null;
35827
+ }
35828
+ async getReceiptById(receiptId2) {
35829
+ return this.receipts.get(receiptId2) ?? this.direct.getReceiptById(receiptId2);
35830
+ }
35831
+ async getAcceptedReceiptForStep(identity) {
35832
+ const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.outcome === "accepted");
35833
+ const stored = await this.direct.getAcceptedReceiptForStep(identity);
35834
+ if (stored)
35835
+ staged.push(stored);
35836
+ return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
35837
+ }
35838
+ async insertReceipt(receipt) {
35839
+ if (this.receipts.has(receipt.receipt_id))
35840
+ return false;
35841
+ if (await this.direct.getReceiptById(receipt.receipt_id))
35026
35842
  return false;
35843
+ const planned = { ...receipt };
35844
+ this.receipts.set(planned.receipt_id, planned);
35845
+ this.mutations.push(() => {
35846
+ try {
35847
+ const result = this.db.query(`
35848
+ INSERT OR IGNORE INTO todos_project_registration_receipts (
35849
+ receipt_id, authority, route, package_version, authority_id, tenant_id,
35850
+ corpus_id, operation_id, step_id, resource_kind, direction,
35851
+ target_selector, idempotency_key, request_digest, precondition_digest,
35852
+ normalized_call_digest, outcome, reason, target_id, result_revision,
35853
+ result_digest, duplicate_of_receipt_id, accepted_receipt_id,
35854
+ created_by_operation, created_at
35855
+ ) VALUES (
35856
+ ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
35857
+ )
35858
+ `).run(planned.receipt_id, planned.authority, planned.route, planned.package_version, planned.authority_id, planned.tenant_id, planned.corpus_id, planned.operation_id, planned.step_id, planned.resource_kind, planned.direction, planned.target_selector, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.outcome, planned.reason, planned.target_id, planned.result_revision, planned.result_digest, planned.duplicate_of_receipt_id, planned.accepted_receipt_id, planned.created_by_operation ? 1 : 0, planned.created_at);
35859
+ if (result.changes !== 1) {
35860
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
35861
+ }
35862
+ } catch (error) {
35863
+ if (error instanceof SqliteRegistrationOptimisticConflict)
35864
+ throw error;
35865
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
35866
+ }
35867
+ });
35868
+ return true;
35027
35869
  }
35028
- if (filter.tags?.length) {
35029
- const taskTags = new Set(task.tags ?? []);
35030
- if (!filter.tags.every((tag) => taskTags.has(tag)))
35870
+ async getBinding(scope, resourceKind, targetSelector) {
35871
+ const key = this.bindingKey(scope, resourceKind, targetSelector);
35872
+ return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
35873
+ }
35874
+ async claimBinding(binding) {
35875
+ const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
35876
+ if (this.bindings.has(key))
35031
35877
  return false;
35878
+ if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
35879
+ return false;
35880
+ }
35881
+ const planned = { ...binding };
35882
+ this.bindings.set(key, planned);
35883
+ this.mutations.push(() => {
35884
+ try {
35885
+ const result = this.db.query(`
35886
+ INSERT OR IGNORE INTO todos_project_registration_bindings (
35887
+ authority_id, tenant_id, corpus_id, resource_kind, target_selector,
35888
+ operation_id, step_id, direction, idempotency_key, request_digest,
35889
+ precondition_digest, normalized_call_digest, state, target_id,
35890
+ accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
35891
+ created_at, updated_at
35892
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
35893
+ `).run(planned.authority_id, planned.tenant_id, planned.corpus_id, planned.resource_kind, planned.target_selector, planned.operation_id, planned.step_id, planned.direction, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.state, planned.target_id, planned.accepted_receipt_id, planned.result_revision, planned.result_digest, planned.removed_receipt_id, planned.created_at, planned.updated_at);
35894
+ if (result.changes !== 1) {
35895
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
35896
+ }
35897
+ } catch (error) {
35898
+ if (error instanceof SqliteRegistrationOptimisticConflict)
35899
+ throw error;
35900
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
35901
+ }
35902
+ });
35903
+ return true;
35032
35904
  }
35033
- if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
35034
- return false;
35035
- return true;
35036
- }
35037
- function listTasksMaybeSearch(filter, db) {
35038
- if (!isSearchQuery(filter))
35039
- return listTasks(filter, db);
35040
- const matched = searchTasks({
35041
- query: filter.query,
35042
- project_id: filter.project_id,
35043
- task_list_id: filter.task_list_id,
35044
- status: filter.status,
35045
- priority: filter.priority,
35046
- assigned_to: filter.assigned_to,
35047
- agent_id: filter.agent_id
35048
- }, undefined, undefined, db).filter((task) => matchesExtraFilters(task, filter));
35049
- const offset = filter.offset && filter.offset > 0 ? Math.trunc(filter.offset) : 0;
35050
- if (filter.limit !== undefined && filter.limit >= 0)
35051
- return matched.slice(offset, offset + filter.limit);
35052
- return offset ? matched.slice(offset) : matched;
35053
- }
35054
- function createLocalSqliteTodosStorageAdapter(options = {}) {
35055
- const database = () => options.db ?? getDatabase();
35056
- let adapter;
35057
- adapter = {
35058
- kind: "sqlite",
35059
- capabilities: {
35060
- localPersistence: true,
35061
- remotePersistence: false,
35062
- transactions: true,
35063
- auditLog: true,
35064
- sync: true
35065
- },
35066
- tasks: {
35067
- create: (input, context) => createTask({
35068
- ...input,
35069
- agent_id: input.agent_id ?? context?.agentId,
35070
- created_by: input.created_by ?? input.agent_id ?? context?.agentId
35071
- }, database()),
35072
- get: (id) => getTask(id, database()),
35073
- resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
35074
- list: (filter = {}) => listTasksMaybeSearch(filter, database()),
35075
- count: (filter = {}) => isSearchQuery(filter) ? listTasksMaybeSearch({ ...filter, limit: undefined, offset: undefined }, database()).length : countTasks(filter, database()),
35076
- update: (id, input) => updateTask(id, input, database()),
35077
- unlock: (id, agentId) => {
35078
- unlockTask(id, agentId, database());
35079
- return true;
35080
- },
35081
- delete: (id) => deleteTask(id, database()),
35082
- start: (id, agentId) => startTask(id, agentId, database()),
35083
- complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
35084
- fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
35085
- claimNext: (agentId, filters) => claimNextTask(agentId, filters, database()),
35086
- getNext: (agentId, filters) => getNextTask(agentId, filters, database()),
35087
- getActiveWork: (filters) => getActiveWork(filters, database()),
35088
- getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
35089
- },
35090
- projects: {
35091
- create: (input) => createProject(input, database()),
35092
- get: (id) => getProject(id, database()),
35093
- getByPath: (path) => getProjectByPath(path, database()),
35094
- list: () => listProjects(database()),
35095
- update: (id, input) => updateProject(id, input, database()),
35096
- rename: (id, input) => renameProject(id, input, database()),
35097
- delete: (id) => deleteProject(id, database())
35098
- },
35099
- plans: {
35100
- create: (input) => createPlan(input, database()),
35101
- get: (id) => getPlan(id, database()),
35102
- list: (projectId) => listPlans(projectId, database()),
35103
- update: (id, input) => updatePlan(id, input, database()),
35104
- delete: (id) => deletePlan(id, database())
35105
- },
35106
- agents: {
35107
- register: (input) => registerAgent(input, database()),
35108
- get: (id) => getAgent(id, database()),
35109
- getByName: (name) => getAgentByName(name, database()),
35110
- list: (options2) => listAgents(options2, database()),
35111
- update: (id, input) => updateAgent(id, input, database())
35112
- },
35113
- taskLists: {
35114
- create: (input) => createTaskList(input, database()),
35115
- get: (id) => getTaskList(id, database()),
35116
- getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
35117
- list: (projectId) => listTaskLists(projectId, database()),
35118
- update: (id, input) => updateTaskList(id, input, database()),
35119
- delete: (id) => deleteTaskList(id, database())
35120
- },
35121
- templates: {
35122
- create: (input) => createTemplate(input, database()),
35123
- get: (id) => getTemplate(id, database()),
35124
- list: () => listTemplates(database()),
35125
- update: (id, input) => updateTemplate(id, input, database()),
35126
- delete: (id) => deleteTemplate(id, database()),
35127
- getWithTasks: (id) => getTemplateWithTasks(id, database())
35128
- },
35129
- audit: {
35130
- logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
35131
- addComment: (input) => addComment(input, database()),
35132
- getComments: (taskId) => listComments(taskId, database()),
35133
- getCommentsPage: (taskId, options2) => {
35134
- if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
35135
- throw new Error("Comment limit must be an integer between 1 and 1001");
35905
+ async setBindingAccepted(scope, resourceKind, targetSelector, update) {
35906
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
35907
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
35908
+ ...binding,
35909
+ state: "accepted",
35910
+ target_id: update.target_id,
35911
+ accepted_receipt_id: update.accepted_receipt_id,
35912
+ result_revision: update.result_revision,
35913
+ result_digest: update.result_digest,
35914
+ updated_at: update.updated_at
35915
+ });
35916
+ this.mutations.push(() => {
35917
+ const result = this.db.query(`
35918
+ UPDATE todos_project_registration_bindings
35919
+ SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
35920
+ result_revision = ?, result_digest = ?, updated_at = ?
35921
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35922
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
35923
+ `).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35924
+ if (result.changes !== 1) {
35925
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
35926
+ }
35927
+ });
35928
+ }
35929
+ async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
35930
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
35931
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
35932
+ ...binding,
35933
+ state: "terminal_nonacceptance",
35934
+ updated_at: updatedAt
35935
+ });
35936
+ this.mutations.push(() => {
35937
+ const result = this.db.query(`
35938
+ UPDATE todos_project_registration_bindings
35939
+ SET state = 'terminal_nonacceptance', updated_at = ?
35940
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35941
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
35942
+ `).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35943
+ if (result.changes !== 1) {
35944
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
35945
+ }
35946
+ });
35947
+ }
35948
+ async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
35949
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
35950
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
35951
+ ...binding,
35952
+ state: "removed",
35953
+ removed_receipt_id: removedReceiptId,
35954
+ updated_at: updatedAt
35955
+ });
35956
+ this.mutations.push(() => {
35957
+ const result = this.db.query(`
35958
+ UPDATE todos_project_registration_bindings
35959
+ SET state = 'removed', removed_receipt_id = ?, updated_at = ?
35960
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
35961
+ AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
35962
+ `).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
35963
+ if (result.changes !== 1) {
35964
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
35965
+ }
35966
+ });
35967
+ }
35968
+ async findProjectConflict(path, taskListSlug) {
35969
+ const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
35970
+ if (planned)
35971
+ return planned;
35972
+ const observed = selectProjectConflict(this.db, path, taskListSlug);
35973
+ this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
35974
+ return observed;
35975
+ }
35976
+ async findTaskListConflict(projectId, slug) {
35977
+ const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
35978
+ if (planned)
35979
+ return planned;
35980
+ const observed = selectTaskListConflict(this.db, projectId, slug);
35981
+ this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
35982
+ return observed;
35983
+ }
35984
+ async createProject(input) {
35985
+ const derivedSlug = normalizeSlug(input.name);
35986
+ const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
35987
+ if (!derivedSlug || !taskListId) {
35988
+ throw new Error("Project name and task-list slug must be non-empty");
35989
+ }
35990
+ const project = {
35991
+ id: uuid(),
35992
+ name: input.name,
35993
+ path: input.path,
35994
+ description: input.description || null,
35995
+ task_list_id: taskListId,
35996
+ task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
35997
+ task_counter: 0,
35998
+ created_at: now(),
35999
+ updated_at: now(),
36000
+ machine_id: currentStorageMachineId(this.db)
36001
+ };
36002
+ project.updated_at = project.created_at;
36003
+ this.projects.set(project.id, project);
36004
+ this.mutations.push(() => {
36005
+ try {
36006
+ const result = this.db.run(`INSERT INTO projects (
36007
+ id, name, path, description, task_list_id, task_prefix,
36008
+ task_counter, created_at, updated_at, machine_id
36009
+ ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
36010
+ project.id,
36011
+ project.name,
36012
+ project.path,
36013
+ project.description,
36014
+ project.task_list_id,
36015
+ project.task_prefix,
36016
+ project.created_at,
36017
+ project.updated_at,
36018
+ project.machine_id ?? null
36019
+ ]);
36020
+ if (result.changes < 1) {
36021
+ throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
35136
36022
  }
35137
- let comments = listComments(taskId, database());
35138
- comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
35139
- if (options2?.before) {
35140
- const before = options2.before;
35141
- comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
36023
+ } catch (error) {
36024
+ if (error instanceof SqliteRegistrationOptimisticConflict)
36025
+ throw error;
36026
+ throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
36027
+ }
36028
+ });
36029
+ return project;
36030
+ }
36031
+ async createTaskList(input) {
36032
+ const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
36033
+ if (!slug)
36034
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
36035
+ const taskList = {
36036
+ id: uuid(),
36037
+ project_id: input.project_id || null,
36038
+ slug,
36039
+ name: input.name,
36040
+ description: input.description || null,
36041
+ metadata: input.metadata ?? {},
36042
+ created_at: now(),
36043
+ updated_at: now(),
36044
+ machine_id: currentStorageMachineId(this.db)
36045
+ };
36046
+ taskList.updated_at = taskList.created_at;
36047
+ this.taskLists.set(taskList.id, taskList);
36048
+ this.mutations.push(() => {
36049
+ try {
36050
+ const result = this.db.run(`INSERT INTO task_lists (
36051
+ id, project_id, slug, name, description, metadata,
36052
+ created_at, updated_at, machine_id
36053
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
36054
+ taskList.id,
36055
+ taskList.project_id,
36056
+ taskList.slug,
36057
+ taskList.name,
36058
+ taskList.description,
36059
+ JSON.stringify(taskList.metadata),
36060
+ taskList.created_at,
36061
+ taskList.updated_at,
36062
+ taskList.machine_id ?? null
36063
+ ]);
36064
+ if (result.changes < 1) {
36065
+ throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
35142
36066
  }
35143
- if (options2?.limit !== undefined)
35144
- comments = comments.slice(-options2.limit);
35145
- return comments;
35146
- },
35147
- getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
35148
- getRecentActivity: (limit) => getRecentActivity(limit, database())
35149
- },
35150
- sync: {
35151
- getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database()),
35152
- exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
35153
- importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
35154
- },
35155
- integrity: {
35156
- report: () => scanSqliteIntegrity(database())
35157
- },
35158
- transaction: (fn) => {
35159
- const tx = database().transaction(() => fn(adapter));
35160
- return tx();
36067
+ } catch (error) {
36068
+ if (error instanceof SqliteRegistrationOptimisticConflict)
36069
+ throw error;
36070
+ throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
36071
+ }
36072
+ });
36073
+ return taskList;
36074
+ }
36075
+ async getProject(id) {
36076
+ if (this.projects.has(id))
36077
+ return this.projects.get(id) ?? null;
36078
+ const observed = selectProject(this.db, id);
36079
+ this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
36080
+ return observed;
36081
+ }
36082
+ async getTaskList(id) {
36083
+ if (this.taskLists.has(id))
36084
+ return this.taskLists.get(id) ?? null;
36085
+ const observed = selectTaskList(this.db, id);
36086
+ this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
36087
+ return observed;
36088
+ }
36089
+ async lockCompensationWrites() {}
36090
+ async hasDependents(resourceKind, targetId) {
36091
+ const observed = hasSqliteDependents(this.db, resourceKind, targetId);
36092
+ this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
36093
+ return observed;
36094
+ }
36095
+ async deleteProject(id) {
36096
+ const project = await this.getProject(id);
36097
+ if (!project)
36098
+ return false;
36099
+ this.projects.set(id, null);
36100
+ this.mutations.push(() => {
36101
+ recordStorageTombstone({
36102
+ object_type: "projects",
36103
+ object_id: id,
36104
+ payload: project
36105
+ }, this.db);
36106
+ if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
36107
+ throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
36108
+ }
36109
+ });
36110
+ return true;
36111
+ }
36112
+ async deleteTaskList(id) {
36113
+ const taskList = await this.getTaskList(id);
36114
+ if (!taskList)
36115
+ return false;
36116
+ this.taskLists.set(id, null);
36117
+ this.mutations.push(() => {
36118
+ recordStorageTombstone({
36119
+ object_type: "task_lists",
36120
+ object_id: id,
36121
+ payload: taskList
36122
+ }, this.db);
36123
+ if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
36124
+ throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
36125
+ }
36126
+ });
36127
+ return true;
36128
+ }
36129
+ bindingKey(scope, resourceKind, targetSelector) {
36130
+ return JSON.stringify([
36131
+ scope.authority_id,
36132
+ scope.tenant_id,
36133
+ scope.corpus_id,
36134
+ resourceKind,
36135
+ targetSelector
36136
+ ]);
36137
+ }
36138
+ async requireBinding(scope, resourceKind, targetSelector, state) {
36139
+ const binding = await this.getBinding(scope, resourceKind, targetSelector);
36140
+ if (!binding || binding.state !== state) {
36141
+ throw new Error(`Todos project registration binding was not ${state}`);
35161
36142
  }
35162
- };
35163
- return adapter;
36143
+ return binding;
36144
+ }
36145
+ availableProjectPrefix(name) {
36146
+ const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
36147
+ const prefix = words.length >= 3 ? words.slice(0, 3).map((word) => word[0].toUpperCase()).join("") : words.length === 2 ? (words[0].slice(0, 2) + words[1][0]).toUpperCase() : words[0].slice(0, 3).toUpperCase();
36148
+ let candidate = prefix;
36149
+ let suffix = 1;
36150
+ while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
36151
+ suffix += 1;
36152
+ candidate = `${prefix}${suffix}`;
36153
+ }
36154
+ return candidate;
36155
+ }
35164
36156
  }
35165
- var TASK_UUID_RE2;
35166
- var init_local_sqlite = __esm(() => {
35167
- init_types();
35168
- init_search();
35169
- init_tasks();
35170
- init_projects();
35171
- init_plans();
35172
- init_agents();
35173
- init_task_lists();
35174
- init_templates();
35175
- init_audit();
35176
- init_comments();
35177
- init_database();
35178
- init_integrity2();
35179
- init_sqlite_snapshot();
35180
- TASK_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
35181
- });
35182
-
35183
- // src/project-registration/sqlite.ts
35184
- var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS;
36157
+ var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS, SqliteRegistrationOptimisticConflict;
35185
36158
  var init_sqlite = __esm(() => {
36159
+ init_database();
36160
+ init_storage_tombstones();
35186
36161
  init_local_sqlite();
35187
36162
  sqliteTransactionTails = new WeakMap;
35188
36163
  PROJECT_REFERENCE_COLUMNS = new Set([
@@ -35192,10 +36167,16 @@ var init_sqlite = __esm(() => {
35192
36167
  "external_project_id"
35193
36168
  ]);
35194
36169
  TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
36170
+ SqliteRegistrationOptimisticConflict = class SqliteRegistrationOptimisticConflict extends Error {
36171
+ constructor(message, options = {}) {
36172
+ super(message, options);
36173
+ this.name = "SqliteRegistrationOptimisticConflict";
36174
+ }
36175
+ };
35195
36176
  });
35196
36177
 
35197
36178
  // src/project-registration/authority.ts
35198
- import { createHash as createHash9 } from "crypto";
36179
+ import { createHash as createHash10 } from "crypto";
35199
36180
  function canonicalProjectRegistrationJson(value) {
35200
36181
  return JSON.stringify(canonicalize2(value));
35201
36182
  }
@@ -35213,7 +36194,7 @@ function canonicalize2(value) {
35213
36194
  return out;
35214
36195
  }
35215
36196
  function digestProjectRegistrationValue(value) {
35216
- return createHash9("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
36197
+ return createHash10("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
35217
36198
  }
35218
36199
  function deriveTodosProjectRegistrationIdempotencyKey(input) {
35219
36200
  return `prk_${digestProjectRegistrationValue({
@@ -35354,7 +36335,7 @@ function taskListRecord(taskList) {
35354
36335
  })
35355
36336
  };
35356
36337
  }
35357
- function receiptId(input) {
36338
+ function receiptId2(input) {
35358
36339
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
35359
36340
  }
35360
36341
  function capabilityMatches(request, capability) {
@@ -35512,7 +36493,7 @@ function assertInverseRequest(request, capability) {
35512
36493
  function makeReceipt(input, createdAt) {
35513
36494
  return {
35514
36495
  ...input,
35515
- receipt_id: receiptId(input),
36496
+ receipt_id: receiptId2(input),
35516
36497
  created_at: createdAt
35517
36498
  };
35518
36499
  }
@@ -38389,6 +39370,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
38389
39370
  Task: taskSchema,
38390
39371
  Project: projectSchema,
38391
39372
  TaskList: taskListSchema,
39373
+ ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
39374
+ ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
39375
+ ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
38392
39376
  TaskComment: taskCommentSchema,
38393
39377
  Plan: planSchema,
38394
39378
  Template: templateSchema,
@@ -38466,6 +39450,29 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
38466
39450
  name: { type: "string", minLength: 1 }
38467
39451
  }
38468
39452
  },
39453
+ ProjectTaskListEnsureApplyInput: {
39454
+ type: "object",
39455
+ additionalProperties: false,
39456
+ required: ["expected_project_revision"],
39457
+ properties: {
39458
+ expected_project_revision: { type: "string", minLength: 1 },
39459
+ idempotency_key: {
39460
+ type: "string",
39461
+ minLength: 8,
39462
+ maxLength: 128,
39463
+ pattern: "^[A-Za-z0-9._:-]+$"
39464
+ }
39465
+ }
39466
+ },
39467
+ ProjectTaskListRollbackInput: {
39468
+ type: "object",
39469
+ additionalProperties: false,
39470
+ required: ["receipt_id", "expected_task_list_revision"],
39471
+ properties: {
39472
+ receipt_id: { type: "string", minLength: 1 },
39473
+ expected_task_list_revision: { type: "string", minLength: 1 }
39474
+ }
39475
+ },
38469
39476
  ErrorResponse: {
38470
39477
  type: "object",
38471
39478
  required: ["error"],
@@ -39568,6 +40575,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
39568
40575
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
39569
40576
  }
39570
40577
  },
40578
+ "/v1/projects/{id}/task-list/ensure": {
40579
+ get: {
40580
+ operationId: "planProjectTaskListEnsure",
40581
+ summary: "Plan a non-mutating repair of a project's declared task list",
40582
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
40583
+ responses: {
40584
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
40585
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
40586
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
40587
+ }
40588
+ },
40589
+ post: {
40590
+ operationId: "ensureProjectTaskList",
40591
+ summary: "Idempotently create an existing project's declared task list",
40592
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
40593
+ requestBody: {
40594
+ required: true,
40595
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureApplyInput" } } }
40596
+ },
40597
+ responses: {
40598
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
40599
+ "201": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
40600
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
40601
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
40602
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
40603
+ }
40604
+ }
40605
+ },
40606
+ "/v1/projects/{id}/task-list/rollback": {
40607
+ post: {
40608
+ operationId: "rollbackProjectTaskListEnsure",
40609
+ summary: "Conditionally remove an unchanged task list created by an accepted ensure receipt",
40610
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
40611
+ requestBody: {
40612
+ required: true,
40613
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackInput" } } }
40614
+ },
40615
+ responses: {
40616
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackResult" } } } },
40617
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
40618
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
40619
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
40620
+ }
40621
+ }
40622
+ },
39571
40623
  "/v1/projects/{id}/rename": {
39572
40624
  post: {
39573
40625
  operationId: "renameProject",
@@ -39849,7 +40901,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
39849
40901
  }
39850
40902
  };
39851
40903
  }
39852
- var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
40904
+ var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
39853
40905
  var init_openapi = __esm(() => {
39854
40906
  init_package_version();
39855
40907
  init_types();
@@ -39897,6 +40949,80 @@ var init_openapi = __esm(() => {
39897
40949
  updated_at: { type: "string" }
39898
40950
  }
39899
40951
  };
40952
+ projectTaskListEnsureReceiptSchema = {
40953
+ type: "object",
40954
+ additionalProperties: false,
40955
+ required: [
40956
+ "schema_version",
40957
+ "receipt_id",
40958
+ "idempotency_key",
40959
+ "project_id",
40960
+ "task_list_id",
40961
+ "slug",
40962
+ "created_by_operation",
40963
+ "result_revision",
40964
+ "result_digest",
40965
+ "rollback_supported",
40966
+ "created_at"
40967
+ ],
40968
+ properties: {
40969
+ schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
40970
+ receipt_id: { type: "string" },
40971
+ idempotency_key: { type: "string" },
40972
+ project_id: { type: "string" },
40973
+ task_list_id: { type: "string" },
40974
+ slug: { type: "string" },
40975
+ created_by_operation: { type: "boolean" },
40976
+ result_revision: { type: "string" },
40977
+ result_digest: { type: "string" },
40978
+ rollback_supported: { type: "boolean" },
40979
+ created_at: { type: "string", format: "date-time" }
40980
+ }
40981
+ };
40982
+ projectTaskListEnsureResultSchema = {
40983
+ type: "object",
40984
+ additionalProperties: false,
40985
+ required: ["mode", "action", "project", "task_list", "receipt"],
40986
+ properties: {
40987
+ mode: { type: "string", enum: ["plan", "apply"] },
40988
+ action: { type: "string", enum: ["would_create", "created", "already_present"] },
40989
+ project: { $ref: "#/components/schemas/Project" },
40990
+ task_list: {
40991
+ oneOf: [
40992
+ { $ref: "#/components/schemas/TaskList" },
40993
+ { type: "null" }
40994
+ ]
40995
+ },
40996
+ receipt: {
40997
+ oneOf: [
40998
+ { $ref: "#/components/schemas/ProjectTaskListEnsureReceipt" },
40999
+ { type: "null" }
41000
+ ]
41001
+ }
41002
+ }
41003
+ };
41004
+ projectTaskListRollbackResultSchema = {
41005
+ type: "object",
41006
+ additionalProperties: false,
41007
+ required: [
41008
+ "schema_version",
41009
+ "action",
41010
+ "project_id",
41011
+ "task_list_id",
41012
+ "accepted_receipt_id",
41013
+ "rollback_receipt_id",
41014
+ "removed_at"
41015
+ ],
41016
+ properties: {
41017
+ schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
41018
+ action: { type: "string", enum: ["removed"] },
41019
+ project_id: { type: "string" },
41020
+ task_list_id: { type: "string" },
41021
+ accepted_receipt_id: { type: "string" },
41022
+ rollback_receipt_id: { type: "string" },
41023
+ removed_at: { type: "string", format: "date-time" }
41024
+ }
41025
+ };
39900
41026
  taskCommentSchema = {
39901
41027
  type: "object",
39902
41028
  required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
@@ -40894,6 +42020,52 @@ async function handleV1Request(req, url, dependencies = {}) {
40894
42020
  }
40895
42021
  return error(405, `method ${method} not allowed on /v1/projects`);
40896
42022
  }
42023
+ if (action === "task-list" && subId === "ensure") {
42024
+ if (method === "GET") {
42025
+ return json4(await planProjectTaskListEnsure(store, id));
42026
+ }
42027
+ if (method !== "POST") {
42028
+ return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
42029
+ }
42030
+ const body = await readJson3(req);
42031
+ if (!body)
42032
+ return error(400, "invalid JSON body");
42033
+ const unknown = Object.keys(body).find((key) => !["expected_project_revision", "idempotency_key"].includes(key));
42034
+ if (unknown)
42035
+ return error(400, `unknown task-list ensure field: ${unknown}`);
42036
+ if (typeof body.expected_project_revision !== "string" || !body.expected_project_revision.trim()) {
42037
+ return error(400, "expected_project_revision must be a non-empty string from a fresh ensure plan");
42038
+ }
42039
+ if (body.idempotency_key !== undefined && typeof body.idempotency_key !== "string") {
42040
+ return error(400, "idempotency_key must be a string");
42041
+ }
42042
+ const result = await applyProjectTaskListEnsure(store, id, {
42043
+ expected_project_revision: body.expected_project_revision,
42044
+ ...typeof body.idempotency_key === "string" ? { idempotency_key: body.idempotency_key } : {}
42045
+ });
42046
+ return json4(result, result.action === "created" ? 201 : 200);
42047
+ }
42048
+ if (action === "task-list" && subId === "rollback") {
42049
+ if (method !== "POST") {
42050
+ return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/rollback`);
42051
+ }
42052
+ const body = await readJson3(req);
42053
+ if (!body)
42054
+ return error(400, "invalid JSON body");
42055
+ const unknown = Object.keys(body).find((key) => !["receipt_id", "expected_task_list_revision"].includes(key));
42056
+ if (unknown)
42057
+ return error(400, `unknown task-list rollback field: ${unknown}`);
42058
+ if (typeof body.receipt_id !== "string" || !body.receipt_id.trim()) {
42059
+ return error(400, "receipt_id must be a non-empty string");
42060
+ }
42061
+ if (typeof body.expected_task_list_revision !== "string" || !body.expected_task_list_revision.trim()) {
42062
+ return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
42063
+ }
42064
+ return json4(await rollbackProjectTaskListEnsure(store, id, {
42065
+ receipt_id: body.receipt_id,
42066
+ expected_task_list_revision: body.expected_task_list_revision
42067
+ }));
42068
+ }
40897
42069
  if (action === "rename") {
40898
42070
  if (method !== "POST")
40899
42071
  return error(405, `method ${method} not allowed on /v1/projects/:id/rename`);
@@ -41232,6 +42404,10 @@ async function handleV1Request(req, url, dependencies = {}) {
41232
42404
  }
41233
42405
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
41234
42406
  } catch (e) {
42407
+ if (e instanceof ProjectTaskListEnsureError) {
42408
+ const status = e.code === "PROJECT_NOT_FOUND" || e.code === "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND" ? 404 : e.code === "PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID" ? 400 : 409;
42409
+ return error(status, e.message, { code: e.code, conflict: status === 409, ...e.details });
42410
+ }
41235
42411
  if (e instanceof TaskReferenceAmbiguousError) {
41236
42412
  return error(409, e.message, {
41237
42413
  code: TaskReferenceAmbiguousError.code,
@@ -41258,6 +42434,7 @@ var init_v1 = __esm(() => {
41258
42434
  init_pr_groups();
41259
42435
  init_project_registration();
41260
42436
  init_redaction();
42437
+ init_project_task_list_ensure();
41261
42438
  JSON_HEADERS3 = { "Content-Type": "application/json" };
41262
42439
  });
41263
42440
 
@@ -48967,7 +50144,7 @@ __export(exports_audit_ledger, {
48967
50144
  LOCAL_AUDIT_LEDGER_INITIAL_HASH: () => LOCAL_AUDIT_LEDGER_INITIAL_HASH,
48968
50145
  LOCAL_AUDIT_LEDGER_HASH_ALGORITHM: () => LOCAL_AUDIT_LEDGER_HASH_ALGORITHM
48969
50146
  });
48970
- import { createHash as createHash10 } from "crypto";
50147
+ import { createHash as createHash11 } from "crypto";
48971
50148
  function canonicalize3(value) {
48972
50149
  if (value === null || typeof value !== "object")
48973
50150
  return JSON.stringify(value);
@@ -48977,7 +50154,7 @@ function canonicalize3(value) {
48977
50154
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalize3(object[key])}`).join(",")}}`;
48978
50155
  }
48979
50156
  function hash(value) {
48980
- return createHash10("sha256").update(value).digest("hex");
50157
+ return createHash11("sha256").update(value).digest("hex");
48981
50158
  }
48982
50159
  function parsePayload3(value) {
48983
50160
  if (!value)
@@ -57693,7 +58870,7 @@ __export(exports_local_backups, {
57693
58870
  TODOS_LOCAL_BACKUP_KIND: () => TODOS_LOCAL_BACKUP_KIND,
57694
58871
  LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
57695
58872
  });
57696
- import { createHash as createHash11 } from "crypto";
58873
+ import { createHash as createHash12 } from "crypto";
57697
58874
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
57698
58875
  import { dirname as dirname9, resolve as resolve20 } from "path";
57699
58876
  import { mkdirSync as mkdirSync10 } from "fs";
@@ -57706,7 +58883,7 @@ function stableJson2(value) {
57706
58883
  return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson2(record[key])}`).join(",")}}`;
57707
58884
  }
57708
58885
  function sha2565(value) {
57709
- return createHash11("sha256").update(stableJson2(value)).digest("hex");
58886
+ return createHash12("sha256").update(stableJson2(value)).digest("hex");
57710
58887
  }
57711
58888
  function sqliteIntegrity(db) {
57712
58889
  let quick = "unknown";
@@ -58387,7 +59564,7 @@ __export(exports_local_snapshots, {
58387
59564
  getLocalSnapshot: () => getLocalSnapshot,
58388
59565
  TODOS_LOCAL_SNAPSHOT_SCHEMA_VERSION: () => TODOS_LOCAL_SNAPSHOT_SCHEMA_VERSION
58389
59566
  });
58390
- import { createHash as createHash12 } from "crypto";
59567
+ import { createHash as createHash13 } from "crypto";
58391
59568
  function source2(version) {
58392
59569
  return {
58393
59570
  packageName: "@hasna/todos",
@@ -58411,7 +59588,7 @@ function stable(value) {
58411
59588
  return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stable(item)]));
58412
59589
  }
58413
59590
  function sha2566(value) {
58414
- return createHash12("sha256").update(JSON.stringify(stable(value))).digest("hex");
59591
+ return createHash13("sha256").update(JSON.stringify(stable(value))).digest("hex");
58415
59592
  }
58416
59593
  function latestTimestamp2(items, fallback) {
58417
59594
  const timestamps = [];
@@ -58983,7 +60160,7 @@ __export(exports_agent_replay_simulator, {
58983
60160
  simulateAgentReplay: () => simulateAgentReplay,
58984
60161
  renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
58985
60162
  });
58986
- import { createHash as createHash13 } from "crypto";
60163
+ import { createHash as createHash14 } from "crypto";
58987
60164
  import { readFileSync as readFileSync15 } from "fs";
58988
60165
  function isObject2(value) {
58989
60166
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -59005,7 +60182,7 @@ function stable2(value) {
59005
60182
  return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
59006
60183
  }
59007
60184
  function fingerprint2(value) {
59008
- return createHash13("sha256").update(JSON.stringify(stable2(value))).digest("hex");
60185
+ return createHash14("sha256").update(JSON.stringify(stable2(value))).digest("hex");
59009
60186
  }
59010
60187
  function unpackFixture(input) {
59011
60188
  if (!isObject2(input))
@@ -59323,7 +60500,7 @@ __export(exports_inbox, {
59323
60500
  deriveInboxTitle: () => deriveInboxTitle,
59324
60501
  createInboxItem: () => createInboxItem
59325
60502
  });
59326
- import { createHash as createHash14 } from "crypto";
60503
+ import { createHash as createHash15 } from "crypto";
59327
60504
  function parseMetadata3(value) {
59328
60505
  if (!value)
59329
60506
  return {};
@@ -59343,7 +60520,7 @@ function compactWhitespace(value) {
59343
60520
  function fingerprintInboxInput(input) {
59344
60521
  const sourceType = input.source_type || detectInboxSourceType(input.body, input.source_url);
59345
60522
  const normalized = compactWhitespace(sanitizePreWriteText(input.body, "inbox.fingerprint")).slice(0, 8000);
59346
- return createHash14("sha256").update(`${sourceType}
60523
+ return createHash15("sha256").update(`${sourceType}
59347
60524
  ${input.source_url || ""}
59348
60525
  ${normalized}`).digest("hex");
59349
60526
  }
@@ -64555,13 +65732,13 @@ __export(exports_environment_snapshots, {
64555
65732
  compareEnvironmentSnapshotFiles: () => compareEnvironmentSnapshotFiles,
64556
65733
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
64557
65734
  });
64558
- import { createHash as createHash15 } from "crypto";
65735
+ import { createHash as createHash16 } from "crypto";
64559
65736
  import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync10 } from "fs";
64560
65737
  import { hostname as hostname2, platform, arch } from "os";
64561
65738
  import { dirname as dirname10, join as join22, resolve as resolve21 } from "path";
64562
65739
  import { tmpdir as tmpdir4 } from "os";
64563
65740
  function sha2567(value) {
64564
- return createHash15("sha256").update(value).digest("hex");
65741
+ return createHash16("sha256").update(value).digest("hex");
64565
65742
  }
64566
65743
  function fileRecord(root, relativePath) {
64567
65744
  const path = join22(root, relativePath);
@@ -64676,8 +65853,8 @@ function defaultSnapshotDir() {
64676
65853
  return join22(dirname10(resolve21(dbPath)), "environment-snapshots");
64677
65854
  }
64678
65855
  function snapshotWithId(snapshot) {
64679
- const digest = sha2567(JSON.stringify(snapshot)).slice(0, 24);
64680
- return { id: `env_${digest}`, ...snapshot };
65856
+ const digest2 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
65857
+ return { id: `env_${digest2}`, ...snapshot };
64681
65858
  }
64682
65859
  function captureEnvironmentSnapshot(input = {}) {
64683
65860
  const root = resolve21(input.root || process.cwd());
@@ -69352,15 +70529,15 @@ __export(exports_task_route_sources, {
69352
70529
  TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION: () => TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION
69353
70530
  });
69354
70531
  import { Database as Database4 } from "bun:sqlite";
69355
- import { createHash as createHash16 } from "crypto";
70532
+ import { createHash as createHash17 } from "crypto";
69356
70533
  import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync11 } from "fs";
69357
70534
  import { basename as basename10, dirname as dirname14, join as join26, resolve as resolve22 } from "path";
69358
70535
  function normalizePath6(input) {
69359
70536
  return resolve22(input);
69360
70537
  }
69361
70538
  function sourceStoreId(sourceDbPath) {
69362
- const digest = createHash16("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
69363
- return `sqlite:${digest}`;
70539
+ const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
70540
+ return `sqlite:${digest2}`;
69364
70541
  }
69365
70542
  function inferSourceRepoPath(sourceDbPath) {
69366
70543
  const normalized = normalizePath6(sourceDbPath);
@@ -69691,7 +70868,7 @@ __export(exports_tester_issue_reports, {
69691
70868
  TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
69692
70869
  TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION
69693
70870
  });
69694
- import { createHash as createHash17 } from "crypto";
70871
+ import { createHash as createHash18 } from "crypto";
69695
70872
  function asObject3(value) {
69696
70873
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
69697
70874
  }
@@ -69863,7 +71040,7 @@ function fingerprintTesterIssueReport(report) {
69863
71040
  normalizeText4(report.failure?.message || report.summary || report.title).slice(0, 240),
69864
71041
  normalizeText4(stackTop).slice(0, 160)
69865
71042
  ].join("::");
69866
- return `testers:${createHash17("sha256").update(raw).digest("hex").slice(0, 16)}`;
71043
+ return `testers:${createHash18("sha256").update(raw).digest("hex").slice(0, 16)}`;
69867
71044
  }
69868
71045
  function priorityForSeverity(severity, fallback) {
69869
71046
  return PRIORITIES5.includes(severity) ? severity : fallback;
@@ -74498,7 +75675,7 @@ var init_dispatch3 = __esm(() => {
74498
75675
  });
74499
75676
 
74500
75677
  // src/lib/delegation-brief.ts
74501
- import { createHash as createHash18 } from "crypto";
75678
+ import { createHash as createHash19 } from "crypto";
74502
75679
  function resolveDelegationBrief(input, sources) {
74503
75680
  const hasPath = typeof input.briefPath === "string" && input.briefPath.length > 0;
74504
75681
  const hasText = typeof input.briefText === "string" && input.briefText.length > 0;
@@ -74558,7 +75735,7 @@ function resolveDelegationBrief(input, sources) {
74558
75735
  ok: true,
74559
75736
  text: text2,
74560
75737
  source: source3,
74561
- sha256: createHash18("sha256").update(text2, "utf8").digest("hex"),
75738
+ sha256: createHash19("sha256").update(text2, "utf8").digest("hex"),
74562
75739
  bytes: Buffer.byteLength(text2, "utf8")
74563
75740
  };
74564
75741
  }
@@ -81101,7 +82278,7 @@ var init_hybrid = __esm(() => {
81101
82278
  });
81102
82279
 
81103
82280
  // src/storage/s3-artifacts.ts
81104
- import { createHash as createHash19, createHmac as createHmac2 } from "crypto";
82281
+ import { createHash as createHash20, createHmac as createHmac2 } from "crypto";
81105
82282
  function createTodosS3ArtifactStore(options) {
81106
82283
  const requestFetch = options.fetch ?? fetch;
81107
82284
  const now4 = options.now ?? (() => new Date);
@@ -81273,7 +82450,7 @@ function toAmzDate(date) {
81273
82450
  return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
81274
82451
  }
81275
82452
  function sha256Hex(value) {
81276
- return createHash19("sha256").update(value).digest("hex");
82453
+ return createHash20("sha256").update(value).digest("hex");
81277
82454
  }
81278
82455
  function hmac(key, value) {
81279
82456
  return createHmac2("sha256", key).update(value).digest();