@hasna/todos 0.11.73 → 0.11.75

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.
package/dist/cli/index.js CHANGED
@@ -12874,6 +12874,7 @@ __export(exports_task_routing, {
12874
12874
  setTaskWorkflowPointers: () => setTaskWorkflowPointers,
12875
12875
  getTaskRouteState: () => getTaskRouteState
12876
12876
  });
12877
+ import { existsSync as existsSync8, statSync as statSync3 } from "fs";
12877
12878
  function machineLocalPath(project, db) {
12878
12879
  const machineId = process.env["TODOS_MACHINE_ID"];
12879
12880
  if (!machineId)
@@ -12911,7 +12912,33 @@ function routeConcurrencyKey(task, project, taskList, projectPath) {
12911
12912
  return `path:${projectPath}`;
12912
12913
  return `task:${task.id}`;
12913
12914
  }
12914
- function getTaskRouteState(taskOrId, db) {
12915
+ function directoryExists(path) {
12916
+ try {
12917
+ return existsSync8(path) && statSync3(path).isDirectory();
12918
+ } catch {
12919
+ return false;
12920
+ }
12921
+ }
12922
+ function classifyRoute(input) {
12923
+ if (input.terminal)
12924
+ return "terminal";
12925
+ if (input.notPending)
12926
+ return "in_progress";
12927
+ if (input.blocked)
12928
+ return "blocked";
12929
+ if (input.locked)
12930
+ return "locked";
12931
+ if (input.missingProjectRoot)
12932
+ return "missing_metadata";
12933
+ if (!input.eligible)
12934
+ return "unroutable";
12935
+ if (input.workflowPointerActive)
12936
+ return "deduped_active";
12937
+ if (input.workflowPointerTerminal)
12938
+ return "terminal_requeue_needed";
12939
+ return "eligible";
12940
+ }
12941
+ function getTaskRouteState(taskOrId, db, options = {}) {
12915
12942
  const d = db || getDatabase();
12916
12943
  const task = typeof taskOrId === "string" ? getTask(taskOrId, d) : taskOrId;
12917
12944
  if (!task)
@@ -12919,8 +12946,10 @@ function getTaskRouteState(taskOrId, db) {
12919
12946
  const { project, projectPath } = resolveProject(task, d);
12920
12947
  const taskList = resolveTaskList(task, project, d);
12921
12948
  const automation = routingAutomationMetadata(task, taskList) ?? {};
12922
- const routeEnabled = routeEnabledForTask(task, taskList) === true;
12949
+ const explicitRouteEnabled = routeEnabledForTask(task, taskList);
12923
12950
  const tagOptIn = task.tags.includes("auto:route") || task.tags.includes("route:enabled");
12951
+ const routeEnabled = explicitRouteEnabled === undefined ? tagOptIn : explicitRouteEnabled;
12952
+ const tagNoAuto = task.tags.includes("no-auto") || task.tags.includes("noauto") || task.tags.includes("no:auto");
12924
12953
  const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
12925
12954
  const locked = Boolean(task.locked_by && !isLockExpired(task.locked_at));
12926
12955
  const blockers = getBlockingDeps(task.id, d);
@@ -12929,10 +12958,21 @@ function getTaskRouteState(taskOrId, db) {
12929
12958
  const requiresApproval = automation.requires_approval === true || task.requires_approval === true;
12930
12959
  const approvalRequired = automation.approval_required === true;
12931
12960
  const approved = Boolean(task.approved_by);
12961
+ const pointers = workflowPointersFromMetadata(task.metadata);
12962
+ const workflowState = (pointers.workflow_state ?? "").trim().toLowerCase();
12963
+ const hasWorkflowPointer = Boolean(pointers.current_workflow_invocation_id || pointers.current_run_id || workflowState);
12964
+ const workflowPointerTerminal = hasWorkflowPointer && TERMINAL_WORKFLOW_STATES.has(workflowState);
12965
+ const workflowPointerActive = hasWorkflowPointer && !workflowPointerTerminal;
12966
+ const verifyProjectRoot = options.verifyProjectRoot === true;
12967
+ let projectRootExists = null;
12968
+ if (verifyProjectRoot) {
12969
+ projectRootExists = projectPath ? directoryExists(projectPath) : false;
12970
+ }
12971
+ const missingProjectRoot = verifyProjectRoot && projectRootExists !== true;
12932
12972
  const gates = {
12933
12973
  route_enabled: routeEnabled,
12934
12974
  tag_opt_in: tagOptIn,
12935
- no_auto: automation.no_auto === true,
12975
+ no_auto: automation.no_auto === true || tagNoAuto,
12936
12976
  manual: automation.manual === true,
12937
12977
  manual_required: automation.manual_required === true,
12938
12978
  requires_approval: requiresApproval,
@@ -12940,7 +12980,10 @@ function getTaskRouteState(taskOrId, db) {
12940
12980
  approved,
12941
12981
  locked,
12942
12982
  blocked,
12943
- terminal
12983
+ terminal,
12984
+ missing_project_root: missingProjectRoot,
12985
+ workflow_pointer_active: workflowPointerActive,
12986
+ workflow_pointer_terminal: workflowPointerTerminal
12944
12987
  };
12945
12988
  const reasons = [];
12946
12989
  if (task.status !== "pending")
@@ -12965,12 +13008,44 @@ function getTaskRouteState(taskOrId, db) {
12965
13008
  reasons.push("approval_required");
12966
13009
  if (automation.allowed === false)
12967
13010
  reasons.push("automation_disallowed");
13011
+ if (missingProjectRoot)
13012
+ reasons.push("missing_project_root");
13013
+ const eligible = reasons.length === 0;
13014
+ const staleAfterMs = options.staleInProgressAfterMs ?? DEFAULT_STALE_IN_PROGRESS_MS;
13015
+ const updatedAtMs = Date.parse(task.updated_at);
13016
+ const ageMs = Number.isNaN(updatedAtMs) ? 0 : Math.max(0, Date.now() - updatedAtMs);
13017
+ const evidence = {
13018
+ owner: task.assigned_to ?? task.locked_by ?? null,
13019
+ assigned_to: task.assigned_to ?? null,
13020
+ locked_by: task.locked_by ?? null,
13021
+ locked_at: task.locked_at ?? null,
13022
+ updated_at: task.updated_at,
13023
+ age_ms: ageMs,
13024
+ stale: task.status === "in_progress" && ageMs > staleAfterMs,
13025
+ stale_after_ms: staleAfterMs,
13026
+ current_run_id: pointers.current_run_id ?? null,
13027
+ current_workflow_invocation_id: pointers.current_workflow_invocation_id ?? null,
13028
+ workflow_state: pointers.workflow_state ?? null,
13029
+ project_root_verified: verifyProjectRoot,
13030
+ project_root_exists: projectRootExists
13031
+ };
13032
+ const route_class = classifyRoute({
13033
+ terminal,
13034
+ notPending: task.status !== "pending",
13035
+ blocked,
13036
+ locked,
13037
+ missingProjectRoot,
13038
+ eligible,
13039
+ workflowPointerActive,
13040
+ workflowPointerTerminal
13041
+ });
12968
13042
  return {
12969
13043
  schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
12970
13044
  task_id: task.id,
12971
13045
  task_short_id: task.short_id,
12972
13046
  status: task.status,
12973
- eligible: reasons.length === 0,
13047
+ eligible,
13048
+ route_class,
12974
13049
  reasons,
12975
13050
  blockers: blockers.map((blocker) => ({
12976
13051
  id: blocker.id,
@@ -12990,7 +13065,8 @@ function getTaskRouteState(taskOrId, db) {
12990
13065
  task_list_name: taskList?.name ?? null,
12991
13066
  concurrency_key: routeConcurrencyKey(task, project, taskList, projectPath)
12992
13067
  },
12993
- pointers: workflowPointersFromMetadata(task.metadata)
13068
+ pointers,
13069
+ evidence
12994
13070
  };
12995
13071
  }
12996
13072
  function setTaskWorkflowPointers(taskId, input, db) {
@@ -13036,12 +13112,28 @@ function pointerPatch(previous, input, key) {
13036
13112
  return previous;
13037
13113
  return typeof value === "string" && value.trim() ? value : undefined;
13038
13114
  }
13115
+ var DEFAULT_STALE_IN_PROGRESS_MS, TERMINAL_WORKFLOW_STATES;
13039
13116
  var init_task_routing = __esm(() => {
13040
13117
  init_database();
13041
13118
  init_projects();
13042
13119
  init_task_crud();
13043
13120
  init_task_lifecycle();
13044
13121
  init_task_lists();
13122
+ DEFAULT_STALE_IN_PROGRESS_MS = 3 * 24 * 60 * 60 * 1000;
13123
+ TERMINAL_WORKFLOW_STATES = new Set([
13124
+ "failed",
13125
+ "cancelled",
13126
+ "canceled",
13127
+ "completed",
13128
+ "complete",
13129
+ "done",
13130
+ "error",
13131
+ "errored",
13132
+ "timeout",
13133
+ "timed_out",
13134
+ "aborted",
13135
+ "superseded"
13136
+ ]);
13045
13137
  });
13046
13138
 
13047
13139
  // src/cli/commands/task-commands.ts
@@ -13185,6 +13277,19 @@ function buildExpectationMetadata(opts) {
13185
13277
  metadata["acceptance"] = parseJsonValue(String(acceptance));
13186
13278
  return metadata;
13187
13279
  }
13280
+ function resolveTaskListRef(ref, projectId) {
13281
+ const db = getDatabase();
13282
+ const exact = getTaskList(ref, db);
13283
+ if (exact)
13284
+ return { id: exact.id };
13285
+ const partial = resolvePartialId(db, "task_lists", ref);
13286
+ if (partial)
13287
+ return { id: partial };
13288
+ const bySlug = getTaskListBySlug(ref, projectId ?? undefined, db);
13289
+ if (bySlug)
13290
+ return { id: bySlug.id };
13291
+ return { error: `Could not resolve task list "${ref}" to a UUID${projectId ? " within the task's project" : ""}. Pass an exact task-list UUID.` };
13292
+ }
13188
13293
  function registerTaskCommands(program2) {
13189
13294
  program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").action((title, opts) => {
13190
13295
  const globalOpts = program2.opts();
@@ -13277,13 +13382,13 @@ function registerTaskCommands(program2) {
13277
13382
  console.log(formatTaskLine(result.task));
13278
13383
  }
13279
13384
  });
13280
- task.command("route-state <id>").description("Show deterministic routing eligibility and workflow pointers for a task").action(async (id) => {
13385
+ task.command("route-state <id>").description("Show deterministic routing eligibility and workflow pointers for a task").option("--verify-project-root", "Filesystem-check the resolved project root and surface missing_project_root before admission").action(async (id, opts) => {
13281
13386
  const globalOpts = program2.opts();
13282
13387
  const resolvedId = resolveTaskId(id);
13283
13388
  const { getTaskRouteState: getTaskRouteState2 } = await Promise.resolve().then(() => (init_task_routing(), exports_task_routing));
13284
13389
  let state;
13285
13390
  try {
13286
- state = getTaskRouteState2(resolvedId);
13391
+ state = getTaskRouteState2(resolvedId, undefined, { verifyProjectRoot: Boolean(opts.verifyProjectRoot) });
13287
13392
  } catch (e) {
13288
13393
  handleError(e);
13289
13394
  }
@@ -13294,8 +13399,12 @@ function registerTaskCommands(program2) {
13294
13399
  console.log(chalk2.bold("Task route state"));
13295
13400
  console.log(` ${chalk2.dim("Task:")} ${state.task_short_id || state.task_id.slice(0, 8)}`);
13296
13401
  console.log(` ${chalk2.dim("Eligible:")} ${state.eligible ? chalk2.green("yes") : chalk2.yellow("no")}`);
13402
+ console.log(` ${chalk2.dim("Class:")} ${state.route_class}`);
13297
13403
  console.log(` ${chalk2.dim("Reasons:")} ${state.reasons.length > 0 ? state.reasons.join(", ") : "none"}`);
13298
13404
  console.log(` ${chalk2.dim("Route:")} ${state.route.concurrency_key}`);
13405
+ if (state.evidence.owner) {
13406
+ console.log(` ${chalk2.dim("Owner:")} ${state.evidence.owner}${state.evidence.stale ? chalk2.yellow(" (stale)") : ""}`);
13407
+ }
13299
13408
  if (state.pointers.current_workflow_invocation_id) {
13300
13409
  console.log(` ${chalk2.dim("Invocation:")} ${state.pointers.current_workflow_invocation_id}`);
13301
13410
  }
@@ -13734,7 +13843,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13734
13843
  console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
13735
13844
  }
13736
13845
  });
13737
- program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list").option("--task-list <id>", "Move to a task list (alias for --list)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").option("--clear-approval", "Remove the approval requirement").action((id, opts) => {
13846
+ program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list (UUID authoritative; project-scoped slug accepted)").option("--task-list <id>", "Move to a task list (alias for --list)").option("--clear-list", "Detach from its task list (reset task_list_id to null)").option("--working-dir <path>", "Repair the task's working_dir to a specific path (routing metadata)").option("--clear-working-dir", "Reset the task's working_dir to null (undo path for routing repairs)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").option("--clear-approval", "Remove the approval requirement").action((id, opts) => {
13738
13847
  const globalOpts = program2.opts();
13739
13848
  opts.tags = opts.tags || opts.tag;
13740
13849
  opts.list = opts.list || opts.taskList;
@@ -13752,15 +13861,22 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13752
13861
  console.error(chalk2.red("Use either --approval or --clear-approval, not both."));
13753
13862
  process.exit(1);
13754
13863
  }
13864
+ if (opts.list && opts.clearList) {
13865
+ console.error(chalk2.red("Use either --list or --clear-list, not both."));
13866
+ process.exit(1);
13867
+ }
13868
+ if (opts.workingDir !== undefined && opts.clearWorkingDir) {
13869
+ console.error(chalk2.red("Use either --working-dir or --clear-working-dir, not both."));
13870
+ process.exit(1);
13871
+ }
13755
13872
  const taskListId = opts.list ? (() => {
13756
- const db = getDatabase();
13757
- const resolved = resolvePartialId(db, "task_lists", opts.list);
13758
- if (!resolved) {
13759
- console.error(chalk2.red(`Could not resolve task list ID: ${opts.list}`));
13873
+ const resolved = resolveTaskListRef(opts.list, current.project_id);
13874
+ if ("error" in resolved) {
13875
+ console.error(chalk2.red(resolved.error));
13760
13876
  process.exit(1);
13761
13877
  }
13762
- return resolved;
13763
- })() : undefined;
13878
+ return resolved.id;
13879
+ })() : opts.clearList ? null : undefined;
13764
13880
  const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
13765
13881
  let task2;
13766
13882
  try {
@@ -13774,6 +13890,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13774
13890
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
13775
13891
  plan_id: planId,
13776
13892
  task_list_id: taskListId,
13893
+ working_dir: opts.workingDir ? resolve9(opts.workingDir) : opts.clearWorkingDir ? null : undefined,
13777
13894
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
13778
13895
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
13779
13896
  due_at: opts.due !== undefined ? opts.due === "" ? null : opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
@@ -13973,12 +14090,13 @@ var init_task_commands = __esm(() => {
13973
14090
  init_database();
13974
14091
  init_projects();
13975
14092
  init_tasks();
14093
+ init_task_lists();
13976
14094
  init_helpers();
13977
14095
  init_types();
13978
14096
  });
13979
14097
 
13980
14098
  // src/lib/plan-artifacts.ts
13981
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
14099
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13982
14100
  import { join as join7, resolve as resolve10 } from "path";
13983
14101
  function assertSafePathSegment(value, label) {
13984
14102
  const trimmed = value.trim();
@@ -14214,7 +14332,7 @@ function readPlanArtifact(plan, db) {
14214
14332
  return null;
14215
14333
  const d = db || getDatabase();
14216
14334
  const paths = resolvePlanArtifactCandidatePaths(plan, d);
14217
- const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
14335
+ const path = existsSync9(paths.primary.file_path) ? paths.primary.file_path : existsSync9(paths.legacy.file_path) ? paths.legacy.file_path : null;
14218
14336
  if (!path)
14219
14337
  return null;
14220
14338
  const markdown = readFileSync4(path, "utf8");
@@ -14229,7 +14347,7 @@ function inspectPlanArtifact(plan, db) {
14229
14347
  return null;
14230
14348
  const d = db || getDatabase();
14231
14349
  const paths = resolvePlanArtifactCandidatePaths(plan, d);
14232
- const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
14350
+ const path = existsSync9(paths.primary.file_path) ? paths.primary.file_path : existsSync9(paths.legacy.file_path) ? paths.legacy.file_path : null;
14233
14351
  if (!path) {
14234
14352
  return {
14235
14353
  path: paths.primary.file_path,
@@ -15705,7 +15823,7 @@ var init_saved_search_views = __esm(() => {
15705
15823
  });
15706
15824
 
15707
15825
  // src/lib/claude-tasks.ts
15708
- import { existsSync as existsSync9, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
15826
+ import { existsSync as existsSync10, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
15709
15827
  import { join as join9 } from "path";
15710
15828
  function getTaskListDir(taskListId) {
15711
15829
  return join9(HOME, ".claude", "tasks", taskListId);
@@ -15727,7 +15845,7 @@ function toSqliteStatus(status) {
15727
15845
  }
15728
15846
  function readPrefixCounter(dir) {
15729
15847
  const path = join9(dir, ".prefix-counter");
15730
- if (!existsSync9(path))
15848
+ if (!existsSync10(path))
15731
15849
  return 0;
15732
15850
  const val = parseInt(readFileSync5(path, "utf-8").trim(), 10);
15733
15851
  return isNaN(val) ? 0 : val;
@@ -15760,7 +15878,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
15760
15878
  }
15761
15879
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
15762
15880
  const dir = getTaskListDir(taskListId);
15763
- if (!existsSync9(dir))
15881
+ if (!existsSync10(dir))
15764
15882
  ensureDir2(dir);
15765
15883
  const filter = {};
15766
15884
  if (projectId)
@@ -15856,7 +15974,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
15856
15974
  }
15857
15975
  function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
15858
15976
  const dir = getTaskListDir(taskListId);
15859
- if (!existsSync9(dir)) {
15977
+ if (!existsSync10(dir)) {
15860
15978
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
15861
15979
  }
15862
15980
  const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
@@ -15949,7 +16067,7 @@ var init_claude_tasks = __esm(() => {
15949
16067
  });
15950
16068
 
15951
16069
  // src/lib/agent-tasks.ts
15952
- import { existsSync as existsSync10 } from "fs";
16070
+ import { existsSync as existsSync11 } from "fs";
15953
16071
  import { join as join10 } from "path";
15954
16072
  function agentBaseDir(agent) {
15955
16073
  const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
@@ -15987,7 +16105,7 @@ function metadataKey(agent) {
15987
16105
  }
15988
16106
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
15989
16107
  const dir = getTaskListDir2(agent, taskListId);
15990
- if (!existsSync10(dir))
16108
+ if (!existsSync11(dir))
15991
16109
  ensureDir2(dir);
15992
16110
  const filter = {};
15993
16111
  if (projectId)
@@ -16070,7 +16188,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
16070
16188
  }
16071
16189
  function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
16072
16190
  const dir = getTaskListDir2(agent, taskListId);
16073
- if (!existsSync10(dir)) {
16191
+ if (!existsSync11(dir)) {
16074
16192
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
16075
16193
  }
16076
16194
  const files = listJsonFiles(dir);
@@ -16242,11 +16360,11 @@ __export(exports_project_bootstrap, {
16242
16360
  discoverProjectWorkspace: () => discoverProjectWorkspace,
16243
16361
  bootstrapProject: () => bootstrapProject
16244
16362
  });
16245
- import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
16363
+ import { existsSync as existsSync12, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
16246
16364
  import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
16247
16365
  function safeStat(path) {
16248
16366
  try {
16249
- return statSync3(path);
16367
+ return statSync4(path);
16250
16368
  } catch {
16251
16369
  return null;
16252
16370
  }
@@ -16261,7 +16379,7 @@ function canonicalPath(input) {
16261
16379
  function findUp(start, marker) {
16262
16380
  let current = canonicalPath(start);
16263
16381
  while (true) {
16264
- if (existsSync11(resolve11(current, marker)))
16382
+ if (existsSync12(resolve11(current, marker)))
16265
16383
  return current;
16266
16384
  const parent = dirname6(current);
16267
16385
  if (parent === current)
@@ -16273,7 +16391,7 @@ function readPackageJson(path) {
16273
16391
  if (!path)
16274
16392
  return null;
16275
16393
  const file = resolve11(path, "package.json");
16276
- if (!existsSync11(file))
16394
+ if (!existsSync12(file))
16277
16395
  return null;
16278
16396
  try {
16279
16397
  const parsed = JSON.parse(readFileSync6(file, "utf-8"));
@@ -16295,7 +16413,7 @@ function workspaceMarker(root, rootPackage) {
16295
16413
  if (rootPackage?.workspaces)
16296
16414
  markers.push("package.json#workspaces");
16297
16415
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
16298
- if (existsSync11(resolve11(root, marker)))
16416
+ if (existsSync12(resolve11(root, marker)))
16299
16417
  markers.push(marker);
16300
16418
  }
16301
16419
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -21818,7 +21936,7 @@ __export(exports_extract, {
21818
21936
  buildCodebaseIndex: () => buildCodebaseIndex,
21819
21937
  EXTRACT_TAGS: () => EXTRACT_TAGS
21820
21938
  });
21821
- import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
21939
+ import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
21822
21940
  import { createHash as createHash3 } from "crypto";
21823
21941
  import { relative as relative3, resolve as resolve12, join as join11 } from "path";
21824
21942
  function stableHash(value) {
@@ -21828,9 +21946,9 @@ function normalizePathForMatch(value) {
21828
21946
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
21829
21947
  }
21830
21948
  function readGitignorePatterns(basePath) {
21831
- const root = statSync4(basePath).isFile() ? resolve12(basePath, "..") : basePath;
21949
+ const root = statSync5(basePath).isFile() ? resolve12(basePath, "..") : basePath;
21832
21950
  const gitignorePath = join11(root, ".gitignore");
21833
- if (!existsSync12(gitignorePath))
21951
+ if (!existsSync13(gitignorePath))
21834
21952
  return [];
21835
21953
  try {
21836
21954
  return readFileSync7(gitignorePath, "utf-8").split(`
@@ -21939,7 +22057,7 @@ function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
21939
22057
  return results;
21940
22058
  }
21941
22059
  function collectFiles(basePath, extensions, excludes, respectGitignore) {
21942
- const stat = statSync4(basePath);
22060
+ const stat = statSync5(basePath);
21943
22061
  if (stat.isFile()) {
21944
22062
  return [basePath];
21945
22063
  }
@@ -21972,10 +22090,10 @@ function buildCodebaseIndex(options) {
21972
22090
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
21973
22091
  const indexed = [];
21974
22092
  for (const file of files) {
21975
- const fullPath = statSync4(basePath).isFile() ? basePath : join11(basePath, file);
22093
+ const fullPath = statSync5(basePath).isFile() ? basePath : join11(basePath, file);
21976
22094
  try {
21977
22095
  const source = readFileSync7(fullPath, "utf-8");
21978
- const relPath = statSync4(basePath).isFile() ? relative3(resolve12(basePath, ".."), fullPath) : file;
22096
+ const relPath = statSync5(basePath).isFile() ? relative3(resolve12(basePath, ".."), fullPath) : file;
21979
22097
  indexed.push({
21980
22098
  file: relPath,
21981
22099
  checksum: stableHash(source).slice(0, 24),
@@ -22003,10 +22121,10 @@ function extractTodos(options, db) {
22003
22121
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
22004
22122
  const allComments = [];
22005
22123
  for (const file of files) {
22006
- const fullPath = statSync4(basePath).isFile() ? basePath : join11(basePath, file);
22124
+ const fullPath = statSync5(basePath).isFile() ? basePath : join11(basePath, file);
22007
22125
  try {
22008
22126
  const source = readFileSync7(fullPath, "utf-8");
22009
- const relPath = statSync4(basePath).isFile() ? relative3(resolve12(basePath, ".."), fullPath) : file;
22127
+ const relPath = statSync5(basePath).isFile() ? relative3(resolve12(basePath, ".."), fullPath) : file;
22010
22128
  const comments = extractFromSource(source, relPath, tags);
22011
22129
  allComments.push(...comments);
22012
22130
  } catch {}
@@ -25133,7 +25251,7 @@ __export(exports_retention_cleanup, {
25133
25251
  applyRetentionCleanup: () => applyRetentionCleanup,
25134
25252
  RETENTION_CLEANUP_CONFIRMATION: () => RETENTION_CLEANUP_CONFIRMATION
25135
25253
  });
25136
- import { existsSync as existsSync13, unlinkSync } from "fs";
25254
+ import { existsSync as existsSync14, unlinkSync } from "fs";
25137
25255
  function normalizeScopes(scopes) {
25138
25256
  if (!scopes || scopes.length === 0)
25139
25257
  return [...ALL_SCOPES];
@@ -25336,7 +25454,7 @@ function applyRetentionCleanup(input, db) {
25336
25454
  for (const artifact of report.candidates.artifact_files) {
25337
25455
  try {
25338
25456
  const path = artifactStorePath(artifact.relative_path);
25339
- if (!existsSync13(path)) {
25457
+ if (!existsSync14(path)) {
25340
25458
  report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
25341
25459
  continue;
25342
25460
  }
@@ -26012,7 +26130,7 @@ __export(exports_local_extensions, {
26012
26130
  discoverLocalExtensions: () => discoverLocalExtensions
26013
26131
  });
26014
26132
  import { createHash as createHash5, createVerify } from "crypto";
26015
- import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
26133
+ import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
26016
26134
  import { basename as basename6, join as join12, resolve as resolve14 } from "path";
26017
26135
  function isObject(value) {
26018
26136
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -26272,10 +26390,10 @@ function verifyExtensionSignature(input) {
26272
26390
  }
26273
26391
  function inspectExtensionSource(source2) {
26274
26392
  const resolved = resolve14(source2);
26275
- if (!existsSync14(resolved))
26393
+ if (!existsSync15(resolved))
26276
26394
  throw new Error(`extension source not found: ${source2}`);
26277
- const stat = statSync5(resolved);
26278
- const manifestPath = stat.isDirectory() ? [join12(resolved, "todos.extension.json"), join12(resolved, "extension.json")].find(existsSync14) : resolved;
26395
+ const stat = statSync6(resolved);
26396
+ const manifestPath = stat.isDirectory() ? [join12(resolved, "todos.extension.json"), join12(resolved, "extension.json")].find(existsSync15) : resolved;
26279
26397
  if (!manifestPath)
26280
26398
  throw new Error(`extension directory ${source2} is missing todos.extension.json`);
26281
26399
  const raw = readFileSync8(manifestPath);
@@ -26375,16 +26493,16 @@ function projectExtensionSources(projectPath) {
26375
26493
  join12(root, ".todos", "todos.extension.json")
26376
26494
  ];
26377
26495
  const extensionDir = join12(root, ".todos", "extensions");
26378
- if (existsSync14(extensionDir)) {
26496
+ if (existsSync15(extensionDir)) {
26379
26497
  for (const entry of readdirSync3(extensionDir)) {
26380
26498
  if (entry.startsWith("."))
26381
26499
  continue;
26382
26500
  const full = join12(extensionDir, entry);
26383
- if (statSync5(full).isDirectory() || entry.endsWith(".json"))
26501
+ if (statSync6(full).isDirectory() || entry.endsWith(".json"))
26384
26502
  candidates.push(full);
26385
26503
  }
26386
26504
  }
26387
- return candidates.filter(existsSync14);
26505
+ return candidates.filter(existsSync15);
26388
26506
  }
26389
26507
  function discoverLocalExtensions(options = {}) {
26390
26508
  const config = loadConfig();
@@ -27684,7 +27802,7 @@ var exports_doctor = {};
27684
27802
  __export(exports_doctor, {
27685
27803
  runTodosDoctor: () => runTodosDoctor
27686
27804
  });
27687
- import { chmodSync, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync7, statSync as statSync6 } from "fs";
27805
+ import { chmodSync, copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync7, statSync as statSync7 } from "fs";
27688
27806
  import { basename as basename7, dirname as dirname7, join as join13 } from "path";
27689
27807
  function tableExists(db, table) {
27690
27808
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
@@ -27779,7 +27897,7 @@ function findMissingProjectRoots(db) {
27779
27897
  continue;
27780
27898
  if (!row.path.startsWith("/"))
27781
27899
  continue;
27782
- if (!existsSync15(row.path))
27900
+ if (!existsSync16(row.path))
27783
27901
  missing++;
27784
27902
  }
27785
27903
  return missing;
@@ -27831,7 +27949,7 @@ function databasePermissionsAreUnsafe(dbPath) {
27831
27949
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
27832
27950
  return false;
27833
27951
  try {
27834
- return (statSync6(dbPath).mode & 63) !== 0;
27952
+ return (statSync7(dbPath).mode & 63) !== 0;
27835
27953
  } catch {
27836
27954
  return false;
27837
27955
  }
@@ -27839,14 +27957,14 @@ function databasePermissionsAreUnsafe(dbPath) {
27839
27957
  function createBackup(dbPath) {
27840
27958
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
27841
27959
  return;
27842
- if (!existsSync15(dbPath))
27960
+ if (!existsSync16(dbPath))
27843
27961
  return;
27844
27962
  const stamp = now().replace(/[:.]/g, "-");
27845
27963
  const backupDir = join13(dirname7(dbPath), `${basename7(dbPath)}.backup-${stamp}`);
27846
27964
  const files = [];
27847
27965
  mkdirSync7(backupDir, { recursive: true });
27848
27966
  for (const source2 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
27849
- if (!existsSync15(source2))
27967
+ if (!existsSync16(source2))
27850
27968
  continue;
27851
27969
  const target = join13(backupDir, basename7(source2));
27852
27970
  copyFileSync(source2, target);
@@ -33872,7 +33990,7 @@ var exports_mention_resolver = {};
33872
33990
  __export(exports_mention_resolver, {
33873
33991
  resolveMentions: () => resolveMentions
33874
33992
  });
33875
- import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
33993
+ import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync8 } from "fs";
33876
33994
  import { basename as basename8, isAbsolute, join as join15, relative as relative5, resolve as resolve17, sep as sep3 } from "path";
33877
33995
  function blankResolution(parsed) {
33878
33996
  return {
@@ -33971,11 +34089,11 @@ function resolveFile(parsed, workspace) {
33971
34089
  return resolution;
33972
34090
  }
33973
34091
  resolution.path = relPath;
33974
- if (!existsSync16(absolutePath)) {
34092
+ if (!existsSync17(absolutePath)) {
33975
34093
  resolution.warnings.push("file does not exist in the local workspace");
33976
34094
  return resolution;
33977
34095
  }
33978
- const stats = statSync7(absolutePath);
34096
+ const stats = statSync8(absolutePath);
33979
34097
  if (!stats.isFile()) {
33980
34098
  resolution.warnings.push("path exists but is not a file");
33981
34099
  return resolution;
@@ -34013,7 +34131,7 @@ function walkSourceFiles(root, current = root, files = []) {
34013
34131
  if (!entry.isFile())
34014
34132
  continue;
34015
34133
  const extension = `.${basename8(entry.name).split(".").pop() || ""}`;
34016
- if (SOURCE_EXTENSIONS.has(extension) && statSync7(absolutePath).size <= 512 * 1024) {
34134
+ if (SOURCE_EXTENSIONS.has(extension) && statSync8(absolutePath).size <= 512 * 1024) {
34017
34135
  files.push(absolutePath);
34018
34136
  }
34019
34137
  }
@@ -42461,7 +42579,7 @@ __export(exports_verification_providers, {
42461
42579
  getVerificationRecord: () => getVerificationRecord,
42462
42580
  discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
42463
42581
  });
42464
- import { existsSync as existsSync17, readFileSync as readFileSync11 } from "fs";
42582
+ import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
42465
42583
  function normalizeName6(name) {
42466
42584
  const normalized = name.trim().toLowerCase();
42467
42585
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -42613,7 +42731,7 @@ Timed out after ${provider.timeout_ms}ms`);
42613
42731
  };
42614
42732
  }
42615
42733
  function runCiLogProvider(input) {
42616
- const text = input.log_text ?? (input.log_path && existsSync17(input.log_path) ? readFileSync11(input.log_path, "utf-8") : "");
42734
+ const text = input.log_text ?? (input.log_path && existsSync18(input.log_path) ? readFileSync11(input.log_path, "utf-8") : "");
42617
42735
  return {
42618
42736
  status: classifyLog(text),
42619
42737
  attempts: 1,
@@ -42625,7 +42743,7 @@ function runBrowserProvider(input) {
42625
42743
  if (!input.artifact_path) {
42626
42744
  return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
42627
42745
  }
42628
- if (!existsSync17(input.artifact_path)) {
42746
+ if (!existsSync18(input.artifact_path)) {
42629
42747
  return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
42630
42748
  }
42631
42749
  return {
@@ -51716,7 +51834,7 @@ __export(exports_environment_snapshots, {
51716
51834
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
51717
51835
  });
51718
51836
  import { createHash as createHash12 } from "crypto";
51719
- import { existsSync as existsSync18, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
51837
+ import { existsSync as existsSync19, readFileSync as readFileSync14, statSync as statSync9 } from "fs";
51720
51838
  import { hostname as hostname2, platform, arch } from "os";
51721
51839
  import { dirname as dirname9, join as join18, resolve as resolve20 } from "path";
51722
51840
  import { tmpdir as tmpdir3 } from "os";
@@ -51725,9 +51843,9 @@ function sha2566(value) {
51725
51843
  }
51726
51844
  function fileRecord(root, relativePath) {
51727
51845
  const path = join18(root, relativePath);
51728
- if (!existsSync18(path))
51846
+ if (!existsSync19(path))
51729
51847
  return null;
51730
- const stat = statSync8(path);
51848
+ const stat = statSync9(path);
51731
51849
  if (!stat.isFile())
51732
51850
  return null;
51733
51851
  const content = readFileSync14(path);
@@ -52428,7 +52546,7 @@ __export(exports_serve, {
52428
52546
  SECURITY_HEADERS: () => SECURITY_HEADERS,
52429
52547
  MIME_TYPES: () => MIME_TYPES
52430
52548
  });
52431
- import { existsSync as existsSync19 } from "fs";
52549
+ import { existsSync as existsSync20 } from "fs";
52432
52550
  import { join as join19, dirname as dirname10, extname } from "path";
52433
52551
  import { fileURLToPath as fileURLToPath2 } from "url";
52434
52552
  function resolveDashboardDir() {
@@ -52445,7 +52563,7 @@ function resolveDashboardDir() {
52445
52563
  }
52446
52564
  candidates.push(join19(process.cwd(), "dashboard", "dist"));
52447
52565
  for (const candidate of candidates) {
52448
- if (existsSync19(candidate))
52566
+ if (existsSync20(candidate))
52449
52567
  return candidate;
52450
52568
  }
52451
52569
  return join19(process.cwd(), "dashboard", "dist");
@@ -52507,7 +52625,7 @@ function json(data, status = 200, headers) {
52507
52625
  });
52508
52626
  }
52509
52627
  function serveStaticFile(filePath) {
52510
- if (!existsSync19(filePath))
52628
+ if (!existsSync20(filePath))
52511
52629
  return null;
52512
52630
  const ext = extname(filePath);
52513
52631
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -52588,7 +52706,7 @@ data: ${data}
52588
52706
  filteredSseClients.delete(client);
52589
52707
  }
52590
52708
  const dashboardDir = resolveDashboardDir();
52591
- const dashboardExists = existsSync19(dashboardDir);
52709
+ const dashboardExists = existsSync20(dashboardDir);
52592
52710
  if (!dashboardExists) {
52593
52711
  console.error(`
52594
52712
  Dashboard not found at: ${dashboardDir}`);
@@ -54421,7 +54539,7 @@ __export(exports_config_serve_commands, {
54421
54539
  registerConfigServeCommands: () => registerConfigServeCommands
54422
54540
  });
54423
54541
  import chalk6 from "chalk";
54424
- import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
54542
+ import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
54425
54543
  import { dirname as dirname11, join as join20 } from "path";
54426
54544
  function registerConfigServeCommands(program2) {
54427
54545
  program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
@@ -54463,7 +54581,7 @@ function registerConfigServeCommands(program2) {
54463
54581
  }
54464
54582
  obj[keys[keys.length - 1]] = parsedValue;
54465
54583
  const dir = dirname11(configPath);
54466
- if (!existsSync20(dir))
54584
+ if (!existsSync21(dir))
54467
54585
  mkdirSync10(dir, { recursive: true });
54468
54586
  writeFileSync8(configPath, JSON.stringify(config2, null, 2));
54469
54587
  if (globalOpts.json) {
@@ -55495,6 +55613,523 @@ var init_config_serve_commands = __esm(() => {
55495
55613
  init_helpers();
55496
55614
  });
55497
55615
 
55616
+ // src/lib/routing-doctor.ts
55617
+ var exports_routing_doctor = {};
55618
+ __export(exports_routing_doctor, {
55619
+ runRoutingDoctor: () => runRoutingDoctor,
55620
+ routingShardOf: () => routingShardOf,
55621
+ routingRepairUndoCommand: () => routingRepairUndoCommand,
55622
+ detectCrossRepoIntent: () => detectCrossRepoIntent,
55623
+ classifyTaskRouting: () => classifyTaskRouting,
55624
+ TODOS_ROUTING_DOCTOR_SCHEMA_VERSION: () => TODOS_ROUTING_DOCTOR_SCHEMA_VERSION
55625
+ });
55626
+ import { copyFileSync as copyFileSync2, existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
55627
+ import { basename as basename9, dirname as dirname12, join as join21 } from "path";
55628
+ function normalizePath5(path) {
55629
+ if (!path)
55630
+ return null;
55631
+ const trimmed = path.replace(/\/+$/, "");
55632
+ return trimmed === "" ? "/" : trimmed;
55633
+ }
55634
+ function looksLikeRemoteUri(path) {
55635
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(path);
55636
+ }
55637
+ function looksLikeLocalAbsolutePath(path) {
55638
+ return path.startsWith("/") && !looksLikeRemoteUri(path);
55639
+ }
55640
+ function looksLikeMacHomePath(path) {
55641
+ return /^\/Users\//.test(path);
55642
+ }
55643
+ function stableHash2(key) {
55644
+ let hash2 = 2166136261;
55645
+ for (let i = 0;i < key.length; i++) {
55646
+ hash2 ^= key.charCodeAt(i);
55647
+ hash2 = Math.imul(hash2, 16777619);
55648
+ }
55649
+ return hash2 >>> 0;
55650
+ }
55651
+ function routingShardOf(task2, total) {
55652
+ if (total <= 1)
55653
+ return 0;
55654
+ const key = task2.project_id ?? task2.id;
55655
+ return stableHash2(key) % total;
55656
+ }
55657
+ function repoSlugForProject(project) {
55658
+ const fromPath = project.path && looksLikeLocalAbsolutePath(project.path) ? basename9(normalizePath5(project.path) ?? "") : "";
55659
+ if (fromPath)
55660
+ return fromPath.toLowerCase();
55661
+ return project.name.trim().toLowerCase().replace(/\s+/g, "-");
55662
+ }
55663
+ function detectCrossRepoIntent(task2, project, knownRepoSlugs) {
55664
+ if (!project)
55665
+ return null;
55666
+ const ownSlug = repoSlugForProject(project);
55667
+ const named = new Set;
55668
+ for (const match of task2.title.matchAll(REPO_NAME_RE))
55669
+ named.add(match[0].toLowerCase());
55670
+ for (const tag of task2.tags) {
55671
+ const m = /^repo:(.+)$/i.exec(tag);
55672
+ if (m && m[1])
55673
+ named.add(m[1].trim().toLowerCase());
55674
+ }
55675
+ if (named.size === 0)
55676
+ return null;
55677
+ if (named.has(ownSlug))
55678
+ return null;
55679
+ for (const candidate of named) {
55680
+ if (candidate !== ownSlug && knownRepoSlugs.has(candidate))
55681
+ return candidate;
55682
+ }
55683
+ return null;
55684
+ }
55685
+ function bump(map, key) {
55686
+ map[key] = (map[key] ?? 0) + 1;
55687
+ }
55688
+ function resolveCanonicalTaskList(project, db) {
55689
+ if (!project?.task_list_id)
55690
+ return null;
55691
+ return getTaskList(project.task_list_id, db) ?? getTaskListBySlug(project.task_list_id, project.id, db);
55692
+ }
55693
+ function taskListState(task2, project, db) {
55694
+ if (!task2.task_list_id)
55695
+ return "null";
55696
+ const resolved = getTaskList(task2.task_list_id, db) ?? (project ? getTaskListBySlug(task2.task_list_id, project.id, db) : null);
55697
+ return resolved ? "ok" : "unresolvable";
55698
+ }
55699
+ function classifyTaskRouting(ctx) {
55700
+ return evaluateTaskRouting(ctx).findings;
55701
+ }
55702
+ function evaluateTaskRouting(ctx) {
55703
+ const { task: task2, project, db, knownRepoSlugs, verifyProjectRoot } = ctx;
55704
+ const rs = getTaskRouteState(task2, db, { verifyProjectRoot });
55705
+ const findings = [];
55706
+ const projectPath = normalizePath5(rs.route.project_path);
55707
+ const workingDir = task2.working_dir ? normalizePath5(task2.working_dir) : null;
55708
+ const projectRootExists = rs.evidence.project_root_exists;
55709
+ const routeOptIn = rs.gates.tag_opt_in || rs.gates.route_enabled;
55710
+ const crossRepoForeign = detectCrossRepoIntent(task2, project, knownRepoSlugs);
55711
+ const base = {
55712
+ task_id: task2.id,
55713
+ task_short_id: task2.short_id,
55714
+ title: task2.title,
55715
+ status: task2.status,
55716
+ project_id: rs.route.project_id,
55717
+ project_name: project?.name ?? null,
55718
+ project_path: rs.route.project_path,
55719
+ working_dir: task2.working_dir,
55720
+ task_list_id: task2.task_list_id,
55721
+ route_eligible: rs.eligible,
55722
+ route_class: rs.route_class,
55723
+ route_reasons: rs.reasons
55724
+ };
55725
+ const listState = taskListState(task2, project, db);
55726
+ const pathIsLocal = projectPath ? looksLikeLocalAbsolutePath(projectPath) : false;
55727
+ const pathInvalidOnMachine = verifyProjectRoot && pathIsLocal && projectRootExists === false;
55728
+ const projectMissingPath = project !== null && (!projectPath || pathIsLocal === false && !looksLikeRemoteUri(projectPath ?? ""));
55729
+ if (!project && !task2.project_id) {
55730
+ if (routeOptIn && !workingDir) {
55731
+ findings.push({
55732
+ ...base,
55733
+ category: "missing_project",
55734
+ severity: "warn",
55735
+ repair_class: "unsupported",
55736
+ expected_working_dir: null,
55737
+ task_list_state: listState,
55738
+ detail: "Route-opted-in task has no project and no working_dir; cannot derive an owning repo path.",
55739
+ suggested_repair: null
55740
+ });
55741
+ }
55742
+ } else if (pathInvalidOnMachine || projectMissingPath) {
55743
+ const macHome = projectPath ? looksLikeMacHomePath(projectPath) : false;
55744
+ findings.push({
55745
+ ...base,
55746
+ category: "invalid_project_path",
55747
+ severity: "warn",
55748
+ repair_class: "blocker_invalid_path",
55749
+ expected_working_dir: null,
55750
+ task_list_state: listState,
55751
+ detail: projectMissingPath ? "Owning project has no usable local/remote path." : `Owning project path does not exist on this machine${macHome ? " (macOS home path \u2014 likely valid on its home Mac)" : ""}. Reported, not rewritten.`,
55752
+ suggested_repair: null
55753
+ });
55754
+ }
55755
+ const haveGoodProjectPath = Boolean(projectPath && pathIsLocal && projectRootExists === true);
55756
+ if (project && projectPath) {
55757
+ if (!workingDir) {
55758
+ if (crossRepoForeign) {
55759
+ findings.push({
55760
+ ...base,
55761
+ category: "null_working_dir",
55762
+ severity: "warn",
55763
+ repair_class: "blocker_cross_repo",
55764
+ expected_working_dir: rs.route.project_path,
55765
+ task_list_state: listState,
55766
+ detail: `working_dir is empty and title names a different repo (${crossRepoForeign}); resolve project ownership before setting a path.`,
55767
+ suggested_repair: null
55768
+ });
55769
+ } else if (haveGoodProjectPath) {
55770
+ findings.push({
55771
+ ...base,
55772
+ category: "null_working_dir",
55773
+ severity: "error",
55774
+ repair_class: "safe_auto",
55775
+ expected_working_dir: rs.route.project_path,
55776
+ task_list_state: listState,
55777
+ detail: "working_dir is empty; set it to the owning project path.",
55778
+ suggested_repair: {
55779
+ field: "working_dir",
55780
+ from: null,
55781
+ to: rs.route.project_path,
55782
+ command: `todos update ${task2.id} --working-dir ${rs.route.project_path}`
55783
+ }
55784
+ });
55785
+ } else if (pathInvalidOnMachine) {
55786
+ findings.push({
55787
+ ...base,
55788
+ category: "null_working_dir",
55789
+ severity: "warn",
55790
+ repair_class: "blocker_invalid_path",
55791
+ expected_working_dir: rs.route.project_path,
55792
+ task_list_state: listState,
55793
+ detail: "working_dir is empty but the owning project path is not present on this machine; cannot safely set it here.",
55794
+ suggested_repair: null
55795
+ });
55796
+ }
55797
+ } else if (!isWorktreePath(task2.working_dir) && workingDir !== projectPath) {
55798
+ if (crossRepoForeign) {
55799
+ findings.push({
55800
+ ...base,
55801
+ category: "wrong_working_dir",
55802
+ severity: "warn",
55803
+ repair_class: "blocker_cross_repo",
55804
+ expected_working_dir: rs.route.project_path,
55805
+ task_list_state: listState,
55806
+ detail: `working_dir (${task2.working_dir}) differs from the owning project and the title names a different repo (${crossRepoForeign}); resolve ownership first.`,
55807
+ suggested_repair: null
55808
+ });
55809
+ } else if (haveGoodProjectPath) {
55810
+ findings.push({
55811
+ ...base,
55812
+ category: "wrong_working_dir",
55813
+ severity: "error",
55814
+ repair_class: "safe_auto",
55815
+ expected_working_dir: rs.route.project_path,
55816
+ task_list_state: listState,
55817
+ detail: `working_dir (${task2.working_dir}) does not match the owning project path; repoint it.`,
55818
+ suggested_repair: {
55819
+ field: "working_dir",
55820
+ from: task2.working_dir,
55821
+ to: rs.route.project_path,
55822
+ command: `todos update ${task2.id} --working-dir ${rs.route.project_path}`
55823
+ }
55824
+ });
55825
+ } else if (pathInvalidOnMachine) {
55826
+ findings.push({
55827
+ ...base,
55828
+ category: "wrong_working_dir",
55829
+ severity: "warn",
55830
+ repair_class: "blocker_invalid_path",
55831
+ expected_working_dir: rs.route.project_path,
55832
+ task_list_state: listState,
55833
+ detail: `working_dir (${task2.working_dir}) differs from the owning project, but that project path is absent on this machine; reported, not rewritten.`,
55834
+ suggested_repair: null
55835
+ });
55836
+ }
55837
+ }
55838
+ }
55839
+ if (listState === "null") {
55840
+ const canonical = resolveCanonicalTaskList(project, db);
55841
+ if (canonical) {
55842
+ findings.push({
55843
+ ...base,
55844
+ category: "null_task_list_id",
55845
+ severity: "error",
55846
+ repair_class: "safe_auto",
55847
+ expected_working_dir: rs.route.project_path,
55848
+ task_list_state: listState,
55849
+ detail: `task_list_id is null; link to the owning project's task list (${canonical.slug ?? canonical.id}) by UUID.`,
55850
+ suggested_repair: {
55851
+ field: "task_list_id",
55852
+ from: null,
55853
+ to: canonical.id,
55854
+ command: `todos update ${task2.id} --list ${canonical.id}`
55855
+ }
55856
+ });
55857
+ } else if (project) {
55858
+ findings.push({
55859
+ ...base,
55860
+ category: "null_task_list_id",
55861
+ severity: "warn",
55862
+ repair_class: "unsupported",
55863
+ expected_working_dir: rs.route.project_path,
55864
+ task_list_state: listState,
55865
+ detail: `task_list_id is null and the owning project has no resolvable task list${project.task_list_id ? ` (slug ${project.task_list_id} resolves to no row)` : ""}; create the list before linking.`,
55866
+ suggested_repair: null
55867
+ });
55868
+ }
55869
+ } else if (listState === "unresolvable") {
55870
+ const canonical = resolveCanonicalTaskList(project, db);
55871
+ if (canonical && canonical.id !== task2.task_list_id) {
55872
+ findings.push({
55873
+ ...base,
55874
+ category: "unresolvable_task_list",
55875
+ severity: "error",
55876
+ repair_class: "safe_auto",
55877
+ expected_working_dir: rs.route.project_path,
55878
+ task_list_state: listState,
55879
+ detail: `task_list_id (${task2.task_list_id}) resolves to no task list; relink to the owning project's list (${canonical.slug ?? canonical.id}) by UUID.`,
55880
+ suggested_repair: {
55881
+ field: "task_list_id",
55882
+ from: task2.task_list_id,
55883
+ to: canonical.id,
55884
+ command: `todos update ${task2.id} --list ${canonical.id}`
55885
+ }
55886
+ });
55887
+ } else {
55888
+ findings.push({
55889
+ ...base,
55890
+ category: "unresolvable_task_list",
55891
+ severity: "warn",
55892
+ repair_class: "blocker_human",
55893
+ expected_working_dir: rs.route.project_path,
55894
+ task_list_state: listState,
55895
+ detail: `task_list_id (${task2.task_list_id}) resolves to no task list and no canonical project list is available to relink to; reported without guessing.`,
55896
+ suggested_repair: null
55897
+ });
55898
+ }
55899
+ }
55900
+ if (rs.gates.tag_opt_in && rs.gates.no_auto) {
55901
+ findings.push({
55902
+ ...base,
55903
+ category: "no_auto_conflict",
55904
+ severity: "warn",
55905
+ repair_class: "blocker_human",
55906
+ expected_working_dir: rs.route.project_path,
55907
+ task_list_state: listState,
55908
+ detail: "Task carries both an auto:route/route:enabled tag and a no-auto deny signal; a triage decision is required (doctor does not guess).",
55909
+ suggested_repair: null
55910
+ });
55911
+ }
55912
+ if (rs.gates.tag_opt_in && !rs.eligible && rs.reasons.includes("route_not_enabled")) {
55913
+ findings.push({
55914
+ ...base,
55915
+ category: "route_not_enabled",
55916
+ severity: "warn",
55917
+ repair_class: "blocker_human",
55918
+ expected_working_dir: rs.route.project_path,
55919
+ task_list_state: listState,
55920
+ detail: "Task is tag-opted-in to routing but an explicit route_enabled:false denies it; reconcile the contradiction (doctor does not override an explicit deny).",
55921
+ suggested_repair: null
55922
+ });
55923
+ }
55924
+ return { findings, eligible: rs.eligible };
55925
+ }
55926
+ function createBackup2(dbPath, generatedAt) {
55927
+ if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
55928
+ return;
55929
+ if (!existsSync22(dbPath))
55930
+ return;
55931
+ const stamp = generatedAt.replace(/[:.]/g, "-");
55932
+ const backupDir = join21(dirname12(dbPath), `${basename9(dbPath)}.routing-doctor-backup-${stamp}`);
55933
+ const files = [];
55934
+ mkdirSync11(backupDir, { recursive: true });
55935
+ for (const source3 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
55936
+ if (!existsSync22(source3))
55937
+ continue;
55938
+ const target = join21(backupDir, basename9(source3));
55939
+ copyFileSync2(source3, target);
55940
+ files.push(target);
55941
+ }
55942
+ return files.length > 0 ? { path: backupDir, files } : undefined;
55943
+ }
55944
+ function routingRepairUndoCommand(repair) {
55945
+ if (repair.field === "working_dir") {
55946
+ return repair.from === null ? `todos update ${repair.task_id} --clear-working-dir` : `todos update ${repair.task_id} --working-dir ${repair.from}`;
55947
+ }
55948
+ return repair.from === null ? `todos update ${repair.task_id} --clear-list` : `todos update ${repair.task_id} --list ${repair.from}`;
55949
+ }
55950
+ function applySafeRepair(finding2, actor, generatedAt, db) {
55951
+ const repair = finding2.suggested_repair;
55952
+ const record = {
55953
+ task_id: finding2.task_id,
55954
+ task_short_id: finding2.task_short_id,
55955
+ category: finding2.category,
55956
+ field: repair.field,
55957
+ from: repair.from,
55958
+ to: repair.to,
55959
+ command: repair.command,
55960
+ applied: false
55961
+ };
55962
+ try {
55963
+ const current = getTask(finding2.task_id, db);
55964
+ if (!current)
55965
+ throw new Error("task not found");
55966
+ const patch = repair.field === "working_dir" ? { version: current.version, working_dir: repair.to } : { version: current.version, task_list_id: repair.to };
55967
+ updateTask(finding2.task_id, patch, db);
55968
+ const recheck = getTaskRouteState(finding2.task_id, db, { verifyProjectRoot: true });
55969
+ addComment({
55970
+ task_id: finding2.task_id,
55971
+ agent_id: actor,
55972
+ type: "comment",
55973
+ content: `[routing-doctor] repaired ${repair.field}: from=${repair.from ?? "(null)"} to=${repair.to}; ` + `source_project=${finding2.project_id ?? "(none)"}${finding2.project_name ? ` (${finding2.project_name})` : ""}` + `${finding2.project_path ? ` path=${finding2.project_path}` : ""}; command="${repair.command}"; ` + `doctor_run=${generatedAt}; route_recheck=eligible:${recheck.eligible} class:${recheck.route_class}`
55974
+ }, db);
55975
+ record.applied = true;
55976
+ } catch (error) {
55977
+ record.error = error instanceof Error ? error.message : String(error);
55978
+ }
55979
+ return record;
55980
+ }
55981
+ function runRoutingDoctor(options = {}) {
55982
+ const db = options.db ?? getDatabase();
55983
+ const dbPath = options.dbPath ?? getDatabasePath();
55984
+ const clock = options.now ?? now;
55985
+ const generatedAt = clock();
55986
+ const apply = options.apply === true;
55987
+ const statuses = options.statuses && options.statuses.length > 0 ? options.statuses : DEFAULT_STATUSES;
55988
+ const verifyProjectRoot = options.verifyProjectRoot !== false;
55989
+ const shardTotal = options.shardTotal && options.shardTotal > 1 ? Math.floor(options.shardTotal) : 1;
55990
+ const shardIndex = shardTotal > 1 ? Math.max(0, Math.floor(options.shardIndex ?? 0)) % shardTotal : 0;
55991
+ const actor = options.actor ?? "routing-doctor";
55992
+ const knownRepoSlugs = new Set;
55993
+ const projectCache = new Map;
55994
+ for (const project of listProjects(db)) {
55995
+ projectCache.set(project.id, project);
55996
+ knownRepoSlugs.add(repoSlugForProject(project));
55997
+ }
55998
+ const resolveProjectCached = (id) => {
55999
+ if (!id)
56000
+ return null;
56001
+ if (projectCache.has(id))
56002
+ return projectCache.get(id) ?? null;
56003
+ const project = getProject(id, db);
56004
+ projectCache.set(id, project);
56005
+ return project;
56006
+ };
56007
+ const filter = {
56008
+ status: statuses,
56009
+ include_archived: options.includeArchived === true
56010
+ };
56011
+ if (options.projectId)
56012
+ filter.project_id = options.projectId;
56013
+ if (options.tag)
56014
+ filter.tags = [options.tag];
56015
+ let tasks = listTasks(filter, db);
56016
+ if (shardTotal > 1)
56017
+ tasks = tasks.filter((task2) => routingShardOf(task2, shardTotal) === shardIndex);
56018
+ if (options.limit && options.limit > 0)
56019
+ tasks = tasks.slice(0, options.limit);
56020
+ const findings = [];
56021
+ let eligible = 0;
56022
+ for (const task2 of tasks) {
56023
+ const project = resolveProjectCached(task2.project_id);
56024
+ const evaluation = evaluateTaskRouting({ task: task2, project, db, knownRepoSlugs, verifyProjectRoot });
56025
+ if (evaluation.findings.length > 0)
56026
+ findings.push(...evaluation.findings);
56027
+ if (evaluation.eligible)
56028
+ eligible++;
56029
+ }
56030
+ const repairs = [];
56031
+ let backup;
56032
+ let undoRecordPath;
56033
+ if (apply) {
56034
+ const safeFindings = findings.filter((f) => f.repair_class === "safe_auto" && f.suggested_repair);
56035
+ if (safeFindings.length > 0) {
56036
+ backup = createBackup2(dbPath, generatedAt);
56037
+ for (const finding2 of safeFindings) {
56038
+ repairs.push(applySafeRepair(finding2, actor, generatedAt, db));
56039
+ }
56040
+ const applied = repairs.filter((r) => r.applied);
56041
+ if (applied.length > 0) {
56042
+ const undoPath = options.undoRecordPath ?? join21(process.cwd(), `todos-routing-doctor-undo-${generatedAt.replace(/[:.]/g, "-")}.json`);
56043
+ const undoRecord = {
56044
+ schema_version: TODOS_ROUTING_DOCTOR_SCHEMA_VERSION,
56045
+ purpose: "Undo record for routing-doctor --apply. Restore each field with the prior value below.",
56046
+ generated_at: generatedAt,
56047
+ actor,
56048
+ database_path: dbPath,
56049
+ backup: backup ?? null,
56050
+ repairs: applied.map((r) => ({
56051
+ task_id: r.task_id,
56052
+ field: r.field,
56053
+ from: r.from,
56054
+ to: r.to,
56055
+ command: r.command,
56056
+ undo_command: routingRepairUndoCommand(r)
56057
+ }))
56058
+ };
56059
+ try {
56060
+ writeFileSync9(undoPath, `${JSON.stringify(undoRecord, null, 2)}
56061
+ `);
56062
+ undoRecordPath = undoPath;
56063
+ } catch {
56064
+ undoRecordPath = undefined;
56065
+ }
56066
+ }
56067
+ }
56068
+ }
56069
+ const residualFindings = apply && repairs.some((r) => r.applied) ? runRoutingDoctor({
56070
+ ...options,
56071
+ apply: false,
56072
+ db,
56073
+ dbPath,
56074
+ now: () => generatedAt
56075
+ }).findings : findings;
56076
+ const byCategory = {};
56077
+ const byRepairClass = {};
56078
+ for (const f of residualFindings) {
56079
+ bump(byCategory, f.category);
56080
+ bump(byRepairClass, f.repair_class);
56081
+ }
56082
+ const safeAuto = residualFindings.filter((f) => f.repair_class === "safe_auto").length;
56083
+ const unsupported = residualFindings.filter((f) => f.repair_class === "unsupported").length;
56084
+ const blockers = residualFindings.filter((f) => f.repair_class.startsWith("blocker_")).length;
56085
+ const repaired = repairs.filter((r) => r.applied).length;
56086
+ const repairFailed = repairs.filter((r) => !r.applied).length;
56087
+ const summary = {
56088
+ inspected: tasks.length,
56089
+ eligible,
56090
+ findings_total: residualFindings.length,
56091
+ by_category: byCategory,
56092
+ by_repair_class: byRepairClass,
56093
+ safe_auto: safeAuto,
56094
+ blockers,
56095
+ unsupported,
56096
+ repaired,
56097
+ repair_failed: repairFailed
56098
+ };
56099
+ return {
56100
+ schema_version: TODOS_ROUTING_DOCTOR_SCHEMA_VERSION,
56101
+ generated_at: generatedAt,
56102
+ ok: residualFindings.length === 0,
56103
+ dry_run: !apply,
56104
+ database_path: dbPath,
56105
+ scope: {
56106
+ statuses,
56107
+ project_id: options.projectId ?? null,
56108
+ tag: options.tag ?? null,
56109
+ shard: shardTotal > 1 ? { index: shardIndex, total: shardTotal } : null,
56110
+ include_archived: options.includeArchived === true,
56111
+ verify_project_root: verifyProjectRoot,
56112
+ limit: options.limit && options.limit > 0 ? options.limit : null
56113
+ },
56114
+ summary,
56115
+ findings: apply ? residualFindings : findings,
56116
+ repairs,
56117
+ backup,
56118
+ undo_record_path: undoRecordPath
56119
+ };
56120
+ }
56121
+ var TODOS_ROUTING_DOCTOR_SCHEMA_VERSION = "todos.routing_doctor.v1", DEFAULT_STATUSES, REPO_NAME_RE;
56122
+ var init_routing_doctor = __esm(() => {
56123
+ init_database();
56124
+ init_comments();
56125
+ init_projects();
56126
+ init_task_lists();
56127
+ init_tasks();
56128
+ init_task_routing();
56129
+ DEFAULT_STATUSES = ["pending", "in_progress"];
56130
+ REPO_NAME_RE = /\bopen-[a-z0-9][a-z0-9-]{1,48}\b/gi;
56131
+ });
56132
+
55498
56133
  // src/lib/task-route-sources.ts
55499
56134
  var exports_task_route_sources = {};
55500
56135
  __export(exports_task_route_sources, {
@@ -55503,9 +56138,9 @@ __export(exports_task_route_sources, {
55503
56138
  });
55504
56139
  import { Database as Database3 } from "bun:sqlite";
55505
56140
  import { createHash as createHash13 } from "crypto";
55506
- import { existsSync as existsSync21, readdirSync as readdirSync5, statSync as statSync9 } from "fs";
55507
- import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve21 } from "path";
55508
- function normalizePath5(input) {
56141
+ import { existsSync as existsSync23, readdirSync as readdirSync5, statSync as statSync10 } from "fs";
56142
+ import { basename as basename10, dirname as dirname13, join as join22, resolve as resolve21 } from "path";
56143
+ function normalizePath6(input) {
55509
56144
  return resolve21(input);
55510
56145
  }
55511
56146
  function sourceStoreId(sourceDbPath) {
@@ -55513,14 +56148,14 @@ function sourceStoreId(sourceDbPath) {
55513
56148
  return `sqlite:${digest}`;
55514
56149
  }
55515
56150
  function inferSourceRepoPath(sourceDbPath) {
55516
- const normalized = normalizePath5(sourceDbPath);
56151
+ const normalized = normalizePath6(sourceDbPath);
55517
56152
  if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
55518
- return dirname12(dirname12(dirname12(normalized)));
56153
+ return dirname13(dirname13(dirname13(normalized)));
55519
56154
  }
55520
- return dirname12(normalized);
56155
+ return dirname13(normalized);
55521
56156
  }
55522
56157
  function createStoreRef(sourceDbPath) {
55523
- const normalized = normalizePath5(sourceDbPath);
56158
+ const normalized = normalizePath6(sourceDbPath);
55524
56159
  return {
55525
56160
  source_store_id: sourceStoreId(normalized),
55526
56161
  source_repo_path: inferSourceRepoPath(normalized),
@@ -55557,7 +56192,7 @@ function storeMatchesAny(ref, patterns) {
55557
56192
  if (patterns.length === 0)
55558
56193
  return false;
55559
56194
  const paths = [ref.source_db_path, ref.source_repo_path].filter((value) => Boolean(value));
55560
- const values = paths.flatMap((value) => [value, basename9(value)]);
56195
+ const values = paths.flatMap((value) => [value, basename10(value)]);
55561
56196
  return patterns.some((pattern) => values.some((value) => matchesPattern4(value, pattern)));
55562
56197
  }
55563
56198
  function shouldIncludeStore(ref, include, exclude) {
@@ -55565,11 +56200,11 @@ function shouldIncludeStore(ref, include, exclude) {
55565
56200
  return included && !storeMatchesAny(ref, exclude);
55566
56201
  }
55567
56202
  function discoverStoresUnderRoot(sourceRoot) {
55568
- const rootPath = normalizePath5(sourceRoot);
56203
+ const rootPath = normalizePath6(sourceRoot);
55569
56204
  const errors2 = [];
55570
56205
  const stores = [];
55571
- if (!existsSync21(rootPath)) {
55572
- const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
56206
+ if (!existsSync23(rootPath)) {
56207
+ const ref = createStoreRef(join22(rootPath, TODO_STORE_RELATIVE_PATH));
55573
56208
  errors2.push({
55574
56209
  ...ref,
55575
56210
  code: "SOURCE_ROOT_MISSING",
@@ -55579,9 +56214,9 @@ function discoverStoresUnderRoot(sourceRoot) {
55579
56214
  }
55580
56215
  let rootStat;
55581
56216
  try {
55582
- rootStat = statSync9(rootPath);
56217
+ rootStat = statSync10(rootPath);
55583
56218
  } catch (error) {
55584
- const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
56219
+ const ref = createStoreRef(join22(rootPath, TODO_STORE_RELATIVE_PATH));
55585
56220
  errors2.push({
55586
56221
  ...ref,
55587
56222
  code: "SOURCE_ROOT_UNREADABLE",
@@ -55594,8 +56229,8 @@ function discoverStoresUnderRoot(sourceRoot) {
55594
56229
  return { stores, errors: errors2 };
55595
56230
  }
55596
56231
  function scanDirectory(dir, depth) {
55597
- const candidate = join21(dir, TODO_STORE_RELATIVE_PATH);
55598
- if (existsSync21(candidate)) {
56232
+ const candidate = join22(dir, TODO_STORE_RELATIVE_PATH);
56233
+ if (existsSync23(candidate)) {
55599
56234
  stores.push(createStoreRef(candidate));
55600
56235
  }
55601
56236
  if (depth >= ROOT_SCAN_MAX_DEPTH)
@@ -55615,7 +56250,7 @@ function discoverStoresUnderRoot(sourceRoot) {
55615
56250
  for (const entry of entries) {
55616
56251
  if (!entry.isDirectory() || SKIPPED_SCAN_DIRS.has(entry.name))
55617
56252
  continue;
55618
- scanDirectory(join21(dir, entry.name), depth + 1);
56253
+ scanDirectory(join22(dir, entry.name), depth + 1);
55619
56254
  }
55620
56255
  }
55621
56256
  scanDirectory(rootPath, 0);
@@ -55641,7 +56276,7 @@ function collectStoreRefs(input) {
55641
56276
  };
55642
56277
  }
55643
56278
  function openReadonlyStore(ref) {
55644
- if (!existsSync21(ref.source_db_path)) {
56279
+ if (!existsSync23(ref.source_db_path)) {
55645
56280
  throw Object.assign(new Error(`Store does not exist: ${ref.source_db_path}`), { code: "STORE_MISSING" });
55646
56281
  }
55647
56282
  return new Database3(ref.source_db_path, { readonly: true, create: false });
@@ -55750,8 +56385,8 @@ function errorCode(error) {
55750
56385
  function discoverTaskRouteSources(input) {
55751
56386
  const include = normalizePatterns(input.include);
55752
56387
  const exclude = normalizePatterns(input.exclude);
55753
- const sourceRoots = (input.sourceRoots ?? []).map(normalizePath5).sort();
55754
- const sourceStores = (input.sourceStores ?? []).map(normalizePath5).sort();
56388
+ const sourceRoots = (input.sourceRoots ?? []).map(normalizePath6).sort();
56389
+ const sourceStores = (input.sourceStores ?? []).map(normalizePath6).sort();
55755
56390
  const limit = Number.isFinite(input.limit ?? NaN) && (input.limit ?? 0) >= 0 ? Math.floor(input.limit ?? 0) : null;
55756
56391
  const collected = collectStoreRefs(input);
55757
56392
  const stores = [];
@@ -55814,7 +56449,7 @@ var init_task_route_sources = __esm(() => {
55814
56449
  init_task_crud();
55815
56450
  init_redaction();
55816
56451
  init_task_routing();
55817
- TODO_STORE_RELATIVE_PATH = join21(".hasna", "todos", "todos.db");
56452
+ TODO_STORE_RELATIVE_PATH = join22(".hasna", "todos", "todos.db");
55818
56453
  SKIPPED_SCAN_DIRS = new Set([
55819
56454
  ".git",
55820
56455
  ".hg",
@@ -56311,7 +56946,7 @@ __export(exports_query_commands, {
56311
56946
  registerQueryCommands: () => registerQueryCommands
56312
56947
  });
56313
56948
  import chalk7 from "chalk";
56314
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
56949
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
56315
56950
  function parseJsonObjectOption2(value, label) {
56316
56951
  if (!value)
56317
56952
  return;
@@ -56955,7 +57590,7 @@ No task claimed (nothing available).`));
56955
57590
  console.log(lines.join(`
56956
57591
  `));
56957
57592
  });
56958
- program2.command("doctor").description("Diagnose and optionally repair local task data issues").option("--apply", "Apply safe repairs. Defaults to dry-run.").option("--fix", "Alias for --apply").option("-j, --json", "Output as JSON").action(async (opts) => {
57593
+ const doctor = program2.command("doctor").description("Diagnose and optionally repair local task data issues").option("--apply", "Apply safe repairs. Defaults to dry-run.").option("--fix", "Alias for --apply").option("-j, --json", "Output as JSON").action(async (opts) => {
56959
57594
  const globalOpts = program2.opts();
56960
57595
  const { runTodosDoctor: runTodosDoctor2 } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
56961
57596
  const result = runTodosDoctor2({ apply: Boolean(opts.apply || opts.fix) });
@@ -56995,19 +57630,140 @@ Repairs`));
56995
57630
  console.log(chalk7[errors2 > 0 ? "red" : "yellow"](`
56996
57631
  ${errors2} error(s), ${warnings} warning(s) remain after repair.`));
56997
57632
  });
57633
+ doctor.command("routing").description("Diagnose (and with --apply, safely repair) task routing-metadata drift: working_dir, task_list_id linkage, invalid paths, cross-repo intent").option("--apply", "Apply safe auto-repairs (working_dir, task_list_id UUID relink) with per-task comments, a DB backup, and an undo record. Defaults to dry-run.").option("--fix", "Alias for --apply").option("--project <id>", "Scope to a single project (id, slug, or path)").option("--tag <tag>", "Scope to tasks carrying this tag").option("--status <statuses>", "Comma-separated statuses to inspect (default: pending,in_progress)").option("--shard <index/total>", "Deterministic project-stable shard, e.g. 0/6").option("--include-archived", "Include archived tasks").option("--no-verify-project-root", "Skip machine-local project-root existence checks").option("--limit <n>", "Cap the number of tasks inspected").option("--undo-record <path>", "Where to write the undo record when --apply mutates").option("-j, --json", "Emit the machine-consumable JSON contract (todos.routing_doctor.v1)").addHelpText("after", `
57634
+ Exit codes (for OpenLoops / deterministic consumers):
57635
+ 0 no findings (clean)
57636
+ 1 routing-metadata findings present (drift detected)
57637
+ 2 invalid invocation (bad --shard/--status/--project/--limit)
57638
+
57639
+ JSON contract: --json emits { schema_version, generated_at, ok, dry_run, scope,
57640
+ summary{inspected,eligible,findings_total,by_category,by_repair_class,safe_auto,
57641
+ blockers,unsupported,repaired,repair_failed}, findings[], repairs[] }. Each finding
57642
+ carries repair_class: safe_auto | blocker_human | blocker_cross_repo |
57643
+ blocker_invalid_path | unsupported. Only safe_auto findings are ever mutated by --apply.`).action(async (opts) => {
57644
+ const globalOpts = program2.opts();
57645
+ const parentOpts = doctor.opts();
57646
+ const applyRequested = Boolean(opts.apply || opts.fix || parentOpts.apply || parentOpts.fix);
57647
+ const { runRoutingDoctor: runRoutingDoctor2 } = await Promise.resolve().then(() => (init_routing_doctor(), exports_routing_doctor));
57648
+ let shardIndex;
57649
+ let shardTotal;
57650
+ if (opts.shard) {
57651
+ const m = /^(\d+)\/(\d+)$/.exec(String(opts.shard).trim());
57652
+ if (!m) {
57653
+ console.error(chalk7.red("--shard must be <index>/<total>, e.g. 0/6"));
57654
+ process.exit(2);
57655
+ }
57656
+ shardIndex = parseInt(m[1], 10);
57657
+ shardTotal = parseInt(m[2], 10);
57658
+ if (shardTotal < 1 || shardIndex >= shardTotal) {
57659
+ console.error(chalk7.red("--shard: index must be < total and total >= 1"));
57660
+ process.exit(2);
57661
+ }
57662
+ }
57663
+ let statuses;
57664
+ if (opts.status) {
57665
+ statuses = String(opts.status).split(",").map((s2) => s2.trim()).filter(Boolean);
57666
+ const invalid = statuses.filter((s2) => !TASK_STATUSES.includes(s2));
57667
+ if (invalid.length > 0) {
57668
+ console.error(chalk7.red(`Invalid status(es): ${invalid.join(", ")}. Valid: ${TASK_STATUSES.join(", ")}`));
57669
+ process.exit(2);
57670
+ }
57671
+ }
57672
+ const projectInput = opts.project || globalOpts.project;
57673
+ let projectId;
57674
+ if (projectInput) {
57675
+ try {
57676
+ projectId = resolveExplicitProject(projectInput).id;
57677
+ } catch {
57678
+ console.error(chalk7.red(`Could not resolve project: ${projectInput}`));
57679
+ process.exit(2);
57680
+ }
57681
+ }
57682
+ let limit;
57683
+ if (opts.limit !== undefined) {
57684
+ limit = parseInt(opts.limit, 10);
57685
+ if (!Number.isFinite(limit) || limit < 1) {
57686
+ console.error(chalk7.red("--limit must be a positive integer"));
57687
+ process.exit(2);
57688
+ }
57689
+ }
57690
+ const result = runRoutingDoctor2({
57691
+ apply: applyRequested,
57692
+ statuses,
57693
+ projectId,
57694
+ tag: opts.tag,
57695
+ shardIndex,
57696
+ shardTotal,
57697
+ includeArchived: Boolean(opts.includeArchived),
57698
+ verifyProjectRoot: opts.verifyProjectRoot !== false,
57699
+ undoRecordPath: opts.undoRecord,
57700
+ limit,
57701
+ actor: globalOpts.agent || "routing-doctor"
57702
+ });
57703
+ if (opts.json || globalOpts.json) {
57704
+ output(result, true);
57705
+ process.exitCode = result.ok ? 0 : 1;
57706
+ return;
57707
+ }
57708
+ const s = result.summary;
57709
+ console.log(chalk7.bold(`todos doctor routing
57710
+ `));
57711
+ console.log(` ${chalk7.dim("Mode:")} ${result.dry_run ? "dry-run" : "apply"}`);
57712
+ const scopeBits = [
57713
+ `statuses=${result.scope.statuses.join(",")}`,
57714
+ result.scope.project_id ? `project=${result.scope.project_id}` : null,
57715
+ result.scope.tag ? `tag=${result.scope.tag}` : null,
57716
+ result.scope.shard ? `shard=${result.scope.shard.index}/${result.scope.shard.total}` : null,
57717
+ result.scope.include_archived ? "archived=included" : null,
57718
+ result.scope.verify_project_root ? null : "verify-project-root=off"
57719
+ ].filter(Boolean);
57720
+ console.log(` ${chalk7.dim("Scope:")} ${scopeBits.join(" \xB7 ")}`);
57721
+ console.log(` ${chalk7.dim("Tasks:")} ${s.inspected} inspected \xB7 ${s.eligible} route-eligible`);
57722
+ console.log(` ${chalk7.dim("Findings:")} ${s.findings_total} (${chalk7.yellow(`${s.safe_auto} safe_auto`)} \xB7 ${chalk7.red(`${s.blockers} blockers`)} \xB7 ${s.unsupported} unsupported)`);
57723
+ const cats = Object.entries(s.by_category).sort((a, b) => b[1] - a[1]);
57724
+ for (const [cat, n] of cats)
57725
+ console.log(` ${chalk7.dim("\xB7")} ${cat}: ${n}`);
57726
+ if (!result.dry_run) {
57727
+ console.log(` ${chalk7.dim("Repaired:")} ${s.repaired}${s.repair_failed ? chalk7.red(` (${s.repair_failed} failed)`) : ""}`);
57728
+ if (result.backup)
57729
+ console.log(` ${chalk7.dim("Backup:")} ${result.backup.path}`);
57730
+ if (result.undo_record_path)
57731
+ console.log(` ${chalk7.dim("Undo:")} ${result.undo_record_path}`);
57732
+ }
57733
+ if (result.findings.length > 0) {
57734
+ console.log(chalk7.bold(`
57735
+ Findings`));
57736
+ const shown = result.findings.slice(0, 50);
57737
+ for (const f of shown) {
57738
+ const id = f.task_short_id || f.task_id.slice(0, 8);
57739
+ const icon = f.severity === "error" ? chalk7.red("x") : chalk7.yellow("!");
57740
+ console.log(` ${icon} ${chalk7.bold(id)} ${f.category} [${f.repair_class}]${f.suggested_repair ? chalk7.dim(` \u2192 ${f.suggested_repair.command}`) : ""}`);
57741
+ console.log(` ${chalk7.dim(f.detail)}`);
57742
+ }
57743
+ if (result.findings.length > shown.length)
57744
+ console.log(chalk7.dim(` \u2026 +${result.findings.length - shown.length} more (use --json for the full set)`));
57745
+ }
57746
+ if (result.ok)
57747
+ console.log(chalk7.green(`
57748
+ No routing-metadata drift detected.`));
57749
+ else
57750
+ console.log(chalk7.yellow(`
57751
+ ${s.findings_total} finding(s). ${result.dry_run ? "Re-run with --apply to fix the safe_auto set; blockers need a human/owning repo." : "Remaining findings need a human/owning repo."}`));
57752
+ process.exitCode = result.ok ? 0 : 1;
57753
+ });
56998
57754
  program2.command("health").description("Check todos system health \u2014 database, config, connectivity").option("-j, --json", "Output as JSON").action(async (opts) => {
56999
57755
  const globalOpts = program2.opts();
57000
57756
  const checks = [];
57001
57757
  try {
57002
57758
  const db = getDatabase();
57003
57759
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
57004
- const { statSync: statSync10 } = await import("fs");
57005
- const { join: join22 } = await import("path");
57760
+ const { statSync: statSync11 } = await import("fs");
57761
+ const { join: join23 } = await import("path");
57006
57762
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
57007
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join22(home, ".hasna", "todos", "todos.db");
57763
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join23(home, ".hasna", "todos", "todos.db");
57008
57764
  let size = "unknown";
57009
57765
  try {
57010
- size = `${(statSync10(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
57766
+ size = `${(statSync11(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
57011
57767
  } catch {}
57012
57768
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk7.dim(dbPath)}` });
57013
57769
  } catch (e) {
@@ -57671,7 +58427,7 @@ Repairs`));
57671
58427
  const bundle = exportHandoffBundle(opts.export, db);
57672
58428
  const json2 = JSON.stringify(bundle, null, 2);
57673
58429
  if (opts.output) {
57674
- writeFileSync9(opts.output, `${json2}
58430
+ writeFileSync10(opts.output, `${json2}
57675
58431
  `);
57676
58432
  if (opts.json || globalOpts.json) {
57677
58433
  console.log(JSON.stringify({ path: opts.output, handoff_id: bundle.handoff.id }));
@@ -57902,7 +58658,7 @@ Repairs`));
57902
58658
  });
57903
58659
  const content = format === "json" ? JSON.stringify(document, null, 2) : renderReleaseNotesMarkdown2(document);
57904
58660
  if (opts.out) {
57905
- writeFileSync9(opts.out, content);
58661
+ writeFileSync10(opts.out, content);
57906
58662
  if (format !== "json")
57907
58663
  console.log(chalk7.green(`Wrote release notes to ${opts.out}`));
57908
58664
  return;
@@ -58017,7 +58773,7 @@ Repairs`));
58017
58773
  redact: Boolean(opts.redact)
58018
58774
  });
58019
58775
  if (opts.out) {
58020
- writeFileSync9(opts.out, exported.content);
58776
+ writeFileSync10(opts.out, exported.content);
58021
58777
  if (!(opts.json || globalOpts.json))
58022
58778
  console.log(chalk7.green(`Wrote ${exported.events.length} events to ${opts.out}`));
58023
58779
  }
@@ -58172,7 +58928,7 @@ Repairs`));
58172
58928
  const bundle = exportTaskBoardBundle(boardId);
58173
58929
  const json2 = JSON.stringify(bundle, null, 2);
58174
58930
  if (opts.out) {
58175
- writeFileSync9(opts.out, json2);
58931
+ writeFileSync10(opts.out, json2);
58176
58932
  if (!(opts.json || program2.opts().json))
58177
58933
  console.log(chalk7.green(`Wrote ${bundle.boards.length} board(s) to ${opts.out}`));
58178
58934
  }
@@ -58845,6 +59601,7 @@ var init_query_commands = __esm(() => {
58845
59601
  init_workflow_states();
58846
59602
  init_local_reports();
58847
59603
  init_helpers();
59604
+ init_types();
58848
59605
  });
58849
59606
 
58850
59607
  // src/cli/commands/mcp-hooks-commands.ts
@@ -58854,21 +59611,21 @@ __export(exports_mcp_hooks_commands, {
58854
59611
  });
58855
59612
  import chalk8 from "chalk";
58856
59613
  import { execSync as execSync3 } from "child_process";
58857
- import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58858
- import { dirname as dirname13, join as join22 } from "path";
59614
+ import { existsSync as existsSync24, readFileSync as readFileSync17, writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, chmodSync as chmodSync2 } from "fs";
59615
+ import { dirname as dirname14, join as join23 } from "path";
58859
59616
  function getMcpBinaryPath() {
58860
59617
  try {
58861
59618
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
58862
59619
  if (p)
58863
59620
  return p;
58864
59621
  } catch {}
58865
- const bunBin = join22(HOME2, ".bun", "bin", "todos-mcp");
58866
- if (existsSync22(bunBin))
59622
+ const bunBin = join23(HOME2, ".bun", "bin", "todos-mcp");
59623
+ if (existsSync24(bunBin))
58867
59624
  return bunBin;
58868
59625
  return "todos-mcp";
58869
59626
  }
58870
59627
  function readJsonFile2(path) {
58871
- if (!existsSync22(path))
59628
+ if (!existsSync24(path))
58872
59629
  return {};
58873
59630
  try {
58874
59631
  return JSON.parse(readFileSync17(path, "utf-8"));
@@ -58877,22 +59634,22 @@ function readJsonFile2(path) {
58877
59634
  }
58878
59635
  }
58879
59636
  function writeJsonFile2(path, data) {
58880
- const dir = dirname13(path);
58881
- if (!existsSync22(dir))
58882
- mkdirSync11(dir, { recursive: true });
58883
- writeFileSync10(path, JSON.stringify(data, null, 2) + `
59637
+ const dir = dirname14(path);
59638
+ if (!existsSync24(dir))
59639
+ mkdirSync12(dir, { recursive: true });
59640
+ writeFileSync11(path, JSON.stringify(data, null, 2) + `
58884
59641
  `);
58885
59642
  }
58886
59643
  function readTomlFile(path) {
58887
- if (!existsSync22(path))
59644
+ if (!existsSync24(path))
58888
59645
  return "";
58889
59646
  return readFileSync17(path, "utf-8");
58890
59647
  }
58891
59648
  function writeTomlFile(path, content) {
58892
- const dir = dirname13(path);
58893
- if (!existsSync22(dir))
58894
- mkdirSync11(dir, { recursive: true });
58895
- writeFileSync10(path, content);
59649
+ const dir = dirname14(path);
59650
+ if (!existsSync24(dir))
59651
+ mkdirSync12(dir, { recursive: true });
59652
+ writeFileSync11(path, content);
58896
59653
  }
58897
59654
  function removeTomlBlock(content, blockName) {
58898
59655
  const lines = content.split(`
@@ -58956,7 +59713,7 @@ function unregisterClaude(_global) {
58956
59713
  }
58957
59714
  }
58958
59715
  function registerCodex(binPath) {
58959
- const configPath = join22(HOME2, ".codex", "config.toml");
59716
+ const configPath = join23(HOME2, ".codex", "config.toml");
58960
59717
  let content = readTomlFile(configPath);
58961
59718
  content = removeTomlBlock(content, "mcp_servers.todos");
58962
59719
  const block = `
@@ -58970,7 +59727,7 @@ args = ["--stdio"]
58970
59727
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
58971
59728
  }
58972
59729
  function unregisterCodex() {
58973
- const configPath = join22(HOME2, ".codex", "config.toml");
59730
+ const configPath = join23(HOME2, ".codex", "config.toml");
58974
59731
  let content = readTomlFile(configPath);
58975
59732
  if (!content.includes("[mcp_servers.todos]")) {
58976
59733
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -58982,7 +59739,7 @@ function unregisterCodex() {
58982
59739
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
58983
59740
  }
58984
59741
  function registerGemini(binPath) {
58985
- const configPath = join22(HOME2, ".gemini", "settings.json");
59742
+ const configPath = join23(HOME2, ".gemini", "settings.json");
58986
59743
  const config = readJsonFile2(configPath);
58987
59744
  if (!config["mcpServers"]) {
58988
59745
  config["mcpServers"] = {};
@@ -58996,7 +59753,7 @@ function registerGemini(binPath) {
58996
59753
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
58997
59754
  }
58998
59755
  function unregisterGemini() {
58999
- const configPath = join22(HOME2, ".gemini", "settings.json");
59756
+ const configPath = join23(HOME2, ".gemini", "settings.json");
59000
59757
  const config = readJsonFile2(configPath);
59001
59758
  const servers = config["mcpServers"];
59002
59759
  if (!servers || !("todos" in servers)) {
@@ -59053,9 +59810,9 @@ function registerMcpHooksCommands(program2) {
59053
59810
  if (p)
59054
59811
  todosBin = p;
59055
59812
  } catch {}
59056
- const hooksDir = join22(process.cwd(), ".claude", "hooks");
59057
- if (!existsSync22(hooksDir))
59058
- mkdirSync11(hooksDir, { recursive: true });
59813
+ const hooksDir = join23(process.cwd(), ".claude", "hooks");
59814
+ if (!existsSync24(hooksDir))
59815
+ mkdirSync12(hooksDir, { recursive: true });
59059
59816
  const hookScript = `#!/usr/bin/env bash
59060
59817
  # Auto-generated by: todos hooks install
59061
59818
  # Syncs todos with Claude Code task list on tool use events.
@@ -59079,11 +59836,11 @@ esac
59079
59836
 
59080
59837
  exit 0
59081
59838
  `;
59082
- const hookPath = join22(hooksDir, "todos-sync.sh");
59083
- writeFileSync10(hookPath, hookScript);
59839
+ const hookPath = join23(hooksDir, "todos-sync.sh");
59840
+ writeFileSync11(hookPath, hookScript);
59084
59841
  execSync3(`chmod +x "${hookPath}"`);
59085
59842
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
59086
- const settingsPath = join22(process.cwd(), ".claude", "settings.json");
59843
+ const settingsPath = join23(process.cwd(), ".claude", "settings.json");
59087
59844
  const settings = readJsonFile2(settingsPath);
59088
59845
  if (!settings["hooks"]) {
59089
59846
  settings["hooks"] = {};
@@ -59956,18 +60713,18 @@ Artifacts:`));
59956
60713
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
59957
60714
  const hookPath = `${gitDir}/hooks/post-commit`;
59958
60715
  const marker = "# todos-auto-link";
59959
- if (existsSync22(hookPath)) {
60716
+ if (existsSync24(hookPath)) {
59960
60717
  const existing = readFileSync17(hookPath, "utf-8");
59961
60718
  if (existing.includes(marker)) {
59962
60719
  console.log(chalk8.yellow("Hook already installed."));
59963
60720
  return;
59964
60721
  }
59965
- writeFileSync10(hookPath, existing + `
60722
+ writeFileSync11(hookPath, existing + `
59966
60723
  ${marker}
59967
60724
  $(dirname "$0")/../../scripts/post-commit-hook.sh
59968
60725
  `);
59969
60726
  } else {
59970
- writeFileSync10(hookPath, `#!/usr/bin/env bash
60727
+ writeFileSync11(hookPath, `#!/usr/bin/env bash
59971
60728
  ${marker}
59972
60729
  $(dirname "$0")/../../scripts/post-commit-hook.sh
59973
60730
  `);
@@ -59984,7 +60741,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
59984
60741
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
59985
60742
  const hookPath = `${gitDir}/hooks/post-commit`;
59986
60743
  const marker = "# todos-auto-link";
59987
- if (!existsSync22(hookPath)) {
60744
+ if (!existsSync24(hookPath)) {
59988
60745
  console.log(chalk8.dim("No post-commit hook found."));
59989
60746
  return;
59990
60747
  }
@@ -59999,7 +60756,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
59999
60756
  if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
60000
60757
  (await import("fs")).unlinkSync(hookPath);
60001
60758
  } else {
60002
- writeFileSync10(hookPath, cleaned + `
60759
+ writeFileSync11(hookPath, cleaned + `
60003
60760
  `);
60004
60761
  }
60005
60762
  console.log(chalk8.green("Post-commit hook removed."));
@@ -60168,9 +60925,9 @@ __export(exports_machines, {
60168
60925
  });
60169
60926
  import chalk10 from "chalk";
60170
60927
  import { execSync as execSync4 } from "child_process";
60171
- import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
60928
+ import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync12 } from "fs";
60172
60929
  import { tmpdir as tmpdir4 } from "os";
60173
- import { join as join23 } from "path";
60930
+ import { join as join24 } from "path";
60174
60931
  function getOrCreateLocalMachineName() {
60175
60932
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
60176
60933
  }
@@ -60208,7 +60965,7 @@ function remoteTempPath(sshAddress) {
60208
60965
  }
60209
60966
  function readRemoteBridgeBundle(sshAddress) {
60210
60967
  const remotePath = remoteTempPath(sshAddress);
60211
- const localPath = join23(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
60968
+ const localPath = join24(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
60212
60969
  try {
60213
60970
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
60214
60971
  scpFromRemote(sshAddress, remotePath, localPath);
@@ -60223,8 +60980,8 @@ function readRemoteBridgeBundle(sshAddress) {
60223
60980
  }
60224
60981
  }
60225
60982
  function writeLocalBridgeBundle() {
60226
- const localPath = join23(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
60227
- writeFileSync11(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
60983
+ const localPath = join24(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
60984
+ writeFileSync12(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
60228
60985
  return localPath;
60229
60986
  }
60230
60987
  function pushLocalBridgeBundle(sshAddress, dryRun) {
@@ -64747,8 +65504,8 @@ __export(exports_sdk_integration_fixtures, {
64747
65504
  TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION: () => TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
64748
65505
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
64749
65506
  });
64750
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
64751
- import { join as join24 } from "path";
65507
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
65508
+ import { join as join25 } from "path";
64752
65509
  function source5(version) {
64753
65510
  return {
64754
65511
  packageName: "@hasna/todos",
@@ -64844,7 +65601,7 @@ function createSdkIntegrationFixturePack(options = {}) {
64844
65601
  };
64845
65602
  }
64846
65603
  function writeSdkIntegrationFixtures(directory, options = {}) {
64847
- mkdirSync12(directory, { recursive: true });
65604
+ mkdirSync13(directory, { recursive: true });
64848
65605
  const pack = createSdkIntegrationFixturePack(options);
64849
65606
  const bundle = getOnboardingFixtureBundle("agent-project-demo");
64850
65607
  const files = [
@@ -64855,8 +65612,8 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
64855
65612
  ];
64856
65613
  const written = [];
64857
65614
  for (const [name, payload] of files) {
64858
- const file = join24(directory, name);
64859
- writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
65615
+ const file = join25(directory, name);
65616
+ writeFileSync13(file, `${JSON.stringify(payload, null, 2)}
64860
65617
  `, "utf-8");
64861
65618
  written.push(file);
64862
65619
  }
@@ -65120,7 +65877,7 @@ var exports_roadmap_commands = {};
65120
65877
  __export(exports_roadmap_commands, {
65121
65878
  registerRoadmapCommands: () => registerRoadmapCommands
65122
65879
  });
65123
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
65880
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
65124
65881
  import chalk21 from "chalk";
65125
65882
  function splitList3(value) {
65126
65883
  return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
@@ -65331,7 +66088,7 @@ function registerRoadmapCommands(program2) {
65331
66088
  const { exportRoadmapBundle: exportRoadmapBundle2, renderRoadmapMarkdown: renderRoadmapMarkdown2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
65332
66089
  const content = opts.format === "markdown" ? renderRoadmapMarkdown2(roadmap) : JSON.stringify(exportRoadmapBundle2(roadmap), null, 2);
65333
66090
  if (opts.out) {
65334
- writeFileSync13(opts.out, content);
66091
+ writeFileSync14(opts.out, content);
65335
66092
  if (!globalOpts.json)
65336
66093
  console.log(chalk21.green(`Wrote roadmap export to ${opts.out}`));
65337
66094
  }