botholomew 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +91 -68
  2. package/package.json +3 -3
  3. package/src/chat/agent.ts +42 -82
  4. package/src/chat/session.ts +29 -25
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +177 -926
  7. package/src/commands/db.ts +9 -13
  8. package/src/commands/init.ts +4 -1
  9. package/src/commands/nuke.ts +57 -90
  10. package/src/commands/schedule.ts +103 -124
  11. package/src/commands/skill.ts +2 -2
  12. package/src/commands/task.ts +86 -95
  13. package/src/commands/thread.ts +107 -112
  14. package/src/commands/worker.ts +88 -88
  15. package/src/constants.ts +93 -16
  16. package/src/context/capabilities.ts +10 -10
  17. package/src/context/fetcher.ts +9 -10
  18. package/src/context/reindex.ts +189 -0
  19. package/src/context/store.ts +630 -0
  20. package/src/db/doctor.ts +1 -8
  21. package/src/db/embeddings.ts +227 -175
  22. package/src/db/sql/19-disk_backed_index.sql +36 -0
  23. package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
  24. package/src/fs/atomic.ts +217 -0
  25. package/src/fs/compat.ts +86 -0
  26. package/src/fs/sandbox.ts +279 -0
  27. package/src/init/index.ts +69 -52
  28. package/src/init/templates.ts +1 -1
  29. package/src/mcpx/client.ts +1 -1
  30. package/src/schedules/schema.ts +19 -0
  31. package/src/schedules/store.ts +296 -0
  32. package/src/skills/commands.ts +1 -3
  33. package/src/tasks/schema.ts +47 -0
  34. package/src/tasks/store.ts +486 -0
  35. package/src/threads/store.ts +559 -0
  36. package/src/tools/capabilities/refresh.ts +42 -21
  37. package/src/tools/context/pipe.ts +15 -71
  38. package/src/tools/context/update-beliefs.ts +3 -3
  39. package/src/tools/context/update-goals.ts +3 -3
  40. package/src/tools/dir/create.ts +26 -23
  41. package/src/tools/dir/size.ts +46 -17
  42. package/src/tools/dir/tree.ts +73 -279
  43. package/src/tools/file/copy.ts +50 -24
  44. package/src/tools/file/count-lines.ts +34 -10
  45. package/src/tools/file/delete.ts +44 -23
  46. package/src/tools/file/edit.ts +39 -14
  47. package/src/tools/file/exists.ts +12 -26
  48. package/src/tools/file/info.ts +25 -85
  49. package/src/tools/file/move.ts +39 -24
  50. package/src/tools/file/read.ts +32 -80
  51. package/src/tools/file/write.ts +14 -91
  52. package/src/tools/registry.ts +3 -7
  53. package/src/tools/schedule/create.ts +2 -2
  54. package/src/tools/schedule/list.ts +7 -3
  55. package/src/tools/search/fuse.ts +12 -33
  56. package/src/tools/search/index.ts +36 -43
  57. package/src/tools/search/regexp.ts +29 -17
  58. package/src/tools/search/semantic.ts +137 -51
  59. package/src/tools/skill/delete.ts +1 -1
  60. package/src/tools/skill/list.ts +1 -1
  61. package/src/tools/skill/write.ts +1 -1
  62. package/src/tools/task/create.ts +41 -16
  63. package/src/tools/task/delete.ts +3 -3
  64. package/src/tools/task/list.ts +6 -3
  65. package/src/tools/task/update.ts +31 -9
  66. package/src/tools/task/view.ts +6 -6
  67. package/src/tools/thread/list.ts +2 -2
  68. package/src/tools/thread/search.ts +208 -0
  69. package/src/tools/thread/view.ts +50 -5
  70. package/src/tools/worker/spawn.ts +28 -14
  71. package/src/tui/App.tsx +12 -19
  72. package/src/tui/components/ContextPanel.tsx +83 -316
  73. package/src/tui/components/SchedulePanel.tsx +34 -48
  74. package/src/tui/components/StatusBar.tsx +15 -15
  75. package/src/tui/components/TaskPanel.tsx +34 -38
  76. package/src/tui/components/ThreadPanel.tsx +29 -38
  77. package/src/tui/components/WorkerPanel.tsx +21 -19
  78. package/src/tui/markdown.ts +2 -8
  79. package/src/types/file-imports.d.ts +9 -0
  80. package/src/utils/title.ts +5 -7
  81. package/src/utils/v7-date.ts +47 -0
  82. package/src/worker/heartbeat.ts +46 -24
  83. package/src/worker/index.ts +13 -15
  84. package/src/worker/llm.ts +30 -37
  85. package/src/worker/prompt.ts +19 -41
  86. package/src/worker/schedules.ts +48 -69
  87. package/src/worker/spawn.ts +11 -11
  88. package/src/worker/tick.ts +39 -43
  89. package/src/workers/store.ts +247 -0
  90. package/src/commands/tools.ts +0 -367
  91. package/src/context/describer.ts +0 -140
  92. package/src/context/drives.ts +0 -110
  93. package/src/context/ingest.ts +0 -162
  94. package/src/context/refresh.ts +0 -183
  95. package/src/db/context.ts +0 -637
  96. package/src/db/daemon-state.ts +0 -6
  97. package/src/db/reembed.ts +0 -113
  98. package/src/db/schedules.ts +0 -213
  99. package/src/db/tasks.ts +0 -347
  100. package/src/db/threads.ts +0 -276
  101. package/src/db/workers.ts +0 -212
  102. package/src/tools/context/list-drives.ts +0 -36
  103. package/src/tools/context/refresh.ts +0 -165
  104. package/src/tools/context/search.ts +0 -54
