@hasna/todos 0.11.86 → 0.11.87

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 (39) hide show
  1. package/dist/cli/cloud-router.d.ts +33 -2
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/task-commands.d.ts +2 -0
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/index.js +934 -412
  7. package/dist/contracts.js +1 -1
  8. package/dist/db/comments.d.ts.map +1 -1
  9. package/dist/index.js +167 -4
  10. package/dist/mcp/index.js +407 -38
  11. package/dist/registry.js +1 -1
  12. package/dist/release-provenance.json +3 -3
  13. package/dist/sdk/index.d.ts +1 -1
  14. package/dist/sdk/index.d.ts.map +1 -1
  15. package/dist/sdk/index.js +21 -0
  16. package/dist/sdk/v1.generated.d.ts +54 -0
  17. package/dist/sdk/v1.generated.d.ts.map +1 -1
  18. package/dist/server/cloud.d.ts +13 -0
  19. package/dist/server/cloud.d.ts.map +1 -1
  20. package/dist/server/index.js +824 -383
  21. package/dist/server/openapi.d.ts +171 -0
  22. package/dist/server/openapi.d.ts.map +1 -1
  23. package/dist/server/v1.d.ts +7 -1
  24. package/dist/server/v1.d.ts.map +1 -1
  25. package/dist/storage/comment-redaction-backfill.d.ts +32 -0
  26. package/dist/storage/comment-redaction-backfill.d.ts.map +1 -0
  27. package/dist/storage/index.d.ts +4 -2
  28. package/dist/storage/index.d.ts.map +1 -1
  29. package/dist/storage/interfaces.d.ts +16 -0
  30. package/dist/storage/interfaces.d.ts.map +1 -1
  31. package/dist/storage/local-sqlite.d.ts.map +1 -1
  32. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  33. package/dist/storage/postgres-sync.d.ts +6 -0
  34. package/dist/storage/postgres-sync.d.ts.map +1 -1
  35. package/dist/storage.d.ts +3 -3
  36. package/dist/storage.d.ts.map +1 -1
  37. package/dist/storage.js +171 -4
  38. package/package.json +2 -1
  39. package/vendor/hasna-contracts-0.5.1.tgz +0 -0
package/dist/cli/index.js CHANGED
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1011
1011
  this._exitCallback = (err) => {
1012
1012
  if (err.code !== "commander.executeSubCommandAsync") {
1013
1013
  throw err;
1014
- } else {}
1014
+ }
1015
1015
  };
1016
1016
  }
1017
1017
  return this;
@@ -8478,6 +8478,310 @@ var init_storage = __esm(() => {
8478
8478
  IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
8479
8479
  });
8480
8480
 
8481
+ // src/lib/sync-utils.ts
8482
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
8483
+ import { join as join2 } from "path";
8484
+ function getHomeDir() {
8485
+ return process.env["HOME"] || process.env["USERPROFILE"] || "~";
8486
+ }
8487
+ function getTodosGlobalDir() {
8488
+ return join2(getHomeDir(), ".hasna", "todos");
8489
+ }
8490
+ function ensureDir(dir) {
8491
+ if (!existsSync2(dir))
8492
+ mkdirSync(dir, { recursive: true });
8493
+ }
8494
+ function listJsonFiles(dir) {
8495
+ if (!existsSync2(dir))
8496
+ return [];
8497
+ return readdirSync(dir).filter((f) => f.endsWith(".json"));
8498
+ }
8499
+ function readJsonFile(path) {
8500
+ try {
8501
+ return JSON.parse(readFileSync2(path, "utf-8"));
8502
+ } catch {
8503
+ return null;
8504
+ }
8505
+ }
8506
+ function writeJsonFile(path, data) {
8507
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
8508
+ `);
8509
+ }
8510
+ function readHighWaterMark(dir) {
8511
+ const path = join2(dir, ".highwatermark");
8512
+ if (!existsSync2(path))
8513
+ return 1;
8514
+ const val = parseInt(readFileSync2(path, "utf-8").trim(), 10);
8515
+ return isNaN(val) ? 1 : val;
8516
+ }
8517
+ function writeHighWaterMark(dir, value) {
8518
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
8519
+ }
8520
+ function getFileMtimeMs(path) {
8521
+ try {
8522
+ return statSync(path).mtimeMs;
8523
+ } catch {
8524
+ return null;
8525
+ }
8526
+ }
8527
+ function parseTimestamp(value) {
8528
+ if (typeof value !== "string")
8529
+ return null;
8530
+ const parsed = Date.parse(value);
8531
+ return Number.isNaN(parsed) ? null : parsed;
8532
+ }
8533
+ function appendSyncConflict(metadata, conflict, limit = 5) {
8534
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
8535
+ const next = [conflict, ...current].slice(0, limit);
8536
+ return { ...metadata, sync_conflicts: next };
8537
+ }
8538
+ var HOME;
8539
+ var init_sync_utils = __esm(() => {
8540
+ HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
8541
+ });
8542
+
8543
+ // src/lib/config.ts
8544
+ var exports_config = {};
8545
+ __export(exports_config, {
8546
+ updateConfig: () => updateConfig,
8547
+ saveConfig: () => saveConfig,
8548
+ resetConfig: () => resetConfig,
8549
+ normalizeApiUrl: () => normalizeApiUrl,
8550
+ loadConfig: () => loadConfig,
8551
+ getTaskPrefixConfig: () => getTaskPrefixConfig,
8552
+ getSyncAgentsFromConfig: () => getSyncAgentsFromConfig,
8553
+ getLocalApiConfig: () => getLocalApiConfig,
8554
+ getConfigPath: () => getConfigPath,
8555
+ getCompletionGuardConfig: () => getCompletionGuardConfig,
8556
+ getAgentTasksDir: () => getAgentTasksDir,
8557
+ getAgentTaskListId: () => getAgentTaskListId,
8558
+ getAgentPoolForProject: () => getAgentPoolForProject
8559
+ });
8560
+ import { existsSync as existsSync3 } from "fs";
8561
+ import { dirname as dirname2, join as join3 } from "path";
8562
+ function getConfigPath() {
8563
+ return join3(getTodosGlobalDir(), "config.json");
8564
+ }
8565
+ function resetConfig() {
8566
+ cached = null;
8567
+ }
8568
+ function normalizeAgent(agent) {
8569
+ return agent.trim().toLowerCase();
8570
+ }
8571
+ function loadConfig() {
8572
+ if (cached)
8573
+ return cached;
8574
+ if (!existsSync3(getConfigPath())) {
8575
+ cached = {};
8576
+ return cached;
8577
+ }
8578
+ const config = readJsonFile(getConfigPath()) || {};
8579
+ if (typeof config.sync_agents === "string") {
8580
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
8581
+ }
8582
+ cached = config;
8583
+ return cached;
8584
+ }
8585
+ function saveConfig(config) {
8586
+ const configPath = getConfigPath();
8587
+ ensureDir(dirname2(configPath));
8588
+ writeJsonFile(configPath, config);
8589
+ cached = config;
8590
+ return config;
8591
+ }
8592
+ function updateConfig(patch) {
8593
+ return saveConfig({ ...loadConfig(), ...patch });
8594
+ }
8595
+ function normalizeApiUrl(value) {
8596
+ const trimmed = value?.trim();
8597
+ if (!trimmed)
8598
+ return null;
8599
+ return trimmed.replace(/\/+$/, "");
8600
+ }
8601
+ function getLocalApiConfig(env = process.env) {
8602
+ const config = loadConfig();
8603
+ const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
8604
+ const configApiUrl = normalizeApiUrl(config.apiUrl);
8605
+ const apiUrl = envApiUrl ?? configApiUrl;
8606
+ const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
8607
+ return {
8608
+ apiUrl,
8609
+ apiKey,
8610
+ source: {
8611
+ apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
8612
+ apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
8613
+ }
8614
+ };
8615
+ }
8616
+ function getSyncAgentsFromConfig() {
8617
+ const config = loadConfig();
8618
+ const agents = config.sync_agents;
8619
+ if (Array.isArray(agents) && agents.length > 0)
8620
+ return agents.map(normalizeAgent);
8621
+ return null;
8622
+ }
8623
+ function getAgentTaskListId(agent) {
8624
+ const config = loadConfig();
8625
+ const key = normalizeAgent(agent);
8626
+ return config.agents?.[key]?.task_list_id || config.task_list_id || null;
8627
+ }
8628
+ function getAgentTasksDir(agent) {
8629
+ const config = loadConfig();
8630
+ const key = normalizeAgent(agent);
8631
+ return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
8632
+ }
8633
+ function getTaskPrefixConfig() {
8634
+ const config = loadConfig();
8635
+ return config.task_prefix || null;
8636
+ }
8637
+ function getAgentPoolForProject(workingDir) {
8638
+ const config = loadConfig();
8639
+ if (workingDir && config.project_pools) {
8640
+ let bestKey = null;
8641
+ let bestLen = 0;
8642
+ for (const key of Object.keys(config.project_pools)) {
8643
+ if (workingDir.startsWith(key) && key.length > bestLen) {
8644
+ bestKey = key;
8645
+ bestLen = key.length;
8646
+ }
8647
+ }
8648
+ if (bestKey && config.project_pools[bestKey]) {
8649
+ return config.project_pools[bestKey];
8650
+ }
8651
+ }
8652
+ return config.agent_pool || null;
8653
+ }
8654
+ function getCompletionGuardConfig(projectPath) {
8655
+ const config = loadConfig();
8656
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
8657
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
8658
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
8659
+ }
8660
+ return global;
8661
+ }
8662
+ var cached = null, GUARD_DEFAULTS;
8663
+ var init_config = __esm(() => {
8664
+ init_sync_utils();
8665
+ GUARD_DEFAULTS = {
8666
+ enabled: false,
8667
+ min_work_seconds: 30,
8668
+ max_completions_per_window: 5,
8669
+ window_minutes: 10,
8670
+ cooldown_seconds: 60
8671
+ };
8672
+ });
8673
+
8674
+ // src/lib/redaction.ts
8675
+ var exports_redaction = {};
8676
+ __export(exports_redaction, {
8677
+ upsertSecretSafetyConfig: () => upsertSecretSafetyConfig,
8678
+ redactValue: () => redactValue,
8679
+ redactEvidenceText: () => redactEvidenceText,
8680
+ listSecretFindings: () => listSecretFindings,
8681
+ hasSecretFindings: () => hasSecretFindings,
8682
+ getSecretSafetyConfig: () => getSecretSafetyConfig
8683
+ });
8684
+ function unique(values) {
8685
+ return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
8686
+ }
8687
+ function cloneRegex(regex) {
8688
+ return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
8689
+ }
8690
+ function customPatterns() {
8691
+ return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
8692
+ try {
8693
+ return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
8694
+ } catch {
8695
+ return [];
8696
+ }
8697
+ });
8698
+ }
8699
+ function secretPatterns() {
8700
+ return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
8701
+ }
8702
+ function isSecretKey(key) {
8703
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
8704
+ return false;
8705
+ if (DEFAULT_SECRET_KEY_PATTERN.test(key))
8706
+ return true;
8707
+ return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
8708
+ }
8709
+ function redactEvidenceText(value) {
8710
+ let redacted = value;
8711
+ for (const pattern of secretPatterns()) {
8712
+ const regex = cloneRegex(pattern.regex);
8713
+ const replacement = pattern.replacement ?? "[REDACTED]";
8714
+ redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
8715
+ }
8716
+ return redacted;
8717
+ }
8718
+ function redactValue(value) {
8719
+ if (typeof value === "string")
8720
+ return redactEvidenceText(value);
8721
+ if (Array.isArray(value))
8722
+ return value.map(redactValue);
8723
+ if (value && typeof value === "object") {
8724
+ const redacted = {};
8725
+ for (const [key, child] of Object.entries(value)) {
8726
+ if (isSecretKey(key)) {
8727
+ redacted[key] = "[REDACTED]";
8728
+ } else {
8729
+ redacted[key] = redactValue(child);
8730
+ }
8731
+ }
8732
+ return redacted;
8733
+ }
8734
+ return value;
8735
+ }
8736
+ function listSecretFindings(value) {
8737
+ const findings = [];
8738
+ for (const pattern of secretPatterns()) {
8739
+ const matches = value.match(cloneRegex(pattern.regex));
8740
+ if (matches?.length)
8741
+ findings.push({ pattern: pattern.name, count: matches.length });
8742
+ }
8743
+ return findings;
8744
+ }
8745
+ function hasSecretFindings(value) {
8746
+ return listSecretFindings(value).length > 0;
8747
+ }
8748
+ function getSecretSafetyConfig() {
8749
+ return {
8750
+ redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
8751
+ redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
8752
+ };
8753
+ }
8754
+ function upsertSecretSafetyConfig(input) {
8755
+ const config = loadConfig();
8756
+ const next = {
8757
+ redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
8758
+ redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
8759
+ };
8760
+ saveConfig({ ...config, secret_safety: next });
8761
+ return next;
8762
+ }
8763
+ var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
8764
+ var init_redaction = __esm(() => {
8765
+ init_config();
8766
+ DEFAULT_SECRET_PATTERNS = [
8767
+ { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
8768
+ { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
8769
+ { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
8770
+ { 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]" },
8771
+ { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
8772
+ ];
8773
+ DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
8774
+ NON_SECRET_USAGE_KEYS = new Set([
8775
+ "tokens",
8776
+ "total_tokens",
8777
+ "token_count",
8778
+ "input_tokens",
8779
+ "output_tokens",
8780
+ "prompt_tokens",
8781
+ "completion_tokens"
8782
+ ]);
8783
+ });
8784
+
8481
8785
  // src/cli/cloud-router.ts
8482
8786
  function getTodosCloudClient(env = process.env) {
8483
8787
  if (_cache !== undefined)
@@ -8503,14 +8807,18 @@ function unwrapTask(raw) {
8503
8807
  }
8504
8808
  function toListQuery(filter = {}) {
8505
8809
  const query = {};
8506
- if (typeof filter.status === "string")
8507
- query["status"] = filter.status;
8508
- if (typeof filter.priority === "string")
8509
- query["priority"] = filter.priority;
8810
+ if (filter.status)
8811
+ query["status"] = Array.isArray(filter.status) ? filter.status.join(",") : filter.status;
8812
+ if (filter.priority)
8813
+ query["priority"] = Array.isArray(filter.priority) ? filter.priority.join(",") : filter.priority;
8510
8814
  if (filter.project_id)
8511
8815
  query["project_id"] = filter.project_id;
8816
+ if (filter.parent_id !== undefined)
8817
+ query["parent_id"] = filter.parent_id ?? "";
8512
8818
  if (filter.plan_id)
8513
8819
  query["plan_id"] = filter.plan_id;
8820
+ if (filter.task_list_id)
8821
+ query["task_list_id"] = filter.task_list_id;
8514
8822
  if (filter.assigned_to)
8515
8823
  query["assigned_to"] = filter.assigned_to;
8516
8824
  if (filter.agent_id)
@@ -8566,10 +8874,82 @@ async function cloudListPlans(client, projectId) {
8566
8874
  }
8567
8875
  async function cloudAddComment(client, taskId, input) {
8568
8876
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
8569
- if (raw && typeof raw === "object" && "comment" in raw) {
8570
- return raw.comment;
8877
+ const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
8878
+ if (!isTaskComment(comment))
8879
+ throw new Error("Invalid cloud comment response");
8880
+ return redactComment(comment);
8881
+ }
8882
+ async function cloudListComments(client, taskId, options = {}) {
8883
+ const limit = options.limit ?? 100;
8884
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
8885
+ throw new Error("Cloud comment limit must be an integer between 1 and 500");
8571
8886
  }
8572
- return raw;
8887
+ if (options.cursor !== undefined && (typeof options.cursor !== "string" || !options.cursor || options.cursor.length > 1024)) {
8888
+ throw new Error("Cloud comment cursor must be a non-empty string");
8889
+ }
8890
+ let raw;
8891
+ try {
8892
+ raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/comments`, {
8893
+ query: { limit, ...options.cursor ? { cursor: options.cursor } : {} }
8894
+ });
8895
+ } catch (error) {
8896
+ const status = error && typeof error === "object" ? error.status : undefined;
8897
+ if (status === 404 || status === 405) {
8898
+ throw new Error("Cloud task comments require a compatible @hasna/todos server; deploy the server endpoint before this CLI.", { cause: error });
8899
+ }
8900
+ throw error;
8901
+ }
8902
+ const envelope = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
8903
+ const candidate = Array.isArray(raw) ? raw : envelope?.comments;
8904
+ if (!Array.isArray(candidate) || !candidate.every(isTaskComment)) {
8905
+ throw new Error("Invalid cloud comments response");
8906
+ }
8907
+ if (envelope?.count !== undefined && (!Number.isSafeInteger(envelope.count) || envelope.count < 0 || envelope.count !== candidate.length)) {
8908
+ throw new Error("Invalid cloud comments response count");
8909
+ }
8910
+ const hasHasMore = envelope ? Object.prototype.hasOwnProperty.call(envelope, "has_more") : false;
8911
+ const hasNextCursor = envelope ? Object.prototype.hasOwnProperty.call(envelope, "next_cursor") : false;
8912
+ if (hasHasMore !== hasNextCursor)
8913
+ throw new Error("Invalid cloud comments pagination response");
8914
+ const paginationSupported = hasHasMore && hasNextCursor;
8915
+ if (!paginationSupported) {
8916
+ const comments = candidate.slice(-limit).map(redactComment);
8917
+ return {
8918
+ comments,
8919
+ count: comments.length,
8920
+ has_more: candidate.length > limit,
8921
+ next_cursor: null,
8922
+ limit,
8923
+ pagination_supported: false
8924
+ };
8925
+ }
8926
+ if (candidate.length > limit)
8927
+ throw new Error("Invalid cloud comments response: page exceeds requested limit");
8928
+ const hasMore = envelope.has_more;
8929
+ const nextCursor = envelope.next_cursor;
8930
+ if (typeof hasMore !== "boolean" || nextCursor !== null && (typeof nextCursor !== "string" || !nextCursor)) {
8931
+ throw new Error("Invalid cloud comments pagination response");
8932
+ }
8933
+ if (hasMore && nextCursor === null || !hasMore && nextCursor !== null) {
8934
+ throw new Error("Invalid cloud comments pagination response");
8935
+ }
8936
+ return {
8937
+ comments: candidate.map(redactComment),
8938
+ count: candidate.length,
8939
+ has_more: hasMore,
8940
+ next_cursor: nextCursor,
8941
+ limit,
8942
+ pagination_supported: true
8943
+ };
8944
+ }
8945
+ function isTaskComment(value) {
8946
+ if (!value || typeof value !== "object" || Array.isArray(value))
8947
+ return false;
8948
+ const comment = value;
8949
+ return typeof comment["id"] === "string" && typeof comment["task_id"] === "string" && (comment["agent_id"] === null || typeof comment["agent_id"] === "string") && (comment["session_id"] === null || typeof comment["session_id"] === "string") && typeof comment["content"] === "string" && (comment["type"] === "comment" || comment["type"] === "progress" || comment["type"] === "note") && (comment["progress_pct"] === null || typeof comment["progress_pct"] === "number") && typeof comment["created_at"] === "string";
8950
+ }
8951
+ function redactComment(comment) {
8952
+ return { ...comment, content: redactEvidenceText(comment.content) };
8573
8953
  }
