@hasna/todos 0.11.95 → 0.12.2

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 (111) hide show
  1. package/README.md +25 -5
  2. package/dist/cli/cloud-router.d.ts +5 -0
  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 +7891 -1907
  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 +1638 -502
  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/task-commits.d.ts.map +1 -1
  38. package/dist/db/task-crud.d.ts.map +1 -1
  39. package/dist/db/task-lifecycle.d.ts.map +1 -1
  40. package/dist/index.d.ts +5 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4621 -833
  43. package/dist/json-contracts.d.ts +4 -2
  44. package/dist/json-contracts.d.ts.map +1 -1
  45. package/dist/lib/activity-audit.d.ts.map +1 -1
  46. package/dist/lib/artifact-store.d.ts.map +1 -1
  47. package/dist/lib/cli-help.d.ts +14 -3
  48. package/dist/lib/cli-help.d.ts.map +1 -1
  49. package/dist/lib/evidence-redaction.d.ts +72 -0
  50. package/dist/lib/evidence-redaction.d.ts.map +1 -0
  51. package/dist/lib/headless-boundaries.d.ts +1 -1
  52. package/dist/lib/headless-boundaries.d.ts.map +1 -1
  53. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  54. package/dist/lib/local-bridge.d.ts +1 -0
  55. package/dist/lib/local-bridge.d.ts.map +1 -1
  56. package/dist/lib/local-encryption.d.ts.map +1 -1
  57. package/dist/lib/prewrite-secrets.d.ts +29 -0
  58. package/dist/lib/prewrite-secrets.d.ts.map +1 -0
  59. package/dist/lib/redaction.d.ts.map +1 -1
  60. package/dist/lib/search.d.ts +3 -0
  61. package/dist/lib/search.d.ts.map +1 -1
  62. package/dist/lib/task-route-sources.d.ts +2 -0
  63. package/dist/lib/task-route-sources.d.ts.map +1 -1
  64. package/dist/lib/task-routing.d.ts.map +1 -1
  65. package/dist/mcp/index.js +6183 -1016
  66. package/dist/mcp.js +1 -1
  67. package/dist/pr-groups/http-client.d.ts +24 -0
  68. package/dist/pr-groups/http-client.d.ts.map +1 -0
  69. package/dist/pr-groups/index.d.ts +9 -0
  70. package/dist/pr-groups/index.d.ts.map +1 -0
  71. package/dist/pr-groups/ledger.d.ts +20 -0
  72. package/dist/pr-groups/ledger.d.ts.map +1 -0
  73. package/dist/pr-groups/postgres.d.ts +21 -0
  74. package/dist/pr-groups/postgres.d.ts.map +1 -0
  75. package/dist/pr-groups/sqlite.d.ts +16 -0
  76. package/dist/pr-groups/sqlite.d.ts.map +1 -0
  77. package/dist/pr-groups/types.d.ts +320 -0
  78. package/dist/pr-groups/types.d.ts.map +1 -0
  79. package/dist/registry.js +1638 -502
  80. package/dist/release-provenance.json +5 -5
  81. package/dist/sdk/client.d.ts +16 -0
  82. package/dist/sdk/client.d.ts.map +1 -1
  83. package/dist/sdk/index.d.ts +2 -1
  84. package/dist/sdk/index.d.ts.map +1 -1
  85. package/dist/sdk/index.js +64 -0
  86. package/dist/sdk/v1.generated.d.ts +285 -0
  87. package/dist/sdk/v1.generated.d.ts.map +1 -1
  88. package/dist/server/cloud.d.ts +3 -0
  89. package/dist/server/cloud.d.ts.map +1 -1
  90. package/dist/server/index.js +6079 -912
  91. package/dist/server/openapi.d.ts +1254 -0
  92. package/dist/server/openapi.d.ts.map +1 -1
  93. package/dist/server/pr-groups.d.ts +7 -0
  94. package/dist/server/pr-groups.d.ts.map +1 -0
  95. package/dist/server/serve.d.ts.map +1 -1
  96. package/dist/server/v1.d.ts +2 -1
  97. package/dist/server/v1.d.ts.map +1 -1
  98. package/dist/storage/cloud-client.d.ts +1 -0
  99. package/dist/storage/cloud-client.d.ts.map +1 -1
  100. package/dist/storage/config.d.ts +5 -5
  101. package/dist/storage/config.d.ts.map +1 -1
  102. package/dist/storage/index.d.ts +1 -1
  103. package/dist/storage/index.d.ts.map +1 -1
  104. package/dist/storage/local-sqlite.d.ts.map +1 -1
  105. package/dist/storage/postgres-sync.d.ts.map +1 -1
  106. package/dist/storage.d.ts +1 -1
  107. package/dist/storage.d.ts.map +1 -1
  108. package/dist/storage.js +1860 -540
  109. package/dist/types/index.d.ts +90 -0
  110. package/dist/types/index.d.ts.map +1 -1
  111. package/package.json +1 -1
package/dist/contracts.js CHANGED
@@ -1203,27 +1203,478 @@ var init_migrations = __esm(() => {
1203
1203
  ALTER TABLE plans ADD COLUMN slug TEXT;
1204
1204
  CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
1205
1205
  INSERT OR IGNORE INTO _migrations (id) VALUES (64);
1206
+ `,
1207
+ `
1208
+ ALTER TABLE agents ADD COLUMN identity_id TEXT;
1209
+ ALTER TABLE agents ADD COLUMN project_id TEXT;
1210
+ ALTER TABLE agents ADD COLUMN runtime_instance_id TEXT;
1211
+
1212
+ CREATE TABLE IF NOT EXISTS agent_identity_source_mappings (
1213
+ id TEXT PRIMARY KEY,
1214
+ local_agent_id TEXT REFERENCES agents(id) ON DELETE RESTRICT,
1215
+ identity_id TEXT,
1216
+ source_authority TEXT NOT NULL CHECK(length(trim(source_authority)) > 0),
1217
+ source_tenant_id TEXT NOT NULL CHECK(length(trim(source_tenant_id)) > 0),
1218
+ source_namespace TEXT NOT NULL CHECK(length(trim(source_namespace)) > 0),
1219
+ source_entity_type TEXT NOT NULL CHECK(length(trim(source_entity_type)) > 0),
1220
+ source_record_id TEXT NOT NULL CHECK(length(trim(source_record_id)) > 0),
1221
+ observed_label TEXT,
1222
+ evidence TEXT NOT NULL DEFAULT '{}',
1223
+ mapping_basis TEXT NOT NULL CHECK(mapping_basis IN ('authoritative', 'imported', 'candidate', 'name_similarity')),
1224
+ status TEXT NOT NULL CHECK(status IN ('active', 'retired', 'quarantined', 'revoked')),
1225
+ revision INTEGER NOT NULL CHECK(revision > 0),
1226
+ created_at TEXT NOT NULL,
1227
+ updated_at TEXT NOT NULL,
1228
+ CHECK(mapping_basis IN ('authoritative', 'imported') OR status IN ('quarantined', 'revoked')),
1229
+ CHECK(status != 'active' OR (mapping_basis IN ('authoritative', 'imported') AND identity_id IS NOT NULL))
1230
+ );
1231
+ DROP INDEX IF EXISTS idx_agent_identity_source_active_unique;
1232
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_identity_source_revision_unique
1233
+ ON agent_identity_source_mappings (
1234
+ source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id, revision
1235
+ )
1236
+ WHERE mapping_basis IN ('authoritative', 'imported');
1237
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_identity
1238
+ ON agent_identity_source_mappings(identity_id);
1239
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_local
1240
+ ON agent_identity_source_mappings(local_agent_id);
1241
+ CREATE TABLE IF NOT EXISTS agent_identity_aliases (
1242
+ id TEXT PRIMARY KEY,
1243
+ local_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,
1244
+ label TEXT NOT NULL,
1245
+ normalized_label TEXT NOT NULL,
1246
+ alias_kind TEXT NOT NULL CHECK(alias_kind IN ('historical', 'candidate')),
1247
+ status TEXT NOT NULL CHECK(status IN ('active', 'quarantined', 'revoked')),
1248
+ created_at TEXT NOT NULL,
1249
+ UNIQUE(local_agent_id, normalized_label)
1250
+ );
1251
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_alias_lookup
1252
+ ON agent_identity_aliases(normalized_label, status);
1253
+
1254
+ CREATE TRIGGER IF NOT EXISTS trg_agents_identity_id_immutable
1255
+ BEFORE UPDATE OF identity_id ON agents
1256
+ WHEN OLD.identity_id IS NOT NULL AND OLD.identity_id IS NOT NEW.identity_id
1257
+ BEGIN
1258
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
1259
+ END;
1260
+
1261
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_identity_immutable
1262
+ BEFORE UPDATE OF identity_id ON agent_identity_source_mappings
1263
+ WHEN OLD.identity_id IS NOT NEW.identity_id
1264
+ BEGIN
1265
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
1266
+ END;
1267
+
1268
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_lineage_immutable
1269
+ BEFORE UPDATE OF source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id
1270
+ ON agent_identity_source_mappings
1271
+ WHEN OLD.source_authority IS NOT NEW.source_authority
1272
+ OR OLD.source_tenant_id IS NOT NEW.source_tenant_id
1273
+ OR OLD.source_namespace IS NOT NEW.source_namespace
1274
+ OR OLD.source_entity_type IS NOT NEW.source_entity_type
1275
+ OR OLD.source_record_id IS NOT NEW.source_record_id
1276
+ BEGIN
1277
+ SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
1278
+ END;
1279
+
1280
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
1281
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
1282
+ ON agent_identity_source_mappings
1283
+ BEGIN
1284
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
1285
+ END;
1286
+
1287
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
1288
+ BEFORE DELETE ON agent_identity_source_mappings
1289
+ BEGIN
1290
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
1291
+ END;
1292
+
1293
+ INSERT OR IGNORE INTO _migrations (id) VALUES (65);
1294
+ `,
1295
+ `
1296
+ CREATE TABLE IF NOT EXISTS pr_groups (
1297
+ schema_version INTEGER NOT NULL DEFAULT 1,
1298
+ id TEXT PRIMARY KEY,
1299
+ identity_key TEXT NOT NULL UNIQUE,
1300
+ root_request_id TEXT NOT NULL,
1301
+ repository TEXT NOT NULL,
1302
+ state TEXT NOT NULL,
1303
+ active_attempt_id TEXT,
1304
+ active_generation TEXT,
1305
+ terminal_attempt_id TEXT,
1306
+ terminal_generation TEXT,
1307
+ terminal_outcome TEXT,
1308
+ terminal_head_sha TEXT,
1309
+ terminal_at TEXT,
1310
+ cleanup_eligible_at TEXT,
1311
+ revision INTEGER NOT NULL DEFAULT 1,
1312
+ created_at TEXT NOT NULL,
1313
+ updated_at TEXT NOT NULL
1314
+ );
1315
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_groups_identity ON pr_groups(identity_key);
1316
+ CREATE INDEX IF NOT EXISTS idx_pr_groups_root_repository ON pr_groups(root_request_id, repository);
1317
+ CREATE INDEX IF NOT EXISTS idx_pr_groups_active_generation ON pr_groups(active_generation);
1318
+
1319
+ CREATE TABLE IF NOT EXISTS pr_group_attempts (
1320
+ schema_version INTEGER NOT NULL DEFAULT 1,
1321
+ id TEXT PRIMARY KEY,
1322
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1323
+ leaf_task_id TEXT NOT NULL,
1324
+ dispatch_attempt TEXT NOT NULL,
1325
+ writer_generation TEXT NOT NULL,
1326
+ previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
1327
+ worktree TEXT NOT NULL,
1328
+ branch TEXT NOT NULL,
1329
+ provider TEXT,
1330
+ provider_run_id TEXT,
1331
+ profile_alias TEXT,
1332
+ status TEXT NOT NULL,
1333
+ admitted_at TEXT NOT NULL,
1334
+ started_at TEXT,
1335
+ last_heartbeat_at TEXT,
1336
+ handed_off_at TEXT,
1337
+ fenced_at TEXT,
1338
+ terminal_at TEXT,
1339
+ created_at TEXT NOT NULL,
1340
+ updated_at TEXT NOT NULL,
1341
+ UNIQUE(group_id, leaf_task_id, dispatch_attempt),
1342
+ UNIQUE(group_id, writer_generation)
1343
+ );
1344
+ CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id);
1345
+ CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation);
1346
+
1347
+ CREATE TABLE IF NOT EXISTS pr_group_events (
1348
+ schema_version INTEGER NOT NULL DEFAULT 1,
1349
+ id TEXT PRIMARY KEY,
1350
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1351
+ attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
1352
+ writer_generation TEXT NOT NULL,
1353
+ sequence INTEGER NOT NULL,
1354
+ idempotency_key TEXT NOT NULL,
1355
+ event_type TEXT NOT NULL,
1356
+ state TEXT NOT NULL,
1357
+ message TEXT,
1358
+ head_sha TEXT,
1359
+ receipt_key TEXT,
1360
+ outcome TEXT,
1361
+ metadata TEXT NOT NULL DEFAULT '{}',
1362
+ payload_hash TEXT NOT NULL,
1363
+ created_at TEXT NOT NULL,
1364
+ UNIQUE(group_id, sequence),
1365
+ UNIQUE(group_id, idempotency_key),
1366
+ UNIQUE(group_id, receipt_key)
1367
+ );
1368
+ CREATE INDEX IF NOT EXISTS idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence);
1369
+ CREATE INDEX IF NOT EXISTS idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence);
1370
+ CREATE INDEX IF NOT EXISTS idx_pr_group_events_receipt ON pr_group_events(group_id, receipt_key);
1371
+ INSERT OR IGNORE INTO _migrations (id) VALUES (66);
1372
+ `,
1373
+ `
1374
+ ALTER TABLE pr_groups ADD COLUMN leaf_task_id TEXT;
1375
+ ALTER TABLE pr_groups ADD COLUMN branch TEXT;
1376
+ ALTER TABLE pr_groups ADD COLUMN pr_number INTEGER;
1377
+ ALTER TABLE pr_groups ADD COLUMN base_sha TEXT;
1378
+ ALTER TABLE pr_groups ADD COLUMN repair_cycle_count INTEGER NOT NULL DEFAULT 0;
1379
+ ALTER TABLE pr_groups ADD COLUMN repair_cycle_limit INTEGER NOT NULL DEFAULT 2;
1380
+ ALTER TABLE pr_group_attempts ADD COLUMN repository TEXT;
1381
+ ALTER TABLE pr_group_attempts ADD COLUMN pr_number INTEGER;
1382
+ ALTER TABLE pr_group_attempts ADD COLUMN base_sha TEXT;
1383
+ ALTER TABLE pr_group_events ADD COLUMN review_receipt_key TEXT;
1384
+ ALTER TABLE pr_group_events ADD COLUMN conditional_merge_receipt_key TEXT;
1385
+ ALTER TABLE pr_group_events ADD COLUMN repository TEXT;
1386
+ ALTER TABLE pr_group_events ADD COLUMN pr_number INTEGER;
1387
+ ALTER TABLE pr_group_events ADD COLUMN base_sha TEXT;
1388
+ ALTER TABLE pr_group_events ADD COLUMN actor_id TEXT;
1389
+ ALTER TABLE pr_group_events ADD COLUMN actor_run_id TEXT;
1390
+ ALTER TABLE pr_group_events ADD COLUMN expected_reviewer_id TEXT;
1391
+ ALTER TABLE pr_group_events ADD COLUMN expected_reviewer_run_id TEXT;
1392
+ ALTER TABLE pr_group_events ADD COLUMN repair_cycle INTEGER;
1393
+ ALTER TABLE pr_group_events ADD COLUMN cleanup_proof TEXT;
1394
+
1395
+ UPDATE pr_groups
1396
+ SET leaf_task_id = COALESCE(leaf_task_id, (
1397
+ SELECT attempt.leaf_task_id
1398
+ FROM pr_group_attempts AS attempt
1399
+ WHERE attempt.group_id = pr_groups.id
1400
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1401
+ attempt.created_at ASC, attempt.id ASC
1402
+ LIMIT 1
1403
+ )),
1404
+ branch = COALESCE(branch, (
1405
+ SELECT attempt.branch
1406
+ FROM pr_group_attempts AS attempt
1407
+ WHERE attempt.group_id = pr_groups.id
1408
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1409
+ attempt.created_at ASC, attempt.id ASC
1410
+ LIMIT 1
1411
+ ));
1412
+
1413
+ UPDATE pr_group_attempts
1414
+ SET repository = COALESCE(repository, (
1415
+ SELECT groups.repository FROM pr_groups AS groups
1416
+ WHERE groups.id = pr_group_attempts.group_id
1417
+ )),
1418
+ pr_number = COALESCE(pr_number, (
1419
+ SELECT groups.pr_number FROM pr_groups AS groups
1420
+ WHERE groups.id = pr_group_attempts.group_id
1421
+ )),
1422
+ base_sha = COALESCE(base_sha, (
1423
+ SELECT groups.base_sha FROM pr_groups AS groups
1424
+ WHERE groups.id = pr_group_attempts.group_id
1425
+ ));
1426
+
1427
+ UPDATE pr_groups
1428
+ SET pr_number = COALESCE(pr_number, (
1429
+ SELECT attempt.pr_number
1430
+ FROM pr_group_attempts AS attempt
1431
+ WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
1432
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1433
+ attempt.created_at ASC, attempt.id ASC
1434
+ LIMIT 1
1435
+ )),
1436
+ base_sha = COALESCE(base_sha, (
1437
+ SELECT attempt.base_sha
1438
+ FROM pr_group_attempts AS attempt
1439
+ WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
1440
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1441
+ attempt.created_at ASC, attempt.id ASC
1442
+ LIMIT 1
1443
+ ));
1444
+
1445
+ UPDATE pr_group_attempts
1446
+ SET pr_number = COALESCE(pr_number, (
1447
+ SELECT groups.pr_number FROM pr_groups AS groups
1448
+ WHERE groups.id = pr_group_attempts.group_id
1449
+ )),
1450
+ base_sha = COALESCE(base_sha, (
1451
+ SELECT groups.base_sha FROM pr_groups AS groups
1452
+ WHERE groups.id = pr_group_attempts.group_id
1453
+ ));
1454
+
1455
+ UPDATE pr_group_events
1456
+ SET repository = COALESCE(repository, (
1457
+ SELECT attempt.repository FROM pr_group_attempts AS attempt
1458
+ WHERE attempt.id = pr_group_events.attempt_id
1459
+ ), (
1460
+ SELECT groups.repository FROM pr_groups AS groups
1461
+ WHERE groups.id = pr_group_events.group_id
1462
+ )),
1463
+ pr_number = COALESCE(pr_number, (
1464
+ SELECT attempt.pr_number FROM pr_group_attempts AS attempt
1465
+ WHERE attempt.id = pr_group_events.attempt_id
1466
+ ), (
1467
+ SELECT groups.pr_number FROM pr_groups AS groups
1468
+ WHERE groups.id = pr_group_events.group_id
1469
+ )),
1470
+ base_sha = COALESCE(base_sha, (
1471
+ SELECT attempt.base_sha FROM pr_group_attempts AS attempt
1472
+ WHERE attempt.id = pr_group_events.attempt_id
1473
+ ), (
1474
+ SELECT groups.base_sha FROM pr_groups AS groups
1475
+ WHERE groups.id = pr_group_events.group_id
1476
+ ));
1477
+
1478
+ INSERT OR IGNORE INTO _migrations (id)
1479
+ SELECT 66
1480
+ WHERE NOT EXISTS (
1481
+ SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
1482
+ )
1483
+ AND NOT EXISTS (
1484
+ SELECT 1 FROM pr_group_attempts WHERE repository IS NULL
1485
+ )
1486
+ AND NOT EXISTS (
1487
+ SELECT 1 FROM pr_group_events WHERE repository IS NULL
1488
+ );
1489
+ `,
1490
+ `
1491
+ PRAGMA foreign_keys = OFF;
1492
+ BEGIN IMMEDIATE;
1493
+
1494
+ ALTER TABLE pr_group_events RENAME TO pr_group_events_v66;
1495
+ ALTER TABLE pr_group_attempts RENAME TO pr_group_attempts_v66;
1496
+ ALTER TABLE pr_groups RENAME TO pr_groups_v66;
1497
+
1498
+ CREATE TABLE pr_groups (
1499
+ schema_version INTEGER NOT NULL DEFAULT 1,
1500
+ id TEXT PRIMARY KEY,
1501
+ identity_key TEXT NOT NULL UNIQUE,
1502
+ root_request_id TEXT NOT NULL,
1503
+ repository TEXT NOT NULL,
1504
+ leaf_task_id TEXT NOT NULL,
1505
+ branch TEXT NOT NULL,
1506
+ pr_number INTEGER,
1507
+ base_sha TEXT,
1508
+ state TEXT NOT NULL,
1509
+ active_attempt_id TEXT,
1510
+ active_generation TEXT,
1511
+ repair_cycle_count INTEGER NOT NULL DEFAULT 0,
1512
+ repair_cycle_limit INTEGER NOT NULL DEFAULT 2,
1513
+ terminal_attempt_id TEXT,
1514
+ terminal_generation TEXT,
1515
+ terminal_outcome TEXT,
1516
+ terminal_head_sha TEXT,
1517
+ terminal_at TEXT,
1518
+ cleanup_eligible_at TEXT,
1519
+ revision INTEGER NOT NULL DEFAULT 1,
1520
+ created_at TEXT NOT NULL,
1521
+ updated_at TEXT NOT NULL
1522
+ );
1523
+
1524
+ CREATE TABLE pr_group_attempts (
1525
+ schema_version INTEGER NOT NULL DEFAULT 1,
1526
+ id TEXT PRIMARY KEY,
1527
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1528
+ leaf_task_id TEXT NOT NULL,
1529
+ dispatch_attempt TEXT NOT NULL,
1530
+ writer_generation TEXT NOT NULL,
1531
+ previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
1532
+ worktree TEXT NOT NULL,
1533
+ branch TEXT NOT NULL,
1534
+ repository TEXT NOT NULL,
1535
+ pr_number INTEGER,
1536
+ base_sha TEXT,
1537
+ provider TEXT,
1538
+ provider_run_id TEXT,
1539
+ profile_alias TEXT,
1540
+ status TEXT NOT NULL,
1541
+ admitted_at TEXT NOT NULL,
1542
+ started_at TEXT,
1543
+ last_heartbeat_at TEXT,
1544
+ handed_off_at TEXT,
1545
+ fenced_at TEXT,
1546
+ terminal_at TEXT,
1547
+ created_at TEXT NOT NULL,
1548
+ updated_at TEXT NOT NULL,
1549
+ UNIQUE(group_id, leaf_task_id, dispatch_attempt),
1550
+ UNIQUE(group_id, writer_generation)
1551
+ );
1552
+
1553
+ CREATE TABLE pr_group_events (
1554
+ schema_version INTEGER NOT NULL DEFAULT 1,
1555
+ id TEXT PRIMARY KEY,
1556
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
1557
+ attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
1558
+ writer_generation TEXT NOT NULL,
1559
+ sequence INTEGER NOT NULL,
1560
+ idempotency_key TEXT NOT NULL,
1561
+ event_type TEXT NOT NULL,
1562
+ state TEXT NOT NULL,
1563
+ message TEXT,
1564
+ head_sha TEXT,
1565
+ receipt_key TEXT UNIQUE,
1566
+ review_receipt_key TEXT,
1567
+ conditional_merge_receipt_key TEXT,
1568
+ outcome TEXT,
1569
+ repository TEXT NOT NULL,
1570
+ pr_number INTEGER,
1571
+ base_sha TEXT,
1572
+ actor_id TEXT,
1573
+ actor_run_id TEXT,
1574
+ expected_reviewer_id TEXT,
1575
+ expected_reviewer_run_id TEXT,
1576
+ repair_cycle INTEGER,
1577
+ ci_proof TEXT,
1578
+ cleanup_proof TEXT,
1579
+ metadata TEXT NOT NULL DEFAULT '{}',
1580
+ payload_hash TEXT NOT NULL,
1581
+ created_at TEXT NOT NULL,
1582
+ UNIQUE(group_id, sequence),
1583
+ UNIQUE(group_id, idempotency_key)
1584
+ );
1585
+
1586
+ INSERT INTO pr_groups (
1587
+ schema_version,id,identity_key,root_request_id,repository,leaf_task_id,branch,
1588
+ pr_number,base_sha,state,active_attempt_id,active_generation,
1589
+ repair_cycle_count,repair_cycle_limit,terminal_attempt_id,terminal_generation,
1590
+ terminal_outcome,terminal_head_sha,terminal_at,cleanup_eligible_at,
1591
+ revision,created_at,updated_at
1592
+ )
1593
+ SELECT
1594
+ schema_version,id,identity_key,root_request_id,repository,leaf_task_id,branch,
1595
+ pr_number,base_sha,state,active_attempt_id,active_generation,
1596
+ repair_cycle_count,repair_cycle_limit,terminal_attempt_id,terminal_generation,
1597
+ terminal_outcome,terminal_head_sha,terminal_at,cleanup_eligible_at,
1598
+ revision,created_at,updated_at
1599
+ FROM pr_groups_v66;
1600
+
1601
+ INSERT INTO pr_group_attempts (
1602
+ schema_version,id,group_id,leaf_task_id,dispatch_attempt,writer_generation,
1603
+ previous_attempt_id,worktree,branch,repository,pr_number,base_sha,
1604
+ provider,provider_run_id,profile_alias,status,admitted_at,started_at,
1605
+ last_heartbeat_at,handed_off_at,fenced_at,terminal_at,created_at,updated_at
1606
+ )
1607
+ SELECT
1608
+ schema_version,id,group_id,leaf_task_id,dispatch_attempt,writer_generation,
1609
+ previous_attempt_id,worktree,branch,repository,pr_number,base_sha,
1610
+ provider,provider_run_id,profile_alias,status,admitted_at,started_at,
1611
+ last_heartbeat_at,handed_off_at,fenced_at,terminal_at,created_at,updated_at
1612
+ FROM pr_group_attempts_v66;
1613
+
1614
+ INSERT INTO pr_group_events (
1615
+ schema_version,id,group_id,attempt_id,writer_generation,sequence,
1616
+ idempotency_key,event_type,state,message,head_sha,receipt_key,
1617
+ review_receipt_key,conditional_merge_receipt_key,outcome,repository,
1618
+ pr_number,base_sha,actor_id,actor_run_id,expected_reviewer_id,
1619
+ expected_reviewer_run_id,repair_cycle,ci_proof,cleanup_proof,
1620
+ metadata,payload_hash,created_at
1621
+ )
1622
+ SELECT
1623
+ schema_version,id,group_id,attempt_id,writer_generation,sequence,
1624
+ idempotency_key,event_type,state,message,head_sha,receipt_key,
1625
+ review_receipt_key,conditional_merge_receipt_key,outcome,repository,
1626
+ pr_number,base_sha,actor_id,actor_run_id,expected_reviewer_id,
1627
+ expected_reviewer_run_id,repair_cycle,NULL,cleanup_proof,
1628
+ metadata,payload_hash,created_at
1629
+ FROM pr_group_events_v66;
1630
+
1631
+ DROP TABLE pr_group_events_v66;
1632
+ DROP TABLE pr_group_attempts_v66;
1633
+ DROP TABLE pr_groups_v66;
1634
+
1635
+ CREATE UNIQUE INDEX idx_pr_groups_identity ON pr_groups(identity_key);
1636
+ CREATE INDEX idx_pr_groups_root_repository ON pr_groups(root_request_id, repository);
1637
+ CREATE INDEX idx_pr_groups_active_generation ON pr_groups(active_generation);
1638
+ CREATE INDEX idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id);
1639
+ CREATE INDEX idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation);
1640
+ CREATE INDEX idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence);
1641
+ CREATE INDEX idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence);
1642
+ CREATE INDEX idx_pr_group_events_receipt ON pr_group_events(receipt_key);
1643
+
1644
+ INSERT OR IGNORE INTO _migrations (id) VALUES (68);
1645
+ COMMIT;
1646
+ PRAGMA foreign_keys = ON;
1206
1647
  `
1207
1648
  ];
1208
1649
  });
1209
1650
 
1210
1651
  // src/db/schema.ts
1211
1652
  function runMigrations(db) {
1653
+ let strictMigrationFailure = null;
1654
+ const executeMigration = (migration, index) => {
1655
+ try {
1656
+ db.exec(migration);
1657
+ } catch (error) {
1658
+ try {
1659
+ db.exec("ROLLBACK");
1660
+ } catch {}
1661
+ if (index + 1 >= 68)
1662
+ strictMigrationFailure = error;
1663
+ }
1664
+ };
1212
1665
  try {
1213
1666
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
1214
1667
  const currentLevel = result?.max_id ?? 0;
1215
1668
  for (let i = currentLevel;i < MIGRATIONS.length; i++) {
1216
- try {
1217
- db.exec(MIGRATIONS[i]);
1218
- } catch {}
1669
+ executeMigration(MIGRATIONS[i], i);
1219
1670
  }
1220
1671
  } catch {
1221
- for (const migration of MIGRATIONS) {
1222
- try {
1223
- db.exec(migration);
1224
- } catch {}
1672
+ for (const [index, migration] of MIGRATIONS.entries()) {
1673
+ executeMigration(migration, index);
1225
1674
  }
1226
1675
  }
1676
+ if (strictMigrationFailure)
1677
+ throw strictMigrationFailure;
1227
1678
  ensureSchema(db);
1228
1679
  }
1229
1680
  function planSlugBase(value) {
@@ -2392,6 +2843,246 @@ function ensureSchema(db) {
2392
2843
  )`);
