@hasna/todos 0.11.94 → 0.12.1

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.
Files changed (124) hide show
  1. package/README.md +5 -5
  2. package/dist/cli/cloud-router.d.ts +17 -1
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/api-key-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/help-commands.d.ts +2 -1
  8. package/dist/cli/commands/help-commands.d.ts.map +1 -1
  9. package/dist/cli/commands/knowledge-commands.d.ts.map +1 -1
  10. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  11. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  12. package/dist/cli/commands/pr-group-commands.d.ts +3 -0
  13. package/dist/cli/commands/pr-group-commands.d.ts.map +1 -0
  14. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  15. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  16. package/dist/cli/commands/risk-commands.d.ts.map +1 -1
  17. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  18. package/dist/cli/commands/usage-ledger-commands.d.ts.map +1 -1
  19. package/dist/cli/helpers.d.ts +23 -0
  20. package/dist/cli/helpers.d.ts.map +1 -1
  21. package/dist/cli/index.js +8539 -1940
  22. package/dist/cli/stage-a.d.ts +17 -0
  23. package/dist/cli/stage-a.d.ts.map +1 -1
  24. package/dist/contracts.js +1665 -523
  25. package/dist/db/agent-names.d.ts +13 -1
  26. package/dist/db/agent-names.d.ts.map +1 -1
  27. package/dist/db/agents.d.ts +4 -0
  28. package/dist/db/agents.d.ts.map +1 -1
  29. package/dist/db/audit.d.ts.map +1 -1
  30. package/dist/db/database.d.ts.map +1 -1
  31. package/dist/db/dispatches.d.ts.map +1 -1
  32. package/dist/db/identity-mapping.d.ts +40 -0
  33. package/dist/db/identity-mapping.d.ts.map +1 -0
  34. package/dist/db/inbox.d.ts.map +1 -1
  35. package/dist/db/migrations.d.ts.map +1 -1
  36. package/dist/db/schema.d.ts.map +1 -1
  37. package/dist/db/storage-tombstones.d.ts +1 -1
  38. package/dist/db/storage-tombstones.d.ts.map +1 -1
  39. package/dist/db/task-commits.d.ts.map +1 -1
  40. package/dist/db/task-crud.d.ts.map +1 -1
  41. package/dist/db/task-lifecycle.d.ts.map +1 -1
  42. package/dist/db/templates.d.ts.map +1 -1
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4784 -882
  46. package/dist/json-contracts.d.ts +4 -2
  47. package/dist/json-contracts.d.ts.map +1 -1
  48. package/dist/lib/activity-audit.d.ts.map +1 -1
  49. package/dist/lib/artifact-store.d.ts.map +1 -1
  50. package/dist/lib/cli-help.d.ts +14 -3
  51. package/dist/lib/cli-help.d.ts.map +1 -1
  52. package/dist/lib/evidence-redaction.d.ts +72 -0
  53. package/dist/lib/evidence-redaction.d.ts.map +1 -0
  54. package/dist/lib/headless-boundaries.d.ts +1 -1
  55. package/dist/lib/headless-boundaries.d.ts.map +1 -1
  56. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  57. package/dist/lib/local-bridge.d.ts +1 -0
  58. package/dist/lib/local-bridge.d.ts.map +1 -1
  59. package/dist/lib/local-encryption.d.ts.map +1 -1
  60. package/dist/lib/prewrite-secrets.d.ts +29 -0
  61. package/dist/lib/prewrite-secrets.d.ts.map +1 -0
  62. package/dist/lib/redaction.d.ts.map +1 -1
  63. package/dist/lib/search.d.ts +3 -0
  64. package/dist/lib/search.d.ts.map +1 -1
  65. package/dist/lib/task-route-sources.d.ts +2 -0
  66. package/dist/lib/task-route-sources.d.ts.map +1 -1
  67. package/dist/lib/task-routing.d.ts.map +1 -1
  68. package/dist/lib/template-semantics.d.ts +8 -0
  69. package/dist/lib/template-semantics.d.ts.map +1 -0
  70. package/dist/mcp/index.js +6631 -1062
  71. package/dist/mcp.js +1 -1
  72. package/dist/pr-groups/http-client.d.ts +24 -0
  73. package/dist/pr-groups/http-client.d.ts.map +1 -0
  74. package/dist/pr-groups/index.d.ts +9 -0
  75. package/dist/pr-groups/index.d.ts.map +1 -0
  76. package/dist/pr-groups/ledger.d.ts +20 -0
  77. package/dist/pr-groups/ledger.d.ts.map +1 -0
  78. package/dist/pr-groups/postgres.d.ts +21 -0
  79. package/dist/pr-groups/postgres.d.ts.map +1 -0
  80. package/dist/pr-groups/sqlite.d.ts +16 -0
  81. package/dist/pr-groups/sqlite.d.ts.map +1 -0
  82. package/dist/pr-groups/types.d.ts +320 -0
  83. package/dist/pr-groups/types.d.ts.map +1 -0
  84. package/dist/registry.js +1665 -523
  85. package/dist/release-provenance.json +5 -5
  86. package/dist/sdk/client.d.ts +16 -0
  87. package/dist/sdk/client.d.ts.map +1 -1
  88. package/dist/sdk/index.d.ts +2 -1
  89. package/dist/sdk/index.d.ts.map +1 -1
  90. package/dist/sdk/index.js +99 -0
  91. package/dist/sdk/v1.generated.d.ts +388 -1
  92. package/dist/sdk/v1.generated.d.ts.map +1 -1
  93. package/dist/server/cloud.d.ts +3 -0
  94. package/dist/server/cloud.d.ts.map +1 -1
  95. package/dist/server/index.js +6538 -969
  96. package/dist/server/openapi.d.ts +2020 -278
  97. package/dist/server/openapi.d.ts.map +1 -1
  98. package/dist/server/pr-groups.d.ts +7 -0
  99. package/dist/server/pr-groups.d.ts.map +1 -0
  100. package/dist/server/serve.d.ts.map +1 -1
  101. package/dist/server/v1.d.ts +2 -1
  102. package/dist/server/v1.d.ts.map +1 -1
  103. package/dist/storage/cloud-client.d.ts +1 -0
  104. package/dist/storage/cloud-client.d.ts.map +1 -1
  105. package/dist/storage/config.d.ts +5 -5
  106. package/dist/storage/config.d.ts.map +1 -1
  107. package/dist/storage/index.d.ts +1 -1
  108. package/dist/storage/index.d.ts.map +1 -1
  109. package/dist/storage/interfaces.d.ts +3 -2
  110. package/dist/storage/interfaces.d.ts.map +1 -1
  111. package/dist/storage/local-sqlite.d.ts.map +1 -1
  112. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  113. package/dist/storage/postgres-sync.d.ts +1 -1
  114. package/dist/storage/postgres-sync.d.ts.map +1 -1
  115. package/dist/storage/shadow-outbox.d.ts.map +1 -1
  116. package/dist/storage/shadow.d.ts +1 -1
  117. package/dist/storage/shadow.d.ts.map +1 -1
  118. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  119. package/dist/storage.d.ts +1 -1
  120. package/dist/storage.d.ts.map +1 -1
  121. package/dist/storage.js +1958 -524
  122. package/dist/types/index.d.ts +90 -0
  123. package/dist/types/index.d.ts.map +1 -1
  124. package/package.json +1 -1
package/dist/storage.js CHANGED
@@ -62,15 +62,15 @@ __export(exports_config, {
62
62
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
63
63
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
64
64
  STORAGE_TABLES: () => STORAGE_TABLES,
65
- CANONICAL_TODOS_RDS_RUNTIME_PATH: () => CANONICAL_TODOS_RDS_RUNTIME_PATH,
65
+ CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
66
66
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
67
- CANONICAL_TODOS_RDS_CLUSTER: () => CANONICAL_TODOS_RDS_CLUSTER
67
+ CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
68
68
  });