8574
8954
  async function cloudTaskHistory(client, taskId) {
8575
8955
  const raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`);
@@ -8657,8 +9037,8 @@ async function cloudLockTask(client, id, agentId) {
8657
9037
  }
8658
9038
  return raw ?? { success: true };
8659
9039
  }
8660
- async function cloudUnlockTask(client, id, agentId) {
8661
- const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/unlock`, agentId ? { agent_id: agentId } : {});
9040
+ async function cloudUnlockTask(client, id, agentId, force = false) {
9041
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/unlock`, { ...agentId ? { agent_id: agentId } : {}, ...force ? { force: true } : {} });
8662
9042
  if (raw && typeof raw === "object" && "success" in raw) {
8663
9043
  return Boolean(raw.success);
8664
9044
  }
@@ -8787,6 +9167,25 @@ async function cloudListTaskLists(client, projectId) {
8787
9167
  return envelope.taskLists;
8788
9168
  return Array.isArray(raw) ? raw : [];
8789
9169
  }
9170
+ async function cloudResolveTaskListRef(client, ref, projectId) {
9171
+ const lists = await cloudListTaskLists(client, projectId);
9172
+ const exact = lists.find((list) => list.id === ref || list.slug === ref);
9173
+ if (exact)
9174
+ return exact.id;
9175
+ const prefixes = lists.filter((list) => list.id.startsWith(ref));
9176
+ return prefixes.length === 1 ? prefixes[0].id : null;
9177
+ }
9178
+ async function cloudCreateTaskList(client, input) {
9179
+ const raw = await client.transport.post("/task-lists", input);
9180
+ if (raw && typeof raw === "object" && "task_list" in raw) {
9181
+ return raw.task_list;
9182
+ }
9183
+ return raw;
9184
+ }
9185
+ async function cloudDeleteTaskList(client, id) {
9186
+ await client.delete("task-lists", id);
9187
+ return true;
9188
+ }
8790
9189
  async function cloudNextTask(client, agent, filters) {
8791
9190
  const query = {};
8792
9191
  if (agent)
@@ -8811,11 +9210,11 @@ async function cloudAllDependencies(client) {
8811
9210
  return Array.isArray(raw) ? raw : [];
8812
9211
  }
8813
9212
  async function cloudGetTasksByIds(client, ids) {
8814
- const unique = Array.from(new Set(ids));
9213
+ const unique2 = Array.from(new Set(ids));
8815
9214
  const map = new Map;
8816
9215
  const CONCURRENCY = 8;
8817
- for (let i = 0;i < unique.length; i += CONCURRENCY) {
8818
- const batch = unique.slice(i, i + CONCURRENCY);
9216
+ for (let i = 0;i < unique2.length; i += CONCURRENCY) {
9217
+ const batch = unique2.slice(i, i + CONCURRENCY);
8819
9218
  const tasks = await Promise.all(batch.map((id) => cloudGetTask(client, id)));
8820
9219
  for (const task of tasks)
8821
9220
  if (task && task.id)
@@ -8913,6 +9312,7 @@ async function cloudTimeline(client, options = {}) {
8913
9312
  var _cache, PRIORITY_RANK;
8914
9313
  var init_cloud_router = __esm(() => {
8915
9314
  init_storage();
9315
+ init_redaction();
8916
9316
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
8917
9317
  });
8918
9318
 
@@ -11215,7 +11615,7 @@ var init_schema = __esm(() => {
11215
11615
  });
11216
11616
 
11217
11617
  // src/db/machines.ts
11218
- import { existsSync as existsSync2 } from "fs";
11618
+ import { existsSync as existsSync4 } from "fs";
11219
11619
  import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
11220
11620
  import { resolve } from "path";
11221
11621
  import { spawnSync } from "child_process";
@@ -11422,7 +11822,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
11422
11822
  message: `${project.name} has ${distinctPaths.length} different machine-local paths`
11423
11823
  });
11424
11824
  }
11425
- if (localRow && !existsSync2(localRow.path)) {
11825
+ if (localRow && !existsSync4(localRow.path)) {
11426
11826
  pathIssues.push({
11427
11827
  type: "path_missing",
11428
11828
  project_id: project.id,
@@ -11433,7 +11833,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
11433
11833
  message: `Local path does not exist on this machine: ${localRow.path}`
11434
11834
  });
11435
11835
  }
11436
- if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync2(project.path)) {
11836
+ if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync4(project.path)) {
11437
11837
  pathIssues.push({
11438
11838
  type: "path_missing",
11439
11839
  project_id: project.id,
@@ -11541,8 +11941,8 @@ var init_machines = __esm(() => {
11541
11941
  });
11542
11942
 
11543
11943
  // src/storage/config.ts
11544
- var exports_config = {};
11545
- __export(exports_config, {
11944
+ var exports_config2 = {};
11945
+ __export(exports_config2, {
11546
11946
  parseStorageMode: () => parseStorageMode,
11547
11947
  loadTodosStorageConfig: () => loadTodosStorageConfig,
11548
11948
  loadStorageConfig: () => loadStorageConfig,
@@ -11705,7 +12105,7 @@ function parsePositiveInteger(value, fallback) {
11705
12105
  return parsed;
11706
12106
  }
11707
12107
  var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres", CANONICAL_TODOS_RDS_DATABASE = "todos", CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
11708
- var init_config = __esm(() => {
12108
+ var init_config2 = __esm(() => {
11709
12109
  TODOS_STORAGE_TABLES = [
11710
12110
  "todos_sync_records",
11711
12111
  "todos_sync_cursors"
@@ -11844,8 +12244,8 @@ __export(exports_database, {
11844
12244
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
11845
12245
  });
11846
12246
  import { Database } from "bun:sqlite";
11847
- import { existsSync as existsSync3, mkdirSync } from "fs";
11848
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
12247
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
12248
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
11849
12249
  function isInMemoryDb(path) {
11850
12250
  return path === ":memory:" || path.startsWith("file::memory:");
11851
12251
  }
@@ -11854,12 +12254,12 @@ function findNearestProjectDb(startDir) {
11854
12254
  const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
11855
12255
  let dir = resolve2(startDir);
11856
12256
  while (true) {
11857
- const candidate = join2(dir, ".hasna", "todos", "todos.db");
11858
- if (existsSync3(candidate))
12257
+ const candidate = join4(dir, ".hasna", "todos", "todos.db");
12258
+ if (existsSync5(candidate))
11859
12259
  return candidate;
11860
12260
  if (dir === stopAt)
11861
12261
  break;
11862
- const parent = dirname2(dir);
12262
+ const parent = dirname3(dir);
11863
12263
  if (parent === dir)
11864
12264
  break;
11865
12265
  dir = parent;
@@ -11869,9 +12269,9 @@ function findNearestProjectDb(startDir) {
11869
12269
  function findGitRoot(startDir) {
11870
12270
  let dir = resolve2(startDir);
11871
12271
  while (true) {
11872
- if (existsSync3(join2(dir, ".git")))
12272
+ if (existsSync5(join4(dir, ".git")))
11873
12273
  return dir;
11874
- const parent = dirname2(dir);
12274
+ const parent = dirname3(dir);
11875
12275
  if (parent === dir)
11876
12276
  break;
11877
12277
  dir = parent;
@@ -11892,25 +12292,25 @@ function getDbPath() {
11892
12292
  if (process.env["TODOS_DB_SCOPE"] === "project") {
11893
12293
  const gitRoot = findGitRoot(cwd);
11894
12294
  if (gitRoot) {
11895
- return join2(gitRoot, ".hasna", "todos", "todos.db");
12295
+ return join4(gitRoot, ".hasna", "todos", "todos.db");
11896
12296
  }
11897
12297
  }
11898
12298
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
11899
- return join2(home, ".hasna", "todos", "todos.db");
12299
+ return join4(home, ".hasna", "todos", "todos.db");
11900
12300
  }
11901
12301
  function getDatabasePath() {
11902
12302
  return getDbPath();
11903
12303
  }
11904
- function ensureDir(filePath) {
12304
+ function ensureDir2(filePath) {
11905
12305
  if (isInMemoryDb(filePath))
11906
12306
  return;
11907
- const dir = dirname2(resolve2(filePath));
11908
- if (!existsSync3(dir)) {
11909
- mkdirSync(dir, { recursive: true });
12307
+ const dir = dirname3(resolve2(filePath));
12308
+ if (!existsSync5(dir)) {
12309
+ mkdirSync2(dir, { recursive: true });
11910
12310
  }
11911
12311
  }
11912
12312
  function openDatabase(path) {
11913
- ensureDir(path);
12313
+ ensureDir2(path);
11914
12314
  const db = new Database(path);
11915
12315
  db.run("PRAGMA journal_mode = WAL");
11916
12316
  db.run("PRAGMA busy_timeout = 5000");
@@ -11923,7 +12323,7 @@ function openDatabase(path) {
11923
12323
  }
11924
12324
  function maybeInstallShadowCapture(db) {
11925
12325
  try {
11926
- const { isTodosShadowEnabled: isTodosShadowEnabled2 } = (init_config(), __toCommonJS(exports_config));
12326
+ const { isTodosShadowEnabled: isTodosShadowEnabled2 } = (init_config2(), __toCommonJS(exports_config2));
11927
12327
  if (!isTodosShadowEnabled2())
11928
12328
  return;
11929
12329
  const { installShadowOutboxSchema: installShadowOutboxSchema2 } = (init_shadow_outbox_schema(), __toCommonJS(exports_shadow_outbox_schema));
@@ -12704,199 +13104,6 @@ var init_helpers = __esm(() => {
12704
13104
  };
12705
13105
  });
12706
13106
 
12707
- // src/lib/sync-utils.ts
12708
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
12709
- import { join as join3 } from "path";
12710
- function getHomeDir() {
12711
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
12712
- }
12713
- function getTodosGlobalDir() {
12714
- return join3(getHomeDir(), ".hasna", "todos");
12715
- }
12716
- function ensureDir2(dir) {
12717
- if (!existsSync4(dir))
12718
- mkdirSync2(dir, { recursive: true });
12719
- }
12720
- function listJsonFiles(dir) {
12721
- if (!existsSync4(dir))
12722
- return [];
12723
- return readdirSync(dir).filter((f) => f.endsWith(".json"));
12724
- }
12725
- function readJsonFile(path) {
12726
- try {
12727
- return JSON.parse(readFileSync2(path, "utf-8"));
12728
- } catch {
12729
- return null;
12730
- }
12731
- }
12732
- function writeJsonFile(path, data) {
12733
- writeFileSync(path, JSON.stringify(data, null, 2) + `
12734
- `);
12735
- }
12736
- function readHighWaterMark(dir) {
12737
- const path = join3(dir, ".highwatermark");
12738
- if (!existsSync4(path))
12739
- return 1;
12740
- const val = parseInt(readFileSync2(path, "utf-8").trim(), 10);
12741
- return isNaN(val) ? 1 : val;
12742
- }
12743
- function writeHighWaterMark(dir, value) {
12744
- writeFileSync(join3(dir, ".highwatermark"), String(value));
12745
- }
12746
- function getFileMtimeMs(path) {
12747
- try {
12748
- return statSync(path).mtimeMs;
12749
- } catch {
12750
- return null;
12751
- }
12752
- }
12753
- function parseTimestamp(value) {
12754
- if (typeof value !== "string")
12755
- return null;
12756
- const parsed = Date.parse(value);
12757
- return Number.isNaN(parsed) ? null : parsed;
12758
- }
12759
- function appendSyncConflict(metadata, conflict, limit = 5) {
12760
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
12761
- const next = [conflict, ...current].slice(0, limit);
12762
- return { ...metadata, sync_conflicts: next };
12763
- }
12764
- var HOME;
12765
- var init_sync_utils = __esm(() => {
12766
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
12767
- });
12768
-
12769
- // src/lib/config.ts
12770
- var exports_config2 = {};
12771
- __export(exports_config2, {
12772
- updateConfig: () => updateConfig,
12773
- saveConfig: () => saveConfig,
12774
- resetConfig: () => resetConfig,
12775
- normalizeApiUrl: () => normalizeApiUrl,
12776
- loadConfig: () => loadConfig,
12777
- getTaskPrefixConfig: () => getTaskPrefixConfig,
12778
- getSyncAgentsFromConfig: () => getSyncAgentsFromConfig,
12779
- getLocalApiConfig: () => getLocalApiConfig,
12780
- getConfigPath: () => getConfigPath,
12781
- getCompletionGuardConfig: () => getCompletionGuardConfig,
12782
- getAgentTasksDir: () => getAgentTasksDir,
12783
- getAgentTaskListId: () => getAgentTaskListId,
12784
- getAgentPoolForProject: () => getAgentPoolForProject
12785
- });
12786
- import { existsSync as existsSync5 } from "fs";
12787
- import { dirname as dirname3, join as join4 } from "path";
12788
- function getConfigPath() {
12789
- return join4(getTodosGlobalDir(), "config.json");
12790
- }
12791
- function resetConfig() {
12792
- cached = null;
12793
- }
12794
- function normalizeAgent(agent) {
12795
- return agent.trim().toLowerCase();
12796
- }
12797
- function loadConfig() {
12798
- if (cached)
12799
- return cached;
12800
- if (!existsSync5(getConfigPath())) {
12801
- cached = {};
12802
- return cached;
12803
- }
12804
- const config = readJsonFile(getConfigPath()) || {};
12805
- if (typeof config.sync_agents === "string") {
12806
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
12807
- }
12808
- cached = config;
12809
- return cached;
12810
- }
12811
- function saveConfig(config) {
12812
- const configPath = getConfigPath();
12813
- ensureDir2(dirname3(configPath));
12814
- writeJsonFile(configPath, config);
12815
- cached = config;
12816
- return config;
12817
- }
12818
- function updateConfig(patch) {
12819
- return saveConfig({ ...loadConfig(), ...patch });
12820
- }
12821
- function normalizeApiUrl(value) {
12822
- const trimmed = value?.trim();
12823
- if (!trimmed)
12824
- return null;
12825
- return trimmed.replace(/\/+$/, "");
12826
- }
12827
- function getLocalApiConfig(env = process.env) {
12828
- const config = loadConfig();
12829
- const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
12830
- const configApiUrl = normalizeApiUrl(config.apiUrl);
12831
- const apiUrl = envApiUrl ?? configApiUrl;
12832
- const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
12833
- return {
12834
- apiUrl,
12835
- apiKey,
12836
- source: {
12837
- apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
12838
- apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
12839
- }
12840
- };
12841
- }
12842
- function getSyncAgentsFromConfig() {
12843
- const config = loadConfig();
12844
- const agents = config.sync_agents;
12845
- if (Array.isArray(agents) && agents.length > 0)
12846
- return agents.map(normalizeAgent);
12847
- return null;
12848
- }
12849
- function getAgentTaskListId(agent) {
12850
- const config = loadConfig();
12851
- const key = normalizeAgent(agent);
12852
- return config.agents?.[key]?.task_list_id || config.task_list_id || null;
12853
- }
12854
- function getAgentTasksDir(agent) {
12855
- const config = loadConfig();
12856
- const key = normalizeAgent(agent);
12857
- return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
12858
- }
12859
- function getTaskPrefixConfig() {
12860
- const config = loadConfig();
12861
- return config.task_prefix || null;
12862
- }
12863
- function getAgentPoolForProject(workingDir) {
12864
- const config = loadConfig();
12865
- if (workingDir && config.project_pools) {
12866
- let bestKey = null;
12867
- let bestLen = 0;
12868
- for (const key of Object.keys(config.project_pools)) {
12869
- if (workingDir.startsWith(key) && key.length > bestLen) {
12870
- bestKey = key;
12871
- bestLen = key.length;
12872
- }
12873
- }
12874
- if (bestKey && config.project_pools[bestKey]) {
12875
- return config.project_pools[bestKey];
12876
- }
12877
- }
12878
- return config.agent_pool || null;
12879
- }
12880
- function getCompletionGuardConfig(projectPath) {
12881
- const config = loadConfig();
12882
- const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
12883
- if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
12884
- return { ...global, ...config.project_overrides[projectPath].completion_guard };
12885
- }
12886
- return global;
12887
- }
12888
- var cached = null, GUARD_DEFAULTS;
12889
- var init_config2 = __esm(() => {
12890
- init_sync_utils();
12891
- GUARD_DEFAULTS = {
12892
- enabled: false,
12893
- min_work_seconds: 30,
12894
- max_completions_per_window: 5,
12895
- window_minutes: 10,
12896
- cooldown_seconds: 60
12897
- };
12898
- });
12899
-
12900
13107
  // src/lib/completion-guard.ts
12901
13108
  function checkCompletionGuard(task, agentId, db, configOverride) {
12902
13109
  let config;
@@ -12942,7 +13149,7 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
12942
13149
  }
12943
13150
  var init_completion_guard = __esm(() => {
12944
13151
  init_types();
12945
- init_config2();
13152
+ init_config();
12946
13153
  init_projects();
12947
13154
  });
12948
13155
 
@@ -12997,117 +13204,6 @@ var init_event_emission_safety = __esm(() => {
12997
13204
  init_sync_utils();
12998
13205
  });
12999
13206
 
13000
- // src/lib/redaction.ts
13001
- var exports_redaction = {};
13002
- __export(exports_redaction, {
13003
- upsertSecretSafetyConfig: () => upsertSecretSafetyConfig,
13004
- redactValue: () => redactValue,
13005
- redactEvidenceText: () => redactEvidenceText,
13006
- listSecretFindings: () => listSecretFindings,
13007
- hasSecretFindings: () => hasSecretFindings,
13008
- getSecretSafetyConfig: () => getSecretSafetyConfig
13009
- });
13010
- function unique(values) {
13011
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
13012
- }
13013
- function cloneRegex(regex) {
13014
- return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
13015
- }
13016
- function customPatterns() {
13017
- return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
13018
- try {
13019
- return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
13020
- } catch {
13021
- return [];
13022
- }
13023
- });
13024
- }
13025
- function secretPatterns() {
13026
- return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
13027
- }
13028
- function isSecretKey(key) {
13029
- if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
13030
- return false;
13031
- if (DEFAULT_SECRET_KEY_PATTERN.test(key))
13032
- return true;
13033
- return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
13034
- }
13035
- function redactEvidenceText(value) {
13036
- let redacted = value;
13037
- for (const pattern of secretPatterns()) {
13038
- const regex = cloneRegex(pattern.regex);
13039
- const replacement = pattern.replacement ?? "[REDACTED]";
13040
- redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
13041
- }
13042
- return redacted;
13043
- }
13044
- function redactValue(value) {
13045
- if (typeof value === "string")
13046
- return redactEvidenceText(value);
13047
- if (Array.isArray(value))
13048
- return value.map(redactValue);
13049
- if (value && typeof value === "object") {
13050
- const redacted = {};
13051
- for (const [key, child] of Object.entries(value)) {
13052
- if (isSecretKey(key)) {
13053
- redacted[key] = "[REDACTED]";
13054
- } else {
13055
- redacted[key] = redactValue(child);
13056
- }
13057
- }
13058
- return redacted;
13059
- }
13060
- return value;
13061
- }
13062
- function listSecretFindings(value) {
13063
- const findings = [];
13064
- for (const pattern of secretPatterns()) {
13065
- const matches = value.match(cloneRegex(pattern.regex));
13066
- if (matches?.length)
13067
- findings.push({ pattern: pattern.name, count: matches.length });
13068
- }
13069
- return findings;
13070
- }
13071
- function hasSecretFindings(value) {
13072
- return listSecretFindings(value).length > 0;
13073
- }
13074
- function getSecretSafetyConfig() {
13075
- return {
13076
- redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
13077
- redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
13078
- };
13079
- }
13080
- function upsertSecretSafetyConfig(input) {
13081
- const config = loadConfig();
13082
- const next = {
13083
- redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
13084
- redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
13085
- };
13086
- saveConfig({ ...config, secret_safety: next });
13087
- return next;
13088
- }
13089
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
13090
- var init_redaction = __esm(() => {
13091
- init_config2();
13092
- DEFAULT_SECRET_PATTERNS = [
13093
- { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
13094
- { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
13095
- { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
13096
- { 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]" },
13097
- { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
13098
- ];
13099
- DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
13100
- NON_SECRET_USAGE_KEYS = new Set([
13101
- "tokens",
13102
- "total_tokens",
13103
- "token_count",
13104
- "input_tokens",
13105
- "output_tokens",
13106
- "prompt_tokens",
13107
- "completion_tokens"
13108
- ]);
13109
- });
13110
-
13111
13207
  // src/lib/workspace-trust.ts
13112
13208
  var exports_workspace_trust = {};
13113
13209
  __export(exports_workspace_trust, {
@@ -13253,7 +13349,7 @@ function checkWorkspacePermission(input = {}) {
13253
13349
  }
13254
13350
  var DEFAULT_DENYLIST, DEFAULT_ENV_REDACTIONS, PRESET_DEFAULTS;
13255
13351
  var init_workspace_trust = __esm(() => {
13256
- init_config2();
13352
+ init_config();
13257
13353
  DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
13258
13354
  DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
13259
13355
  PRESET_DEFAULTS = {
@@ -13496,7 +13592,7 @@ function explainRunnerSandbox(input = {}) {
13496
13592
  }
13497
13593
  var DEFAULT_COMMAND_DENYLIST, DEFAULT_ENV_REDACTIONS2;
13498
13594
  var init_runner_sandbox = __esm(() => {
13499
- init_config2();
13595
+ init_config();
13500
13596
  init_workspace_trust();
13501
13597
  DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
13502
13598
  DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
@@ -13743,7 +13839,7 @@ var LOCAL_EVENT_TYPES, VALID_TARGETS;
13743
13839
  var init_event_hooks = __esm(() => {
13744
13840
  init_redaction();
13745
13841
  init_runner_sandbox();
13746
- init_config2();
13842
+ init_config();
13747
13843
  init_event_emission_safety();
13748
13844
  LOCAL_EVENT_TYPES = [
13749
13845
  "task.created",
@@ -18628,7 +18724,7 @@ function getComment(id, db) {
18628
18724
  }
18629
18725
  function listComments(taskId, db) {
18630
18726
  const d = db || getDatabase();
18631
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
18727
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at, rowid").all(taskId);
18632
18728
  }
18633
18729
  function updateComment(id, input, db) {
18634
18730
  const d = db || getDatabase();
@@ -20256,10 +20352,27 @@ var init_task_routing = __esm(() => {
20256
20352
  // src/cli/commands/task-commands.ts
20257
20353
  var exports_task_commands = {};
20258
20354
  __export(exports_task_commands, {
20259
- registerTaskCommands: () => registerTaskCommands
20355
+ registerTaskCommands: () => registerTaskCommands,
20356
+ escapeTerminalControls: () => escapeTerminalControls
20260
20357
  });
20261
20358
  import chalk2 from "chalk";
20262
20359
  import { basename as basename3, resolve as resolve9 } from "path";
20360
+ function escapeTerminalControls(value) {
20361
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, (character) => {
20362
+ const code = character.charCodeAt(0);
20363
+ if (code === 10)
20364
+ return "\\n";
20365
+ if (code === 13)
20366
+ return "\\r";
20367
+ if (code === 9)
20368
+ return "\\t";
20369
+ return `\\x${code.toString(16).padStart(2, "0")}`;
20370
+ });
20371
+ }
20372
+ function formatHumanComment(comment) {
20373
+ const agent = comment.agent_id ? chalk2.cyan(`[${escapeTerminalControls(comment.agent_id)}] `) : "";
20374
+ return ` ${agent}${chalk2.dim(escapeTerminalControls(comment.created_at))}: ${escapeTerminalControls(comment.content)}`;
20375
+ }
20263
20376
  function resolveProjectIdOrSlug(input) {
20264
20377
  const db = getDatabase();
20265
20378
  if (isPathLike(input)) {
@@ -20416,18 +20529,24 @@ function registerTaskCommands(program2) {
20416
20529
  if (cloud) {
20417
20530
  let task3;
20418
20531
  try {
20532
+ const cloudProjectId = opts.project || globalOpts.project;
20533
+ const cloudTaskListId = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, cloudProjectId) : undefined;
20534
+ if (opts.list && !cloudTaskListId) {
20535
+ throw new Error(`Could not resolve task list ID or slug: ${opts.list}`);
20536
+ }
20419
20537
  task3 = await cloudCreateTask(cloud, {
20420
20538
  title,
20421
20539
  description: opts.description,
20422
20540
  priority: parsePriority(opts.priority),
20541
+ parent_id: opts.parent,
20423
20542
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
20424
20543
  plan_id: opts.plan,
20425
20544
  assigned_to: opts.assign,
20426
20545
  status: parseStatus(opts.status),
20427
- task_list_id: opts.list,
20546
+ task_list_id: cloudTaskListId,
20428
20547
  agent_id: globalOpts.agent,
20429
20548
  session_id: globalOpts.session,
20430
- project_id: opts.project || globalOpts.project,
20549
+ project_id: cloudProjectId,
20431
20550
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
20432
20551
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
20433
20552
  requires_approval: opts.approval || undefined,
@@ -20786,7 +20905,22 @@ function registerTaskCommands(program2) {
20786
20905
  let task2;
20787
20906
  if (cloud) {
20788
20907
  const remote = await cloudGetTask(cloud, resolveTaskId(id));
20789
- task2 = remote ? { subtasks: [], dependencies: [], blocked_by: [], comments: [], ...remote, tags: remote.tags ?? [] } : null;
20908
+ const commentPage = remote ? await cloudListComments(cloud, remote.id) : null;
20909
+ task2 = remote ? {
20910
+ subtasks: [],
20911
+ dependencies: [],
20912
+ blocked_by: [],
20913
+ ...remote,
20914
+ tags: remote.tags ?? [],
20915
+ comments: commentPage.comments,
20916
+ comments_page: {
20917
+ count: commentPage.count,
20918
+ limit: commentPage.limit,
20919
+ has_more: commentPage.has_more,
20920
+ next_cursor: commentPage.next_cursor,
20921
+ pagination_supported: commentPage.pagination_supported
20922
+ }
20923
+ } : null;
20790
20924
  } else {
20791
20925
  const resolvedId = resolveTaskId(id);
20792
20926
  task2 = getTaskWithRelations(resolvedId);
@@ -20870,11 +21004,11 @@ function registerTaskCommands(program2) {
20870
21004
  }
20871
21005
  }
20872
21006
  if (task2.comments.length > 0) {
21007
+ const suffix = task2.comments_page?.has_more ? task2.comments_page.pagination_supported ? ", newer page shown; older comments available" : ", newer comments shown; older comments omitted until the server is upgraded" : "";
20873
21008
  console.log(chalk2.bold(`
20874
- Comments (${task2.comments.length}):`));
21009
+ Comments (${task2.comments.length}${suffix}):`));
20875
21010
  for (const c of task2.comments) {
20876
- const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
20877
- console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
21011
+ console.log(formatHumanComment(c));
20878
21012
  }