2393
2844
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
2394
2845
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
2846
+ ensureTable("pr_groups", `
2847
+ CREATE TABLE pr_groups (
2848
+ schema_version INTEGER NOT NULL DEFAULT 1,
2849
+ id TEXT PRIMARY KEY,
2850
+ identity_key TEXT NOT NULL UNIQUE,
2851
+ root_request_id TEXT NOT NULL,
2852
+ repository TEXT NOT NULL,
2853
+ leaf_task_id TEXT NOT NULL,
2854
+ branch TEXT NOT NULL,
2855
+ pr_number INTEGER,
2856
+ base_sha TEXT,
2857
+ state TEXT NOT NULL,
2858
+ active_attempt_id TEXT,
2859
+ active_generation TEXT,
2860
+ repair_cycle_count INTEGER NOT NULL DEFAULT 0,
2861
+ repair_cycle_limit INTEGER NOT NULL DEFAULT 2,
2862
+ terminal_attempt_id TEXT,
2863
+ terminal_generation TEXT,
2864
+ terminal_outcome TEXT,
2865
+ terminal_head_sha TEXT,
2866
+ terminal_at TEXT,
2867
+ cleanup_eligible_at TEXT,
2868
+ revision INTEGER NOT NULL DEFAULT 1,
2869
+ created_at TEXT NOT NULL,
2870
+ updated_at TEXT NOT NULL
2871
+ )`);
2872
+ ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_groups_identity ON pr_groups(identity_key)");
2873
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_groups_root_repository ON pr_groups(root_request_id, repository)");
2874
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_groups_active_generation ON pr_groups(active_generation)");
2875
+ ensureTable("pr_group_attempts", `
2876
+ CREATE TABLE pr_group_attempts (
2877
+ schema_version INTEGER NOT NULL DEFAULT 1,
2878
+ id TEXT PRIMARY KEY,
2879
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
2880
+ leaf_task_id TEXT NOT NULL,
2881
+ dispatch_attempt TEXT NOT NULL,
2882
+ writer_generation TEXT NOT NULL,
2883
+ previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
2884
+ worktree TEXT NOT NULL,
2885
+ branch TEXT NOT NULL,
2886
+ repository TEXT NOT NULL,
2887
+ pr_number INTEGER,
2888
+ base_sha TEXT,
2889
+ provider TEXT,
2890
+ provider_run_id TEXT,
2891
+ profile_alias TEXT,
2892
+ status TEXT NOT NULL,
2893
+ admitted_at TEXT NOT NULL,
2894
+ started_at TEXT,
2895
+ last_heartbeat_at TEXT,
2896
+ handed_off_at TEXT,
2897
+ fenced_at TEXT,
2898
+ terminal_at TEXT,
2899
+ created_at TEXT NOT NULL,
2900
+ updated_at TEXT NOT NULL,
2901
+ UNIQUE(group_id, leaf_task_id, dispatch_attempt),
2902
+ UNIQUE(group_id, writer_generation)
2903
+ )`);
2904
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id)");
2905
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation)");
2906
+ ensureTable("pr_group_events", `
2907
+ CREATE TABLE pr_group_events (
2908
+ schema_version INTEGER NOT NULL DEFAULT 1,
2909
+ id TEXT PRIMARY KEY,
2910
+ group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
2911
+ attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
2912
+ writer_generation TEXT NOT NULL,
2913
+ sequence INTEGER NOT NULL,
2914
+ idempotency_key TEXT NOT NULL,
2915
+ event_type TEXT NOT NULL,
2916
+ state TEXT NOT NULL,
2917
+ message TEXT,
2918
+ head_sha TEXT,
2919
+ receipt_key TEXT UNIQUE,
2920
+ review_receipt_key TEXT,
2921
+ conditional_merge_receipt_key TEXT,
2922
+ outcome TEXT,
2923
+ repository TEXT NOT NULL,
2924
+ pr_number INTEGER,
2925
+ base_sha TEXT,
2926
+ actor_id TEXT,
2927
+ actor_run_id TEXT,
2928
+ expected_reviewer_id TEXT,
2929
+ expected_reviewer_run_id TEXT,
2930
+ repair_cycle INTEGER,
2931
+ ci_proof TEXT,
2932
+ cleanup_proof TEXT,
2933
+ metadata TEXT NOT NULL DEFAULT '{}',
2934
+ payload_hash TEXT NOT NULL,
2935
+ created_at TEXT NOT NULL,
2936
+ UNIQUE(group_id, sequence),
2937
+ UNIQUE(group_id, idempotency_key)
2938
+ )`);
2939
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence)");
2940
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence)");
2941
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_receipt ON pr_group_events(receipt_key)");
2942
+ ensureColumn("pr_groups", "leaf_task_id", "TEXT");
2943
+ ensureColumn("pr_groups", "branch", "TEXT");
2944
+ ensureColumn("pr_groups", "pr_number", "INTEGER");
2945
+ ensureColumn("pr_groups", "base_sha", "TEXT");
2946
+ ensureColumn("pr_groups", "repair_cycle_count", "INTEGER NOT NULL DEFAULT 0");
2947
+ ensureColumn("pr_groups", "repair_cycle_limit", "INTEGER NOT NULL DEFAULT 2");
2948
+ ensureColumn("pr_group_attempts", "repository", "TEXT");
2949
+ ensureColumn("pr_group_attempts", "pr_number", "INTEGER");
2950
+ ensureColumn("pr_group_attempts", "base_sha", "TEXT");
2951
+ ensureColumn("pr_group_events", "review_receipt_key", "TEXT");
2952
+ ensureColumn("pr_group_events", "conditional_merge_receipt_key", "TEXT");
2953
+ ensureColumn("pr_group_events", "repository", "TEXT");
2954
+ ensureColumn("pr_group_events", "pr_number", "INTEGER");
2955
+ ensureColumn("pr_group_events", "base_sha", "TEXT");
2956
+ ensureColumn("pr_group_events", "actor_id", "TEXT");
2957
+ ensureColumn("pr_group_events", "actor_run_id", "TEXT");
2958
+ ensureColumn("pr_group_events", "expected_reviewer_id", "TEXT");
2959
+ ensureColumn("pr_group_events", "expected_reviewer_run_id", "TEXT");
2960
+ ensureColumn("pr_group_events", "repair_cycle", "INTEGER");
2961
+ ensureColumn("pr_group_events", "ci_proof", "TEXT");
2962
+ ensureColumn("pr_group_events", "cleanup_proof", "TEXT");
2963
+ try {
2964
+ db.exec(`
2965
+ UPDATE pr_groups
2966
+ SET leaf_task_id = COALESCE(leaf_task_id, (
2967
+ SELECT attempt.leaf_task_id
2968
+ FROM pr_group_attempts AS attempt
2969
+ WHERE attempt.group_id = pr_groups.id
2970
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
2971
+ attempt.created_at ASC, attempt.id ASC
2972
+ LIMIT 1
2973
+ )),
2974
+ branch = COALESCE(branch, (
2975
+ SELECT attempt.branch
2976
+ FROM pr_group_attempts AS attempt
2977
+ WHERE attempt.group_id = pr_groups.id
2978
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
2979
+ attempt.created_at ASC, attempt.id ASC
2980
+ LIMIT 1
2981
+ ));
2982
+ UPDATE pr_group_attempts
2983
+ SET repository = COALESCE(repository, (
2984
+ SELECT groups.repository FROM pr_groups AS groups
2985
+ WHERE groups.id = pr_group_attempts.group_id
2986
+ )),
2987
+ pr_number = COALESCE(pr_number, (
2988
+ SELECT groups.pr_number FROM pr_groups AS groups
2989
+ WHERE groups.id = pr_group_attempts.group_id
2990
+ )),
2991
+ base_sha = COALESCE(base_sha, (
2992
+ SELECT groups.base_sha FROM pr_groups AS groups
2993
+ WHERE groups.id = pr_group_attempts.group_id
2994
+ ));
2995
+ UPDATE pr_groups
2996
+ SET pr_number = COALESCE(pr_number, (
2997
+ SELECT attempt.pr_number
2998
+ FROM pr_group_attempts AS attempt
2999
+ WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
3000
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3001
+ attempt.created_at ASC, attempt.id ASC
3002
+ LIMIT 1
3003
+ )),
3004
+ base_sha = COALESCE(base_sha, (
3005
+ SELECT attempt.base_sha
3006
+ FROM pr_group_attempts AS attempt
3007
+ WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
3008
+ ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3009
+ attempt.created_at ASC, attempt.id ASC
3010
+ LIMIT 1
3011
+ ));
3012
+ UPDATE pr_group_attempts
3013
+ SET pr_number = COALESCE(pr_number, (
3014
+ SELECT groups.pr_number FROM pr_groups AS groups
3015
+ WHERE groups.id = pr_group_attempts.group_id
3016
+ )),
3017
+ base_sha = COALESCE(base_sha, (
3018
+ SELECT groups.base_sha FROM pr_groups AS groups
3019
+ WHERE groups.id = pr_group_attempts.group_id
3020
+ ));
3021
+ UPDATE pr_group_events
3022
+ SET repository = COALESCE(repository, (
3023
+ SELECT attempt.repository FROM pr_group_attempts AS attempt
3024
+ WHERE attempt.id = pr_group_events.attempt_id
3025
+ ), (
3026
+ SELECT groups.repository FROM pr_groups AS groups
3027
+ WHERE groups.id = pr_group_events.group_id
3028
+ )),
3029
+ pr_number = COALESCE(pr_number, (
3030
+ SELECT attempt.pr_number FROM pr_group_attempts AS attempt
3031
+ WHERE attempt.id = pr_group_events.attempt_id
3032
+ ), (
3033
+ SELECT groups.pr_number FROM pr_groups AS groups
3034
+ WHERE groups.id = pr_group_events.group_id
3035
+ )),
3036
+ base_sha = COALESCE(base_sha, (
3037
+ SELECT attempt.base_sha FROM pr_group_attempts AS attempt
3038
+ WHERE attempt.id = pr_group_events.attempt_id
3039
+ ), (
3040
+ SELECT groups.base_sha FROM pr_groups AS groups
3041
+ WHERE groups.id = pr_group_events.group_id
3042
+ ));
3043
+ INSERT OR IGNORE INTO _migrations (id)
3044
+ SELECT 66
3045
+ WHERE NOT EXISTS (
3046
+ SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
3047
+ )
3048
+ AND NOT EXISTS (
3049
+ SELECT 1 FROM pr_group_attempts WHERE repository IS NULL
3050
+ )
3051
+ AND NOT EXISTS (
3052
+ SELECT 1 FROM pr_group_events WHERE repository IS NULL
3053
+ );
3054
+ `);
3055
+ } catch {}
3056
+ const strictColumns = [
3057
+ ["pr_groups", "leaf_task_id"],
3058
+ ["pr_groups", "branch"],
3059
+ ["pr_group_attempts", "repository"],
3060
+ ["pr_group_events", "repository"]
3061
+ ];
3062
+ for (const [table, column] of strictColumns) {
3063
+ const definition = db.query(`PRAGMA table_info(${table})`).all();
3064
+ if (definition.find((entry) => entry.name === column)?.notnull !== 1) {
3065
+ throw new Error(`PR-group schema integrity failure: ${table}.${column} is not NOT NULL`);
3066
+ }
3067
+ }
3068
+ const eventColumns = db.query("PRAGMA table_info(pr_group_events)").all();
3069
+ if (!eventColumns.some((entry) => entry.name === "ci_proof")) {
3070
+ throw new Error("PR-group schema integrity failure: pr_group_events.ci_proof is missing");
3071
+ }
3072
+ const receiptIndexes = db.query("PRAGMA index_list(pr_group_events)").all();
3073
+ const hasGlobalReceiptUniqueness = receiptIndexes.some((index) => {
3074
+ if (index.unique !== 1)
3075
+ return false;
3076
+ const columns = db.query(`PRAGMA index_info(${JSON.stringify(index.name)})`).all();
3077
+ return columns.length === 1 && columns[0]?.name === "receipt_key";
3078
+ });
3079
+ if (!hasGlobalReceiptUniqueness) {
3080
+ throw new Error("PR-group schema integrity failure: receipt_key is not globally unique");
3081
+ }
3082
+ const strictMigration = db.query("SELECT 1 AS ok FROM _migrations WHERE id = 68").get();
3083
+ if (strictMigration?.ok !== 1) {
3084
+ throw new Error("PR-group schema integrity failure: migration 68 is not recorded");
3085
+ }
2395
3086
  }
