@hasna/todos 0.15.19 → 0.15.24

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 (58) hide show
  1. package/dist/ai-tools.d.ts +80 -0
  2. package/dist/ai-tools.d.ts.map +1 -0
  3. package/dist/ai.d.ts +313 -0
  4. package/dist/ai.d.ts.map +1 -0
  5. package/dist/cli/cloud-router.d.ts +30 -2
  6. package/dist/cli/cloud-router.d.ts.map +1 -1
  7. package/dist/cli/commands/ai-commands.d.ts +3 -0
  8. package/dist/cli/commands/ai-commands.d.ts.map +1 -0
  9. package/dist/cli/commands/help-commands.d.ts +2 -1
  10. package/dist/cli/commands/help-commands.d.ts.map +1 -1
  11. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  12. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  13. package/dist/cli/index.js +3685 -572
  14. package/dist/cli/stage-a.d.ts +34 -12
  15. package/dist/cli/stage-a.d.ts.map +1 -1
  16. package/dist/contracts.d.ts +1 -0
  17. package/dist/contracts.d.ts.map +1 -1
  18. package/dist/contracts.js +861 -29
  19. package/dist/db/audit.d.ts +8 -0
  20. package/dist/db/audit.d.ts.map +1 -1
  21. package/dist/db/task-lifecycle.d.ts +7 -1
  22. package/dist/db/task-lifecycle.d.ts.map +1 -1
  23. package/dist/db/tasks.d.ts +1 -1
  24. package/dist/db/tasks.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3717 -1205
  28. package/dist/lib/cli-help.d.ts +3 -2
  29. package/dist/lib/cli-help.d.ts.map +1 -1
  30. package/dist/lib/config.d.ts +4 -0
  31. package/dist/lib/config.d.ts.map +1 -1
  32. package/dist/lib/saved-search-views.d.ts.map +1 -1
  33. package/dist/lib/stale-lock-handoff.d.ts +25 -0
  34. package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
  35. package/dist/mcp/index.js +822 -101
  36. package/dist/mcp.js +3 -1
  37. package/dist/project-registration.js +4208 -3709
  38. package/dist/registry.js +826 -29
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/sdk/index.js +8 -1
  41. package/dist/sdk/v1.generated.d.ts +29 -0
  42. package/dist/sdk/v1.generated.d.ts.map +1 -1
  43. package/dist/server/index.js +1155 -434
  44. package/dist/server/openapi.d.ts +189 -0
  45. package/dist/server/openapi.d.ts.map +1 -1
  46. package/dist/server/v1.d.ts.map +1 -1
  47. package/dist/storage/audit-history-import.d.ts +14 -0
  48. package/dist/storage/audit-history-import.d.ts.map +1 -0
  49. package/dist/storage/interfaces.d.ts +12 -1
  50. package/dist/storage/interfaces.d.ts.map +1 -1
  51. package/dist/storage/local-sqlite.d.ts.map +1 -1
  52. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  53. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  54. package/dist/storage.js +531 -34
  55. package/dist/task-manifest.js +30 -4
  56. package/dist/types/index.d.ts +43 -0
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/package.json +3 -1
@@ -70,7 +70,7 @@ var package_default;
70
70
  var init_package = __esm(() => {
71
71
  package_default = {
72
72
  name: "@hasna/todos",
73
- version: "0.15.19",
73
+ version: "0.15.24",
74
74
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
75
75
  type: "module",
76
76
  main: "dist/index.js",
@@ -141,6 +141,8 @@ var init_package = __esm(() => {
141
141
  "dev:mcp": "bun run src/mcp/index.ts",
142
142
  "dev:serve": "bun run src/server/index.ts",
143
143
  "verify:release": "bun run scripts/verify-public-release.ts --mode=review",
144
+ "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
145
+ "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
144
146
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
145
147
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
146
148
  },
@@ -861,7 +863,7 @@ function isBlockingDependencyStatus(status) {
861
863
  function isTerminalStatus(status) {
862
864
  return status === "completed" || status === "failed" || status === "cancelled";
863
865
  }
864
- var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
866
+ var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
865
867
  var init_types = __esm(() => {
866
868
  TASK_STATUSES = [
867
869
  "pending",
@@ -983,6 +985,16 @@ var init_types = __esm(() => {
983
985
  this.name = "LockError";
984
986
  }
985
987
  };
988
+ StaleLockHandoffError = class StaleLockHandoffError extends Error {
989
+ code;
990
+ details;
991
+ constructor(code, message, details = {}) {
992
+ super(message);
993
+ this.code = code;
994
+ this.details = details;
995
+ this.name = "StaleLockHandoffError";
996
+ }
997
+ };
986
998
  AgentNotFoundError = class AgentNotFoundError extends Error {
987
999
  agentId;
988
1000
  static code = "AGENT_NOT_FOUND";
@@ -1196,6 +1208,498 @@ var init_plan_project_link_contract = __esm(() => {
1196
1208
  };
1197
1209
  });
1198
1210
 
1211
+ // src/lib/config.ts
1212
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1213
+ import { dirname, join as join3 } from "path";
1214
+ function getConfigPath() {
1215
+ return join3(getTodosGlobalDir(), "config.json");
1216
+ }
1217
+ function loadConfig() {
1218
+ if (cached)
1219
+ return cached;
1220
+ if (!existsSync3(getConfigPath())) {
1221
+ cached = {};
1222
+ return cached;
1223
+ }
1224
+ const config = readJsonFile(getConfigPath()) || {};
1225
+ if (typeof config.sync_agents === "string") {
1226
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
1227
+ }
1228
+ cached = config;
1229
+ return cached;
1230
+ }
1231
+ function saveConfig(config) {
1232
+ const configPath = getConfigPath();
1233
+ ensureDir(dirname(configPath));
1234
+ writeJsonFile(configPath, config);
1235
+ cached = config;
1236
+ return config;
1237
+ }
1238
+ function getAgentPoolForProject(workingDir) {
1239
+ const config = loadConfig();
1240
+ if (workingDir && config.project_pools) {
1241
+ let bestKey = null;
1242
+ let bestLen = 0;
1243
+ for (const key of Object.keys(config.project_pools)) {
1244
+ if (workingDir.startsWith(key) && key.length > bestLen) {
1245
+ bestKey = key;
1246
+ bestLen = key.length;
1247
+ }
1248
+ }
1249
+ if (bestKey && config.project_pools[bestKey]) {
1250
+ return config.project_pools[bestKey];
1251
+ }
1252
+ }
1253
+ return config.agent_pool || null;
1254
+ }
1255
+ function getCompletionGuardConfig(projectPath) {
1256
+ const config = loadConfig();
1257
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
1258
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
1259
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
1260
+ }
1261
+ return global;
1262
+ }
1263
+ var cached = null, GUARD_DEFAULTS;
1264
+ var init_config2 = __esm(() => {
1265
+ init_sync_utils();
1266
+ GUARD_DEFAULTS = {
1267
+ enabled: false,
1268
+ min_work_seconds: 30,
1269
+ max_completions_per_window: 5,
1270
+ window_minutes: 10,
1271
+ cooldown_seconds: 60
1272
+ };
1273
+ });
1274
+
1275
+ // src/lib/redaction.ts
1276
+ function unique(values) {
1277
+ return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
1278
+ }
1279
+ function cloneRegex(regex) {
1280
+ return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
1281
+ }
1282
+ function customPatterns() {
1283
+ return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
1284
+ try {
1285
+ return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
1286
+ } catch {
1287
+ return [];
1288
+ }
1289
+ });
1290
+ }
1291
+ function secretPatterns() {
1292
+ return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
1293
+ }
1294
+ function isRedactionPlaceholderMatch(match) {
1295
+ const trimmed = match.trim();
1296
+ return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
1297
+ }
1298
+ function isRedactionPlaceholderKey(key) {
1299
+ return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
1300
+ }
1301
+ function isSecretKey(key) {
1302
+ if (isRedactionPlaceholderKey(key))
1303
+ return false;
1304
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
1305
+ return false;
1306
+ if (DEFAULT_SECRET_KEY_PATTERN.test(key))
1307
+ return true;
1308
+ return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
1309
+ }
1310
+ function redactEvidenceText(value) {
1311
+ let redacted = value;
1312
+ for (const pattern of secretPatterns()) {
1313
+ const regex = cloneRegex(pattern.regex);
1314
+ const replacement = pattern.replacement ?? "[REDACTED]";
1315
+ redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
1316
+ }
1317
+ return redacted;
1318
+ }
1319
+ function redactValue(value) {
1320
+ if (typeof value === "string")
1321
+ return redactEvidenceText(value);
1322
+ if (Array.isArray(value))
1323
+ return value.map(redactValue);
1324
+ if (value && typeof value === "object") {
1325
+ const redacted = {};
1326
+ for (const [key, child] of Object.entries(value)) {
1327
+ if (isSecretKey(key)) {
1328
+ redacted[key] = "[REDACTED]";
1329
+ } else {
1330
+ redacted[key] = redactValue(child);
1331
+ }
1332
+ }
1333
+ return redacted;
1334
+ }
1335
+ return value;
1336
+ }
1337
+ function listSecretFindings(value) {
1338
+ const findings = [];
1339
+ for (const pattern of secretPatterns()) {
1340
+ const matches = value.match(cloneRegex(pattern.regex))?.filter((match) => !isRedactionPlaceholderMatch(match));
1341
+ if (matches?.length)
1342
+ findings.push({ pattern: pattern.name, count: matches.length });
1343
+ }
1344
+ return findings;
1345
+ }
1346
+ function getSecretSafetyConfig() {
1347
+ return {
1348
+ redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
1349
+ redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
1350
+ };
1351
+ }
1352
+ function upsertSecretSafetyConfig(input) {
1353
+ const config = loadConfig();
1354
+ const next = {
1355
+ redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
1356
+ redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
1357
+ };
1358
+ saveConfig({ ...config, secret_safety: next });
1359
+ return next;
1360
+ }
1361
+ var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
1362
+ var init_redaction = __esm(() => {
1363
+ init_config2();
1364
+ DEFAULT_SECRET_PATTERNS = [
1365
+ { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
1366
+ { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
1367
+ { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
1368
+ { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
1369
+ { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
1370
+ { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
1371
+ { 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]" },
1372
+ { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
1373
+ ];
1374
+ DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
1375
+ NON_SECRET_USAGE_KEYS = new Set([
1376
+ "tokens",
1377
+ "total_tokens",
1378
+ "token_count",
1379
+ "input_tokens",
1380
+ "output_tokens",
1381
+ "prompt_tokens",
1382
+ "completion_tokens",
1383
+ "cost_tokens"
1384
+ ]);
1385
+ REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
1386
+ });
1387
+
1388
+ // src/lib/secret-redaction.ts
1389
+ function isAllowlisted(text, match, allowlist) {
1390
+ const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
1391
+ return allowlist.some((re) => re.test(context) || re.test(match));
1392
+ }
1393
+ function scanTextForSecrets(text, options = {}) {
1394
+ const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
1395
+ const matches = [];
1396
+ const patterns = [
1397
+ ...DEFAULT_PATTERNS,
1398
+ ...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
1399
+ ];
1400
+ for (const { name, pattern, allowlist_ok } of patterns) {
1401
+ const re = new RegExp(pattern.source, pattern.flags);
1402
+ let m;
1403
+ while ((m = re.exec(text)) !== null) {
1404
+ const match = m[0];
1405
+ if (allowlist_ok && isAllowlisted(text, match, allowlist))
1406
+ continue;
1407
+ if (isAllowlisted(text, match, allowlist))
1408
+ continue;
1409
+ const line = text.slice(0, m.index).split(`
1410
+ `).length;
1411
+ matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
1412
+ }
1413
+ }
1414
+ return {
1415
+ schema_version: SECRET_REDACTION_SCHEMA,
1416
+ clean: matches.length === 0,
1417
+ matches
1418
+ };
1419
+ }
1420
+ function redactText(text, options = {}) {
1421
+ const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
1422
+ let out = text;
1423
+ for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
1424
+ out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
1425
+ if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
1426
+ return match;
1427
+ }
1428
+ return placeholder;
1429
+ });
1430
+ }
1431
+ for (const custom of options.custom_patterns ?? []) {
1432
+ out = out.replace(custom, placeholder);
1433
+ }
1434
+ for (const fn of customRedactors) {
1435
+ out = fn(out);
1436
+ }
1437
+ return out;
1438
+ }
1439
+ function redactExportRecord(record) {
1440
+ const base = redactValue(record);
1441
+ for (const [key, value] of Object.entries(base)) {
1442
+ if (typeof value === "string") {
1443
+ base[key] = redactText(value);
1444
+ }
1445
+ }
1446
+ return base;
1447
+ }
1448
+ var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
1449
+ var init_secret_redaction = __esm(() => {
1450
+ init_redaction();
1451
+ SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
1452
+ DEFAULT_PATTERNS = [
1453
+ { name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
1454
+ { name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
1455
+ { name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
1456
+ { name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
1457
+ { name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
1458
+ { name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
1459
+ { name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
1460
+ { name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
1461
+ ];
1462
+ DEFAULT_ALLOWLIST = [
1463
+ /\[REDACTED\]/,
1464
+ /example\.com/i,
1465
+ /your-api-key-here/i,
1466
+ /sk-test/i,
1467
+ /ghp_xxx/i
1468
+ ];
1469
+ customRedactors = [];
1470
+ });
1471
+
1472
+ // src/lib/prewrite-secrets.ts
1473
+ function mergeFindings(findings) {
1474
+ const byPattern = new Map;
1475
+ for (const finding of findings) {
1476
+ const existing = byPattern.get(finding.pattern);
1477
+ if (!existing) {
1478
+ byPattern.set(finding.pattern, {
1479
+ pattern: finding.pattern,
1480
+ count: finding.count,
1481
+ lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
1482
+ });
1483
+ continue;
1484
+ }
1485
+ existing.count += finding.count;
1486
+ existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
1487
+ }
1488
+ return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
1489
+ }
1490
+ function redactPreWriteText(value) {
1491
+ return redactText(redactEvidenceText(value));
1492
+ }
1493
+ function scanPreWriteText(value, context = "text") {
1494
+ const redacted = redactPreWriteText(value);
1495
+ if (redacted === value) {
1496
+ return {
1497
+ schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
1498
+ clean: true,
1499
+ context,
1500
+ findings: []
1501
+ };
1502
+ }
1503
+ const structural = scanTextForSecrets(value).matches.map((match) => ({
1504
+ pattern: match.pattern,
1505
+ count: 1,
1506
+ lines: match.line ? [match.line] : []
1507
+ }));
1508
+ const configured = listSecretFindings(value).map((finding) => ({
1509
+ pattern: finding.pattern,
1510
+ count: finding.count,
1511
+ lines: []
1512
+ }));
1513
+ return {
1514
+ schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
1515
+ clean: false,
1516
+ context,
1517
+ findings: mergeFindings([...structural, ...configured])
1518
+ };
1519
+ }
1520
+ function assertPreWriteTextClean(value, context = "text") {
1521
+ const scan = scanPreWriteText(value, context);
1522
+ if (!scan.clean)
1523
+ throw new PreWriteSecretError(context, scan.findings);
1524
+ }
1525
+ function sanitizePreWriteText(value, context = "text", options = {}) {
1526
+ const mode = options.mode ?? "redact";
1527
+ if (mode === "block") {
1528
+ assertPreWriteTextClean(value, context);
1529
+ return value;
1530
+ }
1531
+ return redactPreWriteText(value);
1532
+ }
1533
+ function sanitizePreWriteValue(value, context = "value", options = {}) {
1534
+ if (typeof value === "string")
1535
+ return sanitizePreWriteText(value, context, options);
1536
+ if (Array.isArray(value)) {
1537
+ return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
1538
+ }
1539
+ if (value && typeof value === "object") {
1540
+ const sanitized = {};
1541
+ for (const [key, child] of Object.entries(value)) {
1542
+ const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
1543
+ sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
1544
+ }
1545
+ return redactValue(sanitized);
1546
+ }
1547
+ return value;
1548
+ }
1549
+ var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
1550
+ var init_prewrite_secrets = __esm(() => {
1551
+ init_redaction();
1552
+ init_secret_redaction();
1553
+ PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
1554
+ PreWriteSecretError = class PreWriteSecretError extends Error {
1555
+ context;
1556
+ findings;
1557
+ constructor(context, findings) {
1558
+ const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
1559
+ super(`Secret pattern detected before persistence in ${context}: ${summary}`);
1560
+ this.name = "PreWriteSecretError";
1561
+ this.context = context;
1562
+ this.findings = findings;
1563
+ }
1564
+ };
1565
+ });
1566
+
1567
+ // src/lib/stale-lock-handoff.ts
1568
+ function normalizeExactTaskId(value) {
1569
+ if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
1570
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_TASK_ID", "stale-lock handoff requires one exact full task UUID", { task_id: typeof value === "string" ? value : null });
1571
+ }
1572
+ return value.trim().toLowerCase();
1573
+ }
1574
+ function requireNonEmptyString(value, field) {
1575
+ if (typeof value !== "string" || !value.trim()) {
1576
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
1577
+ }
1578
+ const trimmed = value.trim();
1579
+ if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
1580
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
1581
+ }
1582
+ return trimmed;
1583
+ }
1584
+ function requireCanonicalLockVersion(value) {
1585
+ if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
1586
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
1587
+ }
1588
+ const parsed = Date.parse(value);
1589
+ if (value.startsWith("0000-") || Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
1590
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
1591
+ }
1592
+ return value;
1593
+ }
1594
+ function requireStaleThreshold(value) {
1595
+ if (!Number.isSafeInteger(value) || Number(value) <= 0) {
1596
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
1597
+ }
1598
+ return Number(value);
1599
+ }
1600
+ function prepareStaleLockHandoff(input, options = {}) {
1601
+ const taskId = normalizeExactTaskId(input.task_id);
1602
+ const actor = requireNonEmptyString(input.actor, "actor");
1603
+ const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
1604
+ const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
1605
+ const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
1606
+ const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
1607
+ const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
1608
+ if (!reason) {
1609
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
1610
+ }
1611
+ if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
1612
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
1613
+ }
1614
+ const operationTimestamp = options.now ?? new Date().toISOString();
1615
+ if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
1616
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
1617
+ }
1618
+ const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
1619
+ return {
1620
+ task_id: taskId,
1621
+ actor,
1622
+ expected_holder: expectedHolder,
1623
+ expected_lock_version: expectedLockVersion,
1624
+ stale_after_seconds: staleAfterSeconds,
1625
+ new_holder: newHolder,
1626
+ reason,
1627
+ operation_timestamp: operationTimestamp,
1628
+ stale_cutoff: staleCutoff,
1629
+ receipt_id: options.receiptId ?? crypto.randomUUID()
1630
+ };
1631
+ }
1632
+ function buildStaleLockHandoffReceipt(input) {
1633
+ return {
1634
+ schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
1635
+ receipt_id: input.receipt_id,
1636
+ task_id: input.task_id,
1637
+ actor: input.actor,
1638
+ previous_holder: input.expected_holder,
1639
+ previous_lock_version: input.expected_lock_version,
1640
+ new_holder: input.new_holder,
1641
+ new_lock_version: input.operation_timestamp,
1642
+ stale_after_seconds: input.stale_after_seconds,
1643
+ stale_cutoff: input.stale_cutoff,
1644
+ reason: input.reason,
1645
+ created_at: input.operation_timestamp
1646
+ };
1647
+ }
1648
+ function staleLockHandoffHistory(receipt, machineId) {
1649
+ return {
1650
+ id: receipt.receipt_id,
1651
+ task_id: receipt.task_id,
1652
+ action: STALE_LOCK_HANDOFF_ACTION,
1653
+ field: STALE_LOCK_HANDOFF_FIELD,
1654
+ old_value: JSON.stringify({
1655
+ holder: receipt.previous_holder,
1656
+ lock_version: receipt.previous_lock_version
1657
+ }),
1658
+ new_value: JSON.stringify(receipt),
1659
+ agent_id: receipt.actor,
1660
+ created_at: receipt.created_at,
1661
+ machine_id: machineId
1662
+ };
1663
+ }
1664
+ function throwStaleLockHandoffConflict(task, input) {
1665
+ if (!task.locked_by || !task.locked_at) {
1666
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_LOCKED", `Task ${input.task_id} does not have a complete lock to hand off`, { task_id: input.task_id });
1667
+ }
1668
+ if (task.locked_at !== input.expected_lock_version) {
1669
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
1670
+ task_id: input.task_id,
1671
+ expected_lock_version: input.expected_lock_version,
1672
+ current_lock_version: task.locked_at
1673
+ });
1674
+ }
1675
+ if (task.locked_by !== input.expected_holder) {
1676
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
1677
+ task_id: input.task_id,
1678
+ expected_holder: input.expected_holder,
1679
+ current_holder: task.locked_by
1680
+ });
1681
+ }
1682
+ if (isTerminalStatus(task.status)) {
1683
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_TERMINAL", `Task ${input.task_id} is ${task.status} and cannot transfer a lock`, { task_id: input.task_id, status: task.status });
1684
+ }
1685
+ if (task.locked_at >= input.stale_cutoff) {
1686
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
1687
+ task_id: input.task_id,
1688
+ current_lock_version: task.locked_at,
1689
+ stale_cutoff: input.stale_cutoff
1690
+ });
1691
+ }
1692
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
1693
+ }
1694
+ var STALE_LOCK_HANDOFF_SCHEMA_VERSION = "todos.stale-lock-handoff.v1", STALE_LOCK_HANDOFF_ACTION = "stale_lock_handoff", STALE_LOCK_HANDOFF_FIELD = "lock", EXACT_TASK_UUID_RE, CANONICAL_LOCK_VERSION_RE, MAX_REASON_LENGTH = 4096;
1695
+ var init_stale_lock_handoff = __esm(() => {
1696
+ init_types();
1697
+ init_creator_identity();
1698
+ init_prewrite_secrets();
1699
+ EXACT_TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1700
+ CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
1701
+ });
1702
+
1199
1703
  // src/lib/slugs.ts
1200
1704
  function normalizeSlug(value) {
1201
1705
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -1862,182 +2366,82 @@ var init_integrity = __esm(() => {
1862
2366
  };
1863
2367
  });
1864
2368
 
1865
- // src/lib/config.ts
1866
- import { existsSync as existsSync3 } from "fs";
1867
- import { dirname, join as join3 } from "path";
1868
- function getConfigPath() {
1869
- return join3(getTodosGlobalDir(), "config.json");
2369
+ // src/storage/audit-history-import.ts
2370
+ function auditHistoryRowsAreFieldIdentical(left, right) {
2371
+ return AUDIT_HISTORY_FIELDS.every((field) => {
2372
+ const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
2373
+ const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
2374
+ return leftValue === rightValue;
2375
+ });
1870
2376
  }
1871
- function loadConfig() {
1872
- if (cached)
1873
- return cached;
1874
- if (!existsSync3(getConfigPath())) {
1875
- cached = {};
1876
- return cached;
1877
- }
1878
- const config = readJsonFile(getConfigPath()) || {};
1879
- if (typeof config.sync_agents === "string") {
1880
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
1881
- }
1882
- cached = config;
1883
- return cached;
2377
+ function divergentAuditHistoryReplayError(id) {
2378
+ return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
1884
2379
  }
1885
- function saveConfig(config) {
1886
- const configPath = getConfigPath();
1887
- ensureDir(dirname(configPath));
1888
- writeJsonFile(configPath, config);
1889
- cached = config;
1890
- return config;
2380
+ function forbiddenAuditHistoryTombstoneError(id) {
2381
+ return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
1891
2382
  }
1892
- function getAgentPoolForProject(workingDir) {
1893
- const config = loadConfig();
1894
- if (workingDir && config.project_pools) {
1895
- let bestKey = null;
1896
- let bestLen = 0;
1897
- for (const key of Object.keys(config.project_pools)) {
1898
- if (workingDir.startsWith(key) && key.length > bestLen) {
1899
- bestKey = key;
1900
- bestLen = key.length;
1901
- }
1902
- }
1903
- if (bestKey && config.project_pools[bestKey]) {
1904
- return config.project_pools[bestKey];
1905
- }
2383
+ function parseAuditHistoryImportFailure(message) {
2384
+ const divergentPrefix = `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row `;
2385
+ const divergentSuffix = " differs from stored row";
2386
+ if (message.startsWith(divergentPrefix) && message.endsWith(divergentSuffix)) {
2387
+ return {
2388
+ code: AUDIT_HISTORY_DIVERGENT_REPLAY,
2389
+ auditHistoryId: message.slice(divergentPrefix.length, -divergentSuffix.length),
2390
+ conflict: true,
2391
+ status: 409
2392
+ };
1906
2393
  }
1907
- return config.agent_pool || null;
1908
- }
1909
- function getCompletionGuardConfig(projectPath) {
1910
- const config = loadConfig();
1911
- const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
1912
- if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
1913
- return { ...global, ...config.project_overrides[projectPath].completion_guard };
2394
+ const tombstonePrefix = `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone `;
2395
+ const tombstoneSuffix = " is not allowed";
2396
+ if (message.startsWith(tombstonePrefix) && message.endsWith(tombstoneSuffix)) {
2397
+ return {
2398
+ code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
2399
+ auditHistoryId: message.slice(tombstonePrefix.length, -tombstoneSuffix.length),
2400
+ conflict: false,
2401
+ status: 400
2402
+ };
1914
2403
  }
1915
- return global;
2404
+ return null;
1916
2405
  }
1917
- var cached = null, GUARD_DEFAULTS;
1918
- var init_config2 = __esm(() => {
1919
- init_sync_utils();
1920
- GUARD_DEFAULTS = {
1921
- enabled: false,
1922
- min_work_seconds: 30,
1923
- max_completions_per_window: 5,
1924
- window_minutes: 10,
1925
- cooldown_seconds: 60
1926
- };
2406
+ var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY", AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN", AUDIT_HISTORY_FIELDS;
2407
+ var init_audit_history_import = __esm(() => {
2408
+ AUDIT_HISTORY_FIELDS = [
2409
+ "id",
2410
+ "task_id",
2411
+ "action",
2412
+ "field",
2413
+ "old_value",
2414
+ "new_value",
2415
+ "agent_id",
2416
+ "created_at",
2417
+ "machine_id"
2418
+ ];
1927
2419
  });
1928
2420
 
1929
- // src/lib/redaction.ts
1930
- function unique(values) {
1931
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
1932
- }
1933
- function cloneRegex(regex) {
1934
- return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
1935
- }
1936
- function customPatterns() {
1937
- return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
1938
- try {
1939
- return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
1940
- } catch {
1941
- return [];
1942
- }
1943
- });
1944
- }
1945
- function secretPatterns() {
1946
- return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
1947
- }
1948
- function isRedactionPlaceholderMatch(match) {
1949
- const trimmed = match.trim();
1950
- return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
1951
- }
1952
- function isRedactionPlaceholderKey(key) {
1953
- return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
1954
- }
1955
- function isSecretKey(key) {
1956
- if (isRedactionPlaceholderKey(key))
1957
- return false;
1958
- if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
1959
- return false;
1960
- if (DEFAULT_SECRET_KEY_PATTERN.test(key))
1961
- return true;
1962
- return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
1963
- }
1964
- function redactEvidenceText(value) {
1965
- let redacted = value;
1966
- for (const pattern of secretPatterns()) {
1967
- const regex = cloneRegex(pattern.regex);
1968
- const replacement = pattern.replacement ?? "[REDACTED]";
1969
- redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
1970
- }
1971
- return redacted;
1972
- }
1973
- function redactValue(value) {
1974
- if (typeof value === "string")
1975
- return redactEvidenceText(value);
2421
+ // src/task-manifest/canonical.ts
2422
+ import { createHash as createHash3 } from "crypto";
2423
+ function canonicalize(value) {
1976
2424
  if (Array.isArray(value))
1977
- return value.map(redactValue);
1978
- if (value && typeof value === "object") {
1979
- const redacted = {};
1980
- for (const [key, child] of Object.entries(value)) {
1981
- if (isSecretKey(key)) {
1982
- redacted[key] = "[REDACTED]";
1983
- } else {
1984
- redacted[key] = redactValue(child);
1985
- }
1986
- }
1987
- return redacted;
2425
+ return value.map(canonicalize);
2426
+ if (value !== null && typeof value === "object") {
2427
+ return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry2]) => [key, canonicalize(entry2)]));
1988
2428
  }
1989
2429
  return value;
1990
2430
  }
1991
- function listSecretFindings(value) {
1992
- const findings = [];
1993
- for (const pattern of secretPatterns()) {
1994
- const matches = value.match(cloneRegex(pattern.regex))?.filter((match) => !isRedactionPlaceholderMatch(match));
1995
- if (matches?.length)
1996
- findings.push({ pattern: pattern.name, count: matches.length });
1997
- }
1998
- return findings;
2431
+ function canonicalJson(value) {
2432
+ return JSON.stringify(canonicalize(value));
1999
2433
  }
2000
- function getSecretSafetyConfig() {
2001
- return {
2002
- redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
2003
- redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
2004
- };
2434
+ function canonicalDigest(value) {
2435
+ return createHash3("sha256").update(canonicalJson(value)).digest("hex");
2005
2436
  }
2006
- function upsertSecretSafetyConfig(input) {
2007
- const config = loadConfig();
2008
- const next = {
2009
- redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
2010
- redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
2011
- };
2012
- saveConfig({ ...config, secret_safety: next });
2013
- return next;
2437
+ function deterministicUuid(namespace, ...parts) {
2438
+ const bytes = createHash3("sha256").update([namespace, ...parts].join("\x1F")).digest().subarray(0, 16);
2439
+ bytes[6] = bytes[6] & 15 | 80;
2440
+ bytes[8] = bytes[8] & 63 | 128;
2441
+ const hex = bytes.toString("hex");
2442
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2014
2443
  }
2015
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
2016
- var init_redaction = __esm(() => {
2017
- init_config2();
2018
- DEFAULT_SECRET_PATTERNS = [
2019
- { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
2020
- { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
2021
- { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
2022
- { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
2023
- { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
2024
- { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
2025
- { 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]" },
2026
- { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
2027
- ];
2028
- DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
2029
- NON_SECRET_USAGE_KEYS = new Set([
2030
- "tokens",
2031
- "total_tokens",
2032
- "token_count",
2033
- "input_tokens",
2034
- "output_tokens",
2035
- "prompt_tokens",
2036
- "completion_tokens",
2037
- "cost_tokens"
2038
- ]);
2039
- REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
2040
- });
2444
+ var init_canonical = () => {};
2041
2445
 
2042
2446
  // src/storage/postgres-adapter.ts
2043
2447
  import { randomUUID } from "crypto";
@@ -2069,6 +2473,7 @@ function createPostgresTodosStorageAdapter(options) {
2069
2473
  getChangedSince: (since, filters) => getChangedSince(since, filters, store),
2070
2474
  lock: (id, agentId) => lockTask(id, agentId, store),
2071
2475
  unlock: (id, agentId) => unlockTask(id, agentId, store),
2476
+ handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
2072
2477
  getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
2073
2478
  },
2074
2479
  dependencies: {
@@ -2215,6 +2620,91 @@ class PostgresJsonRecordStore {
2215
2620
  LIMIT 1`, [this.service, type, id]);
2216
2621
  return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
2217
2622
  }
2623
+ async handoffStaleLock(input, context = {}) {
2624
+ const prepared = prepareStaleLockHandoff(input);
2625
+ const receipt = buildStaleLockHandoffReceipt(prepared);
2626
+ const history = staleLockHandoffHistory(receipt, this.machineId(context));
2627
+ await this.ensureSchema();
2628
+ const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
2629
+ target AS MATERIALIZED (
2630
+ SELECT payload
2631
+ FROM ${this.tableName}
2632
+ WHERE service = $1
2633
+ AND object_type = 'tasks'
2634
+ AND object_id = $2
2635
+ AND deleted_at IS NULL
2636
+ FOR UPDATE
2637
+ ),
2638
+ updated AS (
2639
+ UPDATE ${this.tableName} AS task_record
2640
+ SET payload = jsonb_set(
2641
+ jsonb_set(
2642
+ jsonb_set(
2643
+ jsonb_set(
2644
+ task_record.payload,
2645
+ '{locked_by}',
2646
+ to_jsonb($6::text),
2647
+ true
2648
+ ),
2649
+ '{locked_at}',
2650
+ to_jsonb($7::text),
2651
+ true
2652
+ ),
2653
+ '{updated_at}',
2654
+ to_jsonb($7::text),
2655
+ true
2656
+ ),
2657
+ '{version}',
2658
+ to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
2659
+ true
2660
+ ),
2661
+ updated_at = $7::timestamptz,
2662
+ source_machine_id = $10,
2663
+ version = COALESCE(task_record.version, 0) + 1
2664
+ FROM target
2665
+ WHERE task_record.service = $1
2666
+ AND task_record.object_type = 'tasks'
2667
+ AND task_record.object_id = $2
2668
+ AND task_record.deleted_at IS NULL
2669
+ AND target.payload->>'locked_by' = $3
2670
+ AND target.payload->>'locked_at' = $4
2671
+ AND $4::timestamptz < $5::timestamptz
2672
+ AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
2673
+ RETURNING task_record.payload
2674
+ ),
2675
+ audit AS (
2676
+ INSERT INTO ${this.tableName} (
2677
+ service, object_type, object_id, payload, updated_at,
2678
+ deleted_at, source_machine_id, version
2679
+ )
2680
+ SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
2681
+ NULL, $10, NULL
2682
+ FROM updated
2683
+ RETURNING payload
2684
+ )
2685
+ SELECT
2686
+ (SELECT payload FROM target) AS current_payload,
2687
+ (SELECT payload FROM updated) AS updated_payload,
2688
+ (SELECT payload FROM audit) AS audit_payload`, [
2689
+ this.service,
2690
+ prepared.task_id,
2691
+ prepared.expected_holder,
2692
+ prepared.expected_lock_version,
2693
+ prepared.stale_cutoff,
2694
+ prepared.new_holder,
2695
+ prepared.operation_timestamp,
2696
+ receipt.receipt_id,
2697
+ jsonbParam(history),
2698
+ this.machineId(context)
2699
+ ]);
2700
+ const row = result.rows[0];
2701
+ if (!row?.current_payload)
2702
+ throw new TaskNotFoundError(prepared.task_id);
2703
+ if (!row.updated_payload || !row.audit_payload) {
2704
+ throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
2705
+ }
2706
+ return receipt;
2707
+ }
2218
2708
  async list(type) {
2219
2709
  return (await this.listRecords(type)).map((record) => record.payload);
2220
2710
  }
@@ -2484,6 +2974,28 @@ class PostgresJsonRecordStore {
2484
2974
  }
2485
2975
  return value;
2486
2976
  }
2977
+ async insertImmutableAuditHistory(value, context = {}) {
2978
+ await this.ensureSchema();
2979
+ const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
2980
+ service, object_type, object_id, payload, updated_at,
2981
+ deleted_at, source_machine_id, version
2982
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
2983
+ ON CONFLICT (service, object_type, object_id) DO NOTHING
2984
+ RETURNING object_id`, [
2985
+ this.service,
2986
+ "audit_history",
2987
+ value.id,
2988
+ jsonbParam(value),
2989
+ value.created_at,
2990
+ context.requestId ?? this.sourceMachineId ?? null
2991
+ ]);
2992
+ if (inserted.rows.length > 0)
2993
+ return "inserted";
2994
+ const existing = await this.get("audit_history", value.id);
2995
+ if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
2996
+ return "identical";
2997
+ throw new Error(divergentAuditHistoryReplayError(value.id));
2998
+ }
2487
2999
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
2488
3000
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
2489
3001
  if (planIds.length === 0)
@@ -3598,16 +4110,17 @@ async function findCommit(sha, store) {
3598
4110
  async function addGitRef(input, store, context) {
3599
4111
  if (!await store.get("tasks", input.task_id))
3600
4112
  throw new Error(`Task not found: ${input.task_id}`);
4113
+ const existing = (await store.list("refs")).filter((ref) => ref.task_id === input.task_id && ref.ref_type === input.ref_type && ref.name === input.name).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id))[0];
3601
4114
  const timestamp = new Date().toISOString();
3602
4115
  const gitRef = {
3603
- id: randomUUID(),
4116
+ id: existing?.id ?? deterministicUuid("todos:git-ref:v1", input.task_id, input.ref_type, input.name),
3604
4117
  task_id: input.task_id,
3605
4118
  ref_type: input.ref_type,
3606
4119
  name: input.name,
3607
- url: input.url ?? null,
3608
- provider: input.provider ?? null,
4120
+ url: input.url ?? existing?.url ?? null,
4121
+ provider: input.provider ?? existing?.provider ?? null,
3609
4122
  metadata: input.metadata ?? {},
3610
- created_at: timestamp,
4123
+ created_at: existing?.created_at ?? timestamp,
3611
4124
  updated_at: timestamp
3612
4125
  };
3613
4126
  await store.upsert("refs", gitRef, context);
@@ -3972,6 +4485,11 @@ async function importSnapshot(snapshot, store, context) {
3972
4485
  result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
3973
4486
  if (result.errors.length > 0)
3974
4487
  return result;
4488
+ const auditHistory = await preflightAuditHistoryImport(snapshot.auditHistory, snapshot.tombstones ?? [], store);
4489
+ result.errors.push(...auditHistory.errors);
4490
+ if (result.errors.length > 0)
4491
+ return result;
4492
+ result.skipped += auditHistory.identical;
3975
4493
  const entries = [
3976
4494
  ...snapshot.tasks.map((row) => ["tasks", row]),
3977
4495
  ...snapshot.projects.map((row) => ["projects", row]),
@@ -3980,9 +4498,20 @@ async function importSnapshot(snapshot, store, context) {
3980
4498
  ...snapshot.agents.map((row) => ["agents", row]),
3981
4499
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
3982
4500
  ...snapshot.templates.map((row) => ["templates", row]),
3983
- ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
3984
- ...snapshot.auditHistory.map((row) => ["audit_history", row])
4501
+ ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
3985
4502
  ];
4503
+ for (const row of auditHistory.rowsToInsert) {
4504
+ try {
4505
+ const outcome = await store.insertImmutableAuditHistory(row, context);
4506
+ if (outcome === "inserted")
4507
+ result.inserted += 1;
4508
+ else
4509
+ result.skipped += 1;
4510
+ } catch (error) {
4511
+ result.errors.push(error instanceof Error ? error.message : String(error));
4512
+ return result;
4513
+ }
4514
+ }
3986
4515
  for (const [type, row] of entries) {
3987
4516
  try {
3988
4517
  const existing = await store.get(type, row.id);
@@ -4016,6 +4545,32 @@ async function importSnapshot(snapshot, store, context) {
4016
4545
  }
4017
4546
  return result;
4018
4547
  }
4548
+ async function preflightAuditHistoryImport(rows, tombstones, store) {
4549
+ const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
4550
+ const rowsToInsert = [];
4551
+ const seen = new Map;
4552
+ let identical = 0;
4553
+ for (const row of rows) {
4554
+ const prior = seen.get(row.id);
4555
+ if (prior) {
4556
+ if (auditHistoryRowsAreFieldIdentical(prior, row))
4557
+ identical += 1;
4558
+ else
4559
+ errors.push(divergentAuditHistoryReplayError(row.id));
4560
+ continue;
4561
+ }
4562
+ seen.set(row.id, row);
4563
+ const existing = await store.get("audit_history", row.id);
4564
+ if (!existing) {
4565
+ rowsToInsert.push(row);
4566
+ } else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
4567
+ identical += 1;
4568
+ } else {
4569
+ errors.push(divergentAuditHistoryReplayError(row.id));
4570
+ }
4571
+ }
4572
+ return { rowsToInsert, identical, errors };
4573
+ }
4019
4574
  async function requireRecord(type, id, store) {
4020
4575
  const record = await store.get(type, id);
4021
4576
  if (!record)
@@ -4122,9 +4677,12 @@ var init_postgres_adapter = __esm(() => {
4122
4677
  init_types();
4123
4678
  init_creator_identity();
4124
4679
  init_plan_project_link_contract();
4680
+ init_stale_lock_handoff();
4125
4681
  init_postgres_sync();
4126
4682
  init_integrity();
4127
4683
  init_redaction();
4684
+ init_audit_history_import();
4685
+ init_canonical();
4128
4686
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
4129
4687
  });
4130
4688
 
@@ -4144,9 +4702,9 @@ var init_types2 = __esm(() => {
4144
4702
  });
4145
4703
 
4146
4704
  // src/pr-groups/ledger.ts
4147
- import { createHash as createHash3 } from "crypto";
4705
+ import { createHash as createHash4 } from "crypto";
4148
4706
  function sha256(value) {
4149
- return createHash3("sha256").update(value).digest("hex");
4707
+ return createHash4("sha256").update(value).digest("hex");
4150
4708
  }
4151
4709
  function stableValue(value) {
4152
4710
  if (Array.isArray(value))
@@ -11583,7 +12141,7 @@ var init_runner_sandbox = __esm(() => {
11583
12141
  });
11584
12142
 
11585
12143
  // src/lib/event-hooks.ts
11586
- import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
12144
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
11587
12145
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
11588
12146
  import { dirname as dirname3, resolve as resolve6 } from "path";
11589
12147
  import { createConnection } from "net";
@@ -11650,7 +12208,7 @@ function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
11650
12208
  payload: redactValue(payload ?? {}),
11651
12209
  source: { package: "@hasna/todos", local_only: true }
11652
12210
  };
11653
- const digest = createHash4("sha256").update(canonicalEvent(base)).digest("hex");
12211
+ const digest = createHash5("sha256").update(canonicalEvent(base)).digest("hex");
11654
12212
  return { ...base, integrity: { algorithm: "sha256", digest } };
11655
12213
  }
11656
12214
  function summarize(value) {
@@ -12833,185 +13391,6 @@ var init_shared_events = __esm(() => {
12833
13391
  init_event_emission_safety();
12834
13392
  });
12835
13393
 
12836
- // src/lib/secret-redaction.ts
12837
- function isAllowlisted(text, match, allowlist) {
12838
- const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
12839
- return allowlist.some((re) => re.test(context) || re.test(match));
12840
- }
12841
- function scanTextForSecrets(text, options = {}) {
12842
- const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
12843
- const matches = [];
12844
- const patterns = [
12845
- ...DEFAULT_PATTERNS,
12846
- ...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
12847
- ];
12848
- for (const { name, pattern, allowlist_ok } of patterns) {
12849
- const re = new RegExp(pattern.source, pattern.flags);
12850
- let m;
12851
- while ((m = re.exec(text)) !== null) {
12852
- const match = m[0];
12853
- if (allowlist_ok && isAllowlisted(text, match, allowlist))
12854
- continue;
12855
- if (isAllowlisted(text, match, allowlist))
12856
- continue;
12857
- const line = text.slice(0, m.index).split(`
12858
- `).length;
12859
- matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
12860
- }
12861
- }
12862
- return {
12863
- schema_version: SECRET_REDACTION_SCHEMA,
12864
- clean: matches.length === 0,
12865
- matches
12866
- };
12867
- }
12868
- function redactText(text, options = {}) {
12869
- const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
12870
- let out = text;
12871
- for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
12872
- out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
12873
- if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
12874
- return match;
12875
- }
12876
- return placeholder;
12877
- });
12878
- }
12879
- for (const custom of options.custom_patterns ?? []) {
12880
- out = out.replace(custom, placeholder);
12881
- }
12882
- for (const fn of customRedactors) {
12883
- out = fn(out);
12884
- }
12885
- return out;
12886
- }
12887
- function redactExportRecord(record) {
12888
- const base = redactValue(record);
12889
- for (const [key, value] of Object.entries(base)) {
12890
- if (typeof value === "string") {
12891
- base[key] = redactText(value);
12892
- }
12893
- }
12894
- return base;
12895
- }
12896
- var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
12897
- var init_secret_redaction = __esm(() => {
12898
- init_redaction();
12899
- SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
12900
- DEFAULT_PATTERNS = [
12901
- { name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
12902
- { name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
12903
- { name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
12904
- { name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
12905
- { name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
12906
- { name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
12907
- { name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
12908
- { name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
12909
- ];
12910
- DEFAULT_ALLOWLIST = [
12911
- /\[REDACTED\]/,
12912
- /example\.com/i,
12913
- /your-api-key-here/i,
12914
- /sk-test/i,
12915
- /ghp_xxx/i
12916
- ];
12917
- customRedactors = [];
12918
- });
12919
-
12920
- // src/lib/prewrite-secrets.ts
12921
- function mergeFindings(findings) {
12922
- const byPattern = new Map;
12923
- for (const finding of findings) {
12924
- const existing = byPattern.get(finding.pattern);
12925
- if (!existing) {
12926
- byPattern.set(finding.pattern, {
12927
- pattern: finding.pattern,
12928
- count: finding.count,
12929
- lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
12930
- });
12931
- continue;
12932
- }
12933
- existing.count += finding.count;
12934
- existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
12935
- }
12936
- return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
12937
- }
12938
- function redactPreWriteText(value) {
12939
- return redactText(redactEvidenceText(value));
12940
- }
12941
- function scanPreWriteText(value, context = "text") {
12942
- const redacted = redactPreWriteText(value);
12943
- if (redacted === value) {
12944
- return {
12945
- schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
12946
- clean: true,
12947
- context,
12948
- findings: []
12949
- };
12950
- }
12951
- const structural = scanTextForSecrets(value).matches.map((match) => ({
12952
- pattern: match.pattern,
12953
- count: 1,
12954
- lines: match.line ? [match.line] : []
12955
- }));
12956
- const configured = listSecretFindings(value).map((finding) => ({
12957
- pattern: finding.pattern,
12958
- count: finding.count,
12959
- lines: []
12960
- }));
12961
- return {
12962
- schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
12963
- clean: false,
12964
- context,
12965
- findings: mergeFindings([...structural, ...configured])
12966
- };
12967
- }
12968
- function assertPreWriteTextClean(value, context = "text") {
12969
- const scan = scanPreWriteText(value, context);
12970
- if (!scan.clean)
12971
- throw new PreWriteSecretError(context, scan.findings);
12972
- }
12973
- function sanitizePreWriteText(value, context = "text", options = {}) {
12974
- const mode = options.mode ?? "redact";
12975
- if (mode === "block") {
12976
- assertPreWriteTextClean(value, context);
12977
- return value;
12978
- }
12979
- return redactPreWriteText(value);
12980
- }
12981
- function sanitizePreWriteValue(value, context = "value", options = {}) {
12982
- if (typeof value === "string")
12983
- return sanitizePreWriteText(value, context, options);
12984
- if (Array.isArray(value)) {
12985
- return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
12986
- }
12987
- if (value && typeof value === "object") {
12988
- const sanitized = {};
12989
- for (const [key, child] of Object.entries(value)) {
12990
- const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
12991
- sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
12992
- }
12993
- return redactValue(sanitized);
12994
- }
12995
- return value;
12996
- }
12997
- var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
12998
- var init_prewrite_secrets = __esm(() => {
12999
- init_redaction();
13000
- init_secret_redaction();
13001
- PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
13002
- PreWriteSecretError = class PreWriteSecretError extends Error {
13003
- context;
13004
- findings;
13005
- constructor(context, findings) {
13006
- const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
13007
- super(`Secret pattern detected before persistence in ${context}: ${summary}`);
13008
- this.name = "PreWriteSecretError";
13009
- this.context = context;
13010
- this.findings = findings;
13011
- }
13012
- };
13013
- });
13014
-
13015
13394
  // src/lib/activity-audit.ts
13016
13395
  var exports_activity_audit = {};
13017
13396
  __export(exports_activity_audit, {
@@ -13204,28 +13583,55 @@ var init_activity_audit = __esm(() => {
13204
13583
  function sanitizeHistoryValue(value, context) {
13205
13584
  return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
13206
13585
  }
13207
- function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, db) {
13586
+ function insertTaskHistory(entry2, db) {
13208
13587
  const d = db || getDatabase();
13209
- const id = uuid();
13210
- const timestamp2 = now();
13211
- const machineId = currentStorageMachineId(d);
13212
- const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
13213
- const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
13588
+ const safeEntry = {
13589
+ ...entry2,
13590
+ field: entry2.field || null,
13591
+ old_value: sanitizeHistoryValue(entry2.old_value, "task_history.old_value"),
13592
+ new_value: sanitizeHistoryValue(entry2.new_value, "task_history.new_value"),
13593
+ agent_id: entry2.agent_id || null,
13594
+ machine_id: entry2.machine_id ?? currentStorageMachineId(d)
13595
+ };
13214
13596
  d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
13215
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
13597
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
13598
+ safeEntry.id,
13599
+ safeEntry.task_id,
13600
+ safeEntry.action,
13601
+ safeEntry.field,
13602
+ safeEntry.old_value,
13603
+ safeEntry.new_value,
13604
+ safeEntry.agent_id,
13605
+ safeEntry.created_at,
13606
+ safeEntry.machine_id ?? null
13607
+ ]);
13216
13608
  try {
13217
13609
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
13218
13610
  logActivity2({
13219
13611
  entity_type: "task",
13220
- entity_id: taskId,
13221
- action,
13222
- field,
13223
- old_value: safeOldValue,
13224
- new_value: safeNewValue,
13225
- actor_id: agentId ?? undefined
13612
+ entity_id: safeEntry.task_id,
13613
+ action: safeEntry.action,
13614
+ field: safeEntry.field ?? undefined,
13615
+ old_value: safeEntry.old_value,
13616
+ new_value: safeEntry.new_value,
13617
+ actor_id: safeEntry.agent_id ?? undefined
13226
13618
  }, d);
13227
13619
  } catch {}
13228
- return { id, task_id: taskId, action, field: field || null, old_value: safeOldValue, new_value: safeNewValue, agent_id: agentId || null, created_at: timestamp2, machine_id: machineId };
13620
+ return safeEntry;
13621
+ }
13622
+ function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, db) {
13623
+ const d = db || getDatabase();
13624
+ return insertTaskHistory({
13625
+ id: uuid(),
13626
+ task_id: taskId,
13627
+ action,
13628
+ field: field || null,
13629
+ old_value: oldValue ?? null,
13630
+ new_value: newValue ?? null,
13631
+ agent_id: agentId || null,
13632
+ created_at: now(),
13633
+ machine_id: currentStorageMachineId(d)
13634
+ }, d);
13229
13635
  }
13230
13636
  function getTaskHistory(taskId, db) {
13231
13637
  const d = db || getDatabase();
@@ -14192,6 +14598,7 @@ __export(exports_task_lifecycle, {
14192
14598
  startTask: () => startTask2,
14193
14599
  spawnNextRecurrence: () => spawnNextRecurrence,
14194
14600
  lockTask: () => lockTask2,
14601
+ handoffStaleTaskLock: () => handoffStaleTaskLock,
14195
14602
  getTasksChangedSince: () => getTasksChangedSince,
14196
14603
  getTaskLockStatus: () => getTaskLockStatus,
14197
14604
  getStaleTasks: () => getStaleTasks,
@@ -14460,6 +14867,38 @@ function unlockTask2(id, agentId, db) {
14460
14867
  WHERE id = ?`, [timestamp2, id]);
14461
14868
  return true;
14462
14869
  }
14870
+ function handoffStaleTaskLock(input, db) {
14871
+ const d = db || getDatabase();
14872
+ const prepared = prepareStaleLockHandoff(input);
14873
+ const receipt = buildStaleLockHandoffReceipt(prepared);
14874
+ const history = staleLockHandoffHistory(receipt, null);
14875
+ const transfer = d.transaction(() => {
14876
+ const result = d.run(`UPDATE tasks
14877
+ SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
14878
+ WHERE id = ?
14879
+ AND locked_by = ?
14880
+ AND locked_at = ?
14881
+ AND julianday(locked_at) < julianday(?)
14882
+ AND status NOT IN ('completed', 'failed', 'cancelled')`, [
14883
+ prepared.new_holder,
14884
+ prepared.operation_timestamp,
14885
+ prepared.operation_timestamp,
14886
+ prepared.task_id,
14887
+ prepared.expected_holder,
14888
+ prepared.expected_lock_version,
14889
+ prepared.stale_cutoff
14890
+ ]);
14891
+ if (result.changes === 0) {
14892
+ const current = getTask(prepared.task_id, d);
14893
+ if (!current)
14894
+ throw new TaskNotFoundError(prepared.task_id);
14895
+ throwStaleLockHandoffConflict(current, prepared);
14896
+ }
14897
+ insertTaskHistory(history, d);
14898
+ });
14899
+ transfer();
14900
+ return receipt;
14901
+ }
14463
14902
  function getTaskLockStatus(id, db) {
14464
14903
  const d = db || getDatabase();
14465
14904
  const task = getTask(id, d);
@@ -14760,6 +15199,7 @@ var init_task_lifecycle = __esm(() => {
14760
15199
  init_task_crud();
14761
15200
  init_task_graph();
14762
15201
  init_prewrite_secrets();
15202
+ init_stale_lock_handoff();
14763
15203
  });
14764
15204
 
14765
15205
  // src/db/task-crud.ts
@@ -16778,8 +17218,8 @@ var init_boards = __esm(() => {
16778
17218
  });
16779
17219
 
16780
17220
  // src/lib/artifact-store.ts
16781
- import { createHash as createHash5 } from "crypto";
16782
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
17221
+ import { createHash as createHash6 } from "crypto";
17222
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
16783
17223
  import { basename, dirname as dirname4, join as join6, resolve as resolve7 } from "path";
16784
17224
  import { tmpdir as tmpdir2 } from "os";
16785
17225
  function isInMemoryDb2(path) {
@@ -16803,7 +17243,7 @@ function artifactStorePath(relativePath) {
16803
17243
  return join6(artifactStoreRoot(), normalized);
16804
17244
  }
16805
17245
  function sha2562(buffer) {
16806
- return createHash5("sha256").update(buffer).digest("hex");
17246
+ return createHash6("sha256").update(buffer).digest("hex");
16807
17247
  }
16808
17248
  function isTextLike(buffer, path) {
16809
17249
  if (buffer.includes(0))
@@ -16846,7 +17286,7 @@ function storeArtifactContent(input) {
16846
17286
  const sourceStat = statSync2(sourcePath);
16847
17287
  if (!sourceStat.isFile())
16848
17288
  throw new Error(`Artifact path is not a file: ${sanitizePreWriteText(input.path, "artifact.path")}`);
16849
- const sourceBuffer = readFileSync2(sourcePath);
17289
+ const sourceBuffer = readFileSync3(sourcePath);
16850
17290
  const sourceSha = sha2562(sourceBuffer);
16851
17291
  const textLike = isTextLike(sourceBuffer, input.path);
16852
17292
  let storedBuffer = sourceBuffer;
@@ -16933,7 +17373,7 @@ function verifyStoredArtifact(input) {
16933
17373
  message: "stored artifact content is missing"
16934
17374
  };
16935
17375
  }
16936
- const buffer = readFileSync2(storedPath);
17376
+ const buffer = readFileSync3(storedPath);
16937
17377
  const actualSha = sha2562(buffer);
16938
17378
  const actualSize = buffer.length;
16939
17379
  const ok = actualSha === store.sha256 && actualSize === store.size_bytes;
@@ -16953,7 +17393,7 @@ function exportStoredArtifactContent(input) {
16953
17393
  const report = verifyStoredArtifact(input);
16954
17394
  if (report.status !== "ok" || !report.relative_path || !report.actual_sha256 || report.actual_size_bytes === null)
16955
17395
  return null;
16956
- const content = readFileSync2(artifactStorePath(report.relative_path));
17396
+ const content = readFileSync3(artifactStorePath(report.relative_path));
16957
17397
  return {
16958
17398
  artifact_id: input.id,
16959
17399
  sha256: report.actual_sha256,
@@ -18332,6 +18772,7 @@ __export(exports_tasks, {
18332
18772
  insertTaskTags: () => insertTaskTags,
18333
18773
  importTaskBoardBundle: () => importTaskBoardBundle,
18334
18774
  importCalendarIcs: () => importCalendarIcs,
18775
+ handoffStaleTaskLock: () => handoffStaleTaskLock,
18335
18776
  getTimeReport: () => getTimeReport,
18336
18777
  getTimeLogs: () => getTimeLogs,
18337
18778
  getTasksChangedSince: () => getTasksChangedSince,
@@ -19304,8 +19745,11 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
19304
19745
  const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
19305
19746
  result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
19306
19747
  }
19748
+ const auditImport = preflightAuditHistoryImport2(d, snapshot.auditHistory, snapshot.tombstones ?? []);
19749
+ result.errors.push(...auditImport.errors);
19307
19750
  if (result.errors.length > 0)
19308
19751
  return result;
19752
+ result.skipped += auditImport.identicalReplayCount;
19309
19753
  const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
19310
19754
  for (const row of rows) {
19311
19755
  try {
@@ -19340,10 +19784,72 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
19340
19784
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
19341
19785
  }
19342
19786
  });
19343
- applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
19787
+ insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
19344
19788
  applyTombstones(d, snapshot.tombstones ?? [], result);
19345
19789
  return result;
19346
19790
  }
19791
+ function preflightAuditHistoryImport2(db, rows, tombstones) {
19792
+ const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
19793
+ const rowsToInsert = [];
19794
+ const seen = new Map;
19795
+ let identicalReplayCount = 0;
19796
+ for (const rawRow of rows) {
19797
+ try {
19798
+ const row = asRecord(rawRow);
19799
+ if (typeof row.id !== "string" || !row.id) {
19800
+ throw new Error("task_history row is missing id");
19801
+ }
19802
+ const prior = seen.get(row.id);
19803
+ if (prior) {
19804
+ if (auditHistoryRowsAreFieldIdentical(prior, row))
19805
+ identicalReplayCount += 1;
19806
+ else
19807
+ errors.push(divergentAuditHistoryReplayError(row.id));
19808
+ continue;
19809
+ }
19810
+ seen.set(row.id, row);
19811
+ const existing = getAuditHistoryById(db, row.id);
19812
+ if (!existing) {
19813
+ rowsToInsert.push(row);
19814
+ } else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
19815
+ identicalReplayCount += 1;
19816
+ } else {
19817
+ errors.push(divergentAuditHistoryReplayError(row.id));
19818
+ }
19819
+ } catch (error) {
19820
+ errors.push(error instanceof Error ? error.message : String(error));
19821
+ }
19822
+ }
19823
+ return { rowsToInsert, identicalReplayCount, errors };
19824
+ }
19825
+ function insertAuditHistoryRows(db, rows, result) {
19826
+ for (const rawRow of rows) {
19827
+ try {
19828
+ const row = asRecord(rawRow);
19829
+ const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
19830
+ if (!presentColumns.includes("id"))
19831
+ presentColumns.unshift("id");
19832
+ const placeholders = presentColumns.map(() => "?").join(", ");
19833
+ const values = presentColumns.map((column) => valueForColumn(column, row[column]));
19834
+ const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders})`, values).changes;
19835
+ if (changes > 0) {
19836
+ result.inserted += 1;
19837
+ continue;
19838
+ }
19839
+ const existing = getAuditHistoryById(db, String(row["id"]));
19840
+ if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
19841
+ result.skipped += 1;
19842
+ } else {
19843
+ result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
19844
+ }
19845
+ } catch (error) {
19846
+ result.errors.push(error instanceof Error ? error.message : String(error));
19847
+ }
19848
+ }
19849
+ }
19850
+ function getAuditHistoryById(db, id) {
19851
+ return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
19852
+ }
19347
19853
  function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
19348
19854
  const id = row["id"];
19349
19855
  if (typeof id !== "string" || !id)
@@ -19441,7 +19947,7 @@ function tableForTombstone(objectType) {
19441
19947
  return "task_templates";
19442
19948
  if (objectType === "template_tasks")
19443
19949
  return "template_tasks";
19444
- return "task_history";
19950
+ throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
19445
19951
  }
19446
19952
  function listRows(db, table, columns) {
19447
19953
  return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
@@ -19488,6 +19994,7 @@ var init_sqlite_snapshot = __esm(() => {
19488
19994
  init_tasks();
19489
19995
  init_templates();
19490
19996
  init_storage_tombstones();
19997
+ init_audit_history_import();
19491
19998
  PROJECT_COLUMNS = [
19492
19999
  "id",
19493
20000
  "name",
@@ -19752,6 +20259,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
19752
20259
  unlockTask2(id, agentId, database());
19753
20260
  return true;
19754
20261
  },
20262
+ handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
19755
20263
  delete: (id) => deleteTask(id, database()),
19756
20264
  start: (id, agentId) => startTask2(id, agentId, database()),
19757
20265
  complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
@@ -20468,25 +20976,25 @@ var init_sqlite = __esm(() => {
20468
20976
  });
20469
20977
 
20470
20978
  // src/project-registration/authority.ts
20471
- import { createHash as createHash6 } from "crypto";
20979
+ import { createHash as createHash7 } from "crypto";
20472
20980
  function canonicalProjectRegistrationJson(value) {
20473
- return JSON.stringify(canonicalize(value));
20981
+ return JSON.stringify(canonicalize2(value));
20474
20982
  }
20475
- function canonicalize(value) {
20983
+ function canonicalize2(value) {
20476
20984
  if (Array.isArray(value))
20477
- return value.map(canonicalize);
20985
+ return value.map(canonicalize2);
20478
20986
  if (!value || typeof value !== "object")
20479
20987
  return value;
20480
20988
  const out = {};
20481
20989
  for (const key of Object.keys(value).sort()) {
20482
20990
  const entry2 = value[key];
20483
20991
  if (entry2 !== undefined)
20484
- out[key] = canonicalize(entry2);
20992
+ out[key] = canonicalize2(entry2);
20485
20993
  }
20486
20994
  return out;
20487
20995
  }
20488
20996
  function digestProjectRegistrationValue(value) {
20489
- return createHash6("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
20997
+ return createHash7("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
20490
20998
  }
20491
20999
  function deriveTodosProjectRegistrationIdempotencyKey(input) {
20492
21000
  return `prk_${digestProjectRegistrationValue({
@@ -21481,31 +21989,6 @@ var init_project_registration = __esm(() => {
21481
21989
  init_types3();
21482
21990
  });
21483
21991
 
21484
- // src/task-manifest/canonical.ts
21485
- import { createHash as createHash7 } from "crypto";
21486
- function canonicalize2(value) {
21487
- if (Array.isArray(value))
21488
- return value.map(canonicalize2);
21489
- if (value !== null && typeof value === "object") {
21490
- return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry2]) => [key, canonicalize2(entry2)]));
21491
- }
21492
- return value;
21493
- }
21494
- function canonicalJson(value) {
21495
- return JSON.stringify(canonicalize2(value));
21496
- }
21497
- function canonicalDigest(value) {
21498
- return createHash7("sha256").update(canonicalJson(value)).digest("hex");
21499
- }
21500
- function deterministicUuid(namespace, ...parts) {
21501
- const bytes = createHash7("sha256").update([namespace, ...parts].join("\x1F")).digest().subarray(0, 16);
21502
- bytes[6] = bytes[6] & 15 | 80;
21503
- bytes[8] = bytes[8] & 63 | 128;
21504
- const hex = bytes.toString("hex");
21505
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21506
- }
21507
- var init_canonical = () => {};
21508
-
21509
21992
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
21510
21993
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
21511
21994
  const t = typeof data;
@@ -29130,6 +29613,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29130
29613
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
29131
29614
  ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
29132
29615
  TaskComment: taskCommentSchema,
29616
+ StaleLockHandoffInput: staleLockHandoffInputSchema,
29617
+ StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
29133
29618
  TaskGitRef: taskGitRefSchema,
29134
29619
  Plan: planSchema,
29135
29620
  PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
@@ -29152,6 +29637,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29152
29637
  plan_id: { type: "string" },
29153
29638
  assigned_to: { type: "string" },
29154
29639
  agent_id: { type: "string" },
29640
+ created_by: { type: "string" },
29155
29641
  tags: { type: "array", items: { type: "string" } }
29156
29642
  }
29157
29643
  },
@@ -30337,6 +30823,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30337
30823
  }
30338
30824
  }
30339
30825
  },
30826
+ "/v1/tasks/{id}/stale-lock-handoff": {
30827
+ post: {
30828
+ operationId: "handoffStaleTaskLock",
30829
+ summary: "Atomically transfer one exact stale task lock",
30830
+ description: "Compares one full task UUID, current holder, and exact locked_at version, verifies the lock is strictly older than the supplied threshold, then transfers it directly and writes an immutable task-history receipt in the same backend transaction.",
30831
+ parameters: [
30832
+ {
30833
+ name: "id",
30834
+ in: "path",
30835
+ required: true,
30836
+ schema: { type: "string", format: "uuid" },
30837
+ description: "Exact full task UUID. Short ids and prefixes are rejected."
30838
+ }
30839
+ ],
30840
+ requestBody: {
30841
+ required: true,
30842
+ content: {
30843
+ "application/json": {
30844
+ schema: { $ref: "#/components/schemas/StaleLockHandoffInput" }
30845
+ }
30846
+ }
30847
+ },
30848
+ responses: {
30849
+ "200": {
30850
+ content: {
30851
+ "application/json": {
30852
+ schema: {
30853
+ type: "object",
30854
+ additionalProperties: false,
30855
+ required: ["receipt"],
30856
+ properties: {
30857
+ receipt: { $ref: "#/components/schemas/StaleLockHandoffReceipt" }
30858
+ }
30859
+ }
30860
+ }
30861
+ }
30862
+ },
30863
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
30864
+ "403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
30865
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
30866
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
30867
+ "501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
30868
+ }
30869
+ }
30870
+ },
30340
30871
  "/v1/tasks/{id}/refs": {
30341
30872
  get: {
30342
30873
  operationId: "listTaskGitRefs",
@@ -30933,7 +31464,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30933
31464
  }
30934
31465
  };
30935
31466
  }
30936
- var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
31467
+ var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
30937
31468
  var init_openapi = __esm(() => {
30938
31469
  init_package_version();
30939
31470
  init_types();
@@ -30949,9 +31480,12 @@ var init_openapi = __esm(() => {
30949
31480
  parent_id: { type: "string", nullable: true },
30950
31481
  assigned_to: { type: "string", nullable: true },
30951
31482
  agent_id: { type: "string", nullable: true },
31483
+ created_by: { type: "string", nullable: true },
30952
31484
  reason: { type: "string", nullable: true },
30953
31485
  tags: { type: "array", items: { type: "string" } },
30954
31486
  version: { type: "number" },
31487
+ locked_by: { type: "string", nullable: true },
31488
+ locked_at: { type: "string", format: "date-time", nullable: true },
30955
31489
  created_at: { type: "string" },
30956
31490
  updated_at: { type: "string" }
30957
31491
  }
@@ -31116,6 +31650,69 @@ var init_openapi = __esm(() => {
31116
31650
  created_at: { type: "string", format: "date-time" }
31117
31651
  }
31118
31652
  };
31653
+ staleLockHandoffInputSchema = {
31654
+ type: "object",
31655
+ additionalProperties: false,
31656
+ required: [
31657
+ "expected_holder",
31658
+ "expected_lock_version",
31659
+ "stale_after_seconds",
31660
+ "new_holder",
31661
+ "reason"
31662
+ ],
31663
+ properties: {
31664
+ expected_holder: { type: "string", minLength: 1 },
31665
+ expected_lock_version: {
31666
+ type: "string",
31667
+ format: "date-time",
31668
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
31669
+ description: "Exact authoritative locked_at token read from the task; no default or normalization is applied."
31670
+ },
31671
+ stale_after_seconds: {
31672
+ type: "integer",
31673
+ minimum: 1,
31674
+ description: "Lock age threshold supplied by the caller. The lock must be strictly older at the CAS instant."
31675
+ },
31676
+ new_holder: {
31677
+ type: "string",
31678
+ minLength: 1,
31679
+ description: "Must match the agent bound to the authenticated API key."
31680
+ },
31681
+ reason: { type: "string", minLength: 1, maxLength: 4096 }
31682
+ }
31683
+ };
31684
+ staleLockHandoffReceiptSchema = {
31685
+ type: "object",
31686
+ additionalProperties: false,
31687
+ required: [
31688
+ "schema_version",
31689
+ "receipt_id",
31690
+ "task_id",
31691
+ "actor",
31692
+ "previous_holder",
31693
+ "previous_lock_version",
31694
+ "new_holder",
31695
+ "new_lock_version",
31696
+ "stale_after_seconds",
31697
+ "stale_cutoff",
31698
+ "reason",
31699
+ "created_at"
31700
+ ],
31701
+ properties: {
31702
+ schema_version: { type: "string", enum: ["todos.stale-lock-handoff.v1"] },
31703
+ receipt_id: { type: "string", format: "uuid" },
31704
+ task_id: { type: "string", format: "uuid" },
31705
+ actor: { type: "string" },
31706
+ previous_holder: { type: "string" },
31707
+ previous_lock_version: { type: "string", format: "date-time" },
31708
+ new_holder: { type: "string" },
31709
+ new_lock_version: { type: "string", format: "date-time" },
31710
+ stale_after_seconds: { type: "integer", minimum: 1 },
31711
+ stale_cutoff: { type: "string", format: "date-time" },
31712
+ reason: { type: "string" },
31713
+ created_at: { type: "string", format: "date-time" }
31714
+ }
31715
+ };
31119
31716
  taskGitRefSchema = {
31120
31717
  type: "object",
31121
31718
  additionalProperties: false,
@@ -32357,6 +32954,45 @@ async function handleV1Request(req, url, dependencies = {}) {
32357
32954
  return error(405, `method ${method} not allowed on /v1/tasks`);
32358
32955
  }
32359
32956
  if (action) {
32957
+ if (action === "stale-lock-handoff") {
32958
+ if (method !== "POST") {
32959
+ return error(405, "method must be POST on /v1/tasks/:id/stale-lock-handoff");
32960
+ }
32961
+ const exactId = normalizeExactTaskId(id);
32962
+ if (!principal.agent) {
32963
+ return error(403, "stale-lock handoff requires an authenticated agent-bound key", {
32964
+ code: "STALE_LOCK_HANDOFF_ACTOR_MISMATCH"
32965
+ });
32966
+ }
32967
+ if (typeof store.tasks.handoffStaleLock !== "function") {
32968
+ return error(501, "stale-lock handoff is not supported by this storage backend");
32969
+ }
32970
+ const body3 = await readJson3(req) ?? {};
32971
+ const allowed = new Set([
32972
+ "expected_holder",
32973
+ "expected_lock_version",
32974
+ "stale_after_seconds",
32975
+ "new_holder",
32976
+ "reason"
32977
+ ]);
32978
+ const unknown = Object.keys(body3).find((key2) => !allowed.has(key2));
32979
+ if (unknown) {
32980
+ return error(400, `unknown stale-lock handoff field: ${unknown}`, {
32981
+ code: "STALE_LOCK_HANDOFF_INVALID_INPUT",
32982
+ field: unknown
32983
+ });
32984
+ }
32985
+ const receipt = await store.tasks.handoffStaleLock({
32986
+ task_id: exactId,
32987
+ actor: principal.agent,
32988
+ expected_holder: body3.expected_holder,
32989
+ expected_lock_version: body3.expected_lock_version,
32990
+ stale_after_seconds: body3.stale_after_seconds,
32991
+ new_holder: body3.new_holder,
32992
+ reason: body3.reason
32993
+ }, contextFromPrincipal(principal));
32994
+ return json5({ receipt });
32995
+ }
32360
32996
  if (action === "comments") {
32361
32997
  if (method === "GET") {
32362
32998
  if (!await store.tasks.get(id))
@@ -33145,7 +33781,24 @@ async function handleV1Request(req, url, dependencies = {}) {
33145
33781
  if (received === 0) {
33146
33782
  return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
33147
33783
  }
33784
+ const forbiddenAuditTombstone = (snapshot.tombstones ?? []).find((tombstone) => tombstone.object_type === "audit_history");
33785
+ if (forbiddenAuditTombstone) {
33786
+ return error(400, forbiddenAuditHistoryTombstoneError(forbiddenAuditTombstone.object_id), {
33787
+ code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
33788
+ conflict: false,
33789
+ audit_history_id: forbiddenAuditTombstone.object_id
33790
+ });
33791
+ }
33148
33792
  const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
33793
+ const auditFailureMessage = result.errors.find((message) => parseAuditHistoryImportFailure(message) !== null);
33794
+ if (auditFailureMessage) {
33795
+ const failure = parseAuditHistoryImportFailure(auditFailureMessage);
33796
+ return error(failure.status, auditFailureMessage, {
33797
+ code: failure.code,
33798
+ conflict: failure.conflict,
33799
+ audit_history_id: failure.auditHistoryId
33800
+ });
33801
+ }
33149
33802
  return json5({ result, received });
33150
33803
  }
33151
33804
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
@@ -33168,6 +33821,14 @@ async function handleV1Request(req, url, dependencies = {}) {
33168
33821
  if (e instanceof TaskNotFoundError) {
33169
33822
  return error(404, e.message, { code: TaskNotFoundError.code });
33170
33823
  }
33824
+ if (e instanceof StaleLockHandoffError) {
33825
+ const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
33826
+ return error(status2, e.message, {
33827
+ code: e.code,
33828
+ conflict: status2 === 409,
33829
+ ...e.details
33830
+ });
33831
+ }
33171
33832
  if (e instanceof PlanNotFoundError) {
33172
33833
  return error(404, e.message, { code: PlanNotFoundError.code });
33173
33834
  }
@@ -33202,6 +33863,8 @@ var init_v1 = __esm(() => {
33202
33863
  init_redaction();
33203
33864
  init_project_task_list_ensure();
33204
33865
  init_plan_project_link();
33866
+ init_stale_lock_handoff();
33867
+ init_audit_history_import();
33205
33868
  JSON_HEADERS4 = { "Content-Type": "application/json" };
33206
33869
  RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
33207
33870
  });
@@ -50727,7 +51390,7 @@ var init_token_utils = __esm(() => {
50727
51390
  });
50728
51391
 
50729
51392
  // src/lib/assignee-validation.ts
50730
- import { readFileSync as readFileSync3 } from "fs";
51393
+ import { readFileSync as readFileSync4 } from "fs";
50731
51394
  import { homedir as homedir3 } from "os";
50732
51395
  import { join as join9 } from "path";
50733
51396
  function defaultSeatRosterPath() {
@@ -50735,7 +51398,7 @@ function defaultSeatRosterPath() {
50735
51398
  }
50736
51399
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
50737
51400
  try {
50738
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
51401
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
50739
51402
  const slugs = new Set;
50740
51403
  for (const agent of parsed.agents ?? []) {
50741
51404
  if (typeof agent?.slug === "string" && agent.slug.trim()) {
@@ -63967,6 +64630,7 @@ var init_http_client = __esm(() => {
63967
64630
  });
63968
64631
 
63969
64632
  // src/cli/cloud-router.ts
64633
+ import { randomUUID as randomUUID4 } from "crypto";
63970
64634
  import { resolve as resolvePath } from "path";
63971
64635
  function cleanMode(value) {
63972
64636
  const normalized = value?.trim().toLowerCase();
@@ -64367,20 +65031,28 @@ async function cloudGetTask(client, id) {
64367
65031
  const raw = await client.get("tasks", id);
64368
65032
  return raw == null ? null : unwrapTask(raw);
64369
65033
  }
64370
- async function cloudCreateTask(client, input) {
65034
+ async function cloudCreateTask(client, input, verification2 = {}) {
64371
65035
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
64372
- const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
65036
+ const expectedCreatedBy = typeof verification2.expectedCreatedBy === "string" && verification2.expectedCreatedBy.trim() ? verification2.expectedCreatedBy : null;
65037
+ if (expectedCreatedBy !== null) {
65038
+ await requireTaskCreatorCapability(client);
65039
+ }
65040
+ const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.transport.post("/tasks", input, {
65041
+ idempotencyKey: randomUUID4(),
65042
+ retry: false
65043
+ }), ["PARENT_TASK_NOT_FOUND"]));
64373
65044
  if (!created || typeof created.id !== "string" || !created.id.trim()) {
64374
65045
  throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
64375
65046
  }
64376
65047
  const persisted = await cloudGetTask(client, created.id);
64377
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId) {
64378
- throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + "stored task id and parent_id; no success row or local SQLite fallback is permitted");
65048
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
65049
+ const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
65050
+ throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id and parent_id${creatorDetail}; no success row or local SQLite fallback is permitted`);
64379
65051
  }
64380
65052
  return persisted;
64381
65053
  }
64382
65054
  async function cloudUpdateTask(client, id, patch) {
64383
- return unwrapTask(await client.update("tasks", id, patch));
65055
+ return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
64384
65056
  }
64385
65057
  async function cloudDeleteTask(client, id) {
64386
65058
  try {
@@ -64396,6 +65068,51 @@ async function cloudTaskAction(client, id, action, body2 = {}) {
64396
65068
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body2);
64397
65069
  return unwrapTask(raw);
64398
65070
  }
65071
+ function resolveOpenApiSchema(document, schema2) {
65072
+ if (!schema2 || typeof schema2 !== "object" || Array.isArray(schema2))
65073
+ return null;
65074
+ const value = schema2;
65075
+ if (typeof value["$ref"] !== "string")
65076
+ return value;
65077
+ const reference = value["$ref"];
65078
+ if (!reference.startsWith("#/"))
65079
+ return null;
65080
+ let current = document;
65081
+ for (const segment of reference.slice(2).split("/")) {
65082
+ const key2 = segment.replace(/~1/g, "/").replace(/~0/g, "~");
65083
+ if (!current || typeof current !== "object" || Array.isArray(current))
65084
+ return null;
65085
+ current = current[key2];
65086
+ }
65087
+ return current && typeof current === "object" && !Array.isArray(current) ? current : null;
65088
+ }
65089
+ function openApiSchemaProperty(document, schemaName, propertyName) {
65090
+ const schema2 = resolveOpenApiSchema(document, {
65091
+ $ref: `#/components/schemas/${schemaName}`
65092
+ });
65093
+ const properties = schema2?.["properties"];
65094
+ if (!properties || typeof properties !== "object" || Array.isArray(properties))
65095
+ return null;
65096
+ const property = properties[propertyName];
65097
+ return property && typeof property === "object" && !Array.isArray(property) ? property : null;
65098
+ }
65099
+ async function fetchTaskCreatorCapability(client) {
65100
+ const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
65101
+ const inputProperty = openApiSchemaProperty(document, "CreateTaskInput", "created_by");
65102
+ const outputProperty = openApiSchemaProperty(document, "Task", "created_by");
65103
+ return inputProperty?.["type"] === "string" && outputProperty?.["type"] === "string" && outputProperty?.["nullable"] === true;
65104
+ }
65105
+ async function requireTaskCreatorCapability(client) {
65106
+ const authority = remoteAuthorityBase(client);
65107
+ let capability = taskCreatorCapabilityCache.get(authority);
65108
+ if (!capability) {
65109
+ capability = fetchTaskCreatorCapability(client);
65110
+ taskCreatorCapabilityCache.set(authority, capability);
65111
+ }
65112
+ if (!await capability) {
65113
+ throw new Error(`REMOTE_CREATED_BY_UNSUPPORTED: configured Todos authority ${authority} does not advertise created_by ` + "on both CreateTaskInput and Task in /v1/openapi.json; no task mutation was sent; deploy a compatible " + "@hasna/todos /v1 server before retrying; local SQLite fallback is disabled");
65114
+ }
65115
+ }
64399
65116
  async function cloudGetStats(client) {
64400
65117
  const raw = await requiredRemoteRoute(client, "/v1/stats", () => client.transport.get("/stats"));
64401
65118
  return raw ?? {};
@@ -64591,7 +65308,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
64591
65308
  return input.toLowerCase();
64592
65309
  return (await cloudResolveTaskList(client, ref, projectId)).id;
64593
65310
  }
64594
- var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
65311
+ var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
64595
65312
  var init_cloud_router = __esm(() => {
64596
65313
  init_storage();
64597
65314
  init_mode();
@@ -64611,7 +65328,9 @@ var init_cloud_router = __esm(() => {
64611
65328
  };
64612
65329
  completionCapabilityCache = new Map;
64613
65330
  retryCapabilityCache = new Map;
65331
+ taskCreatorCapabilityCache = new Map;
64614
65332
  gitRefCapabilityCache = new Map;
65333
+ remoteCommandCapabilityCache = new Map;
64615
65334
  SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
64616
65335
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
64617
65336
  listTagsCapabilityCache = new Map;
@@ -65007,7 +65726,7 @@ var init_task_crud2 = __esm(() => {
65007
65726
  });
65008
65727
 
65009
65728
  // src/lib/project-bootstrap.ts
65010
- import { existsSync as existsSync9, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
65729
+ import { existsSync as existsSync9, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
65011
65730
  import { basename as basename3, dirname as dirname6, resolve as resolve9 } from "path";
65012
65731
  function safeStat(path) {
65013
65732
  try {
@@ -65041,7 +65760,7 @@ function readPackageJson(path) {
65041
65760
  if (!existsSync9(file))
65042
65761
  return null;
65043
65762
  try {
65044
- const parsed = JSON.parse(readFileSync4(file, "utf-8"));
65763
+ const parsed = JSON.parse(readFileSync5(file, "utf-8"));
65045
65764
  return parsed && typeof parsed === "object" ? parsed : null;
65046
65765
  } catch {
65047
65766
  return null;
@@ -65603,7 +66322,7 @@ var init_retention_cleanup = __esm(() => {
65603
66322
  });
65604
66323
 
65605
66324
  // src/lib/mention-resolver.ts
65606
- import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
66325
+ import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
65607
66326
  import { basename as basename4, isAbsolute, join as join10, relative as relative3, resolve as resolve10, sep as sep3 } from "path";
65608
66327
  function blankResolution(parsed) {
65609
66328
  return {
@@ -65712,7 +66431,7 @@ function resolveFile(parsed, workspace) {
65712
66431
  return resolution;
65713
66432
  }
65714
66433
  if (parsed.line !== undefined) {
65715
- const lineCount = readFileSync5(absolutePath, "utf-8").split(/\r?\n/).length;
66434
+ const lineCount = readFileSync6(absolutePath, "utf-8").split(/\r?\n/).length;
65716
66435
  if (parsed.line < 1 || parsed.line > lineCount) {
65717
66436
  resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
65718
66437
  return resolution;
@@ -65765,7 +66484,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
65765
66484
  const pattern = symbolPattern(name);
65766
66485
  const matches = [];
65767
66486
  for (const file of walkSourceFiles(workspace)) {
65768
- const lines = readFileSync5(file, "utf-8").split(/\r?\n/);
66487
+ const lines = readFileSync6(file, "utf-8").split(/\r?\n/);
65769
66488
  for (let index = 0;index < lines.length; index += 1) {
65770
66489
  const line = lines[index];
65771
66490
  const found = pattern.exec(line);
@@ -69432,7 +70151,7 @@ var init_audit_ledger = __esm(() => {
69432
70151
  });
69433
70152
 
69434
70153
  // src/lib/release-compatibility.ts
69435
- import { readFileSync as readFileSync6 } from "fs";
70154
+ import { readFileSync as readFileSync7 } from "fs";
69436
70155
  import { join as join11, resolve as resolve12 } from "path";
69437
70156
  import { Database as Database2 } from "bun:sqlite";
69438
70157
  function pass(id, message, details) {
@@ -69445,7 +70164,7 @@ function warn(id, message, details) {
69445
70164
  return { id, status: "warning", message, details };
69446
70165
  }
69447
70166
  function readPackageJson2(root) {
69448
- return JSON.parse(readFileSync6(join11(root, "package.json"), "utf8"));
70167
+ return JSON.parse(readFileSync7(join11(root, "package.json"), "utf8"));
69449
70168
  }
69450
70169
  function sortedKeys(value) {
69451
70170
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -69826,6 +70545,8 @@ function searchTaskEntities(filters, db) {
69826
70545
  return tasks.filter((task2) => taskMatchesSavedFilters(task2, filters, db)).slice(0, normalizeLimit(filters.limit));
69827
70546
  }
69828
70547
  function searchProjects(filters, db) {
70548
+ if (filters.agent_id)
70549
+ return [];
69829
70550
  const params = [];
69830
70551
  let sql = "SELECT * FROM projects WHERE 1=1";
69831
70552
  if (filters.project_id) {
@@ -99368,7 +100089,7 @@ var init_agent_run_dispatcher = __esm(() => {
99368
100089
  });
99369
100090
 
99370
100091
  // src/lib/verification-providers.ts
99371
- import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
100092
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
99372
100093
  function normalizeName5(name) {
99373
100094
  const normalized = name.trim().toLowerCase();
99374
100095
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -99520,7 +100241,7 @@ Timed out after ${provider.timeout_ms}ms`);
99520
100241
  };
99521
100242
  }
99522
100243
  function runCiLogProvider(input) {
99523
- const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ? readFileSync7(input.log_path, "utf-8") : "");
100244
+ const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ? readFileSync8(input.log_path, "utf-8") : "");
99524
100245
  return {
99525
100246
  status: classifyLog(text),
99526
100247
  attempts: 1,
@@ -102261,7 +102982,7 @@ var init_local_bridge = __esm(() => {
102261
102982
 
102262
102983
  // src/lib/local-backups.ts
102263
102984
  import { createHash as createHash12 } from "crypto";
102264
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
102985
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
102265
102986
  import { dirname as dirname7, resolve as resolve13 } from "path";
102266
102987
  import { mkdirSync as mkdirSync6 } from "fs";
102267
102988
  function stableJson2(value) {
@@ -102371,7 +103092,7 @@ function writeLocalBackupFile(backup, outputPath) {
102371
103092
  return path;
102372
103093
  }
102373
103094
  function readLocalBackupFile(path) {
102374
- return JSON.parse(readFileSync8(resolve13(path), "utf-8"));
103095
+ return JSON.parse(readFileSync9(resolve13(path), "utf-8"));
102375
103096
  }
102376
103097
  function verifyLocalBackup(value, options = {}, db) {
102377
103098
  const verifiedAt = options.verified_at ?? now();
@@ -103822,7 +104543,7 @@ __export(exports_local_extensions, {
103822
104543
  discoverLocalExtensions: () => discoverLocalExtensions
103823
104544
  });
103824
104545
  import { createHash as createHash15, createVerify } from "crypto";
103825
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
104546
+ import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
103826
104547
  import { basename as basename5, join as join12, resolve as resolve14 } from "path";
103827
104548
  function isObject3(value) {
103828
104549
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -103904,7 +104625,7 @@ function normalizeManifest(input) {
103904
104625
  };
103905
104626
  }
103906
104627
  function parseJson2(path) {
103907
- return JSON.parse(readFileSync9(path, "utf8"));
104628
+ return JSON.parse(readFileSync10(path, "utf8"));
103908
104629
  }
103909
104630
  function sha2566(bytes) {
103910
104631
  return `sha256:${createHash15("sha256").update(bytes).digest("hex")}`;
@@ -104088,7 +104809,7 @@ function inspectExtensionSource(source3) {
104088
104809
  const manifestPath = stat.isDirectory() ? [join12(resolved, "todos.extension.json"), join12(resolved, "extension.json")].find(existsSync13) : resolved;
104089
104810
  if (!manifestPath)
104090
104811
  throw new Error(`extension directory ${source3} is missing todos.extension.json`);
104091
- const raw = readFileSync9(manifestPath);
104812
+ const raw = readFileSync10(manifestPath);
104092
104813
  const parsed = parseJson2(manifestPath);
104093
104814
  const bundle = isObject3(parsed) && isObject3(parsed["manifest"]);
104094
104815
  const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
@@ -108601,7 +109322,7 @@ __export(exports_extract, {
108601
109322
  buildCodebaseIndex: () => buildCodebaseIndex,
108602
109323
  EXTRACT_TAGS: () => EXTRACT_TAGS
108603
109324
  });
108604
- import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
109325
+ import { existsSync as existsSync14, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
108605
109326
  import { createHash as createHash17 } from "crypto";
108606
109327
  import { relative as relative5, resolve as resolve15, join as join13 } from "path";
108607
109328
  function stableHash(value) {
@@ -108616,7 +109337,7 @@ function readGitignorePatterns(basePath) {
108616
109337
  if (!existsSync14(gitignorePath))
108617
109338
  return [];
108618
109339
  try {
108619
- return readFileSync10(gitignorePath, "utf-8").split(`
109340
+ return readFileSync11(gitignorePath, "utf-8").split(`
108620
109341
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!"));
108621
109342
  } catch {
108622
109343
  return [];
@@ -108757,7 +109478,7 @@ function buildCodebaseIndex(options) {
108757
109478
  for (const file of files) {
108758
109479
  const fullPath = statSync7(basePath).isFile() ? basePath : join13(basePath, file);
108759
109480
  try {
108760
- const source3 = readFileSync10(fullPath, "utf-8");
109481
+ const source3 = readFileSync11(fullPath, "utf-8");
108761
109482
  const relPath = statSync7(basePath).isFile() ? relative5(resolve15(basePath, ".."), fullPath) : file;
108762
109483
  indexed.push({
108763
109484
  file: relPath,
@@ -108788,7 +109509,7 @@ function extractTodos(options, db) {
108788
109509
  for (const file of files) {
108789
109510
  const fullPath = statSync7(basePath).isFile() ? basePath : join13(basePath, file);
108790
109511
  try {
108791
- const source3 = readFileSync10(fullPath, "utf-8");
109512
+ const source3 = readFileSync11(fullPath, "utf-8");
108792
109513
  const relPath = statSync7(basePath).isFile() ? relative5(resolve15(basePath, ".."), fullPath) : file;
108793
109514
  const comments = extractFromSource(source3, relPath, tags);
108794
109515
  allComments.push(...comments);
@@ -110330,7 +111051,7 @@ __export(exports_environment_snapshots, {
110330
111051
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
110331
111052
  });
110332
111053
  import { createHash as createHash18 } from "crypto";
110333
- import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
111054
+ import { existsSync as existsSync15, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
110334
111055
  import { hostname as hostname3, platform, arch } from "os";
110335
111056
  import { dirname as dirname8, join as join15, resolve as resolve16 } from "path";
110336
111057
  import { tmpdir as tmpdir3 } from "os";
@@ -110344,7 +111065,7 @@ function fileRecord(root, relativePath) {
110344
111065
  const stat = statSync8(path);
110345
111066
  if (!stat.isFile())
110346
111067
  return null;
110347
- const content = readFileSync11(path);
111068
+ const content = readFileSync12(path);
110348
111069
  return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
110349
111070
  }
110350
111071
  function manifestRecord(root, relativePath) {