20879
21013
  }
20880
21014
  });
@@ -20900,7 +21034,23 @@ function registerTaskCommands(program2) {
20900
21034
  let task2;
20901
21035
  if (cloud) {
20902
21036
  const remote = await cloudGetTask(cloud, resolvedId);
20903
- task2 = remote ? { subtasks: [], dependencies: [], blocked_by: [], comments: [], checklist: [], ...remote, tags: remote.tags ?? [] } : null;
21037
+ const commentPage = remote ? await cloudListComments(cloud, remote.id) : null;
21038
+ task2 = remote ? {
21039
+ subtasks: [],
21040
+ dependencies: [],
21041
+ blocked_by: [],
21042
+ checklist: [],
21043
+ ...remote,
21044
+ tags: remote.tags ?? [],
21045
+ comments: commentPage.comments,
21046
+ comments_page: {
21047
+ count: commentPage.count,
21048
+ limit: commentPage.limit,
21049
+ has_more: commentPage.has_more,
21050
+ next_cursor: commentPage.next_cursor,
21051
+ pagination_supported: commentPage.pagination_supported
21052
+ }
21053
+ } : null;
20904
21054
  } else {
20905
21055
  task2 = getTaskWithRelations(resolvedId);
20906
21056
  }
@@ -21007,11 +21157,11 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21007
21157
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
21008
21158
  }