69
- function getCanonicalTodosRdsConfig() {
69
+ function getCanonicalTodosRdsConfig(env = process.env) {
70
70
  return {
71
- cluster: CANONICAL_TODOS_RDS_CLUSTER,
71
+ cluster: clean(env[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
72
72
  database: CANONICAL_TODOS_RDS_DATABASE,
73
- runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
73
+ runtimeSecretPath: clean(env[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
74
74
  primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
75
75
  fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
76
76
  };
@@ -203,7 +203,7 @@ function parsePositiveInteger(value, fallback) {
203
203
  }
204
204
  return parsed;
205
205
  }
206
- var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres", CANONICAL_TODOS_RDS_DATABASE = "todos", CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
206
+ var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
207
207
  var init_config = __esm(() => {
208
208
  TODOS_STORAGE_TABLES = [
209
209
  "todos_sync_records",
@@ -246,144 +246,6 @@ var init_config = __esm(() => {
246
246
  };
247
247
  });
248
248
 
249
- // src/types/index.ts
250
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
251
- var init_types = __esm(() => {
252
- TASK_STATUSES = [
253
- "pending",
254
- "in_progress",
255
- "completed",
256
- "failed",
257
- "cancelled"
258
- ];
259
- TASK_PRIORITIES = [
260
- "low",
261
- "medium",
262
- "high",
263
- "critical"
264
- ];
265
- PLAN_STATUSES = ["active", "completed", "archived"];
266
- VersionConflictError = class VersionConflictError extends Error {
267
- taskId;
268
- expectedVersion;
269
- actualVersion;
270
- static code = "VERSION_CONFLICT";
271
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
272
- constructor(taskId, expectedVersion, actualVersion) {
273
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
274
- this.taskId = taskId;
275
- this.expectedVersion = expectedVersion;
276
- this.actualVersion = actualVersion;
277
- this.name = "VersionConflictError";
278
- }
279
- };
280
- TaskNotFoundError = class TaskNotFoundError extends Error {
281
- taskId;
282
- static code = "TASK_NOT_FOUND";
283
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
284
- constructor(taskId) {
285
- super(`Task not found: ${taskId}`);
286
- this.taskId = taskId;
287
- this.name = "TaskNotFoundError";
288
- }
289
- };
290
- ProjectNotFoundError = class ProjectNotFoundError extends Error {
291
- projectId;
292
- static code = "PROJECT_NOT_FOUND";
293
- static suggestion = "Use list_projects to see available projects.";
294
- constructor(projectId) {
295
- super(`Project not found: ${projectId}`);
296
- this.projectId = projectId;
297
- this.name = "ProjectNotFoundError";
298
- }
299
- };
300
- ResourceConflictError = class ResourceConflictError extends Error {
301
- code;
302
- constructor(code, message) {
303
- super(message);
304
- this.code = code;
305
- this.name = "ResourceConflictError";
306
- }
307
- };
308
- PlanNotFoundError = class PlanNotFoundError extends Error {
309
- planId;
310
- static code = "PLAN_NOT_FOUND";
311
- static suggestion = "Use list_plans to see available plans.";
312
- constructor(planId) {
313
- super(`Plan not found: ${planId}`);
314
- this.planId = planId;
315
- this.name = "PlanNotFoundError";
316
- }
317
- };
318
- LockError = class LockError extends Error {
319
- taskId;
320
- lockedBy;
321
- static code = "LOCK_ERROR";
322
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
323
- constructor(taskId, lockedBy) {
324
- super(`Task ${taskId} is locked by ${lockedBy}`);
325
- this.taskId = taskId;
326
- this.lockedBy = lockedBy;
327
- this.name = "LockError";
328
- }
329
- };
330
- AgentNotFoundError = class AgentNotFoundError extends Error {
331
- agentId;
332
- static code = "AGENT_NOT_FOUND";
333
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
334
- constructor(agentId) {
335
- super(`Agent not found: ${agentId}`);
336
- this.agentId = agentId;
337
- this.name = "AgentNotFoundError";
338
- }
339
- };
340
- TaskListNotFoundError = class TaskListNotFoundError extends Error {
341
- taskListId;
342
- static code = "TASK_LIST_NOT_FOUND";
343
- static suggestion = "Use list_task_lists to see available lists.";
344
- constructor(taskListId) {
345
- super(`Task list not found: ${taskListId}`);
346
- this.taskListId = taskListId;
347
- this.name = "TaskListNotFoundError";
348
- }
349
- };
350
- DependencyCycleError = class DependencyCycleError extends Error {
351
- taskId;
352
- dependsOn;
353
- static code = "DEPENDENCY_CYCLE";
354
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
355
- constructor(taskId, dependsOn) {
356
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
357
- this.taskId = taskId;
358
- this.dependsOn = dependsOn;
359
- this.name = "DependencyCycleError";
360
- }
361
- };
362
- CompletionGuardError = class CompletionGuardError extends Error {
363
- reason;
364
- retryAfterSeconds;
365
- static code = "COMPLETION_BLOCKED";
366
- static suggestion = "Wait for the cooldown period, then retry.";
367
- constructor(reason, retryAfterSeconds) {
368
- super(reason);
369
- this.reason = reason;
370
- this.retryAfterSeconds = retryAfterSeconds;
371
- this.name = "CompletionGuardError";
372
- }
373
- };
374
- DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
375
- DispatchNotFoundError = class DispatchNotFoundError extends Error {
376
- dispatchId;
377
- static code = "DISPATCH_NOT_FOUND";
378
- static suggestion = "Check the dispatch ID with list_dispatches.";
379
- constructor(dispatchId) {
380
- super(`Dispatch not found: ${dispatchId}`);
381
- this.dispatchId = dispatchId;
382
- this.name = "DispatchNotFoundError";
383
- }
384
- };
385
- });
386
-
387
249
  // src/db/migrations.ts
388
250
  var MIGRATIONS;
389
251
  var init_migrations = __esm(() => {
@@ -1548,27 +1410,478 @@ var init_migrations = __esm(() => {
1548
1410
  ALTER TABLE plans ADD COLUMN slug TEXT;
1549
1411
  CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
1550
1412
  INSERT OR IGNORE INTO _migrations (id) VALUES (64);
1413
+ `,
1414
+ `
1415
+ ALTER TABLE agents ADD COLUMN identity_id TEXT;
1416
+ ALTER TABLE agents ADD COLUMN project_id TEXT;
1417
+ ALTER TABLE agents ADD COLUMN runtime_instance_id TEXT;
1418
+
1419
+ CREATE TABLE IF NOT EXISTS agent_identity_source_mappings (
1420
+ id TEXT PRIMARY KEY,
1421
+ local_agent_id TEXT REFERENCES agents(id) ON DELETE RESTRICT,
1422
+ identity_id TEXT,
1423
+ source_authority TEXT NOT NULL CHECK(length(trim(source_authority)) > 0),
1424
+ source_tenant_id TEXT NOT NULL CHECK(length(trim(source_tenant_id)) > 0),
1425
+ source_namespace TEXT NOT NULL CHECK(length(trim(source_namespace)) > 0),
1426
+ source_entity_type TEXT NOT NULL CHECK(length(trim(source_entity_type)) > 0),
1427
+ source_record_id TEXT NOT NULL CHECK(length(trim(source_record_id)) > 0),
1428
+ observed_label TEXT,
1429
+ evidence TEXT NOT NULL DEFAULT '{}',
1430
+ mapping_basis TEXT NOT NULL CHECK(mapping_basis IN ('authoritative', 'imported', 'candidate', 'name_similarity')),
1431
+ status TEXT NOT NULL CHECK(status IN ('active', 'retired', 'quarantined', 'revoked')),
1432
+ revision INTEGER NOT NULL CHECK(revision > 0),
1433
+ created_at TEXT NOT NULL,
1434
+ updated_at TEXT NOT NULL,
1435
+ CHECK(mapping_basis IN ('authoritative', 'imported') OR status IN ('quarantined', 'revoked')),
1436
+ CHECK(status != 'active' OR (mapping_basis IN ('authoritative', 'imported') AND identity_id IS NOT NULL))
1437
+ );
1438
+ DROP INDEX IF EXISTS idx_agent_identity_source_active_unique;
1439
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_identity_source_revision_unique
1440
+ ON agent_identity_source_mappings (
1441
+ source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id, revision
1442
+ )
1443
+ WHERE mapping_basis IN ('authoritative', 'imported');
1444
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_identity
1445
+ ON agent_identity_source_mappings(identity_id);
1446
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_local
1447
+ ON agent_identity_source_mappings(local_agent_id);
1448
+ CREATE TABLE IF NOT EXISTS agent_identity_aliases (
1449
+ id TEXT PRIMARY KEY,
1450
+ local_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,
1451
+ label TEXT NOT NULL,
1452
+ normalized_label TEXT NOT NULL,
1453
+ alias_kind TEXT NOT NULL CHECK(alias_kind IN ('historical', 'candidate')),
1454
+ status TEXT NOT NULL CHECK(status IN ('active', 'quarantined', 'revoked')),
1455
+ created_at TEXT NOT NULL,
1456
+ UNIQUE(local_agent_id, normalized_label)
1457
+ );
1458
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_alias_lookup
1459
+ ON agent_identity_aliases(normalized_label, status);
1460
+
1461
+ CREATE TRIGGER IF NOT EXISTS trg_agents_identity_id_immutable
1462
+ BEFORE UPDATE OF identity_id ON agents
1463
+ WHEN OLD.identity_id IS NOT NULL AND OLD.identity_id IS NOT NEW.identity_id
1464
+ BEGIN
1465
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
1466
+ END;
1467
+
1468
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_identity_immutable
1469
+ BEFORE UPDATE OF identity_id ON agent_identity_source_mappings
1470
+ WHEN OLD.identity_id IS NOT NEW.identity_id
1471
+ BEGIN
1472
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
1473
+ END;
1474
+
1475
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_lineage_immutable
1476
+ BEFORE UPDATE OF source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id
1477
+ ON agent_identity_source_mappings
1478
+ WHEN OLD.source_authority IS NOT NEW.source_authority
1479
+ OR OLD.source_tenant_id IS NOT NEW.source_tenant_id
1480
+ OR OLD.source_namespace IS NOT NEW.source_namespace
1481
+ OR OLD.source_entity_type IS NOT NEW.source_entity_type
1482
+ OR OLD.source_record_id IS NOT NEW.source_record_id
1483
+ BEGIN
1484
+ SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
1485
+ END;
1486
+
1487
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
1488
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
1489
+ ON agent_identity_source_mappings
1490
+ BEGIN
1491
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
1492
+ END;
1493
+
1494
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
1495
+ BEFORE DELETE ON agent_identity_source_mappings
1496
+ BEGIN
1497
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
1498
+ END;
1499
+
1500
+ INSERT OR IGNORE INTO _migrations (id) VALUES (65);
1501
+ `,
1502
+ `
1503
+ CREATE TABLE IF NOT EXISTS pr_groups (
1504
+ schema_version INTEGER NOT NULL DEFAULT 1,
1505
+ id TEXT PRIMARY KEY,
1506
+ identity_key TEXT NOT NULL UNIQUE,
1507
+ root_request_id TEXT NOT NULL,
1508
+ repository TEXT NOT NULL,
1509
+ state TEXT NOT NULL,
1510
+ active_attempt_id TEXT,
1511
+ active_generation TEXT,
1512
+ terminal_attempt_id TEXT,
1513
+ terminal_generation TEXT,
1514
+ terminal_outcome TEXT,
1515
+ terminal_head_sha TEXT,
1516
+ terminal_at TEXT,
1517
+ cleanup_eligible_at TEXT,
1518
+ revision INTEGER NOT NULL DEFAULT 1,
1519
+ created_at TEXT NOT NULL,
1520
+ updated_at TEXT NOT NULL
1521
+ );
1522
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_groups_identity ON pr_groups(identity_key);
1523
+ CREATE INDEX IF NOT EXISTS idx_pr_groups_root_repository ON pr_groups(root_request_id, repository);
1524
+ CREATE INDEX IF NOT EXISTS idx_pr_groups_active_generation ON pr_groups(active_generation);
1525
+
1526
+ CREATE TABLE IF NOT EXISTS pr_group_attempts (
1527
+ schema_version INTEGER NOT NULL DEFAULT 1,
1528
+ id TEXT PRIMARY KEY,
1529
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1530
+ leaf_task_id TEXT NOT NULL,
1531
+ dispatch_attempt TEXT NOT NULL,
1532
+ writer_generation TEXT NOT NULL,
1533
+ previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
1534
+ worktree TEXT NOT NULL,
1535
+ branch TEXT NOT NULL,
1536
+ provider TEXT,
1537
+ provider_run_id TEXT,
1538
+ profile_alias TEXT,
1539
+ status TEXT NOT NULL,
1540
+ admitted_at TEXT NOT NULL,
1541
+ started_at TEXT,
1542
+ last_heartbeat_at TEXT,
1543
+ handed_off_at TEXT,
1544
+ fenced_at TEXT,
1545
+ terminal_at TEXT,
1546
+ created_at TEXT NOT NULL,
1547
+ updated_at TEXT NOT NULL,
1548
+ UNIQUE(group_id, leaf_task_id, dispatch_attempt),
1549
+ UNIQUE(group_id, writer_generation)
1550
+ );
1551
+ CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id);
1552
+ CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation);
1553
+
1554
+ CREATE TABLE IF NOT EXISTS pr_group_events (
1555
+ schema_version INTEGER NOT NULL DEFAULT 1,
1556
+ id TEXT PRIMARY KEY,
1557
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1558
+ attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
1559
+ writer_generation TEXT NOT NULL,
1560
+ sequence INTEGER NOT NULL,
1561
+ idempotency_key TEXT NOT NULL,
1562
+ event_type TEXT NOT NULL,
1563
+ state TEXT NOT NULL,
1564
+ message TEXT,
1565
+ head_sha TEXT,
1566
+ receipt_key TEXT,
1567
+ outcome TEXT,
1568
+ metadata TEXT NOT NULL DEFAULT '{}',
1569
+ payload_hash TEXT NOT NULL,
1570
+ created_at TEXT NOT NULL,
1571
+ UNIQUE(group_id, sequence),
1572
+ UNIQUE(group_id, idempotency_key),
1573
+ UNIQUE(group_id, receipt_key)
1574
+ );
1575
+ CREATE INDEX IF NOT EXISTS idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence);
1576
+ CREATE INDEX IF NOT EXISTS idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence);
1577
+ CREATE INDEX IF NOT EXISTS idx_pr_group_events_receipt ON pr_group_events(group_id, receipt_key);
1578
+ INSERT OR IGNORE INTO _migrations (id) VALUES (66);
1579
+ `,
1580
+ `
1581
+ ALTER TABLE pr_groups ADD COLUMN leaf_task_id TEXT;
1582
+ ALTER TABLE pr_groups ADD COLUMN branch TEXT;
1583
+ ALTER TABLE pr_groups ADD COLUMN pr_number INTEGER;
1584
+ ALTER TABLE pr_groups ADD COLUMN base_sha TEXT;
1585
+ ALTER TABLE pr_groups ADD COLUMN repair_cycle_count INTEGER NOT NULL DEFAULT 0;
1586
+ ALTER TABLE pr_groups ADD COLUMN repair_cycle_limit INTEGER NOT NULL DEFAULT 2;
1587
+ ALTER TABLE pr_group_attempts ADD COLUMN repository TEXT;
1588
+ ALTER TABLE pr_group_attempts ADD COLUMN pr_number INTEGER;
1589
+ ALTER TABLE pr_group_attempts ADD COLUMN base_sha TEXT;
1590
+ ALTER TABLE pr_group_events ADD COLUMN review_receipt_key TEXT;
1591
+ ALTER TABLE pr_group_events ADD COLUMN conditional_merge_receipt_key TEXT;
1592
+ ALTER TABLE pr_group_events ADD COLUMN repository TEXT;
1593
+ ALTER TABLE pr_group_events ADD COLUMN pr_number INTEGER;
1594
+ ALTER TABLE pr_group_events ADD COLUMN base_sha TEXT;
1595
+ ALTER TABLE pr_group_events ADD COLUMN actor_id TEXT;
1596
+ ALTER TABLE pr_group_events ADD COLUMN actor_run_id TEXT;
1597
+ ALTER TABLE pr_group_events ADD COLUMN expected_reviewer_id TEXT;
1598
+ ALTER TABLE pr_group_events ADD COLUMN expected_reviewer_run_id TEXT;
1599
+ ALTER TABLE pr_group_events ADD COLUMN repair_cycle INTEGER;
1600
+ ALTER TABLE pr_group_events ADD COLUMN cleanup_proof TEXT;
1601
+
1602
+ UPDATE pr_groups
1603
+ SET leaf_task_id = COALESCE(leaf_task_id, (
1604
+ SELECT attempt.leaf_task_id
1605
+ FROM pr_group_attempts AS attempt
1606
+ WHERE attempt.group_id = pr_groups.id
1607
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1608
+ attempt.created_at ASC, attempt.id ASC
1609
+ LIMIT 1
1610
+ )),
1611
+ branch = COALESCE(branch, (
1612
+ SELECT attempt.branch
1613
+ FROM pr_group_attempts AS attempt
1614
+ WHERE attempt.group_id = pr_groups.id
1615
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1616
+ attempt.created_at ASC, attempt.id ASC
1617
+ LIMIT 1
1618
+ ));
1619
+
1620
+ UPDATE pr_group_attempts
1621
+ SET repository = COALESCE(repository, (
1622
+ SELECT groups.repository FROM pr_groups AS groups
1623
+ WHERE groups.id = pr_group_attempts.group_id
1624
+ )),
1625
+ pr_number = COALESCE(pr_number, (
1626
+ SELECT groups.pr_number FROM pr_groups AS groups
1627
+ WHERE groups.id = pr_group_attempts.group_id
1628
+ )),
1629
+ base_sha = COALESCE(base_sha, (
1630
+ SELECT groups.base_sha FROM pr_groups AS groups
1631
+ WHERE groups.id = pr_group_attempts.group_id
1632
+ ));
1633
+
1634
+ UPDATE pr_groups
1635
+ SET pr_number = COALESCE(pr_number, (
1636
+ SELECT attempt.pr_number
1637
+ FROM pr_group_attempts AS attempt
1638
+ WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
1639
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1640
+ attempt.created_at ASC, attempt.id ASC
1641
+ LIMIT 1
1642
+ )),
1643
+ base_sha = COALESCE(base_sha, (
1644
+ SELECT attempt.base_sha
1645
+ FROM pr_group_attempts AS attempt
1646
+ WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
1647
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1648
+ attempt.created_at ASC, attempt.id ASC
1649
+ LIMIT 1
1650
+ ));
1651
+
1652
+ UPDATE pr_group_attempts
1653
+ SET pr_number = COALESCE(pr_number, (
1654
+ SELECT groups.pr_number FROM pr_groups AS groups
1655
+ WHERE groups.id = pr_group_attempts.group_id
1656
+ )),
1657
+ base_sha = COALESCE(base_sha, (
1658
+ SELECT groups.base_sha FROM pr_groups AS groups
1659
+ WHERE groups.id = pr_group_attempts.group_id
1660
+ ));
1661
+
1662
+ UPDATE pr_group_events
1663
+ SET repository = COALESCE(repository, (
1664
+ SELECT attempt.repository FROM pr_group_attempts AS attempt
1665
+ WHERE attempt.id = pr_group_events.attempt_id
1666
+ ), (
1667
+ SELECT groups.repository FROM pr_groups AS groups
1668
+ WHERE groups.id = pr_group_events.group_id
1669
+ )),
1670
+ pr_number = COALESCE(pr_number, (
1671
+ SELECT attempt.pr_number FROM pr_group_attempts AS attempt
1672
+ WHERE attempt.id = pr_group_events.attempt_id
1673
+ ), (
1674
+ SELECT groups.pr_number FROM pr_groups AS groups
1675
+ WHERE groups.id = pr_group_events.group_id
1676
+ )),
1677
+ base_sha = COALESCE(base_sha, (
1678
+ SELECT attempt.base_sha FROM pr_group_attempts AS attempt
1679
+ WHERE attempt.id = pr_group_events.attempt_id
1680
+ ), (
1681
+ SELECT groups.base_sha FROM pr_groups AS groups
1682
+ WHERE groups.id = pr_group_events.group_id
1683
+ ));
1684
+
1685
+ INSERT OR IGNORE INTO _migrations (id)
1686
+ SELECT 66
1687
+ WHERE NOT EXISTS (
1688
+ SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
1689
+ )
1690
+ AND NOT EXISTS (
1691
+ SELECT 1 FROM pr_group_attempts WHERE repository IS NULL
1692
+ )
1693
+ AND NOT EXISTS (
1694
+ SELECT 1 FROM pr_group_events WHERE repository IS NULL
1695
+ );
1696
+ `,
1697
+ `
1698
+ PRAGMA foreign_keys = OFF;
1699
+ BEGIN IMMEDIATE;
1700
+
1701
+ ALTER TABLE pr_group_events RENAME TO pr_group_events_v66;
1702
+ ALTER TABLE pr_group_attempts RENAME TO pr_group_attempts_v66;
1703
+ ALTER TABLE pr_groups RENAME TO pr_groups_v66;
1704
+
1705
+ CREATE TABLE pr_groups (
1706
+ schema_version INTEGER NOT NULL DEFAULT 1,
1707
+ id TEXT PRIMARY KEY,
1708
+ identity_key TEXT NOT NULL UNIQUE,
1709
+ root_request_id TEXT NOT NULL,
1710
+ repository TEXT NOT NULL,
1711
+ leaf_task_id TEXT NOT NULL,
1712
+ branch TEXT NOT NULL,
1713
+ pr_number INTEGER,
1714
+ base_sha TEXT,
1715
+ state TEXT NOT NULL,
1716
+ active_attempt_id TEXT,
1717
+ active_generation TEXT,
1718
+ repair_cycle_count INTEGER NOT NULL DEFAULT 0,
1719
+ repair_cycle_limit INTEGER NOT NULL DEFAULT 2,
1720
+ terminal_attempt_id TEXT,
1721
+ terminal_generation TEXT,
1722
+ terminal_outcome TEXT,
1723
+ terminal_head_sha TEXT,
1724
+ terminal_at TEXT,
1725
+ cleanup_eligible_at TEXT,
1726
+ revision INTEGER NOT NULL DEFAULT 1,
1727
+ created_at TEXT NOT NULL,
1728
+ updated_at TEXT NOT NULL
1729
+ );
1730
+
1731
+ CREATE TABLE pr_group_attempts (
1732
+ schema_version INTEGER NOT NULL DEFAULT 1,
1733
+ id TEXT PRIMARY KEY,
1734
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1735
+ leaf_task_id TEXT NOT NULL,
1736
+ dispatch_attempt TEXT NOT NULL,
1737
+ writer_generation TEXT NOT NULL,
1738
+ previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
1739
+ worktree TEXT NOT NULL,
1740
+ branch TEXT NOT NULL,
1741
+ repository TEXT NOT NULL,
1742
+ pr_number INTEGER,
1743
+ base_sha TEXT,
1744
+ provider TEXT,
1745
+ provider_run_id TEXT,
1746
+ profile_alias TEXT,
1747
+ status TEXT NOT NULL,
1748
+ admitted_at TEXT NOT NULL,
1749
+ started_at TEXT,
1750
+ last_heartbeat_at TEXT,
1751
+ handed_off_at TEXT,
1752
+ fenced_at TEXT,
1753
+ terminal_at TEXT,
1754
+ created_at TEXT NOT NULL,
1755
+ updated_at TEXT NOT NULL,
1756
+ UNIQUE(group_id, leaf_task_id, dispatch_attempt),
1757
+ UNIQUE(group_id, writer_generation)
1758
+ );
1759
+
1760
+ CREATE TABLE pr_group_events (
1761
+ schema_version INTEGER NOT NULL DEFAULT 1,
1762
+ id TEXT PRIMARY KEY,
1763
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1764
+ attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
1765
+ writer_generation TEXT NOT NULL,
1766
+ sequence INTEGER NOT NULL,
1767
+ idempotency_key TEXT NOT NULL,
1768
+ event_type TEXT NOT NULL,
1769
+ state TEXT NOT NULL,
1770
+ message TEXT,
1771
+ head_sha TEXT,
1772
+ receipt_key TEXT UNIQUE,
1773
+ review_receipt_key TEXT,
1774
+ conditional_merge_receipt_key TEXT,
1775
+ outcome TEXT,
1776
+ repository TEXT NOT NULL,
1777
+ pr_number INTEGER,
1778
+ base_sha TEXT,
1779
+ actor_id TEXT,
1780
+ actor_run_id TEXT,
1781
+ expected_reviewer_id TEXT,
1782
+ expected_reviewer_run_id TEXT,
1783
+ repair_cycle INTEGER,
1784
+ ci_proof TEXT,
1785
+ cleanup_proof TEXT,
1786
+ metadata TEXT NOT NULL DEFAULT '{}',
1787
+ payload_hash TEXT NOT NULL,
1788
+ created_at TEXT NOT NULL,
1789
+ UNIQUE(group_id, sequence),
1790
+ UNIQUE(group_id, idempotency_key)
1791
+ );
1792
+
1793
+ INSERT INTO pr_groups (
1794
+ schema_version,id,identity_key,root_request_id,repository,leaf_task_id,branch,
1795
+ pr_number,base_sha,state,active_attempt_id,active_generation,
1796
+ repair_cycle_count,repair_cycle_limit,terminal_attempt_id,terminal_generation,
1797
+ terminal_outcome,terminal_head_sha,terminal_at,cleanup_eligible_at,
1798
+ revision,created_at,updated_at
1799
+ )
1800
+ SELECT
1801
+ schema_version,id,identity_key,root_request_id,repository,leaf_task_id,branch,
1802
+ pr_number,base_sha,state,active_attempt_id,active_generation,
1803
+ repair_cycle_count,repair_cycle_limit,terminal_attempt_id,terminal_generation,
1804
+ terminal_outcome,terminal_head_sha,terminal_at,cleanup_eligible_at,
1805
+ revision,created_at,updated_at
1806
+ FROM pr_groups_v66;
1807
+
1808
+ INSERT INTO pr_group_attempts (
1809
+ schema_version,id,group_id,leaf_task_id,dispatch_attempt,writer_generation,
1810
+ previous_attempt_id,worktree,branch,repository,pr_number,base_sha,
1811
+ provider,provider_run_id,profile_alias,status,admitted_at,started_at,
1812
+ last_heartbeat_at,handed_off_at,fenced_at,terminal_at,created_at,updated_at
1813
+ )
1814
+ SELECT
1815
+ schema_version,id,group_id,leaf_task_id,dispatch_attempt,writer_generation,
1816
+ previous_attempt_id,worktree,branch,repository,pr_number,base_sha,
1817
+ provider,provider_run_id,profile_alias,status,admitted_at,started_at,
1818
+ last_heartbeat_at,handed_off_at,fenced_at,terminal_at,created_at,updated_at
1819
+ FROM pr_group_attempts_v66;
1820
+
1821
+ INSERT INTO pr_group_events (
1822
+ schema_version,id,group_id,attempt_id,writer_generation,sequence,
1823
+ idempotency_key,event_type,state,message,head_sha,receipt_key,
1824
+ review_receipt_key,conditional_merge_receipt_key,outcome,repository,
1825
+ pr_number,base_sha,actor_id,actor_run_id,expected_reviewer_id,
1826
+ expected_reviewer_run_id,repair_cycle,ci_proof,cleanup_proof,
1827
+ metadata,payload_hash,created_at
1828
+ )
1829
+ SELECT
1830
+ schema_version,id,group_id,attempt_id,writer_generation,sequence,
1831
+ idempotency_key,event_type,state,message,head_sha,receipt_key,
1832
+ review_receipt_key,conditional_merge_receipt_key,outcome,repository,
1833
+ pr_number,base_sha,actor_id,actor_run_id,expected_reviewer_id,
1834
+ expected_reviewer_run_id,repair_cycle,NULL,cleanup_proof,
1835
+ metadata,payload_hash,created_at
1836
+ FROM pr_group_events_v66;
1837
+
1838
+ DROP TABLE pr_group_events_v66;
1839
+ DROP TABLE pr_group_attempts_v66;
1840
+ DROP TABLE pr_groups_v66;
1841
+
1842
+ CREATE UNIQUE INDEX idx_pr_groups_identity ON pr_groups(identity_key);
1843
+ CREATE INDEX idx_pr_groups_root_repository ON pr_groups(root_request_id, repository);
1844
+ CREATE INDEX idx_pr_groups_active_generation ON pr_groups(active_generation);
1845
+ CREATE INDEX idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id);
1846
+ CREATE INDEX idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation);
1847
+ CREATE INDEX idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence);
1848
+ CREATE INDEX idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence);
1849
+ CREATE INDEX idx_pr_group_events_receipt ON pr_group_events(receipt_key);
1850
+
1851
+ INSERT OR IGNORE INTO _migrations (id) VALUES (68);
1852
+ COMMIT;
1853
+ PRAGMA foreign_keys = ON;
1551
1854
  `
1552
1855
  ];
1553
1856
  });
1554
1857
 
1555
1858
  // src/db/schema.ts
1556
1859
  function runMigrations(db) {
1860
+ let strictMigrationFailure = null;
1861
+ const executeMigration = (migration, index) => {
1862
+ try {
1863
+ db.exec(migration);
1864
+ } catch (error) {
1865
+ try {
1866
+ db.exec("ROLLBACK");
1867
+ } catch {}
1868
+ if (index + 1 >= 68)
1869
+ strictMigrationFailure = error;
1870
+ }
1871
+ };
1557
1872
  try {
1558
1873
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
1559
1874
  const currentLevel = result?.max_id ?? 0;
1560
1875
  for (let i = currentLevel;i < MIGRATIONS.length; i++) {
1561
- try {
1562
- db.exec(MIGRATIONS[i]);
1563
- } catch {}
1876
+ executeMigration(MIGRATIONS[i], i);
1564
1877
  }
1565
1878
  } catch {
1566
- for (const migration of MIGRATIONS) {
1567
- try {
1568
- db.exec(migration);
1569
- } catch {}
1879
+ for (const [index, migration] of MIGRATIONS.entries()) {
1880
+ executeMigration(migration, index);
1570
1881
  }
1571
1882
  }
1883
+ if (strictMigrationFailure)
1884
+ throw strictMigrationFailure;
1572
1885
  ensureSchema(db);
1573
1886
  }
1574
1887
  function planSlugBase(value) {
@@ -2737,6 +3050,246 @@ function ensureSchema(db) {
2737
3050
  )`);
2738
3051
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
2739
3052
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
3053
+ ensureTable("pr_groups", `
3054
+ CREATE TABLE pr_groups (
3055
+ schema_version INTEGER NOT NULL DEFAULT 1,
3056
+ id TEXT PRIMARY KEY,
3057
+ identity_key TEXT NOT NULL UNIQUE,
3058
+ root_request_id TEXT NOT NULL,
3059
+ repository TEXT NOT NULL,
3060
+ leaf_task_id TEXT NOT NULL,
3061
+ branch TEXT NOT NULL,
3062
+ pr_number INTEGER,
3063
+ base_sha TEXT,
3064
+ state TEXT NOT NULL,
3065
+ active_attempt_id TEXT,
3066
+ active_generation TEXT,
3067
+ repair_cycle_count INTEGER NOT NULL DEFAULT 0,
3068
+ repair_cycle_limit INTEGER NOT NULL DEFAULT 2,
3069
+ terminal_attempt_id TEXT,
3070
+ terminal_generation TEXT,
3071
+ terminal_outcome TEXT,
3072
+ terminal_head_sha TEXT,
3073
+ terminal_at TEXT,
3074
+ cleanup_eligible_at TEXT,
3075
+ revision INTEGER NOT NULL DEFAULT 1,
3076
+ created_at TEXT NOT NULL,
3077
+ updated_at TEXT NOT NULL
3078
+ )`);
3079
+ ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_groups_identity ON pr_groups(identity_key)");
3080
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_groups_root_repository ON pr_groups(root_request_id, repository)");
3081
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_groups_active_generation ON pr_groups(active_generation)");
3082
+ ensureTable("pr_group_attempts", `
3083
+ CREATE TABLE pr_group_attempts (
3084
+ schema_version INTEGER NOT NULL DEFAULT 1,
3085
+ id TEXT PRIMARY KEY,
3086
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
3087
+ leaf_task_id TEXT NOT NULL,
3088
+ dispatch_attempt TEXT NOT NULL,
3089
+ writer_generation TEXT NOT NULL,
3090
+ previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
3091
+ worktree TEXT NOT NULL,
3092
+ branch TEXT NOT NULL,
3093
+ repository TEXT NOT NULL,
3094
+ pr_number INTEGER,
3095
+ base_sha TEXT,
3096
+ provider TEXT,
3097
+ provider_run_id TEXT,
3098
+ profile_alias TEXT,
3099
+ status TEXT NOT NULL,
3100
+ admitted_at TEXT NOT NULL,
3101
+ started_at TEXT,
3102
+ last_heartbeat_at TEXT,
3103
+ handed_off_at TEXT,
3104
+ fenced_at TEXT,
3105
+ terminal_at TEXT,
3106
+ created_at TEXT NOT NULL,
3107
+ updated_at TEXT NOT NULL,
3108
+ UNIQUE(group_id, leaf_task_id, dispatch_attempt),
3109
+ UNIQUE(group_id, writer_generation)
3110
+ )`);
3111
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id)");
3112
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation)");
3113
+ ensureTable("pr_group_events", `
3114
+ CREATE TABLE pr_group_events (
3115
+ schema_version INTEGER NOT NULL DEFAULT 1,
3116
+ id TEXT PRIMARY KEY,
3117
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
3118
+ attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
3119
+ writer_generation TEXT NOT NULL,
3120
+ sequence INTEGER NOT NULL,
3121
+ idempotency_key TEXT NOT NULL,
3122
+ event_type TEXT NOT NULL,
3123
+ state TEXT NOT NULL,
3124
+ message TEXT,
3125
+ head_sha TEXT,
3126
+ receipt_key TEXT UNIQUE,
3127
+ review_receipt_key TEXT,
3128
+ conditional_merge_receipt_key TEXT,
3129
+ outcome TEXT,
3130
+ repository TEXT NOT NULL,
3131
+ pr_number INTEGER,
3132
+ base_sha TEXT,
3133
+ actor_id TEXT,
3134
+ actor_run_id TEXT,
3135
+ expected_reviewer_id TEXT,
3136
+ expected_reviewer_run_id TEXT,
3137
+ repair_cycle INTEGER,
3138
+ ci_proof TEXT,
3139
+ cleanup_proof TEXT,
3140
+ metadata TEXT NOT NULL DEFAULT '{}',
3141
+ payload_hash TEXT NOT NULL,
3142
+ created_at TEXT NOT NULL,
3143
+ UNIQUE(group_id, sequence),
3144
+ UNIQUE(group_id, idempotency_key)
3145
+ )`);
3146
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence)");
3147
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence)");
3148
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_receipt ON pr_group_events(receipt_key)");
3149
+ ensureColumn("pr_groups", "leaf_task_id", "TEXT");
3150
+ ensureColumn("pr_groups", "branch", "TEXT");
3151
+ ensureColumn("pr_groups", "pr_number", "INTEGER");
3152
+ ensureColumn("pr_groups", "base_sha", "TEXT");
3153
+ ensureColumn("pr_groups", "repair_cycle_count", "INTEGER NOT NULL DEFAULT 0");
3154
+ ensureColumn("pr_groups", "repair_cycle_limit", "INTEGER NOT NULL DEFAULT 2");
3155
+ ensureColumn("pr_group_attempts", "repository", "TEXT");
3156
+ ensureColumn("pr_group_attempts", "pr_number", "INTEGER");
3157
+ ensureColumn("pr_group_attempts", "base_sha", "TEXT");
3158
+ ensureColumn("pr_group_events", "review_receipt_key", "TEXT");
3159
+ ensureColumn("pr_group_events", "conditional_merge_receipt_key", "TEXT");
3160
+ ensureColumn("pr_group_events", "repository", "TEXT");
3161
+ ensureColumn("pr_group_events", "pr_number", "INTEGER");
3162
+ ensureColumn("pr_group_events", "base_sha", "TEXT");
3163
+ ensureColumn("pr_group_events", "actor_id", "TEXT");
3164
+ ensureColumn("pr_group_events", "actor_run_id", "TEXT");
3165
+ ensureColumn("pr_group_events", "expected_reviewer_id", "TEXT");
3166
+ ensureColumn("pr_group_events", "expected_reviewer_run_id", "TEXT");
3167
+ ensureColumn("pr_group_events", "repair_cycle", "INTEGER");
3168
+ ensureColumn("pr_group_events", "ci_proof", "TEXT");
3169
+ ensureColumn("pr_group_events", "cleanup_proof", "TEXT");
3170
+ try {
3171
+ db.exec(`
3172
+ UPDATE pr_groups
3173
+ SET leaf_task_id = COALESCE(leaf_task_id, (
3174
+ SELECT attempt.leaf_task_id
3175
+ FROM pr_group_attempts AS attempt
3176
+ WHERE attempt.group_id = pr_groups.id
3177
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3178
+ attempt.created_at ASC, attempt.id ASC
3179
+ LIMIT 1
3180
+ )),
3181
+ branch = COALESCE(branch, (
3182
+ SELECT attempt.branch
3183
+ FROM pr_group_attempts AS attempt
3184
+ WHERE attempt.group_id = pr_groups.id
3185
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3186
+ attempt.created_at ASC, attempt.id ASC
3187
+ LIMIT 1
3188
+ ));
3189
+ UPDATE pr_group_attempts
3190
+ SET repository = COALESCE(repository, (
3191
+ SELECT groups.repository FROM pr_groups AS groups
3192
+ WHERE groups.id = pr_group_attempts.group_id
3193
+ )),
3194
+ pr_number = COALESCE(pr_number, (
3195
+ SELECT groups.pr_number FROM pr_groups AS groups
3196
+ WHERE groups.id = pr_group_attempts.group_id
3197
+ )),
3198
+ base_sha = COALESCE(base_sha, (
3199
+ SELECT groups.base_sha FROM pr_groups AS groups
3200
+ WHERE groups.id = pr_group_attempts.group_id
3201
+ ));
3202
+ UPDATE pr_groups
3203
+ SET pr_number = COALESCE(pr_number, (
3204
+ SELECT attempt.pr_number
3205
+ FROM pr_group_attempts AS attempt
3206
+ WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
3207
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3208
+ attempt.created_at ASC, attempt.id ASC
3209
+ LIMIT 1
3210
+ )),
3211
+ base_sha = COALESCE(base_sha, (
3212
+ SELECT attempt.base_sha
3213
+ FROM pr_group_attempts AS attempt
3214
+ WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
3215
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3216
+ attempt.created_at ASC, attempt.id ASC
3217
+ LIMIT 1
3218
+ ));
3219
+ UPDATE pr_group_attempts
3220
+ SET pr_number = COALESCE(pr_number, (
3221
+ SELECT groups.pr_number FROM pr_groups AS groups
3222
+ WHERE groups.id = pr_group_attempts.group_id
3223
+ )),
3224
+ base_sha = COALESCE(base_sha, (
3225
+ SELECT groups.base_sha FROM pr_groups AS groups
3226
+ WHERE groups.id = pr_group_attempts.group_id
3227
+ ));
3228
+ UPDATE pr_group_events
3229
+ SET repository = COALESCE(repository, (
3230
+ SELECT attempt.repository FROM pr_group_attempts AS attempt
3231
+ WHERE attempt.id = pr_group_events.attempt_id
3232
+ ), (
3233
+ SELECT groups.repository FROM pr_groups AS groups
3234
+ WHERE groups.id = pr_group_events.group_id
3235
+ )),
3236
+ pr_number = COALESCE(pr_number, (
3237
+ SELECT attempt.pr_number FROM pr_group_attempts AS attempt
3238
+ WHERE attempt.id = pr_group_events.attempt_id
3239
+ ), (
3240
+ SELECT groups.pr_number FROM pr_groups AS groups
3241
+ WHERE groups.id = pr_group_events.group_id
3242
+ )),
3243
+ base_sha = COALESCE(base_sha, (
3244
+ SELECT attempt.base_sha FROM pr_group_attempts AS attempt
3245
+ WHERE attempt.id = pr_group_events.attempt_id
3246
+ ), (
3247
+ SELECT groups.base_sha FROM pr_groups AS groups
3248
+ WHERE groups.id = pr_group_events.group_id
3249
+ ));
3250
+ INSERT OR IGNORE INTO _migrations (id)
3251
+ SELECT 66
3252
+ WHERE NOT EXISTS (
3253
+ SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
3254
+ )
3255
+ AND NOT EXISTS (
3256
+ SELECT 1 FROM pr_group_attempts WHERE repository IS NULL
3257
+ )
3258
+ AND NOT EXISTS (
3259
+ SELECT 1 FROM pr_group_events WHERE repository IS NULL
3260
+ );
3261
+ `);
3262
+ } catch {}
3263
+ const strictColumns = [
3264
+ ["pr_groups", "leaf_task_id"],
3265
+ ["pr_groups", "branch"],
3266
+ ["pr_group_attempts", "repository"],
3267
+ ["pr_group_events", "repository"]
3268
+ ];
3269
+ for (const [table, column] of strictColumns) {
3270
+ const definition = db.query(`PRAGMA table_info(${table})`).all();
3271
+ if (definition.find((entry) => entry.name === column)?.notnull !== 1) {
3272
+ throw new Error(`PR-group schema integrity failure: ${table}.${column} is not NOT NULL`);
3273
+ }
3274
+ }
3275
+ const eventColumns = db.query("PRAGMA table_info(pr_group_events)").all();
3276
+ if (!eventColumns.some((entry) => entry.name === "ci_proof")) {
3277
+ throw new Error("PR-group schema integrity failure: pr_group_events.ci_proof is missing");
3278
+ }
3279
+ const receiptIndexes = db.query("PRAGMA index_list(pr_group_events)").all();
3280
+ const hasGlobalReceiptUniqueness = receiptIndexes.some((index) => {
3281
+ if (index.unique !== 1)
3282
+ return false;
3283
+ const columns = db.query(`PRAGMA index_info(${JSON.stringify(index.name)})`).all();
3284
+ return columns.length === 1 && columns[0]?.name === "receipt_key";
3285
+ });
3286
+ if (!hasGlobalReceiptUniqueness) {
3287
+ throw new Error("PR-group schema integrity failure: receipt_key is not globally unique");
3288
+ }
3289
+ const strictMigration = db.query("SELECT 1 AS ok FROM _migrations WHERE id = 68").get();
3290
+ if (strictMigration?.ok !== 1) {
3291
+ throw new Error("PR-group schema integrity failure: migration 68 is not recorded");
3292
+ }
2740
3293
  }