2396
3087
  function backfillTaskTags(db) {
2397
3088
  try {
@@ -2732,6 +3423,334 @@ var init_machines = __esm(() => {
2732
3423
  ];
2733
3424
  });
2734
3425
 
3426
+ // src/types/index.ts
3427
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3428
+ var init_types = __esm(() => {
3429
+ TASK_STATUSES = [
3430
+ "pending",
3431
+ "in_progress",
3432
+ "completed",
3433
+ "failed",
3434
+ "cancelled"
3435
+ ];
3436
+ TASK_PRIORITIES = [
3437
+ "low",
3438
+ "medium",
3439
+ "high",
3440
+ "critical"
3441
+ ];
3442
+ PLAN_STATUSES = ["active", "completed", "archived"];
3443
+ VersionConflictError = class VersionConflictError extends Error {
3444
+ taskId;
3445
+ expectedVersion;
3446
+ actualVersion;
3447
+ static code = "VERSION_CONFLICT";
3448
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
3449
+ constructor(taskId, expectedVersion, actualVersion) {
3450
+ super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
3451
+ this.taskId = taskId;
3452
+ this.expectedVersion = expectedVersion;
3453
+ this.actualVersion = actualVersion;
3454
+ this.name = "VersionConflictError";
3455
+ }
3456
+ };
3457
+ TaskNotFoundError = class TaskNotFoundError extends Error {
3458
+ taskId;
3459
+ static code = "TASK_NOT_FOUND";
3460
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
3461
+ constructor(taskId) {
3462
+ super(`Task not found: ${taskId}`);
3463
+ this.taskId = taskId;
3464
+ this.name = "TaskNotFoundError";
3465
+ }
3466
+ };
3467
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
3468
+ projectId;
3469
+ static code = "PROJECT_NOT_FOUND";
3470
+ static suggestion = "Use list_projects to see available projects.";
3471
+ constructor(projectId) {
3472
+ super(`Project not found: ${projectId}`);
3473
+ this.projectId = projectId;
3474
+ this.name = "ProjectNotFoundError";
3475
+ }
3476
+ };
3477
+ ResourceConflictError = class ResourceConflictError extends Error {
3478
+ code;
3479
+ constructor(code, message) {
3480
+ super(message);
3481
+ this.code = code;
3482
+ this.name = "ResourceConflictError";
3483
+ }
3484
+ };
3485
+ PlanNotFoundError = class PlanNotFoundError extends Error {
3486
+ planId;
3487
+ static code = "PLAN_NOT_FOUND";
3488
+ static suggestion = "Use list_plans to see available plans.";
3489
+ constructor(planId) {
3490
+ super(`Plan not found: ${planId}`);
3491
+ this.planId = planId;
3492
+ this.name = "PlanNotFoundError";
3493
+ }
3494
+ };
3495
+ LockError = class LockError extends Error {
3496
+ taskId;
3497
+ lockedBy;
3498
+ static code = "LOCK_ERROR";
3499
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
3500
+ constructor(taskId, lockedBy) {
3501
+ super(`Task ${taskId} is locked by ${lockedBy}`);
3502
+ this.taskId = taskId;
3503
+ this.lockedBy = lockedBy;
3504
+ this.name = "LockError";
3505
+ }
3506
+ };
3507
+ AgentNotFoundError = class AgentNotFoundError extends Error {
3508
+ agentId;
3509
+ static code = "AGENT_NOT_FOUND";
3510
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
3511
+ constructor(agentId) {
3512
+ super(`Agent not found: ${agentId}`);
3513
+ this.agentId = agentId;
3514
+ this.name = "AgentNotFoundError";
3515
+ }
3516
+ };
3517
+ IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
3518
+ code = "IDENTITY_ALIAS_AMBIGUOUS";
3519
+ candidates;
3520
+ constructor(subject, candidates = []) {
3521
+ super(`Identity alias or source is ambiguous: ${subject}`);
3522
+ this.name = "IdentityAliasAmbiguousError";
3523
+ this.candidates = [...new Set(candidates)].sort();
3524
+ }
3525
+ };
3526
+ IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
3527
+ code = "IDENTITY_ID_IMMUTABLE";
3528
+ constructor(agentId) {
3529
+ super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
3530
+ this.name = "IdentityIdImmutableError";
3531
+ }
3532
+ };
3533
+ TaskListNotFoundError = class TaskListNotFoundError extends Error {
3534
+ taskListId;
3535
+ static code = "TASK_LIST_NOT_FOUND";
3536
+ static suggestion = "Use list_task_lists to see available lists.";
3537
+ constructor(taskListId) {
3538
+ super(`Task list not found: ${taskListId}`);
3539
+ this.taskListId = taskListId;
3540
+ this.name = "TaskListNotFoundError";
3541
+ }
3542
+ };
3543
+ DependencyCycleError = class DependencyCycleError extends Error {
3544
+ taskId;
3545
+ dependsOn;
3546
+ static code = "DEPENDENCY_CYCLE";
3547
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
3548
+ constructor(taskId, dependsOn) {
3549
+ super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
3550
+ this.taskId = taskId;
3551
+ this.dependsOn = dependsOn;
3552
+ this.name = "DependencyCycleError";
3553
+ }
3554
+ };
3555
+ CompletionGuardError = class CompletionGuardError extends Error {
3556
+ reason;
3557
+ retryAfterSeconds;
3558
+ static code = "COMPLETION_BLOCKED";
3559
+ static suggestion = "Wait for the cooldown period, then retry.";
3560
+ constructor(reason, retryAfterSeconds) {
3561
+ super(reason);
3562
+ this.reason = reason;
3563
+ this.retryAfterSeconds = retryAfterSeconds;
3564
+ this.name = "CompletionGuardError";
3565
+ }
3566
+ };
3567
+ DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
3568
+ DispatchNotFoundError = class DispatchNotFoundError extends Error {
3569
+ dispatchId;
3570
+ static code = "DISPATCH_NOT_FOUND";
3571
+ static suggestion = "Check the dispatch ID with list_dispatches.";
3572
+ constructor(dispatchId) {
3573
+ super(`Dispatch not found: ${dispatchId}`);
3574
+ this.dispatchId = dispatchId;
3575
+ this.name = "DispatchNotFoundError";
3576
+ }
3577
+ };
3578
+ });
3579
+
3580
+ // src/db/identity-mapping.ts
3581
+ function timestamp() {
3582
+ return new Date().toISOString();
3583
+ }
3584
+ function mappingId() {
3585
+ return crypto.randomUUID();
3586
+ }
3587
+ function normalizedAlias(label) {
3588
+ const normalized = label.trim().toLocaleLowerCase("en-US");
3589
+ if (!normalized)
3590
+ throw new Error("IDENTITY_ALIAS_INVALID: label is required");
3591
+ return normalized;
3592
+ }
3593
+ function ensureColumn(db, table, column, type) {
3594
+ const columns = db.query(`PRAGMA table_info(${table})`).all();
3595
+ if (!columns.some((item) => item.name === column)) {
3596
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
3597
+ }
3598
+ }
3599
+ function ensureAgentIdentitySchema(db) {
3600
+ const install = db.transaction(() => {
3601
+ ensureColumn(db, "agents", "identity_id", "TEXT");
3602
+ ensureColumn(db, "agents", "project_id", "TEXT");
3603
+ ensureColumn(db, "agents", "runtime_instance_id", "TEXT");
3604
+ db.exec(`
3605
+ CREATE TABLE IF NOT EXISTS agent_identity_source_mappings (
3606
+ id TEXT PRIMARY KEY,
3607
+ local_agent_id TEXT REFERENCES agents(id) ON DELETE RESTRICT,
3608
+ identity_id TEXT,
3609
+ source_authority TEXT NOT NULL CHECK(length(trim(source_authority)) > 0),
3610
+ source_tenant_id TEXT NOT NULL CHECK(length(trim(source_tenant_id)) > 0),
3611
+ source_namespace TEXT NOT NULL CHECK(length(trim(source_namespace)) > 0),
3612
+ source_entity_type TEXT NOT NULL CHECK(length(trim(source_entity_type)) > 0),
3613
+ source_record_id TEXT NOT NULL CHECK(length(trim(source_record_id)) > 0),
3614
+ observed_label TEXT,
3615
+ evidence TEXT NOT NULL DEFAULT '{}',
3616
+ mapping_basis TEXT NOT NULL CHECK(mapping_basis IN ('authoritative', 'imported', 'candidate', 'name_similarity')),
3617
+ status TEXT NOT NULL CHECK(status IN ('active', 'retired', 'quarantined', 'revoked')),
3618
+ revision INTEGER NOT NULL CHECK(revision > 0),
3619
+ created_at TEXT NOT NULL,
3620
+ updated_at TEXT NOT NULL,
3621
+ CHECK(mapping_basis IN ('authoritative', 'imported') OR status IN ('quarantined', 'revoked')),
3622
+ CHECK(status != 'active' OR (mapping_basis IN ('authoritative', 'imported') AND identity_id IS NOT NULL))
3623
+ );
3624
+ `);
3625
+ ensureColumn(db, "agent_identity_source_mappings", "revision", "INTEGER NOT NULL DEFAULT 1");
3626
+ ensureColumn(db, "agent_identity_source_mappings", "evidence", "TEXT NOT NULL DEFAULT '{}'");
3627
+ db.exec(`
3628
+ DROP INDEX IF EXISTS idx_agent_identity_source_active_unique;
3629
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_identity_source_revision_unique
3630
+ ON agent_identity_source_mappings (
3631
+ source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id, revision
3632
+ )
3633
+ WHERE mapping_basis IN ('authoritative', 'imported');
3634
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_identity
3635
+ ON agent_identity_source_mappings(identity_id);
3636
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_source_local
3637
+ ON agent_identity_source_mappings(local_agent_id);
3638
+ DROP INDEX IF EXISTS idx_agents_identity_unique;
3639
+
3640
+ CREATE TABLE IF NOT EXISTS agent_identity_aliases (
3641
+ id TEXT PRIMARY KEY,
3642
+ local_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,
3643
+ label TEXT NOT NULL,
3644
+ normalized_label TEXT NOT NULL,
3645
+ alias_kind TEXT NOT NULL CHECK(alias_kind IN ('historical', 'candidate')),
3646
+ status TEXT NOT NULL CHECK(status IN ('active', 'quarantined', 'revoked')),
3647
+ created_at TEXT NOT NULL,
3648
+ UNIQUE(local_agent_id, normalized_label)
3649
+ );
3650
+ CREATE INDEX IF NOT EXISTS idx_agent_identity_alias_lookup
3651
+ ON agent_identity_aliases(normalized_label, status);
3652
+
3653
+ CREATE TRIGGER IF NOT EXISTS trg_agents_identity_id_immutable
3654
+ BEFORE UPDATE OF identity_id ON agents
3655
+ WHEN OLD.identity_id IS NOT NULL AND OLD.identity_id IS NOT NEW.identity_id
3656
+ BEGIN
3657
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
3658
+ END;
3659
+
3660
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_identity_immutable
3661
+ BEFORE UPDATE OF identity_id ON agent_identity_source_mappings
3662
+ WHEN OLD.identity_id IS NOT NEW.identity_id
3663
+ BEGIN
3664
+ SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
3665
+ END;
3666
+
3667
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_lineage_immutable
3668
+ BEFORE UPDATE OF source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id
3669
+ ON agent_identity_source_mappings
3670
+ WHEN OLD.source_authority IS NOT NEW.source_authority
3671
+ OR OLD.source_tenant_id IS NOT NEW.source_tenant_id
3672
+ OR OLD.source_namespace IS NOT NEW.source_namespace
3673
+ OR OLD.source_entity_type IS NOT NEW.source_entity_type
3674
+ OR OLD.source_record_id IS NOT NEW.source_record_id
3675
+ BEGIN
3676
+ SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
3677
+ END;
3678
+
3679
+ DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
3680
+ CREATE TRIGGER trg_agent_identity_mapping_history_immutable
3681
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
3682
+ ON agent_identity_source_mappings
3683
+ BEGIN
3684
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
3685
+ END;
3686
+
3687
+ DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
3688
+ CREATE TRIGGER trg_agent_identity_mapping_history_append_only
3689
+ BEFORE DELETE ON agent_identity_source_mappings
3690
+ BEGIN
3691
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
3692
+ END;
3693
+ `);
3694
+ db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
3695
+ });
3696
+ install.immediate();
3697
+ }
3698
+ function storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db) {
3699
+ const normalized = normalizedAlias(label);
3700
+ const exists = db.query("SELECT id FROM agents WHERE id = ?").get(agentId);
3701
+ if (!exists)
3702
+ throw new Error(`Agent not found: ${agentId}`);
3703
+ const existing = db.query(`
3704
+ SELECT * FROM agent_identity_aliases
3705
+ WHERE local_agent_id = ? AND normalized_label = ?
3706
+ `).get(agentId, normalized);
3707
+ if (existing) {
3708
+ if (aliasKind === "historical" && (existing.alias_kind !== "historical" || existing.status !== "active")) {
3709
+ db.run("UPDATE agent_identity_aliases SET label = ?, alias_kind = 'historical', status = 'active' WHERE id = ?", [label.trim(), existing.id]);
3710
+ return db.query("SELECT * FROM agent_identity_aliases WHERE id = ?").get(existing.id);
3711
+ }
3712
+ return existing;
3713
+ }
3714
+ const id = mappingId();
3715
+ db.run(`
3716
+ INSERT INTO agent_identity_aliases (
3717
+ id, local_agent_id, label, normalized_label, alias_kind, status, created_at
3718
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
3719
+ `, [id, agentId, label.trim(), normalized, aliasKind, status, timestamp()]);
3720
+ return db.query("SELECT * FROM agent_identity_aliases WHERE id = ?").get(id);
3721
+ }
3722
+ function storeAgentAlias(agentId, label, aliasKind, status, db) {
3723
+ if (db.inTransaction) {
3724
+ return storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db);
3725
+ }
3726
+ const store = db.transaction(() => storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db));
3727
+ return store.immediate();
3728
+ }
3729
+ function recordAgentAlias(agentId, label, db) {
3730
+ return storeAgentAlias(agentId, label, "historical", "active", db);
3731
+ }
3732
+ var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
3733
+ var init_identity_mapping = __esm(() => {
3734
+ init_types();
3735
+ init_types();
3736
+ IDENTITY_PROJECTION_CONTRACT = Object.freeze({
3737
+ name: "hasna.todos.agent-identity-projection/v1",
3738
+ version: 1,
3739
+ foundation_repository: "hasna/identities",
3740
+ foundation_pull_request: 14,
3741
+ foundation_contract: "hasna.identities.agent-identity/v1",
3742
+ foundation_version: 1,
3743
+ foundation_commit: "14d83e3e45df5748944a871a07c4aacece893169",
3744
+ foundation_fixture_id: "hasna.identities.agent-identity/v1/conformance/1",
3745
+ foundation_fixture_source_path: "docs/fixtures/agent-identity-v1.conformance.json",
3746
+ foundation_fixture_local_path: "src/db/fixtures/agent-identity-v1.conformance.json",
3747
+ foundation_fixture_sha256: "sha256:2d22e72b7315d0b06b3c67c71674cfd8c2dff552727eea00efb325f52c9420af",
3748
+ foundation_default_read_preference: "canonical_first",
3749
+ consumer_rollout_default_read_preference: "legacy_first",
3750
+ lease_fence_authority: "external Runtime Coordination"
3751
+ });
3752
+ });
3753
+
2735
3754
  // src/storage/config.ts
