@hasna/todos 0.15.47 → 0.15.49

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 (38) hide show
  1. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  2. package/dist/cli/index.js +296 -81
  3. package/dist/contracts.js +56 -5
  4. package/dist/db/task-crud.d.ts +6 -0
  5. package/dist/db/task-crud.d.ts.map +1 -1
  6. package/dist/db/tasks.d.ts +1 -1
  7. package/dist/db/tasks.d.ts.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +187 -70
  11. package/dist/lib/dedupe-projection.d.ts +48 -0
  12. package/dist/lib/dedupe-projection.d.ts.map +1 -0
  13. package/dist/lib/redaction.d.ts.map +1 -1
  14. package/dist/lib/task-dedupe.d.ts +5 -9
  15. package/dist/lib/task-dedupe.d.ts.map +1 -1
  16. package/dist/mcp/index.js +246 -80
  17. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  18. package/dist/mcp.js +3 -2
  19. package/dist/project-registration.js +128 -68
  20. package/dist/registry.js +56 -5
  21. package/dist/release-provenance.json +5 -5
  22. package/dist/server/cloud.d.ts.map +1 -1
  23. package/dist/server/index.js +248 -82
  24. package/dist/server/routes.d.ts.map +1 -1
  25. package/dist/server/v1.d.ts.map +1 -1
  26. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  27. package/dist/storage/postgres-errors.d.ts +15 -0
  28. package/dist/storage/postgres-errors.d.ts.map +1 -0
  29. package/dist/storage/postgres-sync.d.ts +7 -0
  30. package/dist/storage/postgres-sync.d.ts.map +1 -1
  31. package/dist/storage/shadow-outbox.d.ts.map +1 -1
  32. package/dist/storage/shadow.d.ts.map +1 -1
  33. package/dist/storage.js +139 -66
  34. package/dist/task-manifest.js +118 -47
  35. package/dist/task-subtree-transfer.js +326 -44
  36. package/dist/types/index.d.ts +2 -2
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -5531,13 +5531,15 @@ function upsertSecretSafetyConfig(input) {
5531
5531
  saveConfig({ ...config, secret_safety: next });
5532
5532
  return next;
5533
5533
  }