21009
21159
  if (task2.comments.length > 0) {
21160
+ const suffix = task2.comments_page?.has_more ? task2.comments_page.pagination_supported ? ", newer page shown; older comments available" : ", newer comments shown; older comments omitted until the server is upgraded" : "";
21010
21161
  console.log(chalk2.bold(`
21011
- Comments (${task2.comments.length}):`));
21162
+ Comments (${task2.comments.length}${suffix}):`));
21012
21163
  for (const c of task2.comments) {
21013
- const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
21014
- console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
21164
+ console.log(formatHumanComment(c));
21015
21165
  }
21016
21166
  }
21017
21167
  if (task2.checklist && task2.checklist.length > 0) {
@@ -21307,7 +21457,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21307
21457
  const resolvedId = resolveTaskId(id);
21308
21458
  try {
21309
21459
  if (cloud)
21310
- await cloudUnlockTask(cloud, resolvedId, globalOpts.agent);
21460
+ await cloudUnlockTask(cloud, resolvedId, globalOpts.agent, !globalOpts.agent);
21311
21461
  else
21312
21462
  unlockTask(resolvedId, globalOpts.agent);
21313
21463
  } catch (e) {
@@ -23262,7 +23412,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
23262
23412
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
23263
23413
  const dir = getTaskListDir(taskListId);
23264
23414
  if (!existsSync10(dir))
23265
- ensureDir2(dir);
23415
+ ensureDir(dir);
23266
23416
  const filter = {};
23267
23417
  if (projectId)
23268
23418
  filter["project_id"] = projectId;
@@ -23445,7 +23595,7 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
23445
23595
  }
23446
23596
  var init_claude_tasks = __esm(() => {
23447
23597
  init_tasks();
23448
- init_config2();
23598
+ init_config();
23449
23599
  init_sync_utils();
23450
23600
  });
23451
23601
 
@@ -23489,7 +23639,7 @@ function metadataKey(agent) {
23489
23639
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
23490
23640
  const dir = getTaskListDir2(agent, taskListId);
23491
23641
  if (!existsSync11(dir))
23492
- ensureDir2(dir);
23642
+ ensureDir(dir);
23493
23643
  const filter = {};
23494
23644
  if (projectId)
23495
23645
  filter["project_id"] = projectId;
@@ -23662,7 +23812,7 @@ function syncAgentTaskList(agent, taskListId, projectId, options = {}) {
23662
23812
  var init_agent_tasks = __esm(() => {
23663
23813
  init_tasks();
23664
23814
  init_sync_utils();
23665
- init_config2();
23815
+ init_config();
23666
23816
  });
23667
23817
 
23668
23818
  // src/lib/sync.ts
@@ -23734,7 +23884,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
23734
23884
  var init_sync = __esm(() => {
23735
23885
  init_claude_tasks();
23736
23886
  init_agent_tasks();
23737
- init_config2();
23887
+ init_config();
23738
23888
  });
23739
23889
 
23740
23890
  // src/lib/project-bootstrap.ts
@@ -31447,7 +31597,7 @@ function applyExportProfile(data, options = {}) {
31447
31597
  }
31448
31598
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value", TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge", TODOS_ENCRYPTION_SCHEMA_VERSION = 1, DEFAULT_ENCRYPTION_PROFILE = "default", DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY", EncryptionKeyUnavailableError, EncryptedPayloadError;
31449
31599
  var init_local_encryption = __esm(() => {
31450
- init_config2();
31600
+ init_config();
31451
31601
  init_redaction();
31452
31602
  EncryptionKeyUnavailableError = class EncryptionKeyUnavailableError extends Error {
31453
31603
  keyEnv;
@@ -32626,7 +32776,7 @@ var init_project_commands = __esm(() => {
32626
32776
  init_cloud_router();
32627
32777
  init_saved_search_views();
32628
32778
  init_sync();
32629
- init_config2();
32779
+ init_config();
32630
32780
  init_helpers();
32631
32781
  });
32632
32782
 
@@ -33626,9 +33776,11 @@ ${isOnline ? chalk5.green("\u25CF") : chalk5.dim("\u25CB")} ${chalk5.bold(agent.
33626
33776
  program2.command("lists").aliases(["task-lists", "tl"]).description("List and manage task lists").option("--add <name>", "Create a task list").option("--slug <slug>", "Custom slug (with --add)").option("-d, --description <text>", "Description (with --add)").option("--delete <id>", "Delete a task list").action(async (opts) => {
33627
33777
  try {
33628
33778
  const globalOpts = program2.opts();
33629
- const projectId = autoProject(globalOpts);
33779
+ const cloud = getTodosCloudClient();
33780
+ const projectId = cloud ? globalOpts.project : autoProject(globalOpts);
33630
33781
  if (opts.add) {
33631
- const list = createTaskList({ name: opts.add, slug: opts.slug, description: opts.description, project_id: projectId });
33782
+ const input = { name: opts.add, slug: opts.slug, description: opts.description, project_id: projectId };
33783
+ const list = cloud ? await cloudCreateTaskList(cloud, input) : createTaskList(input);
33632
33784
  if (globalOpts.json) {
33633
33785
  output(list, true);
33634
33786
  return;
@@ -33640,6 +33792,14 @@ ${isOnline ? chalk5.green("\u25CF") : chalk5.dim("\u25CB")} ${chalk5.bold(agent.
33640
33792
  return;
33641
33793
  }
33642
33794
  if (opts.delete) {
33795
+ if (cloud) {
33796
+ const resolved2 = await cloudResolveTaskListRef(cloud, opts.delete, projectId ?? undefined);
33797
+ if (!resolved2)
33798
+ throw new Error(`Task list not found or ambiguous: ${opts.delete}`);
33799
+ await cloudDeleteTaskList(cloud, resolved2);
33800
+ console.log(chalk5.green("Task list deleted."));
33801
+ return;
33802
+ }
33643
33803
  const db = getDatabase();
33644
33804
  const resolved = resolvePartialId(db, "task_lists", opts.delete);
33645
33805
  if (!resolved) {
@@ -33650,7 +33810,6 @@ ${isOnline ? chalk5.green("\u25CF") : chalk5.dim("\u25CB")} ${chalk5.bold(agent.
33650
33810
  console.log(chalk5.green("Task list deleted."));
33651
33811
  return;
33652
33812
  }
33653
- const cloud = getTodosCloudClient();
33654
33813
  const lists = cloud ? await cloudListTaskLists(cloud, projectId ?? undefined) : listTaskLists(projectId);
33655
33814
  if (globalOpts.json) {
33656
33815
  output(lists, true);
@@ -35065,7 +35224,7 @@ function renderExtensionSummary(record) {
35065
35224
  }
35066
35225
  var BUILTIN_CLI_COMMANDS;
35067
35226
  var init_local_extensions = __esm(() => {
35068
- init_config2();
35227
+ init_config();
35069
35228
  init_mcp();
35070
35229
  init_package_version();
35071
35230
  init_redaction();
@@ -35497,7 +35656,7 @@ var init_policy_packs = __esm(() => {
35497
35656
  init_database();
35498
35657
  init_tasks();
35499
35658
  init_task_runs();
35500
- init_config2();
35659
+ init_config();
35501
35660
  });
35502
35661
 
35503
35662
  // src/db/checkpoints.ts
@@ -36020,7 +36179,7 @@ function describeTerminalNotificationRule(rule) {
36020
36179
  }
36021
36180
  var SEVERITY_ORDER, EVENT_SEVERITY, VALID_SEVERITIES, VALID_FORMATS;
36022
36181
  var init_terminal_notifications = __esm(() => {
36023
- init_config2();
36182
+ init_config();
36024
36183
  init_event_hooks();
36025
36184
  init_redaction();
36026
36185
  SEVERITY_ORDER = {
@@ -37523,7 +37682,7 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
37523
37682
  return createTodosCloudQueryClient(url, options);
37524
37683
  }
37525
37684
  var init_cloud_client = __esm(() => {
37526
- init_config();
37685
+ init_config2();
37527
37686
  });
37528
37687
 
37529
37688
  // src/storage/sqlite-snapshot.ts
@@ -37905,6 +38064,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
37905
38064
  list: (filter = {}) => listTasks(filter, database()),
37906
38065
  count: (filter = {}) => countTasks(filter, database()),
37907
38066
  update: (id, input) => updateTask(id, input, database()),
38067
+ unlock: (id, agentId) => {
38068
+ unlockTask(id, agentId, database());
38069
+ return true;
38070
+ },
37908
38071
  delete: (id) => deleteTask(id, database()),
37909
38072
  start: (id, agentId) => startTask(id, agentId, database()),
37910
38073
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
@@ -37956,6 +38119,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
37956
38119
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
37957
38120
  addComment: (input) => addComment(input, database()),
37958
38121
  getComments: (taskId) => listComments(taskId, database()),
38122
+ getCommentsPage: (taskId, options2) => {
38123
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
38124
+ throw new Error("Comment limit must be an integer between 1 and 1001");
38125
+ }
38126
+ let comments = listComments(taskId, database());
38127
+ comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
38128
+ if (options2?.before) {
38129
+ const before = options2.before;
38130
+ comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
38131
+ }
38132
+ if (options2?.limit !== undefined)
38133
+ comments = comments.slice(-options2.limit);
38134
+ return comments;
38135
+ },
37959
38136
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
37960
38137
  getRecentActivity: (limit) => getRecentActivity(limit, database())
37961
38138
  },
@@ -38013,6 +38190,12 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
38013
38190
  )`
38014
38191
  ];