2736
3755
  var exports_config = {};
2737
3756
  __export(exports_config, {
@@ -2755,15 +3774,15 @@ __export(exports_config, {
2755
3774
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
2756
3775
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
2757
3776
  STORAGE_TABLES: () => STORAGE_TABLES,
2758
- CANONICAL_TODOS_RDS_RUNTIME_PATH: () => CANONICAL_TODOS_RDS_RUNTIME_PATH,
3777
+ CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
2759
3778
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
2760
- CANONICAL_TODOS_RDS_CLUSTER: () => CANONICAL_TODOS_RDS_CLUSTER
3779
+ CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
2761
3780
  });
2762
- function getCanonicalTodosRdsConfig() {
3781
+ function getCanonicalTodosRdsConfig(env = process.env) {
2763
3782
  return {
2764
- cluster: CANONICAL_TODOS_RDS_CLUSTER,
3783
+ cluster: clean(env[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
2765
3784
  database: CANONICAL_TODOS_RDS_DATABASE,
2766
- runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
3785
+ runtimeSecretPath: clean(env[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
2767
3786
  primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
2768
3787
  fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
2769
3788
  };
@@ -2896,7 +3915,7 @@ function parsePositiveInteger(value, fallback) {
2896
3915
  }
2897
3916
  return parsed;
2898
3917
  }
2899
- 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";
3918
+ 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";
2900
3919
  var init_config = __esm(() => {
2901
3920
  TODOS_STORAGE_TABLES = [
2902
3921
  "todos_sync_records",
@@ -2958,7 +3977,7 @@ function installShadowOutboxSchema(db) {
2958
3977
  status TEXT NOT NULL DEFAULT 'pending',
2959
3978
  revision INTEGER NOT NULL DEFAULT 0
2960
3979
  )`);
2961
- ensureColumn(db, "shadow_outbox", "revision", "INTEGER NOT NULL DEFAULT 0");
3980
+ ensureColumn2(db, "shadow_outbox", "revision", "INTEGER NOT NULL DEFAULT 0");
2962
3981
  db.run(`CREATE UNIQUE INDEX IF NOT EXISTS shadow_outbox_pending_uq
2963
3982
  ON shadow_outbox(object_type, object_id) WHERE status='pending'`);
2964
3983
  db.run(`CREATE INDEX IF NOT EXISTS shadow_outbox_ready_idx
@@ -2980,7 +3999,7 @@ function installShadowOutboxSchema(db) {
2980
3999
  }
2981
4000
  }
2982
4001
  }
2983
- function ensureColumn(db, table, column, definition) {
4002
+ function ensureColumn2(db, table, column, definition) {
2984
4003
  const rows = db.query(`PRAGMA table_info(${table})`).all();
2985
4004
  if (rows.some((row) => row.name === column))
2986
4005
  return;
@@ -3108,6 +4127,7 @@ function openDatabase(path) {
3108
4127
  db.run("PRAGMA busy_timeout = 5000");
3109
4128
  db.run("PRAGMA foreign_keys = ON");
3110
4129
  runMigrations(db);
4130
+ ensureAgentIdentitySchema(db);
3111
4131
  backfillTaskTags(db);
3112
4132
  backfillMachineId(db);
3113
4133
  maybeInstallShadowCapture(db);
@@ -3157,214 +4177,88 @@ function uuid() {
3157
4177
  return crypto.randomUUID();
3158
4178
  }
3159
4179
  function isLockExpired(lockedAt, nowMs = Date.now()) {
3160
- if (!lockedAt)
3161
- return true;
3162
- const lockTime = new Date(lockedAt).getTime();
3163
- const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
3164
- return nowMs - lockTime > expiryMs;
3165
- }
3166
- function lockExpiryCutoff(nowMs = Date.now()) {
3167
- const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
3168
- return new Date(nowMs - expiryMs).toISOString();
3169
- }
3170
- function clearExpiredLocks(db) {
3171
- const cutoff = lockExpiryCutoff();
3172
- db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
3173
- }
3174
- function slugifyRef(value) {
3175
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3176
- }
3177
- function resolvePartialId(db, table, partialId) {
3178
- if (!ALLOWED_TABLES.has(table)) {
3179
- throw new Error(`Invalid table name: ${table}`);
3180
- }
3181
- if (partialId.length >= 36) {
3182
- const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
3183
- return row?.id ?? null;
3184
- }
3185
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
3186
- if (rows.length === 1) {
3187
- return rows[0].id;
3188
- }
3189
- if (rows.length > 1) {
3190
- return null;
3191
- }
3192
- if (table === "tasks") {
3193
- const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
3194
- if (shortIdRows.length === 1) {
3195
- return shortIdRows[0].id;
3196
- }
3197
- }
3198
- if (table === "task_lists") {
3199
- const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
3200
- if (slugRow)
3201
- return slugRow.id;
3202
- }
3203
- if (table === "plans") {
3204
- const slug = slugifyRef(partialId);
3205
- if (slug) {
3206
- const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
3207
- if (slugRows.length === 1)
3208
- return slugRows[0].id;
3209
- if (slugRows.length > 1)
3210
- return null;
3211
- }
3212
- }
3213
- if (table === "projects") {
3214
- const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
3215
- if (nameRow)
3216
- return nameRow.id;
3217
- }
3218
- if (table === "agents") {
3219
- const nameRow = db.query("SELECT id FROM agents WHERE lower(name) = ?").get(partialId.toLowerCase());
3220
- if (nameRow)
3221
- return nameRow.id;
3222
- }
3223
- return null;
3224
- }
3225
- var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
3226
- var init_database = __esm(() => {
3227
- init_schema();
3228
- init_machines();
3229
- ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
3230
- });
3231
-
3232
- // src/types/index.ts
3233
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3234
- var init_types = __esm(() => {
3235
- TASK_STATUSES = [
3236
- "pending",
3237
- "in_progress",
3238
- "completed",
3239
- "failed",
3240
- "cancelled"
3241
- ];
3242
- TASK_PRIORITIES = [
3243
- "low",
3244
- "medium",
3245
- "high",
3246
- "critical"
3247
- ];
3248
- PLAN_STATUSES = ["active", "completed", "archived"];
3249
- VersionConflictError = class VersionConflictError extends Error {
3250
- taskId;
3251
- expectedVersion;
3252
- actualVersion;
3253
- static code = "VERSION_CONFLICT";
3254
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
3255
- constructor(taskId, expectedVersion, actualVersion) {
3256
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
3257
- this.taskId = taskId;
3258
- this.expectedVersion = expectedVersion;
3259
- this.actualVersion = actualVersion;
3260
- this.name = "VersionConflictError";
3261
- }
3262
- };
3263
- TaskNotFoundError = class TaskNotFoundError extends Error {
3264
- taskId;
3265
- static code = "TASK_NOT_FOUND";
3266
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
3267
- constructor(taskId) {
3268
- super(`Task not found: ${taskId}`);
3269
- this.taskId = taskId;
3270
- this.name = "TaskNotFoundError";
3271
- }
3272
- };
3273
- ProjectNotFoundError = class ProjectNotFoundError extends Error {
3274
- projectId;
3275
- static code = "PROJECT_NOT_FOUND";
3276
- static suggestion = "Use list_projects to see available projects.";
3277
- constructor(projectId) {
3278
- super(`Project not found: ${projectId}`);
3279
- this.projectId = projectId;
3280
- this.name = "ProjectNotFoundError";
3281
- }
3282
- };
3283
- ResourceConflictError = class ResourceConflictError extends Error {
3284
- code;
3285
- constructor(code, message) {
3286
- super(message);
3287
- this.code = code;
3288
- this.name = "ResourceConflictError";
3289
- }
3290
- };
3291
- PlanNotFoundError = class PlanNotFoundError extends Error {
3292
- planId;
3293
- static code = "PLAN_NOT_FOUND";
3294
- static suggestion = "Use list_plans to see available plans.";
3295
- constructor(planId) {
3296
- super(`Plan not found: ${planId}`);
3297
- this.planId = planId;
3298
- this.name = "PlanNotFoundError";
3299
- }
3300
- };
3301
- LockError = class LockError extends Error {
3302
- taskId;
3303
- lockedBy;
3304
- static code = "LOCK_ERROR";
3305
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
3306
- constructor(taskId, lockedBy) {
3307
- super(`Task ${taskId} is locked by ${lockedBy}`);
3308
- this.taskId = taskId;
3309
- this.lockedBy = lockedBy;
3310
- this.name = "LockError";
3311
- }
3312
- };
3313
- AgentNotFoundError = class AgentNotFoundError extends Error {
3314
- agentId;
3315
- static code = "AGENT_NOT_FOUND";
3316
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
3317
- constructor(agentId) {
3318
- super(`Agent not found: ${agentId}`);
3319
- this.agentId = agentId;
3320
- this.name = "AgentNotFoundError";
3321
- }
3322
- };
3323
- TaskListNotFoundError = class TaskListNotFoundError extends Error {
3324
- taskListId;
3325
- static code = "TASK_LIST_NOT_FOUND";
3326
- static suggestion = "Use list_task_lists to see available lists.";
3327
- constructor(taskListId) {
3328
- super(`Task list not found: ${taskListId}`);
3329
- this.taskListId = taskListId;
3330
- this.name = "TaskListNotFoundError";
3331
- }
3332
- };
3333
- DependencyCycleError = class DependencyCycleError extends Error {
3334
- taskId;
3335
- dependsOn;
3336
- static code = "DEPENDENCY_CYCLE";
3337
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
3338
- constructor(taskId, dependsOn) {
3339
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
3340
- this.taskId = taskId;
3341
- this.dependsOn = dependsOn;
3342
- this.name = "DependencyCycleError";
4180
+ if (!lockedAt)
4181
+ return true;
4182
+ const lockTime = new Date(lockedAt).getTime();
4183
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
4184
+ return nowMs - lockTime > expiryMs;
4185
+ }
4186
+ function lockExpiryCutoff(nowMs = Date.now()) {
4187
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
4188
+ return new Date(nowMs - expiryMs).toISOString();
4189
+ }
4190
+ function clearExpiredLocks(db) {
4191
+ const cutoff = lockExpiryCutoff();
4192
+ db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
4193
+ }
4194
+ function slugifyRef(value) {
4195
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4196
+ }
4197
+ function resolvePartialId(db, table, partialId) {
4198
+ if (!ALLOWED_TABLES.has(table)) {
4199
+ throw new Error(`Invalid table name: ${table}`);
4200
+ }
4201
+ if (partialId.length >= 36) {
4202
+ const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
4203
+ return row?.id ?? null;
4204
+ }
4205
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
4206
+ if (rows.length === 1) {
4207
+ return rows[0].id;
4208
+ }
4209
+ if (rows.length > 1) {
4210
+ return null;
4211
+ }
4212
+ if (table === "tasks") {
4213
+ const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
4214
+ if (shortIdRows.length === 1) {
4215
+ return shortIdRows[0].id;
3343
4216
  }
3344
- };
3345
- CompletionGuardError = class CompletionGuardError extends Error {
3346
- reason;
3347
- retryAfterSeconds;
3348
- static code = "COMPLETION_BLOCKED";
3349
- static suggestion = "Wait for the cooldown period, then retry.";
3350
- constructor(reason, retryAfterSeconds) {
3351
- super(reason);
3352
- this.reason = reason;
3353
- this.retryAfterSeconds = retryAfterSeconds;
3354
- this.name = "CompletionGuardError";
4217
+ }
4218
+ if (table === "task_lists") {
4219
+ const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
4220
+ if (slugRow)
4221
+ return slugRow.id;
4222
+ }
4223
+ if (table === "plans") {
4224
+ const slug = slugifyRef(partialId);
4225
+ if (slug) {
4226
+ const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
4227
+ if (slugRows.length === 1)
4228
+ return slugRows[0].id;
4229
+ if (slugRows.length > 1)
4230
+ return null;
3355
4231
  }
3356
- };
3357
- DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
3358
- DispatchNotFoundError = class DispatchNotFoundError extends Error {
3359
- dispatchId;
3360
- static code = "DISPATCH_NOT_FOUND";
3361
- static suggestion = "Check the dispatch ID with list_dispatches.";
3362
- constructor(dispatchId) {
3363
- super(`Dispatch not found: ${dispatchId}`);
3364
- this.dispatchId = dispatchId;
3365
- this.name = "DispatchNotFoundError";
4232
+ }
4233
+ if (table === "projects") {
4234
+ const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
4235
+ if (nameRow)
4236
+ return nameRow.id;
4237
+ }
4238
+ if (table === "agents") {
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));
3366
4251
  }
3367
- };
4252
+ }
4253
+ return null;
4254
+ }
4255
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
4256
+ var init_database = __esm(() => {
4257
+ init_schema();
4258
+ init_machines();
4259
+ init_identity_mapping();
4260
+ init_types();
4261
+ ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
3368
4262
  });
3369
4263
 
3370
4264
  // src/lib/sync-utils.ts
@@ -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, field2, 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, field2 || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
7075
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field2 || 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, field2, oldValue, newValue, agentId, db)
6065
7080
  entity_id: taskId,
6066
7081
  action,
6067
7082
  field: field2,
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: field2 || 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: field2 || 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));
@@ -7139,9 +8155,9 @@ function startTask(id, agentId, db) {
7139
8155
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
7140
8156
  }
7141
8157
  const cutoff = lockExpiryCutoff();
7142
- const timestamp = now();
8158
+ const timestamp2 = now();
7143
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 = ?
7144
- 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]);
7145
8161
  if (result.changes === 0) {
7146
8162
  const current = getTask(id, d);
7147
8163
  if (!current)
@@ -7153,7 +8169,7 @@ function startTask(id, agentId, db) {
7153
8169
  throw new Error(`Task ${id} could not be started because it changed during claim`);
7154
8170
  }
7155
8171
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
7156
- 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 };
7157
8173
  const payload = taskEventData(startedTask, { agent_id: agentId });
7158
8174
  dispatchWebhook2("task.started", payload, d).catch(() => {});
7159
8175
  emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
@@ -7176,7 +8192,13 @@ function completeTask(id, agentId, db, options) {
7176
8192
  throw new LockError(id, task.locked_by);
7177
8193
  }
7178
8194
  checkCompletionGuard(task, agentId || null, d);
7179
- 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;
7180
8202
  const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
7181
8203
  const completionMeta = {};
7182
8204
  if (hasEvidence)
@@ -7185,21 +8207,21 @@ function completeTask(id, agentId, db, options) {
7185
8207
  completionMeta._completion = { confidence: options.confidence };
7186
8208
  }
7187
8209
  const hasMeta = Object.keys(completionMeta).length > 0;
7188
- const timestamp = options?.completed_at || now();
8210
+ const timestamp2 = options?.completed_at || now();
7189
8211
  const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
7190
8212
  const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
7191
8213
  const finalVersion = versionBeforeStatus + 1;
7192
8214
  const tx = d.transaction(() => {
7193
8215
  if (hasMeta) {
7194
- const meta2 = { ...task.metadata, ...completionMeta };
7195
- 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]);
7196
8218
  if (metaResult.changes === 0) {
7197
8219
  const current = getTask(id, d);
7198
8220
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
7199
8221
  }
7200
8222
  }
7201
8223
  const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
7202
- WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
8224
+ WHERE id = ? AND version = ?`, [timestamp2, confidence, timestamp2, id, versionBeforeStatus]);
7203
8225
  if (statusResult.changes === 0) {
7204
8226
  const current = getTask(id, d);
7205
8227
  throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
@@ -7212,20 +8234,20 @@ function completeTask(id, agentId, db, options) {
7212
8234
  status: "completed",
7213
8235
  locked_by: null,
7214
8236
  locked_at: null,
7215
- completed_at: timestamp,
8237
+ completed_at: timestamp2,
7216
8238
  confidence,
7217
8239
  version: finalVersion,
7218
- updated_at: timestamp,
8240
+ updated_at: timestamp2,
7219
8241
  metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
7220
8242
  };
7221
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
8243
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp2 });
7222
8244
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
7223
8245
  emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
7224
- 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 });
7225
8247
  let spawnedTask = null;
7226
8248
  if (task.recurrence_rule && !options?.skip_recurrence) {
7227
8249
  try {
7228
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
8250
+ spawnedTask = spawnNextRecurrence(task, d, timestamp2);
7229
8251
  } catch (e) {
7230
8252
  spawnedTask = null;
7231
8253
  console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
@@ -7275,7 +8297,7 @@ function completeTask(id, agentId, db, options) {
7275
8297
  emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
7276
8298
  }
7277
8299
  }
7278
- 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 };
7279
8301
  }
7280
8302
  function lockTask(id, agentId, db) {
7281
8303
  const d = db || getDatabase();
@@ -7289,15 +8311,15 @@ function lockTask(id, agentId, db) {
7289
8311
  };
7290
8312
  }
7291
8313
  if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
7292
- const timestamp2 = now();
7293
- 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]);
7294
8316
  logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
7295
- 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) };
7296
8318
  }
7297
8319
  const cutoff = lockExpiryCutoff();
7298
- const timestamp = now();
8320
+ const timestamp2 = now();
7299
8321
  const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
7300
- 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]);
7301
8323
  if (result.changes === 0) {
7302
8324
  const current = getTask(id, d);
7303
8325
  if (!current)
@@ -7322,7 +8344,7 @@ function lockTask(id, agentId, db) {
7322
8344
  };
7323
8345
  }
7324
8346
  logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
7325
- 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) };
7326
8348
  }
7327
8349
  function unlockTask(id, agentId, db) {
7328
8350
  const d = db || getDatabase();
@@ -7332,9 +8354,9 @@ function unlockTask(id, agentId, db) {
7332
8354
  if (agentId && task.locked_by && task.locked_by !== agentId) {
7333
8355
  throw new LockError(id, task.locked_by);
7334
8356
  }
7335
- const timestamp = now();
8357
+ const timestamp2 = now();
7336
8358
  d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
7337
- WHERE id = ?`, [timestamp, id]);
8359
+ WHERE id = ?`, [timestamp2, id]);
7338
8360
  return true;
7339
8361
  }
7340
8362
  function getTaskLockStatus(id, db) {
@@ -7459,20 +8481,23 @@ function failTask(id, agentId, reason, options, db) {
7459
8481
  const task = getTask(id, d);
7460
8482
  if (!task)
7461
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;
7462
8486
  const meta = {
7463
8487
  ...task.metadata,
7464
8488
  _failure: {
7465
- reason: reason || "Unknown failure",
7466
- error_code: options?.error_code || null,
8489
+ reason: safeReason,
8490
+ error_code: safeErrorCode,
7467
8491
  failed_by: agentId || null,
7468
8492
  failed_at: now(),
7469
8493
  retry_requested: options?.retry || false
7470
8494
  }
7471
8495
  };
7472
- const timestamp = now();
8496
+ const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
8497
+ const timestamp2 = now();
7473
8498
  const failTx = d.transaction(() => {
7474
8499
  const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
7475
- WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
8500
+ WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
7476
8501
  if (res.changes === 0) {
7477
8502
  const current = getTask(id, d);
7478
8503
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -7484,22 +8509,22 @@ function failTask(id, agentId, reason, options, db) {
7484
8509
  status: "failed",
7485
8510
  locked_by: null,
7486
8511
  locked_at: null,
7487
- metadata: meta,
8512
+ metadata: safeMeta,
7488
8513
  version: task.version + 1,
7489
- updated_at: timestamp
8514
+ updated_at: timestamp2
7490
8515
  };
7491
8516
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
7492
- 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 });
7493
8518
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
7494
8519
  emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
7495
- 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 });
7496
8521
  let retryTask;
7497
8522
  if (options?.retry) {
7498
8523
  const retryCount = (task.retry_count || 0) + 1;
7499
8524
  const maxRetries = task.max_retries || 3;
7500
8525
  if (retryCount > maxRetries) {
7501
8526
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
7502
- 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")),
7503
8528
  id
7504
8529
  ]);
7505
8530
  } else {
@@ -7518,7 +8543,7 @@ function failTask(id, agentId, reason, options, db) {
7518
8543
  plan_id: task.plan_id ?? undefined,
7519
8544
  assigned_to: task.assigned_to ?? undefined,
7520
8545
  tags: task.tags,
7521
- 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 } },
7522
8547
  estimated_minutes: task.estimated_minutes ?? undefined,
7523
8548
  recurrence_rule: task.recurrence_rule ?? undefined,
7524
8549
  due_at: retryAfter
@@ -7560,15 +8585,15 @@ function stealTask(agentId, opts, db) {
7560
8585
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
7561
8586
  staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
7562
8587
  const target = staleTasks[0];
7563
- const timestamp = now();
8588
+ const timestamp2 = now();
7564
8589
  const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
7565
8590
  const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
7566
- 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]);
7567
8592
  if (result.changes === 0)
7568
8593
  return null;
7569
8594
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
7570
8595
  logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
7571
- 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 };
7572
8597
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7573
8598
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7574
8599
  emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
@@ -7629,6 +8654,7 @@ var init_task_lifecycle = __esm(() => {
7629
8654
  init_templates();
7630
8655
  init_task_crud();
7631
8656
  init_task_graph();
8657
+ init_prewrite_secrets();
7632
8658
  });
7633
8659
 
7634
8660
  // src/db/task-crud.ts
@@ -7666,9 +8692,29 @@ function addMetadataConditions(metadata, conditions, params) {
7666
8692
  params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
7667
8693
  }
7668
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
+ }
7669
8714
  function createTask(input, db) {
8715
+ input = sanitizeCreateTaskInput(input);
7670
8716
  const d = db || getDatabase();
7671
- const timestamp = now();
8717
+ const timestamp2 = now();
7672
8718
  const tags = input.tags || [];
7673
8719
  const machineId = currentStorageMachineId(d);
7674
8720
  const assignedBy = input.assigned_by || input.agent_id;
@@ -7695,8 +8741,8 @@ function createTask(input, db) {
7695
8741
  input.working_dir || null,
7696
8742
  JSON.stringify(tags),
7697
8743
  JSON.stringify(input.metadata || {}),
7698
- timestamp,
7699
- timestamp,
8744
+ timestamp2,
8745
+ timestamp2,
7700
8746
  input.due_at || null,
7701
8747
  input.estimated_minutes || null,
7702
8748
  input.sla_minutes ?? null,
@@ -8020,10 +9066,11 @@ function updateTask(id, input, db) {
8020
9066
  if (task.version !== input.version) {
8021
9067
  throw new VersionConflictError(id, input.version, task.version);
8022
9068
  }
8023
- const timestamp = now();
8024
- const completionTimestamp = input.completed_at ?? timestamp;
9069
+ input = sanitizeUpdateTaskInput(input);
9070
+ const timestamp2 = now();
9071
+ const completionTimestamp = input.completed_at ?? timestamp2;
8025
9072
  const sets = ["version = version + 1", "updated_at = ?"];
8026
- const params = [timestamp];
9073
+ const params = [timestamp2];
8027
9074
  if (input.title !== undefined) {
8028
9075
  sets.push("title = ?");
8029
9076
  params.push(input.title);
@@ -8172,7 +9219,7 @@ function updateTask(id, input, db) {
8172
9219
  tags: input.tags ?? task.tags,
8173
9220
  metadata: input.metadata ?? task.metadata,
8174
9221
  version: task.version + 1,
8175
- updated_at: timestamp,
9222
+ updated_at: timestamp2,
8176
9223
  locked_by: completedNow ? null : task.locked_by,
8177
9224
  locked_at: completedNow ? null : task.locked_at,
8178
9225
  completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
@@ -8184,7 +9231,7 @@ function updateTask(id, input, db) {
8184
9231
  retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
8185
9232
  requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
8186
9233
  approved_by: input.approved_by ?? task.approved_by,
8187
- approved_at: input.approved_by ? timestamp : task.approved_at
9234
+ approved_at: input.approved_by ? timestamp2 : task.approved_at
8188
9235
  };
8189
9236
  const databasePath = databasePathFromDatabase(d);
8190
9237
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
@@ -8233,6 +9280,7 @@ var init_task_crud = __esm(() => {
8233
9280
  init_webhooks();
8234
9281
  init_checklists();
8235
9282
  init_storage_tombstones();
9283
+ init_prewrite_secrets();
8236
9284
  });
8237
9285
 
8238
9286
  // src/db/task-status.ts
@@ -8364,10 +9412,10 @@ function redistributeStaleTasks(agentId, options, db) {
8364
9412
  const maxAge = options?.max_age_minutes ?? 60;
8365
9413
  const stale = getStaleTasks(maxAge, options?.project_id ? { project_id: options.project_id } : undefined, d);
8366
9414
  const limited = options?.limit ? stale.slice(0, options.limit) : stale;
8367
- const timestamp = now();
9415
+ const timestamp2 = now();
8368
9416
  const released = [];
8369
9417
  for (const t of limited) {
8370
- 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]);
8371
9419
  released.push({ ...t, locked_by: null, locked_at: null, status: "pending" });
8372
9420
  }
8373
9421
  const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
@@ -8839,7 +9887,7 @@ function resolveCreateSlug(input, projectId, db) {
8839
9887
  function createPlan(input, db) {
8840
9888
  const d = db || getDatabase();
8841
9889
  const id = uuid();
8842
- const timestamp = now();
9890
+ const timestamp2 = now();
8843
9891
  const projectId = input.project_id || null;
8844
9892
  const slug = resolveCreateSlug(input, projectId, d);
8845
9893
  const machineId = currentStorageMachineId(d);
@@ -8853,8 +9901,8 @@ function createPlan(input, db) {
8853
9901
  input.name,
8854
9902
  input.description || null,
8855
9903
  input.status || "active",
8856
- timestamp,
8857
- timestamp,
9904
+ timestamp2,
9905
+ timestamp2,
8858
9906
  machineId
8859
9907
  ]);
8860
9908
  return getPlan(id, d);
@@ -8991,7 +10039,7 @@ function maybeFilter(value) {
8991
10039
  function createTaskBoard(input, db) {
8992
10040
  const d = db || getDatabase();
8993
10041
  const id = uuid();
8994
- const timestamp = now();
10042
+ const timestamp2 = now();
8995
10043
  const scope = input.scope || "tasks";
8996
10044
  const lanes = normalizeLanes(scope, input.lanes);
8997
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)
@@ -9005,8 +10053,8 @@ function createTaskBoard(input, db) {
9005
10053
  input.agent_id || null,
9006
10054
  JSON.stringify(lanes),
9007
10055
  JSON.stringify(input.filters || {}),
9008
- timestamp,
9009
- timestamp
10056
+ timestamp2,
10057
+ timestamp2
9010
10058
  ]);
9011
10059
  return getTaskBoard(id, d);
9012
10060
  }
@@ -9407,14 +10455,14 @@ function storeArtifactContent(input) {
9407
10455
  return null;
9408
10456
  const sourceStat = statSync2(sourcePath);
9409
10457
  if (!sourceStat.isFile())
9410
- 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")}`);
9411
10459
  const sourceBuffer = readFileSync3(sourcePath);
9412
10460
  const sourceSha = sha256(sourceBuffer);
9413
10461
  const textLike = isTextLike(sourceBuffer, input.path);
9414
10462
  let storedBuffer = sourceBuffer;
9415
10463
  let redactionStatus = textLike ? "clean" : "binary_or_unknown";
9416
10464
  if (textLike) {
9417
- const redactedText = redactEvidenceText(sourceBuffer.toString("utf8"));
10465
+ const redactedText = sanitizePreWriteText(sourceBuffer.toString("utf8"), "artifact.content");
9418
10466
  storedBuffer = Buffer.from(redactedText);
9419
10467
  if (!storedBuffer.equals(sourceBuffer))
9420
10468
  redactionStatus = "redacted";
@@ -9448,7 +10496,7 @@ function storeArtifactContent(input) {
9448
10496
  expires_at: retentionDays === null ? null : retentionExpiresAt(createdAt, retentionDays)
9449
10497
  },
9450
10498
  source: {
9451
- path: input.path,
10499
+ path: sanitizePreWriteText(input.path, "artifact.source_path"),
9452
10500
  size_bytes: sourceBuffer.length,
9453
10501
  sha256: sourceSha
9454
10502
  }
@@ -9628,6 +10676,7 @@ var DEFAULT_DELETED_RETENTION_DAYS = 30;
9628
10676
  var init_artifact_store = __esm(() => {
9629
10677
  init_database();
9630
10678
  init_redaction();
10679
+ init_prewrite_secrets();
9631
10680
  });
9632
10681
 
9633
10682
  // src/db/comments.ts
@@ -9637,17 +10686,17 @@ function addComment(input, db) {
9637
10686
  throw new TaskNotFoundError(input.task_id);
9638
10687
  }
9639
10688
  const id = uuid();
9640
- const timestamp = now();
10689
+ const timestamp2 = now();
9641
10690
  d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
9642
10691
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
9643
10692
  id,
9644
10693
  input.task_id,
9645
10694
  input.agent_id || null,
9646
10695
  input.session_id || null,
9647
- redactEvidenceText(input.content),
10696
+ sanitizePreWriteText(input.content, "comment.content"),
9648
10697
  input.type || "comment",
9649
10698
  input.progress_pct ?? null,
9650
- timestamp
10699
+ timestamp2
9651
10700
  ]);
9652
10701
  return getComment(id, d);
9653
10702
  }
@@ -9671,21 +10720,21 @@ var init_comments = __esm(() => {
9671
10720
  init_types();
9672
10721
  init_database();
9673
10722
  init_tasks();
9674
- init_redaction();
10723
+ init_prewrite_secrets();
9675
10724
  });
9676
10725
 
9677
10726
  // src/db/task-files.ts
9678
10727
  function addTaskFile(input, db) {
9679
10728
  const d = db || getDatabase();
9680
10729
  const id = uuid();
9681
- const timestamp = now();
10730
+ const timestamp2 = now();
9682
10731
  const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
9683
10732
  if (existing) {
9684
- 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]);
9685
10734
  return getTaskFile(existing.id, d);
9686
10735
  }
9687
10736
  d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
9688
- 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]);
9689
10738
  return getTaskFile(id, d);