@@ -1,15 +1,14 @@
1
1
  import ansis from "ansis";
2
2
  import type { Command } from "commander";
3
3
  import { getDbPath } from "../constants.ts";
4
- import { withDb as coreWithDb } from "../db/connection.ts";
5
4
  import {
6
5
  isPidAlive,
7
6
  type ProbeResult,
8
7
  probeAllTables,
9
8
  repairDatabase,
10
9
  } from "../db/doctor.ts";
11
- import { listWorkers, type Worker } from "../db/workers.ts";
12
10
  import { logger } from "../utils/logger.ts";
11
+ import { listWorkers, type Worker } from "../workers/store.ts";
13
12
 
14
13
  function statusBadge(status: ProbeResult["status"]): string {
15
14
  switch (status) {
@@ -81,17 +80,14 @@ async function doctor(program: Command, repair: boolean): Promise<void> {
81
80
 
82
81
  // Repair requires exclusive access — refuse if any worker is actually
83
82
  // running, otherwise the EXPORT would race with the worker's writes.
84
- // Stale `status='running'` rows whose PID is dead (the exact case that
85
- // tends to coexist with workers-table corruption) are reported but do
86
- // not block repair: trying to flip them to `stopped` would just trip
87
- // the same corruption we're about to fix.
88
- const running = await coreWithDb(dbPath, async (conn) => {
89
- try {
90
- return await listWorkers(conn, { status: "running" });
91
- } catch {
92
- return [] as Worker[];
93
- }
94
- });
83
+ // Stale 'running' worker JSON files whose PID is dead are reported but
84
+ // don't block repair.
85
+ let running: Worker[];
86
+ try {
87
+ running = await listWorkers(dir, { status: "running" });
88
+ } catch {
89
+ running = [];
90
+ }
95
91
  const live = running.filter((w) => isPidAlive(w.pid));
96
92
  const stale = running.filter((w) => !isPidAlive(w.pid));
97
93
  if (live.length > 0) {
@@ -6,7 +6,10 @@ export function registerInitCommand(program: Command) {
6
6
  program
7
7
  .command("init")
8
8
  .description("Initialize a new Botholomew project in the current directory")
9
- .option("--force", "overwrite existing .botholomew directory")
9
+ .option(
10
+ "--force",
11
+ "overwrite existing project files; also bypass the unsupported-filesystem check (iCloud/Dropbox/etc)",
12
+ )
10
13
  .action(async (opts) => {
11
14
  const dir = program.opts().dir;
12
15
  try {
@@ -1,56 +1,23 @@
1
+ import { rm } from "node:fs/promises";
1
2
  import ansis from "ansis";
2
3
  import type { Command } from "commander";
3
- import type { DbConnection } from "../db/connection.ts";
4
- import { deleteAllContextItems } from "../db/context.ts";
5
- import { deleteAllDaemonState } from "../db/daemon-state.ts";
6
- import { deleteAllSchedules } from "../db/schedules.ts";
7
- import { deleteAllTasks } from "../db/tasks.ts";
8
- import { deleteAllThreads } from "../db/threads.ts";
9
- import { listWorkers } from "../db/workers.ts";
4
+ import {
5
+ CONTEXT_DIR,
6
+ getContextDir,
7
+ SCHEDULES_DIR,
8
+ TASKS_DIR,
9
+ THREADS_DIR,
10
+ } from "../constants.ts";
11
+ import { deleteAllSchedules } from "../schedules/store.ts";
12
+ import { deleteAllTasks } from "../tasks/store.ts";
13
+ import { deleteAllThreads } from "../threads/store.ts";
10
14
  import { logger } from "../utils/logger.ts";
11
- import { withDb } from "./with-db.ts";
15
+ import { listWorkers } from "../workers/store.ts";
12
16
 
13
17
  type NukeScope = "context" | "tasks" | "schedules" | "threads" | "all";
14
18
 
15
- const TABLES_BY_SCOPE: Record<NukeScope, string[]> = {
16
- context: ["context_items", "embeddings"],
17
- tasks: ["tasks"],
18
- schedules: ["schedules"],
19
- threads: ["threads", "interactions"],
20
- all: [
21
- "context_items",
22
- "embeddings",
23
- "tasks",
24
- "schedules",
25
- "threads",
26
- "interactions",
27
- "daemon_state",
28
- ],
29
- };
30
-
31
- async function countRows(conn: DbConnection, table: string): Promise<number> {
32
- const row = await conn.queryGet<{ cnt: number }>(
33
- `SELECT COUNT(*) AS cnt FROM ${table}`,
34
- );
35
- return row ? Number(row.cnt) : 0;
36
- }
37
-
38
- function printDryRun(scope: NukeScope, counts: Record<string, number>) {
39
- console.log(ansis.red.bold(`Nuke scope: ${scope}`));
40
- console.log("Would delete:");
41
- const nameWidth = Math.max(...Object.keys(counts).map((k) => k.length));
42
- for (const [table, count] of Object.entries(counts)) {
43
- const padded = table.padEnd(nameWidth + 2);
44
- console.log(` ${padded}${ansis.dim(`${count} rows`)}`);
45
- }
46
- console.log("");
47
- console.log(
48
- ansis.yellow("Re-run with --yes to confirm. This cannot be undone."),
49
- );
50
- }
51
-
52
- async function ensureNoRunningWorkers(conn: DbConnection): Promise<boolean> {
53
- const running = await listWorkers(conn, { status: "running" });
19
+ async function ensureNoRunningWorkers(projectDir: string): Promise<boolean> {
20
+ const running = await listWorkers(projectDir, { status: "running" });
54
21
  if (running.length > 0) {
55
22
  logger.error(
56
23
  `${running.length} worker(s) running. Stop them first: botholomew worker stop <id>`,
@@ -63,33 +30,24 @@ async function ensureNoRunningWorkers(conn: DbConnection): Promise<boolean> {
63
30
  return true;
64
31
  }
65
32
 
66
- async function runNuke(conn: DbConnection, scope: NukeScope): Promise<void> {
67
- // Not wrapped in a transaction: DuckDB's FK index checks on DELETE FROM
68
- // threads inside a transaction see stale interactions rows even after
69
- // DELETE FROM interactions ran in the same transaction. Each helper is
70
- // already a small sequence of statements, so auto-commit is fine for a
71
- // destructive dev-time tool.
33
+ async function runNuke(projectDir: string, scope: NukeScope): Promise<void> {
72
34
  if (scope === "context" || scope === "all") {
73
- const { contextItems, embeddings } = await deleteAllContextItems(conn);
74
- logger.success(
75
- `Deleted ${contextItems} context_items, ${embeddings} embeddings`,
76
- );
35
+ await rm(getContextDir(projectDir), { recursive: true, force: true });
36
+ logger.success(`Removed ${CONTEXT_DIR}/ directory`);
77
37
  }
78
38
  if (scope === "tasks" || scope === "all") {
79
- const n = await deleteAllTasks(conn);
80
- logger.success(`Deleted ${n} tasks`);
39
+ const n = await deleteAllTasks(projectDir);
40
+ logger.success(`Deleted ${n} task file(s) from ${TASKS_DIR}/`);
81
41
  }
82
42
  if (scope === "schedules" || scope === "all") {
83
- const n = await deleteAllSchedules(conn);
84
- logger.success(`Deleted ${n} schedules`);
43
+ const n = await deleteAllSchedules(projectDir);
44
+ logger.success(`Deleted ${n} schedule file(s) from ${SCHEDULES_DIR}/`);
85
45
  }
86
46
  if (scope === "threads" || scope === "all") {
87
- const { threads, interactions } = await deleteAllThreads(conn);
88
- logger.success(`Deleted ${threads} threads, ${interactions} interactions`);
89
- }
90
- if (scope === "all") {
91
- const n = await deleteAllDaemonState(conn);
92
- logger.success(`Deleted ${n} daemon_state entries`);
47
+ const { threads, interactions } = await deleteAllThreads(projectDir);
48
+ logger.success(
49
+ `Deleted ${threads} threads (${interactions} interactions) from ${THREADS_DIR}/`,
50
+ );
93
51
  }
94
52
  }
95
53
 
@@ -103,50 +61,59 @@ function registerScope(
103
61
  .command(scope)
104
62
  .description(description)
105
63
  .option("-y, --yes", "confirm the deletion (required)")
106
- .action((opts) =>
107
- withDb(program, async (conn) => {
108
- if (!(await ensureNoRunningWorkers(conn))) {
109
- process.exit(1);
110
- }
111
- const tables = TABLES_BY_SCOPE[scope];
112
- const counts: Record<string, number> = {};
113
- for (const t of tables) {
114
- counts[t] = await countRows(conn, t);
115
- }
64
+ .action(async (opts) => {
65
+ const dir = program.opts().dir;
66
+ if (!(await ensureNoRunningWorkers(dir))) {
67
+ process.exit(1);
68
+ }
116
69
 
117
- if (!opts.yes) {
118
- printDryRun(scope, counts);
119
- process.exit(1);
120
- }
70
+ if (!opts.yes) {
71
+ console.log(ansis.red.bold(`Nuke scope: ${scope}`));
72
+ console.log(
73
+ ansis.yellow(
74
+ `Re-run with --yes to confirm. This will delete files on disk; cannot be undone.`,
75
+ ),
76
+ );
77
+ process.exit(1);
78
+ }
121
79
 
122
- await runNuke(conn, scope);
123
- }),
124
- );
80
+ await runNuke(dir, scope);
81
+ });
125
82
  }
126
83
 
127
84
  export function registerNukeCommand(program: Command) {
128
85
  const nuke = program
129
86
  .command("nuke")
130
- .description("Bulk-erase sections of the database");
87
+ .description("Bulk-erase sections of the project");
131
88
 
132
89
  registerScope(
133
90
  program,
134
91
  nuke,
135
92
  "context",
136
- "Erase all context_items and embeddings",
93
+ `Erase the entire ${CONTEXT_DIR}/ directory (user-curated knowledge)`,
94
+ );
95
+ registerScope(
96
+ program,
97
+ nuke,
98
+ "tasks",
99
+ `Delete all task files in ${TASKS_DIR}/`,
100
+ );
101
+ registerScope(
102
+ program,
103
+ nuke,
104
+ "schedules",
105
+ `Delete all schedule files in ${SCHEDULES_DIR}/`,
137
106
  );
138
- registerScope(program, nuke, "tasks", "Erase all tasks");
139
- registerScope(program, nuke, "schedules", "Erase all schedules");
140
107
  registerScope(
141
108
  program,
142
109
  nuke,
143
110
  "threads",
144
- "Erase all threads and interactions (worker + chat history)",
111
+ `Delete all conversation history in ${THREADS_DIR}/`,
145
112
  );
146
113
  registerScope(
147
114
  program,
148
115
  nuke,
149
116
  "all",
150
- "Erase everything in the database (preserves schema, skills, and on-disk soul/beliefs/goals)",
117
+ "Erase all agent-writable data: context/, tasks/, schedules/, threads/",
151
118
  );
152
119
  }
@@ -1,15 +1,15 @@
1
1
  import ansis from "ansis";
2
2
  import type { Command } from "commander";
3
- import type { Schedule } from "../db/schedules.ts";
3
+ import type { Schedule } from "../schedules/schema.ts";
4
4
  import {
5
5
  createSchedule,
6
6
  deleteSchedule,
7
7
  getSchedule,
8
8
  listSchedules,
9
+ markScheduleRun,
9
10
  updateSchedule,
10
- } from "../db/schedules.ts";
11
+ } from "../schedules/store.ts";
11
12
  import { logger } from "../utils/logger.ts";
12
- import { withDb } from "./with-db.ts";
13
13
 
14
14
  export function registerScheduleCommand(program: Command) {
15
15
  const schedule = program.command("schedule").description("Manage schedules");
@@ -21,31 +21,22 @@ export function registerScheduleCommand(program: Command) {
21
21
  .option("--disabled", "show only disabled schedules")
22
22
  .option("-l, --limit <n>", "max number of schedules", Number.parseInt)
23
23
  .option("-o, --offset <n>", "skip first N schedules", Number.parseInt)
24
- .action((opts) =>
25
- withDb(program, async (conn) => {
26
- const filters: {
27
- enabled?: boolean;
28
- limit?: number;
29
- offset?: number;
30
- } = {
31
- limit: opts.limit,
32
- offset: opts.offset,
33
- };
34
- if (opts.enabled) filters.enabled = true;
35
- if (opts.disabled) filters.enabled = false;
36
-
37
- const schedules = await listSchedules(conn, filters);
38
-
39
- if (schedules.length === 0) {
40
- logger.dim("No schedules found.");
41
- return;
42
- }
43
-
44
- for (const s of schedules) {
45
- printSchedule(s);
46
- }
47
- }),
48
- );
24
+ .action(async (opts) => {
25
+ const dir = program.opts().dir;
26
+ const filters: { enabled?: boolean; limit?: number; offset?: number } = {
27
+ limit: opts.limit,
28
+ offset: opts.offset,
29
+ };
30
+ if (opts.enabled) filters.enabled = true;
31
+ if (opts.disabled) filters.enabled = false;
32
+
33
+ const schedules = await listSchedules(dir, filters);
34
+ if (schedules.length === 0) {
35
+ logger.dim("No schedules found.");
36
+ return;
37
+ }
38
+ for (const s of schedules) printSchedule(s);
39
+ });
49
40
 
50
41
  schedule
51
42
  .command("add <name>")
@@ -55,116 +46,108 @@ export function registerScheduleCommand(program: Command) {
55
46
  "how often to run (e.g. 'every morning')",
56
47
  )
57
48
  .option("--description <text>", "schedule description", "")
58
- .action((name, opts) =>
59
- withDb(program, async (conn) => {
60
- const s = await createSchedule(conn, {
61
- name,
62
- description: opts.description,
63
- frequency: opts.frequency,
64
- });
65
- logger.success(`Created schedule: ${s.name} (${s.id})`);
66
- }),
67
- );
49
+ .action(async (name, opts) => {
50
+ const dir = program.opts().dir;
51
+ const s = await createSchedule(dir, {
52
+ name,
53
+ description: opts.description,
54
+ frequency: opts.frequency,
55
+ });
56
+ logger.success(`Created schedule: ${s.name} (${s.id})`);
57
+ });
68
58
 
69
59
  schedule
70
60
  .command("view <id>")
71
61
  .description("View schedule details")
72
- .action((id) =>
73
- withDb(program, async (conn) => {
74
- const s = await getSchedule(conn, id);
75
- if (!s) {
76
- logger.error(`Schedule not found: ${id}`);
77
- process.exit(1);
78
- }
79
- printScheduleDetail(s);
80
- }),
81
- );
62
+ .action(async (id) => {
63
+ const dir = program.opts().dir;
64
+ const s = await getSchedule(dir, id);
65
+ if (!s) {
66
+ logger.error(`Schedule not found: ${id}`);
67
+ process.exit(1);
68
+ }
69
+ printScheduleDetail(s);
70
+ });
82
71
 
83
72
  schedule
84
73
  .command("enable <id>")
85
74
  .description("Enable a schedule")
86
- .action((id) =>
87
- withDb(program, async (conn) => {
88
- const s = await updateSchedule(conn, id, { enabled: true });
89
- if (!s) {
90
- logger.error(`Schedule not found: ${id}`);
91
- process.exit(1);
92
- }
93
- logger.success(`Enabled schedule: ${s.name}`);
94
- }),
95
- );
75
+ .action(async (id) => {
76
+ const dir = program.opts().dir;
77
+ const s = await updateSchedule(dir, id, { enabled: true });
78
+ if (!s) {
79
+ logger.error(`Schedule not found: ${id}`);
80
+ process.exit(1);
81
+ }
82
+ logger.success(`Enabled schedule: ${s.name}`);
83
+ });
96
84
 
97
85
  schedule
98
86
  .command("disable <id>")
99
87
  .description("Disable a schedule")
100
- .action((id) =>
101
- withDb(program, async (conn) => {
102
- const s = await updateSchedule(conn, id, { enabled: false });
103
- if (!s) {
104
- logger.error(`Schedule not found: ${id}`);
105
- process.exit(1);
106
- }
107
- logger.success(`Disabled schedule: ${s.name}`);
108
- }),
109
- );
88
+ .action(async (id) => {
89
+ const dir = program.opts().dir;
90
+ const s = await updateSchedule(dir, id, { enabled: false });
91
+ if (!s) {
92
+ logger.error(`Schedule not found: ${id}`);
93
+ process.exit(1);
94
+ }
95
+ logger.success(`Disabled schedule: ${s.name}`);
96
+ });
110
97
 
111
98
  schedule
112
99
  .command("delete <id>")
113
100
  .description("Delete a schedule")
114
- .action((id) =>
115
- withDb(program, async (conn) => {
116
- const deleted = await deleteSchedule(conn, id);
117
- if (!deleted) {
118
- logger.error(`Schedule not found: ${id}`);
119
- process.exit(1);
120
- }
121
- logger.success(`Deleted schedule: ${id}`);
122
- }),
123
- );
101
+ .action(async (id) => {
102
+ const dir = program.opts().dir;
103
+ const deleted = await deleteSchedule(dir, id);
104
+ if (!deleted) {
105
+ logger.error(`Schedule not found: ${id}`);
106
+ process.exit(1);
107
+ }
108
+ logger.success(`Deleted schedule: ${id}`);
109
+ });
124
110
 
125
111
  schedule
126
112
  .command("trigger <id>")
127
113
  .description("Manually trigger a schedule (creates tasks immediately)")
128
- .action((id) =>
129
- withDb(program, async (conn, dir) => {
130
- const s = await getSchedule(conn, id);
131
- if (!s) {
132
- logger.error(`Schedule not found: ${id}`);
133
- process.exit(1);
134
- }
135
-
136
- // Lazy import to avoid loading LLM deps for non-trigger commands
137
- const { evaluateSchedule } = await import("../worker/schedules.ts");
138
- const { loadConfig } = await import("../config/loader.ts");
139
- const { createTask } = await import("../db/tasks.ts");
140
- const { markScheduleRun } = await import("../db/schedules.ts");
141
-
142
- const config = await loadConfig(dir);
143
- const evaluation = await evaluateSchedule(config, s);
144
-
145
- if (evaluation.tasksToCreate.length === 0) {
146
- logger.dim("Schedule evaluated but produced no tasks.");
147
- } else {
148
- const createdIds: string[] = [];
149
- for (const taskDef of evaluation.tasksToCreate) {
150
- const blockedBy = (taskDef.depends_on ?? [])
151
- .map((i: number) => createdIds[i])
152
- .filter(Boolean) as string[];
153
- const t = await createTask(conn, {
154
- name: taskDef.name,
155
- description: taskDef.description,
156
- priority: taskDef.priority,
157
- blocked_by: blockedBy,
158
- });
159
- createdIds.push(t.id);
160
- logger.success(`Created task: ${t.name} (${t.id})`);
161
- }
114
+ .action(async (id) => {
115
+ const dir = program.opts().dir;
116
+ const s = await getSchedule(dir, id);
117
+ if (!s) {
118
+ logger.error(`Schedule not found: ${id}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ const { evaluateSchedule } = await import("../worker/schedules.ts");
123
+ const { loadConfig } = await import("../config/loader.ts");
124
+ const { createTask } = await import("../tasks/store.ts");
125
+
126
+ const config = await loadConfig(dir);
127
+ const evaluation = await evaluateSchedule(config, s);
128
+
129
+ if (evaluation.tasksToCreate.length === 0) {
130
+ logger.dim("Schedule evaluated but produced no tasks.");
131
+ } else {
132
+ const createdIds: string[] = [];
133
+ for (const taskDef of evaluation.tasksToCreate) {
134
+ const blockedBy = (taskDef.depends_on ?? [])
135
+ .map((i: number) => createdIds[i])
136
+ .filter(Boolean) as string[];
137
+ const t = await createTask(dir, {
138
+ name: taskDef.name,
139
+ description: taskDef.description,
140
+ priority: taskDef.priority,
141
+ blocked_by: blockedBy,
142
+ });
143
+ createdIds.push(t.id);
144
+ logger.success(`Created task: ${t.name} (${t.id})`);
162
145
  }
146
+ }
163
147
 
164
- await markScheduleRun(conn, s.id);
165
- logger.info(`Marked schedule "${s.name}" as run.`);
166
- }),
167
- );
148
+ await markScheduleRun(dir, s.id);
149
+ logger.info(`Marked schedule "${s.name}" as run.`);
150
+ });
168
151
  }
169
152
 
170
153
  function enabledColor(enabled: boolean): string {
@@ -173,9 +156,7 @@ function enabledColor(enabled: boolean): string {
173
156
 
174
157
  function printSchedule(s: Schedule) {
175
158
  const id = ansis.dim(s.id);
176
- const lastRun = s.last_run_at
177
- ? s.last_run_at.toISOString()
178
- : ansis.dim("never");
159
+ const lastRun = s.last_run_at ?? ansis.dim("never");
179
160
  console.log(
180
161
  ` ${id} ${enabledColor(s.enabled)} ${s.frequency} ${s.name} (last: ${lastRun})`,
181
162
  );
@@ -187,9 +168,7 @@ function printScheduleDetail(s: Schedule) {
187
168
  console.log(` Status: ${enabledColor(s.enabled)}`);
188
169
  console.log(` Frequency: ${s.frequency}`);
189
170
  if (s.description) console.log(` Description: ${s.description}`);
190
- console.log(
191
- ` Last run: ${s.last_run_at ? s.last_run_at.toISOString() : ansis.dim("never")}`,
192
- );
193
- console.log(` Created: ${s.created_at.toISOString()}`);
194
- console.log(` Updated: ${s.updated_at.toISOString()}`);
171
+ console.log(` Last run: ${s.last_run_at ?? ansis.dim("never")}`);
172
+ console.log(` Created: ${s.created_at}`);
173
+ console.log(` Updated: ${s.updated_at}`);
195
174
  }
@@ -13,7 +13,7 @@ export function registerSkillCommand(program: Command) {
13
13
 
14
14
  skill
15
15
  .command("validate [file]")
16
- .description("Validate skill files in .botholomew/skills/")
16
+ .description("Validate skill files in skills/")
17
17
  .action(async (file?: string) => {
18
18
  const dir = program.opts().dir;
19
19
 
@@ -26,7 +26,7 @@ export function registerSkillCommand(program: Command) {
26
26
 
27
27
  skill
28
28
  .command("list")
29
- .description("List all skills loaded from .botholomew/skills/")
29
+ .description("List all skills loaded from skills/")
30
30
  .option("-l, --limit <n>", "max number of skills", Number.parseInt)
31
31
  .option("-o, --offset <n>", "skip first N skills", Number.parseInt)
32
32
  .action(async (opts: { limit?: number; offset?: number }) => {