38015
38192
  }
38193
+ function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
38194
+ assertSafeIdentifier(tableName);
38195
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
38196
+ ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
38197
+ WHERE object_type = 'comments' AND deleted_at IS NULL`;
38198
+ }
38016
38199
 
38017
38200
  class PostgresTodosSyncStore {
38018
38201
  client;
@@ -38538,7 +38721,7 @@ function __resetRuntimeShadowForTests() {
38538
38721
  }
38539
38722
  var _capturedDb = null, _outbox = null, _cloud = null, _exitRegistered = false;
38540
38723
  var init_shadow_runtime = __esm(() => {
38541
- init_config();
38724
+ init_config2();
38542
38725
  init_cloud_client();
38543
38726
  init_shadow_outbox();
38544
38727
  });
@@ -38994,7 +39177,24 @@ function createPostgresTodosStorageAdapter(options) {
38994
39177
  audit: {
38995
39178
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context),
38996
39179
  addComment: (input, context) => addComment2(input, store, context),
38997
- getComments: async (taskId) => (await store.list("comments")).filter((comment) => comment.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
39180
+ getComments: async (taskId) => {
39181
+ const pages = [];
39182
+ let before;
39183
+ while (true) {
39184
+ const page = await store.listComments(taskId, { limit: 1000, ...before ? { before } : {} });
39185
+ if (page.length === 0)
39186
+ break;
39187
+ pages.unshift(page);
39188
+ if (page.length < 1000)
39189
+ break;
39190
+ const oldest = page[0];
39191
+ before = { created_at: oldest.created_at, id: oldest.id };
39192
+ }
39193
+ return pages.flat().map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
39194
+ },
39195
+ getCommentsPage: async (taskId, options2) => {
39196
+ return (await store.listComments(taskId, options2)).map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
39197
+ },
38998
39198
  getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
38999
39199
  getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
39000
39200
  },
@@ -39044,6 +39244,27 @@ class PostgresJsonRecordStore {
39044
39244
  async list(type) {
39045
39245
  return (await this.listRecords(type)).map((record) => record.payload);
39046
39246
  }
39247
+ async listComments(taskId, options = {}) {
39248
+ await this.ensureSchema();
39249
+ const limit = options.limit ?? 100;
39250
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
39251
+ throw new Error("Postgres comment limit must be an integer between 1 and 1001");
39252
+ }
39253
+ const params = [this.service, taskId];
39254
+ let cursorPredicate = "";
39255
+ if (options.before) {
39256
+ params.push(options.before.created_at, options.before.id);
39257
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
39258
+ }
39259
+ params.push(limit);
39260
+ const result = await this.options.client.query(`/* todos:list-comments */ SELECT payload FROM ${this.tableName}
39261
+ WHERE service = $1 AND object_type = 'comments' AND deleted_at IS NULL
39262
+ AND payload->>'task_id' = $2
39263
+ ${cursorPredicate}
39264
+ ORDER BY payload->>'created_at' DESC, object_id DESC
39265
+ LIMIT $${params.length}`, params);
39266
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
39267
+ }
39047
39268
  async listRecords(type) {
39048
39269
  await this.ensureSchema();
39049
39270
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -39437,7 +39658,7 @@ async function lockTask2(id, agentId, store) {
39437
39658
  async function unlockTask2(id, agentId, store) {
39438
39659
  const task = await requireRecord("tasks", id, store);
39439
39660
  if (agentId && task.locked_by && task.locked_by !== agentId) {
39440
- throw new Error(`Task ${id} is locked by ${task.locked_by}, not ${agentId}`);
39661
+ throw new LockError(id, task.locked_by);
39441
39662
  }
39442
39663
  await patchTask(task, { locked_by: null, locked_at: null }, store);
39443
39664
  return true;
@@ -39802,13 +40023,16 @@ async function addComment2(input, store, context) {
39802
40023
  task_id: input.task_id,
39803
40024
  agent_id: input.agent_id ?? context?.agentId ?? null,
39804
40025
  session_id: input.session_id ?? context?.sessionId ?? null,
39805
- content: input.content,
40026
+ content: redactEvidenceText(input.content),
39806
40027
  type: input.type ?? "comment",
39807
40028
  progress_pct: input.progress_pct ?? null,
39808
40029
  created_at: new Date().toISOString()
39809
40030
  };
39810
40031
  return store.upsert("comments", comment, context);
39811
40032
  }
40033
+ function redactComment2(comment) {
40034
+ return { ...comment, content: redactEvidenceText(comment.content) };
40035
+ }
39812
40036
  async function exportSnapshot(store) {
39813
40037
  return {
39814
40038
  exportedAt: new Date().toISOString(),
@@ -39954,7 +40178,109 @@ function numberValue2(value) {
39954
40178
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
39955
40179
  }
39956
40180
  var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_BY = "ORDER BY CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC";
39957
- var init_postgres_adapter = () => {};
40181
+ var init_postgres_adapter = __esm(() => {
40182
+ init_types();
40183
+ init_redaction();
40184
+ });
40185
+
40186
+ // src/storage/comment-redaction-backfill.ts
40187
+ function assertSafeIdentifier2(value) {
40188
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
40189
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
40190
+ }
40191
+ }
40192
+ function payloadObject(value) {
40193
+ if (value && typeof value === "object" && !Array.isArray(value))
40194
+ return value;
40195
+ if (typeof value !== "string")
40196
+ return null;
40197
+ try {
40198
+ const parsed = JSON.parse(value);
40199
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
40200
+ } catch {
40201
+ return null;
40202
+ }
40203
+ }
40204
+ async function backfillPostgresCommentRedaction(client, options = {}) {
40205
+ const apply = options.apply === true;
40206
+ if (apply && options.confirmation !== COMMENT_REDACTION_BACKFILL_CONFIRMATION) {
40207
+ throw new Error(`Applying the comment redaction backfill requires confirmation ${COMMENT_REDACTION_BACKFILL_CONFIRMATION}`);
40208
+ }
40209
+ const tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
40210
+ assertSafeIdentifier2(tableName);
40211
+ const service = options.service ?? "todos";
40212
+ const batchSize = options.batchSize ?? 100;
40213
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 500) {
40214
+ throw new Error("Comment redaction backfill batchSize must be an integer between 1 and 500");
40215
+ }
40216
+ const result = {
40217
+ dry_run: !apply,
40218
+ scanned: 0,
40219
+ candidates: 0,
40220
+ updated: 0,
40221
+ conflicts: 0,
40222
+ batches: 0,
40223
+ remaining_candidates: 0
40224
+ };
40225
+ let afterId = "";
40226
+ while (true) {
40227
+ const page = await client.query(`/* todos:comment-redaction-backfill-scan */
40228
+ SELECT object_id, payload
40229
+ FROM ${tableName}
40230
+ WHERE service = $1 AND object_type = 'comments'
40231
+ AND object_id > $2
40232
+ ORDER BY object_id ASC
40233
+ LIMIT $3`, [service, afterId, batchSize]);
40234
+ if (page.rows.length === 0)
40235
+ break;
40236
+ result.batches += 1;
40237
+ for (const row of page.rows) {
40238
+ afterId = row.object_id;
40239
+ result.scanned += 1;
40240
+ const payload = payloadObject(row.payload);
40241
+ const original = payload?.["content"];
40242
+ if (typeof original !== "string")
40243
+ continue;
40244
+ const redacted = redactEvidenceText(original);
40245
+ if (redacted === original)
40246
+ continue;
40247
+ result.candidates += 1;
40248
+ if (!apply)
40249
+ continue;
40250
+ const nextPayload = { ...payload, content: redacted };
40251
+ const update = await client.query(`/* todos:comment-redaction-backfill-apply */
40252
+ UPDATE ${tableName}
40253
+ SET payload = $3::jsonb
40254
+ WHERE service = $1 AND object_type = 'comments' AND object_id = $2
40255
+ AND payload = $4::jsonb
40256
+ RETURNING object_id`, [service, row.object_id, nextPayload, row.payload]);
40257
+ if (update.rows.length === 1)
40258
+ result.updated += 1;
40259
+ else
40260
+ result.conflicts += 1;
40261
+ }
40262
+ if (page.rows.length < batchSize)
40263
+ break;
40264
+ }
40265
+ if (!apply) {
40266
+ result.remaining_candidates = result.candidates;
40267
+ return result;
40268
+ }
40269
+ const verification = await backfillPostgresCommentRedaction(client, {
40270
+ ...options,
40271
+ apply: false,
40272
+ confirmation: undefined
40273
+ });
40274
+ result.remaining_candidates = verification.candidates;
40275
+ return result;
40276
+ }
40277
+ function isCommentRedactionBackfillComplete(result) {
40278
+ return !result.dry_run && result.conflicts === 0 && result.remaining_candidates === 0;
40279
+ }
40280
+ var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
40281
+ var init_comment_redaction_backfill = __esm(() => {
40282
+ init_redaction();
40283
+ });
39958
40284
 
39959
40285
  // src/server/cloud.ts
39960
40286
  var exports_cloud = {};
@@ -39968,7 +40294,9 @@ __export(exports_cloud, {
39968
40294
  getCloudStorageAdapter: () => getCloudStorageAdapter,
39969
40295
  getApiKeyStore: () => getApiKeyStore,
39970
40296
  ensureCloudSchema: () => ensureCloudSchema,
40297
+ ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
39971
40298
  closeCloud: () => closeCloud,
40299
+ backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
39972
40300
  TODOS_APP_SLUG: () => TODOS_APP_SLUG
39973
40301
  });
39974
40302
  function resolveCloudDatabaseUrl(env = process.env) {
@@ -40046,6 +40374,9 @@ async function ensureCloudSchema() {
40046
40374
  })();
40047
40375
  return schemaEnsured;
40048
40376
  }
40377
+ async function ensureCloudCommentCursorIndex() {
40378
+ await getClient().query(postgresTodosCommentCursorIndexSql());
40379
+ }
40049
40380
  async function normalizeCloudPayloads() {
40050
40381
  const client = getClient();
40051
40382
  const res = await client.query(`UPDATE todos_sync_records