9690
10739
  }
9691
10740
  function getTaskFile(id, db) {
@@ -9702,8 +10751,8 @@ function findTasksByFile(path, db) {
9702
10751
  }
9703
10752
  function updateTaskFileStatus(taskId, path, status, agentId, db) {
9704
10753
  const d = db || getDatabase();
9705
- const timestamp = now();
9706
- 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]);
9707
10756
  const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
9708
10757
  return row;
9709
10758
  }
@@ -9748,8 +10797,16 @@ function rowToVerification(row) {
9748
10797
  function linkTaskToCommit(input, db) {
9749
10798
  const d = db || getDatabase();
9750
10799
  const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
9751
- const ciJson = input.ci_snapshot ? JSON.stringify(input.ci_snapshot) : null;
9752
- 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;
9753
10810
  if (existing) {
9754
10811
  d.run(`UPDATE task_commits SET
9755
10812
  message = COALESCE(?, message),
@@ -9765,17 +10822,17 @@ function linkTaskToCommit(input, db) {
9765
10822
  repo_path = COALESCE(?, repo_path),
9766
10823
  traceability = COALESCE(?, traceability)
9767
10824
  WHERE id = ?`, [
9768
- input.message ?? null,
9769
- input.author ?? null,
9770
- input.files_changed ? JSON.stringify(input.files_changed) : null,
10825
+ message,
10826
+ author,
10827
+ filesChanged,
9771
10828
  input.committed_at ?? null,
9772
- input.branch ?? null,
9773
- input.pr_url ?? null,
10829
+ branch,
10830
+ prUrl,
9774
10831
  input.pr_number ?? null,
9775
- input.pr_state ?? null,
10832
+ prState,
9776
10833
  ciJson,
9777
- input.release_tag ?? null,
9778
- input.repo_path ?? null,
10834
+ releaseTag,
10835
+ repoPath,
9779
10836
  traceJson,
9780
10837
  existing.id
9781
10838
  ]);
@@ -9789,17 +10846,17 @@ function linkTaskToCommit(input, db) {
9789
10846
  id,
9790
10847
  input.task_id,
9791
10848
  input.sha,
9792
- input.message ?? null,
9793
- input.author ?? null,
9794
- input.files_changed ? JSON.stringify(input.files_changed) : null,
10849
+ message,
10850
+ author,
10851
+ filesChanged,
9795
10852
  input.committed_at ?? null,
9796
- input.branch ?? null,
9797
- input.pr_url ?? null,
10853
+ branch,
10854
+ prUrl,
9798
10855
  input.pr_number ?? null,
9799
- input.pr_state ?? null,
10856
+ prState,
9800
10857
  ciJson,
9801
- input.release_tag ?? null,
9802
- input.repo_path ?? null,
10858
+ releaseTag,
10859
+ repoPath,
9803
10860
  traceJson ?? "{}",
9804
10861
  now()
9805
10862
  ]);
@@ -9828,13 +10885,16 @@ function addTaskVerification(input, db) {
9828
10885
  const d = db || getDatabase();
9829
10886
  const id = uuid();
9830
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;
9831
10891
  d.run("INSERT INTO task_verifications (id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
9832
10892
  id,
9833
10893
  input.task_id,
9834
- input.command,
10894
+ command,
9835
10895
  input.status || "unknown",
9836
- input.output_summary ?? null,
9837
- input.artifact_path ?? null,
10896
+ outputSummary,
10897
+ artifactPath,
9838
10898
  input.agent_id ?? null,
9839
10899
  runAt,
9840
10900
  now()
@@ -9864,6 +10924,7 @@ function getTaskTraceability(taskId, db) {
9864
10924
  }
9865
10925
  var init_task_commits = __esm(() => {
9866
10926
  init_database();
10927
+ init_prewrite_secrets();
9867
10928
  });
9868
10929
 
9869
10930
  // src/db/task-runs.ts
@@ -9974,7 +11035,7 @@ function startTaskRun(input, db) {
9974
11035
  if (!getTask(input.task_id, d))
9975
11036
  throw new TaskNotFoundError(input.task_id);
9976
11037
  const id = input.id ?? uuid();
9977
- const timestamp = input.started_at || now();
11038
+ const timestamp2 = input.started_at || now();
9978
11039
  if (input.claim && input.agent_id) {
9979
11040
  startTask(input.task_id, input.agent_id, d);
9980
11041
  }
@@ -9982,12 +11043,12 @@ function startTaskRun(input, db) {
9982
11043
  id,
9983
11044
  input.task_id,
9984
11045
  input.agent_id ?? null,
9985
- input.title ? redactEvidenceText(input.title) : null,
9986
- input.summary ? redactEvidenceText(input.summary) : null,
9987
- JSON.stringify(redactValue(input.metadata || {})),
9988
- timestamp,
9989
- timestamp,
9990
- 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
9991
11052
  ]);
9992
11053
  addTaskRunEvent({
9993
11054
  run_id: id,
@@ -9995,7 +11056,7 @@ function startTaskRun(input, db) {
9995
11056
  message: input.summary || input.title || "run started",
9996
11057
  data: { title: input.title, claim: Boolean(input.claim) },
9997
11058
  agent_id: input.agent_id,
9998
- created_at: timestamp
11059
+ created_at: timestamp2
9999
11060
  }, d);
10000
11061
  if (input.claim && input.agent_id) {
10001
11062
  addTaskRunEvent({
@@ -10004,7 +11065,7 @@ function startTaskRun(input, db) {
10004
11065
  message: `claimed by ${input.agent_id}`,
10005
11066
  data: { agent_id: input.agent_id },
10006
11067
  agent_id: input.agent_id,
10007
- created_at: timestamp
11068
+ created_at: timestamp2
10008
11069
  }, d);
10009
11070
  }
10010
11071
  const run = getTaskRun(id, d);
@@ -10019,7 +11080,7 @@ function beginTaskRunTransaction(input, db) {
10019
11080
  const d = db || getDatabase();
10020
11081
  if (!getTask(input.task_id, d))
10021
11082
  throw new TaskNotFoundError(input.task_id);
10022
- const timestamp = input.started_at || now();
11083
+ const timestamp2 = input.started_at || now();
10023
11084
  const key = normalizeTransactionKey(input);
10024
11085
  const existing = findTaskRunByTransactionKey(key, input.task_id, d);
10025
11086
  const dryRun = !input.apply;
@@ -10028,7 +11089,7 @@ function beginTaskRunTransaction(input, db) {
10028
11089
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10029
11090
  local_only: true,
10030
11091
  dry_run: dryRun,
10031
- processed_at: timestamp,
11092
+ processed_at: timestamp2,
10032
11093
  action: "matched",
10033
11094
  key,
10034
11095
  run: summarizeTaskRun(existing),
@@ -10041,7 +11102,7 @@ function beginTaskRunTransaction(input, db) {
10041
11102
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10042
11103
  local_only: true,
10043
11104
  dry_run: true,
10044
- processed_at: timestamp,
11105
+ processed_at: timestamp2,
10045
11106
  action: "preview",
10046
11107
  key,
10047
11108
  run: null,
@@ -10049,14 +11110,14 @@ function beginTaskRunTransaction(input, db) {
10049
11110
  commands: loopRunCommands(null, key)
10050
11111
  };
10051
11112
  }
10052
- const metadata = redactValue({
11113
+ const metadata = sanitizePreWriteValue({
10053
11114
  ...input.metadata || {},
10054
11115
  loop_transaction: {
10055
11116
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10056
11117
  idempotency_key: key,
10057
11118
  loop_id: input.loop_id ?? null,
10058
11119
  loop_run_id: input.loop_run_id ?? null,
10059
- first_seen_at: timestamp
11120
+ first_seen_at: timestamp2
10060
11121
  },
10061
11122
  idempotency_key: key
10062
11123
  });
@@ -10070,8 +11131,8 @@ function beginTaskRunTransaction(input, db) {
10070
11131
  input.loop_id ?? null,
10071
11132
  input.loop_run_id ?? null,
10072
11133
  JSON.stringify(metadata),
10073
- timestamp,
10074
- timestamp
11134
+ timestamp2,
11135
+ timestamp2
10075
11136
  ]);
10076
11137
  const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
10077
11138
  if (!transaction)
@@ -10089,16 +11150,16 @@ function beginTaskRunTransaction(input, db) {
10089
11150
  summary: input.summary,
10090
11151
  metadata,
10091
11152
  claim: input.claim,
10092
- started_at: timestamp
11153
+ started_at: timestamp2
10093
11154
  }, d);
10094
- 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]);
10095
11156
  return { run, action: "created" };
10096
11157
  })();
10097
11158
  return {
10098
11159
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10099
11160
  local_only: true,
10100
11161
  dry_run: false,
10101
- processed_at: timestamp,
11162
+ processed_at: timestamp2,
10102
11163
  action: created.action,
10103
11164
  key,
10104
11165
  run: summarizeTaskRun(created.run),
@@ -10113,19 +11174,19 @@ function addTaskRunEvent(input, db) {
10113
11174
  if (!run)
10114
11175
  throw new Error(`Run not found: ${input.run_id}`);
10115
11176
  const id = uuid();
10116
- const timestamp = input.created_at || now();
11177
+ const timestamp2 = input.created_at || now();
10117
11178
  d.run("INSERT INTO task_run_events (id, run_id, task_id, event_type, message, data, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
10118
11179
  id,
10119
11180
  run.id,
10120
11181
  run.task_id,
10121
11182
  input.event_type,
10122
- input.message ? redactEvidenceText(input.message) : null,
10123
- JSON.stringify(redactValue(input.data || {})),
11183
+ input.message ? sanitizePreWriteText(input.message, "run_event.message") : null,
11184
+ JSON.stringify(sanitizePreWriteValue(input.data || {}, "run_event.data")),
10124
11185
  input.agent_id ?? run.agent_id,
10125
- timestamp
11186
+ timestamp2
10126
11187
  ]);
10127
11188
  if (input.event_type === "comment" && input.message) {
10128
- 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);
10129
11190
  }
10130
11191
  return rowToEvent(d.query("SELECT * FROM task_run_events WHERE id = ?").get(id));
10131
11192
  }
@@ -10137,10 +11198,10 @@ function addTaskRunCommand(input, db) {
10137
11198
  throw new Error(`Run not found: ${input.run_id}`);
10138
11199
  const id = uuid();
10139
11200
  const status = input.status || "unknown";
10140
- const timestamp = now();
10141
- const command = redactEvidenceText(input.command);
10142
- const outputSummary = input.output_summary ? redactEvidenceText(input.output_summary) : null;
10143
- 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;
10144
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
10145
11206
  id,
10146
11207
  run.id,
@@ -10152,8 +11213,8 @@ function addTaskRunCommand(input, db) {
10152
11213
  artifactPath,
10153
11214
  input.agent_id ?? run.agent_id,
10154
11215
  input.started_at ?? null,
10155
- input.completed_at ?? timestamp,
10156
- timestamp
11216
+ input.completed_at ?? timestamp2,
11217
+ timestamp2
10157
11218
  ]);
10158
11219
  addTaskVerification({
10159
11220
  task_id: run.task_id,
@@ -10162,7 +11223,7 @@ function addTaskRunCommand(input, db) {
10162
11223
  output_summary: outputSummary ?? undefined,
10163
11224
  artifact_path: artifactPath ?? undefined,
10164
11225
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10165
- run_at: input.completed_at ?? timestamp
11226
+ run_at: input.completed_at ?? timestamp2
10166
11227
  }, d);
10167
11228
  addTaskRunEvent({
10168
11229
  run_id: run.id,
@@ -10181,7 +11242,7 @@ function addTaskRunCommand(input, db) {
10181
11242
  }
10182
11243
  },
10183
11244
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10184
- created_at: timestamp
11245
+ created_at: timestamp2
10185
11246
  }, d);
10186
11247
  return d.query("SELECT * FROM task_run_commands WHERE id = ?").get(id);
10187
11248
  }
@@ -10195,7 +11256,7 @@ function addTaskRunFile(input, db) {
10195
11256
  task_id: run.task_id,
10196
11257
  path: input.path,
10197
11258
  status: input.status || "modified",
10198
- note: input.note ? redactEvidenceText(input.note) : undefined,
11259
+ note: input.note ? sanitizePreWriteText(input.note, "run_file.note") : undefined,
10199
11260
  agent_id: input.agent_id ?? run.agent_id ?? undefined
10200
11261
  }, d);
10201
11262
  addTaskRunEvent({
@@ -10214,19 +11275,19 @@ function addTaskRunArtifact(input, db) {
10214
11275
  if (!run)
10215
11276
  throw new Error(`Run not found: ${input.run_id}`);
10216
11277
  const id = uuid();
10217
- const timestamp = now();
10218
- const path = redactEvidenceText(input.path);
10219
- const description = input.description ? redactEvidenceText(input.description) : null;
10220
- 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");
10221
11282
  let sizeBytes = input.size_bytes ?? null;
10222
11283
  let digest = input.sha256 ?? null;
10223
- 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;
10224
11285
  if (stored) {
10225
11286
  sizeBytes = stored.size_bytes;
10226
11287
  digest = stored.sha256;
10227
11288
  metadata["artifact_store"] = stored.store;
10228
11289
  } else if (input.store_content === true) {
10229
- throw new Error(`Artifact file not found: ${input.path}`);
11290
+ throw new Error(`Artifact file not found: ${path}`);
10230
11291
  }
10231
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
10232
11293
  id,
@@ -10239,7 +11300,7 @@ function addTaskRunArtifact(input, db) {
10239
11300
  digest,
10240
11301
  JSON.stringify(metadata),
10241
11302
  input.agent_id ?? run.agent_id,
10242
- timestamp
11303
+ timestamp2
10243
11304
  ]);
10244
11305
  addTaskRunEvent({
10245
11306
  run_id: run.id,
@@ -10247,7 +11308,7 @@ function addTaskRunArtifact(input, db) {
10247
11308
  message: description || path,
10248
11309
  data: { path, artifact_type: input.artifact_type, size_bytes: sizeBytes, sha256: digest, stored: Boolean(stored) },
10249
11310
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10250
- created_at: timestamp
11311
+ created_at: timestamp2
10251
11312
  }, d);
10252
11313
  return rowToArtifact(d.query("SELECT * FROM task_run_artifacts WHERE id = ?").get(id));
10253
11314
  }
@@ -10267,28 +11328,28 @@ function finishTaskRun(input, db) {
10267
11328
  const run = getTaskRun(runId, d);
10268
11329
  if (!run)
10269
11330
  throw new Error(`Run not found: ${input.run_id}`);
10270
- const timestamp = input.completed_at || now();
10271
- const summary = input.summary ? redactEvidenceText(input.summary) : null;
10272
- 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]);
10273
11334
  addTaskRunEvent({
10274
11335
  run_id: run.id,
10275
11336
  event_type: input.status,
10276
11337
  message: summary || `run ${input.status}`,
10277
11338
  data: { status: input.status },
10278
11339
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
10279
- created_at: timestamp
11340
+ created_at: timestamp2
10280
11341
  }, d);
10281
11342
  const updated = getTaskRun(run.id, d);
10282
11343
  emitLocalEventHooksQuiet({
10283
11344
  type: `run.${input.status}`,
10284
- 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 },
10285
11346
  databasePath: databasePathFromDatabase(d)
10286
11347
  });
10287
11348
  return updated;
10288
11349
  }
