@hasna/todos 0.11.74 → 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
@@ -13277,6 +13277,19 @@ function buildExpectationMetadata(opts) {
13277
13277
  metadata["acceptance"] = parseJsonValue(String(acceptance));
13278
13278
  return metadata;
13279
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
+ }
13280
13293
  function registerTaskCommands(program2) {
13281
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) => {
13282
13295
  const globalOpts = program2.opts();
@@ -13830,7 +13843,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13830
13843
  console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
13831
13844
  }
13832
13845
  });
13833
- 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) => {
13834
13847
  const globalOpts = program2.opts();
13835
13848
  opts.tags = opts.tags || opts.tag;
13836
13849
  opts.list = opts.list || opts.taskList;
@@ -13848,15 +13861,22 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13848
13861
  console.error(chalk2.red("Use either --approval or --clear-approval, not both."));
13849
13862
  process.exit(1);
13850
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
+ }
13851
13872
  const taskListId = opts.list ? (() => {
13852
- const db = getDatabase();
13853
- const resolved = resolvePartialId(db, "task_lists", opts.list);
13854
- if (!resolved) {
13855
- 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));
13856
13876
  process.exit(1);
13857
13877
  }
13858
- return resolved;
13859
- })() : undefined;
13878
+ return resolved.id;
13879
+ })() : opts.clearList ? null : undefined;
13860
13880
  const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
13861
13881
  let task2;