@@ -40054,6 +40385,9 @@ async function normalizeCloudPayloads() {
40054
40385
  RETURNING object_id AS id`);
40055
40386
  return res.rows.length;
40056
40387
  }
40388
+ function backfillCloudCommentRedaction(options = {}) {
40389
+ return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
40390
+ }
40057
40391
  async function pingCloud() {
40058
40392
  const client = getClient();
40059
40393
  const res = await client.query("select 1 as ok");
@@ -40074,6 +40408,7 @@ var init_cloud = __esm(() => {
40074
40408
  init_auth();
40075
40409
  init_cloud_client();
40076
40410
  init_postgres_adapter();
40411
+ init_comment_redaction_backfill();
40077
40412
  });
40078
40413
 
40079
40414
  // src/server/openapi.ts
@@ -40097,6 +40432,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40097
40432
  schemas: {
40098
40433
  Task: taskSchema,
40099
40434
  Project: projectSchema,
40435
+ TaskComment: taskCommentSchema,
40100
40436
  CreateTaskInput: {
40101
40437
  type: "object",
40102
40438
  required: ["title"],
@@ -40131,6 +40467,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40131
40467
  description: { type: "string" },
40132
40468
  task_prefix: { type: "string" }
40133
40469
  }
40470
+ },
40471
+ CreateTaskCommentInput: {
40472
+ type: "object",
40473
+ required: ["content"],
40474
+ properties: {
40475
+ content: { type: "string", minLength: 1 },
40476
+ agent_id: { type: "string" },
40477
+ session_id: { type: "string" },
40478
+ type: { type: "string", enum: ["comment", "progress", "note"] },
40479
+ progress_pct: { type: "number" }
40480
+ }
40134
40481
  }
40135
40482
  }
40136
40483
  },
@@ -40229,6 +40576,61 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40229
40576
  }
40230
40577
  }
40231
40578
  },
40579
+ "/v1/tasks/{id}/comments": {
40580
+ get: {
40581
+ operationId: "listTaskComments",
40582
+ summary: "List a bounded page of task comments",
40583
+ description: "Returns the newest page in oldest-to-newest display order. Use next_cursor to request older pages; count is the page size, not a total. Pagination-aware clients must send limit during the mixed-version rollout.",
40584
+ parameters: [
40585
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
40586
+ { name: "limit", in: "query", required: true, schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
40587
+ { name: "cursor", in: "query", schema: { type: "string" } }
40588
+ ],
40589
+ responses: {
40590
+ "200": {
40591
+ content: {
40592
+ "application/json": {
40593
+ schema: {
40594
+ type: "object",
40595
+ required: ["comments", "count", "has_more", "next_cursor"],
40596
+ properties: {
40597
+ comments: { type: "array", maxItems: 500, items: { $ref: "#/components/schemas/TaskComment" } },
40598
+ count: { type: "integer", minimum: 0, maximum: 500 },
40599
+ has_more: { type: "boolean" },
40600
+ next_cursor: { type: "string", nullable: true }
40601
+ }
40602
+ }
40603
+ }
40604
+ }
40605
+ },
40606
+ "426": {
40607
+ description: "Upgrade required: a predecessor client omitted limit and the complete legacy history exceeds 500 comments, or the configured storage adapter lacks cursor pagination support."
40608
+ }
40609
+ }
40610
+ },
40611
+ post: {
40612
+ operationId: "createTaskComment",
40613
+ summary: "Create a task comment",
40614
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
40615
+ requestBody: {
40616
+ required: true,
40617
+ content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTaskCommentInput" } } }
40618
+ },
40619
+ responses: {
40620
+ "201": {
40621
+ content: {
40622
+ "application/json": {
40623
+ schema: {
40624
+ type: "object",
40625
+ required: ["comment"],
40626
+ properties: { comment: { $ref: "#/components/schemas/TaskComment" } }
40627
+ }
40628
+ }
40629
+ }
40630
+ }
40631
+ }
40632
+ }
40633
+ },
40232
40634
  "/v1/tasks/{id}/start": {
40233
40635
  post: {
40234
40636
  operationId: "startTask",
@@ -40347,7 +40749,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40347
40749
  }
40348
40750
  };
40349
40751
  }
40350
- var taskSchema, projectSchema;
40752
+ var taskSchema, projectSchema, taskCommentSchema;
40351
40753
  var init_openapi = __esm(() => {
40352
40754
  init_package_version();
40353
40755
  taskSchema = {
@@ -40378,6 +40780,20 @@ var init_openapi = __esm(() => {
40378
40780
  updated_at: { type: "string" }
40379
40781
  }
40380
40782
  };
40783
+ taskCommentSchema = {
40784
+ type: "object",
40785
+ required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
40786
+ properties: {
40787
+ id: { type: "string" },
40788
+ task_id: { type: "string" },
40789
+ agent_id: { type: "string", nullable: true },
40790
+ session_id: { type: "string", nullable: true },
40791
+ content: { type: "string" },
40792
+ type: { type: "string", enum: ["comment", "progress", "note"] },
40793
+ progress_pct: { type: "number", nullable: true },
40794
+ created_at: { type: "string", format: "date-time" }
40795
+ }
40796
+ };
40381
40797
  });
40382
40798
 
40383
40799
  // src/server/v1.ts
@@ -40407,6 +40823,29 @@ function contextFromPrincipal(principal, body) {
40407
40823
  const agentId = body?.agent_id || principal.agent || undefined;
40408
40824
  return agentId ? { agentId } : {};
40409
40825
  }
40826
+ function redactComment3(comment) {
40827
+ return { ...comment, content: redactEvidenceText(comment.content) };
40828
+ }
40829
+ function encodeCommentCursor(comment) {
40830
+ return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
40831
+ }
40832
+ function decodeCommentCursor(value) {
40833
+ if (value.length > 1024)
40834
+ throw new Error("invalid comment cursor");
40835
+ let parsed;
40836
+ try {
40837
+ parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
40838
+ } catch {
40839
+ throw new Error("invalid comment cursor");
40840
+ }
40841
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
40842
+ throw new Error("invalid comment cursor");
40843
+ const cursor = parsed;
40844
+ if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
40845
+ throw new Error("invalid comment cursor");
40846
+ }
40847
+ return { created_at: cursor["created_at"], id: cursor["id"] };
40848
+ }
40410
40849
  function normalizeImportSnapshot(raw) {
40411
40850
  const body = raw && typeof raw === "object" ? raw : {};
40412
40851
  const arr = (v) => Array.isArray(v) ? v : [];
@@ -40427,7 +40866,7 @@ function normalizeImportSnapshot(raw) {
40427
40866
  function countSnapshotRecords(s) {
40428
40867
  return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
40429
40868
  }
40430
- async function handleV1Request(req, url) {
40869
+ async function handleV1Request(req, url, dependencies = {}) {
40431
40870
  const path = url.pathname;
40432
40871
  if (path !== "/v1" && !path.startsWith("/v1/"))
40433
40872
  return null;
@@ -40436,7 +40875,7 @@ async function handleV1Request(req, url) {
40436
40875
  const requiredScopes = [isWrite ? "todos:write" : "todos:read"];
40437
40876
  let verifier;
40438
40877
  try {
40439
- verifier = getCloudVerifier();
40878
+ verifier = (dependencies.getVerifier ?? getCloudVerifier)();
40440
40879
  } catch (e) {
40441
40880
  return error(503, e.message);
40442
40881
  }
@@ -40445,8 +40884,8 @@ async function handleV1Request(req, url) {
40445
40884
  return error(decision.status, decision.message, { reason: decision.reason });
40446
40885
  }
40447
40886
  const principal = decision.principal;
40448
- await ensureCloudSchema();
40449
- const store = getCloudStorageAdapter();
40887
+ await (dependencies.ensureSchema ?? ensureCloudSchema)();
40888
+ const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
40450
40889
  const segments = path.split("/").filter(Boolean);
40451
40890
  const resource = segments[1];
40452
40891
  const id = segments[2];
@@ -40532,10 +40971,16 @@ async function handleV1Request(req, url) {
40532
40971
  if (!id) {
40533
40972
  if (method === "GET") {
40534
40973
  const filter = {
40535
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
40536
- ...url.searchParams.get("priority") ? { priority: url.searchParams.get("priority") } : {},
40974
+ ...url.searchParams.get("status") ? {
40975
+ status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
40976
+ } : {},
40977
+ ...url.searchParams.get("priority") ? {
40978
+ priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
40979
+ } : {},
40537
40980
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
40981
+ ...url.searchParams.has("parent_id") ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : {},
40538
40982
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
40983
+ ...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
40539
40984
  ...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
40540
40985
  ...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
40541
40986
  ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
@@ -40559,8 +41004,47 @@ async function handleV1Request(req, url) {
40559
41004
  if (action) {
40560
41005
  if (action === "comments") {
40561
41006
  if (method === "GET") {
40562
- const comments = await store.audit.getComments(id);
40563
- return json2({ comments, count: comments.length });
41007
+ if (!await store.tasks.get(id))
41008
+ return error(404, "task not found");
41009
+ const rawLimit = url.searchParams.get("limit");
41010
+ const cursor = url.searchParams.get("cursor");
41011
+ if (rawLimit === null && cursor === null) {
41012
+ const storageContext = contextFromPrincipal(principal);
41013
+ const legacyPage = (await (store.audit.getCommentsPage ? store.audit.getCommentsPage(id, { limit: LEGACY_COMMENT_RESPONSE_LIMIT + 1 }, storageContext) : store.audit.getComments(id, storageContext))).map(redactComment3);
41014
+ if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
41015
+ return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
41016
+ }
41017
+ return json2({
41018
+ comments: legacyPage,
41019
+ count: legacyPage.length,
41020
+ has_more: false,
41021
+ next_cursor: null
41022
+ });
41023
+ }
41024
+ const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
41025
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
41026
+ return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
41027
+ }
41028
+ let before;
41029
+ if (cursor) {
41030
+ try {
41031
+ before = decodeCommentCursor(cursor);
41032
+ } catch {
41033
+ return error(400, "invalid comment cursor");
41034
+ }
41035
+ }
41036
+ if (!store.audit.getCommentsPage) {
41037
+ return error(426, "storage adapter must be upgraded to support cursor-paginated comments");
41038
+ }
41039
+ const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
41040
+ const hasMore = page.length > limit;
41041
+ const comments = hasMore ? page.slice(1) : page;
41042
+ return json2({
41043
+ comments,
41044
+ count: comments.length,
41045
+ has_more: hasMore,
41046
+ next_cursor: hasMore && comments[0] ? encodeCommentCursor(comments[0]) : null
41047
+ });
40564
41048
  }
40565
41049
  if (method === "POST") {
40566
41050
  const body2 = await readJson(req) ?? {};
@@ -40578,7 +41062,7 @@ async function handleV1Request(req, url) {
40578
41062
  type: body2.type,
40579
41063
  progress_pct: body2.progress_pct
40580
41064
  }, contextFromPrincipal(principal, body2));
40581
- return json2({ comment }, 201);
41065
+ return json2({ comment: redactComment3(comment) }, 201);
40582
41066
  }
40583
41067
  return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
40584
41068
  }
@@ -40593,17 +41077,30 @@ async function handleV1Request(req, url) {
40593
41077
  if (action === "lock" || action === "unlock") {
40594
41078
  if (method !== "POST")
40595
41079
  return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
40596
- if (typeof store.tasks.lock !== "function" || typeof store.tasks.unlock !== "function") {
40597
- return error(501, "task locking is not supported by this storage backend");
40598
- }
40599
41080
  const body2 = await readJson(req) ?? {};
40600
41081
  if (!await store.tasks.get(id))
40601
41082
  return error(404, "task not found");
40602
41083
  if (action === "lock") {
40603
- const agentId2 = body2.agent_id || principal.agent || "todos-serve";
40604
- return json2({ result: await store.tasks.lock(id, agentId2) });
41084
+ if (typeof store.tasks.lock !== "function")
41085
+ return error(501, "task locking is not supported by this storage backend");
41086
+ const agentId3 = body2.agent_id || principal.agent || "todos-serve";
41087
+ return json2({ result: await store.tasks.lock(id, agentId3) });
40605
41088
  }
40606
- const released = await store.tasks.unlock(id, body2.agent_id || principal.agent || undefined);
41089
+ if (typeof store.tasks.unlock !== "function")
41090
+ return error(501, "task unlocking is not supported by this storage backend");
41091
+ if (body2.force === true) {
41092
+ if (!principal.scopes.includes("todos:*"))
41093
+ return error(403, "force unlock requires todos:* scope");
41094
+ const released2 = await store.tasks.unlock(id);
41095
+ return json2({ success: released2 });
41096
+ }
41097
+ if (body2.agent_id && principal.agent && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
41098
+ return error(403, "unlock agent_id must match the authenticated agent");
41099
+ }
41100
+ const agentId2 = principal.agent || body2.agent_id;
41101
+ if (!agentId2)
41102
+ return error(403, "unlock requires an agent-bound key or force=true");
41103
+ const released = await store.tasks.unlock(id, agentId2);
40607
41104
  return json2({ success: released });
40608
41105
  }
40609
41106
  if (action === "dependencies") {
@@ -40886,12 +41383,28 @@ async function handleV1Request(req, url) {
40886
41383
  const activity = await store.audit.getRecentActivity(limit);
40887
41384
  return json2({ activity, count: activity.length });
40888
41385
  }
40889
- if (resource === "task-lists" && !id) {
40890
- if (method !== "GET")
40891
- return error(405, `method ${method} not allowed on /v1/task-lists`);
40892
- const projectId = url.searchParams.get("project_id") ?? undefined;
40893
- const taskLists = await store.taskLists.list(projectId);
40894
- return json2({ task_lists: taskLists, count: taskLists.length });
41386
+ if (resource === "task-lists") {
41387
+ if (!id && method === "GET") {
41388
+ const projectId = url.searchParams.get("project_id") ?? undefined;
41389
+ const taskLists = await store.taskLists.list(projectId);
41390
+ return json2({ task_lists: taskLists, count: taskLists.length });
41391
+ }
41392
+ if (!id && method === "POST") {
41393
+ const body = await readJson(req);
41394
+ if (!body || typeof body.name !== "string" || !body.name.trim())
41395
+ return error(400, "name is required");
41396
+ const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
41397
+ return json2({ task_list: taskList }, 201);
41398
+ }
41399
+ if (id && method === "GET") {
41400
+ const taskList = await store.taskLists.get(id);
41401
+ return taskList ? json2({ task_list: taskList }) : error(404, "task list not found");
41402
+ }
41403
+ if (id && method === "DELETE") {
41404
+ const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
41405
+ return deleted ? json2({ deleted: true, id }) : error(404, "task list not found");
41406
+ }
41407
+ return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
40895
41408
  }
40896
41409
  if (resource === "dependencies" && !id) {
40897
41410
  if (method !== "GET")
@@ -40899,8 +41412,8 @@ async function handleV1Request(req, url) {
40899
41412
  if (typeof store.dependencies?.listAll !== "function") {
40900
41413
  return error(501, "dependency edge listing is not supported by this storage backend");
40901
41414
  }
40902
- const dependencies = await store.dependencies.listAll();
40903
- return json2({ dependencies, count: dependencies.length });
41415
+ const dependencies2 = await store.dependencies.listAll();
41416
+ return json2({ dependencies: dependencies2, count: dependencies2.length });
40904
41417
  }
40905
41418
  if (resource === "commits" && id) {
40906
41419
  if (method !== "GET")
@@ -40957,12 +41470,16 @@ async function handleV1Request(req, url) {
40957
41470
  }
40958
41471
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
40959
41472
  } catch (e) {
41473
+ if (e instanceof LockError)
41474
+ return error(409, e.message, { code: LockError.code });
40960
41475
  return error(500, e.message || "internal error");
40961
41476
  }
40962
41477
  }
40963
- var JSON_HEADERS;
41478
+ var JSON_HEADERS, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
40964
41479
  var init_v1 = __esm(() => {
41480
+ init_types();
40965
41481
  init_cloud();
41482
+ init_redaction();
40966
41483
  JSON_HEADERS = { "Content-Type": "application/json" };
40967
41484
  });
40968
41485
 
@@ -47197,7 +47714,7 @@ var init_workflow_states = __esm(() => {
47197
47714
  init_tasks();
47198
47715
  init_types();
47199
47716
  init_database();
47200
- init_config2();
47717
+ init_config();
47201
47718
  init_local_fields();
47202
47719
  DEFAULT_WORKFLOW_STATES = [
47203
47720
  { name: "pending", canonical_status: "pending", aliases: ["todo", "backlog"], transitions: null, terminal: false },
@@ -48362,7 +48879,7 @@ var init_roadmaps = __esm(() => {
48362
48879
  init_tasks();
48363
48880
  init_plans();
48364
48881
  init_task_runs();
48365
- init_config2();
48882
+ init_config();
48366
48883
  });
48367
48884
 
48368
48885
  // src/lib/capacity-forecasts.ts
@@ -48587,7 +49104,7 @@ var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
48587
49104
  var init_capacity_forecasts = __esm(() => {
48588
49105
  init_tasks();
48589
49106
  init_task_relations();
48590
- init_config2();
49107
+ init_config();
48591
49108
  });
48592
49109
 
48593
49110
  // src/lib/audit-ledger.ts
@@ -48879,7 +49396,7 @@ var LOCAL_AUDIT_LEDGER_SCHEMA_VERSION = 1, LOCAL_AUDIT_LEDGER_HASH_ALGORITHM = "
48879
49396
  var init_audit_ledger = __esm(() => {
48880
49397
  init_database();
48881
49398
  init_task_runs();
48882
- init_config2();
49399
+ init_config();
48883
49400
  init_redaction();
48884
49401
  LOCAL_AUDIT_LEDGER_INITIAL_HASH = "0".repeat(64);
48885
49402
  });
@@ -54760,7 +55277,7 @@ var init_agent_run_dispatcher = __esm(() => {
54760
55277
  init_database();
54761
55278
  init_redaction();
54762
55279
  init_runner_sandbox();
54763
- init_config2();
55280
+ init_config();
54764
55281
  });
54765
55282
 
54766
55283
  // src/lib/verification-providers.ts
@@ -55105,7 +55622,7 @@ var init_verification_providers = __esm(() => {
55105
55622
  init_task_commits();
55106
55623
  init_database();
55107
55624
  init_tasks();
55108
- init_config2();
55625
+ init_config();
55109
55626
  init_redaction();
55110
55627
  DEFAULT_RETRY = {
55111
55628
  attempts: 1,
@@ -61931,7 +62448,7 @@ var init_review_queues = __esm(() => {
61931
62448
  init_audit();
61932
62449
  init_database();
61933
62450
  init_tasks();
61934
- init_config2();
62451
+ init_config();
61935
62452
  init_event_emission_safety();
61936
62453
  init_event_hooks();
61937
62454
  init_task_contracts();
@@ -63818,7 +64335,7 @@ ID: ${updated.id}${taskNote}`
63818
64335
  var init_agents2 = __esm(() => {
63819
64336
  init_zod();
63820
64337
  init_agents();
63821
- init_config2();
64338
+ init_config();
63822
64339
  init_database();
63823
64340
  init_cloud_router();
63824
64341
  });
@@ -64252,7 +64769,7 @@ function captureEnvironmentSnapshot(input = {}) {
64252
64769
  }
64253
64770
  function writeEnvironmentSnapshot(snapshot, outputPath) {
64254
64771
  const path = outputPath ? resolve20(outputPath) : join18(defaultSnapshotDir(), `${snapshot.id}.json`);
64255
- ensureDir2(dirname9(path));
64772
+ ensureDir(dirname9(path));
64256
64773
  writeJsonFile(path, snapshot);
64257
64774
  return path;
64258
64775
  }
@@ -67895,7 +68412,7 @@ Commit Links (${commitRows.length}):`));
67895
68412
  var init_config_serve_commands = __esm(() => {
67896
68413
  init_database();
67897
68414
  init_tasks();
67898
- init_config2();
68415
+ init_config();
67899
68416
  init_sync_utils();
67900
68417
  init_helpers();
67901
68418
  });
@@ -70101,7 +70618,7 @@ Findings`));
70101
70618
  checks.push({ name: "Migrations", ok: false, message: "Could not read migration version" });