10289
11350
  function finishTaskRunTransaction(input, db) {
10290
11351
  const d = db || getDatabase();
10291
- const timestamp = input.completed_at || now();
11352
+ const timestamp2 = input.completed_at || now();
10292
11353
  const status = input.status || "completed";
10293
11354
  const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
10294
11355
  const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
@@ -10306,7 +11367,7 @@ function finishTaskRunTransaction(input, db) {
10306
11367
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10307
11368
  local_only: true,
10308
11369
  dry_run: dryRun,
10309
- processed_at: timestamp,
11370
+ processed_at: timestamp2,
10310
11371
  action: conflict ? "conflict" : "matched",
10311
11372
  key: resolvedKey,
10312
11373
  run: summarizeTaskRun(run),
@@ -10319,10 +11380,10 @@ function finishTaskRunTransaction(input, db) {
10319
11380
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10320
11381
  local_only: true,
10321
11382
  dry_run: true,
10322
- processed_at: timestamp,
11383
+ processed_at: timestamp2,
10323
11384
  action: "preview",
10324
11385
  key: resolvedKey,
10325
- 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 }),
10326
11387
  warnings: [],
10327
11388
  commands: loopRunCommands(run, resolvedKey)
10328
11389
  };
@@ -10332,13 +11393,13 @@ function finishTaskRunTransaction(input, db) {
10332
11393
  status,
10333
11394
  summary: input.summary,
10334
11395
  agent_id: input.agent_id,
10335
- completed_at: timestamp
11396
+ completed_at: timestamp2
10336
11397
  }, d);