2741
3294
  function backfillTaskTags(db) {
2742
3295
  try {
@@ -3077,6 +3630,334 @@ var init_machines = __esm(() => {
3077
3630
  ];
3078
3631
  });
3079
3632
 
3633
+ // src/types/index.ts
3634
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3635
+ var init_types = __esm(() => {
3636
+ TASK_STATUSES = [
3637
+ "pending",
3638
+ "in_progress",
3639
+ "completed",
3640
+ "failed",
3641
+ "cancelled"
3642
+ ];
3643
+ TASK_PRIORITIES = [
3644
+ "low",
3645
+ "medium",
3646
+ "high",
3647
+ "critical"
3648
+ ];
3649
+ PLAN_STATUSES = ["active", "completed", "archived"];
3650
+ VersionConflictError = class VersionConflictError extends Error {
3651
+ taskId;
3652
+ expectedVersion;
3653
+ actualVersion;
3654
+ static code = "VERSION_CONFLICT";
3655
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
3656
+ constructor(taskId, expectedVersion, actualVersion) {
3657
+ super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
3658
+ this.taskId = taskId;
3659
+ this.expectedVersion = expectedVersion;
3660
+ this.actualVersion = actualVersion;
3661
+ this.name = "VersionConflictError";
3662
+ }
3663
+ };
3664
+ TaskNotFoundError = class TaskNotFoundError extends Error {
3665
+ taskId;
3666
+ static code = "TASK_NOT_FOUND";
3667
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
3668
+ constructor(taskId) {
3669
+ super(`Task not found: ${taskId}`);
3670
+ this.taskId = taskId;
3671
+ this.name = "TaskNotFoundError";
3672
+ }
3673
+ };
3674
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
3675
+ projectId;
3676
+ static code = "PROJECT_NOT_FOUND";
3677
+ static suggestion = "Use list_projects to see available projects.";
3678
+ constructor(projectId) {
3679
+ super(`Project not found: ${projectId}`);
3680
+ this.projectId = projectId;
3681
+ this.name = "ProjectNotFoundError";
3682
+ }
3683
+ };
3684
+ ResourceConflictError = class ResourceConflictError extends Error {
3685
+ code;
3686
+ constructor(code, message) {
3687
+ super(message);
3688
+ this.code = code;
3689
+ this.name = "ResourceConflictError";
3690
+ }
3691
+ };
3692
+ PlanNotFoundError = class PlanNotFoundError extends Error {
3693
+ planId;
3694
+ static code = "PLAN_NOT_FOUND";
3695
+ static suggestion = "Use list_plans to see available plans.";
3696
+ constructor(planId) {
3697
+ super(`Plan not found: ${planId}`);
3698
+ this.planId = planId;
3699
+ this.name = "PlanNotFoundError";
3700
+ }
3701
+ };
3702
+ LockError = class LockError extends Error {
3703
+ taskId;
3704
+ lockedBy;
3705
+ static code = "LOCK_ERROR";
3706
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
3707
+ constructor(taskId, lockedBy) {
3708
+ super(`Task ${taskId} is locked by ${lockedBy}`);
3709
+ this.taskId = taskId;
3710
+ this.lockedBy = lockedBy;
3711
+ this.name = "LockError";
3712
+ }
3713
+ };
3714
+ AgentNotFoundError = class AgentNotFoundError extends Error {
3715
+ agentId;
3716
+ static code = "AGENT_NOT_FOUND";
3717
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
3718
+ constructor(agentId) {
3719
+ super(`Agent not found: ${agentId}`);
3720
+ this.agentId = agentId;
3721
+ this.name = "AgentNotFoundError";
3722
+ }
3723
+ };
3724
+ IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
3725
+ code = "IDENTITY_ALIAS_AMBIGUOUS";
3726
+ candidates;
3727
+ constructor(subject, candidates = []) {
3728
+ super(`Identity alias or source is ambiguous: ${subject}`);
3729
+ this.name = "IdentityAliasAmbiguousError";
3730
+ this.candidates = [...new Set(candidates)].sort();
3731
+ }
3732
+ };
3733
+ IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
3734
+ code = "IDENTITY_ID_IMMUTABLE";
3735
+ constructor(agentId) {
3736
+ super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
3737
+ this.name = "IdentityIdImmutableError";
3738
+ }
3739
+ };
3740
+ TaskListNotFoundError = class TaskListNotFoundError extends Error {
3741
+ taskListId;
3742
+ static code = "TASK_LIST_NOT_FOUND";
3743
+ static suggestion = "Use list_task_lists to see available lists.";
3744
+ constructor(taskListId) {
3745
+ super(`Task list not found: ${taskListId}`);
3746
+ this.taskListId = taskListId;
3747
+ this.name = "TaskListNotFoundError";
3748
+ }
3749
+ };
3750
+ DependencyCycleError = class DependencyCycleError extends Error {
3751
+ taskId;
3752
+ dependsOn;
3753
+ static code = "DEPENDENCY_CYCLE";
3754
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
3755
+ constructor(taskId, dependsOn) {
3756
+ super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
3757
+ this.taskId = taskId;
3758
+ this.dependsOn = dependsOn;
3759
+ this.name = "DependencyCycleError";
3760
+ }
3761
+ };
3762
+ CompletionGuardError = class CompletionGuardError extends Error {
3763
+ reason;
3764
+ retryAfterSeconds;
3765
+ static code = "COMPLETION_BLOCKED";
3766
+ static suggestion = "Wait for the cooldown period, then retry.";
3767
+ constructor(reason, retryAfterSeconds) {
3768
+ super(reason);
3769
+ this.reason = reason;
3770
+ this.retryAfterSeconds = retryAfterSeconds;
3771
+ this.name = "CompletionGuardError";
3772
+ }
3773
+ };
3774
+ DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
3775
+ DispatchNotFoundError = class DispatchNotFoundError extends Error {
3776
+ dispatchId;
3777
+ static code = "DISPATCH_NOT_FOUND";
3778
+ static suggestion = "Check the dispatch ID with list_dispatches.";
3779
+ constructor(dispatchId) {
3780
+ super(`Dispatch not found: ${dispatchId}`);
3781
+ this.dispatchId = dispatchId;
3782
+ this.name = "DispatchNotFoundError";
3783
+ }
3784
+ };
3785
+ });
3786
+
3787
+ // src/db/identity-mapping.ts
3788
+ function timestamp() {
3789
+ return new Date().toISOString();
3790
+ }
3791
+ function mappingId() {
3792
+ return crypto.randomUUID();
3793
+ }
3794
+ function normalizedAlias(label) {
3795
+ const normalized = label.trim().toLocaleLowerCase("en-US");
3796
+ if (!normalized)
3797
+ throw new Error("IDENTITY_ALIAS_INVALID: label is required");
3798
+ return normalized;
3799
+ }
3800
+ function ensureColumn(db, table, column, type) {
3801
+ const columns = db.query(`PRAGMA table_info(${table})`).all();
3802
+ if (!columns.some((item) => item.name === column)) {
3803
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
3804
+ }
3805
+ }
3806
+ function ensureAgentIdentitySchema(db) {
3807
+ const install = db.transaction(() => {
3808
+ ensureColumn(db, "agents", "identity_id", "TEXT");
3809
+ ensureColumn(db, "agents", "project_id", "TEXT");
3810
+ ensureColumn(db, "agents", "runtime_instance_id", "TEXT");
3811
+ db.exec(`
3812
+ CREATE TABLE IF NOT EXISTS agent_identity_source_mappings (
3813
+ id TEXT PRIMARY KEY,
3814
+ local_agent_id TEXT REFERENCES agents(id) ON DELETE RESTRICT,
3815
+ identity_id TEXT,
3816
+ source_authority TEXT NOT NULL CHECK(length(trim(source_authority)) > 0),
3817
+ source_tenant_id TEXT NOT NULL CHECK(length(trim(source_tenant_id)) > 0),
3818
+ source_namespace TEXT NOT NULL CHECK(length(trim(source_namespace)) > 0),
3819
+ source_entity_type TEXT NOT NULL CHECK(length(trim(source_entity_type)) > 0),
3820
+ source_record_id TEXT NOT NULL CHECK(length(trim(source_record_id)) > 0),
3821
+ observed_label TEXT,
3822
+ evidence TEXT NOT NULL DEFAULT '{}',
3823
+ mapping_basis TEXT NOT NULL CHECK(mapping_basis IN ('authoritative', 'imported', 'candidate', 'name_similarity')),
3824
+ status TEXT NOT NULL CHECK(status IN ('active', 'retired', 'quarantined', 'revoked')),
3825
+ revision INTEGER NOT NULL CHECK(revision > 0),
3826
+ created_at TEXT NOT NULL,
3827
+ updated_at TEXT NOT NULL,
3828
+ CHECK(mapping_basis IN ('authoritative', 'imported') OR status IN ('quarantined', 'revoked')),
3829
+ CHECK(status != 'active' OR (mapping_basis IN ('authoritative', 'imported') AND identity_id IS NOT NULL))
3830
+ );
3831
+ `);
3832
+ ensureColumn(db, "agent_identity_source_mappings", "revision", "INTEGER NOT NULL DEFAULT 1");
3833
+ ensureColumn(db, "agent_identity_source_mappings", "evidence", "TEXT NOT NULL DEFAULT '{}'");
3834
+ db.exec(`
3835
+ DROP INDEX IF EXISTS idx_agent_identity_source_active_unique;
3836
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_identity_source_revision_unique
3837
+ ON agent_identity_source_mappings (
3838
+ source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id, revision
3839
+ )
3840
+ WHERE mapping_basis IN ('authoritative', 'imported');
3841
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_identity
3842
+ ON agent_identity_source_mappings(identity_id);
3843
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_local
3844
+ ON agent_identity_source_mappings(local_agent_id);
3845
+ DROP INDEX IF EXISTS idx_agents_identity_unique;
3846
+
3847
+ CREATE TABLE IF NOT EXISTS agent_identity_aliases (
3848
+ id TEXT PRIMARY KEY,
3849
+ local_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,
3850
+ label TEXT NOT NULL,
3851
+ normalized_label TEXT NOT NULL,
3852
+ alias_kind TEXT NOT NULL CHECK(alias_kind IN ('historical', 'candidate')),
3853
+ status TEXT NOT NULL CHECK(status IN ('active', 'quarantined', 'revoked')),
3854
+ created_at TEXT NOT NULL,
3855
+ UNIQUE(local_agent_id, normalized_label)
3856
+ );
3857
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_alias_lookup
3858
+ ON agent_identity_aliases(normalized_label, status);
3859
+
3860
+ CREATE TRIGGER IF NOT EXISTS trg_agents_identity_id_immutable
3861
+ BEFORE UPDATE OF identity_id ON agents
3862
+ WHEN OLD.identity_id IS NOT NULL AND OLD.identity_id IS NOT NEW.identity_id
3863
+ BEGIN
3864
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
3865
+ END;
3866
+
3867
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_identity_immutable
3868
+ BEFORE UPDATE OF identity_id ON agent_identity_source_mappings
3869
+ WHEN OLD.identity_id IS NOT NEW.identity_id
3870
+ BEGIN
3871
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
3872
+ END;
3873
+
3874
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_lineage_immutable
3875
+ BEFORE UPDATE OF source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id
3876
+ ON agent_identity_source_mappings
3877
+ WHEN OLD.source_authority IS NOT NEW.source_authority
3878
+ OR OLD.source_tenant_id IS NOT NEW.source_tenant_id
3879
+ OR OLD.source_namespace IS NOT NEW.source_namespace
3880
+ OR OLD.source_entity_type IS NOT NEW.source_entity_type
3881
+ OR OLD.source_record_id IS NOT NEW.source_record_id
3882
+ BEGIN
3883
+ SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
3884
+ END;
3885
+
3886
+ DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
3887
+ CREATE TRIGGER trg_agent_identity_mapping_history_immutable
3888
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
3889
+ ON agent_identity_source_mappings
3890
+ BEGIN
3891
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
3892
+ END;
3893
+
3894
+ DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
3895
+ CREATE TRIGGER trg_agent_identity_mapping_history_append_only
3896
+ BEFORE DELETE ON agent_identity_source_mappings
3897
+ BEGIN
3898
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
3899
+ END;
3900
+ `);
3901
+ db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
3902
+ });
3903
+ install.immediate();
3904
+ }
3905
+ function storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db) {
3906
+ const normalized = normalizedAlias(label);
3907
+ const exists = db.query("SELECT id FROM agents WHERE id = ?").get(agentId);
3908
+ if (!exists)
3909
+ throw new Error(`Agent not found: ${agentId}`);
3910
+ const existing = db.query(`
3911
+ SELECT * FROM agent_identity_aliases
3912
+ WHERE local_agent_id = ? AND normalized_label = ?
3913
+ `).get(agentId, normalized);
3914
+ if (existing) {
3915
+ if (aliasKind === "historical" && (existing.alias_kind !== "historical" || existing.status !== "active")) {
3916
+ db.run("UPDATE agent_identity_aliases SET label = ?, alias_kind = 'historical', status = 'active' WHERE id = ?", [label.trim(), existing.id]);
3917
+ return db.query("SELECT * FROM agent_identity_aliases WHERE id = ?").get(existing.id);
3918
+ }
3919
+ return existing;
3920
+ }
3921
+ const id = mappingId();
3922
+ db.run(`
3923
+ INSERT INTO agent_identity_aliases (
3924
+ id, local_agent_id, label, normalized_label, alias_kind, status, created_at
3925
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
3926
+ `, [id, agentId, label.trim(), normalized, aliasKind, status, timestamp()]);
3927
+ return db.query("SELECT * FROM agent_identity_aliases WHERE id = ?").get(id);
3928
+ }
3929
+ function storeAgentAlias(agentId, label, aliasKind, status, db) {
3930
+ if (db.inTransaction) {
3931
+ return storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db);
3932
+ }
3933
+ const store = db.transaction(() => storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db));
3934
+ return store.immediate();
3935
+ }
3936
+ function recordAgentAlias(agentId, label, db) {
3937
+ return storeAgentAlias(agentId, label, "historical", "active", db);
3938
+ }
3939
+ var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
3940
+ var init_identity_mapping = __esm(() => {
3941
+ init_types();
3942
+ init_types();
3943
+ IDENTITY_PROJECTION_CONTRACT = Object.freeze({
3944
+ name: "hasna.todos.agent-identity-projection/v1",
3945
+ version: 1,
3946
+ foundation_repository: "hasna/identities",
3947
+ foundation_pull_request: 14,
3948
+ foundation_contract: "hasna.identities.agent-identity/v1",
3949
+ foundation_version: 1,
3950
+ foundation_commit: "14d83e3e45df5748944a871a07c4aacece893169",
3951
+ foundation_fixture_id: "hasna.identities.agent-identity/v1/conformance/1",
3952
+ foundation_fixture_source_path: "docs/fixtures/agent-identity-v1.conformance.json",
3953
+ foundation_fixture_local_path: "src/db/fixtures/agent-identity-v1.conformance.json",
3954
+ foundation_fixture_sha256: "sha256:2d22e72b7315d0b06b3c67c71674cfd8c2dff552727eea00efb325f52c9420af",
3955
+ foundation_default_read_preference: "canonical_first",
3956
+ consumer_rollout_default_read_preference: "legacy_first",
3957
+ lease_fence_authority: "external Runtime Coordination"
3958
+ });
3959
+ });
3960
+
3080
3961
  // src/storage/shadow-outbox-schema.ts
3081
3962
  var exports_shadow_outbox_schema = {};
3082
3963
  __export(exports_shadow_outbox_schema, {
@@ -3096,7 +3977,7 @@ function installShadowOutboxSchema(db) {
3096
3977
  status TEXT NOT NULL DEFAULT 'pending',
3097
3978
  revision INTEGER NOT NULL DEFAULT 0
3098
3979
  )`);
3099
- ensureColumn(db, "shadow_outbox", "revision", "INTEGER NOT NULL DEFAULT 0");
3980
+ ensureColumn2(db, "shadow_outbox", "revision", "INTEGER NOT NULL DEFAULT 0");
3100
3981
  db.run(`CREATE UNIQUE INDEX IF NOT EXISTS shadow_outbox_pending_uq
3101
3982
  ON shadow_outbox(object_type, object_id) WHERE status='pending'`);
3102
3983
  db.run(`CREATE INDEX IF NOT EXISTS shadow_outbox_ready_idx
@@ -3118,7 +3999,7 @@ function installShadowOutboxSchema(db) {
3118
3999
  }
3119
4000
  }
3120
4001
  }