5534
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
5534
+ var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
5535
5535
  var init_redaction = __esm(() => {
5536
5536
  init_config2();
5537
+ XAI_PREFIX = ["x", "ai", "-"].join("");
5537
5538
  DEFAULT_SECRET_PATTERNS = [
5538
5539
  { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
5539
5540
  { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
5540
5541
  { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
5542
+ { name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
5541
5543
  { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
5542
5544
  { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
5543
5545
  { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
@@ -9933,6 +9935,12 @@ function listTasks(filter = {}, db) {
9933
9935
  const d = db || getDatabase();
9934
9936
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
9935
9937
  clearExpiredLocks2(d);
9938
+ if (filter.limit !== undefined) {
9939
+ const limit = filter.limit;
9940
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
9941
+ throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
9942
+ }
9943
+ }
9936
9944
  const conditions = [];
9937
9945
  const params = [];
9938
9946
  if (filter.project_id) {
@@ -10466,6 +10474,7 @@ function deleteTask(id, db) {
10466
10474
  }
10467
10475
  return result.changes > 0;
10468
10476
  }
10477
+ var MAX_TASK_LIST_LIMIT = 1e5;
10469
10478
  var init_task_crud = __esm(() => {
10470
10479
  init_types();
10471
10480
  init_database();
@@ -13184,7 +13193,7 @@ var init_dispatches = __esm(() => {
13184
13193
  // package.json
13185
13194
  var package_default = {
13186
13195
  name: "@hasna/todos",
13187
- version: "0.15.47",
13196
+ version: "0.15.49",
13188
13197
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13189
13198
  type: "module",
13190
13199
  main: "dist/index.js",
@@ -13248,6 +13257,7 @@ var package_default = {
13248
13257
  ],
13249
13258
  scripts: {
13250
13259
  build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
13260
+ "build:js": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
13251
13261
  "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
13252
13262
  migrate: "bun run src/server/index.ts migrate",
13253
13263
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
@@ -13302,7 +13312,7 @@ var package_default = {
13302
13312
  author: "Andrei Hasna <andrei@hasna.com>",
13303
13313
  license: "Apache-2.0",
13304
13314
  dependencies: {
13305
- "@hasna/contracts": "0.13.4",
13315
+ "@hasna/contracts": "0.14.0",
13306
13316
  "@hasna/events": "^0.1.11",
13307
13317
  "@modelcontextprotocol/sdk": "^1.12.1",
13308
13318
  chalk: "^5.4.1",
@@ -22129,6 +22139,47 @@ function autoDetectFileRelationships(taskId, db) {
22129
22139
 
22130
22140
  // src/lib/task-dedupe.ts
22131
22141
  init_tasks();
22142
+
22143
+ // src/lib/dedupe-projection.ts
22144
+ init_redaction();
22145
+ var DEDUPE_SOURCE_KEY_ALLOWLIST = [
22146
+ "github_url",
22147
+ "github_issue_url",
22148
+ "github_pr_url",
22149
+ "source_url",
22150
+ "url",
22151
+ "external_url",
22152
+ "issue_url",
22153
+ "github_owner",
22154
+ "github_repo",
22155
+ "github_number"
22156
+ ];
22157
+ function projectTasksForDedupe(tasks) {
22158
+ return tasks.map((task2) => {
22159
+ const metadata = {};
22160
+ for (const key of DEDUPE_SOURCE_KEY_ALLOWLIST) {
22161
+ if (key in task2.metadata) {
22162
+ metadata[key] = redactValue(task2.metadata[key]);
22163
+ }
22164
+ }
22165
+ return {
22166
+ id: task2.id,
22167
+ short_id: task2.short_id,
22168
+ title: redactEvidenceText(task2.title),
22169
+ description: task2.description === null || task2.description === undefined ? null : redactEvidenceText(task2.description),
22170
+ status: task2.status,
22171
+ created_at: task2.created_at,
22172
+ updated_at: task2.updated_at,
22173
+ project_id: task2.project_id,
22174
+ task_list_id: task2.task_list_id,
22175
+ assigned_to: task2.assigned_to,
22176
+ priority: task2.priority,
22177
+ metadata
22178
+ };
22179
+ });
22180
+ }
22181
+
22182
+ // src/lib/task-dedupe.ts
22132
22183
  var DEFAULT_THRESHOLD = 0.74;
22133
22184
  var STOP_WORDS = new Set([
22134
22185
  "a",
@@ -22321,10 +22372,10 @@ function olderFirst(left, right) {
22321
22372
  function findDuplicateTasks(options = {}, db) {
22322
22373
  const d = db || getDatabase();
22323
22374
  const threshold = options.threshold ?? DEFAULT_THRESHOLD;
22324
- const fingerprints = listTasks({
22375
+ const fingerprints = projectTasksForDedupe(listTasks({
22325
22376
  include_archived: Boolean(options.include_archived),
22326
22377
  limit: options.limit ?? 1000
22327
- }, d).map(fingerprint);
22378
+ }, d)).map(fingerprint);
22328
22379
  const candidates = [];
22329
22380
  for (let i = 0;i < fingerprints.length; i++) {
22330
22381
  for (let j = i + 1;j < fingerprints.length; j++) {
@@ -27632,6 +27683,30 @@ function changedSinceStampNewer(stamp, since) {
27632
27683
  // src/storage/postgres-adapter.ts
27633
27684
  init_stale_lock_handoff();
27634
27685
 
27686
+ // src/storage/postgres-sync.ts
27687
+ init_types();
27688
+
27689
+ // src/storage/postgres-errors.ts
27690
+ function isPostgresUniqueViolation(error) {
27691
+ if (typeof error !== "object" || error === null)
27692
+ return false;
27693
+ const candidate = error;
27694
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
27695
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
27696
+ const cause = candidate.cause;
27697
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
27698
+ }
27699
+ return states.some((state) => String(state) === "23505");
27700
+ }
27701
+ function postgresConstraintName(error) {
27702
+ if (typeof error !== "object" || error === null)
27703
+ return "";
27704
+ const candidate = error;
27705
+ const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
27706
+ const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
27707
+ return typeof constraint === "string" ? constraint : "";
27708
+ }
27709
+
27635
27710
  // src/storage/postgres-sync.ts
27636
27711
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
27637
27712
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -27834,53 +27909,98 @@ class PostgresTodosSyncStore {
27834
27909
  if (routingErrors.length > 0) {
27835
27910
  throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
27836
27911
  }
27837
- const existing = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
27838
- FROM ${this.tableName}
27839
- WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
27840
- const existingProjects = [];
27841
- const existingTaskLists = [];
27842
- for (const row of existing.rows) {
27843
- const payload = payloadRecord(row.payload);
27844
- if (row.object_type === "projects")
27845
- existingProjects.push(payload);
27846
- if (row.object_type === "task_lists")
27847
- existingTaskLists.push(payload);
27848
- }
27849
- const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
27850
- if (destinationErrors.length > 0) {
27851
- throw new Error(`Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
27852
- }
27853
- const result = { records: 0, objectTypes: {} };
27854
- const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
27855
- for (const entry of snapshotEntries(snapshot)) {
27856
- if (entry.deletedAt === null)
27857
- assertCanonicalScopedSlugEntry(entry);
27858
- await this.client.query(`INSERT INTO ${this.tableName} (
27859
- service, object_type, object_id, payload, updated_at,
27860
- deleted_at, source_machine_id, version
27861
- ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
27862
- ON CONFLICT (service, object_type, object_id) DO UPDATE SET
27863
- payload = EXCLUDED.payload,
27864
- updated_at = EXCLUDED.updated_at,
27865
- deleted_at = EXCLUDED.deleted_at,
27866
- source_machine_id = EXCLUDED.source_machine_id,
27867
- version = EXCLUDED.version
27868
- WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
27869
- OR (${this.tableName}.updated_at = EXCLUDED.updated_at
27870
- AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
27871
- this.service,
27872
- entry.type,
27873
- entry.id,
27874
- entry.payload,
27875
- entry.updatedAt,
27876
- entry.deletedAt,
27877
- sourceMachineId,
27878
- entry.version
27879
- ]);
27880
- result.records += 1;
27881
- result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
27912
+ const push = async (client) => {
27913
+ const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
27914
+ FROM ${this.tableName}
27915
+ WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
27916
+ const existingProjects = [];
27917
+ const existingTaskLists = [];
27918
+ for (const row of existing.rows) {
27919
+ const payload = payloadRecord(row.payload);
27920
+ if (row.object_type === "projects")
27921
+ existingProjects.push(payload);
27922
+ if (row.object_type === "task_lists")
27923
+ existingTaskLists.push(payload);
27924
+ }
27925
+ const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
27926
+ if (destinationErrors.length > 0) {
27927
+ throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
27928
+ }
27929
+ const result = { records: 0, objectTypes: {} };
27930
+ const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
27931
+ for (const entry of snapshotEntries(snapshot)) {
27932
+ if (entry.deletedAt === null)
27933
+ assertCanonicalScopedSlugEntry(entry);
27934
+ try {
27935
+ await client.query(`INSERT INTO ${this.tableName} (
27936
+ service, object_type, object_id, payload, updated_at,
27937
+ deleted_at, source_machine_id, version
27938
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
27939
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
27940
+ payload = EXCLUDED.payload,
27941
+ updated_at = EXCLUDED.updated_at,
27942
+ deleted_at = EXCLUDED.deleted_at,
27943
+ source_machine_id = EXCLUDED.source_machine_id,
27944
+ version = EXCLUDED.version
27945
+ WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
27946
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
27947
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
27948
+ this.service,
27949
+ entry.type,
27950
+ entry.id,
27951
+ entry.payload,
27952
+ entry.updatedAt,
27953
+ entry.deletedAt,
27954
+ sourceMachineId,
27955
+ entry.version
27956
+ ]);
27957
+ } catch (error) {
27958
+ await this.classifySyncInsertConflict(error, entry);
27959
+ }
27960
+ result.records += 1;
27961
+ result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
27962
+ }
27963
+ return result;
27964
+ };
27965
+ if (typeof this.client.transaction === "function") {
27966
+ return this.client.transaction((client) => push(client));
27882
27967
  }
27883
- return result;
27968
+ return push(this.client);
27969
+ }
27970
+ async classifySyncInsertConflict(error, entry) {
27971
+ if (!isPostgresUniqueViolation(error))
27972
+ throw error;
27973
+ const payload = entry.payload;
27974
+ const taskListSlug = String(payload["slug"] ?? "");
27975
+ const projectSlug = String(payload["task_list_id"] ?? "");
27976
+ const constraintName = postgresConstraintName(error);
27977
+ if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
27978
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
27979
+ }
27980
+ if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
27981
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
27982
+ }
27983
+ if (entry.type === "task_lists") {
27984
+ const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
27985
+ const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
27986
+ SELECT 1 FROM ${this.tableName}
27987
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
27988
+ AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
27989
+ ) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
27990
+ if (conflict.rows[0]?.conflict) {
27991
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
27992
+ }
27993
+ } else if (entry.type === "projects") {
27994
+ const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
27995
+ SELECT 1 FROM ${this.tableName}
27996
+ WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
27997
+ AND deleted_at IS NULL AND payload->>'task_list_id' = $3
27998
+ ) AS conflict`, [this.service, entry.id, projectSlug]);
27999
+ if (conflict.rows[0]?.conflict) {
28000
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
28001
+ }
28002
+ }
28003
+ throw error;
27884
28004
  }
27885
28005
  async pullSnapshot(options = {}) {
27886
28006
  const params = [this.service];
@@ -30592,27 +30712,9 @@ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
30592
30712
  }
30593
30713
  throw lastError;
30594
30714
  }
30595
- function isPostgresUniqueViolation(error) {
30596
- if (typeof error !== "object" || error === null)
30597
- return false;
30598
- const candidate = error;
30599
- const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
30600
- if (typeof candidate.cause === "object" && candidate.cause !== null) {
30601
- const cause = candidate.cause;
30602
- states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
30603
- }
30604
- return states.some((state) => String(state) === "23505");
30605
- }
30606
- function postgresConstraintName(error) {
30607
- if (typeof error !== "object" || error === null)
30608
- return "";
30609
- const candidate = error;
30610
- const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
30611
- const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
30612
- return typeof constraint === "string" ? constraint : "";
30613
- }
30614
30715
 
30615
30716
  // src/storage/shadow.ts
30717
+ init_types();
30616
30718
  var SNAPSHOT_TO_OBJECT_TYPE = {
30617
30719
  tasks: "tasks",
30618
30720
  projects: "projects",
@@ -30754,6 +30856,12 @@ class TodosShadowMirror {
30754
30856
  const message = error instanceof Error ? error.message : String(error);
30755
30857
  const objectType = op.kind === "upsert" ? SNAPSHOT_TO_OBJECT_TYPE[op.key] : op.objectType;
30756
30858
  const id = op.kind === "upsert" ? String(op.record["id"]) : op.id;
30859
+ if (error instanceof ResourceConflictError) {
30860
+ this.metrics.failed += 1;
30861
+ this.metrics.lastError = message;
30862
+ this.onEvent?.({ type: "dropped", objectType, id, error: message });
30863
+ return;
30864
+ }
30757
30865
  this.metrics.retries += 1;
30758
30866
  this.metrics.lastError = message;
30759
30867
  op.attempts += 1;
@@ -31104,6 +31212,7 @@ function assertPostgresAdapterCapabilities(adapter) {
31104
31212
  }
31105
31213
  }
31106
31214
  // src/storage/shadow-outbox.ts
31215
+ init_types();
31107
31216
  init_shadow_outbox_schema();
31108
31217
  init_shadow_outbox_schema();
31109
31218
  var MAX_BACKOFF_MS = 5 * 60000;
@@ -31233,6 +31342,12 @@ class TodosShadowOutbox {
31233
31342
  this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
31234
31343
  } catch (error) {
31235
31344
  const message = error instanceof Error ? error.message : String(error);
31345
+ if (error instanceof ResourceConflictError) {
31346
+ this.metrics.lastError = message;
31347
+ this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [row.attempts + 1, message, row.seq, row.revision]);
31348
+ this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
31349
+ return;
31350
+ }
31236
31351
  this.metrics.retries += 1;
31237
31352
  this.metrics.lastError = message;
31238
31353
  const attempts = row.attempts + 1;
@@ -68238,6 +68353,7 @@ export {
68238
68353
  queryTasksByWorkflowState,
68239
68354
  queryTasksByLocalFields,
68240
68355
  purgeArtifact,
68356
+ projectTasksForDedupe,
68241
68357
  processDueReminders,
68242
68358
  previewUserScaffold,
68243
68359
  previewTemplate,
@@ -69206,6 +69322,7 @@ export {
69206
69322
  DEFAULT_LEASE_MINUTES,
69207
69323
  DEFAULT_ENCRYPTION_PROFILE,
69208
69324
  DEFAULT_ENCRYPTION_KEY_ENV,
69325
+ DEDUPE_SOURCE_KEY_ALLOWLIST,
69209
69326
  DECISION_STATUSES,
69210
69327
  DECISION_RECORD_SCHEMA,
69211
69328
  DB_BACKUP_SCHEMA,
@@ -0,0 +1,48 @@
1
+ import type { Task, TaskPriority, TaskStatus } from "../types/index.js";
2
+ /**
3
+ * The ONLY metadata keys the dedup projection carries. This is exactly the
4
+ * source-key set the fingerprint in `task-dedupe.ts` consumes
5
+ * (`sourceKeysFor`). Any other metadata — free-form `token`, `api_key`,
6
+ * `secret`, arbitrary composites — is never projected, so a credential value
7
+ * sitting in free-form metadata can never reach a dedup workflow's capture.
8
+ *
9
+ * Regression O15-00170 (incidents 713001/713022/713043-46/713119): workflows
10
+ * doing deduplication captured credential-bearing whole-task composites from
11
+ * `todos list --json` / `--format compact` / `--format csv` because no
12
+ * package-owned bounded projection existed. This allowlist is the bounded
13
+ * surface that abstraction provides.
14
+ */
15
+ export declare const DEDUPE_SOURCE_KEY_ALLOWLIST: readonly ["github_url", "github_issue_url", "github_pr_url", "source_url", "url", "external_url", "issue_url", "github_owner", "github_repo", "github_number"];
16
+ /**
17
+ * A task reduced to exactly the fields the dedup fingerprint consumes, plus a
18
+ * bounded source-key metadata object. It deliberately excludes free-form
19
+ * `metadata`, `tags`, comments, run/plan fields, and every other composite
20
+ * field of `Task` — the surfaces where credential values leaked. The carried
21
+ * free-form text (`title`, `description`) is redacted through the same
22
+ * pattern redactor every other output surface uses, so a credential embedded
23
+ * in prose never survives into the projection.
24
+ */
25
+ export interface DedupeTaskProjection {
26
+ id: string;
27
+ short_id: string | null;
28
+ title: string;
29
+ description: string | null;
30
+ status: TaskStatus;
31
+ created_at: string;
32
+ updated_at: string;
33
+ project_id: string | null;
34
+ task_list_id: string | null;
35
+ assigned_to: string | null;
36
+ priority: TaskPriority;
37
+ metadata: Record<string, unknown>;
38
+ }
39
+ /**
40
+ * Project whole tasks into the bounded dedup shape.
41
+ *
42
+ * This is the package-owned "canonical-machine" abstraction: the only way a
43
+ * dedup workflow may capture task state for deduplication. It never emits a
44
+ * whole-task composite, so a credential that lives in free-form metadata,
45
+ * tags, or a nested composite cannot be captured with it.
46
+ */
47
+ export declare function projectTasksForDedupe(tasks: Task[]): DedupeTaskProjection[];
48
+ //# sourceMappingURL=dedupe-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe-projection.d.ts","sourceRoot":"","sources":["../../src/lib/dedupe-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGxE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,gKAW9B,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,oBAAoB,EAAE,CAyB3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../src/lib/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AA6FD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAe1C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAOjE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAK1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CAQtF"}
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../src/lib/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAqGD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAe1C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAOjE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAK1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CAQtF"}
@@ -1,23 +1,19 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import type { Task } from "../types/index.js";
3
+ import { type DedupeTaskProjection } from "./dedupe-projection.js";
3
4
  export interface FindDuplicateTasksOptions {
4
5
  threshold?: number;
5
6
  limit?: number;
6
7
  include_archived?: boolean;
7
8
  }
8
- export interface TaskDuplicateProbe {
9
- title: string;
10
- description?: string | null;
11
- metadata?: Record<string, unknown>;
12
- }
13
9
  export interface TaskDuplicateMatch {
14
- task: Task;
10
+ task: DedupeTaskProjection;
15
11
  score: number;
16
12
  reasons: string[];
17
13
  }
18
14
  export interface DuplicateTaskCandidate {
19
- primary_task: Task;
20
- duplicate_task: Task;
15
+ primary_task: DedupeTaskProjection;
16
+ duplicate_task: DedupeTaskProjection;
21
17
  score: number;
22
18
  reasons: string[];
23
19
  }
@@ -55,7 +51,7 @@ export declare function findDuplicateTasks(options?: FindDuplicateTasksOptions,
55
51
  * Score a prospective task against local tasks without inserting a temporary
56
52
  * row. Producer previews use this to report likely duplicates before apply.
57
53
  */
58
- export declare function findDuplicateTasksForInput(input: TaskDuplicateProbe, options?: FindDuplicateTasksOptions, db?: Database): TaskDuplicateMatch[];
54
+ export declare function findDuplicateTasksForInput(input: DedupeTaskProjection, options?: FindDuplicateTasksOptions, db?: Database): TaskDuplicateMatch[];
59
55
  export declare function mergeDuplicateTask(input: MergeDuplicateTaskInput, db?: Database): TaskMergeResult;
60
56
  export declare const findDuplicateCandidates: typeof findDuplicateTasks;
61
57
  //# sourceMappingURL=task-dedupe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-dedupe.d.ts","sourceRoot":"","sources":["../../src/lib/task-dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAM9C,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,IAAI,CAAC;IACnB,kBAAkB,EAAE,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AA2ND,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,sBAAsB,EAAE,CAqBnH;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE,yBAA8B,EACvC,EAAE,CAAC,EAAE,QAAQ,GACZ,kBAAkB,EAAE,CActB;AAqJD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,eAAe,CAiGjG;AAGD,eAAO,MAAM,uBAAuB,2BAAqB,CAAC"}
1
+ {"version":3,"file":"task-dedupe.d.ts","sourceRoot":"","sources":["../../src/lib/task-dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAK9C,OAAO,EAAyB,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE1F,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,oBAAoB,CAAC;IACnC,cAAc,EAAE,oBAAoB,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,IAAI,CAAC;IACnB,kBAAkB,EAAE,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AA2ND,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,sBAAsB,EAAE,CAwBnH;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,GAAE,yBAA8B,EACvC,EAAE,CAAC,EAAE,QAAQ,GACZ,kBAAkB,EAAE,CActB;AAqJD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,eAAe,CAiGjG;AAGD,eAAO,MAAM,uBAAuB,2BAAqB,CAAC"}