@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
package/dist/contracts.js CHANGED
@@ -3662,7 +3662,7 @@ function isBlockingDependencyStatus(status) {
3662
3662
  function isTerminalStatus(status) {
3663
3663
  return status === "completed" || status === "failed" || status === "cancelled";
3664
3664
  }
3665
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3665
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3666
3666
  var init_types = __esm(() => {
3667
3667
  TASK_STATUSES = [
3668
3668
  "pending",
@@ -3785,6 +3785,16 @@ var init_types = __esm(() => {
3785
3785
  this.name = "LockError";
3786
3786
  }
3787
3787
  };
3788
+ StaleLockHandoffError = class StaleLockHandoffError extends Error {
3789
+ code;
3790
+ details;
3791
+ constructor(code, message, details = {}) {
3792
+ super(message);
3793
+ this.code = code;
3794
+ this.details = details;
3795
+ this.name = "StaleLockHandoffError";
3796
+ }
3797
+ };
3788
3798
  AgentNotFoundError = class AgentNotFoundError extends Error {
3789
3799
  agentId;
3790
3800
  static code = "AGENT_NOT_FOUND";
@@ -4721,7 +4731,7 @@ var init_database = __esm(() => {
4721
4731
  });
4722
4732
 
4723
4733
  // src/lib/config.ts
4724
- import { existsSync as existsSync4 } from "fs";
4734
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
4725
4735
  import { dirname as dirname2, join as join3 } from "path";
4726
4736
  function getConfigPath() {
4727
4737
  return join3(getTodosGlobalDir(), "config.json");
@@ -4753,6 +4763,22 @@ function saveConfig(config) {
4753
4763
  function updateConfig(patch) {
4754
4764
  return saveConfig({ ...loadConfig(), ...patch });
4755
4765
  }
4766
+ function getTodosAiConfig() {
4767
+ const configPath = getConfigPath();
4768
+ if (!existsSync4(configPath))
4769
+ return {};
4770
+ const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
4771
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4772
+ throw new Error("Todos config must be a JSON object");
4773
+ }
4774
+ const ai = parsed["ai"];
4775
+ if (ai === undefined)
4776
+ return {};
4777
+ if (ai === null || typeof ai !== "object" || Array.isArray(ai)) {
4778
+ throw new Error("Todos AI configuration must be an object");
4779
+ }
4780
+ return { ...ai };
4781
+ }
4756
4782
  function normalizeApiUrl(value) {
4757
4783
  const trimmed = value?.trim();
4758
4784
  if (!trimmed)
@@ -7083,7 +7109,7 @@ var init_shared_events = __esm(() => {
7083
7109
  });
7084
7110
 
7085
7111
  // src/lib/secret-redaction.ts
7086
- import { readFileSync as readFileSync2, existsSync as existsSync6 } from "fs";
7112
+ import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
7087
7113
  function registerCustomRedactor(fn) {
7088
7114
  customRedactors.push(fn);
7089
7115
  }
@@ -7150,7 +7176,7 @@ function scanAndRedactText(text, options = {}) {
7150
7176
  function scanFileForSecrets(path, options = {}) {
7151
7177
  if (!existsSync6(path))
7152
7178
  throw new Error(`File not found: ${path}`);
7153
- const content = readFileSync2(path, "utf8");
7179
+ const content = readFileSync3(path, "utf8");
7154
7180
  return scanAndRedactText(content, options);
7155
7181
  }
7156
7182
  function safeStringify(value, space) {
@@ -7503,28 +7529,55 @@ var init_activity_audit = __esm(() => {
7503
7529
  function sanitizeHistoryValue(value, context) {
7504
7530
  return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
7505
7531
  }
7506
- function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db) {
7532
+ function insertTaskHistory(entry, db) {
7507
7533
  const d = db || getDatabase();
7508
- const id = uuid();
7509
- const timestamp2 = now();
7510
- const machineId = currentStorageMachineId(d);
7511
- const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
7512
- const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
7534
+ const safeEntry = {
7535
+ ...entry,
7536
+ field: entry.field || null,
7537
+ old_value: sanitizeHistoryValue(entry.old_value, "task_history.old_value"),
7538
+ new_value: sanitizeHistoryValue(entry.new_value, "task_history.new_value"),
7539
+ agent_id: entry.agent_id || null,
7540
+ machine_id: entry.machine_id ?? currentStorageMachineId(d)
7541
+ };
7513
7542
  d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
7514
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field2 || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
7543
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7544
+ safeEntry.id,
7545
+ safeEntry.task_id,
7546
+ safeEntry.action,
7547
+ safeEntry.field,
7548
+ safeEntry.old_value,
7549
+ safeEntry.new_value,
7550
+ safeEntry.agent_id,
7551
+ safeEntry.created_at,
7552
+ safeEntry.machine_id ?? null
7553
+ ]);
7515
7554
  try {
7516
7555
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
7517
7556
  logActivity2({
7518
7557
  entity_type: "task",
7519
- entity_id: taskId,
7520
- action,
7521
- field: field2,
7522
- old_value: safeOldValue,
7523
- new_value: safeNewValue,
7524
- actor_id: agentId ?? undefined
7558
+ entity_id: safeEntry.task_id,
7559
+ action: safeEntry.action,
7560
+ field: safeEntry.field ?? undefined,
7561
+ old_value: safeEntry.old_value,
7562
+ new_value: safeEntry.new_value,
7563
+ actor_id: safeEntry.agent_id ?? undefined
7525
7564
  }, d);
7526
7565
  } catch {}
7527
- return { id, task_id: taskId, action, field: field2 || null, old_value: safeOldValue, new_value: safeNewValue, agent_id: agentId || null, created_at: timestamp2, machine_id: machineId };
7566
+ return safeEntry;
7567
+ }
7568
+ function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db) {
7569
+ const d = db || getDatabase();
7570
+ return insertTaskHistory({
7571
+ id: uuid(),
7572
+ task_id: taskId,
7573
+ action,
7574
+ field: field2 || null,
7575
+ old_value: oldValue ?? null,
7576
+ new_value: newValue ?? null,
7577
+ agent_id: agentId || null,
7578
+ created_at: now(),
7579
+ machine_id: currentStorageMachineId(d)
7580
+ }, d);
7528
7581
  }
7529
7582
  function getTaskHistory(taskId, db) {
7530
7583
  const d = db || getDatabase();
@@ -8547,6 +8600,142 @@ var init_task_graph = __esm(() => {
8547
8600
  init_task_crud();
8548
8601
  });
8549
8602
 
8603
+ // src/lib/stale-lock-handoff.ts
8604
+ function normalizeExactTaskId(value) {
8605
+ if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
8606
+ 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 });
8607
+ }
8608
+ return value.trim().toLowerCase();
8609
+ }
8610
+ function requireNonEmptyString(value, field2) {
8611
+ if (typeof value !== "string" || !value.trim()) {
8612
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field2} must be a non-empty string`, { field: field2 });
8613
+ }
8614
+ const trimmed = value.trim();
8615
+ if (field2 === "reason" && trimmed.length > MAX_REASON_LENGTH) {
8616
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field: field2, max_length: MAX_REASON_LENGTH });
8617
+ }
8618
+ return trimmed;
8619
+ }
8620
+ function requireCanonicalLockVersion(value) {
8621
+ if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
8622
+ 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" });
8623
+ }
8624
+ const parsed = Date.parse(value);
8625
+ if (value.startsWith("0000-") || Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
8626
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
8627
+ }
8628
+ return value;
8629
+ }
8630
+ function requireStaleThreshold(value) {
8631
+ if (!Number.isSafeInteger(value) || Number(value) <= 0) {
8632
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
8633
+ }
8634
+ return Number(value);
8635
+ }
8636
+ function prepareStaleLockHandoff(input, options = {}) {
8637
+ const taskId = normalizeExactTaskId(input.task_id);
8638
+ const actor = requireNonEmptyString(input.actor, "actor");
8639
+ const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
8640
+ const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
8641
+ const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
8642
+ const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
8643
+ const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
8644
+ if (!reason) {
8645
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
8646
+ }
8647
+ if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
8648
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
8649
+ }
8650
+ const operationTimestamp = options.now ?? new Date().toISOString();
8651
+ if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
8652
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
8653
+ }
8654
+ const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
8655
+ return {
8656
+ task_id: taskId,
8657
+ actor,
8658
+ expected_holder: expectedHolder,
8659
+ expected_lock_version: expectedLockVersion,
8660
+ stale_after_seconds: staleAfterSeconds,
8661
+ new_holder: newHolder,
8662
+ reason,
8663
+ operation_timestamp: operationTimestamp,
8664
+ stale_cutoff: staleCutoff,
8665
+ receipt_id: options.receiptId ?? crypto.randomUUID()
8666
+ };
8667
+ }
8668
+ function buildStaleLockHandoffReceipt(input) {
8669
+ return {
8670
+ schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
8671
+ receipt_id: input.receipt_id,
8672
+ task_id: input.task_id,
8673
+ actor: input.actor,
8674
+ previous_holder: input.expected_holder,
8675
+ previous_lock_version: input.expected_lock_version,
8676
+ new_holder: input.new_holder,
8677
+ new_lock_version: input.operation_timestamp,
8678
+ stale_after_seconds: input.stale_after_seconds,
8679
+ stale_cutoff: input.stale_cutoff,
8680
+ reason: input.reason,
8681
+ created_at: input.operation_timestamp
8682
+ };
8683
+ }
8684
+ function staleLockHandoffHistory(receipt, machineId) {
8685
+ return {
8686
+ id: receipt.receipt_id,
8687
+ task_id: receipt.task_id,
8688
+ action: STALE_LOCK_HANDOFF_ACTION,
8689
+ field: STALE_LOCK_HANDOFF_FIELD,
8690
+ old_value: JSON.stringify({
8691
+ holder: receipt.previous_holder,
8692
+ lock_version: receipt.previous_lock_version
8693
+ }),
8694
+ new_value: JSON.stringify(receipt),
8695
+ agent_id: receipt.actor,
8696
+ created_at: receipt.created_at,
8697
+ machine_id: machineId
8698
+ };
8699
+ }
8700
+ function throwStaleLockHandoffConflict(task, input) {
8701
+ if (!task.locked_by || !task.locked_at) {
8702
+ 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 });
8703
+ }
8704
+ if (task.locked_at !== input.expected_lock_version) {
8705
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
8706
+ task_id: input.task_id,
8707
+ expected_lock_version: input.expected_lock_version,
8708
+ current_lock_version: task.locked_at
8709
+ });
8710
+ }
8711
+ if (task.locked_by !== input.expected_holder) {
8712
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
8713
+ task_id: input.task_id,
8714
+ expected_holder: input.expected_holder,
8715
+ current_holder: task.locked_by
8716
+ });
8717
+ }
8718
+ if (isTerminalStatus(task.status)) {
8719
+ 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 });
8720
+ }
8721
+ if (task.locked_at >= input.stale_cutoff) {
8722
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
8723
+ task_id: input.task_id,
8724
+ current_lock_version: task.locked_at,
8725
+ stale_cutoff: input.stale_cutoff
8726
+ });
8727
+ }
8728
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
8729
+ }
8730
+ 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;
8731
+ var init_stale_lock_handoff = __esm(() => {
8732
+ init_types();
8733
+ init_creator_identity();
8734
+ init_prewrite_secrets();
8735
+ 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;
8736
+ CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
8737
+ });
8738
+
8550
8739
  // src/db/task-lifecycle.ts
8551
8740
  var exports_task_lifecycle = {};
8552
8741
  __export(exports_task_lifecycle, {
@@ -8555,6 +8744,7 @@ __export(exports_task_lifecycle, {
8555
8744
  startTask: () => startTask,
8556
8745
  spawnNextRecurrence: () => spawnNextRecurrence,
8557
8746
  lockTask: () => lockTask,
8747
+ handoffStaleTaskLock: () => handoffStaleTaskLock,
8558
8748
  getTasksChangedSince: () => getTasksChangedSince,
8559
8749
  getTaskLockStatus: () => getTaskLockStatus,
8560
8750
  getStaleTasks: () => getStaleTasks,
@@ -8823,6 +9013,38 @@ function unlockTask(id, agentId, db) {
8823
9013
  WHERE id = ?`, [timestamp2, id]);
8824
9014
  return true;
8825
9015
  }
9016
+ function handoffStaleTaskLock(input, db) {
9017
+ const d = db || getDatabase();
9018
+ const prepared = prepareStaleLockHandoff(input);
9019
+ const receipt = buildStaleLockHandoffReceipt(prepared);
9020
+ const history = staleLockHandoffHistory(receipt, null);
9021
+ const transfer = d.transaction(() => {
9022
+ const result = d.run(`UPDATE tasks
9023
+ SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
9024
+ WHERE id = ?
9025
+ AND locked_by = ?
9026
+ AND locked_at = ?
9027
+ AND julianday(locked_at) < julianday(?)
9028
+ AND status NOT IN ('completed', 'failed', 'cancelled')`, [
9029
+ prepared.new_holder,
9030
+ prepared.operation_timestamp,
9031
+ prepared.operation_timestamp,
9032
+ prepared.task_id,
9033
+ prepared.expected_holder,
9034
+ prepared.expected_lock_version,
9035
+ prepared.stale_cutoff
9036
+ ]);
9037
+ if (result.changes === 0) {
9038
+ const current = getTask(prepared.task_id, d);
9039
+ if (!current)
9040
+ throw new TaskNotFoundError(prepared.task_id);
9041
+ throwStaleLockHandoffConflict(current, prepared);
9042
+ }
9043
+ insertTaskHistory(history, d);
9044
+ });
9045
+ transfer();
9046
+ return receipt;
9047
+ }
8826
9048
  function getTaskLockStatus(id, db) {
8827
9049
  const d = db || getDatabase();
8828
9050
  const task = getTask(id, d);
@@ -9123,6 +9345,7 @@ var init_task_lifecycle = __esm(() => {
9123
9345
  init_task_crud();
9124
9346
  init_task_graph();
9125
9347
  init_prewrite_secrets();
9348
+ init_stale_lock_handoff();
9126
9349
  });
9127
9350
 
9128
9351
  // src/db/task-crud.ts
@@ -11001,7 +11224,7 @@ var init_boards = __esm(() => {
11001
11224
 
11002
11225
  // src/lib/artifact-store.ts
11003
11226
  import { createHash as createHash3 } from "crypto";
11004
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
11227
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
11005
11228
  import { basename, dirname as dirname4, join as join5, resolve as resolve7 } from "path";
11006
11229
  import { tmpdir as tmpdir2 } from "os";
11007
11230
  function isInMemoryDb2(path) {
@@ -11068,7 +11291,7 @@ function storeArtifactContent(input) {
11068
11291
  const sourceStat = statSync2(sourcePath);
11069
11292
  if (!sourceStat.isFile())
11070
11293
  throw new Error(`Artifact path is not a file: ${sanitizePreWriteText(input.path, "artifact.path")}`);
11071
- const sourceBuffer = readFileSync3(sourcePath);
11294
+ const sourceBuffer = readFileSync4(sourcePath);
11072
11295
  const sourceSha = sha256(sourceBuffer);
11073
11296
  const textLike = isTextLike(sourceBuffer, input.path);
11074
11297
  let storedBuffer = sourceBuffer;
@@ -11155,7 +11378,7 @@ function verifyStoredArtifact(input) {
11155
11378
  message: "stored artifact content is missing"
11156
11379
  };
11157
11380
  }
11158
- const buffer = readFileSync3(storedPath);
11381
+ const buffer = readFileSync4(storedPath);
11159
11382
  const actualSha = sha256(buffer);
11160
11383
  const actualSize = buffer.length;
11161
11384
  const ok = actualSha === store.sha256 && actualSize === store.size_bytes;
@@ -11175,7 +11398,7 @@ function exportStoredArtifactContent(input) {
11175
11398
  const report = verifyStoredArtifact(input);
11176
11399
  if (report.status !== "ok" || !report.relative_path || !report.actual_sha256 || report.actual_size_bytes === null)
11177
11400
  return null;
11178
- const content = readFileSync3(artifactStorePath(report.relative_path));
11401
+ const content = readFileSync4(artifactStorePath(report.relative_path));
11179
11402
  return {
11180
11403
  artifact_id: input.id,
11181
11404
  sha256: report.actual_sha256,
@@ -11226,7 +11449,7 @@ function getArtifactStoreRoot(dbPath) {
11226
11449
  return join5(dirname4(resolve7(path)), "artifacts");
11227
11450
  }
11228
11451
  function computeContentHash(path) {
11229
- return sha256(readFileSync3(resolve7(path)));
11452
+ return sha256(readFileSync4(resolve7(path)));
11230
11453
  }
11231
11454
  function storeArtifactFile(input) {
11232
11455
  const sourcePath = resolve7(input.sourcePath);
@@ -11236,7 +11459,7 @@ function storeArtifactFile(input) {
11236
11459
  if (!statSync2(sourcePath).isFile()) {
11237
11460
  throw new Error(`Source path is not a file: ${input.sourcePath}`);
11238
11461
  }
11239
- const buffer = readFileSync3(sourcePath);
11462
+ const buffer = readFileSync4(sourcePath);
11240
11463
  const contentHash = sha256(buffer);
11241
11464
  const mimeType = mediaTypeFor(sourcePath, isTextLike(buffer, sourcePath));
11242
11465
  const storageMode = input.storageMode ?? "copy";
@@ -12435,7 +12658,7 @@ var init_tasks = __esm(() => {
12435
12658
  // package.json
12436
12659
  var package_default = {
12437
12660
  name: "@hasna/todos",
12438
- version: "0.15.19",
12661
+ version: "0.15.24",
12439
12662
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12440
12663
  type: "module",
12441
12664
  main: "dist/index.js",
@@ -12506,6 +12729,8 @@ var package_default = {
12506
12729
  "dev:mcp": "bun run src/mcp/index.ts",
12507
12730
  "dev:serve": "bun run src/server/index.ts",
12508
12731
  "verify:release": "bun run scripts/verify-public-release.ts --mode=review",
12732
+ "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
12733
+ "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
12509
12734
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
12510
12735
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
12511
12736
  },
@@ -15204,6 +15429,578 @@ function createJsonContractsManifest(options = {}) {
15204
15429
  var TODOS_JSON_CONTRACTS_MANIFEST = createJsonContractsManifest({
15205
15430
  generatedAt: "1970-01-01T00:00:00.000Z"
15206
15431
  });
15432
+
15433
+ // src/ai.ts
15434
+ var TODOS_AI_SCHEMA_VERSION = 1;
15435
+ var TODOS_AI_RUNTIME_PROTOCOL_VERSION = 1;
15436
+ var TODOS_AI_RUNTIME_SPECIFIER = "@hasna/todos-ai/runtime";
15437
+ var TODOS_AI_UPDATE_TASK_RESULT_SCHEMA = "todos.ai.update_task.v1";
15438
+ var TODOS_AI_FORMATS = ["text", "json", "stream-json"];
15439
+ var TODOS_AI_WRITE_MODES = ["read-only", "plan", "execute"];
15440
+ var TODOS_AI_APPROVAL_MODES = ["deny", "required", "prompt", "existing"];
15441
+ var TODOS_AI_RUN_STATUSES = [
15442
+ "answered",
15443
+ "needs_input",
15444
+ "needs_approval",
15445
+ "completed",
15446
+ "failed"
15447
+ ];
15448
+ var TODOS_AI_RUNTIME_EVENT_TYPES = [
15449
+ "run.started",
15450
+ "run.progress",
15451
+ "text.delta",
15452
+ "tool.started",
15453
+ "tool.completed",
15454
+ "input.required",
15455
+ "approval.required"
15456
+ ];
15457
+ var TODOS_AI_TOOL_EFFECTS = ["read", "control", "write"];
15458
+ var TODOS_AI_UPDATE_TASK_FIELDS = [
15459
+ "title",
15460
+ "description",
15461
+ "status",
15462
+ "priority",
15463
+ "assigned_to",
15464
+ "tags",
15465
+ "due_at"
15466
+ ];
15467
+ var TODOS_AI_UPDATE_TASK_LIMITS = {
15468
+ max_title_bytes: 1024,
15469
+ max_description_bytes: 8192,
15470
+ max_assignee_bytes: 256,
15471
+ max_tags: 16,
15472
+ max_tag_bytes: 128,
15473
+ max_due_at_bytes: 128,
15474
+ min_idempotency_key_bytes: 8,
15475
+ max_idempotency_key_bytes: 128,
15476
+ max_result_bytes: 65536
15477
+ };
15478
+ var TODOS_AI_DEFAULTS = {
15479
+ format: "text",
15480
+ max_steps: 8,
15481
+ timeout_ms: 60000,
15482
+ write_mode: "read-only"
15483
+ };
15484
+ var TODOS_AI_LIMITS = {
15485
+ max_prompt_bytes: 1048576,
15486
+ max_json_bytes: 1048576,
15487
+ max_result_bytes: 4194304,
15488
+ max_variable_count: 100,
15489
+ max_variable_value_bytes: 65536,
15490
+ max_approval_refs: 32,
15491
+ max_approval_ref_bytes: 1024,
15492
+ max_resume_run_id_bytes: 1024,
15493
+ max_pending_input_prompt_bytes: 1024,
15494
+ max_pending_input_fields: 16,
15495
+ max_pending_input_field_bytes: 128,
15496
+ max_pending_approval_id_bytes: 256,
15497
+ max_pending_approval_summary_bytes: 1024,
15498
+ max_pending_approval_operations: 4,
15499
+ max_pending_approval_bytes: 8192,
15500
+ max_stream_events: 1000,
15501
+ max_stream_record_bytes: 262144,
15502
+ max_stream_bytes: 8388608,
15503
+ min_steps: 1,
15504
+ max_steps: 20,
15505
+ min_timeout_ms: 1000,
15506
+ max_timeout_ms: 600000
15507
+ };
15508
+ var TODOS_AI_EXIT_CODES = {
15509
+ success: 0,
15510
+ usage: 2,
15511
+ needs_input: 3,
15512
+ needs_approval: 4,
15513
+ runtime_unavailable: 5,
15514
+ failed: 6,
15515
+ timeout: 124,
15516
+ interrupted: 130
15517
+ };
15518
+ var TODOS_AI_PROTOCOL = {
15519
+ schema_version: TODOS_AI_SCHEMA_VERSION,
15520
+ runtime_protocol_version: TODOS_AI_RUNTIME_PROTOCOL_VERSION,
15521
+ runtime_specifier: TODOS_AI_RUNTIME_SPECIFIER,
15522
+ formats: TODOS_AI_FORMATS,
15523
+ write_modes: TODOS_AI_WRITE_MODES,
15524
+ approval_modes: TODOS_AI_APPROVAL_MODES,
15525
+ statuses: TODOS_AI_RUN_STATUSES,
15526
+ event_types: TODOS_AI_RUNTIME_EVENT_TYPES,
15527
+ tool_effects: TODOS_AI_TOOL_EFFECTS,
15528
+ defaults: TODOS_AI_DEFAULTS,
15529
+ limits: TODOS_AI_LIMITS,
15530
+ exit_codes: TODOS_AI_EXIT_CODES
15531
+ };
15532
+
15533
+ class TodosAiContractError extends Error {
15534
+ code;
15535
+ exitCode;
15536
+ constructor(code, message, exitCode = TODOS_AI_EXIT_CODES.usage, options) {
15537
+ super(message, options);
15538
+ this.code = code;
15539
+ this.exitCode = exitCode;
15540
+ this.name = "TodosAiContractError";
15541
+ }
15542
+ }
15543
+
15544
+ class TodosAiNeedsInputSignal extends Error {
15545
+ pending_input;
15546
+ constructor(pendingInput) {
15547
+ if (!isTodosAiJsonValue(pendingInput) || !isPendingInput(pendingInput)) {
15548
+ throw new TodosAiContractError("invalid_input", "Todos AI pending input must be bounded stable control data");
15549
+ }
15550
+ super("Todos AI input required");
15551
+ this.name = "TodosAiNeedsInputSignal";
15552
+ this.pending_input = {
15553
+ prompt: pendingInput.prompt,
15554
+ fields: [...pendingInput.fields]
15555
+ };
15556
+ }
15557
+ }
15558
+
15559
+ class TodosAiNeedsApprovalSignal extends Error {
15560
+ pending_approval;
15561
+ constructor(pendingApproval) {
15562
+ if (!isTodosAiJsonValue(pendingApproval) || !isPendingApproval(pendingApproval)) {
15563
+ throw new TodosAiContractError("invalid_input", "Todos AI pending approval must be bounded stable control data");
15564
+ }
15565
+ super("Todos AI approval required");
15566
+ this.name = "TodosAiNeedsApprovalSignal";
15567
+ this.pending_approval = {
15568
+ id: pendingApproval.id,
15569
+ summary: pendingApproval.summary,
15570
+ operations: pendingApproval.operations.map((operation) => JSON.parse(JSON.stringify(operation)))
15571
+ };
15572
+ }
15573
+ }
15574
+ function isRecord(value) {
15575
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15576
+ }
15577
+ function isJsonObject(value) {
15578
+ if (!isRecord(value))
15579
+ return false;
15580
+ const prototype = Object.getPrototypeOf(value);
15581
+ return prototype === Object.prototype || prototype === null;
15582
+ }
15583
+ function prototypeDefinesToJson(value) {
15584
+ let prototype = Object.getPrototypeOf(value);
15585
+ while (prototype !== null) {
15586
+ if (Object.getOwnPropertyDescriptor(prototype, "toJSON") !== undefined)
15587
+ return true;
15588
+ prototype = Object.getPrototypeOf(prototype);
15589
+ }
15590
+ return false;
15591
+ }
15592
+ function isStableJsonArray(value, ancestors, depth) {
15593
+ if (Object.getPrototypeOf(value) !== Array.prototype || prototypeDefinesToJson(value)) {
15594
+ return false;
15595
+ }
15596
+ const keys = Reflect.ownKeys(value);
15597
+ if (keys.length !== value.length + 1)
15598
+ return false;
15599
+ for (const key of keys) {
15600
+ if (key === "length")
15601
+ continue;
15602
+ if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key))
15603
+ return false;
15604
+ const index = Number(key);
15605
+ if (!Number.isSafeInteger(index) || index < 0 || index >= value.length)
15606
+ return false;
15607
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
15608
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
15609
+ return false;
15610
+ if (!isTodosAiJsonValueInternal(descriptor.value, ancestors, depth + 1))
15611
+ return false;
15612
+ }
15613
+ return true;
15614
+ }
15615
+ function isStableJsonObject(value, ancestors, depth) {
15616
+ if (!isJsonObject(value) || prototypeDefinesToJson(value))
15617
+ return false;
15618
+ for (const key of Reflect.ownKeys(value)) {
15619
+ if (typeof key !== "string")
15620
+ return false;
15621
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
15622
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
15623
+ return false;
15624
+ if (!isTodosAiJsonValueInternal(descriptor.value, ancestors, depth + 1))
15625
+ return false;
15626
+ }
15627
+ return true;
15628
+ }
15629
+ function isOneOf(value, values) {
15630
+ return typeof value === "string" && values.includes(value);
15631
+ }
15632
+ function selectedString(...values) {
15633
+ for (const value of values) {
15634
+ if (value === undefined || value === null)
15635
+ continue;
15636
+ const trimmed = value.trim();
15637
+ if (trimmed)
15638
+ return trimmed;
15639
+ }
15640
+ return null;
15641
+ }
15642
+ function parseEnum(value, values, field2, fallback) {
15643
+ if (value === null)
15644
+ return fallback;
15645
+ if (isOneOf(value, values))
15646
+ return value;
15647
+ throw new TodosAiContractError("invalid_configuration", `${field2} must be one of: ${values.join(", ")}`);
15648
+ }
15649
+ function parseBoundedInteger(value, field2, fallback, min, max) {
15650
+ if (value === undefined || value === null || value === "")
15651
+ return fallback;
15652
+ const parsed = typeof value === "number" ? value : Number(value);
15653
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
15654
+ throw new TodosAiContractError("invalid_configuration", `${field2} must be an integer between ${min} and ${max}`);
15655
+ }
15656
+ return parsed;
15657
+ }
15658
+ function normalizeApprovalRefs(values) {
15659
+ const refs = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
15660
+ if (refs.length > TODOS_AI_LIMITS.max_approval_refs) {
15661
+ throw new TodosAiContractError("invalid_configuration", `--approval may be repeated at most ${TODOS_AI_LIMITS.max_approval_refs} times`);
15662
+ }
15663
+ for (const ref of refs) {
15664
+ if (new TextEncoder().encode(ref).byteLength > TODOS_AI_LIMITS.max_approval_ref_bytes) {
15665
+ throw new TodosAiContractError("invalid_configuration", `--approval references may not exceed ${TODOS_AI_LIMITS.max_approval_ref_bytes} bytes`);
15666
+ }
15667
+ }
15668
+ return refs;
15669
+ }
15670
+ function resolveTodosAiCommandOptions(input) {
15671
+ const cli = input.cli ?? {};
15672
+ const config = input.config ?? {};
15673
+ const env = input.env ?? {};
15674
+ const format = parseEnum(selectedString(cli.format, env["TODOS_AI_FORMAT"], config.format), TODOS_AI_FORMATS, "format", TODOS_AI_DEFAULTS.format);
15675
+ const configuredWriteMode = parseEnum(selectedString(cli.writeMode, env["TODOS_AI_WRITE_MODE"], config.write_mode), TODOS_AI_WRITE_MODES, "write mode", TODOS_AI_DEFAULTS.write_mode);
15676
+ if (cli.dryRun && cli.writeMode === "execute") {
15677
+ throw new TodosAiContractError("invalid_configuration", "--dry-run cannot be combined with --write-mode execute");
15678
+ }
15679
+ const writeMode = cli.dryRun ? "plan" : configuredWriteMode;
15680
+ const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode, env["TODOS_AI_APPROVAL_MODE"], config.approval_mode);
15681
+ const defaultApproval = writeMode === "execute" ? input.interactive ? "prompt" : "required" : "deny";
15682
+ const approvalMode = cli.dryRun ? parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", "deny") : parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", defaultApproval);
15683
+ const approvalRefs = normalizeApprovalRefs(cli.approvalRefs);
15684
+ if (cli.dryRun && approvalMode !== "deny") {
15685
+ throw new TodosAiContractError("invalid_configuration", "--dry-run requires approval mode deny");
15686
+ }
15687
+ if (writeMode !== "execute" && approvalMode !== "deny") {
15688
+ throw new TodosAiContractError("invalid_configuration", "approval modes other than deny require --write-mode execute");
15689
+ }
15690
+ if (writeMode === "execute" && approvalMode === "deny") {
15691
+ throw new TodosAiContractError("invalid_configuration", "--write-mode execute requires approval mode required, prompt, or existing");
15692
+ }
15693
+ if (!input.interactive && approvalMode === "prompt") {
15694
+ throw new TodosAiContractError("invalid_configuration", "approval mode prompt is unavailable in non-interactive mode");
15695
+ }
15696
+ if (approvalMode === "existing" && approvalRefs.length === 0) {
15697
+ throw new TodosAiContractError("invalid_configuration", "approval mode existing requires at least one --approval reference");
15698
+ }
15699
+ if (approvalMode !== "existing" && approvalRefs.length > 0) {
15700
+ throw new TodosAiContractError("invalid_configuration", "--approval references require --approval-mode existing");
15701
+ }
15702
+ return {
15703
+ provider: selectedString(cli.provider, env["TODOS_AI_PROVIDER"], config.provider),
15704
+ model: selectedString(cli.model, env["TODOS_AI_MODEL"], config.model),
15705
+ profile: selectedString(cli.profile, env["TODOS_AI_PROFILE"], config.profile),
15706
+ format,
15707
+ max_steps: parseBoundedInteger(cli.maxSteps ?? env["TODOS_AI_MAX_STEPS"] ?? config.max_steps, "max steps", TODOS_AI_DEFAULTS.max_steps, TODOS_AI_LIMITS.min_steps, TODOS_AI_LIMITS.max_steps),
15708
+ timeout_ms: parseBoundedInteger(cli.timeoutMs ?? env["TODOS_AI_TIMEOUT_MS"] ?? config.timeout_ms, "timeout", TODOS_AI_DEFAULTS.timeout_ms, TODOS_AI_LIMITS.min_timeout_ms, TODOS_AI_LIMITS.max_timeout_ms),
15709
+ write_mode: writeMode,
15710
+ approval_mode: approvalMode,
15711
+ approval_refs: approvalRefs,
15712
+ dry_run: cli.dryRun === true,
15713
+ interactive: input.interactive
15714
+ };
15715
+ }
15716
+ function normalizeTodosAiPrompt(value) {
15717
+ const prompt = value.trim();
15718
+ if (new TextEncoder().encode(prompt).byteLength > TODOS_AI_LIMITS.max_prompt_bytes) {
15719
+ throw new TodosAiContractError("invalid_input", `prompt exceeds ${TODOS_AI_LIMITS.max_prompt_bytes} bytes`);
15720
+ }
15721
+ return prompt;
15722
+ }
15723
+ function parseTodosAiJson(value, field2) {
15724
+ if (new TextEncoder().encode(value).byteLength > TODOS_AI_LIMITS.max_json_bytes) {
15725
+ throw new TodosAiContractError("invalid_input", `${field2} exceeds ${TODOS_AI_LIMITS.max_json_bytes} bytes`);
15726
+ }
15727
+ let parsed;
15728
+ try {
15729
+ parsed = JSON.parse(value);
15730
+ } catch (error) {
15731
+ const detail = error instanceof Error ? error.message : String(error);
15732
+ throw new TodosAiContractError("invalid_input", `${field2} must be valid JSON: ${detail}`);
15733
+ }
15734
+ if (!isTodosAiJsonValue(parsed)) {
15735
+ throw new TodosAiContractError("invalid_input", `${field2} must contain only stable JSON values`);
15736
+ }
15737
+ return parsed;
15738
+ }
15739
+ function parseTodosAiOutputSchema(value) {
15740
+ const parsed = parseTodosAiJson(value, "output schema");
15741
+ if (!isRecord(parsed)) {
15742
+ throw new TodosAiContractError("invalid_input", "output schema must be a JSON object");
15743
+ }
15744
+ return parsed;
15745
+ }
15746
+ var SENSITIVE_VARIABLE_KEY = /(?:^|[_.-])(api[_-]?key|credential|password|secret|token)(?:$|[_.-])/i;
15747
+ function parseTodosAiVariables(values) {
15748
+ if (values.length > TODOS_AI_LIMITS.max_variable_count) {
15749
+ throw new TodosAiContractError("invalid_input", `--var may be repeated at most ${TODOS_AI_LIMITS.max_variable_count} times`);
15750
+ }
15751
+ const variables = Object.create(null);
15752
+ for (const entry of values) {
15753
+ const separator = entry.indexOf("=");
15754
+ const key = separator >= 0 ? entry.slice(0, separator).trim() : "";
15755
+ const value = separator >= 0 ? entry.slice(separator + 1) : "";
15756
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/.test(key)) {
15757
+ throw new TodosAiContractError("invalid_input", `invalid --var entry ${JSON.stringify(entry)}; expected non-secret key=value`);
15758
+ }
15759
+ if (SENSITIVE_VARIABLE_KEY.test(key)) {
15760
+ throw new TodosAiContractError("invalid_input", `--var ${key} is credential-shaped; provide credentials through the runtime's secret configuration`);
15761
+ }
15762
+ if (Object.hasOwn(variables, key)) {
15763
+ throw new TodosAiContractError("invalid_input", `duplicate --var key: ${key}`);
15764
+ }
15765
+ if (new TextEncoder().encode(value).byteLength > TODOS_AI_LIMITS.max_variable_value_bytes) {
15766
+ throw new TodosAiContractError("invalid_input", `--var ${key} exceeds ${TODOS_AI_LIMITS.max_variable_value_bytes} bytes`);
15767
+ }
15768
+ variables[key] = value;
15769
+ }
15770
+ return variables;
15771
+ }
15772
+ function isTodosAiJsonValue(value) {
15773
+ return isTodosAiJsonValueInternal(value, new Set, 0);
15774
+ }
15775
+ function isTodosAiUpdateTaskResult(value) {
15776
+ if (!isTodosAiJsonValue(value) || !isRecord(value) || !hasOnlyKeys(value, [
15777
+ "schema",
15778
+ "operation",
15779
+ "mode",
15780
+ "applied",
15781
+ "readback_verified",
15782
+ "source",
15783
+ "target",
15784
+ "changed_fields",
15785
+ "approval_ref",
15786
+ "payload_digest",
15787
+ "idempotency"
15788
+ ]) || value["schema"] !== TODOS_AI_UPDATE_TASK_RESULT_SCHEMA || value["operation"] !== "update_task" || value["mode"] !== "plan" && value["mode"] !== "execute" || value["source"] !== "sqlite" && value["source"] !== "http") {
15789
+ return false;
15790
+ }
15791
+ const changedFields = value["changed_fields"];
15792
+ const target = value["target"];
15793
+ const idempotency = value["idempotency"];
15794
+ const payloadDigest = value["payload_digest"];
15795
+ const approvalRef = value["approval_ref"];
15796
+ if (!Array.isArray(changedFields) || changedFields.length === 0 || changedFields.length > TODOS_AI_UPDATE_TASK_FIELDS.length || !changedFields.every((field2) => typeof field2 === "string" && TODOS_AI_UPDATE_TASK_FIELDS.includes(field2)) || new Set(changedFields).size !== changedFields.length || !TODOS_AI_UPDATE_TASK_FIELDS.filter((field2) => changedFields.includes(field2)).every((field2, index) => changedFields[index] === field2) || !isRecord(target) || !hasOnlyKeys(target, ["task_id", "expected_version", "result_version"]) || typeof target["task_id"] !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target["task_id"]) || !Number.isSafeInteger(target["expected_version"]) || target["expected_version"] < 0 || typeof payloadDigest !== "string" || !/^[0-9a-f]{64}$/.test(payloadDigest) || approvalRef !== `todos-ai:update_task:${payloadDigest}` || !isRecord(idempotency) || !hasOnlyKeys(idempotency, ["key", "scope", "replay"]) || !boundedUtf8String(idempotency["key"], TODOS_AI_UPDATE_TASK_LIMITS.max_idempotency_key_bytes) || new TextEncoder().encode(idempotency["key"]).byteLength < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(idempotency["key"]) || idempotency["scope"] !== "run" || typeof idempotency["replay"] !== "boolean" || new TextEncoder().encode(JSON.stringify(value)).byteLength > TODOS_AI_UPDATE_TASK_LIMITS.max_result_bytes) {
15797
+ return false;
15798
+ }
15799
+ if (value["mode"] === "plan") {
15800
+ return value["applied"] === false && value["readback_verified"] === false && target["result_version"] === null;
15801
+ }
15802
+ return value["applied"] === true && value["readback_verified"] === true && Number.isSafeInteger(target["result_version"]) && target["result_version"] === target["expected_version"] + 1;
15803
+ }
15804
+ function isTodosAiJsonValueInternal(value, ancestors, depth) {
15805
+ if (depth > 64)
15806
+ return false;
15807
+ if (value === null || typeof value === "string" || typeof value === "boolean")
15808
+ return true;
15809
+ if (typeof value === "number")
15810
+ return Number.isFinite(value);
15811
+ if (typeof value !== "object")
15812
+ return false;
15813
+ if (ancestors.has(value))
15814
+ return false;
15815
+ ancestors.add(value);
15816
+ try {
15817
+ return Array.isArray(value) ? isStableJsonArray(value, ancestors, depth) : isStableJsonObject(value, ancestors, depth);
15818
+ } catch {
15819
+ return false;
15820
+ } finally {
15821
+ ancestors.delete(value);
15822
+ }
15823
+ }
15824
+ function isUsage(value) {
15825
+ if (!isRecord(value))
15826
+ return false;
15827
+ return ["input_tokens", "output_tokens", "total_tokens"].every((key) => Number.isSafeInteger(value[key]) && value[key] >= 0);
15828
+ }
15829
+ function hasOnlyKeys(value, keys) {
15830
+ const allowed = new Set(keys);
15831
+ return Reflect.ownKeys(value).every((key) => typeof key === "string" && allowed.has(key));
15832
+ }
15833
+ function boundedUtf8String(value, maximum, allowEmpty = false) {
15834
+ return typeof value === "string" && (allowEmpty || value.length > 0) && new TextEncoder().encode(value).byteLength <= maximum;
15835
+ }
15836
+ function isPendingInput(value) {
15837
+ if (!isRecord(value) || !hasOnlyKeys(value, ["prompt", "fields"]))
15838
+ return false;
15839
+ if (!boundedUtf8String(value["prompt"], TODOS_AI_LIMITS.max_pending_input_prompt_bytes)) {
15840
+ return false;
15841
+ }
15842
+ if (!Array.isArray(value["fields"]) || value["fields"].length === 0 || value["fields"].length > TODOS_AI_LIMITS.max_pending_input_fields) {
15843
+ return false;
15844
+ }
15845
+ const fields = value["fields"];
15846
+ const unique = new Set;
15847
+ for (const field2 of fields) {
15848
+ if (!boundedUtf8String(field2, TODOS_AI_LIMITS.max_pending_input_field_bytes) || !/^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/.test(field2) || unique.has(field2)) {
15849
+ return false;
15850
+ }
15851
+ unique.add(field2);
15852
+ }
15853
+ return true;
15854
+ }
15855
+ function isPendingApproval(value) {
15856
+ if (!isRecord(value) || !hasOnlyKeys(value, ["id", "summary", "operations"])) {
15857
+ return false;
15858
+ }
15859
+ if (!boundedUtf8String(value["id"], TODOS_AI_LIMITS.max_pending_approval_id_bytes) || !boundedUtf8String(value["summary"], TODOS_AI_LIMITS.max_pending_approval_summary_bytes) || !Array.isArray(value["operations"]) || value["operations"].length === 0 || value["operations"].length > TODOS_AI_LIMITS.max_pending_approval_operations || !value["operations"].every(isRecord) || !value["operations"].every(isTodosAiJsonValue)) {
15860
+ return false;
15861
+ }
15862
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength <= TODOS_AI_LIMITS.max_pending_approval_bytes;
15863
+ }
15864
+ function isAiError(value) {
15865
+ if (!isRecord(value))
15866
+ return false;
15867
+ const code = value["code"];
15868
+ const validCode = [
15869
+ "invalid_input",
15870
+ "invalid_configuration",
15871
+ "runtime_unavailable",
15872
+ "runtime_incompatible",
15873
+ "runtime_invalid_result",
15874
+ "needs_input",
15875
+ "needs_approval",
15876
+ "timeout",
15877
+ "interrupted",
15878
+ "provider_error",
15879
+ "tool_error",
15880
+ "schema_error",
15881
+ "internal_error"
15882
+ ];
15883
+ return typeof code === "string" && validCode.includes(code) && typeof value["message"] === "string" && typeof value["retryable"] === "boolean" && (value["details"] === null || isRecord(value["details"]) && isTodosAiJsonValue(value["details"]));
15884
+ }
15885
+ function isTodosAiRunResult(value) {
15886
+ if (!isRecord(value) || !isTodosAiJsonValue(value))
15887
+ return false;
15888
+ const structurallyValid = value["schema_version"] === TODOS_AI_SCHEMA_VERSION && typeof value["run_id"] === "string" && Boolean(value["run_id"]) && isOneOf(value["status"], TODOS_AI_RUN_STATUSES) && (value["answer"] === null || typeof value["answer"] === "string") && isTodosAiJsonValue(value["data"]) && Number.isSafeInteger(value["steps"]) && value["steps"] >= 0 && (value["usage"] === null || isUsage(value["usage"])) && (value["pending_input"] === null || isPendingInput(value["pending_input"])) && (value["pending_approval"] === null || isPendingApproval(value["pending_approval"])) && (value["error"] === null || isAiError(value["error"]));
15889
+ if (!structurallyValid)
15890
+ return false;
15891
+ switch (value["status"]) {
15892
+ case "answered":
15893
+ return typeof value["answer"] === "string" && value["pending_input"] === null && value["pending_approval"] === null && value["error"] === null;
15894
+ case "completed":
15895
+ return isTodosAiUpdateTaskResult(value["data"]) && value["data"]["mode"] === "execute" && value["data"]["applied"] === true && value["data"]["readback_verified"] === true && value["pending_input"] === null && value["pending_approval"] === null && value["error"] === null;
15896
+ case "needs_input":
15897
+ return value["pending_input"] !== null && value["pending_approval"] === null && value["error"] === null;
15898
+ case "needs_approval":
15899
+ return value["pending_input"] === null && value["pending_approval"] !== null && value["error"] === null;
15900
+ case "failed":
15901
+ return value["pending_input"] === null && value["pending_approval"] === null && value["error"] !== null;
15902
+ }
15903
+ return false;
15904
+ }
15905
+ function assertTodosAiRunResult(value) {
15906
+ if (!isTodosAiRunResult(value)) {
15907
+ throw new TodosAiContractError("runtime_invalid_result", "optional AI runtime returned a result that does not satisfy the Todos AI protocol", TODOS_AI_EXIT_CODES.failed);
15908
+ }
15909
+ return value;
15910
+ }
15911
+ function isTodosAiRuntimeEvent(value) {
15912
+ if (!isRecord(value) || !isTodosAiJsonValue(value))
15913
+ return false;
15914
+ return value["schema_version"] === TODOS_AI_SCHEMA_VERSION && typeof value["run_id"] === "string" && Boolean(value["run_id"]) && Number.isSafeInteger(value["sequence"]) && value["sequence"] >= 0 && isOneOf(value["type"], TODOS_AI_RUNTIME_EVENT_TYPES) && typeof value["timestamp"] === "string" && isRecord(value["data"]) && isTodosAiJsonValue(value["data"]);
15915
+ }
15916
+ function assertTodosAiRuntimeModule(value) {
15917
+ if (!isRecord(value) || value["TODOS_AI_RUNTIME_PROTOCOL_VERSION"] !== TODOS_AI_RUNTIME_PROTOCOL_VERSION || typeof value["createTodosAiRuntime"] !== "function") {
15918
+ throw new TodosAiContractError("runtime_incompatible", `optional AI runtime must implement protocol ${TODOS_AI_RUNTIME_PROTOCOL_VERSION}`, TODOS_AI_EXIT_CODES.runtime_unavailable);
15919
+ }
15920
+ return value;
15921
+ }
15922
+ function assertTodosAiRuntime(value) {
15923
+ if (!isRecord(value) || typeof value["run"] !== "function") {
15924
+ throw new TodosAiContractError("runtime_incompatible", `optional AI runtime must implement protocol ${TODOS_AI_RUNTIME_PROTOCOL_VERSION}`, TODOS_AI_EXIT_CODES.runtime_unavailable);
15925
+ }
15926
+ return value;
15927
+ }
15928
+ var defaultTodosAiRuntimeImporter = async (specifier) => import(specifier);
15929
+ async function loadTodosAiRuntime(context, importer = defaultTodosAiRuntimeImporter) {
15930
+ let imported;
15931
+ try {
15932
+ imported = await importer(TODOS_AI_RUNTIME_SPECIFIER);
15933
+ } catch (cause) {
15934
+ throw new TodosAiContractError("runtime_unavailable", `optional AI runtime is unavailable; install a compatible ${TODOS_AI_RUNTIME_SPECIFIER}`, TODOS_AI_EXIT_CODES.runtime_unavailable, { cause });
15935
+ }
15936
+ const runtimeModule = assertTodosAiRuntimeModule(imported);
15937
+ return assertTodosAiRuntime(await runtimeModule.createTodosAiRuntime(context));
15938
+ }
15939
+ function createTodosAiFailureResult(runId, code, message, retryable = false, details = null) {
15940
+ return {
15941
+ schema_version: TODOS_AI_SCHEMA_VERSION,
15942
+ run_id: runId,
15943
+ status: "failed",
15944
+ answer: null,
15945
+ data: null,
15946
+ steps: 0,
15947
+ usage: null,
15948
+ pending_input: null,
15949
+ pending_approval: null,
15950
+ error: { code, message, retryable, details }
15951
+ };
15952
+ }
15953
+ function createTodosAiNeedsInputResult(runId, message) {
15954
+ return {
15955
+ schema_version: TODOS_AI_SCHEMA_VERSION,
15956
+ run_id: runId,
15957
+ status: "needs_input",
15958
+ answer: null,
15959
+ data: null,
15960
+ steps: 0,
15961
+ usage: null,
15962
+ pending_input: { prompt: message, fields: ["prompt"] },
15963
+ pending_approval: null,
15964
+ error: null
15965
+ };
15966
+ }
15967
+ function createTodosAiNeedsApprovalResult(runId, pendingApproval) {
15968
+ const signal = new TodosAiNeedsApprovalSignal(pendingApproval);
15969
+ return {
15970
+ schema_version: TODOS_AI_SCHEMA_VERSION,
15971
+ run_id: runId,
15972
+ status: "needs_approval",
15973
+ answer: null,
15974
+ data: null,
15975
+ steps: 0,
15976
+ usage: null,
15977
+ pending_input: null,
15978
+ pending_approval: signal.pending_approval,
15979
+ error: null
15980
+ };
15981
+ }
15982
+ function todosAiExitCodeForResult(result) {
15983
+ if (result.status === "answered" || result.status === "completed")
15984
+ return TODOS_AI_EXIT_CODES.success;
15985
+ if (result.status === "needs_input")
15986
+ return TODOS_AI_EXIT_CODES.needs_input;
15987
+ if (result.status === "needs_approval")
15988
+ return TODOS_AI_EXIT_CODES.needs_approval;
15989
+ switch (result.error?.code) {
15990
+ case "invalid_input":
15991
+ case "invalid_configuration":
15992
+ return TODOS_AI_EXIT_CODES.usage;
15993
+ case "runtime_unavailable":
15994
+ case "runtime_incompatible":
15995
+ return TODOS_AI_EXIT_CODES.runtime_unavailable;
15996
+ case "timeout":
15997
+ return TODOS_AI_EXIT_CODES.timeout;
15998
+ case "interrupted":
15999
+ return TODOS_AI_EXIT_CODES.interrupted;
16000
+ default:
16001
+ return TODOS_AI_EXIT_CODES.failed;
16002
+ }
16003
+ }
15207
16004
  // src/lib/onboarding-fixtures.ts
15208
16005
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
15209
16006
  import { join as join6 } from "path";
@@ -16159,7 +16956,7 @@ function importOnboardingFixture(options = {}) {
16159
16956
  // src/lib/local-backups.ts
16160
16957
  init_database();
16161
16958
  import { createHash as createHash4 } from "crypto";
16162
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
16959
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
16163
16960
  import { dirname as dirname5, resolve as resolve8 } from "path";
16164
16961
  import { mkdirSync as mkdirSync6 } from "fs";
16165
16962
  var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
@@ -16274,7 +17071,7 @@ function writeLocalBackupFile(backup, outputPath) {
16274
17071
  return path;
16275
17072
  }
16276
17073
  function readLocalBackupFile(path) {
16277
- return JSON.parse(readFileSync4(resolve8(path), "utf-8"));
17074
+ return JSON.parse(readFileSync5(resolve8(path), "utf-8"));
16278
17075
  }
16279
17076
  function verifyLocalBackup(value, options = {}, db) {
16280
17077
  const verifiedAt = options.verified_at ?? now();
@@ -20221,7 +21018,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
20221
21018
  // src/lib/release-compatibility.ts
20222
21019
  init_migrations();
20223
21020
  init_schema();
20224
- import { readFileSync as readFileSync5 } from "fs";
21021
+ import { readFileSync as readFileSync6 } from "fs";
20225
21022
  import { join as join8, resolve as resolve9 } from "path";
20226
21023
  import { Database as Database2 } from "bun:sqlite";
20227
21024
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
@@ -20268,7 +21065,7 @@ function warn(id, message, details) {
20268
21065
  return { id, status: "warning", message, details };
20269
21066
  }
20270
21067
  function readPackageJson(root) {
20271
- return JSON.parse(readFileSync5(join8(root, "package.json"), "utf8"));
21068
+ return JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
20272
21069
  }
20273
21070
  function sortedKeys(value) {
20274
21071
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -23894,11 +24691,13 @@ export {
23894
24691
  upsertEncryptionProfile,
23895
24692
  updateRoadmap,
23896
24693
  updateMilestone,
24694
+ todosAiExitCodeForResult,
23897
24695
  summarizeRoadmap,
23898
24696
  summarizeMilestone,
23899
24697
  sealLocalAuditLedger,
23900
24698
  returnReviewItem,
23901
24699
  restoreLocalBackup,
24700
+ resolveTodosAiCommandOptions,
23902
24701
  requestReviewQueue,
23903
24702
  reopenReviewItem,
23904
24703
  renderRoadmapMarkdown,
@@ -23912,7 +24711,12 @@ export {
23912
24711
  readTesterIssueReportsPayload,
23913
24712
  readLocalBackupFile,
23914
24713
  pollLocalSnapshots,
24714
+ parseTodosAiVariables,
24715
+ parseTodosAiOutputSchema,
24716
+ parseTodosAiJson,
24717
+ normalizeTodosAiPrompt,
23915
24718
  normalizeTesterIssueReport,
24719
+ loadTodosAiRuntime,
23916
24720
  listSdkIntegrationExamples,
23917
24721
  listRoadmaps,
23918
24722
  listReviewRoutingRules,
@@ -23924,6 +24728,10 @@ export {
23924
24728
  listLocalReportTypes,
23925
24729
  listLocalAuditLedgerCheckpoints,
23926
24730
  listEncryptionProfiles,
24731
+ isTodosAiUpdateTaskResult,
24732
+ isTodosAiRuntimeEvent,
24733
+ isTodosAiRunResult,
24734
+ isTodosAiJsonValue,
23927
24735
  isEncryptedValue,
23928
24736
  isEncryptedBridgeBundle,
23929
24737
  importRoadmapBundle,
@@ -23947,6 +24755,9 @@ export {
23947
24755
  decryptValue,
23948
24756
  decryptString,
23949
24757
  decryptBridgeBundle,
24758
+ createTodosAiNeedsInputResult,
24759
+ createTodosAiNeedsApprovalResult,
24760
+ createTodosAiFailureResult,
23950
24761
  createSdkIntegrationFixturePack,
23951
24762
  createRoadmap,
23952
24763
  createReleaseCompatibilityReport,
@@ -23961,7 +24772,12 @@ export {
23961
24772
  claimReviewItem,
23962
24773
  checkLocalNotifications,
23963
24774
  checkLocalIntegrity,
24775
+ assertTodosAiRuntimeModule,
24776
+ assertTodosAiRunResult,
23964
24777
  approveReviewItem,
24778
+ TodosAiNeedsInputSignal,
24779
+ TodosAiNeedsApprovalSignal,
24780
+ TodosAiContractError,
23965
24781
  TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
23966
24782
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT,
23967
24783
  TODOS_ONBOARDING_FIXTURE_SOURCE,
@@ -23981,6 +24797,22 @@ export {
23981
24797
  TODOS_ENCRYPTED_BRIDGE_KIND,
23982
24798
  TODOS_CONTRACTS,
23983
24799
  TODOS_API_ROUTES,
24800
+ TODOS_AI_WRITE_MODES,
24801
+ TODOS_AI_UPDATE_TASK_RESULT_SCHEMA,
24802
+ TODOS_AI_UPDATE_TASK_LIMITS,
24803
+ TODOS_AI_UPDATE_TASK_FIELDS,
24804
+ TODOS_AI_TOOL_EFFECTS,
24805
+ TODOS_AI_SCHEMA_VERSION,
24806
+ TODOS_AI_RUN_STATUSES,
24807
+ TODOS_AI_RUNTIME_SPECIFIER,
24808
+ TODOS_AI_RUNTIME_PROTOCOL_VERSION,
24809
+ TODOS_AI_RUNTIME_EVENT_TYPES,
24810
+ TODOS_AI_PROTOCOL,
24811
+ TODOS_AI_LIMITS,
24812
+ TODOS_AI_FORMATS,
24813
+ TODOS_AI_EXIT_CODES,
24814
+ TODOS_AI_DEFAULTS,
24815
+ TODOS_AI_APPROVAL_MODES,
23984
24816
  TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
23985
24817
  TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
23986
24818
  TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,