@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/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +896 -139
- package/dist/index.js +185 -94
- package/dist/lib/routing-doctor.d.ts +144 -0
- package/dist/lib/routing-doctor.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts +48 -1
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/release-provenance.json +3 -3
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13757
|
-
|
|
13758
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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
|
|
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 =
|
|
21949
|
+
const root = statSync5(basePath).isFile() ? resolve12(basePath, "..") : basePath;
|
|
21832
21950
|
const gitignorePath = join11(root, ".gitignore");
|
|
21833
|
-
if (!
|
|
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 =
|
|
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 =
|
|
22093
|
+
const fullPath = statSync5(basePath).isFile() ? basePath : join11(basePath, file);
|
|
21976
22094
|
try {
|
|
21977
22095
|
const source = readFileSync7(fullPath, "utf-8");
|
|
21978
|
-
const relPath =
|
|
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 =
|
|
22124
|
+
const fullPath = statSync5(basePath).isFile() ? basePath : join11(basePath, file);
|
|
22007
22125
|
try {
|
|
22008
22126
|
const source = readFileSync7(fullPath, "utf-8");
|
|
22009
|
-
const relPath =
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
26393
|
+
if (!existsSync15(resolved))
|
|
26276
26394
|
throw new Error(`extension source not found: ${source2}`);
|
|
26277
|
-
const stat =
|
|
26278
|
-
const manifestPath = stat.isDirectory() ? [join12(resolved, "todos.extension.json"), join12(resolved, "extension.json")].find(
|
|
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 (
|
|
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 (
|
|
26501
|
+
if (statSync6(full).isDirectory() || entry.endsWith(".json"))
|
|
26384
26502
|
candidates.push(full);
|
|
26385
26503
|
}
|
|
26386
26504
|
}
|
|
26387
|
-
return candidates.filter(
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 =
|
|
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) &&
|
|
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
|
|
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 &&
|
|
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 (!
|
|
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
|
|
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 (!
|
|
51846
|
+
if (!existsSync19(path))
|
|
51729
51847
|
return null;
|
|
51730
|
-
const stat =
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
|
55507
|
-
import { basename as
|
|
55508
|
-
function
|
|
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 =
|
|
56151
|
+
const normalized = normalizePath6(sourceDbPath);
|
|
55517
56152
|
if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
|
|
55518
|
-
return
|
|
56153
|
+
return dirname13(dirname13(dirname13(normalized)));
|
|
55519
56154
|
}
|
|
55520
|
-
return
|
|
56155
|
+
return dirname13(normalized);
|
|
55521
56156
|
}
|
|
55522
56157
|
function createStoreRef(sourceDbPath) {
|
|
55523
|
-
const normalized =
|
|
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,
|
|
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 =
|
|
56203
|
+
const rootPath = normalizePath6(sourceRoot);
|
|
55569
56204
|
const errors2 = [];
|
|
55570
56205
|
const stores = [];
|
|
55571
|
-
if (!
|
|
55572
|
-
const ref = createStoreRef(
|
|
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 =
|
|
56217
|
+
rootStat = statSync10(rootPath);
|
|
55583
56218
|
} catch (error) {
|
|
55584
|
-
const ref = createStoreRef(
|
|
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 =
|
|
55598
|
-
if (
|
|
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(
|
|
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 (!
|
|
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(
|
|
55754
|
-
const sourceStores = (input.sourceStores ?? []).map(
|
|
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 =
|
|
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
|
|
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:
|
|
57005
|
-
const { join:
|
|
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"] ||
|
|
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 = `${(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
58858
|
-
import { dirname as
|
|
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 =
|
|
58866
|
-
if (
|
|
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 (!
|
|
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 =
|
|
58881
|
-
if (!
|
|
58882
|
-
|
|
58883
|
-
|
|
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 (!
|
|
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 =
|
|
58893
|
-
if (!
|
|
58894
|
-
|
|
58895
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
59057
|
-
if (!
|
|
59058
|
-
|
|
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 =
|
|
59083
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
60722
|
+
writeFileSync11(hookPath, existing + `
|
|
59966
60723
|
${marker}
|
|
59967
60724
|
$(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
59968
60725
|
`);
|
|
59969
60726
|
} else {
|
|
59970
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
60227
|
-
|
|
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
|
|
64751
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
64859
|
-
|
|
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
|
|
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
|
-
|
|
66091
|
+
writeFileSync14(opts.out, content);
|
|
65335
66092
|
if (!globalOpts.json)
|
|
65336
66093
|
console.log(chalk21.green(`Wrote roadmap export to ${opts.out}`));
|
|
65337
66094
|
}
|