70102
70619
  }
70103
70620
  try {
70104
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), exports_config2));
70621
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
70105
70622
  loadConfig2();
70106
70623
  checks.push({ name: "Config", ok: true, message: "Loaded successfully" });
70107
70624
  } catch (e) {
@@ -79532,7 +80049,7 @@ var init_factory = __esm(() => {
79532
80049
  init_postgres_adapter();
79533
80050
  init_shadow();
79534
80051
  init_cloud_client();
79535
- init_config();
80052
+ init_config2();
79536
80053
  });
79537
80054
 
79538
80055
  // src/storage/s3-artifacts.ts
@@ -79974,6 +80491,7 @@ __export(exports_storage, {
79974
80491
  signAwsV4Request: () => signAwsV4Request,
79975
80492
  registerShadowExitFlush: () => registerShadowExitFlush,
79976
80493
  postgresTodosSyncSchemaSql: () => postgresTodosSyncSchemaSql,
80494
+ postgresTodosCommentCursorIndexSql: () => postgresTodosCommentCursorIndexSql,
79977
80495
  planRunArtifactsS3Sync: () => planRunArtifactsS3Sync,
79978
80496
  parseStorageMode: () => parseStorageMode,
79979
80497
  maybeInstallShadowCapture: () => maybeInstallShadowCapture2,
@@ -79981,6 +80499,7 @@ __export(exports_storage, {
79981
80499
  loadStorageConfig: () => loadStorageConfig,
79982
80500
  isTodosShadowEnabled: () => isTodosShadowEnabled,
79983
80501
  isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
80502
+ isCommentRedactionBackfillComplete: () => isCommentRedactionBackfillComplete,
79984
80503
  installShadowOutboxSchema: () => installShadowOutboxSchema,
79985
80504
  importSqliteTodosStorageSnapshot: () => importSqliteTodosStorageSnapshot,
79986
80505
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
@@ -80008,6 +80527,7 @@ __export(exports_storage, {
80008
80527
  closeRuntimeShadowCloud: () => closeRuntimeShadowCloud,
80009
80528
  buildS3ObjectUrl: () => buildS3ObjectUrl,
80010
80529
  buildS3ObjectKey: () => buildS3ObjectKey,
80530
+ backfillPostgresCommentRedaction: () => backfillPostgresCommentRedaction,
80011
80531
  assertTodosShadowConfig: () => assertTodosShadowConfig,
80012
80532
  assertTodosRemoteStorageConfig: () => assertTodosRemoteStorageConfig,
80013
80533
  TodosShadowOutbox: () => TodosShadowOutbox,
@@ -80020,12 +80540,13 @@ __export(exports_storage, {
80020
80540
  PostgresTodosSyncStore: () => PostgresTodosSyncStore,
80021
80541
  DEFAULT_TODOS_POSTGRES_SYNC_TABLE: () => DEFAULT_TODOS_POSTGRES_SYNC_TABLE,
80022
80542
  DEFAULT_TODOS_POSTGRES_CURSOR_TABLE: () => DEFAULT_TODOS_POSTGRES_CURSOR_TABLE,
80543
+ COMMENT_REDACTION_BACKFILL_CONFIRMATION: () => COMMENT_REDACTION_BACKFILL_CONFIRMATION,
80023
80544
  CANONICAL_TODOS_RDS_RUNTIME_PATH: () => CANONICAL_TODOS_RDS_RUNTIME_PATH,
80024
80545
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
80025
80546
  CANONICAL_TODOS_RDS_CLUSTER: () => CANONICAL_TODOS_RDS_CLUSTER
80026
80547
  });
80027
80548
  var init_storage2 = __esm(() => {
80028
- init_config();
80549
+ init_config2();
80029
80550
  init_factory();
80030
80551
  init_shadow();
80031
80552
  init_shadow_outbox();
@@ -80034,6 +80555,7 @@ var init_storage2 = __esm(() => {
80034
80555
  init_hybrid();
80035
80556
  init_local_sqlite();
80036
80557
  init_sqlite_snapshot();
80558
+ init_comment_redaction_backfill();
80037
80559
  init_postgres_adapter();
80038
80560
  init_s3_artifacts();
80039
80561
  init_s3_artifact_sync();