13862
13882
  try {
@@ -13870,6 +13890,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13870
13890
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
13871
13891
  plan_id: planId,
13872
13892
  task_list_id: taskListId,
13893
+ working_dir: opts.workingDir ? resolve9(opts.workingDir) : opts.clearWorkingDir ? null : undefined,
13873
13894
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
13874
13895
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
13875
13896
  due_at: opts.due !== undefined ? opts.due === "" ? null : opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
@@ -14069,6 +14090,7 @@ var init_task_commands = __esm(() => {
14069
14090
  init_database();
14070
14091
  init_projects();
14071
14092
  init_tasks();
14093
+ init_task_lists();
14072
14094
  init_helpers();
14073
14095
  init_types();
14074
14096
  });
@@ -55591,6 +55613,523 @@ var init_config_serve_commands = __esm(() => {
55591
55613
  init_helpers();
55592
55614
  });
55593
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
+
55594
56133
  // src/lib/task-route-sources.ts
55595
56134
  var exports_task_route_sources = {};
55596
56135
  __export(exports_task_route_sources, {
@@ -55599,9 +56138,9 @@ __export(exports_task_route_sources, {
55599
56138
  });
55600
56139
  import { Database as Database3 } from "bun:sqlite";
55601
56140
  import { createHash as createHash13 } from "crypto";
55602
- import { existsSync as existsSync22, readdirSync as readdirSync5, statSync as statSync10 } from "fs";
55603
- import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve21 } from "path";
55604
- 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) {
55605
56144
  return resolve21(input);
55606
56145
  }
55607
56146
  function sourceStoreId(sourceDbPath) {
@@ -55609,14 +56148,14 @@ function sourceStoreId(sourceDbPath) {
55609
56148
  return `sqlite:${digest}`;
55610
56149
  }
55611
56150
  function inferSourceRepoPath(sourceDbPath) {
55612
- const normalized = normalizePath5(sourceDbPath);
56151
+ const normalized = normalizePath6(sourceDbPath);
55613
56152
  if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
55614
- return dirname12(dirname12(dirname12(normalized)));
56153
+ return dirname13(dirname13(dirname13(normalized)));
55615
56154
  }
55616
- return dirname12(normalized);
56155
+ return dirname13(normalized);
55617
56156
  }
55618
56157
  function createStoreRef(sourceDbPath) {
55619
- const normalized = normalizePath5(sourceDbPath);
56158
+ const normalized = normalizePath6(sourceDbPath);
55620
56159
  return {
55621
56160
  source_store_id: sourceStoreId(normalized),
55622
56161
  source_repo_path: inferSourceRepoPath(normalized),
@@ -55653,7 +56192,7 @@ function storeMatchesAny(ref, patterns) {
55653
56192
  if (patterns.length === 0)
55654
56193
  return false;
55655
56194
  const paths = [ref.source_db_path, ref.source_repo_path].filter((value) => Boolean(value));
55656
- const values = paths.flatMap((value) => [value, basename9(value)]);
56195
+ const values = paths.flatMap((value) => [value, basename10(value)]);
55657
56196
  return patterns.some((pattern) => values.some((value) => matchesPattern4(value, pattern)));
55658
56197
  }
55659
56198
  function shouldIncludeStore(ref, include, exclude) {
@@ -55661,11 +56200,11 @@ function shouldIncludeStore(ref, include, exclude) {
55661
56200
  return included && !storeMatchesAny(ref, exclude);
55662
56201
  }
55663
56202
  function discoverStoresUnderRoot(sourceRoot) {
55664
- const rootPath = normalizePath5(sourceRoot);
56203
+ const rootPath = normalizePath6(sourceRoot);
55665
56204
  const errors2 = [];
55666
56205
  const stores = [];
55667
- if (!existsSync22(rootPath)) {
55668
- const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
56206
+ if (!existsSync23(rootPath)) {
56207
+ const ref = createStoreRef(join22(rootPath, TODO_STORE_RELATIVE_PATH));
55669
56208
  errors2.push({
55670
56209
  ...ref,
55671
56210
  code: "SOURCE_ROOT_MISSING",
@@ -55677,7 +56216,7 @@ function discoverStoresUnderRoot(sourceRoot) {
55677
56216
  try {
55678
56217
  rootStat = statSync10(rootPath);
55679
56218
  } catch (error) {
55680
- const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
56219
+ const ref = createStoreRef(join22(rootPath, TODO_STORE_RELATIVE_PATH));
55681
56220
  errors2.push({
55682
56221
  ...ref,
55683
56222
  code: "SOURCE_ROOT_UNREADABLE",
@@ -55690,8 +56229,8 @@ function discoverStoresUnderRoot(sourceRoot) {
55690
56229
  return { stores, errors: errors2 };
55691
56230
  }
55692
56231
  function scanDirectory(dir, depth) {
55693
- const candidate = join21(dir, TODO_STORE_RELATIVE_PATH);
55694
- if (existsSync22(candidate)) {
56232
+ const candidate = join22(dir, TODO_STORE_RELATIVE_PATH);
56233
+ if (existsSync23(candidate)) {
55695
56234
  stores.push(createStoreRef(candidate));
55696
56235
  }
55697
56236
  if (depth >= ROOT_SCAN_MAX_DEPTH)
@@ -55711,7 +56250,7 @@ function discoverStoresUnderRoot(sourceRoot) {
55711
56250
  for (const entry of entries) {
55712
56251
  if (!entry.isDirectory() || SKIPPED_SCAN_DIRS.has(entry.name))
55713
56252
  continue;
55714
- scanDirectory(join21(dir, entry.name), depth + 1);
56253
+ scanDirectory(join22(dir, entry.name), depth + 1);
55715
56254
  }
55716
56255
  }
55717
56256
  scanDirectory(rootPath, 0);
@@ -55737,7 +56276,7 @@ function collectStoreRefs(input) {
55737
56276
  };
55738
56277
  }
55739
56278
  function openReadonlyStore(ref) {
55740
- if (!existsSync22(ref.source_db_path)) {
56279
+ if (!existsSync23(ref.source_db_path)) {
55741
56280
  throw Object.assign(new Error(`Store does not exist: ${ref.source_db_path}`), { code: "STORE_MISSING" });
55742
56281
  }
55743
56282
  return new Database3(ref.source_db_path, { readonly: true, create: false });
@@ -55846,8 +56385,8 @@ function errorCode(error) {
55846
56385
  function discoverTaskRouteSources(input) {
55847
56386
  const include = normalizePatterns(input.include);
55848
56387
  const exclude = normalizePatterns(input.exclude);
55849
- const sourceRoots = (input.sourceRoots ?? []).map(normalizePath5).sort();
55850
- const sourceStores = (input.sourceStores ?? []).map(normalizePath5).sort();
56388
+ const sourceRoots = (input.sourceRoots ?? []).map(normalizePath6).sort();
56389
+ const sourceStores = (input.sourceStores ?? []).map(normalizePath6).sort();
55851
56390
  const limit = Number.isFinite(input.limit ?? NaN) && (input.limit ?? 0) >= 0 ? Math.floor(input.limit ?? 0) : null;
55852
56391
  const collected = collectStoreRefs(input);
55853
56392
  const stores = [];
@@ -55910,7 +56449,7 @@ var init_task_route_sources = __esm(() => {
55910
56449
  init_task_crud();
55911
56450
  init_redaction();
55912
56451
  init_task_routing();
55913
- TODO_STORE_RELATIVE_PATH = join21(".hasna", "todos", "todos.db");
56452
+ TODO_STORE_RELATIVE_PATH = join22(".hasna", "todos", "todos.db");
55914
56453
  SKIPPED_SCAN_DIRS = new Set([
55915
56454
  ".git",
55916
56455
  ".hg",
@@ -56407,7 +56946,7 @@ __export(exports_query_commands, {
56407
56946
  registerQueryCommands: () => registerQueryCommands
56408
56947
  });
56409
56948
  import chalk7 from "chalk";
56410
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
56949
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
56411
56950
  function parseJsonObjectOption2(value, label) {
56412
56951
  if (!value)
56413
56952
  return;
@@ -57051,7 +57590,7 @@ No task claimed (nothing available).`));
57051
57590
  console.log(lines.join(`
57052
57591
  `));
57053
57592
  });
57054
- 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) => {
57055
57594
  const globalOpts = program2.opts();
57056
57595
  const { runTodosDoctor: runTodosDoctor2 } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
57057
57596
  const result = runTodosDoctor2({ apply: Boolean(opts.apply || opts.fix) });
@@ -57091,6 +57630,127 @@ Repairs`));
57091
57630
  console.log(chalk7[errors2 > 0 ? "red" : "yellow"](`
57092
57631
  ${errors2} error(s), ${warnings} warning(s) remain after repair.`));
57093
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
+ });
57094
57754
  program2.command("health").description("Check todos system health \u2014 database, config, connectivity").option("-j, --json", "Output as JSON").action(async (opts) => {
57095
57755
  const globalOpts = program2.opts();
57096
57756
  const checks = [];
@@ -57098,9 +57758,9 @@ Repairs`));
57098
57758
  const db = getDatabase();
57099
57759
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
57100
57760
  const { statSync: statSync11 } = await import("fs");
57101
- const { join: join22 } = await import("path");
57761
+ const { join: join23 } = await import("path");
57102
57762
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
57103
- 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");
57104
57764
  let size = "unknown";
57105
57765
  try {
57106
57766
  size = `${(statSync11(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -57767,7 +58427,7 @@ Repairs`));
57767
58427
  const bundle = exportHandoffBundle(opts.export, db);
57768
58428
  const json2 = JSON.stringify(bundle, null, 2);
57769
58429
  if (opts.output) {
57770
- writeFileSync9(opts.output, `${json2}
58430
+ writeFileSync10(opts.output, `${json2}
57771
58431
  `);
57772
58432
  if (opts.json || globalOpts.json) {
57773
58433
  console.log(JSON.stringify({ path: opts.output, handoff_id: bundle.handoff.id }));
@@ -57998,7 +58658,7 @@ Repairs`));
57998
58658
  });
57999
58659
  const content = format === "json" ? JSON.stringify(document, null, 2) : renderReleaseNotesMarkdown2(document);
58000
58660
  if (opts.out) {
58001
- writeFileSync9(opts.out, content);
58661
+ writeFileSync10(opts.out, content);
58002
58662
  if (format !== "json")
58003
58663
  console.log(chalk7.green(`Wrote release notes to ${opts.out}`));
58004
58664
  return;
@@ -58113,7 +58773,7 @@ Repairs`));
58113
58773
  redact: Boolean(opts.redact)
58114
58774
  });
58115
58775
  if (opts.out) {
58116
- writeFileSync9(opts.out, exported.content);
58776
+ writeFileSync10(opts.out, exported.content);
58117
58777
  if (!(opts.json || globalOpts.json))
58118
58778
  console.log(chalk7.green(`Wrote ${exported.events.length} events to ${opts.out}`));
58119
58779
  }
@@ -58268,7 +58928,7 @@ Repairs`));
58268
58928
  const bundle = exportTaskBoardBundle(boardId);
58269
58929
  const json2 = JSON.stringify(bundle, null, 2);
58270
58930
  if (opts.out) {
58271
- writeFileSync9(opts.out, json2);
58931
+ writeFileSync10(opts.out, json2);
58272
58932
  if (!(opts.json || program2.opts().json))
58273
58933
  console.log(chalk7.green(`Wrote ${bundle.boards.length} board(s) to ${opts.out}`));
58274
58934
  }
@@ -58941,6 +59601,7 @@ var init_query_commands = __esm(() => {
58941
59601
  init_workflow_states();
58942
59602
  init_local_reports();
58943
59603
  init_helpers();
59604
+ init_types();
58944
59605
  });
58945
59606
 
58946
59607
  // src/cli/commands/mcp-hooks-commands.ts
@@ -58950,21 +59611,21 @@ __export(exports_mcp_hooks_commands, {
58950
59611
  });
58951
59612
  import chalk8 from "chalk";
58952
59613
  import { execSync as execSync3 } from "child_process";
58953
- import { existsSync as existsSync23, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58954
- 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";
58955
59616
  function getMcpBinaryPath() {
58956
59617
  try {
58957
59618
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
58958
59619
  if (p)
58959
59620
  return p;
58960
59621
  } catch {}
58961
- const bunBin = join22(HOME2, ".bun", "bin", "todos-mcp");
58962
- if (existsSync23(bunBin))
59622
+ const bunBin = join23(HOME2, ".bun", "bin", "todos-mcp");
59623
+ if (existsSync24(bunBin))
58963
59624
  return bunBin;
58964
59625
  return "todos-mcp";
58965
59626
  }
58966
59627
  function readJsonFile2(path) {
58967
- if (!existsSync23(path))
59628
+ if (!existsSync24(path))
58968
59629
  return {};
58969
59630
  try {
58970
59631
  return JSON.parse(readFileSync17(path, "utf-8"));
@@ -58973,22 +59634,22 @@ function readJsonFile2(path) {
58973
59634
  }
58974
59635
  }
58975
59636
  function writeJsonFile2(path, data) {
58976
- const dir = dirname13(path);
58977
- if (!existsSync23(dir))
58978
- mkdirSync11(dir, { recursive: true });
58979
- 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) + `
58980
59641
  `);
58981
59642
  }
58982
59643
  function readTomlFile(path) {
58983
- if (!existsSync23(path))
59644
+ if (!existsSync24(path))
58984
59645
  return "";
58985
59646
  return readFileSync17(path, "utf-8");
58986
59647
  }
58987
59648
  function writeTomlFile(path, content) {
58988
- const dir = dirname13(path);
58989
- if (!existsSync23(dir))
58990
- mkdirSync11(dir, { recursive: true });
58991
- writeFileSync10(path, content);
59649
+ const dir = dirname14(path);
59650
+ if (!existsSync24(dir))
59651
+ mkdirSync12(dir, { recursive: true });
59652
+ writeFileSync11(path, content);
58992
59653
  }
58993
59654
  function removeTomlBlock(content, blockName) {
58994
59655
  const lines = content.split(`
@@ -59052,7 +59713,7 @@ function unregisterClaude(_global) {
59052
59713
  }
59053
59714
  }
59054
59715
  function registerCodex(binPath) {
59055
- const configPath = join22(HOME2, ".codex", "config.toml");
59716
+ const configPath = join23(HOME2, ".codex", "config.toml");
59056
59717
  let content = readTomlFile(configPath);
59057
59718
  content = removeTomlBlock(content, "mcp_servers.todos");
59058
59719
  const block = `
@@ -59066,7 +59727,7 @@ args = ["--stdio"]
59066
59727
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
59067
59728
  }
59068
59729
  function unregisterCodex() {
59069
- const configPath = join22(HOME2, ".codex", "config.toml");
59730
+ const configPath = join23(HOME2, ".codex", "config.toml");
59070
59731
  let content = readTomlFile(configPath);
59071
59732
  if (!content.includes("[mcp_servers.todos]")) {
59072
59733
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -59078,7 +59739,7 @@ function unregisterCodex() {
59078
59739
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
59079
59740
  }
59080
59741
  function registerGemini(binPath) {
59081
- const configPath = join22(HOME2, ".gemini", "settings.json");
59742
+ const configPath = join23(HOME2, ".gemini", "settings.json");
59082
59743
  const config = readJsonFile2(configPath);
59083
59744
  if (!config["mcpServers"]) {
59084
59745
  config["mcpServers"] = {};
@@ -59092,7 +59753,7 @@ function registerGemini(binPath) {
59092
59753
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
59093
59754
  }
59094
59755
  function unregisterGemini() {
59095
- const configPath = join22(HOME2, ".gemini", "settings.json");
59756
+ const configPath = join23(HOME2, ".gemini", "settings.json");
59096
59757
  const config = readJsonFile2(configPath);
59097
59758
  const servers = config["mcpServers"];
59098
59759
  if (!servers || !("todos" in servers)) {
@@ -59149,9 +59810,9 @@ function registerMcpHooksCommands(program2) {
59149
59810
  if (p)
59150
59811
  todosBin = p;
59151
59812
  } catch {}
59152
- const hooksDir = join22(process.cwd(), ".claude", "hooks");
59153
- if (!existsSync23(hooksDir))
59154
- mkdirSync11(hooksDir, { recursive: true });
59813
+ const hooksDir = join23(process.cwd(), ".claude", "hooks");
59814
+ if (!existsSync24(hooksDir))
59815
+ mkdirSync12(hooksDir, { recursive: true });
59155
59816
  const hookScript = `#!/usr/bin/env bash
59156
59817
  # Auto-generated by: todos hooks install
59157
59818
  # Syncs todos with Claude Code task list on tool use events.
@@ -59175,11 +59836,11 @@ esac
59175
59836
 
59176
59837
  exit 0
59177
59838
  `;
59178
- const hookPath = join22(hooksDir, "todos-sync.sh");
59179
- writeFileSync10(hookPath, hookScript);
59839
+ const hookPath = join23(hooksDir, "todos-sync.sh");
59840
+ writeFileSync11(hookPath, hookScript);
59180
59841
  execSync3(`chmod +x "${hookPath}"`);
59181
59842
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
59182
- const settingsPath = join22(process.cwd(), ".claude", "settings.json");
59843
+ const settingsPath = join23(process.cwd(), ".claude", "settings.json");
59183
59844
  const settings = readJsonFile2(settingsPath);
59184
59845
  if (!settings["hooks"]) {
59185
59846
  settings["hooks"] = {};
@@ -60052,18 +60713,18 @@ Artifacts:`));
60052
60713
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
60053
60714
  const hookPath = `${gitDir}/hooks/post-commit`;
60054
60715
  const marker = "# todos-auto-link";
60055
- if (existsSync23(hookPath)) {
60716
+ if (existsSync24(hookPath)) {
60056
60717
  const existing = readFileSync17(hookPath, "utf-8");
60057
60718
  if (existing.includes(marker)) {
60058
60719
  console.log(chalk8.yellow("Hook already installed."));
60059
60720
  return;
60060
60721
  }
60061
- writeFileSync10(hookPath, existing + `
60722
+ writeFileSync11(hookPath, existing + `
60062
60723
  ${marker}
60063
60724
  $(dirname "$0")/../../scripts/post-commit-hook.sh
60064
60725
  `);
60065
60726
  } else {
60066
- writeFileSync10(hookPath, `#!/usr/bin/env bash
60727
+ writeFileSync11(hookPath, `#!/usr/bin/env bash
60067
60728
  ${marker}
60068
60729
  $(dirname "$0")/../../scripts/post-commit-hook.sh
60069
60730
  `);
@@ -60080,7 +60741,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
60080
60741
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
60081
60742
  const hookPath = `${gitDir}/hooks/post-commit`;
60082
60743
  const marker = "# todos-auto-link";
60083
- if (!existsSync23(hookPath)) {
60744
+ if (!existsSync24(hookPath)) {
60084
60745
  console.log(chalk8.dim("No post-commit hook found."));
60085
60746
  return;
60086
60747
  }
@@ -60095,7 +60756,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
60095
60756
  if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
60096
60757
  (await import("fs")).unlinkSync(hookPath);
60097
60758
  } else {
60098
- writeFileSync10(hookPath, cleaned + `
60759
+ writeFileSync11(hookPath, cleaned + `
60099
60760
  `);
60100
60761
  }
60101
60762
  console.log(chalk8.green("Post-commit hook removed."));
@@ -60264,9 +60925,9 @@ __export(exports_machines, {
60264
60925
  });
60265
60926
  import chalk10 from "chalk";
60266
60927
  import { execSync as execSync4 } from "child_process";
60267
- 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";
60268
60929
  import { tmpdir as tmpdir4 } from "os";
60269
- import { join as join23 } from "path";
60930
+ import { join as join24 } from "path";
60270
60931
  function getOrCreateLocalMachineName() {
60271
60932
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
60272
60933
  }
@@ -60304,7 +60965,7 @@ function remoteTempPath(sshAddress) {
60304
60965
  }
60305
60966
  function readRemoteBridgeBundle(sshAddress) {
60306
60967
  const remotePath = remoteTempPath(sshAddress);
60307
- const localPath = join23(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
60968
+ const localPath = join24(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
60308
60969
  try {
60309
60970
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
60310
60971
  scpFromRemote(sshAddress, remotePath, localPath);
@@ -60319,8 +60980,8 @@ function readRemoteBridgeBundle(sshAddress) {
60319
60980
  }
60320
60981
  }
60321
60982
  function writeLocalBridgeBundle() {
60322
- const localPath = join23(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
60323
- 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));
60324
60985
  return localPath;
60325
60986
  }
60326
60987
  function pushLocalBridgeBundle(sshAddress, dryRun) {
@@ -64843,8 +65504,8 @@ __export(exports_sdk_integration_fixtures, {
64843
65504
  TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION: () => TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
64844
65505
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
64845
65506
  });
64846
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
64847
- import { join as join24 } from "path";
65507
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
65508
+ import { join as join25 } from "path";
64848
65509
  function source5(version) {
64849
65510
  return {
64850
65511
  packageName: "@hasna/todos",
@@ -64940,7 +65601,7 @@ function createSdkIntegrationFixturePack(options = {}) {
64940
65601
  };
64941
65602
  }
64942
65603
  function writeSdkIntegrationFixtures(directory, options = {}) {
64943
- mkdirSync12(directory, { recursive: true });
65604
+ mkdirSync13(directory, { recursive: true });
64944
65605
  const pack = createSdkIntegrationFixturePack(options);
64945
65606
  const bundle = getOnboardingFixtureBundle("agent-project-demo");
64946
65607
  const files = [
@@ -64951,8 +65612,8 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
64951
65612
  ];
64952
65613
  const written = [];
64953
65614
  for (const [name, payload] of files) {
64954
- const file = join24(directory, name);
64955
- writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
65615
+ const file = join25(directory, name);
65616
+ writeFileSync13(file, `${JSON.stringify(payload, null, 2)}
64956
65617
  `, "utf-8");
64957
65618
  written.push(file);
64958
65619
  }
@@ -65216,7 +65877,7 @@ var exports_roadmap_commands = {};
65216
65877
  __export(exports_roadmap_commands, {
65217
65878
  registerRoadmapCommands: () => registerRoadmapCommands
65218
65879
  });
65219
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
65880
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
65220
65881
  import chalk21 from "chalk";
65221
65882
  function splitList3(value) {
65222
65883
  return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
@@ -65427,7 +66088,7 @@ function registerRoadmapCommands(program2) {
65427
66088
  const { exportRoadmapBundle: exportRoadmapBundle2, renderRoadmapMarkdown: renderRoadmapMarkdown2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
65428
66089
  const content = opts.format === "markdown" ? renderRoadmapMarkdown2(roadmap) : JSON.stringify(exportRoadmapBundle2(roadmap), null, 2);
65429
66090
  if (opts.out) {
65430
- writeFileSync13(opts.out, content);
66091
+ writeFileSync14(opts.out, content);
65431
66092
  if (!globalOpts.json)
65432
66093
  console.log(chalk21.green(`Wrote roadmap export to ${opts.out}`));
65433
66094
  }