10337
11398
  return {
10338
11399
  schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
10339
11400
  local_only: true,
10340
11401
  dry_run: false,
10341
- processed_at: timestamp,
11402
+ processed_at: timestamp2,
10342
11403
  action: "finished",
10343
11404
  key: resolvedKey,
10344
11405
  run: summarizeTaskRun(finished),
@@ -10368,7 +11429,7 @@ var init_task_runs = __esm(() => {
10368
11429
  init_artifact_store();
10369
11430
  init_event_emission_safety();
10370
11431
  init_event_hooks();
10371
- init_redaction();
11432
+ init_prewrite_secrets();
10372
11433
  init_types();
10373
11434
  init_comments();
10374
11435
  init_database();
@@ -10496,7 +11557,7 @@ function inWindow(event, query) {
10496
11557
  function createCalendarItem(input, db) {
10497
11558
  const d = db || getDatabase();
10498
11559
  const id = uuid();
10499
- const timestamp = now();
11560
+ const timestamp2 = now();
10500
11561
  const kind = input.kind || "work_block";
10501
11562
  d.run(`INSERT INTO local_calendar_items (
10502
11563
  id, kind, title, description, starts_at, ends_at, timezone, project_id, task_id, plan_id, run_id,
@@ -10515,8 +11576,8 @@ function createCalendarItem(input, db) {
10515
11576
  input.run_id || null,
10516
11577
  input.recurrence_rule || null,
10517
11578
  JSON.stringify(input.metadata || {}),
10518
- timestamp,
10519
- timestamp
11579
+ timestamp2,
11580
+ timestamp2
10520
11581
  ]);
10521
11582
  return getCalendarItem(id, d);
10522
11583
  }
@@ -10762,7 +11823,7 @@ var init_tasks = __esm(() => {
10762
11823
  // package.json
10763
11824
  var package_default = {
10764
11825
  name: "@hasna/todos",
10765
- version: "0.11.95",
11826
+ version: "0.12.2",
10766
11827
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
10767
11828
  type: "module",
10768
11829
  main: "dist/index.js",
@@ -11396,12 +12457,66 @@ function contract(input) {
11396
12457
  }
11397
12458
  };
11398
12459
  }
12460
+ function closedContract(input) {
12461
+ return {
12462
+ ...input,
12463
+ additionalProperties: false,
12464
+ evolution: {
12465
+ additionalFields: "forbidden",
12466
+ removingRequiredFields: "breaking",
12467
+ changingRequiredFieldTypes: "breaking",
12468
+ nullableToNonNullable: "breaking"
12469
+ }
12470
+ };
12471
+ }
11399
12472
  var idField = field("string", "Stable UUID or short identifier.");
11400
12473
  var nullableIdField = field(["string", "null"], "Stable identifier when attached to another object.", true);
11401
12474
  var isoDateField = field("string", "ISO-8601 timestamp string.");
11402
12475
  var nullableIsoDateField = field(["string", "null"], "ISO-8601 timestamp string, or null when not set.", true);
11403
12476
  var metadataField = field("object", "JSON object metadata. Unknown keys are extension data.");
11404
12477
  var tagsField = field("array", "Array of string tags.");
12478
+ var TODOS_AUTHORITATIVE_JSON_PROJECTIONS = [
12479
+ closedContract({
12480
+ id: "pr_group_ci_proof",
12481
+ name: "Authoritative PR Group CI Proof",
12482
+ description: "Closed successful provider-CI evidence bound to one repository, PR, base, and exact head before conditional merge authorization.",
12483
+ surfaces: ["api", "sdk"],
12484
+ stability: "stable",
12485
+ required: {
12486
+ provider: field("string", "Authoritative CI provider identity."),
12487
+ provider_run_id: field("string", "Provider-owned CI run or check-suite identity."),
12488
+ status: field("string", "Terminal successful CI status."),
12489
+ repository: field("string", "Canonical repository identity."),
12490
+ pr_number: field("integer", "Concrete pull-request number."),
12491
+ base_sha: field("string", "Exact base commit SHA."),
12492
+ head_sha: field("string", "Exact reviewed and merge-authorized head commit SHA.")
12493
+ },
12494
+ optional: {}
12495
+ }),
12496
+ closedContract({
12497
+ id: "pr_group_state_view",
12498
+ name: "Authoritative PR Group State View",
12499
+ description: "Closed package-owned projection for immutable task-to-PR lineage, repair accounting, append-only receipts, adapters, and diagnostics.",
12500
+ surfaces: ["cli", "api", "sdk"],
12501
+ stability: "stable",
12502
+ required: {
12503
+ schema_version: field("integer", "PR-group ledger schema version."),
12504
+ authoritative: field("boolean", "Always true for package-owned ledger projections."),
12505
+ authority: field("string", "Authority kind: local or remote."),
12506
+ group: field("object", "Immutable PR-group lineage and current durable state."),
12507
+ attempts: field("array", "Bounded writer-generation attempt history."),
12508
+ latest_event: field(["object", "null"], "Latest append-only lifecycle event, or null.", true),
12509
+ review_receipts: field("array", "Exact-head review receipts."),
12510
+ conditional_merge_receipts: field("array", "Sequence-aware conditional merge receipts."),
12511
+ merge_receipts: field("array", "Exact-head merge outcome receipts."),
12512
+ cleanup_receipts: field("array", "Durable cleanup authorization receipts."),
12513
+ cleanup_eligible: field("boolean", "Whether the complete cleanup proof has been accepted."),
12514
+ adapters: field("object", "Closed WorkRun, EvidenceRef, ProofBundle, and DecisionEnvelope projections."),
12515
+ diagnostics: field("object", "Bounded-projection completeness and limit diagnostics.")
12516
+ },
12517
+ optional: {}
12518
+ })
12519
+ ];
11405
12520
  var TODOS_JSON_CONTRACTS = [
11406
12521
  contract({
11407
12522
  id: "task",
@@ -13423,7 +14538,7 @@ function matchesType(value, expected) {
13423
14538
  return actual === "integer" && expected.includes("number");
13424
14539
  }
13425
14540
  function getJsonContract(contractId) {
13426
- return TODOS_JSON_CONTRACTS.find((contractItem) => contractItem.id === contractId) ?? null;
14541
+ return [...TODOS_JSON_CONTRACTS, ...TODOS_AUTHORITATIVE_JSON_PROJECTIONS].find((contractItem) => contractItem.id === contractId) ?? null;
13427
14542
  }
13428
14543
  function validateJsonContract(contractId, value) {
13429
14544
  const contractItem = getJsonContract(contractId);
@@ -13433,6 +14548,7 @@ function validateJsonContract(contractId, value) {
13433
14548
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
13434
14549
  const missingRequired = [];
13435
14550
  const typeMismatches = [];
14551
+ const unknownFields = contractItem.additionalProperties ? [] : Object.keys(record).filter((fieldName) => !(fieldName in contractItem.required) && !(fieldName in contractItem.optional));
13436
14552
  for (const [fieldName, fieldContract] of Object.entries(contractItem.required)) {
13437
14553
  if (!(fieldName in record)) {
13438
14554
  missingRequired.push(fieldName);
@@ -13445,7 +14561,7 @@ function validateJsonContract(contractId, value) {
13445
14561
  }
13446
14562
  }
13447
14563
  return {
13448
- ok: missingRequired.length === 0 && typeMismatches.length === 0,
14564
+ ok: missingRequired.length === 0 && typeMismatches.length === 0 && unknownFields.length === 0,
13449
14565
  contractId,
13450
14566
  missingRequired,
13451
14567
  typeMismatches
@@ -13457,7 +14573,8 @@ function createJsonContractsManifest(options = {}) {
13457
14573
  schemaVersion: 1,
13458
14574
  generatedAt: options.generatedAt ?? new Date().toISOString(),
13459
14575
  package: source2(version),
13460
- contracts: TODOS_JSON_CONTRACTS
14576
+ contracts: TODOS_JSON_CONTRACTS,
14577
+ authoritativeProjections: TODOS_AUTHORITATIVE_JSON_PROJECTIONS
13461
14578
  };
13462
14579
  }
13463
14580
  var TODOS_JSON_CONTRACTS_MANIFEST = createJsonContractsManifest({
@@ -13473,6 +14590,7 @@ init_tasks();
13473
14590
  init_artifact_store();
13474
14591
  init_sync_utils();
13475
14592
  init_redaction();
14593
+ init_prewrite_secrets();
13476
14594
  var TODOS_LOCAL_BRIDGE_KIND = "hasna.todos.local-bridge";
13477
14595
  var TODOS_LOCAL_BRIDGE_SCHEMA_VERSION = 1;
13478
14596
  var dataKeys = [
@@ -13589,6 +14707,24 @@ var tableByKey = {
13589
14707
  local_calendar_items: "local_calendar_items"
13590
14708
  };
13591
14709
  var jsonColumns = new Set(["metadata", "tags", "data", "files_changed", "filters", "lanes"]);
14710
+ var structuralColumns = new Set([
14711
+ "id",
14712
+ "task_id",
14713
+ "run_id",
14714
+ "project_id",
14715
+ "plan_id",
14716
+ "task_list_id",
14717
+ "parent_id",
14718
+ "cycle_id",
14719
+ "depends_on",
14720
+ "external_project_id",
14721
+ "external_task_id",
14722
+ "short_id",
14723
+ "sha",
14724
+ "machine_id",
14725
+ "version",
14726
+ "schema_version"
14727
+ ]);
13592
14728
  function packageSource(version) {
13593
14729
  return {
13594
14730
  packageName: "@hasna/todos",
@@ -13683,7 +14819,7 @@ function createLocalBridgeBundle(options = {}, db) {
13683
14819
  const runRows = queryByTaskIds(d, "SELECT * FROM task_runs WHERE task_id IN (__TASK_IDS__) ORDER BY started_at, created_at", taskIds);
13684
14820
  const runIds = runRows.map((row) => String(row.id));
13685
14821
  const project = options.project_id ? d.query("SELECT * FROM projects WHERE id = ?").get(options.project_id) : null;
13686
- const data = redactValue({
14822
+ const rawData = {
13687
14823
  projects: options.project_id ? project ? [project] : [] : d.query("SELECT * FROM projects ORDER BY name").all(),
13688
14824
  task_lists: (options.project_id ? d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM task_lists ORDER BY name").all()).map(rowToTaskList2),
13689
14825
  plans: options.project_id ? d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at").all(options.project_id) : d.query("SELECT * FROM plans ORDER BY created_at").all(),
@@ -13701,7 +14837,8 @@ function createLocalBridgeBundle(options = {}, db) {
13701
14837
  saved_views: (options.project_id ? d.query("SELECT * FROM saved_search_views WHERE json_extract(filters, '$.project_id') = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM saved_search_views ORDER BY name").all()).map(rowToSavedView),
13702
14838
  task_boards: (options.project_id ? d.query("SELECT * FROM task_boards WHERE project_id = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM task_boards ORDER BY name").all()).map(rowToTaskBoard2),
13703
14839
  local_calendar_items: (options.project_id ? d.query("SELECT * FROM local_calendar_items WHERE project_id = ? ORDER BY starts_at, created_at").all(options.project_id) : d.query("SELECT * FROM local_calendar_items ORDER BY starts_at, created_at").all()).map(rowToCalendarItem2)
13704
- });
14840
+ };
14841
+ const data = options.redaction === "unsafe_plaintext" ? rawData : sanitizePreWriteValue(redactValue(rawData), "local_bridge.export");
13705
14842
  const artifactContents = data.run_artifacts.map((artifact) => exportStoredArtifactContent({
13706
14843
  id: artifact.id,
13707
14844
  path: artifact.path,
@@ -13789,6 +14926,13 @@ function prepareValue(column, value) {
13789
14926
  return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
13790
14927
  return value === undefined ? null : value;
13791
14928
  }
14929
+ function sanitizeImportRecord(tableKey, row) {
14930
+ const sanitized = {};
14931
+ for (const [column, value] of Object.entries(row)) {
14932
+ sanitized[column] = structuralColumns.has(column) ? value : sanitizePreWriteValue(value, `local_bridge.${tableKey}.${column}`);
14933
+ }
14934
+ return sanitized;
14935
+ }
13792
14936
  function slugifyPlanValue(value) {
13793
14937
  return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
13794
14938
  }
@@ -13823,10 +14967,11 @@ function insertRecord(db, tableKey, row) {
13823
14967
  const table = tableByKey[tableKey];
13824
14968
  const columns = insertColumns[tableKey];
13825
14969
  const placeholders = columns.map(() => "?").join(", ");
13826
- const params = columns.map((column) => prepareValue(column, row[column]));
14970
+ const safeRow = sanitizeImportRecord(tableKey, row);
14971
+ const params = columns.map((column) => prepareValue(column, safeRow[column]));
13827
14972
  const result = db.run(`INSERT OR IGNORE INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`, params);
13828
14973
  if (tableKey === "tasks" && result.changes > 0) {
13829
- const tags = parseJsonArray2(row.tags);
14974
+ const tags = parseJsonArray2(safeRow.tags);
13830
14975
  const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
13831
14976
  for (const tag of tags) {
13832
14977
  if (tag)
@@ -17287,14 +18432,14 @@ function getTaskContract(taskId, db) {
17287
18432
  }
17288
18433
  function requestTaskReview(input, db) {
17289
18434
  const task2 = taskOrThrow(input.task_id, db);
17290
- const timestamp = now();
18435
+ const timestamp2 = now();
17291
18436
  const previous = readReview(task2.id, task2.metadata);
17292
18437
  const entry = {
17293
18438
  state: "requested",
17294
18439
  actor: input.requester,
17295
18440
  notes: input.notes ?? null,
17296
18441
  changes_requested: [],
17297
- at: timestamp
18442
+ at: timestamp2
17298
18443
  };
17299
18444
  const review = {
17300
18445
  task_id: task2.id,
@@ -17303,7 +18448,7 @@ function requestTaskReview(input, db) {
17303
18448
  reviewer: input.reviewer ?? previous.reviewer,
17304
18449
  notes: input.notes ?? previous.notes,
17305
18450
  changes_requested: [],
17306
- requested_at: timestamp,
18451
+ requested_at: timestamp2,
17307
18452
  reviewed_at: null,
17308
18453
  history: [...previous.history, entry]
17309
18454
  };
@@ -17312,7 +18457,7 @@ function requestTaskReview(input, db) {
17312
18457
  }
17313
18458
  function recordTaskReview(input, db) {
17314
18459
  const task2 = taskOrThrow(input.task_id, db);
17315
- const timestamp = now();
18460
+ const timestamp2 = now();
17316
18461
  const previous = readReview(task2.id, task2.metadata);
17317
18462
  const changes = cleanList(input.changes_requested);
17318
18463
  const entry = {
@@ -17320,7 +18465,7 @@ function recordTaskReview(input, db) {
17320
18465
  actor: input.reviewer,
17321
18466
  notes: input.notes ?? null,
17322
18467
  changes_requested: changes,
17323
- at: timestamp
18468
+ at: timestamp2
17324
18469
  };
17325
18470
  const review = {
17326
18471
  task_id: task2.id,
@@ -17330,7 +18475,7 @@ function recordTaskReview(input, db) {
17330
18475
  notes: input.notes ?? previous.notes,
17331
18476
  changes_requested: input.state === "changes_requested" ? changes : [],
17332
18477
  requested_at: previous.requested_at,
17333
- reviewed_at: timestamp,
18478
+ reviewed_at: timestamp2,
17334
18479
  history: [...previous.history, entry]
17335
18480
  };
17336
18481
  writeMetadata(task2.id, { ...task2.metadata, [REVIEW_KEY]: review }, db);
@@ -17405,7 +18550,7 @@ function taskOrThrow2(taskId, db) {
17405
18550
  return task2;
17406
18551
  }
17407
18552
  function normalizeRule(input, existing) {
17408
- const timestamp = now();
18553
+ const timestamp2 = now();
17409
18554
  const name = input.name.trim();
17410
18555
  if (!name)
17411
18556
  throw new Error("Review routing rule name is required");
@@ -17417,8 +18562,8 @@ function normalizeRule(input, existing) {
17417
18562
  tags: input.tags === undefined ? existing?.tags ?? [] : cleanList2(input.tags),
17418
18563
  priorities: input.priorities === undefined ? existing?.priorities ?? [] : cleanPriorityList(input.priorities),
17419
18564
  project_id: input.project_id ?? existing?.project_id ?? null,
17420
- created_at: existing?.created_at ?? timestamp,
17421
- updated_at: timestamp
18565
+ created_at: existing?.created_at ?? timestamp2,
18566
+ updated_at: timestamp2
17422
18567
  };
17423
18568
  }
17424
18569
  function readQueue(task2) {
@@ -17626,7 +18771,7 @@ function requestReviewQueue(input, db) {
17626
18771
  notes: input.notes ?? input.reason
17627
18772
  }, d);
17628
18773
  const updatedTask = taskOrThrow2(task2.id, d);
17629
- const timestamp = now();
18774
+ const timestamp2 = now();
17630
18775
  const previous = readQueue(updatedTask);
17631
18776
  const queue = {
17632
18777
  schema_version: 1,
@@ -17639,10 +18784,10 @@ function requestReviewQueue(input, db) {
17639
18784
  notes: input.notes ?? previous?.notes ?? null,
17640
18785
  changes_requested: [],
17641
18786
  routing_rule: route.rule,
17642
- requested_at: timestamp,
18787
+ requested_at: timestamp2,
17643
18788
  claimed_at: null,
17644
18789
  decided_at: null,
17645
- updated_at: timestamp,
18790
+ updated_at: timestamp2,
17646
18791
  history: appendHistory(previous ?? {
17647
18792
  schema_version: 1,
17648
18793
  state: "requested",
@@ -17657,7 +18802,7 @@ function requestReviewQueue(input, db) {
17657
18802
  requested_at: null,
17658
18803
  claimed_at: null,
17659
18804
  decided_at: null,
17660
- updated_at: timestamp,
18805
+ updated_at: timestamp2,
17661
18806
  history: []
17662
18807
  }, "requested", input.requester, input.notes ?? input.reason)
17663
18808
  };
@@ -17669,14 +18814,14 @@ function claimReviewItem(input, db) {
17669
18814
  const previous = derivedQueue(task2, d);
17670
18815
  if (!previous)
17671
18816
  throw new Error(`Task is not in a review queue: ${task2.id}`);
17672
- const timestamp = now();
18817
+ const timestamp2 = now();
17673
18818
  const queue = {
17674
18819
  ...previous,
17675
18820
  state: "claimed",
17676
18821
  reviewer: input.reviewer,
17677
18822
  claimed_by: input.reviewer,
17678
- claimed_at: timestamp,
17679
- updated_at: timestamp,
18823
+ claimed_at: timestamp2,
18824
+ updated_at: timestamp2,
17680
18825
  history: appendHistory(previous, "claimed", input.reviewer, input.note)
17681
18826
  };
17682
18827
  return writeQueue(task2, queue, input.reviewer, "claimed", d);
@@ -17689,15 +18834,15 @@ function approveReviewItem(input, db) {
17689
18834
  throw new Error(`Task is not in a review queue: ${task2.id}`);
17690
18835
  recordTaskReview({ task_id: task2.id, state: "approved", reviewer: input.reviewer, notes: input.note }, d);
17691
18836
  const updatedTask = taskOrThrow2(task2.id, d);
17692
- const timestamp = now();
18837
+ const timestamp2 = now();
17693
18838
  const queue = {
17694
18839
  ...previous,
17695
18840
  state: "approved",
17696
18841
  reviewer: input.reviewer,
17697
18842
  claimed_by: previous.claimed_by ?? input.reviewer,
17698
18843
  changes_requested: [],
17699
- decided_at: timestamp,
17700
- updated_at: timestamp,
18844
+ decided_at: timestamp2,
18845
+ updated_at: timestamp2,
17701
18846
  history: appendHistory(previous, "approved", input.reviewer, input.note)
17702
18847
  };
17703
18848
  return writeQueue(updatedTask, queue, input.reviewer, "approved", d);
@@ -17717,15 +18862,15 @@ function returnReviewItem(input, db) {
17717
18862
  changes_requested: changes
17718
18863
  }, d);
17719
18864
  const updatedTask = taskOrThrow2(task2.id, d);
17720
- const timestamp = now();
18865
+ const timestamp2 = now();
17721
18866
  const queue = {
17722
18867
  ...previous,
17723
18868
  state: "returned",
17724
18869
  reviewer: input.reviewer,
17725
18870
  claimed_by: previous.claimed_by ?? input.reviewer,
17726
18871
  changes_requested: changes,
17727
- decided_at: timestamp,
17728
- updated_at: timestamp,
18872
+ decided_at: timestamp2,
18873
+ updated_at: timestamp2,
17729
18874
  history: appendHistory(previous, "returned", input.reviewer, input.note)
17730
18875
  };
17731
18876
  return writeQueue(updatedTask, queue, input.reviewer, "returned", d);
@@ -17738,15 +18883,15 @@ function reopenReviewItem(input, db) {
17738
18883
  throw new Error(`Task is not in a review queue: ${task2.id}`);
17739
18884
  recordTaskReview({ task_id: task2.id, state: "reopened", reviewer: input.reviewer, notes: input.note }, d);
17740
18885
  const updatedTask = taskOrThrow2(task2.id, d);
17741
- const timestamp = now();
18886
+ const timestamp2 = now();
17742
18887
  const queue = {
17743
18888
  ...previous,
17744
18889
  state: "reopened",
17745
18890
  reviewer: input.reviewer,
17746
18891
  claimed_by: null,
17747
18892
  changes_requested: [],
17748
- decided_at: timestamp,
17749
- updated_at: timestamp,
18893
+ decided_at: timestamp2,
18894
+ updated_at: timestamp2,
17750
18895
  history: appendHistory(previous, "reopened", input.reviewer, input.note)
17751
18896
  };
17752
18897
  return writeQueue(updatedTask, queue, input.reviewer, "reopened", d);
@@ -17791,7 +18936,7 @@ init_plans();
17791
18936
  init_task_runs();
17792
18937
  init_config2();
17793
18938
  var LOCAL_ROADMAP_SCHEMA_VERSION = 1;
17794
- function timestamp() {
18939
+ function timestamp2() {
17795
18940
  return new Date().toISOString();
17796
18941
  }
17797
18942
  function newId(prefix) {
@@ -17882,7 +19027,7 @@ function createRoadmap(input) {
17882
19027
  if (!name)
17883
19028
  throw new Error("Roadmap name is required");
17884
19029
  const store = readStore();
17885
- const now3 = timestamp();
19030
+ const now3 = timestamp2();
17886
19031
  const roadmap = {
17887
19032
  id: newId("roadmap"),
17888
19033
  name,
@@ -17924,7 +19069,7 @@ function updateRoadmap(idOrName, input) {
17924
19069
  owner: input.owner === undefined ? existing.owner : cleanString(input.owner),
17925
19070
  agent_id: input.agent_id === undefined ? existing.agent_id : cleanString(input.agent_id),
17926
19071
  release: input.release === undefined ? existing.release : cleanString(input.release),
17927
- updated_at: timestamp()
19072
+ updated_at: timestamp2()
17928
19073
  };
17929
19074
  store.roadmaps[id] = updated;
17930
19075
  writeStore(store);
@@ -17954,7 +19099,7 @@ function createMilestone(input) {
17954
19099
  const title = input.title.trim();
17955
19100
  if (!title)
17956
19101
  throw new Error("Milestone title is required");
17957
- const now3 = timestamp();
19102
+ const now3 = timestamp2();
17958
19103
  const milestone = {
17959
19104
  id: newId("milestone"),
17960
19105
  roadmap_id: roadmapId,
@@ -18001,7 +19146,7 @@ function updateMilestone(idOrTitle, input) {
18001
19146
  run_ids: input.run_ids === undefined ? existing.run_ids : cleanList3(input.run_ids),
18002
19147
  release: input.release === undefined ? existing.release : cleanString(input.release),
18003
19148
  tags: input.tags === undefined ? existing.tags : cleanList3(input.tags),
18004
- updated_at: timestamp()
19149
+ updated_at: timestamp2()
18005
19150
  };
18006
19151
  store.milestones[id] = updated;
18007
19152
  writeStore(store);
@@ -18016,11 +19161,11 @@ function deleteMilestone(idOrTitle) {
18016
19161
  delete store.milestones[id];
18017
19162
  const roadmap = store.roadmaps[milestone.roadmap_id];
18018
19163
  if (roadmap)
18019
- store.roadmaps[roadmap.id] = { ...roadmap, milestone_ids: roadmap.milestone_ids.filter((item) => item !== id), updated_at: timestamp() };
19164
+ store.roadmaps[roadmap.id] = { ...roadmap, milestone_ids: roadmap.milestone_ids.filter((item) => item !== id), updated_at: timestamp2() };
18020
19165
  for (const key of Object.keys(store.releases)) {
18021
19166
  const release = store.releases[key];
18022
19167
  if (release.milestone_ids.includes(id))
18023
- store.releases[key] = { ...release, milestone_ids: release.milestone_ids.filter((item) => item !== id), updated_at: timestamp() };
19168
+ store.releases[key] = { ...release, milestone_ids: release.milestone_ids.filter((item) => item !== id), updated_at: timestamp2() };
18024
19169
  }
18025
19170
  writeStore(store);
18026
19171
  return true;
@@ -18033,7 +19178,7 @@ function upsertReleaseGroup(input) {
18033
19178
  throw new Error("Release group name is required");
18034
19179
  const key = releaseKey(roadmapId, name);
18035
19180
  const existing = store.releases[key];
18036
- const now3 = timestamp();
19181
+ const now3 = timestamp2();
18037
19182
  const release = {
18038
19183
  name,
18039
19184
  version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
@@ -18102,7 +19247,7 @@ function exportRoadmapBundle(idOrName) {
18102
19247
  schema_version: LOCAL_ROADMAP_SCHEMA_VERSION,
18103
19248
  kind: "hasna.todos.roadmap-bundle",
18104
19249
  local_only: true,
18105
- exported_at: timestamp(),
19250
+ exported_at: timestamp2(),
18106
19251
  roadmap,
18107
19252
  milestones: roadmap.milestone_ids.map((milestoneId) => store.milestones[milestoneId]).filter(isMilestone),
18108
19253
  releases: listReleaseGroups(roadmap.id)
@@ -18753,7 +19898,7 @@ function issueToTask(issue, opts) {
18753
19898
  }
18754
19899
 
18755
19900
  // src/db/inbox.ts
18756
- init_task_runs();
19901
+ init_prewrite_secrets();
18757
19902
  init_database();
18758
19903
  init_tasks();
18759
19904
  function parseMetadata3(value) {
@@ -18774,7 +19919,7 @@ function compactWhitespace(value) {
18774
19919
  }
18775
19920
  function fingerprintInboxInput(input) {
18776
19921
  const sourceType = input.source_type || detectInboxSourceType(input.body, input.source_url);
18777
- const normalized = compactWhitespace(redactEvidenceText(input.body)).slice(0, 8000);
19922
+ const normalized = compactWhitespace(sanitizePreWriteText(input.body, "inbox.fingerprint")).slice(0, 8000);
18778
19923
  return createHash6("sha256").update(`${sourceType}
18779
19924
  ${input.source_url || ""}
18780
19925
  ${normalized}`).digest("hex");
@@ -18803,34 +19948,20 @@ function deriveInboxTitle(body, sourceType, sourceUrl) {
18803
19948
  return `${prefix}: ${compactWhitespace(firstUseful).slice(0, 90)}`;
18804
19949
  }
18805
19950
  function redactedMetadata(value) {
18806
- const redacted = {};
18807
- for (const [key, child] of Object.entries(value)) {
18808
- if (/api[_-]?key|token|secret|password/i.test(key)) {
18809
- redacted[key] = "[REDACTED]";
18810
- } else if (typeof child === "string") {
18811
- redacted[key] = redactEvidenceText(child);
18812
- } else if (child && typeof child === "object" && !Array.isArray(child)) {
18813
- redacted[key] = redactedMetadata(child);
18814
- } else if (Array.isArray(child)) {
18815
- redacted[key] = child.map((item) => typeof item === "string" ? redactEvidenceText(item) : item);
18816
- } else {
18817
- redacted[key] = child;
18818
- }
18819
- }
18820
- return redacted;
19951
+ return sanitizePreWriteValue(value, "inbox.metadata");
18821
19952
  }
18822
19953
  function createInboxItem(input, db) {
18823
19954
  const d = db || getDatabase();
18824
- const body = redactEvidenceText(input.body);
19955
+ const body = sanitizePreWriteText(input.body, "inbox.body");
18825
19956
  const sourceType = input.source_type || detectInboxSourceType(body, input.source_url);
18826
- const title = redactEvidenceText(input.title || deriveInboxTitle(body, sourceType, input.source_url));
19957
+ const title = sanitizePreWriteText(input.title || deriveInboxTitle(body, sourceType, input.source_url), "inbox.title");
18827
19958
  const fingerprint = fingerprintInboxInput({ body, source_type: sourceType, source_url: input.source_url });
18828
19959
  const existing = d.query("SELECT * FROM inbox_items WHERE fingerprint = ?").get(fingerprint);
18829
19960
  if (existing) {
18830
19961
  const item = rowToInboxItem(existing);
18831
19962
  return { item, task: item.task_id ? getTask(item.task_id, d) : null, duplicate: true };
18832
19963
  }
18833
- const timestamp2 = now();
19964
+ const timestamp3 = now();
18834
19965
  let task2 = null;
18835
19966
  const metadata = {
18836
19967
  ...input.metadata || {},
@@ -18842,8 +19973,8 @@ function createInboxItem(input, db) {
18842
19973
  title,
18843
19974
  description: [
18844
19975
  `**Inbox source:** ${sourceType}`,
18845
- input.source_name ? `**Source name:** ${redactEvidenceText(input.source_name)}` : null,
18846
- input.source_url ? `**Source URL:** ${redactEvidenceText(input.source_url)}` : null,
19976
+ input.source_name ? `**Source name:** ${sanitizePreWriteText(input.source_name, "inbox.source_name")}` : null,
19977
+ input.source_url ? `**Source URL:** ${sanitizePreWriteText(input.source_url, "inbox.source_url")}` : null,
18847
19978
  `**Captured context:**
18848
19979
  \`\`\`
18849
19980
  ${body.slice(0, 4000)}
@@ -18864,15 +19995,15 @@ ${body.slice(0, 4000)}
18864
19995
  id,
18865
19996
  task2?.id ?? null,
18866
19997
  sourceType,
18867
- input.source_name ? redactEvidenceText(input.source_name) : null,
18868
- input.source_url ? redactEvidenceText(input.source_url) : null,
19998
+ input.source_name ? sanitizePreWriteText(input.source_name, "inbox.source_name") : null,
19999
+ input.source_url ? sanitizePreWriteText(input.source_url, "inbox.source_url") : null,
18869
20000
  title,
18870
20001
  body,
18871
20002
  fingerprint,
18872
20003
  input.status || "triaged",
18873
20004
  JSON.stringify(redactedMetadata(metadata)),
18874
- timestamp2,
18875
- timestamp2
20005
+ timestamp3,
20006
+ timestamp3
18876
20007
  ]);
18877
20008
  return { item: getInboxItem(id, d), task: task2, duplicate: false };
18878
20009
  }
@@ -18926,7 +20057,7 @@ function rowToRelationship(row) {
18926
20057
  function addTaskRelationship(input, db) {
18927
20058
  const d = db || getDatabase();
18928
20059
  const id = uuid();
18929
- const timestamp2 = now();
20060
+ const timestamp3 = now();
18930
20061
  if (input.source_task_id === input.target_task_id) {
18931
20062
  throw new Error("Cannot create a relationship between a task and itself");
18932
20063
  }
@@ -18952,7 +20083,7 @@ function addTaskRelationship(input, db) {
18952
20083
  input.relationship_type,
18953
20084
  JSON.stringify(input.metadata || {}),
18954
20085
  input.created_by || null,
18955
- timestamp2
20086
+ timestamp3
18956
20087
  ]);
18957
20088
  return getTaskRelationship(id, d);
18958
20089
  }
@@ -20181,15 +21312,15 @@ function storedReportSummary(report) {
20181
21312
  occurred_at: report.occurred_at ?? null
20182
21313
  };
20183
21314
  }
20184
- function testerMetadata(report, fingerprint2, previous, timestamp2) {
21315
+ function testerMetadata(report, fingerprint2, previous, timestamp3) {
20185
21316
  const occurrenceCount = typeof previous?.["occurrence_count"] === "number" ? previous["occurrence_count"] + 1 : 1;
20186
21317
  const previousRecent = previous?.["recent_reports"];
20187
21318
  const recent = Array.isArray(previousRecent) ? previousRecent : [];
20188
21319
  return {
20189
21320
  schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
20190
21321
  fingerprint: fingerprint2,
20191
- first_seen_at: asString3(previous?.["first_seen_at"]) || timestamp2,
20192
- last_seen_at: timestamp2,
21322
+ first_seen_at: asString3(previous?.["first_seen_at"]) || timestamp3,
21323
+ last_seen_at: timestamp3,
20193
21324
  occurrence_count: occurrenceCount,
20194
21325
  latest_report: storedReportSummary(report),
20195
21326
  recent_reports: [...recent.slice(-4), storedReportSummary(report)]
@@ -20229,7 +21360,7 @@ function commandsFor(task2) {
20229
21360
  `todos dedupe scan --threshold 0.8 --json`
20230
21361
  ];
20231
21362
  }
20232
- function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db) {
21363
+ function updateExistingTask(task2, report, fingerprint2, input, timestamp3, db) {
20233
21364
  if (input.update_existing === false)
20234
21365
  return { action: "matched", task: task2 };
20235
21366
  const previous = asObject3(task2.metadata["tester_issue_report"]);
@@ -20249,7 +21380,7 @@ function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db)
20249
21380
  ...sourceMetadata(report),
20250
21381
  external_ref: fingerprint2,
20251
21382
  tester_issue_fingerprint: fingerprint2,
20252
- tester_issue_report: testerMetadata(report, fingerprint2, previous, timestamp2)
21383
+ tester_issue_report: testerMetadata(report, fingerprint2, previous, timestamp3)
20253
21384
  },
20254
21385
  ...input.assigned_to !== undefined ? { assigned_to: input.assigned_to } : {},
20255
21386
  task_type: task2.task_type || "bug"
@@ -20258,7 +21389,7 @@ function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db)
20258
21389
  }
20259
21390
  function upsertTesterIssueReport(input, db) {
20260
21391
  const d = db || getDatabase();
20261
- const timestamp2 = now();
21392
+ const timestamp3 = now();
20262
21393
  const warnings = [];
20263
21394
  const report = normalizeTesterIssueReport(input.report, input.default_priority || "medium");
20264
21395
  const fingerprint2 = fingerprintTesterIssueReport(report);
@@ -20269,7 +21400,7 @@ function upsertTesterIssueReport(input, db) {
20269
21400
  schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
20270
21401
  local_only: true,
20271
21402
  dry_run: true,
20272
- processed_at: timestamp2,
21403
+ processed_at: timestamp3,
20273
21404
  action,
20274
21405
  fingerprint: fingerprint2,
20275
21406
  report,
@@ -20279,12 +21410,12 @@ function upsertTesterIssueReport(input, db) {
20279
21410
  };
20280
21411
  }
20281
21412
  if (existing) {
20282
- const updated = updateExistingTask(existing, report, fingerprint2, input, timestamp2, d);
21413
+ const updated = updateExistingTask(existing, report, fingerprint2, input, timestamp3, d);
20283
21414
  return {
20284
21415
  schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
20285
21416
  local_only: true,
20286
21417
  dry_run: false,
20287
- processed_at: timestamp2,
21418
+ processed_at: timestamp3,
20288
21419
  action: updated.action,
20289
21420
  fingerprint: fingerprint2,
20290
21421
  report,
@@ -20304,7 +21435,7 @@ function upsertTesterIssueReport(input, db) {
20304
21435
  ...sourceMetadata(report),
20305
21436
  external_ref: fingerprint2,
20306
21437
  tester_issue_fingerprint: fingerprint2,
20307
- tester_issue_report: testerMetadata(report, fingerprint2, null, timestamp2),
21438
+ tester_issue_report: testerMetadata(report, fingerprint2, null, timestamp3),
20308
21439
  tester_issue_report_raw: redactValue({
20309
21440
  ...report,
20310
21441
  evidence: report.evidence ? {
@@ -20323,7 +21454,7 @@ function upsertTesterIssueReport(input, db) {
20323
21454
  schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
20324
21455
  local_only: true,
20325
21456
  dry_run: false,
20326
- processed_at: timestamp2,
21457
+ processed_at: timestamp3,
20327
21458
  action: "created",
20328
21459
  fingerprint: fingerprint2,
20329
21460
  report,
@@ -20420,10 +21551,10 @@ function parseQuietHours(value, timezone = "local") {
20420
21551
  parseClock(end);
20421
21552
  return { start, end, timezone };
20422
21553
  }
20423
- function isQuietTime(quiet, timestamp2) {
21554
+ function isQuietTime(quiet, timestamp3) {
20424
21555
  if (!quiet)
20425
21556
  return false;
20426
- const date = new Date(timestamp2);
21557
+ const date = new Date(timestamp3);
20427
21558
  if (Number.isNaN(date.getTime()))
20428
21559
  return false;
20429
21560
  const minutes = quiet.timezone === "utc" ? date.getUTCHours() * 60 + date.getUTCMinutes() : date.getHours() * 60 + date.getMinutes();
@@ -20442,7 +21573,7 @@ function normalizeRule2(input, existing) {
20442
21573
  throw new Error(`unsupported terminal notification severity: ${severity}`);
20443
21574
  if (!VALID_FORMATS.has(format))
20444
21575
  throw new Error(`unsupported terminal notification format: ${format}`);
20445
- const timestamp2 = new Date().toISOString();
21576
+ const timestamp3 = new Date().toISOString();
20446
21577
  return {
20447
21578
  ...existing,
20448
21579
  name: safeName2(input.name),
@@ -20457,8 +21588,8 @@ function normalizeRule2(input, existing) {
20457
21588
  project_ids: normalizeList(input.project_ids) ?? existing?.project_ids,
20458
21589
  contains: normalizeList(input.contains) ?? existing?.contains,
20459
21590
  quiet_hours: input.quiet_hours ?? existing?.quiet_hours,
20460
- created_at: existing?.created_at || timestamp2,
20461
- updated_at: timestamp2
21591
+ created_at: existing?.created_at || timestamp3,
21592
+ updated_at: timestamp3
20462
21593
  };
20463
21594
  }
20464
21595
  function eventSeverity(eventType) {
@@ -20519,7 +21650,7 @@ function removeTerminalNotificationRule(name) {
20519
21650
  return true;
20520
21651
  }
20521
21652
  function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules()) {
20522
- const timestamp2 = input.timestamp || new Date().toISOString();
21653
+ const timestamp3 = input.timestamp || new Date().toISOString();
20523
21654
  const payload = redactValue(input.payload || {});
20524
21655
  return rules.map((rule) => {
20525
21656
  const skipped = [];
@@ -20540,7 +21671,7 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
20540
21671
  skipped.push("project does not match rule");
20541
21672
  if (!containsMatches(rule.contains, payload))
20542
21673
  skipped.push("payload text does not match rule");
20543
- if (isQuietTime(rule.quiet_hours, timestamp2))
21674
+ if (isQuietTime(rule.quiet_hours, timestamp3))
20544
21675
  skipped.push("quiet hours active");
20545
21676
  const matched = skipped.length === 0;
20546
21677
  const title = asString4(payload["title"]) || asString4(payload["name"]) || input.type;
@@ -20551,7 +21682,7 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
20551
21682
  severity,
20552
21683
  title,
20553
21684
  message: `${input.type}: ${title}`,
20554
- timestamp: timestamp2,
21685
+ timestamp: timestamp3,
20555
21686
  task_id: taskId,
20556
21687
  project_id: asString4(payload["project_id"]),
20557
21688
  agent_id: asString4(payload["agent_id"]) || asString4(payload["assigned_to"]),
@@ -20607,10 +21738,10 @@ function parseClock2(value) {
20607
21738
  throw new Error("quiet hours must use HH:MM-HH:MM");
20608
21739
  return Number(match[1]) * 60 + Number(match[2]);
20609
21740
  }
20610
- function quietActive(quiet, timestamp2) {
21741
+ function quietActive(quiet, timestamp3) {
20611
21742
  if (!quiet)
20612
21743
  return false;
20613
- const date = new Date(timestamp2);
21744
+ const date = new Date(timestamp3);
20614
21745
  if (Number.isNaN(date.getTime()))
20615
21746
  return false;
20616
21747
  const current = quiet.timezone === "utc" ? date.getUTCHours() * 60 + date.getUTCMinutes() : date.getHours() * 60 + date.getMinutes();
@@ -21544,6 +22675,7 @@ function renderLocalReportMarkdown(report) {
21544
22675
  // src/lib/local-encryption.ts
21545
22676
  init_config2();
21546
22677
  init_redaction();
22678
+ init_prewrite_secrets();
21547
22679
  import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
21548
22680
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
21549
22681
  var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
@@ -21603,7 +22735,7 @@ function upsertEncryptionProfile(input) {
21603
22735
  const name = normalizeProfileName(input.name);
21604
22736
  const config = loadConfig();
21605
22737
  const existing = config.encryption_profiles?.[name];
21606
- const timestamp2 = now3();
22738
+ const timestamp3 = now3();
21607
22739
  const profile = {
21608
22740
  name,
21609
22741
  algorithm: "aes-256-gcm",
@@ -21611,8 +22743,8 @@ function upsertEncryptionProfile(input) {
21611
22743
  key_env: input.key_env?.trim() || existing?.key_env || DEFAULT_ENCRYPTION_KEY_ENV,
21612
22744
  salt: input.salt || existing?.salt || randomBase64(16),
21613
22745
  description: input.description ?? existing?.description,
21614
- created_at: existing?.created_at || timestamp2,
21615
- updated_at: timestamp2
22746
+ created_at: existing?.created_at || timestamp3,
22747
+ updated_at: timestamp3
21616
22748
  };
21617
22749
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(profile.key_env))
21618
22750
  throw new Error("key_env must be a valid environment variable name");
@@ -21710,6 +22842,9 @@ function looksSensitiveKey(key) {
21710
22842
  return /api[_-]?key|token|secret|password|credential|evidence|artifact|output|summary|metadata/i.test(key);
21711
22843
  }
21712
22844
  function encryptSensitiveFields(value, options = {}) {
22845
+ if (typeof value === "string") {
22846
+ return scanPreWriteText(value, "encrypted_export.string").clean ? value : encryptValue(value, options);
22847
+ }
21713
22848
  if (!value || typeof value !== "object")
21714
22849
  return value;
21715
22850
  if (Array.isArray(value))
@@ -21788,14 +22923,15 @@ function applyExportProfile(data, options = {}) {
21788
22923
  const warnings = [];
21789
22924
  if (profile === "plaintext") {
21790
22925
  if (!options.acknowledge_plaintext) {
21791
- warnings.push("Export profile 'plaintext' includes unredacted data; pass acknowledge_plaintext=true to confirm.");
22926
+ throw new Error("Export profile 'plaintext' includes unredacted data; pass acknowledge_plaintext=true to confirm, or use the redacted/encrypted profiles.");
21792
22927
  }
22928
+ warnings.push("Export profile 'plaintext' includes unredacted data; prefer encrypted exports for sensitive task evidence.");
21793
22929
  return { profile, data, warnings };
21794
22930
  }
21795
22931
  if (profile === "encrypted") {
21796
22932
  return { profile, data: encryptSensitiveFields(data, { profile: DEFAULT_ENCRYPTION_PROFILE, env: options.env }), warnings };
21797
22933
  }
21798
- return { profile, data: redactValue(data), warnings };
22934
+ return { profile, data: sanitizePreWriteValue(redactValue(data), "export.redacted"), warnings };
21799
22935
  }
21800
22936
 
21801
22937
  // src/contracts.ts