3121
- function ensureColumn(db, table, column, definition) {
4002
+ function ensureColumn2(db, table, column, definition) {
3122
4003
  const rows = db.query(`PRAGMA table_info(${table})`).all();
3123
4004
  if (rows.some((row) => row.name === column))
3124
4005
  return;
@@ -3246,6 +4127,7 @@ function openDatabase(path) {
3246
4127
  db.run("PRAGMA busy_timeout = 5000");
3247
4128
  db.run("PRAGMA foreign_keys = ON");
3248
4129
  runMigrations(db);
4130
+ ensureAgentIdentitySchema(db);
3249
4131
  backfillTaskTags(db);
3250
4132
  backfillMachineId(db);
3251
4133
  maybeInstallShadowCapture(db);
@@ -3354,9 +4236,19 @@ function resolvePartialId(db, table, partialId) {
3354
4236
  return nameRow.id;
3355
4237
  }
3356
4238
  if (table === "agents") {
3357
- const nameRow = db.query("SELECT id FROM agents WHERE lower(name) = ?").get(partialId.toLowerCase());
3358
- if (nameRow)
3359
- return nameRow.id;
4239
+ const normalized = partialId.trim().toLowerCase();
4240
+ const matches = db.query(`
4241
+ SELECT id FROM agents WHERE lower(name) = ?
4242
+ UNION
4243
+ SELECT local_agent_id AS id FROM agent_identity_aliases
4244
+ WHERE normalized_label = ? AND status = 'active'
4245
+ ORDER BY id
4246
+ `).all(normalized, normalized);
4247
+ if (matches.length === 1)
4248
+ return matches[0].id;
4249
+ if (matches.length > 1) {
4250
+ throw new IdentityAliasAmbiguousError(partialId, matches.map((match) => match.id));
4251
+ }
3360
4252
  }
3361
4253
  return null;
3362
4254
  }
@@ -3364,6 +4256,8 @@ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
3364
4256
  var init_database = __esm(() => {
3365
4257
  init_schema();
3366
4258
  init_machines();
4259
+ init_identity_mapping();
4260
+ init_types();
3367
4261
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
3368
4262
  });
3369
4263
 
@@ -3706,7 +4600,7 @@ function createProject(input, db) {
3706
4600
  const d = db || getDatabase();
3707
4601
  return d.transaction(() => {
3708
4602
  const id = uuid();
3709
- const timestamp = now();
4603
+ const timestamp2 = now();
3710
4604
  const derivedSlug = slugify(input.name);
3711
4605
  const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
3712
4606
  if (!derivedSlug || !taskListId)
@@ -3718,7 +4612,7 @@ function createProject(input, db) {
3718
4612
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
3719
4613
  const machineId = currentStorageMachineId(d);
3720
4614
  d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
3721
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
4615
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
3722
4616
  return getProject(id, d);
3723
4617
  })();
3724
4618
  }
@@ -3831,7 +4725,7 @@ function rowToSource(row) {
3831
4725
  function addProjectSource(input, db) {
3832
4726
  const d = db || getDatabase();
3833
4727
  const id = uuid();
3834
- const timestamp = now();
4728
+ const timestamp2 = now();
3835
4729
  d.run(`INSERT INTO project_sources (id, project_id, type, name, uri, description, metadata, created_at, updated_at)
3836
4730
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3837
4731
  id,
@@ -3841,8 +4735,8 @@ function addProjectSource(input, db) {
3841
4735
  input.uri,
3842
4736
  input.description || null,
3843
4737
  JSON.stringify(input.metadata || {}),
3844
- timestamp,
3845
- timestamp
4738
+ timestamp2,
4739
+ timestamp2
3846
4740
  ]);
3847
4741
  return rowToSource(d.query("SELECT * FROM project_sources WHERE id = ?").get(id));
3848
4742
  }
@@ -4060,6 +4954,11 @@ function customPatterns() {
4060
4954
  function secretPatterns() {
4061
4955
  return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
4062
4956
  }
4957
+ function isRedactionPlaceholderMatch(match) {
4958
+ const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
4959
+ const trimmed = match.trim();
4960
+ return new RegExp(`^${placeholder}$`).test(trimmed) || new RegExp(`=\\s*['"]?${placeholder}['"]?$`).test(trimmed);
4961
+ }
4063
4962
  function isSecretKey(key) {
4064
4963
  if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
4065
4964
  return false;
@@ -4097,7 +4996,7 @@ function redactValue(value) {
4097
4996
  function listSecretFindings(value) {
4098
4997
  const findings = [];
4099
4998
  for (const pattern of secretPatterns()) {
4100
- const matches = value.match(cloneRegex(pattern.regex));
4999
+ const matches = value.match(cloneRegex(pattern.regex))?.filter((match) => !isRedactionPlaceholderMatch(match));
4101
5000
  if (matches?.length)
4102
5001
  findings.push({ pattern: pattern.name, count: matches.length });
4103
5002
  }
@@ -4128,6 +5027,9 @@ var init_redaction = __esm(() => {
4128
5027
  { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
4129
5028
  { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
4130
5029
  { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
5030
+ { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
5031
+ { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
5032
+ { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
4131
5033
  { name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
4132
5034
  { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
4133
5035
  ];
@@ -4202,7 +5104,7 @@ function upsertWorkspaceTrustProfile(input) {
4202
5104
  const preset = input.preset || existing?.preset || "standard";
4203
5105
  const presetChanged = Boolean(existing && input.preset && input.preset !== existing.preset);
4204
5106
  const base = presetChanged ? defaultProfile(root, preset) : existing || defaultProfile(root, preset);
4205
- const timestamp = new Date().toISOString();
5107
+ const timestamp2 = new Date().toISOString();
4206
5108
  const profile = {
4207
5109
  ...base,
4208
5110
  ...PRESET_DEFAULTS[preset],
@@ -4215,8 +5117,8 @@ function upsertWorkspaceTrustProfile(input) {
4215
5117
  write_scopes: unique2(input.write_scopes ?? base.write_scopes ?? PRESET_DEFAULTS[preset].write_scopes),
4216
5118
  env_redactions: unique2(input.env_redactions ?? base.env_redactions ?? PRESET_DEFAULTS[preset].env_redactions),
4217
5119
  require_prompt_for_unsafe: input.require_prompt_for_unsafe ?? base.require_prompt_for_unsafe ?? PRESET_DEFAULTS[preset].require_prompt_for_unsafe,
4218
- created_at: existing?.created_at || timestamp,
4219
- updated_at: timestamp
5120
+ created_at: existing?.created_at || timestamp2,
5121
+ updated_at: timestamp2
4220
5122
  };
4221
5123
  saveConfig({
4222
5124
  ...config,
@@ -4417,7 +5319,7 @@ function upsertRunnerSandboxProfile(input) {
4417
5319
  const existing = config.runner_sandboxes?.[input.name];
4418
5320
  const root = normalizePath2(input.root || existing?.root || process.cwd());
4419
5321
  const base = existing || defaultProfile2(input.name, root);
4420
- const timestamp = new Date().toISOString();
5322
+ const timestamp2 = new Date().toISOString();
4421
5323
  const profile = {
4422
5324
  ...base,
4423
5325
  name: input.name,
@@ -4431,8 +5333,8 @@ function upsertRunnerSandboxProfile(input) {
4431
5333
  network_policy: input.network_policy || base.network_policy,
4432
5334
  require_approval: input.require_approval ?? base.require_approval,
4433
5335
  audit_evidence: input.audit_evidence ?? base.audit_evidence,
4434
- created_at: existing?.created_at || timestamp,
4435
- updated_at: timestamp
5336
+ created_at: existing?.created_at || timestamp2,
5337
+ updated_at: timestamp2
4436
5338
  };
4437
5339
  saveConfig({
4438
5340
  ...config,
@@ -4549,7 +5451,7 @@ function normalizeHook(input, existing) {
4549
5451
  throw new Error("socket event hooks require socket_path");
4550
5452
  if (input.target === "script" && !input.command && !existing?.command)
4551
5453
  throw new Error("script event hooks require command");
4552
- const timestamp = new Date().toISOString();
5454
+ const timestamp2 = new Date().toISOString();
4553
5455
  return {
4554
5456
  ...existing,
4555
5457
  name: safeName(input.name),
@@ -4566,8 +5468,8 @@ function normalizeHook(input, existing) {
4566
5468
  attempts: clampAttempts(input.retry?.attempts ?? existing?.retry?.attempts ?? 1),
4567
5469
  backoff_ms: Math.max(0, input.retry?.backoff_ms ?? existing?.retry?.backoff_ms ?? 0)
4568
5470
  },
4569
- created_at: existing?.created_at || timestamp,
4570
- updated_at: timestamp
5471
+ created_at: existing?.created_at || timestamp2,
5472
+ updated_at: timestamp2
4571
5473
  };
4572
5474
  }
4573
5475
  function clampAttempts(value) {
@@ -4581,11 +5483,11 @@ function eventMatches(hook, eventType) {
4581
5483
  function canonicalEvent(input) {
4582
5484
  return JSON.stringify(input);
4583
5485
  }
4584
- function buildEnvelope(type, payload, timestamp = new Date().toISOString()) {
5486
+ function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
4585
5487
  const base = {
4586
5488
  id: randomUUID(),
4587
5489
  type,
4588
- timestamp,
5490
+ timestamp: timestamp2,
4589
5491
  payload: redactValue(payload ?? {}),
4590
5492
  source: { package: "@hasna/todos", local_only: true }
4591
5493
  };
@@ -4999,11 +5901,11 @@ class JsonEventsStore {
4999
5901
  });
5000
5902
  }
5001
5903
  }
5002
- function buildSignatureBase(timestamp, body) {
5003
- return `${timestamp}.${body}`;
5904
+ function buildSignatureBase(timestamp2, body) {
5905
+ return `${timestamp2}.${body}`;
5004
5906
  }
5005
- function signPayload(secret, timestamp, body) {
5006
- const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
5907
+ function signPayload(secret, timestamp2, body) {
5908
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp2, body)).digest("hex");
5007
5909
  return `sha256=${digest}`;
5008
5910
  }
5009
5911
  function now2() {
@@ -5016,17 +5918,17 @@ function buildWebhookRequest(event, channel) {
5016
5918
  if (!channel.webhook)
5017
5919
  throw new Error(`Channel ${channel.id} has no webhook config`);
5018
5920
  const body = JSON.stringify(event);
5019
- const timestamp = event.time;
5921
+ const timestamp2 = event.time;
5020
5922
  const headers = {
5021
5923
  "Content-Type": "application/json",
5022
5924
  "User-Agent": "@hasna/events",
5023
5925
  "X-Hasna-Event-Id": event.id,
5024
5926
  "X-Hasna-Event-Type": event.type,
5025
- "X-Hasna-Timestamp": timestamp,
5927
+ "X-Hasna-Timestamp": timestamp2,
5026
5928
  ...channel.webhook.headers
5027
5929
  };
5028
5930
  if (channel.webhook.secret) {
5029
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
5931
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
5030
5932
  }
5031
5933
  return { body, headers };
5032
5934
  }
@@ -5180,11 +6082,11 @@ class EventsClient {
5180
6082
  this.transportOptions = { fetchImpl: options.fetchImpl };
5181
6083
  }
5182
6084
  async addChannel(input) {
5183
- const timestamp = new Date().toISOString();
6085
+ const timestamp2 = new Date().toISOString();
5184
6086
  return this.store.addChannel({
5185
6087
  ...input,
5186
- createdAt: input.createdAt ?? timestamp,
5187
- updatedAt: input.updatedAt ?? timestamp
6088
+ createdAt: input.createdAt ?? timestamp2,
6089
+ updatedAt: input.updatedAt ?? timestamp2
5188
6090
  });
5189
6091
  }
5190
6092
  async listChannels() {
@@ -5256,12 +6158,12 @@ class EventsClient {
5256
6158
  const match = await this.matchChannel(id, input);
5257
6159
  const event = match.event;
5258
6160
  if (options.honorFilters && !match.matched) {
5259
- const timestamp = new Date().toISOString();
6161
+ const timestamp2 = new Date().toISOString();
5260
6162
  const result2 = createDeliveryResult(event, channel, [{
5261
6163
  attempt: 1,
5262
6164
  status: "skipped",
5263
- startedAt: timestamp,
5264
- completedAt: timestamp,
6165
+ startedAt: timestamp2,
6166
+ completedAt: timestamp2,
5265
6167
  error: match.reason
5266
6168
  }]);
5267
6169
  result2.metadata = { reason: "filter_mismatch" };
@@ -5382,7 +6284,7 @@ function createTaskList(input, db) {
5382
6284
  const d = db || getDatabase();
5383
6285
  return d.transaction(() => {
5384
6286
  const id = uuid();
5385
- const timestamp = now();
6287
+ const timestamp2 = now();
5386
6288
  const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
5387
6289
  if (!slug)
5388
6290
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
@@ -5393,7 +6295,7 @@ function createTaskList(input, db) {
5393
6295
  throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
5394
6296
  }
5395
6297
  d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
5396
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
6298
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp2, timestamp2, machineId]);
5397
6299
  return getTaskList(id, d);
5398
6300
  })();
5399
6301
  }
@@ -5866,6 +6768,110 @@ var init_secret_redaction = __esm(() => {
5866
6768
  customRedactors = [];
5867
6769
  });
5868
6770
 
6771
+ // src/lib/prewrite-secrets.ts
6772
+ function mergeFindings(findings) {
6773
+ const byPattern = new Map;
6774
+ for (const finding of findings) {
6775
+ const existing = byPattern.get(finding.pattern);
6776
+ if (!existing) {
6777
+ byPattern.set(finding.pattern, {
6778
+ pattern: finding.pattern,
6779
+ count: finding.count,
6780
+ lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
6781
+ });
6782
+ continue;
6783
+ }
6784
+ existing.count += finding.count;
6785
+ existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
6786
+ }
6787
+ return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
6788
+ }
6789
+ function redactPreWriteText(value) {
6790
+ return redactText(redactEvidenceText(value));
6791
+ }
6792
+ function scanPreWriteText(value, context = "text") {
6793
+ const redacted = redactPreWriteText(value);
6794
+ if (redacted === value) {
6795
+ return {
6796
+ schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
6797
+ clean: true,
6798
+ context,
6799
+ findings: []
6800
+ };
6801
+ }
6802
+ const structural = scanTextForSecrets(value).matches.map((match) => ({
6803
+ pattern: match.pattern,
6804
+ count: 1,
6805
+ lines: match.line ? [match.line] : []
6806
+ }));
6807
+ const configured = listSecretFindings(value).map((finding) => ({
6808
+ pattern: finding.pattern,
6809
+ count: finding.count,
6810
+ lines: []
6811
+ }));
6812
+ return {
6813
+ schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
6814
+ clean: false,
6815
+ context,
6816
+ findings: mergeFindings([...structural, ...configured])
6817
+ };
6818
+ }
6819
+ function assertPreWriteTextClean(value, context = "text") {
6820
+ const scan = scanPreWriteText(value, context);
6821
+ if (!scan.clean)
6822
+ throw new PreWriteSecretError(context, scan.findings);
6823
+ }
6824
+ function sanitizePreWriteText(value, context = "text", options = {}) {
6825
+ const mode = options.mode ?? "redact";
6826
+ if (mode === "block") {
6827
+ assertPreWriteTextClean(value, context);
6828
+ return value;
6829
+ }
6830
+ return redactPreWriteText(value);
6831
+ }
6832
+ function sanitizePreWriteValue(value, context = "value", options = {}) {
6833
+ if (typeof value === "string")
6834
+ return sanitizePreWriteText(value, context, options);
6835
+ if (Array.isArray(value)) {
6836
+ return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
6837
+ }
6838
+ if (value && typeof value === "object") {
6839
+ const sanitized = {};
6840
+ for (const [key, child] of Object.entries(value)) {
6841
+ const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
6842
+ sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
6843
+ }
6844
+ return redactValue(sanitized);
6845
+ }
6846
+ return value;
6847
+ }
6848
+ function sanitizePreWriteJsonString(value, context = "json") {
6849
+ if (value === undefined || value === null)
6850
+ return null;
6851
+ try {
6852
+ return JSON.stringify(sanitizePreWriteValue(JSON.parse(value), context));
6853
+ } catch {
6854
+ return sanitizePreWriteText(value, context);
6855
+ }
6856
+ }
6857
+ var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
6858
+ var init_prewrite_secrets = __esm(() => {
6859
+ init_redaction();
6860
+ init_secret_redaction();
6861
+ PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
6862
+ PreWriteSecretError = class PreWriteSecretError extends Error {
6863
+ context;
6864
+ findings;
6865
+ constructor(context, findings) {
6866
+ const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
6867
+ super(`Secret pattern detected before persistence in ${context}: ${summary}`);
6868
+ this.name = "PreWriteSecretError";
6869
+ this.context = context;
6870
+ this.findings = findings;
6871
+ }
6872
+ };
6873
+ });
6874
+
5869
6875
  // src/lib/activity-audit.ts
5870
6876
  var exports_activity_audit = {};
5871
6877
  __export(exports_activity_audit, {
@@ -5914,6 +6920,9 @@ function logActivity(input, db) {
5914
6920
  const id = uuid();
5915
6921
  const ts = input.created_at ?? now();
5916
6922
  const machineId = input.machine_id ?? getMachineId2();
6923
+ const oldValue = input.old_value == null ? null : sanitizePreWriteText(input.old_value, "activity.old_value");
6924
+ const newValue = input.new_value == null ? null : sanitizePreWriteText(input.new_value, "activity.new_value");
6925
+ const metadata = sanitizePreWriteValue(input.metadata ?? {}, "activity.metadata");
5917
6926
  d.run(`INSERT INTO activity_log (
5918
6927
  id, entity_type, entity_id, action, field, old_value, new_value,
5919
6928
  actor_id, session_id, machine_id, metadata, created_at
@@ -5923,12 +6932,12 @@ function logActivity(input, db) {
5923
6932
  input.entity_id,
5924
6933
  input.action,
5925
6934
  input.field ?? null,
5926
- input.old_value ?? null,
5927
- input.new_value ?? null,
6935
+ oldValue,
6936
+ newValue,
5928
6937
  input.actor_id ?? null,
5929
6938
  input.session_id ?? null,
5930
6939
  machineId,
5931
- JSON.stringify(input.metadata ?? {}),
6940
+ JSON.stringify(metadata),
5932
6941
  ts
5933
6942
  ]);
5934
6943
  return getActivityRecord(id, d);
@@ -6039,6 +7048,7 @@ var ACTIVITY_LOG_SCHEMA = "todos.activity_log.v1", ACTIVITY_ENTITY_TYPES;
6039
7048
  var init_activity_audit = __esm(() => {
6040
7049
  init_database();
6041
7050
  init_secret_redaction();
7051
+ init_prewrite_secrets();
6042
7052
  ACTIVITY_ENTITY_TYPES = [
6043
7053
  "task",
6044
7054
  "project",
@@ -6051,13 +7061,18 @@ var init_activity_audit = __esm(() => {
6051
7061
  });
6052
7062
 
6053
7063
  // src/db/audit.ts
7064
+ function sanitizeHistoryValue(value, context) {
7065
+ return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
7066
+ }
6054
7067
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
6055
7068
  const d = db || getDatabase();
6056
7069
  const id = uuid();
6057
- const timestamp = now();
7070
+ const timestamp2 = now();
6058
7071
  const machineId = currentStorageMachineId(d);
7072
+ const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
7073
+ const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
6059
7074
  d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
6060
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
7075
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
6061
7076
  try {
6062
7077
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
6063
7078
  logActivity2({
@@ -6065,12 +7080,12 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
6065
7080
  entity_id: taskId,
6066
7081
  action,
6067
7082
  field,
6068
- old_value: oldValue ?? null,
6069
- new_value: newValue ?? null,
7083
+ old_value: safeOldValue,
7084
+ new_value: safeNewValue,
6070
7085
  actor_id: agentId ?? undefined
6071
7086
  }, d);
6072
7087
  } catch {}
6073
- return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp, machine_id: machineId };
7088
+ return { id, task_id: taskId, action, field: field || null, old_value: safeOldValue, new_value: safeNewValue, agent_id: agentId || null, created_at: timestamp2, machine_id: machineId };
6074
7089
  }
6075
7090
  function getTaskHistory(taskId, db) {
6076
7091
  const d = db || getDatabase();
@@ -6109,6 +7124,7 @@ function getRecap(hours = 8, projectId, db) {
6109
7124
  var init_audit = __esm(() => {
6110
7125
  init_database();
6111
7126
  init_storage_tombstones();
7127
+ init_prewrite_secrets();
6112
7128
  });
6113
7129
 
6114
7130
  // src/db/webhooks.ts
@@ -6341,27 +7357,27 @@ function getChecklist(taskId, db) {
6341
7357
  function addChecklistItem(input, db) {
6342
7358
  const d = db || getDatabase();
6343
7359
  const id = uuid();
6344
- const timestamp = now();
7360
+ const timestamp2 = now();
6345
7361
  let position = input.position;
6346
7362
  if (position === undefined) {
6347
7363
  const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
6348
7364
  position = (maxRow?.max_pos ?? -1) + 1;
6349
7365
  }
6350
- d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text, timestamp, timestamp]);
7366
+ d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text, timestamp2, timestamp2]);
6351
7367
  return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
6352
7368
  }
6353
7369
  function checkChecklistItem(id, checked, db) {
6354
7370
  const d = db || getDatabase();
6355
- const timestamp = now();
6356
- const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
7371
+ const timestamp2 = now();
7372
+ const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp2, id]);
6357
7373
  if (result.changes === 0)
6358
7374
  return null;
6359
7375
  return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
6360
7376
  }
6361
7377
  function updateChecklistItemText(id, text, db) {
6362
7378
  const d = db || getDatabase();
6363
- const timestamp = now();
6364
- const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp, id]);
7379
+ const timestamp2 = now();
7380
+ const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp2, id]);
6365
7381
  if (result.changes === 0)
6366
7382
  return null;
6367
7383
  return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
@@ -6483,6 +7499,50 @@ var init_recurrence = __esm(() => {
6483
7499
  };
6484
7500
  });
6485
7501
 
7502
+ // src/lib/template-semantics.ts
7503
+ function resolveTemplateVariables(templateVars, provided) {
7504
+ const merged = { ...provided };
7505
+ for (const variable of templateVars) {
7506
+ if (merged[variable.name] === undefined && variable.default !== undefined) {
7507
+ merged[variable.name] = variable.default;
7508
+ }
7509
+ }
7510
+ const missing = templateVars.filter((variable) => variable.required && merged[variable.name] === undefined).map((variable) => variable.name);
7511
+ if (missing.length > 0) {
7512
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
7513
+ }
7514
+ return merged;
7515
+ }
7516
+ function substituteTemplateVariables(text, variables) {
7517
+ let result = text;
7518
+ for (const [key, value] of Object.entries(variables)) {
7519
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
7520
+ }
7521
+ return result;
7522
+ }
7523
+ function evaluateTemplateCondition(condition, variables) {
7524
+ if (!condition || condition.trim() === "")
7525
+ return true;
7526
+ const trimmed = condition.trim();
7527
+ const equal = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
7528
+ if (equal)
7529
+ return (variables[equal[1]] ?? "") === equal[2].trim();
7530
+ const unequal = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
7531
+ if (unequal)
7532
+ return (variables[unequal[1]] ?? "") !== unequal[2].trim();
7533
+ const falsy = trimmed.match(/^!\{([^}]+)\}$/);
7534
+ if (falsy) {
7535
+ const value = variables[falsy[1]];
7536
+ return !value || value === "" || value === "false";
7537
+ }
7538
+ const truthy = trimmed.match(/^\{([^}]+)\}$/);
7539
+ if (truthy) {
7540
+ const value = variables[truthy[1]];
7541
+ return !!value && value !== "" && value !== "false";
7542
+ }
7543
+ return true;
7544
+ }
7545
+
6486
7546
  // src/db/templates.ts
6487
7547
  function rowToTemplate(row) {
6488
7548
  return {
@@ -6558,6 +7618,14 @@ function deleteTemplate(id, db) {
6558
7618
  payload: template,
6559
7619
  version: template.version
6560
7620
  }, d);
7621
+ for (const task of getTemplateTasks(resolved, d)) {
7622
+ recordStorageTombstone({
7623
+ object_type: "template_tasks",
7624
+ object_id: task.id,
7625
+ payload: task,
7626
+ version: 1
7627
+ }, d);
7628
+ }
6561
7629
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
6562
7630
  }
6563
7631
  function updateTemplate(id, updates, db) {
@@ -6689,34 +7757,7 @@ function getTemplateTasks(templateId, db) {
6689
7757
  return rows.map(rowToTemplateTask);
6690
7758
  }
6691
7759
  function evaluateCondition(condition, variables) {
6692
- if (!condition || condition.trim() === "")
6693
- return true;
6694
- const trimmed = condition.trim();
6695
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
6696
- if (eqMatch) {
6697
- const varName = eqMatch[1];
6698
- const expected = eqMatch[2].trim();
6699
- return (variables[varName] ?? "") === expected;
6700
- }
6701
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
6702
- if (neqMatch) {
6703
- const varName = neqMatch[1];
6704
- const expected = neqMatch[2].trim();
6705
- return (variables[varName] ?? "") !== expected;
6706
- }
6707
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
6708
- if (falsyMatch) {
6709
- const varName = falsyMatch[1];
6710
- const val = variables[varName];
6711
- return !val || val === "" || val === "false";
6712
- }
6713
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
6714
- if (truthyMatch) {
6715
- const varName = truthyMatch[1];
6716
- const val = variables[varName];
6717
- return !!val && val !== "" && val !== "false";
6718
- }
6719
- return true;
7760
+ return evaluateTemplateCondition(condition, variables);
6720
7761
  }
6721
7762
  function exportTemplate(id, db) {
6722
7763
  const d = db || getDatabase();
@@ -6789,29 +7830,10 @@ function listTemplateVersions(id, db) {
6789
7830
  return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
6790
7831
  }
6791
7832
  function resolveVariables(templateVars, provided) {
6792
- const merged = { ...provided };
6793
- for (const v of templateVars) {
6794
- if (merged[v.name] === undefined && v.default !== undefined) {
6795
- merged[v.name] = v.default;
6796
- }
6797
- }
6798
- const missing = [];
6799
- for (const v of templateVars) {
6800
- if (v.required && merged[v.name] === undefined) {
6801
- missing.push(v.name);
6802
- }
6803
- }
6804
- if (missing.length > 0) {
6805
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
6806
- }
6807
- return merged;
7833
+ return resolveTemplateVariables(templateVars, provided);
6808
7834
  }
6809
7835
  function substituteVars(text, variables) {
6810
- let result = text;
6811
- for (const [key, val] of Object.entries(variables)) {
6812
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
6813
- }
6814
- return result;
7836
+ return substituteTemplateVariables(text, variables);
6815
7837
  }
6816
7838
  function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
6817
7839
  const d = db || getDatabase();
@@ -7133,9 +8155,9 @@ function startTask(id, agentId, db) {
7133
8155
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
7134
8156
  }
7135
8157
  const cutoff = lockExpiryCutoff();
7136
- const timestamp = now();
8158
+ const timestamp2 = now();
7137
8159
  const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
7138
- WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
8160
+ WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp2, timestamp2, timestamp2, id, agentId, cutoff]);
7139
8161
  if (result.changes === 0) {
7140
8162
  const current = getTask(id, d);
7141
8163
  if (!current)
@@ -7147,7 +8169,7 @@ function startTask(id, agentId, db) {
7147
8169
  throw new Error(`Task ${id} could not be started because it changed during claim`);
7148
8170
  }
7149
8171
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
7150
- const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
8172
+ const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp2, started_at: task.started_at || timestamp2, version: task.version + 1, updated_at: timestamp2 };
7151
8173
  const payload = taskEventData(startedTask, { agent_id: agentId });
7152
8174
  dispatchWebhook2("task.started", payload, d).catch(() => {});
7153
8175
  emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
@@ -7170,7 +8192,13 @@ function completeTask(id, agentId, db, options) {
7170
8192
  throw new LockError(id, task.locked_by);
7171
8193
  }
7172
8194
  checkCompletionGuard(task, agentId || null, d);
7173
- const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes, attachment_ids: options.attachment_ids } : undefined;
8195
+ const evidence = options ? sanitizePreWriteValue({
8196
+ files_changed: options.files_changed,
8197
+ test_results: options.test_results,
8198
+ commit_hash: options.commit_hash,
8199
+ notes: options.notes,
8200
+ attachment_ids: options.attachment_ids
8201
+ }, "task.completion_evidence") : undefined;
7174
8202
  const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
7175
8203
  const completionMeta = {};
7176
8204
  if (hasEvidence)
@@ -7179,21 +8207,21 @@ function completeTask(id, agentId, db, options) {
7179
8207
  completionMeta._completion = { confidence: options.confidence };
7180
8208
  }
7181
8209
  const hasMeta = Object.keys(completionMeta).length > 0;
7182
- const timestamp = options?.completed_at || now();
8210
+ const timestamp2 = options?.completed_at || now();
7183
8211
  const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
7184
8212
  const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
7185
8213
  const finalVersion = versionBeforeStatus + 1;
7186
8214
  const tx = d.transaction(() => {
7187
8215
  if (hasMeta) {
7188
- const meta2 = { ...task.metadata, ...completionMeta };
7189
- const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
8216
+ const meta2 = sanitizePreWriteValue({ ...task.metadata, ...completionMeta }, "task.metadata");
8217
+ const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp2, id, task.version]);
7190
8218
  if (metaResult.changes === 0) {
7191
8219
  const current = getTask(id, d);
7192
8220
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
7193
8221
  }
7194
8222
  }
7195
8223
  const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
7196
- WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
8224
+ WHERE id = ? AND version = ?`, [timestamp2, confidence, timestamp2, id, versionBeforeStatus]);
7197
8225
  if (statusResult.changes === 0) {
7198
8226
  const current = getTask(id, d);
7199
8227
  throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
@@ -7206,20 +8234,20 @@ function completeTask(id, agentId, db, options) {
7206
8234
  status: "completed",
7207
8235
  locked_by: null,
7208
8236
  locked_at: null,
7209
- completed_at: timestamp,
8237
+ completed_at: timestamp2,
7210
8238
  confidence,
7211
8239
  version: finalVersion,
7212
- updated_at: timestamp,
8240
+ updated_at: timestamp2,
7213
8241
  metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
7214
8242
  };
7215
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
8243
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp2 });
7216
8244
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
7217
8245
  emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
7218
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
8246
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp2 }, databasePath });
7219
8247
  let spawnedTask = null;
7220
8248
  if (task.recurrence_rule && !options?.skip_recurrence) {
7221
8249
  try {
7222
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
8250
+ spawnedTask = spawnNextRecurrence(task, d, timestamp2);
7223
8251
  } catch (e) {
7224
8252
  spawnedTask = null;
7225
8253
  console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
@@ -7269,7 +8297,7 @@ function completeTask(id, agentId, db, options) {
7269
8297
  emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
7270
8298
  }
7271
8299
  }
7272
- return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
8300
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp2, confidence, version: finalVersion, updated_at: timestamp2, metadata: meta };
7273
8301
  }
7274
8302
  function lockTask(id, agentId, db) {
7275
8303
  const d = db || getDatabase();
@@ -7283,15 +8311,15 @@ function lockTask(id, agentId, db) {
7283
8311
  };
7284
8312
  }
7285
8313
  if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
7286
- const timestamp2 = now();
7287
- d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
8314
+ const timestamp3 = now();
8315
+ d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp3, timestamp3, id, agentId]);
7288
8316
  logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
7289
- return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
8317
+ return { success: true, locked_by: agentId, locked_at: timestamp3, expires_at: lockExpiresAt(timestamp3) };
7290
8318
  }
7291
8319
  const cutoff = lockExpiryCutoff();
7292
- const timestamp = now();
8320
+ const timestamp2 = now();
7293
8321
  const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
7294
- WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
8322
+ WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp2, timestamp2, id, agentId, cutoff]);
7295
8323
  if (result.changes === 0) {
7296
8324
  const current = getTask(id, d);
7297
8325
  if (!current)
@@ -7316,7 +8344,7 @@ function lockTask(id, agentId, db) {
7316
8344
  };
7317
8345
  }
7318
8346
  logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
7319
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
8347
+ return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
7320
8348
  }
7321
8349
  function unlockTask(id, agentId, db) {
7322
8350
  const d = db || getDatabase();
@@ -7326,9 +8354,9 @@ function unlockTask(id, agentId, db) {
7326
8354
  if (agentId && task.locked_by && task.locked_by !== agentId) {
7327
8355
  throw new LockError(id, task.locked_by);
7328
8356
  }
7329
- const timestamp = now();
8357
+ const timestamp2 = now();
7330
8358
  d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
7331
- WHERE id = ?`, [timestamp, id]);
8359
+ WHERE id = ?`, [timestamp2, id]);
7332
8360
  return true;
7333
8361
  }
7334
8362
  function getTaskLockStatus(id, db) {
@@ -7410,7 +8438,7 @@ function getNextTask(agentId, filters, db) {
7410
8438
  }
7411
8439
  sql += `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at ASC LIMIT 1`;
7412
8440
  const row = d.query(sql).get(...params);
7413
- return row ? rowToTask(row) : null;
8441
+ return row ? rowToTask2(row) : null;
7414
8442
  }
7415
8443
  function getActiveWork(filters, db) {
7416
8444
  const d = db || getDatabase();
@@ -7445,7 +8473,7 @@ function getTasksChangedSince(since, filters, db) {
7445
8473
  }
7446
8474
  const where = conditions.join(" AND ");
7447
8475
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
7448
- return rows.map(rowToTask);
8476
+ return rows.map(rowToTask2);
7449
8477
  }
7450
8478
  function failTask(id, agentId, reason, options, db) {
7451
8479
  const d = db || getDatabase();
@@ -7453,20 +8481,23 @@ function failTask(id, agentId, reason, options, db) {
7453
8481
  const task = getTask(id, d);
7454
8482
  if (!task)
7455
8483
  throw new TaskNotFoundError(id);
8484
+ const safeReason = sanitizePreWriteText(reason || "Unknown failure", "task.failure.reason");
8485
+ const safeErrorCode = options?.error_code ? sanitizePreWriteText(options.error_code, "task.failure.error_code") : null;
7456
8486
  const meta = {
7457
8487
  ...task.metadata,
7458
8488
  _failure: {
7459
- reason: reason || "Unknown failure",
7460
- error_code: options?.error_code || null,
8489
+ reason: safeReason,
8490
+ error_code: safeErrorCode,
7461
8491
  failed_by: agentId || null,
7462
8492
  failed_at: now(),
7463
8493
  retry_requested: options?.retry || false
7464
8494
  }
7465
8495
  };
7466
- const timestamp = now();
8496
+ const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
8497
+ const timestamp2 = now();
7467
8498
  const failTx = d.transaction(() => {
7468
8499
  const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
7469
- WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
8500
+ WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
7470
8501
  if (res.changes === 0) {
7471
8502
  const current = getTask(id, d);
7472
8503
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -7478,22 +8509,22 @@ function failTask(id, agentId, reason, options, db) {
7478
8509
  status: "failed",
7479
8510
  locked_by: null,
7480
8511
  locked_at: null,
7481
- metadata: meta,
8512
+ metadata: safeMeta,
7482
8513
  version: task.version + 1,
7483
- updated_at: timestamp
8514
+ updated_at: timestamp2
7484
8515
  };
7485
8516
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
7486
- const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
8517
+ const failurePayload = taskEventData(failedTask, { reason: safeReason, error_code: safeErrorCode, agent_id: agentId });
7487
8518
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
7488
8519
  emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
7489
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
8520
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason: safeReason, error_code: safeErrorCode, agent_id: agentId }, severity: "warning", databasePath });
7490
8521
  let retryTask;
7491
8522
  if (options?.retry) {
7492
8523
  const retryCount = (task.retry_count || 0) + 1;
7493
8524
  const maxRetries = task.max_retries || 3;
7494
8525
  if (retryCount > maxRetries) {
7495
8526
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
7496
- JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
8527
+ JSON.stringify(sanitizePreWriteValue({ ...safeMeta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }, "task.retry_exhausted.metadata")),
7497
8528
  id
7498
8529
  ]);
7499
8530
  } else {
@@ -7512,7 +8543,7 @@ function failTask(id, agentId, reason, options, db) {
7512
8543
  plan_id: task.plan_id ?? undefined,
7513
8544
  assigned_to: task.assigned_to ?? undefined,
7514
8545
  tags: task.tags,
7515
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
8546
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: safeReason } },
7516
8547
  estimated_minutes: task.estimated_minutes ?? undefined,
7517
8548
  recurrence_rule: task.recurrence_rule ?? undefined,
7518
8549
  due_at: retryAfter
@@ -7542,7 +8573,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
7542
8573
  }
7543
8574
  const where = conditions.join(" AND ");
7544
8575
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
7545
- return rows.map(rowToTask);
8576
+ return rows.map(rowToTask2);
7546
8577
  }
7547
8578
  function stealTask(agentId, opts, db) {
7548
8579
  const d = db || getDatabase();
@@ -7554,15 +8585,15 @@ function stealTask(agentId, opts, db) {
7554
8585
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
7555
8586
  staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
7556
8587
  const target = staleTasks[0];
7557
- const timestamp = now();
8588
+ const timestamp2 = now();
7558
8589
  const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
7559
8590
  const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
7560
- WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
8591
+ WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp2, timestamp2, target.id, cutoff, cutoff]);
7561
8592
  if (result.changes === 0)
7562
8593
  return null;
7563
8594
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
7564
8595
  logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
7565
- const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
8596
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp2, updated_at: timestamp2, version: target.version + 1 };
7566
8597
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7567
8598
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7568
8599
  emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
@@ -7623,10 +8654,11 @@ var init_task_lifecycle = __esm(() => {
7623
8654
  init_templates();
7624
8655
  init_task_crud();
7625
8656
  init_task_graph();
8657
+ init_prewrite_secrets();
7626
8658
  });
7627
8659
 
7628
8660
  // src/db/task-crud.ts
7629
- function rowToTask(row) {
8661
+ function rowToTask2(row) {
7630
8662
  return {
7631
8663
  ...row,
7632
8664
  tags: JSON.parse(row.tags || "[]"),
@@ -7660,9 +8692,29 @@ function addMetadataConditions(metadata, conditions, params) {
7660
8692
  params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
7661
8693
  }
7662
8694
  }
8695
+ function sanitizeCreateTaskInput(input) {
8696
+ return {
8697
+ ...input,
8698
+ title: sanitizePreWriteText(input.title, "task.title"),
8699
+ description: input.description !== undefined ? sanitizePreWriteText(input.description, "task.description") : undefined,
8700
+ tags: input.tags !== undefined ? sanitizePreWriteValue(input.tags, "task.tags") : undefined,
8701
+ metadata: input.metadata !== undefined ? sanitizePreWriteValue(input.metadata, "task.metadata") : undefined,
8702
+ reason: input.reason !== undefined ? sanitizePreWriteText(input.reason, "task.reason") : undefined
8703
+ };
8704
+ }
8705
+ function sanitizeUpdateTaskInput(input) {
8706
+ return {
8707
+ ...input,
8708
+ title: input.title !== undefined ? sanitizePreWriteText(input.title, "task.title") : undefined,
8709
+ description: input.description !== undefined ? sanitizePreWriteText(input.description, "task.description") : undefined,
8710
+ tags: input.tags !== undefined ? sanitizePreWriteValue(input.tags, "task.tags") : undefined,
8711
+ metadata: input.metadata !== undefined ? sanitizePreWriteValue(input.metadata, "task.metadata") : undefined
8712
+ };
8713
+ }
7663
8714
  function createTask(input, db) {
8715
+ input = sanitizeCreateTaskInput(input);
7664
8716
  const d = db || getDatabase();
7665
- const timestamp = now();
8717
+ const timestamp2 = now();
7666
8718
  const tags = input.tags || [];
7667
8719
  const machineId = currentStorageMachineId(d);
7668
8720
  const assignedBy = input.assigned_by || input.agent_id;
@@ -7689,8 +8741,8 @@ function createTask(input, db) {
7689
8741
  input.working_dir || null,
7690
8742
  JSON.stringify(tags),
7691
8743
  JSON.stringify(input.metadata || {}),
7692
- timestamp,
7693
- timestamp,
8744
+ timestamp2,
8745
+ timestamp2,
7694
8746
  input.due_at || null,
7695
8747
  input.estimated_minutes || null,
7696
8748
  input.sla_minutes ?? null,
@@ -7736,7 +8788,7 @@ function getTask(id, db) {
7736
8788
  const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
7737
8789
  if (!row)
7738
8790
  return null;
7739
- return rowToTask(row);
8791
+ return rowToTask2(row);
7740
8792
  }
7741
8793
  function getTaskWithRelations(id, db) {
7742
8794
  const d = db || getDatabase();
@@ -7744,15 +8796,15 @@ function getTaskWithRelations(id, db) {
7744
8796
  if (!task)
7745
8797
  return null;
7746
8798
  const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
7747
- const subtasks = subtaskRows.map(rowToTask);
8799
+ const subtasks = subtaskRows.map(rowToTask2);
7748
8800
  const depRows = d.query(`SELECT t.* FROM tasks t
7749
8801
  JOIN task_dependencies td ON td.depends_on = t.id
7750
8802
  WHERE td.task_id = ?`).all(id);
7751
- const dependencies = depRows.map(rowToTask);
8803
+ const dependencies = depRows.map(rowToTask2);
7752
8804
  const blockedByRows = d.query(`SELECT t.* FROM tasks t
7753
8805
  JOIN task_dependencies td ON td.task_id = t.id
7754
8806
  WHERE td.depends_on = ?`).all(id);
7755
- const blocked_by = blockedByRows.map(rowToTask);
8807
+ const blocked_by = blockedByRows.map(rowToTask2);
7756
8808
  const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
7757
8809
  const parent = task.parent_id ? getTask(task.parent_id, d) : null;
7758
8810
  const checklist = getChecklist(id, d);
@@ -7868,7 +8920,7 @@ function listTasks(filter = {}, db) {
7868
8920
  }
7869
8921
  }
7870
8922
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
7871
- return rows.map(rowToTask);
8923
+ return rows.map(rowToTask2);
7872
8924
  }
7873
8925
  function getTaskByFingerprint(fingerprint, db) {
7874
8926
  const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
@@ -8014,10 +9066,11 @@ function updateTask(id, input, db) {
8014
9066
  if (task.version !== input.version) {
8015
9067
  throw new VersionConflictError(id, input.version, task.version);
8016
9068
  }
8017
- const timestamp = now();
8018
- const completionTimestamp = input.completed_at ?? timestamp;
9069
+ input = sanitizeUpdateTaskInput(input);
9070
+ const timestamp2 = now();
9071
+ const completionTimestamp = input.completed_at ?? timestamp2;
8019
9072
  const sets = ["version = version + 1", "updated_at = ?"];
8020
- const params = [timestamp];
9073
+ const params = [timestamp2];
8021
9074
  if (input.title !== undefined) {
8022
9075
  sets.push("title = ?");
8023
9076
  params.push(input.title);
@@ -8166,7 +9219,7 @@ function updateTask(id, input, db) {
8166
9219
  tags: input.tags ?? task.tags,
8167
9220
  metadata: input.metadata ?? task.metadata,
8168
9221
  version: task.version + 1,
8169
- updated_at: timestamp,
9222
+ updated_at: timestamp2,
8170
9223
  locked_by: completedNow ? null : task.locked_by,
8171
9224
  locked_at: completedNow ? null : task.locked_at,
8172
9225
  completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
@@ -8178,7 +9231,7 @@ function updateTask(id, input, db) {
8178
9231
  retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
8179
9232
  requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
8180
9233
  approved_by: input.approved_by ?? task.approved_by,
8181
- approved_at: input.approved_by ? timestamp : task.approved_at
9234
+ approved_at: input.approved_by ? timestamp2 : task.approved_at
8182
9235
  };
8183
9236
  const databasePath = databasePathFromDatabase(d);
8184
9237
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
@@ -8210,7 +9263,7 @@ function deleteTask(id, db) {
8210
9263
  recordStorageTombstone({
8211
9264
  object_type: "tasks",
8212
9265
  object_id: id,
8213
- payload: rowToTask(row),
9266
+ payload: rowToTask2(row),
8214
9267
  version: row.version
8215
9268
  }, d);
8216
9269
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
@@ -8227,6 +9280,7 @@ var init_task_crud = __esm(() => {
8227
9280
  init_webhooks();
8228
9281
  init_checklists();
8229
9282
  init_storage_tombstones();
9283
+ init_prewrite_secrets();
8230
9284
  });
8231
9285
 
8232
9286
  // src/db/task-status.ts
@@ -8358,10 +9412,10 @@ function redistributeStaleTasks(agentId, options, db) {
8358
9412
  const maxAge = options?.max_age_minutes ?? 60;
8359
9413
  const stale = getStaleTasks(maxAge, options?.project_id ? { project_id: options.project_id } : undefined, d);
8360
9414
  const limited = options?.limit ? stale.slice(0, options.limit) : stale;
8361
- const timestamp = now();
9415
+ const timestamp2 = now();
8362
9416
  const released = [];
8363
9417
  for (const t of limited) {
8364
- d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, status = 'pending', version = version + 1, updated_at = ? WHERE id = ?`, [timestamp, t.id]);
9418
+ d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, status = 'pending', version = version + 1, updated_at = ? WHERE id = ?`, [timestamp2, t.id]);
8365
9419
  released.push({ ...t, locked_by: null, locked_at: null, status: "pending" });
8366
9420
  }
8367
9421
  const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
@@ -8501,7 +9555,7 @@ function getOverdueTasks(projectId, db, at = new Date) {
8501
9555
  }
8502
9556
  query += ` ORDER BY due_at ASC`;
8503
9557
  const rows = d.query(query).all(...params);
8504
- return rows.map(rowToTask);
9558
+ return rows.map(rowToTask2);
8505
9559
  }
8506
9560
  function getEscalatedTasks(opts = {}, db, at = new Date) {
8507
9561
  const d = db || getDatabase();
@@ -8521,7 +9575,7 @@ function getEscalatedTasks(opts = {}, db, at = new Date) {
8521
9575
  params.push(opts.agent_id);
8522
9576
  }
8523
9577
  const rows = d.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")} ORDER BY due_at ASC, created_at ASC`).all(...params);
8524
- return rows.map(rowToTask).map((task) => {
9578
+ return rows.map(rowToTask2).map((task) => {
8525
9579
  const reasons = [];
8526
9580
  const breachedTimes = [];
8527
9581
  if (task.due_at) {
@@ -8833,7 +9887,7 @@ function resolveCreateSlug(input, projectId, db) {
8833
9887
  function createPlan(input, db) {
8834
9888
  const d = db || getDatabase();
8835
9889
  const id = uuid();
8836
- const timestamp = now();
9890
+ const timestamp2 = now();
8837
9891
  const projectId = input.project_id || null;
8838
9892
  const slug = resolveCreateSlug(input, projectId, d);
8839
9893
  const machineId = currentStorageMachineId(d);
@@ -8847,8 +9901,8 @@ function createPlan(input, db) {
8847
9901
  input.name,
8848
9902
  input.description || null,
8849
9903
  input.status || "active",
8850
- timestamp,
8851
- timestamp,
9904
+ timestamp2,
9905
+ timestamp2,
8852
9906
  machineId
8853
9907
  ]);
8854
9908
  return getPlan(id, d);
@@ -8985,7 +10039,7 @@ function maybeFilter(value) {
8985
10039
  function createTaskBoard(input, db) {
8986
10040
  const d = db || getDatabase();
8987
10041
  const id = uuid();
8988
- const timestamp = now();
10042
+ const timestamp2 = now();
8989
10043
  const scope = input.scope || "tasks";
8990
10044
  const lanes = normalizeLanes(scope, input.lanes);
8991
10045
  d.run(`INSERT INTO task_boards (id, name, scope, project_id, task_list_id, plan_id, agent_id, lanes, filters, created_at, updated_at)
@@ -8999,8 +10053,8 @@ function createTaskBoard(input, db) {
8999
10053
  input.agent_id || null,
9000
10054
  JSON.stringify(lanes),
9001
10055
  JSON.stringify(input.filters || {}),
9002
- timestamp,
9003
- timestamp
10056
+ timestamp2,
10057
+ timestamp2
9004
10058
  ]);
9005
10059
  return getTaskBoard(id, d);
9006
10060
  }
@@ -9401,14 +10455,14 @@ function storeArtifactContent(input) {
9401
10455
  return null;
9402
10456
  const sourceStat = statSync2(sourcePath);
9403
10457
  if (!sourceStat.isFile())
9404
- throw new Error(`Artifact path is not a file: ${input.path}`);
10458
+ throw new Error(`Artifact path is not a file: ${sanitizePreWriteText(input.path, "artifact.path")}`);
9405
10459
  const sourceBuffer = readFileSync3(sourcePath);
9406
10460
  const sourceSha = sha256(sourceBuffer);
9407
10461
  const textLike = isTextLike(sourceBuffer, input.path);
9408
10462
  let storedBuffer = sourceBuffer;
9409
10463
  let redactionStatus = textLike ? "clean" : "binary_or_unknown";
9410
10464
  if (textLike) {
9411
- const redactedText = redactEvidenceText(sourceBuffer.toString("utf8"));
10465
+ const redactedText = sanitizePreWriteText(sourceBuffer.toString("utf8"), "artifact.content");
9412
10466
  storedBuffer = Buffer.from(redactedText);
9413
10467
  if (!storedBuffer.equals(sourceBuffer))
9414
10468
  redactionStatus = "redacted";
@@ -9442,7 +10496,7 @@ function storeArtifactContent(input) {
9442
10496
  expires_at: retentionDays === null ? null : retentionExpiresAt(createdAt, retentionDays)
9443
10497
  },
9444
10498
  source: {
9445
- path: input.path,
10499
+ path: sanitizePreWriteText(input.path, "artifact.source_path"),
9446
10500
  size_bytes: sourceBuffer.length,
9447
10501
  sha256: sourceSha
9448
10502
  }
@@ -9622,6 +10676,7 @@ var DEFAULT_DELETED_RETENTION_DAYS = 30;
9622
10676
  var init_artifact_store = __esm(() => {
9623
10677
  init_database();
9624
10678
  init_redaction();
10679
+ init_prewrite_secrets();
9625
10680
  });
9626
10681
 
9627
10682
  // src/db/comments.ts
@@ -9631,17 +10686,17 @@ function addComment(input, db) {
9631
10686
  throw new TaskNotFoundError(input.task_id);
9632
10687
  }
9633
10688
  const id = uuid();
9634
- const timestamp = now();
10689
+ const timestamp2 = now();
9635
10690
  d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
9636
10691
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
9637
10692
  id,
9638
10693
  input.task_id,
9639
10694
  input.agent_id || null,
9640
10695
  input.session_id || null,
9641
- redactEvidenceText(input.content),
10696
+ sanitizePreWriteText(input.content, "comment.content"),
9642
10697
  input.type || "comment",
9643
10698
  input.progress_pct ?? null,
9644
- timestamp
10699
+ timestamp2
9645
10700
  ]);
9646
10701
  return getComment(id, d);
9647
10702
  }
@@ -9665,21 +10720,21 @@ var init_comments = __esm(() => {
9665
10720
  init_types();
9666
10721
  init_database();
9667
10722
  init_tasks();
9668
- init_redaction();
10723
+ init_prewrite_secrets();
9669
10724
  });
9670
10725
 
9671
10726
  // src/db/task-files.ts
9672
10727
  function addTaskFile(input, db) {
9673
10728
  const d = db || getDatabase();
9674
10729
  const id = uuid();
9675
- const timestamp = now();
10730
+ const timestamp2 = now();
9676
10731
  const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
9677
10732
  if (existing) {
9678
- d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
10733
+ d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp2, existing.id]);
9679
10734
  return getTaskFile(existing.id, d);
9680
10735
  }
9681
10736
  d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
9682
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
10737
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp2, timestamp2]);
9683
10738
  return getTaskFile(id, d);
9684
10739
  }
9685
10740
  function getTaskFile(id, db) {
@@ -9696,8 +10751,8 @@ function findTasksByFile(path, db) {
9696
10751
  }
9697
10752
  function updateTaskFileStatus(taskId, path, status, agentId, db) {
9698
10753
  const d = db || getDatabase();
9699
- const timestamp = now();
9700
- d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
10754
+ const timestamp2 = now();
10755
+ d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp2, taskId, path]);
9701
10756
  const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
9702
10757
  return row;
9703
10758
  }
@@ -9742,8 +10797,16 @@ function rowToVerification(row) {
9742
10797
  function linkTaskToCommit(input, db) {
9743
10798
  const d = db || getDatabase();
9744
10799
  const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
9745
- const ciJson = input.ci_snapshot ? JSON.stringify(input.ci_snapshot) : null;
9746
- const traceJson = input.traceability ? JSON.stringify(input.traceability) : null;
10800
+ const ciJson = input.ci_snapshot ? JSON.stringify(sanitizePreWriteValue(input.ci_snapshot, "commit.ci_snapshot")) : null;
10801
+ const traceJson = input.traceability ? JSON.stringify(sanitizePreWriteValue(input.traceability, "commit.traceability")) : null;
10802
+ const message = input.message ? sanitizePreWriteText(input.message, "commit.message") : null;
10803
+ const author = input.author ? sanitizePreWriteText(input.author, "commit.author") : null;
10804
+ const filesChanged = input.files_changed ? JSON.stringify(sanitizePreWriteValue(input.files_changed, "commit.files_changed")) : null;
10805
+ const branch = input.branch ? sanitizePreWriteText(input.branch, "commit.branch") : null;
10806
+ const prUrl = input.pr_url ? sanitizePreWriteText(input.pr_url, "commit.pr_url") : null;
10807
+ const prState = input.pr_state ? sanitizePreWriteText(input.pr_state, "commit.pr_state") : null;
10808
+ const releaseTag = input.release_tag ? sanitizePreWriteText(input.release_tag, "commit.release_tag") : null;
10809
+ const repoPath = input.repo_path ? sanitizePreWriteText(input.repo_path, "commit.repo_path") : null;
9747
10810
  if (existing) {
9748
10811
  d.run(`UPDATE task_commits SET
9749
10812
  message = COALESCE(?, message),
@@ -9759,17 +10822,17 @@ function linkTaskToCommit(input, db) {
9759
10822
  repo_path = COALESCE(?, repo_path),
9760
10823
  traceability = COALESCE(?, traceability)
9761
10824
  WHERE id = ?`, [
9762
- input.message ?? null,
9763
- input.author ?? null,
9764
- input.files_changed ? JSON.stringify(input.files_changed) : null,
10825
+ message,
10826
+ author,
10827
+ filesChanged,
9765
10828
  input.committed_at ?? null,
9766
- input.branch ?? null,
9767
- input.pr_url ?? null,
10829
+ branch,
10830
+ prUrl,
9768
10831
  input.pr_number ?? null,
9769
- input.pr_state ?? null,
10832
+ prState,
9770
10833
  ciJson,
9771
- input.release_tag ?? null,
9772
- input.repo_path ?? null,
10834
+ releaseTag,
10835
+ repoPath,
9773
10836
  traceJson,
9774
10837
  existing.id
9775
10838
  ]);
@@ -9783,17 +10846,17 @@ function linkTaskToCommit(input, db) {
9783
10846
  id,
9784
10847
  input.task_id,
9785
10848
  input.sha,
9786
- input.message ?? null,
9787
- input.author ?? null,
9788
- input.files_changed ? JSON.stringify(input.files_changed) : null,
10849
+ message,
10850
+ author,
10851
+ filesChanged,
9789
10852
  input.committed_at ?? null,
9790
- input.branch ?? null,
9791
- input.pr_url ?? null,
10853
+ branch,
10854
+ prUrl,
9792
10855
  input.pr_number ?? null,
9793
- input.pr_state ?? null,
10856
+ prState,
9794
10857
  ciJson,
9795
- input.release_tag ?? null,
9796
- input.repo_path ?? null,
10858
+ releaseTag,
10859
+ repoPath,
9797
10860
  traceJson ?? "{}",
9798
10861
  now()
9799
10862
  ]);
@@ -9822,13 +10885,16 @@ function addTaskVerification(input, db) {
9822
10885
  const d = db || getDatabase();
9823
10886
  const id = uuid();
9824
10887
  const runAt = input.run_at || now();
10888
+ const command = sanitizePreWriteText(input.command, "verification.command");
10889
+ const outputSummary = input.output_summary ? sanitizePreWriteText(input.output_summary, "verification.output_summary") : null;
10890
+ const artifactPath = input.artifact_path ? sanitizePreWriteText(input.artifact_path, "verification.artifact_path") : null;
9825
10891
  d.run("INSERT INTO task_verifications (id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
9826
10892
  id,
9827
10893
  input.task_id,
9828
- input.command,
10894
+ command,
9829
10895
  input.status || "unknown",
9830
- input.output_summary ?? null,
9831
- input.artifact_path ?? null,
10896
+ outputSummary,
10897
+ artifactPath,
9832
10898
  input.agent_id ?? null,
9833
10899
  runAt,
9834
10900
  now()
@@ -9858,6 +10924,7 @@ function getTaskTraceability(taskId, db) {
9858
10924
  }
9859
10925
  var init_task_commits = __esm(() => {
9860
10926
  init_database();
10927
+ init_prewrite_secrets();
9861
10928
  });
9862
10929
 
9863
10930
  // src/db/task-runs.ts
@@ -9968,7 +11035,7 @@ function startTaskRun(input, db) {
9968
11035
  if (!getTask(input.task_id, d))
9969
11036
  throw new TaskNotFoundError(input.task_id);
9970
11037
  const id = input.id ?? uuid();
9971
- const timestamp = input.started_at || now();
11038
+ const timestamp2 = input.started_at || now();
9972
11039
  if (input.claim && input.agent_id) {
9973
11040
  startTask(input.task_id, input.agent_id, d);
9974
11041
  }
@@ -9976,12 +11043,12 @@ function startTaskRun(input, db) {
9976
11043
  id,
9977
11044
  input.task_id,
9978
11045
  input.agent_id ?? null,
9979
- input.title ? redactEvidenceText(input.title) : null,
9980
- input.summary ? redactEvidenceText(input.summary) : null,
9981
- JSON.stringify(redactValue(input.metadata || {})),
9982
- timestamp,
9983
- timestamp,
9984
- timestamp
11046
+ input.title ? sanitizePreWriteText(input.title, "run.title") : null,
11047
+ input.summary ? sanitizePreWriteText(input.summary, "run.summary") : null,
11048
+ JSON.stringify(sanitizePreWriteValue(input.metadata || {}, "run.metadata")),
11049
+ timestamp2,
11050
+ timestamp2,
11051
+ timestamp2
9985
11052
  ]);
9986
11053
  addTaskRunEvent({
9987
11054
  run_id: id,
@@ -9989,7 +11056,7 @@ function startTaskRun(input, db) {
9989
11056
  message: input.summary || input.title || "run started",
9990
11057
  data: { title: input.title, claim: Boolean(input.claim) },
9991
11058
  agent_id: input.agent_id,
9992
- created_at: timestamp
11059
+ created_at: timestamp2
9993
11060
  }, d);
9994
11061
  if (input.claim && input.agent_id) {
9995
11062
  addTaskRunEvent({
@@ -9998,7 +11065,7 @@ function startTaskRun(input, db) {
9998
11065
  message: `claimed by ${input.agent_id}`,
9999
11066
  data: { agent_id: input.agent_id },
10000
11067
  agent_id: input.agent_id,
10001
- created_at: timestamp
11068
+ created_at: timestamp2
10002
11069
  }, d);
10003
11070
  }
10004
11071
  const run = getTaskRun(id, d);
@@ -10013,7 +11080,7 @@ function beginTaskRunTransaction(input, db) {
10013
11080
  const d = db || getDatabase();
10014
11081
  if (!getTask(input.task_id, d))
10015
11082
  throw new TaskNotFoundError(input.task_id);
10016
- const timestamp = input.started_at || now();
11083
+ const timestamp2 = input.started_at || now();
10017
11084
  const key = normalizeTransactionKey(input);
10018
11085
  const existing = findTaskRunByTransactionKey(key, input.task_id, d);
10019
11086
  const dryRun = !input.apply;
@@ -10022,7 +11089,7 @@ function beginTaskRunTransaction(input, db) {
10022
11089
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10023
11090
  local_only: true,
10024
11091
  dry_run: dryRun,
10025
- processed_at: timestamp,
11092
+ processed_at: timestamp2,
10026
11093
  action: "matched",
10027
11094
  key,
10028
11095
  run: summarizeTaskRun(existing),
@@ -10035,7 +11102,7 @@ function beginTaskRunTransaction(input, db) {
10035
11102
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10036
11103
  local_only: true,
10037
11104
  dry_run: true,
10038
- processed_at: timestamp,
11105
+ processed_at: timestamp2,
10039
11106
  action: "preview",
10040
11107
  key,
10041
11108
  run: null,
@@ -10043,14 +11110,14 @@ function beginTaskRunTransaction(input, db) {
10043
11110
  commands: loopRunCommands(null, key)
10044
11111
  };
10045
11112
  }
10046
- const metadata = redactValue({
11113
+ const metadata = sanitizePreWriteValue({
10047
11114
  ...input.metadata || {},
10048
11115
  loop_transaction: {
10049
11116
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10050
11117
  idempotency_key: key,
10051
11118
  loop_id: input.loop_id ?? null,
10052
11119
  loop_run_id: input.loop_run_id ?? null,
10053
- first_seen_at: timestamp
11120
+ first_seen_at: timestamp2
10054
11121
  },
10055
11122
  idempotency_key: key
10056
11123
  });
@@ -10064,8 +11131,8 @@ function beginTaskRunTransaction(input, db) {
10064
11131
  input.loop_id ?? null,
10065
11132
  input.loop_run_id ?? null,
10066
11133
  JSON.stringify(metadata),
10067
- timestamp,
10068
- timestamp
11134
+ timestamp2,
11135
+ timestamp2
10069
11136
  ]);
10070
11137
  const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
10071
11138
  if (!transaction)
@@ -10083,16 +11150,16 @@ function beginTaskRunTransaction(input, db) {
10083
11150
  summary: input.summary,
10084
11151
  metadata,
10085
11152
  claim: input.claim,
10086
- started_at: timestamp
11153
+ started_at: timestamp2
10087
11154
  }, d);
10088
- d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp, transaction.id]);
11155
+ d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp2, transaction.id]);
10089
11156
  return { run, action: "created" };
10090
11157
  })();
10091
11158
  return {
10092
11159
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10093
11160
  local_only: true,
10094
11161
  dry_run: false,
10095
- processed_at: timestamp,
11162
+ processed_at: timestamp2,
10096
11163
  action: created.action,
10097
11164
  key,
10098
11165
  run: summarizeTaskRun(created.run),
@@ -10107,19 +11174,19 @@ function addTaskRunEvent(input, db) {
10107
11174
  if (!run)
10108
11175
  throw new Error(`Run not found: ${input.run_id}`);
10109
11176
  const id = uuid();
10110
- const timestamp = input.created_at || now();
11177
+ const timestamp2 = input.created_at || now();
10111
11178
  d.run("INSERT INTO task_run_events (id, run_id, task_id, event_type, message, data, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
10112
11179
  id,
10113
11180
  run.id,
10114
11181
  run.task_id,
10115
11182
  input.event_type,
10116
- input.message ? redactEvidenceText(input.message) : null,
10117
- JSON.stringify(redactValue(input.data || {})),
11183
+ input.message ? sanitizePreWriteText(input.message, "run_event.message") : null,
11184
+ JSON.stringify(sanitizePreWriteValue(input.data || {}, "run_event.data")),
10118
11185
  input.agent_id ?? run.agent_id,
10119
- timestamp
11186
+ timestamp2
10120
11187
  ]);
10121
11188
  if (input.event_type === "comment" && input.message) {
10122
- addComment({ task_id: run.task_id, content: redactEvidenceText(input.message), type: "comment", agent_id: input.agent_id ?? run.agent_id ?? undefined }, d);
11189
+ addComment({ task_id: run.task_id, content: sanitizePreWriteText(input.message, "run_event.comment"), type: "comment", agent_id: input.agent_id ?? run.agent_id ?? undefined }, d);
10123
11190
  }
10124
11191
  return rowToEvent(d.query("SELECT * FROM task_run_events WHERE id = ?").get(id));
10125
11192
  }
@@ -10131,10 +11198,10 @@ function addTaskRunCommand(input, db) {
10131
11198
  throw new Error(`Run not found: ${input.run_id}`);
10132
11199
  const id = uuid();
10133
11200
  const status = input.status || "unknown";
10134
- const timestamp = now();
10135
- const command = redactEvidenceText(input.command);
10136
- const outputSummary = input.output_summary ? redactEvidenceText(input.output_summary) : null;
10137
- const artifactPath = input.artifact_path ? redactEvidenceText(input.artifact_path) : null;
11201
+ const timestamp2 = now();
11202
+ const command = sanitizePreWriteText(input.command, "run_command.command");
11203
+ const outputSummary = input.output_summary ? sanitizePreWriteText(input.output_summary, "run_command.output_summary") : null;
11204
+ const artifactPath = input.artifact_path ? sanitizePreWriteText(input.artifact_path, "run_command.artifact_path") : null;
10138
11205
  d.run("INSERT INTO task_run_commands (id, run_id, task_id, command, status, exit_code, output_summary, artifact_path, agent_id, started_at, completed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
10139
11206
  id,
10140
11207
  run.id,
@@ -10146,8 +11213,8 @@ function addTaskRunCommand(input, db) {
10146
11213
  artifactPath,
10147
11214
  input.agent_id ?? run.agent_id,
10148
11215
  input.started_at ?? null,
10149
- input.completed_at ?? timestamp,
10150
- timestamp
11216
+ input.completed_at ?? timestamp2,
11217
+ timestamp2
10151
11218
  ]);
10152
11219
  addTaskVerification({
10153
11220
  task_id: run.task_id,
@@ -10156,7 +11223,7 @@ function addTaskRunCommand(input, db) {
10156
11223
  output_summary: outputSummary ?? undefined,
10157
11224
  artifact_path: artifactPath ?? undefined,
10158
11225
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10159
- run_at: input.completed_at ?? timestamp
11226
+ run_at: input.completed_at ?? timestamp2
10160
11227
  }, d);
10161
11228
  addTaskRunEvent({
10162
11229
  run_id: run.id,
@@ -10175,7 +11242,7 @@ function addTaskRunCommand(input, db) {
10175
11242
  }
10176
11243
  },
10177
11244
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10178
- created_at: timestamp
11245
+ created_at: timestamp2
10179
11246
  }, d);
10180
11247
  return d.query("SELECT * FROM task_run_commands WHERE id = ?").get(id);
10181
11248
  }
@@ -10189,7 +11256,7 @@ function addTaskRunFile(input, db) {
10189
11256
  task_id: run.task_id,
10190
11257
  path: input.path,
10191
11258
  status: input.status || "modified",
10192
- note: input.note ? redactEvidenceText(input.note) : undefined,
11259
+ note: input.note ? sanitizePreWriteText(input.note, "run_file.note") : undefined,
10193
11260
  agent_id: input.agent_id ?? run.agent_id ?? undefined
10194
11261
  }, d);
10195
11262
  addTaskRunEvent({
@@ -10208,19 +11275,19 @@ function addTaskRunArtifact(input, db) {
10208
11275
  if (!run)
10209
11276
  throw new Error(`Run not found: ${input.run_id}`);
10210
11277
  const id = uuid();
10211
- const timestamp = now();
10212
- const path = redactEvidenceText(input.path);
10213
- const description = input.description ? redactEvidenceText(input.description) : null;
10214
- const metadata = redactValue(input.metadata || {});
11278
+ const timestamp2 = now();
11279
+ const path = sanitizePreWriteText(input.path, "run_artifact.path");
11280
+ const description = input.description ? sanitizePreWriteText(input.description, "run_artifact.description") : null;
11281
+ const metadata = sanitizePreWriteValue(input.metadata || {}, "run_artifact.metadata");
10215
11282
  let sizeBytes = input.size_bytes ?? null;
10216
11283
  let digest = input.sha256 ?? null;
10217
- const stored = input.store_content !== false ? storeArtifactContent({ path: input.path, metadata, retention_days: input.retention_days, created_at: timestamp }) : null;
11284
+ const stored = input.store_content !== false ? storeArtifactContent({ path: input.path, metadata, retention_days: input.retention_days, created_at: timestamp2 }) : null;
10218
11285
  if (stored) {
10219
11286
  sizeBytes = stored.size_bytes;
10220
11287
  digest = stored.sha256;
10221
11288
  metadata["artifact_store"] = stored.store;
10222
11289
  } else if (input.store_content === true) {
10223
- throw new Error(`Artifact file not found: ${input.path}`);
11290
+ throw new Error(`Artifact file not found: ${path}`);
10224
11291
  }
10225
11292
  d.run("INSERT INTO task_run_artifacts (id, run_id, task_id, path, artifact_type, description, size_bytes, sha256, metadata, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
10226
11293
  id,
@@ -10233,7 +11300,7 @@ function addTaskRunArtifact(input, db) {
10233
11300
  digest,
10234
11301
  JSON.stringify(metadata),
10235
11302
  input.agent_id ?? run.agent_id,
10236
- timestamp
11303
+ timestamp2
10237
11304
  ]);
10238
11305
  addTaskRunEvent({
10239
11306
  run_id: run.id,
@@ -10241,7 +11308,7 @@ function addTaskRunArtifact(input, db) {
10241
11308
  message: description || path,
10242
11309
  data: { path, artifact_type: input.artifact_type, size_bytes: sizeBytes, sha256: digest, stored: Boolean(stored) },
10243
11310
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10244
- created_at: timestamp
11311
+ created_at: timestamp2
10245
11312
  }, d);
10246
11313
  return rowToArtifact(d.query("SELECT * FROM task_run_artifacts WHERE id = ?").get(id));
10247
11314
  }
@@ -10261,28 +11328,28 @@ function finishTaskRun(input, db) {
10261
11328
  const run = getTaskRun(runId, d);
10262
11329
  if (!run)
10263
11330
  throw new Error(`Run not found: ${input.run_id}`);
10264
- const timestamp = input.completed_at || now();
10265
- const summary = input.summary ? redactEvidenceText(input.summary) : null;
10266
- d.run("UPDATE task_runs SET status = ?, summary = COALESCE(?, summary), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, summary, timestamp, timestamp, run.id]);
11331
+ const timestamp2 = input.completed_at || now();
11332
+ const summary = input.summary ? sanitizePreWriteText(input.summary, "run.summary") : null;
11333
+ d.run("UPDATE task_runs SET status = ?, summary = COALESCE(?, summary), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, summary, timestamp2, timestamp2, run.id]);
10267
11334
  addTaskRunEvent({
10268
11335
  run_id: run.id,
10269
11336
  event_type: input.status,
10270
11337
  message: summary || `run ${input.status}`,
10271
11338
  data: { status: input.status },
10272
11339
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10273
- created_at: timestamp
11340
+ created_at: timestamp2
10274
11341
  }, d);
10275
11342
  const updated = getTaskRun(run.id, d);
10276
11343
  emitLocalEventHooksQuiet({
10277
11344
  type: `run.${input.status}`,
10278
- payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp },
11345
+ payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp2 },
10279
11346
  databasePath: databasePathFromDatabase(d)
10280
11347
  });
10281
11348
  return updated;
10282
11349
  }
10283
11350
  function finishTaskRunTransaction(input, db) {
10284
11351
  const d = db || getDatabase();
10285
- const timestamp = input.completed_at || now();
11352
+ const timestamp2 = input.completed_at || now();
10286
11353
  const status = input.status || "completed";
10287
11354
  const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
10288
11355
  const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
@@ -10300,7 +11367,7 @@ function finishTaskRunTransaction(input, db) {
10300
11367
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10301
11368
  local_only: true,
10302
11369
  dry_run: dryRun,
10303
- processed_at: timestamp,
11370
+ processed_at: timestamp2,
10304
11371
  action: conflict ? "conflict" : "matched",
10305
11372
  key: resolvedKey,
10306
11373
  run: summarizeTaskRun(run),
@@ -10313,10 +11380,10 @@ function finishTaskRunTransaction(input, db) {
10313
11380
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10314
11381
  local_only: true,
10315
11382
  dry_run: true,
10316
- processed_at: timestamp,
11383
+ processed_at: timestamp2,
10317
11384
  action: "preview",
10318
11385
  key: resolvedKey,
10319
- run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
11386
+ run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp2, updated_at: timestamp2 }),
10320
11387
  warnings: [],
10321
11388
  commands: loopRunCommands(run, resolvedKey)
10322
11389
  };
@@ -10326,13 +11393,13 @@ function finishTaskRunTransaction(input, db) {
10326
11393
  status,
10327
11394
  summary: input.summary,
10328
11395
  agent_id: input.agent_id,
10329
- completed_at: timestamp
11396
+ completed_at: timestamp2
10330
11397
  }, d);
10331
11398
  return {
10332
11399
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10333
11400
  local_only: true,
10334
11401
  dry_run: false,
10335
- processed_at: timestamp,
11402
+ processed_at: timestamp2,
10336
11403
  action: "finished",
10337
11404
  key: resolvedKey,
10338
11405
  run: summarizeTaskRun(finished),
@@ -10362,7 +11429,7 @@ var init_task_runs = __esm(() => {
10362
11429
  init_artifact_store();
10363
11430
  init_event_emission_safety();
10364
11431
  init_event_hooks();
10365
- init_redaction();
11432
+ init_prewrite_secrets();
10366
11433
  init_types();
10367
11434
  init_comments();
10368
11435
  init_database();
@@ -10490,7 +11557,7 @@ function inWindow(event, query) {
10490
11557
  function createCalendarItem(input, db) {
10491
11558
  const d = db || getDatabase();
10492
11559
  const id = uuid();
10493
- const timestamp = now();
11560
+ const timestamp2 = now();
10494
11561
  const kind = input.kind || "work_block";
10495
11562
  d.run(`INSERT INTO local_calendar_items (
10496
11563
  id, kind, title, description, starts_at, ends_at, timezone, project_id, task_id, plan_id, run_id,
@@ -10509,8 +11576,8 @@ function createCalendarItem(input, db) {
10509
11576
  input.run_id || null,
10510
11577
  input.recurrence_rule || null,
10511
11578
  JSON.stringify(input.metadata || {}),
10512
- timestamp,
10513
- timestamp
11579
+ timestamp2,
11580
+ timestamp2
10514
11581
  ]);
10515
11582
  return getCalendarItem(id, d);
10516
11583
  }
@@ -10757,18 +11824,172 @@ var init_tasks = __esm(() => {
10757
11824
  // src/storage/index.ts
10758
11825
  init_config();
10759
11826
 
11827
+ // src/lib/search.ts
11828
+ init_database();
11829
+ function rowToTask(row) {
11830
+ return {
11831
+ ...row,
11832
+ tags: JSON.parse(row.tags || "[]"),
11833
+ metadata: JSON.parse(row.metadata || "{}"),
11834
+ status: row.status,
11835
+ priority: row.priority,
11836
+ requires_approval: Boolean(row.requires_approval)
11837
+ };
11838
+ }
11839
+ var DEFAULT_SEARCH_LIMIT = 1000;
11840
+ var BM25_WEIGHTS = "0.0, 10.0, 5.0, 3.0";
11841
+ function hasFts(db) {
11842
+ try {
11843
+ const result = db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
11844
+ return result !== null;
11845
+ } catch {
11846
+ return false;
11847
+ }
11848
+ }
11849
+ function buildFtsMatchQuery(raw) {
11850
+ const terms = [];
11851
+ const re = /"([^"]*)"|(\S+)/g;
11852
+ let m;
11853
+ while ((m = re.exec(raw)) !== null) {
11854
+ if (m[1] !== undefined) {
11855
+ const phrase = m[1].replace(/"/g, " ").replace(/\s+/g, " ").trim();
11856
+ if (phrase)
11857
+ terms.push(`"${phrase}"`);
11858
+ } else {
11859
+ const cleaned = m[2].replace(/["*^():]/g, " ").replace(/\s+/g, " ").trim();
11860
+ if (cleaned)
11861
+ terms.push(`"${cleaned}"*`);
11862
+ }
11863
+ }
11864
+ return terms.join(" AND ");
11865
+ }
11866
+ function boundedLimit(limit) {
11867
+ if (!Number.isFinite(limit) || !limit || limit <= 0)
11868
+ return DEFAULT_SEARCH_LIMIT;
11869
+ return Math.trunc(limit);
11870
+ }
11871
+ function searchTasks(options, projectId, taskListId, db) {
11872
+ const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
11873
+ const d = db || getDatabase();
11874
+ clearExpiredLocks(d);
11875
+ const params = [];
11876
+ let sql;
11877
+ const raw = opts.query?.trim() ?? "";
11878
+ const q = raw === "*" ? "" : raw;
11879
+ const ftsMatch = q ? buildFtsMatchQuery(q) : "";
11880
+ const useFts = hasFts(d) && q !== "" && ftsMatch !== "";
11881
+ if (useFts) {
11882
+ const pattern = `%${q}%`;
11883
+ sql = `SELECT t.* FROM tasks t
11884
+ LEFT JOIN (
11885
+ SELECT rowid, bm25(tasks_fts, ${BM25_WEIGHTS}) AS rank
11886
+ FROM tasks_fts WHERE tasks_fts MATCH ?
11887
+ ) fts ON fts.rowid = t.rowid
11888
+ WHERE (
11889
+ fts.rowid IS NOT NULL
11890
+ OR t.id LIKE ?
11891
+ OR t.short_id LIKE ?
11892
+ OR t.title LIKE ?
11893
+ OR t.description LIKE ?
11894
+ OR t.working_dir LIKE ?
11895
+ OR t.metadata LIKE ?
11896
+ OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
11897
+ )`;
11898
+ params.push(ftsMatch, pattern, pattern, pattern, pattern, pattern, pattern, pattern);
11899
+ } else if (q) {
11900
+ const pattern = `%${q}%`;
11901
+ sql = `SELECT t.* FROM tasks t WHERE (
11902
+ t.id LIKE ?
11903
+ OR t.short_id LIKE ?
11904
+ OR t.title LIKE ?
11905
+ OR t.description LIKE ?
11906
+ OR t.working_dir LIKE ?
11907
+ OR t.metadata LIKE ?
11908
+ OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
11909
+ )`;
11910
+ params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
11911
+ } else {
11912
+ sql = `SELECT t.* FROM tasks t WHERE 1=1`;
11913
+ }
11914
+ if (opts.project_id) {
11915
+ sql += " AND t.project_id = ?";
11916
+ params.push(opts.project_id);
11917
+ }
11918
+ if (opts.task_list_id) {
11919
+ sql += " AND t.task_list_id = ?";
11920
+ params.push(opts.task_list_id);
11921
+ }
11922
+ if (opts.status) {
11923
+ if (Array.isArray(opts.status)) {
11924
+ sql += ` AND t.status IN (${opts.status.map(() => "?").join(",")})`;
11925
+ params.push(...opts.status);
11926
+ } else {
11927
+ sql += " AND t.status = ?";
11928
+ params.push(opts.status);
11929
+ }
11930
+ }
11931
+ if (opts.priority) {
11932
+ if (Array.isArray(opts.priority)) {
11933
+ sql += ` AND t.priority IN (${opts.priority.map(() => "?").join(",")})`;
11934
+ params.push(...opts.priority);
11935
+ } else {
11936
+ sql += " AND t.priority = ?";
11937
+ params.push(opts.priority);
11938
+ }
11939
+ }
11940
+ if (opts.assigned_to) {
11941
+ sql += " AND t.assigned_to = ?";
11942
+ params.push(opts.assigned_to);
11943
+ }
11944
+ if (opts.agent_id) {
11945
+ sql += " AND t.agent_id = ?";
11946
+ params.push(opts.agent_id);
11947
+ }
11948
+ if (opts.created_after) {
11949
+ sql += " AND t.created_at > ?";
11950
+ params.push(opts.created_after);
11951
+ }
11952
+ if (opts.updated_after) {
11953
+ sql += " AND t.updated_at > ?";
11954
+ params.push(opts.updated_after);
11955
+ }
11956
+ if (opts.has_dependencies === true) {
11957
+ sql += " AND t.id IN (SELECT task_id FROM task_dependencies)";
11958
+ } else if (opts.has_dependencies === false) {
11959
+ sql += " AND t.id NOT IN (SELECT task_id FROM task_dependencies)";
11960
+ }
11961
+ if (opts.is_blocked === true) {
11962
+ sql += " AND t.id IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
11963
+ } else if (opts.is_blocked === false) {
11964
+ sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
11965
+ }
11966
+ if (useFts) {
11967
+ sql += ` ORDER BY (fts.rowid IS NULL) ASC, fts.rank ASC,
11968
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
11969
+ t.created_at DESC`;
11970
+ } else {
11971
+ sql += ` ORDER BY
11972
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
11973
+ t.created_at DESC`;
11974
+ }
11975
+ sql += ` LIMIT ?`;
11976
+ params.push(boundedLimit(opts.limit));
11977
+ const rows = d.query(sql).all(...params);
11978
+ return rows.map(rowToTask);
11979
+ }
11980
+
10760
11981
  // src/storage/local-sqlite.ts
10761
11982
  init_tasks();
10762
11983
  init_projects();
10763
11984
  init_plans();
10764
11985
 
10765
11986
  // src/db/agents.ts
11987
+ init_types();
10766
11988
  init_database();
11989
+ init_identity_mapping();
10767
11990
  init_storage_tombstones();
10768
11991
 
10769
11992
  // src/db/agent-names.ts
10770
- init_database();
10771
-
10772
11993
  class InvalidAgentNameError extends Error {
10773
11994
  suggestions;
10774
11995
  constructor(name, reason, suggestions = []) {
@@ -11049,45 +12270,10 @@ function validateAgentName(name, existingNames = []) {
11049
12270
  }
11050
12271
  return normalized;
11051
12272
  }
11052
- function tableHasColumn(db, table, column) {
11053
- try {
11054
- return db.query(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
11055
- } catch {
11056
- return false;
11057
- }
11058
- }
11059
- function updateReferences(db, oldName, newName) {
11060
- const refs = [
11061
- ["tasks", "assigned_to"],
11062
- ["tasks", "agent_id"],
11063
- ["tasks", "locked_by"],
11064
- ["tasks", "assigned_by"],
11065
- ["plans", "agent_id"],
11066
- ["sessions", "agent_id"],
11067
- ["task_comments", "agent_id"],
11068
- ["task_history", "agent_id"],
11069
- ["webhooks", "agent_id"],
11070
- ["task_files", "agent_id"],
11071
- ["task_time_logs", "agent_id"],
11072
- ["task_watchers", "agent_id"],
11073
- ["task_checkpoints", "agent_id"],
11074
- ["task_heartbeats", "agent_id"],
11075
- ["project_agent_roles", "agent_id"]
11076
- ];
11077
- let changed = 0;
11078
- for (const [table, column] of refs) {
11079
- if (!tableHasColumn(db, table, column))
11080
- continue;
11081
- try {
11082
- changed += db.run(`UPDATE ${table} SET ${column} = ? WHERE LOWER(${column}) = ?`, [newName, oldName]).changes;
11083
- } catch {}
11084
- }
11085
- return changed;
11086
- }
11087
12273
  function normalizeGeneratedAgentNames(db) {
11088
12274
  const rows = db.query("SELECT * FROM agents ORDER BY created_at, id").all();
11089
12275
  const existing = new Set(rows.map((agent) => normalizeAgentNameInput(agent.name)));
11090
- const renamed = [];
12276
+ const planned = [];
11091
12277
  for (const agent of rows) {
11092
12278
  const oldName = normalizeAgentNameInput(agent.name);
11093
12279
  if (!isBlockedAgentName(oldName))
@@ -11097,18 +12283,20 @@ function normalizeGeneratedAgentNames(db) {
11097
12283
  if (!replacement) {
11098
12284
  throw new Error("No safe agent names are available for normalization");
11099
12285
  }
11100
- existing.delete(oldName);
11101
12286
  existing.add(replacement);
11102
- db.run("UPDATE agents SET name = ?, last_seen_at = ? WHERE id = ?", [replacement, now(), agent.id]);
11103
- const referenceUpdates = updateReferences(db, oldName, replacement);
11104
- renamed.push({
12287
+ planned.push({
11105
12288
  id: agent.id,
11106
12289
  old_name: oldName,
11107
12290
  new_name: replacement,
11108
- reference_updates: referenceUpdates
12291
+ applied: false,
12292
+ disposition: "candidate",
12293
+ alias_kind: "candidate",
12294
+ status: "quarantined",
12295
+ name_updates: 0,
12296
+ reference_updates: 0
11109
12297
  });
11110
12298
  }
11111
- return renamed;
12299
+ return planned;
11112
12300
  }
11113
12301
 
11114
12302
  // src/db/agents.ts
@@ -11130,7 +12318,6 @@ function autoReleaseStaleAgents(db) {
11130
12318
  return result.changes;
11131
12319
  }
11132
12320
  function getAvailableNamesFromPool(pool, db) {
11133
- autoReleaseStaleAgents(db);
11134
12321
  const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
11135
12322
  const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
11136
12323
  return pool.map(normalizeAgentNameInput).filter((name) => !activeNames.has(name));
@@ -11141,6 +12328,10 @@ function shortUuid() {
11141
12328
  function rowToAgent(row) {
11142
12329
  return {
11143
12330
  ...row,
12331
+ identity_id: row.identity_id ?? null,
12332
+ project_id: row.project_id ?? null,
12333
+ runtime_instance_id: row.runtime_instance_id ?? null,
12334
+ machine_id: row.machine_id ?? null,
11144
12335
  permissions: JSON.parse(row.permissions || '["*"]'),
11145
12336
  capabilities: JSON.parse(row.capabilities || "[]"),
11146
12337
  status: row.status || "active",
@@ -11149,11 +12340,37 @@ function rowToAgent(row) {
11149
12340
  }
11150
12341
  function registerAgent(input, db) {
11151
12342
  const d = db || getDatabase();
12343
+ const register = d.transaction(() => registerAgentInCurrentTransaction(input, d));
12344
+ return register.immediate();
12345
+ }
12346
+ function registerAgentInCurrentTransaction(input, d) {
11152
12347
  const machineId = currentStorageMachineId(d);
12348
+ const identityId = input.identity_id === undefined ? null : input.identity_id.trim();
12349
+ if (input.identity_id !== undefined && !identityId) {
12350
+ throw new Error("identity_id must be a non-empty canonical identifier");
12351
+ }
11153
12352
  const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
11154
12353
  const normalizedName = validateAgentName(input.name, existingNames);
11155
- const existing = getAgentByName(normalizedName, d);
12354
+ const existingByName = getAgentByNameProjection(normalizedName, d);
12355
+ const identityRows = identityId ? d.query("SELECT id FROM agents WHERE identity_id = ? ORDER BY id").all(identityId) : [];
12356
+ if (identityRows.length > 1) {
12357
+ throw new IdentityAliasAmbiguousError(identityId, identityRows.map((row) => row.id));
12358
+ }
12359
+ const existingByIdentity = identityRows[0] ? getAgent(identityRows[0].id, d) : null;
12360
+ if (existingByIdentity && existingByName && existingByIdentity.id !== existingByName.id) {
12361
+ throw new IdentityAliasAmbiguousError(normalizedName, [existingByIdentity.id, existingByName.id]);
12362
+ }
12363
+ if (identityId && existingByName && !existingByIdentity && existingByName.identity_id !== identityId) {
12364
+ throw new IdentityAliasAmbiguousError(normalizedName, [existingByName.id]);
12365
+ }
12366
+ const existing = existingByIdentity || existingByName;
11156
12367
  if (existing) {
12368
+ if (existing.identity_id && !identityId) {
12369
+ return buildConflictError(existing, new Date(existing.last_seen_at).getTime(), input.pool, d);
12370
+ }
12371
+ if (identityId && existing.identity_id && identityId !== existing.identity_id) {
12372
+ throw new IdentityIdImmutableError(existing.id);
12373
+ }
11157
12374
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
11158
12375
  const activeWindowMs = getActiveWindowMs();
11159
12376
  const isActive = Date.now() - lastSeenMs < activeWindowMs;
@@ -11190,6 +12407,14 @@ function registerAgent(input, db) {
11190
12407
  updates.push("active_project_id = ?");
11191
12408
  params.push(input.project_id);
11192
12409
  }
12410
+ if (input.project_id) {
12411
+ updates.push("project_id = ?");
12412
+ params.push(input.project_id);
12413
+ }
12414
+ if (input.runtime_instance_id) {
12415
+ updates.push("runtime_instance_id = ?");
12416
+ params.push(input.runtime_instance_id);
12417
+ }
11193
12418
  if (!existing.machine_id && machineId) {
11194
12419
  updates.push("machine_id = ?");
11195
12420
  params.push(machineId);
@@ -11199,11 +12424,12 @@ function registerAgent(input, db) {
11199
12424
  return getAgent(existing.id, d);
11200
12425
  }
11201
12426
  const id = shortUuid();
11202
- const timestamp = now();
11203
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id, machine_id)
11204
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12427
+ const timestamp2 = now();
12428
+ d.run(`INSERT INTO agents (id, name, identity_id, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, project_id, runtime_instance_id, active_project_id, machine_id)
12429
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11205
12430
  id,
11206
12431
  normalizedName,
12432
+ identityId,
11207
12433
  input.description || null,
11208
12434
  input.role || "agent",
11209
12435
  input.title || null,
@@ -11213,10 +12439,12 @@ function registerAgent(input, db) {
11213
12439
  input.reports_to || null,
11214
12440
  input.org_id || null,
11215
12441
  JSON.stringify(input.metadata || {}),
11216
- timestamp,
11217
- timestamp,
12442
+ timestamp2,
12443
+ timestamp2,
11218
12444
  input.session_id || null,
11219
12445
  input.working_dir || null,
12446
+ input.project_id || null,
12447
+ input.runtime_instance_id || null,
11220
12448
  input.project_id && input.session_id ? input.project_id : null,
11221
12449
  machineId
11222
12450
  ]);
@@ -11256,12 +12484,15 @@ function getAgent(id, db) {
11256
12484
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
11257
12485
  return row ? rowToAgent(row) : null;
11258
12486
  }
11259
- function getAgentByName(name, db) {
11260
- const d = db || getDatabase();
12487
+ function getAgentByNameProjection(name, d) {
11261
12488
  const normalizedName = name.trim().toLowerCase();
11262
12489
  const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
11263
12490
  return row ? rowToAgent(row) : null;
11264
12491
  }
12492
+ function getAgentByName(name, db) {
12493
+ const agent = getAgentByNameProjection(name, db || getDatabase());
12494
+ return agent ? { ...agent, identity_id: null } : null;
12495
+ }
11265
12496
  function listAgents(opts, db) {
11266
12497
  let d;
11267
12498
  let includeArchived = false;
@@ -11271,7 +12502,6 @@ function listAgents(opts, db) {
11271
12502
  includeArchived = opts?.include_archived ?? false;
11272
12503
  d = db || getDatabase();
11273
12504
  }
11274
- autoReleaseStaleAgents(d);
11275
12505
  if (includeArchived) {
11276
12506
  return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
11277
12507
  }
@@ -11288,21 +12518,19 @@ function updateAgent(id, input, db) {
11288
12518
  throw new Error(`Agent not found: ${id}`);
11289
12519
  const sets = ["last_seen_at = ?"];
11290
12520
  const params = [now()];
12521
+ let historicalLabel = null;
11291
12522
  if (input.name !== undefined) {
11292
12523
  const existingNames = d.query("SELECT name FROM agents WHERE id != ?").all(id).map((row) => row.name);
11293
12524
  const newName = validateAgentName(input.name, existingNames);
11294
- const holder = getAgentByName(newName, d);
11295
- if (holder && holder.id !== id) {
11296
- const lastSeenMs = new Date(holder.last_seen_at).getTime();
11297
- const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
11298
- if (isActive && holder.status === "active") {
11299
- throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
12525
+ if (newName !== agent.name) {
12526
+ const holder = getAgentByName(newName, d);
12527
+ if (holder && holder.id !== id) {
12528
+ throw new IdentityAliasAmbiguousError(newName, [holder.id, id]);
11300
12529
  }
11301
- const evictedName = `${holder.name}__evicted_${holder.id}`;
11302
- d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
12530
+ historicalLabel = agent.name;
12531
+ sets.push("name = ?");
12532
+ params.push(newName);
11303
12533
  }
11304
- sets.push("name = ?");
11305
- params.push(newName);
11306
12534
  }
11307
12535
  if (input.description !== undefined) {
11308
12536
  sets.push("description = ?");
@@ -11341,7 +12569,12 @@ function updateAgent(id, input, db) {
11341
12569
  params.push(JSON.stringify(input.metadata));
11342
12570
  }
11343
12571
  params.push(id);
11344
- d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
12572
+ const apply = d.transaction(() => {
12573
+ if (historicalLabel)
12574
+ recordAgentAlias(id, historicalLabel, d);
12575
+ d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
12576
+ });
12577
+ apply.immediate();
11345
12578
  return getAgent(id, d);
11346
12579
  }
11347
12580
  function deleteAgent(id, db) {
@@ -11499,6 +12732,21 @@ var TEMPLATE_COLUMNS = [
11499
12732
  "machine_id",
11500
12733
  "synced_at"
11501
12734
  ];
12735
+ var TEMPLATE_TASK_COLUMNS = [
12736
+ "id",
12737
+ "template_id",
12738
+ "position",
12739
+ "title_pattern",
12740
+ "description",
12741
+ "priority",
12742
+ "tags",
12743
+ "task_type",
12744
+ "condition",
12745
+ "include_template_id",
12746
+ "depends_on_positions",
12747
+ "metadata",
12748
+ "created_at"
12749
+ ];
11502
12750
  var TASK_COLUMNS = [
11503
12751
  "id",
11504
12752
  "short_id",
@@ -11567,7 +12815,7 @@ var AUDIT_COLUMNS = [
11567
12815
  "created_at",
11568
12816
  "machine_id"
11569
12817
  ];
11570
- var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
12818
+ var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
11571
12819
  var BOOLEAN_COLUMNS = new Set(["requires_approval"]);
11572
12820
  function exportSqliteTodosStorageSnapshot(db) {
11573
12821
  const d = db ?? getDatabase();
@@ -11581,6 +12829,7 @@ function exportSqliteTodosStorageSnapshot(db) {
11581
12829
  agents: listAgents({ include_archived: true }, d),
11582
12830
  taskLists: listTaskLists(undefined, d),
11583
12831
  templates: listTemplates(d),
12832
+ templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
11584
12833
  auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
11585
12834
  tombstones: listStorageTombstones(d)
11586
12835
  };
@@ -11630,6 +12879,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
11630
12879
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
11631
12880
  applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
11632
12881
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
12882
+ applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
11633
12883
  applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
11634
12884
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
11635
12885
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
@@ -11734,6 +12984,8 @@ function tableForTombstone(objectType) {
11734
12984
  return "task_lists";
11735
12985
  if (objectType === "templates")
11736
12986
  return "task_templates";
12987
+ if (objectType === "template_tasks")
12988
+ return "template_tasks";
11737
12989
  return "task_history";
11738
12990
  }
11739
12991
  function listRows(db, table, columns) {
@@ -11793,6 +13045,52 @@ function resolveTaskRefLocal(db, ref) {
11793
13045
  return getTask(shortIdRows[0].id, db);
11794
13046
  return null;
11795
13047
  }
13048
+ function isSearchQuery(filter) {
13049
+ const q = filter.query?.trim();
13050
+ return !!q && q !== "*";
13051
+ }
13052
+ function matchesExtraFilters(task, filter) {
13053
+ if (filter.ids && !filter.ids.includes(task.id))
13054
+ return false;
13055
+ if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
13056
+ return false;
13057
+ if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
13058
+ return false;
13059
+ if (filter.session_id !== undefined && task.session_id !== filter.session_id)
13060
+ return false;
13061
+ if (filter.has_recurrence !== undefined && Boolean(task.recurrence_rule) !== filter.has_recurrence)
13062
+ return false;
13063
+ if (filter.task_type !== undefined) {
13064
+ const allowed = Array.isArray(filter.task_type) ? filter.task_type : [filter.task_type];
13065
+ if (!allowed.includes(task.task_type ?? ""))
13066
+ return false;
13067
+ }
13068
+ if (filter.tags?.length) {
13069
+ const taskTags = new Set(task.tags ?? []);
13070
+ if (!filter.tags.every((tag) => taskTags.has(tag)))
13071
+ return false;
13072
+ }
13073
+ if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
13074
+ return false;
13075
+ return true;
13076
+ }
13077
+ function listTasksMaybeSearch(filter, db) {
13078
+ if (!isSearchQuery(filter))
13079
+ return listTasks(filter, db);
13080
+ const matched = searchTasks({
13081
+ query: filter.query,
13082
+ project_id: filter.project_id,
13083
+ task_list_id: filter.task_list_id,
13084
+ status: filter.status,
13085
+ priority: filter.priority,
13086
+ assigned_to: filter.assigned_to,
13087
+ agent_id: filter.agent_id
13088
+ }, undefined, undefined, db).filter((task) => matchesExtraFilters(task, filter));
13089
+ const offset = filter.offset && filter.offset > 0 ? Math.trunc(filter.offset) : 0;
13090
+ if (filter.limit !== undefined && filter.limit >= 0)
13091
+ return matched.slice(offset, offset + filter.limit);
13092
+ return offset ? matched.slice(offset) : matched;
13093
+ }
11796
13094
  function createLocalSqliteTodosStorageAdapter(options = {}) {
11797
13095
  const database = () => options.db ?? getDatabase();
11798
13096
  let adapter;
@@ -11809,8 +13107,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
11809
13107
  create: (input) => createTask(input, database()),
11810
13108
  get: (id) => getTask(id, database()),
11811
13109
  resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
11812
- list: (filter = {}) => listTasks(filter, database()),
11813
- count: (filter = {}) => countTasks(filter, database()),
13110
+ list: (filter = {}) => listTasksMaybeSearch(filter, database()),
13111
+ count: (filter = {}) => isSearchQuery(filter) ? listTasksMaybeSearch({ ...filter, limit: undefined, offset: undefined }, database()).length : countTasks(filter, database()),
11814
13112
  update: (id, input) => updateTask(id, input, database()),
11815
13113
  unlock: (id, agentId) => {
11816
13114
  unlockTask(id, agentId, database());
@@ -11920,6 +13218,27 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
11920
13218
  `CREATE INDEX IF NOT EXISTS ${tableName}_task_status_idx ON ${tableName} ((payload->>'status')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
11921
13219
  `CREATE INDEX IF NOT EXISTS ${tableName}_task_project_idx ON ${tableName} ((payload->>'project_id')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
11922
13220
  `CREATE INDEX IF NOT EXISTS ${tableName}_payload_gin ON ${tableName} USING gin (payload jsonb_path_ops)`,
13221
+ `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
13222
+ `CREATE EXTENSION IF NOT EXISTS unaccent`,
13223
+ `CREATE OR REPLACE FUNCTION todos_immutable_unaccent(text)
13224
+ RETURNS text
13225
+ LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
13226
+ AS $$ SELECT unaccent('unaccent', $1) $$`,
13227
+ `ALTER TABLE ${tableName}
13228
+ ADD COLUMN IF NOT EXISTS task_search_tsv tsvector
13229
+ GENERATED ALWAYS AS (
13230
+ setweight(to_tsvector('simple', todos_immutable_unaccent(COALESCE(payload->>'title', ''))), 'A')
13231
+ || setweight(to_tsvector('simple', todos_immutable_unaccent(COALESCE(payload->>'description', ''))), 'B')
13232
+ || setweight(to_tsvector('simple', todos_immutable_unaccent(translate(COALESCE(payload->>'tags', '[]'), '[]",', ' '))), 'C')
13233
+ ) STORED`,
13234
+ `CREATE INDEX IF NOT EXISTS ${tableName}_task_search_tsv_idx
13235
+ ON ${tableName} USING gin (task_search_tsv)
13236
+ WHERE object_type = 'tasks' AND deleted_at IS NULL`,
13237
+ `CREATE INDEX IF NOT EXISTS ${tableName}_task_search_trgm_idx
13238
+ ON ${tableName} USING gin (
13239
+ todos_immutable_unaccent(COALESCE(payload->>'title', '') || ' ' || COALESCE(payload->>'description', '')) gin_trgm_ops
13240
+ )
13241
+ WHERE object_type = 'tasks' AND deleted_at IS NULL`,
11923
13242
  `CREATE TABLE IF NOT EXISTS ${cursorTableName} (
11924
13243
  service text NOT NULL,
11925
13244
  cursor_name text NOT NULL,
@@ -12152,6 +13471,7 @@ function snapshotEntries(snapshot) {
12152
13471
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
12153
13472
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
12154
13473
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
13474
+ ...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
12155
13475
  ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
12156
13476
  ...(snapshot.tombstones ?? []).map((tombstone) => ({
12157
13477
  type: tombstone.object_type,
@@ -12203,6 +13523,7 @@ function rowsToSnapshot(rows) {
12203
13523
  agents: [],
12204
13524
  taskLists: [],
12205
13525
  templates: [],
13526
+ templateTasks: [],
12206
13527
  auditHistory: [],
12207
13528
  tombstones: []
12208
13529
  };
@@ -12237,6 +13558,8 @@ function rowsToSnapshot(rows) {
12237
13558
  snapshot.taskLists.push(payload);
12238
13559
  else if (row.object_type === "templates")
12239
13560
  snapshot.templates.push(payload);
13561
+ else if (row.object_type === "template_tasks")
13562
+ snapshot.templateTasks.push(payload);
12240
13563
  else if (row.object_type === "audit_history")
12241
13564
  snapshot.auditHistory.push(payload);
12242
13565
  }
@@ -12399,10 +13722,13 @@ function createPostgresTodosStorageAdapter(options) {
12399
13722
  get: (id) => store.get("templates", id),
12400
13723
  list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
12401
13724
  update: (id, input) => updateTemplate2(id, input, store),
12402
- delete: (id, context) => store.delete("templates", id, context),
13725
+ delete: (id, context) => deleteTemplate2(id, store, context),
12403
13726
  getWithTasks: async (id) => {
12404
13727
  const template = await store.get("templates", id);
12405
- return template ? { ...template, tasks: [] } : null;
13728
+ if (!template)
13729
+ return null;
13730
+ const tasks = (await store.list("template_tasks")).filter((task) => task.template_id === id).sort((left, right) => left.position - right.position || left.id.localeCompare(right.id));
13731
+ return { ...template, tasks };
12406
13732
  }
12407
13733
  },
12408
13734
  audit: {
@@ -12550,12 +13876,23 @@ class PostgresJsonRecordStore {
12550
13876
  }
12551
13877
  if (filter.include_subtasks !== true)
12552
13878
  conds.push(`(payload->>'parent_id' IS NULL OR payload->>'parent_id' = '')`);
12553
- return { where: conds.join(" AND "), params };
13879
+ let queryRef;
13880
+ const rawQuery = filter.query?.trim() ?? "";
13881
+ if (rawQuery && rawQuery !== "*") {
13882
+ queryRef = p(rawQuery);
13883
+ const clauses = [`task_search_tsv @@ websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))`];
13884
+ if (!/\s/.test(rawQuery)) {
13885
+ clauses.push(`todos_immutable_unaccent(${queryRef}) <% todos_immutable_unaccent(` + `COALESCE(payload->>'title', '') || ' ' || COALESCE(payload->>'description', ''))`);
13886
+ }
13887
+ conds.push(`(${clauses.join(" OR ")})`);
13888
+ }
13889
+ return { where: conds.join(" AND "), params, queryRef };
12554
13890
  }
12555
13891
  async listTasks(filter) {
12556
13892
  await this.ensureSchema();
12557
- const { where, params } = this.buildTaskFilterSql(filter);
12558
- let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${TASK_ORDER_BY}`;
13893
+ const { where, params, queryRef } = this.buildTaskFilterSql(filter);
13894
+ const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
13895
+ let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
12559
13896
  if (filter.limit !== undefined) {
12560
13897
  params.push(filter.limit);
12561
13898
  sql += ` LIMIT $${params.length}`;
@@ -12690,6 +14027,57 @@ class PostgresJsonRecordStore {
12690
14027
  }
12691
14028
  return value;
12692
14029
  }
14030
+ async createTemplateWithTasks(template, tasks, context = {}) {
14031
+ await this.ensureSchema();
14032
+ const records = [
14033
+ { object_type: "templates", object_id: template.id, payload: template, updated_at: template.created_at, version: template.version },
14034
+ ...tasks.map((task) => ({ object_type: "template_tasks", object_id: task.id, payload: task, updated_at: task.created_at, version: 1 }))
14035
+ ];
14036
+ const result = await this.options.client.query(`/* todos:create-template-with-tasks-atomic */ WITH input AS (
14037
+ SELECT value->>'object_type' AS object_type,
14038
+ value->>'object_id' AS object_id,
14039
+ value->'payload' AS payload,
14040
+ value->>'updated_at' AS updated_at,
14041
+ COALESCE((value->>'version')::integer, 1) AS version
14042
+ FROM jsonb_array_elements($2::jsonb) AS value
14043
+ ) INSERT INTO ${this.tableName} (
14044
+ service, object_type, object_id, payload, updated_at,
14045
+ deleted_at, source_machine_id, version
14046
+ ) SELECT $1, object_type, object_id, payload, updated_at::timestamptz,
14047
+ NULL, $3, version
14048
+ FROM input
14049
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
14050
+ payload = EXCLUDED.payload,
14051
+ updated_at = EXCLUDED.updated_at,
14052
+ deleted_at = NULL,
14053
+ source_machine_id = EXCLUDED.source_machine_id,
14054
+ version = EXCLUDED.version
14055
+ WHERE ${this.tableName}.updated_at IS NULL
14056
+ OR ${this.tableName}.updated_at < EXCLUDED.updated_at
14057
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
14058
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
14059
+ RETURNING object_type, object_id`, [this.service, jsonbParam(records), this.machineId(context)]);
14060
+ if (result.rows.length !== records.length) {
14061
+ throw new Error("Template checklist write was rejected before completion; no partial template was committed");
14062
+ }
14063
+ }
14064
+ async deleteTemplateWithTasks(id, context = {}) {
14065
+ await this.ensureSchema();
14066
+ const timestamp2 = new Date().toISOString();
14067
+ const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
14068
+ SELECT 1 FROM ${this.tableName}
14069
+ WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
14070
+ ) UPDATE ${this.tableName} AS record SET
14071
+ deleted_at = $3::timestamptz,
14072
+ updated_at = $3::timestamptz,
14073
+ source_machine_id = COALESCE($4, record.source_machine_id),
14074
+ version = COALESCE(record.version, 0) + 1
14075
+ WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
14076
+ AND (record.object_type = 'templates' AND record.object_id = $2
14077
+ OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
14078
+ RETURNING record.object_type`, [this.service, id, timestamp2, this.machineId(context)]);
14079
+ return result.rows.some((row) => row.object_type === "templates");
14080
+ }
12693
14081
  async completeTask(id, agentId, options) {
12694
14082
  await this.ensureSchema();
12695
14083
  const operationTimestamp = new Date().toISOString();
@@ -12751,7 +14139,7 @@ class PostgresJsonRecordStore {
12751
14139
  const normalizedSlug = slugifyRaw(newSlug);
12752
14140
  if (!normalizedSlug)
12753
14141
  throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
12754
- const timestamp = new Date().toISOString();
14142
+ const timestamp2 = new Date().toISOString();
12755
14143
  try {
12756
14144
  const result = await this.options.client.query(`/* todos:rename-project-atomic */ WITH target AS (
12757
14145
  SELECT payload, payload->>'task_list_id' AS old_slug
@@ -12799,7 +14187,7 @@ class PostgresJsonRecordStore {
12799
14187
  EXISTS (SELECT 1 FROM project_conflict) AS project_conflict,
12800
14188
  EXISTS (SELECT 1 FROM task_list_conflict) AS task_list_conflict,
12801
14189
  COALESCE((SELECT payload FROM updated_project), (SELECT payload FROM target)) AS project,
12802
- (SELECT count(*) FROM updated_lists) AS task_lists_updated`, [this.service, id, normalizedSlug, name ?? null, timestamp, this.machineId(context)]);
14190
+ (SELECT count(*) FROM updated_lists) AS task_lists_updated`, [this.service, id, normalizedSlug, name ?? null, timestamp2, this.machineId(context)]);
12803
14191
  const row = result.rows[0];
12804
14192
  if (!row?.found)
12805
14193
  throw new ProjectNotFoundError(id);
@@ -12852,19 +14240,19 @@ class PostgresJsonRecordStore {
12852
14240
  const existing = await this.get(type, id);
12853
14241
  if (!existing)
12854
14242
  return false;
12855
- const timestamp = new Date().toISOString();
14243
+ const timestamp2 = new Date().toISOString();
12856
14244
  return this.tombstone({
12857
14245
  object_type: type,
12858
14246
  object_id: id,
12859
- deleted_at: timestamp,
12860
- updated_at: timestamp,
14247
+ deleted_at: timestamp2,
14248
+ updated_at: timestamp2,
12861
14249
  payload: existing,
12862
14250
  version: numberValue2(existing["version"])
12863
14251
  }, context);
12864
14252
  }
12865
14253
  async deletePlan(id, context = {}) {
12866
14254
  await this.ensureSchema();
12867
- const timestamp = new Date().toISOString();
14255
+ const timestamp2 = new Date().toISOString();
12868
14256
  const result = await this.options.client.query(`/* todos:delete-plan-atomic */ WITH target AS (
12869
14257
  SELECT payload FROM ${this.tableName}
12870
14258
  WHERE service = $1 AND object_type = 'plans' AND object_id = $2 AND deleted_at IS NULL
@@ -12894,7 +14282,7 @@ class PostgresJsonRecordStore {
12894
14282
  RETURNING 1
12895
14283
  ) SELECT
12896
14284
  EXISTS (SELECT 1 FROM target) AS found,
12897
- (SELECT count(*) FROM updated_tasks) AS tasks_updated`, [this.service, id, timestamp, this.machineId(context)]);
14285
+ (SELECT count(*) FROM updated_tasks) AS tasks_updated`, [this.service, id, timestamp2, this.machineId(context)]);
12898
14286
  return Boolean(result.rows[0]?.found);
12899
14287
  }
12900
14288
  async tombstone(tombstone, context = {}) {
@@ -12955,7 +14343,7 @@ class PostgresJsonRecordStore {
12955
14343
  }
12956
14344
  }
12957
14345
  async function createTask2(input, store, context) {
12958
- const timestamp = new Date().toISOString();
14346
+ const timestamp2 = new Date().toISOString();
12959
14347
  const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
12960
14348
  const task = {
12961
14349
  id: randomUUID3(),
@@ -12977,8 +14365,8 @@ async function createTask2(input, store, context) {
12977
14365
  version: 1,
12978
14366
  locked_by: null,
12979
14367
  locked_at: null,
12980
- created_at: timestamp,
12981
- updated_at: timestamp,
14368
+ created_at: timestamp2,
14369
+ updated_at: timestamp2,
12982
14370
  started_at: null,
12983
14371
  completed_at: null,
12984
14372
  due_at: input.due_at ?? null,
@@ -13110,9 +14498,9 @@ async function lockTask2(id, agentId, store) {
13110
14498
  if (task.locked_by && task.locked_by !== agentId && !cloudLockExpired(task.locked_at)) {
13111
14499
  return { success: false, locked_by: task.locked_by, locked_at: task.locked_at ?? undefined, error: `Task is locked by ${task.locked_by}` };
13112
14500
  }
13113
- const timestamp = new Date().toISOString();
13114
- await patchTask(task, { locked_by: agentId, locked_at: timestamp }, store);
13115
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: cloudLockExpiresAt(timestamp) };
14501
+ const timestamp2 = new Date().toISOString();
14502
+ await patchTask(task, { locked_by: agentId, locked_at: timestamp2 }, store);
14503
+ return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: cloudLockExpiresAt(timestamp2) };
13116
14504
  }
13117
14505
  async function unlockTask2(id, agentId, store) {
13118
14506
  const task = await requireRecord("tasks", id, store);
@@ -13151,8 +14539,8 @@ async function addDependency2(taskId, dependsOn, store, context) {
13151
14539
  for (const next of adjacency.get(node) ?? [])
13152
14540
  queue.push(next);
13153
14541
  }
13154
- const timestamp = new Date().toISOString();
13155
- const record = { id: dependencyId(taskId, dependsOn), task_id: taskId, depends_on: dependsOn, created_at: timestamp, updated_at: timestamp };
14542
+ const timestamp2 = new Date().toISOString();
14543
+ const record = { id: dependencyId(taskId, dependsOn), task_id: taskId, depends_on: dependsOn, created_at: timestamp2, updated_at: timestamp2 };
13156
14544
  await store.upsert("dependencies", record, context);
13157
14545
  return { task_id: taskId, depends_on: dependsOn };
13158
14546
  }
@@ -13173,7 +14561,7 @@ async function listDependencies(taskId, store) {
13173
14561
  async function addVerification(input, store, context) {
13174
14562
  if (!await store.get("tasks", input.task_id))
13175
14563
  throw new Error(`Task not found: ${input.task_id}`);
13176
- const timestamp = new Date().toISOString();
14564
+ const timestamp2 = new Date().toISOString();
13177
14565
  const verification = {
13178
14566
  id: randomUUID3(),
13179
14567
  task_id: input.task_id,
@@ -13182,10 +14570,10 @@ async function addVerification(input, store, context) {
13182
14570
  output_summary: input.output_summary ?? null,
13183
14571
  artifact_path: input.artifact_path ?? null,
13184
14572
  agent_id: input.agent_id ?? context?.agentId ?? null,
13185
- run_at: timestamp,
13186
- created_at: timestamp
14573
+ run_at: timestamp2,
14574
+ created_at: timestamp2
13187
14575
  };
13188
- await store.upsert("verifications", { ...verification, updated_at: timestamp }, context);
14576
+ await store.upsert("verifications", { ...verification, updated_at: timestamp2 }, context);
13189
14577
  return verification;
13190
14578
  }
13191
14579
  async function listVerifications(taskId, store) {
@@ -13194,7 +14582,7 @@ async function listVerifications(taskId, store) {
13194
14582
  async function addCommit(input, store, context) {
13195
14583
  if (!await store.get("tasks", input.task_id))
13196
14584
  throw new Error(`Task not found: ${input.task_id}`);
13197
- const timestamp = new Date().toISOString();
14585
+ const timestamp2 = new Date().toISOString();
13198
14586
  const commit = {
13199
14587
  id: randomUUID3(),
13200
14588
  task_id: input.task_id,
@@ -13202,9 +14590,9 @@ async function addCommit(input, store, context) {
13202
14590
  message: input.message ?? null,
13203
14591
  author: input.author ?? null,
13204
14592
  files_changed: input.files_changed ?? null,
13205
- created_at: timestamp
14593
+ created_at: timestamp2
13206
14594
  };
13207
- await store.upsert("commits", { ...commit, updated_at: timestamp }, context);
14595
+ await store.upsert("commits", { ...commit, updated_at: timestamp2 }, context);
13208
14596
  return commit;
13209
14597
  }
13210
14598
  async function listCommits(taskId, store) {
@@ -13217,7 +14605,7 @@ async function findCommit(sha, store) {
13217
14605
  async function addGitRef(input, store, context) {
13218
14606
  if (!await store.get("tasks", input.task_id))
13219
14607
  throw new Error(`Task not found: ${input.task_id}`);
13220
- const timestamp = new Date().toISOString();
14608
+ const timestamp2 = new Date().toISOString();
13221
14609
  const gitRef = {
13222
14610
  id: randomUUID3(),
13223
14611
  task_id: input.task_id,
@@ -13226,8 +14614,8 @@ async function addGitRef(input, store, context) {
13226
14614
  url: input.url ?? null,
13227
14615
  provider: input.provider ?? null,
13228
14616
  metadata: input.metadata ?? {},
13229
- created_at: timestamp,
13230
- updated_at: timestamp
14617
+ created_at: timestamp2,
14618
+ updated_at: timestamp2
13231
14619
  };
13232
14620
  await store.upsert("refs", gitRef, context);
13233
14621
  return gitRef;
@@ -13238,7 +14626,8 @@ async function listGitRefs(taskId, store) {
13238
14626
  async function findGitRefs(ref, store) {
13239
14627
  return (await store.list("refs")).filter((r) => r.name === ref).sort((a, b) => b.created_at.localeCompare(a.created_at));
13240
14628
  }
13241
- var TASK_ORDER_BY = "ORDER BY CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC";
14629
+ var TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC";
14630
+ var TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
13242
14631
  function toFilterArray(value) {
13243
14632
  return Array.isArray(value) ? value : [value];
13244
14633
  }
@@ -13284,7 +14673,7 @@ async function getChangedSince(since, filters, store) {
13284
14673
  return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
13285
14674
  }
13286
14675
  async function createProject2(input, store, context) {
13287
- const timestamp = new Date().toISOString();
14676
+ const timestamp2 = new Date().toISOString();
13288
14677
  const derivedSlug = slugifyRaw(input.name);
13289
14678
  const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugifyRaw(input.task_list_id);
13290
14679
  if (!derivedSlug || !taskListId)
@@ -13297,8 +14686,8 @@ async function createProject2(input, store, context) {
13297
14686
  task_list_id: taskListId,
13298
14687
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
13299
14688
  task_counter: 0,
13300
- created_at: timestamp,
13301
- updated_at: timestamp,
14689
+ created_at: timestamp2,
14690
+ updated_at: timestamp2,
13302
14691
  machine_id: store.machineId(context),
13303
14692
  synced_at: null
13304
14693
  };
@@ -13313,7 +14702,7 @@ async function updateProject2(id, input, store) {
13313
14702
  return store.upsert("projects", updated);
13314
14703
  }
13315
14704
  async function createPlan2(input, store, context) {
13316
- const timestamp = new Date().toISOString();
14705
+ const timestamp2 = new Date().toISOString();
13317
14706
  const projectId = input.project_id ?? context?.projectId ?? null;
13318
14707
  const slug = await resolvePostgresPlanSlug({
13319
14708
  name: input.name,
@@ -13330,8 +14719,8 @@ async function createPlan2(input, store, context) {
13330
14719
  name: input.name,
13331
14720
  description: input.description ?? null,
13332
14721
  status: input.status ?? "active",
13333
- created_at: timestamp,
13334
- updated_at: timestamp,
14722
+ created_at: timestamp2,
14723
+ updated_at: timestamp2,
13335
14724
  machine_id: store.machineId(context),
13336
14725
  synced_at: null
13337
14726
  }, context);
@@ -13355,7 +14744,7 @@ async function registerAgent2(input, store, context) {
13355
14744
  if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
13356
14745
  return { conflict: true, message: `Agent name '${input.name}' is already active` };
13357
14746
  }
13358
- const timestamp = new Date().toISOString();
14747
+ const timestamp2 = new Date().toISOString();
13359
14748
  const agent = {
13360
14749
  id: existing?.id ?? randomUUID3().slice(0, 8),
13361
14750
  name: input.name,
@@ -13369,8 +14758,8 @@ async function registerAgent2(input, store, context) {
13369
14758
  capabilities: input.capabilities ?? existing?.capabilities ?? [],
13370
14759
  status: "active",
13371
14760
  metadata: input.metadata ?? existing?.metadata ?? {},
13372
- created_at: existing?.created_at ?? timestamp,
13373
- last_seen_at: timestamp,
14761
+ created_at: existing?.created_at ?? timestamp2,
14762
+ last_seen_at: timestamp2,
13374
14763
  session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
13375
14764
  working_dir: input.working_dir ?? existing?.working_dir ?? null,
13376
14765
  active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
@@ -13415,7 +14804,7 @@ async function releaseAgent2(idOrName, sessionId, store, context) {
13415
14804
  return { agent: updated, released: true };
13416
14805
  }
13417
14806
  async function createTaskList2(input, store, context) {
13418
- const timestamp = new Date().toISOString();
14807
+ const timestamp2 = new Date().toISOString();
13419
14808
  const slug = slugifyRaw(input.slug === undefined ? input.name : input.slug);
13420
14809
  if (!slug)
13421
14810
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
@@ -13426,8 +14815,8 @@ async function createTaskList2(input, store, context) {
13426
14815
  name: input.name,
13427
14816
  description: input.description ?? null,
13428
14817
  metadata: input.metadata ?? {},
13429
- created_at: timestamp,
13430
- updated_at: timestamp,
14818
+ created_at: timestamp2,
14819
+ updated_at: timestamp2,
13431
14820
  machine_id: store.machineId(context),
13432
14821
  synced_at: null
13433
14822
  }, context);
@@ -13453,8 +14842,8 @@ async function updateTaskList2(id, input, store) {
13453
14842
  });
13454
14843
  }
13455
14844
  async function createTemplate2(input, store, context) {
13456
- const timestamp = new Date().toISOString();
13457
- return store.upsert("templates", {
14845
+ const timestamp2 = new Date().toISOString();
14846
+ const template = {
13458
14847
  id: randomUUID3(),
13459
14848
  name: input.name,
13460
14849
  title_pattern: input.title_pattern,
@@ -13466,10 +14855,33 @@ async function createTemplate2(input, store, context) {
13466
14855
  project_id: input.project_id ?? context?.projectId ?? null,
13467
14856
  plan_id: input.plan_id ?? null,
13468
14857
  metadata: input.metadata ?? {},
13469
- created_at: timestamp,
14858
+ created_at: timestamp2,
13470
14859
  machine_id: store.machineId(context),
13471
14860
  synced_at: null
13472
- }, context);
14861
+ };
14862
+ const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp2);
14863
+ await store.createTemplateWithTasks(template, tasks, context);
14864
+ return template;
14865
+ }
14866
+ function buildTemplateTasks(templateId, inputs, timestamp2) {
14867
+ return inputs.map((input, position) => ({
14868
+ id: randomUUID3(),
14869
+ template_id: templateId,
14870
+ position,
14871
+ title_pattern: input.title_pattern,
14872
+ description: input.description ?? null,
14873
+ priority: input.priority ?? "medium",
14874
+ tags: input.tags ?? [],
14875
+ task_type: input.task_type ?? null,
14876
+ condition: input.condition ?? null,
14877
+ include_template_id: input.include_template_id ?? null,
14878
+ depends_on_positions: input.depends_on ?? [],
14879
+ metadata: input.metadata ?? {},
14880
+ created_at: timestamp2
14881
+ }));
14882
+ }
14883
+ async function deleteTemplate2(id, store, context) {
14884
+ return store.deleteTemplateWithTasks(id, context);
13473
14885
  }
13474
14886
  async function updateTemplate2(id, input, store) {
13475
14887
  const template = await store.get("templates", id);
@@ -13525,6 +14937,7 @@ async function exportSnapshot(store) {
13525
14937
  agents: await store.list("agents"),
13526
14938
  taskLists: await store.list("task_lists"),
13527
14939
  templates: await store.list("templates"),
14940
+ templateTasks: await store.list("template_tasks"),
13528
14941
  auditHistory: await store.list("audit_history"),
13529
14942
  tombstones: await store.listTombstones()
13530
14943
  };
@@ -13549,6 +14962,7 @@ async function importSnapshot(snapshot, store, context) {
13549
14962
  ...snapshot.agents.map((row) => ["agents", row]),
13550
14963
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
13551
14964
  ...snapshot.templates.map((row) => ["templates", row]),
14965
+ ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
13552
14966
  ...snapshot.auditHistory.map((row) => ["audit_history", row])
13553
14967
  ];
13554
14968
  for (const [type, row] of entries) {
@@ -13694,6 +15108,7 @@ var SNAPSHOT_TO_OBJECT_TYPE = {
13694
15108
  agents: "agents",
13695
15109
  taskLists: "task_lists",
13696
15110
  templates: "templates",
15111
+ templateTasks: "template_tasks",
13697
15112
  auditHistory: "audit_history"
13698
15113
  };
13699
15114
 
@@ -13885,6 +15300,7 @@ function emptySnapshot() {
13885
15300
  agents: [],
13886
15301
  taskLists: [],
13887
15302
  templates: [],
15303
+ templateTasks: [],
13888
15304
  auditHistory: [],
13889
15305
  tombstones: []
13890
15306
  };
@@ -14088,10 +15504,23 @@ function createTodosCloudQueryClient(url, options = {}) {
14088
15504
  idleTimeout: options.idleTimeout ?? 20,
14089
15505
  connectionTimeout: options.connectionTimeout ?? 15
14090
15506
  });
14091
- return {
14092
- async query(text, values = []) {
14093
- const result = await sql.unsafe(text, values.length ? [...values] : []);
14094
- return { rows: toRows(result) };
15507
+ const queryWith = async (handle, text, values = []) => {
15508
+ const result = await handle.unsafe(text, values.length ? [...values] : []);
15509
+ return { rows: toRows(result) };
15510
+ };
15511
+ const client = {
15512
+ query(text, values = []) {
15513
+ return queryWith(sql, text, values);
15514
+ },
15515
+ async transaction(fn) {
15516
+ if (typeof sql.begin !== "function") {
15517
+ throw new Error("PR_GROUP_ATOMICITY_UNAVAILABLE: Bun.SQL transaction support is required");
15518
+ }
15519
+ return sql.begin(async (handle) => fn({
15520
+ query(text, values = []) {
15521
+ return queryWith(handle, text, values);
15522
+ }
15523
+ }));
14095
15524
  },
14096
15525
  async close() {
14097
15526
  if (typeof sql.end === "function")
@@ -14100,6 +15529,7 @@ function createTodosCloudQueryClient(url, options = {}) {
14100
15529
  await sql.close();
14101
15530
  }
14102
15531
  };
15532
+ return client;
14103
15533
  }
14104
15534
  function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
14105
15535
  const url = getTodosStorageDatabaseUrl(env);
@@ -14341,6 +15771,9 @@ class TodosShadowOutbox {
14341
15771
  case "templates":
14342
15772
  snapshot.templates.push(record);
14343
15773
  break;
15774
+ case "template_tasks":
15775
+ snapshot.templateTasks.push(record);
15776
+ break;
14344
15777
  case "audit_history":
14345
15778
  snapshot.auditHistory.push(record);
14346
15779
  break;
@@ -14407,6 +15840,7 @@ function emptySnapshot2() {
14407
15840
  agents: [],
14408
15841
  taskLists: [],
14409
15842
  templates: [],
15843
+ templateTasks: [],
14410
15844
  auditHistory: [],
14411
15845
  tombstones: []
14412
15846
  };
@@ -15065,7 +16499,7 @@ export {
15065
16499
  PostgresScopedSlugMigrationConflictError,
15066
16500
  PostgresScopedSlugIndexBuildError,
15067
16501
  COMMENT_REDACTION_BACKFILL_CONFIRMATION,
15068
- CANONICAL_TODOS_RDS_RUNTIME_PATH,
16502
+ CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
15069
16503
  CANONICAL_TODOS_RDS_DATABASE,
15070
- CANONICAL_TODOS_RDS_CLUSTER
16504
+ CANONICAL_TODOS_RDS_CLUSTER_ENV
15071
16505
  };