@inixiative/hivemind 0.1.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 (287) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/agents/agents.test.d.ts +1 -0
  4. package/dist/agents/agents.test.js +167 -0
  5. package/dist/agents/getActiveAgents.d.ts +6 -0
  6. package/dist/agents/getActiveAgents.js +11 -0
  7. package/dist/agents/getAgent.d.ts +6 -0
  8. package/dist/agents/getAgent.js +7 -0
  9. package/dist/agents/getAgentBySessionId.d.ts +10 -0
  10. package/dist/agents/getAgentBySessionId.js +17 -0
  11. package/dist/agents/index.d.ts +10 -0
  12. package/dist/agents/index.js +12 -0
  13. package/dist/agents/markAgentDead.d.ts +6 -0
  14. package/dist/agents/markAgentDead.js +26 -0
  15. package/dist/agents/markAgentIdle.d.ts +5 -0
  16. package/dist/agents/markAgentIdle.js +12 -0
  17. package/dist/agents/registerAgent.d.ts +6 -0
  18. package/dist/agents/registerAgent.js +29 -0
  19. package/dist/agents/types.d.ts +30 -0
  20. package/dist/agents/types.js +1 -0
  21. package/dist/agents/unregisterAgent.d.ts +5 -0
  22. package/dist/agents/unregisterAgent.js +8 -0
  23. package/dist/agents/updateAgentContext.d.ts +5 -0
  24. package/dist/agents/updateAgentContext.js +12 -0
  25. package/dist/agents/updateAgentTask.d.ts +5 -0
  26. package/dist/agents/updateAgentTask.js +12 -0
  27. package/dist/agents/updateAgentWorktree.d.ts +5 -0
  28. package/dist/agents/updateAgentWorktree.js +12 -0
  29. package/dist/cli/index.d.ts +8 -0
  30. package/dist/cli/index.js +8 -0
  31. package/dist/cli/init.d.ts +14 -0
  32. package/dist/cli/init.js +71 -0
  33. package/dist/cli/install.d.ts +8 -0
  34. package/dist/cli/install.js +47 -0
  35. package/dist/cli/join.d.ts +9 -0
  36. package/dist/cli/join.js +38 -0
  37. package/dist/cli/registerMcp.d.ts +28 -0
  38. package/dist/cli/registerMcp.js +138 -0
  39. package/dist/cli/status.d.ts +8 -0
  40. package/dist/cli/status.js +82 -0
  41. package/dist/cli/watch.d.ts +6 -0
  42. package/dist/cli/watch.js +68 -0
  43. package/dist/cli.d.ts +12 -0
  44. package/dist/cli.js +49 -0
  45. package/dist/coordinator/coordinator.test.d.ts +1 -0
  46. package/dist/coordinator/coordinator.test.js +171 -0
  47. package/dist/coordinator/index.d.ts +16 -0
  48. package/dist/coordinator/index.js +166 -0
  49. package/dist/coordinator/spawn.d.ts +22 -0
  50. package/dist/coordinator/spawn.js +66 -0
  51. package/dist/datetime/datetime.test.d.ts +1 -0
  52. package/dist/datetime/datetime.test.js +63 -0
  53. package/dist/datetime/formatDate.d.ts +6 -0
  54. package/dist/datetime/formatDate.js +11 -0
  55. package/dist/datetime/formatDatetime.d.ts +6 -0
  56. package/dist/datetime/formatDatetime.js +12 -0
  57. package/dist/datetime/formatTime.d.ts +6 -0
  58. package/dist/datetime/formatTime.js +11 -0
  59. package/dist/datetime/index.d.ts +4 -0
  60. package/dist/datetime/index.js +7 -0
  61. package/dist/datetime/isStale.d.ts +10 -0
  62. package/dist/datetime/isStale.js +18 -0
  63. package/dist/datetime/now.d.ts +6 -0
  64. package/dist/datetime/now.js +9 -0
  65. package/dist/datetime/parseDatetime.d.ts +7 -0
  66. package/dist/datetime/parseDatetime.js +28 -0
  67. package/dist/db/constants.d.ts +4 -0
  68. package/dist/db/constants.js +6 -0
  69. package/dist/db/db.test.d.ts +1 -0
  70. package/dist/db/db.test.js +141 -0
  71. package/dist/db/ensureProjectDirs.d.ts +4 -0
  72. package/dist/db/ensureProjectDirs.js +12 -0
  73. package/dist/db/getConnection.d.ts +19 -0
  74. package/dist/db/getConnection.js +51 -0
  75. package/dist/db/getCurrentProject.d.ts +8 -0
  76. package/dist/db/getCurrentProject.js +14 -0
  77. package/dist/db/getProjectPaths.d.ts +21 -0
  78. package/dist/db/getProjectPaths.js +26 -0
  79. package/dist/db/index.d.ts +10 -0
  80. package/dist/db/index.js +13 -0
  81. package/dist/db/initializeDb.d.ts +7 -0
  82. package/dist/db/initializeDb.js +23 -0
  83. package/dist/db/nextEventSeq.d.ts +5 -0
  84. package/dist/db/nextEventSeq.js +13 -0
  85. package/dist/db/nextSubtaskSeq.d.ts +5 -0
  86. package/dist/db/nextSubtaskSeq.js +12 -0
  87. package/dist/db/nextTaskSeq.d.ts +5 -0
  88. package/dist/db/nextTaskSeq.js +13 -0
  89. package/dist/db/resetDb.d.ts +10 -0
  90. package/dist/db/resetDb.js +36 -0
  91. package/dist/events/emit.d.ts +6 -0
  92. package/dist/events/emit.js +31 -0
  93. package/dist/events/events.test.d.ts +1 -0
  94. package/dist/events/events.test.js +145 -0
  95. package/dist/events/getEventsByAgent.d.ts +6 -0
  96. package/dist/events/getEventsByAgent.js +14 -0
  97. package/dist/events/getEventsByBranch.d.ts +6 -0
  98. package/dist/events/getEventsByBranch.js +12 -0
  99. package/dist/events/getEventsByPlan.d.ts +6 -0
  100. package/dist/events/getEventsByPlan.js +14 -0
  101. package/dist/events/getEventsByWorktree.d.ts +6 -0
  102. package/dist/events/getEventsByWorktree.js +12 -0
  103. package/dist/events/getEventsSince.d.ts +12 -0
  104. package/dist/events/getEventsSince.js +47 -0
  105. package/dist/events/getRecentEvents.d.ts +6 -0
  106. package/dist/events/getRecentEvents.js +12 -0
  107. package/dist/events/index.d.ts +8 -0
  108. package/dist/events/index.js +9 -0
  109. package/dist/events/types.d.ts +34 -0
  110. package/dist/events/types.js +1 -0
  111. package/dist/git/getBranch.d.ts +4 -0
  112. package/dist/git/getBranch.js +14 -0
  113. package/dist/git/getCurrentWorktree.d.ts +5 -0
  114. package/dist/git/getCurrentWorktree.js +15 -0
  115. package/dist/git/getGitInfo.d.ts +10 -0
  116. package/dist/git/getGitInfo.js +23 -0
  117. package/dist/git/getRepoName.d.ts +4 -0
  118. package/dist/git/getRepoName.js +32 -0
  119. package/dist/git/getRepoRoot.d.ts +4 -0
  120. package/dist/git/getRepoRoot.js +14 -0
  121. package/dist/git/getWorktrees.d.ts +10 -0
  122. package/dist/git/getWorktrees.js +39 -0
  123. package/dist/git/index.d.ts +9 -0
  124. package/dist/git/index.js +7 -0
  125. package/dist/git/isGitRepo.d.ts +4 -0
  126. package/dist/git/isGitRepo.js +13 -0
  127. package/dist/hooks/index.d.ts +1 -0
  128. package/dist/hooks/index.js +1 -0
  129. package/dist/hooks/sessionStart.d.ts +21 -0
  130. package/dist/hooks/sessionStart.js +93 -0
  131. package/dist/ids/generateHex.d.ts +4 -0
  132. package/dist/ids/generateHex.js +7 -0
  133. package/dist/ids/getParentTaskId.d.ts +7 -0
  134. package/dist/ids/getParentTaskId.js +15 -0
  135. package/dist/ids/getPlanHexFromTaskId.d.ts +6 -0
  136. package/dist/ids/getPlanHexFromTaskId.js +9 -0
  137. package/dist/ids/ids.test.d.ts +1 -0
  138. package/dist/ids/ids.test.js +215 -0
  139. package/dist/ids/index.d.ts +16 -0
  140. package/dist/ids/index.js +17 -0
  141. package/dist/ids/isSubtask.d.ts +7 -0
  142. package/dist/ids/isSubtask.js +11 -0
  143. package/dist/ids/isValidId.d.ts +9 -0
  144. package/dist/ids/isValidId.js +22 -0
  145. package/dist/ids/makeAgentId.d.ts +8 -0
  146. package/dist/ids/makeAgentId.js +15 -0
  147. package/dist/ids/makeEventId.d.ts +11 -0
  148. package/dist/ids/makeEventId.js +12 -0
  149. package/dist/ids/makePlanId.d.ts +11 -0
  150. package/dist/ids/makePlanId.js +15 -0
  151. package/dist/ids/makeSubtaskId.d.ts +8 -0
  152. package/dist/ids/makeSubtaskId.js +15 -0
  153. package/dist/ids/makeTaskId.d.ts +8 -0
  154. package/dist/ids/makeTaskId.js +14 -0
  155. package/dist/ids/makeWorktreeId.d.ts +5 -0
  156. package/dist/ids/makeWorktreeId.js +12 -0
  157. package/dist/ids/parseId.d.ts +11 -0
  158. package/dist/ids/parseId.js +26 -0
  159. package/dist/ids/sanitizeLabel.d.ts +7 -0
  160. package/dist/ids/sanitizeLabel.js +12 -0
  161. package/dist/ids/typedIds.d.ts +34 -0
  162. package/dist/ids/typedIds.js +22 -0
  163. package/dist/ids/types.d.ts +14 -0
  164. package/dist/ids/types.js +1 -0
  165. package/dist/init/claudeConfig.d.ts +39 -0
  166. package/dist/init/claudeConfig.js +161 -0
  167. package/dist/llm/extractTasks.d.ts +28 -0
  168. package/dist/llm/extractTasks.js +108 -0
  169. package/dist/llm/index.d.ts +2 -0
  170. package/dist/llm/index.js +2 -0
  171. package/dist/llm/reconcileTasks.d.ts +21 -0
  172. package/dist/llm/reconcileTasks.js +82 -0
  173. package/dist/mcp/server.d.ts +2 -0
  174. package/dist/mcp/server.js +100 -0
  175. package/dist/mcp/tools/emitEvent.d.ts +62 -0
  176. package/dist/mcp/tools/emitEvent.js +84 -0
  177. package/dist/mcp/tools/events.d.ts +55 -0
  178. package/dist/mcp/tools/events.js +56 -0
  179. package/dist/mcp/tools/index.d.ts +18 -0
  180. package/dist/mcp/tools/index.js +13 -0
  181. package/dist/mcp/tools/query.d.ts +54 -0
  182. package/dist/mcp/tools/query.js +70 -0
  183. package/dist/mcp/tools/register.d.ts +47 -0
  184. package/dist/mcp/tools/register.js +79 -0
  185. package/dist/mcp/tools/reset.d.ts +38 -0
  186. package/dist/mcp/tools/reset.js +56 -0
  187. package/dist/mcp/tools/setup.d.ts +42 -0
  188. package/dist/mcp/tools/setup.js +75 -0
  189. package/dist/mcp/tools/status.d.ts +44 -0
  190. package/dist/mcp/tools/status.js +74 -0
  191. package/dist/mcp/tools/tasks.d.ts +116 -0
  192. package/dist/mcp/tools/tasks.js +143 -0
  193. package/dist/mcp/tools/worktreeCleanup.d.ts +38 -0
  194. package/dist/mcp/tools/worktreeCleanup.js +67 -0
  195. package/dist/plans/createPlan.d.ts +6 -0
  196. package/dist/plans/createPlan.js +29 -0
  197. package/dist/plans/getActivePlans.d.ts +6 -0
  198. package/dist/plans/getActivePlans.js +11 -0
  199. package/dist/plans/getPlan.d.ts +6 -0
  200. package/dist/plans/getPlan.js +7 -0
  201. package/dist/plans/index.d.ts +5 -0
  202. package/dist/plans/index.js +5 -0
  203. package/dist/plans/plans.test.d.ts +1 -0
  204. package/dist/plans/plans.test.js +107 -0
  205. package/dist/plans/types.d.ts +32 -0
  206. package/dist/plans/types.js +1 -0
  207. package/dist/plans/updatePlanStatus.d.ts +6 -0
  208. package/dist/plans/updatePlanStatus.js +8 -0
  209. package/dist/tasks/assignTask.d.ts +7 -0
  210. package/dist/tasks/assignTask.js +20 -0
  211. package/dist/tasks/blockTask.d.ts +5 -0
  212. package/dist/tasks/blockTask.js +12 -0
  213. package/dist/tasks/claimTask.d.ts +7 -0
  214. package/dist/tasks/claimTask.js +21 -0
  215. package/dist/tasks/completeTask.d.ts +6 -0
  216. package/dist/tasks/completeTask.js +18 -0
  217. package/dist/tasks/createTask.d.ts +6 -0
  218. package/dist/tasks/createTask.js +36 -0
  219. package/dist/tasks/getPendingTasks.d.ts +6 -0
  220. package/dist/tasks/getPendingTasks.js +19 -0
  221. package/dist/tasks/getTask.d.ts +6 -0
  222. package/dist/tasks/getTask.js +7 -0
  223. package/dist/tasks/getTasksByPlan.d.ts +6 -0
  224. package/dist/tasks/getTasksByPlan.js +11 -0
  225. package/dist/tasks/index.d.ts +11 -0
  226. package/dist/tasks/index.js +12 -0
  227. package/dist/tasks/startTask.d.ts +5 -0
  228. package/dist/tasks/startTask.js +12 -0
  229. package/dist/tasks/tasks.test.d.ts +1 -0
  230. package/dist/tasks/tasks.test.js +209 -0
  231. package/dist/tasks/types.d.ts +36 -0
  232. package/dist/tasks/types.js +1 -0
  233. package/dist/tasks/unclaimTask.d.ts +5 -0
  234. package/dist/tasks/unclaimTask.js +12 -0
  235. package/dist/test/factories/agentFactory.d.ts +13 -0
  236. package/dist/test/factories/agentFactory.js +5 -0
  237. package/dist/test/factories/eventFactory.d.ts +20 -0
  238. package/dist/test/factories/eventFactory.js +16 -0
  239. package/dist/test/factories/factories.test.d.ts +1 -0
  240. package/dist/test/factories/factories.test.js +101 -0
  241. package/dist/test/factories/index.d.ts +4 -0
  242. package/dist/test/factories/index.js +4 -0
  243. package/dist/test/factories/planFactory.d.ts +15 -0
  244. package/dist/test/factories/planFactory.js +14 -0
  245. package/dist/test/factories/taskFactory.d.ts +22 -0
  246. package/dist/test/factories/taskFactory.js +44 -0
  247. package/dist/test/multiAgentDemo.d.ts +16 -0
  248. package/dist/test/multiAgentDemo.js +20 -0
  249. package/dist/test/multiAgentDemo.test.d.ts +1 -0
  250. package/dist/test/multiAgentDemo.test.js +14 -0
  251. package/dist/test/setup.d.ts +9 -0
  252. package/dist/test/setup.js +50 -0
  253. package/dist/utils/index.d.ts +5 -0
  254. package/dist/utils/index.js +5 -0
  255. package/dist/utils/math.d.ts +6 -0
  256. package/dist/utils/math.js +10 -0
  257. package/dist/utils/math.test.d.ts +1 -0
  258. package/dist/utils/math.test.js +26 -0
  259. package/dist/utils/string.d.ts +11 -0
  260. package/dist/utils/string.js +17 -0
  261. package/dist/utils/string.test.d.ts +1 -0
  262. package/dist/utils/string.test.js +30 -0
  263. package/dist/watcher/index.d.ts +1 -0
  264. package/dist/watcher/index.js +1 -0
  265. package/dist/watcher/planWatcher.d.ts +31 -0
  266. package/dist/watcher/planWatcher.js +171 -0
  267. package/dist/worktrees/getAllWorktrees.d.ts +6 -0
  268. package/dist/worktrees/getAllWorktrees.js +10 -0
  269. package/dist/worktrees/getWorktreeById.d.ts +6 -0
  270. package/dist/worktrees/getWorktreeById.js +7 -0
  271. package/dist/worktrees/getWorktreeByPath.d.ts +6 -0
  272. package/dist/worktrees/getWorktreeByPath.js +7 -0
  273. package/dist/worktrees/index.d.ts +9 -0
  274. package/dist/worktrees/index.js +13 -0
  275. package/dist/worktrees/registerWorktree.d.ts +6 -0
  276. package/dist/worktrees/registerWorktree.js +28 -0
  277. package/dist/worktrees/removeWorktree.d.ts +6 -0
  278. package/dist/worktrees/removeWorktree.js +9 -0
  279. package/dist/worktrees/syncWorktreesFromGit.d.ts +7 -0
  280. package/dist/worktrees/syncWorktreesFromGit.js +51 -0
  281. package/dist/worktrees/types.d.ts +29 -0
  282. package/dist/worktrees/types.js +1 -0
  283. package/dist/worktrees/updateWorktreeCommit.d.ts +5 -0
  284. package/dist/worktrees/updateWorktreeCommit.js +13 -0
  285. package/dist/worktrees/updateWorktreeSeen.d.ts +5 -0
  286. package/dist/worktrees/updateWorktreeSeen.js +13 -0
  287. package/package.json +58 -0
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'bun:test';
2
+ import { createTestDb, cleanupAllTestDbs } from '../test/setup';
3
+ import { buildAgent, buildPlan } from '../test/factories';
4
+ import { createPlan } from './createPlan';
5
+ import { getPlan } from './getPlan';
6
+ import { getActivePlans } from './getActivePlans';
7
+ import { updatePlanStatus } from './updatePlanStatus';
8
+ describe('Plans Module', () => {
9
+ let testDb;
10
+ beforeEach(() => {
11
+ testDb = createTestDb();
12
+ });
13
+ afterEach(() => {
14
+ testDb.cleanup();
15
+ });
16
+ afterAll(() => {
17
+ cleanupAllTestDbs();
18
+ });
19
+ describe('createPlan', () => {
20
+ it('creates a new plan', () => {
21
+ const { agent } = buildAgent(testDb.db, { label: 'test' });
22
+ const plan = createPlan(testDb.db, {
23
+ title: 'Implement Auth',
24
+ created_by: agent.id,
25
+ });
26
+ expect(plan.id).toMatch(/^pln_[a-f0-9]{6}$/);
27
+ expect(plan.title).toBe('Implement Auth');
28
+ expect(plan.status).toBe('active');
29
+ expect(plan.created_by).toBe(agent.id);
30
+ });
31
+ it('creates plan with label', () => {
32
+ const { agent } = buildAgent(testDb.db);
33
+ const plan = createPlan(testDb.db, {
34
+ title: 'Auth Feature',
35
+ label: 'auth',
36
+ created_by: agent.id,
37
+ });
38
+ expect(plan.id).toMatch(/^pln_[a-f0-9]{6}_auth$/);
39
+ expect(plan.label).toBe('auth');
40
+ });
41
+ it('creates plan with branch', () => {
42
+ const { agent } = buildAgent(testDb.db);
43
+ const plan = createPlan(testDb.db, {
44
+ title: 'Feature Work',
45
+ branch: 'feature/auth',
46
+ created_by: agent.id,
47
+ });
48
+ expect(plan.branch).toBe('feature/auth');
49
+ });
50
+ it('creates plan with description', () => {
51
+ const { agent } = buildAgent(testDb.db);
52
+ const plan = createPlan(testDb.db, {
53
+ title: 'Complex Feature',
54
+ description: 'This plan implements a complex feature with multiple tasks.',
55
+ created_by: agent.id,
56
+ });
57
+ expect(plan.description).toBe('This plan implements a complex feature with multiple tasks.');
58
+ });
59
+ });
60
+ describe('getPlan', () => {
61
+ it('retrieves existing plan', () => {
62
+ const { plan: created } = buildPlan(testDb.db, { title: 'Test Plan' });
63
+ const retrieved = getPlan(testDb.db, created.id);
64
+ expect(retrieved).toBeDefined();
65
+ expect(retrieved.id).toBe(created.id);
66
+ expect(retrieved.title).toBe('Test Plan');
67
+ });
68
+ it('returns null for non-existent plan', () => {
69
+ const plan = getPlan(testDb.db, 'pln_nonexistent');
70
+ expect(plan).toBeNull();
71
+ });
72
+ });
73
+ describe('getActivePlans', () => {
74
+ it('returns only active plans', () => {
75
+ const { plan: plan1 } = buildPlan(testDb.db, { title: 'Active Plan' });
76
+ const { plan: plan2 } = buildPlan(testDb.db, { title: 'Completed Plan' });
77
+ updatePlanStatus(testDb.db, plan2.id, 'complete');
78
+ const active = getActivePlans(testDb.db);
79
+ expect(active).toHaveLength(1);
80
+ expect(active[0].id).toBe(plan1.id);
81
+ });
82
+ it('returns empty array when no active plans', () => {
83
+ const active = getActivePlans(testDb.db);
84
+ expect(active).toHaveLength(0);
85
+ });
86
+ });
87
+ describe('updatePlanStatus', () => {
88
+ it('updates plan to complete', () => {
89
+ const { plan } = buildPlan(testDb.db, { title: 'Test Plan' });
90
+ const success = updatePlanStatus(testDb.db, plan.id, 'complete');
91
+ expect(success).toBe(true);
92
+ const updated = getPlan(testDb.db, plan.id);
93
+ expect(updated.status).toBe('complete');
94
+ });
95
+ it('updates plan to paused', () => {
96
+ const { plan } = buildPlan(testDb.db, { title: 'Test Plan' });
97
+ const success = updatePlanStatus(testDb.db, plan.id, 'paused');
98
+ expect(success).toBe(true);
99
+ const updated = getPlan(testDb.db, plan.id);
100
+ expect(updated.status).toBe('paused');
101
+ });
102
+ it('returns false for non-existent plan', () => {
103
+ const result = updatePlanStatus(testDb.db, 'pln_nonexistent', 'complete');
104
+ expect(result).toBe(false);
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Plan status values
3
+ */
4
+ export type PlanStatus = 'active' | 'paused' | 'complete';
5
+ /**
6
+ * Plan record as stored in DB
7
+ */
8
+ export type Plan = {
9
+ id: string;
10
+ hex: string;
11
+ label: string | null;
12
+ title: string;
13
+ description: string | null;
14
+ status: PlanStatus;
15
+ branch: string | null;
16
+ worktree_id: string | null;
17
+ claude_session_id: string | null;
18
+ created_at: string;
19
+ created_by: string | null;
20
+ };
21
+ /**
22
+ * Input for creating a plan
23
+ */
24
+ export type CreatePlanInput = {
25
+ title: string;
26
+ description?: string;
27
+ branch?: string;
28
+ worktree_id?: string;
29
+ claude_session_id?: string;
30
+ created_by?: string;
31
+ label?: string;
32
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { PlanStatus } from './types';
3
+ /**
4
+ * Update a plan's status
5
+ */
6
+ export declare function updatePlanStatus(db: Database, planId: string, status: PlanStatus): boolean;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Update a plan's status
3
+ */
4
+ export function updatePlanStatus(db, planId, status) {
5
+ const stmt = db.prepare('UPDATE plans SET status = ? WHERE id = ?');
6
+ const result = stmt.run(status, planId);
7
+ return result.changes > 0;
8
+ }
@@ -0,0 +1,7 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task } from './types';
3
+ /**
4
+ * Assign a task to an agent (force claim, even if already claimed)
5
+ * Use this when coordinating work - e.g., spawning a new agent for a task
6
+ */
7
+ export declare function assignTask(db: Database, taskId: string, agentId: string): Task | null;
@@ -0,0 +1,20 @@
1
+ import { now } from '../datetime/now';
2
+ /**
3
+ * Assign a task to an agent (force claim, even if already claimed)
4
+ * Use this when coordinating work - e.g., spawning a new agent for a task
5
+ */
6
+ export function assignTask(db, taskId, agentId) {
7
+ const timestamp = now();
8
+ const stmt = db.prepare(`
9
+ UPDATE tasks
10
+ SET status = 'claimed', claimed_by = ?, claimed_at = ?
11
+ WHERE id = ?
12
+ `);
13
+ const result = stmt.run(agentId, timestamp, taskId);
14
+ if (result.changes === 0) {
15
+ return null; // Task not found
16
+ }
17
+ // Return updated task
18
+ const getStmt = db.prepare('SELECT * FROM tasks WHERE id = ?');
19
+ return getStmt.get(taskId);
20
+ }
@@ -0,0 +1,5 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ /**
3
+ * Mark a task as blocked
4
+ */
5
+ export declare function blockTask(db: Database, taskId: string, reason?: string): boolean;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Mark a task as blocked
3
+ */
4
+ export function blockTask(db, taskId, reason) {
5
+ const stmt = db.prepare(`
6
+ UPDATE tasks
7
+ SET status = 'blocked'
8
+ WHERE id = ? AND status IN ('claimed', 'in_progress')
9
+ `);
10
+ const result = stmt.run(taskId);
11
+ return result.changes > 0;
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task } from './types';
3
+ /**
4
+ * Claim a task for an agent
5
+ * Returns the task if successful, null if already claimed or not found
6
+ */
7
+ export declare function claimTask(db: Database, taskId: string, agentId: string): Task | null;
@@ -0,0 +1,21 @@
1
+ import { now } from '../datetime/now';
2
+ /**
3
+ * Claim a task for an agent
4
+ * Returns the task if successful, null if already claimed or not found
5
+ */
6
+ export function claimTask(db, taskId, agentId) {
7
+ const timestamp = now();
8
+ // Try to claim (only if pending)
9
+ const stmt = db.prepare(`
10
+ UPDATE tasks
11
+ SET status = 'claimed', claimed_by = ?, claimed_at = ?
12
+ WHERE id = ? AND status = 'pending'
13
+ `);
14
+ const result = stmt.run(agentId, timestamp, taskId);
15
+ if (result.changes === 0) {
16
+ return null; // Already claimed or not found
17
+ }
18
+ // Return updated task
19
+ const getStmt = db.prepare('SELECT * FROM tasks WHERE id = ?');
20
+ return getStmt.get(taskId);
21
+ }
@@ -0,0 +1,6 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task } from './types';
3
+ /**
4
+ * Mark a task as complete with an outcome
5
+ */
6
+ export declare function completeTask(db: Database, taskId: string, outcome?: string): Task | null;
@@ -0,0 +1,18 @@
1
+ import { now } from '../datetime/now';
2
+ /**
3
+ * Mark a task as complete with an outcome
4
+ */
5
+ export function completeTask(db, taskId, outcome) {
6
+ const timestamp = now();
7
+ const stmt = db.prepare(`
8
+ UPDATE tasks
9
+ SET status = 'done', completed_at = ?, outcome = ?
10
+ WHERE id = ? AND status IN ('claimed', 'in_progress')
11
+ `);
12
+ const result = stmt.run(timestamp, outcome ?? null, taskId);
13
+ if (result.changes === 0) {
14
+ return null;
15
+ }
16
+ const getStmt = db.prepare('SELECT * FROM tasks WHERE id = ?');
17
+ return getStmt.get(taskId);
18
+ }
@@ -0,0 +1,6 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task, CreateTaskInput } from './types';
3
+ /**
4
+ * Create a new task in a plan
5
+ */
6
+ export declare function createTask(db: Database, input: CreateTaskInput): Task;
@@ -0,0 +1,36 @@
1
+ import { makeTaskId } from '../ids/makeTaskId';
2
+ import { parseId } from '../ids/parseId';
3
+ import { nextTaskSeq } from '../db/nextTaskSeq';
4
+ /**
5
+ * Create a new task in a plan
6
+ */
7
+ export function createTask(db, input) {
8
+ // Get plan hex from plan ID
9
+ const planParsed = parseId(input.plan_id);
10
+ const seq = nextTaskSeq(db, input.plan_id);
11
+ const seqStr = String(seq).padStart(3, '0');
12
+ const id = makeTaskId(planParsed.hex, seq, input.label);
13
+ const parsed = parseId(id);
14
+ const stmt = db.prepare(`
15
+ INSERT INTO tasks (id, plan_hex, seq, label, plan_id, title, description, status, branch, worktree_id, parent_task_id)
16
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)
17
+ `);
18
+ stmt.run(id, planParsed.hex, seqStr, parsed.label ?? null, input.plan_id, input.title, input.description ?? null, input.branch ?? null, input.worktree_id ?? null, input.parent_task_id ?? null);
19
+ return {
20
+ id,
21
+ plan_hex: planParsed.hex,
22
+ seq: seqStr,
23
+ label: parsed.label ?? null,
24
+ plan_id: input.plan_id,
25
+ title: input.title,
26
+ description: input.description ?? null,
27
+ status: 'pending',
28
+ branch: input.branch ?? null,
29
+ worktree_id: input.worktree_id ?? null,
30
+ claimed_by: null,
31
+ claimed_at: null,
32
+ completed_at: null,
33
+ outcome: null,
34
+ parent_task_id: input.parent_task_id ?? null,
35
+ };
36
+ }
@@ -0,0 +1,6 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task } from './types';
3
+ /**
4
+ * Get all pending (unclaimed) tasks, optionally filtered by plan
5
+ */
6
+ export declare function getPendingTasks(db: Database, planId?: string): Task[];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Get all pending (unclaimed) tasks, optionally filtered by plan
3
+ */
4
+ export function getPendingTasks(db, planId) {
5
+ if (planId) {
6
+ const stmt = db.prepare(`
7
+ SELECT * FROM tasks
8
+ WHERE plan_id = ? AND status = 'pending'
9
+ ORDER BY seq ASC
10
+ `);
11
+ return stmt.all(planId);
12
+ }
13
+ const stmt = db.prepare(`
14
+ SELECT * FROM tasks
15
+ WHERE status = 'pending'
16
+ ORDER BY plan_id, seq ASC
17
+ `);
18
+ return stmt.all();
19
+ }
@@ -0,0 +1,6 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task } from './types';
3
+ /**
4
+ * Get a task by ID
5
+ */
6
+ export declare function getTask(db: Database, taskId: string): Task | null;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Get a task by ID
3
+ */
4
+ export function getTask(db, taskId) {
5
+ const stmt = db.prepare('SELECT * FROM tasks WHERE id = ?');
6
+ return stmt.get(taskId) ?? null;
7
+ }
@@ -0,0 +1,6 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { Task } from './types';
3
+ /**
4
+ * Get all tasks for a plan
5
+ */
6
+ export declare function getTasksByPlan(db: Database, planId: string): Task[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Get all tasks for a plan
3
+ */
4
+ export function getTasksByPlan(db, planId) {
5
+ const stmt = db.prepare(`
6
+ SELECT * FROM tasks
7
+ WHERE plan_id = ?
8
+ ORDER BY seq ASC
9
+ `);
10
+ return stmt.all(planId);
11
+ }
@@ -0,0 +1,11 @@
1
+ export type { TaskStatus, Task, CreateTaskInput } from './types';
2
+ export { createTask } from './createTask';
3
+ export { getTask } from './getTask';
4
+ export { getTasksByPlan } from './getTasksByPlan';
5
+ export { getPendingTasks } from './getPendingTasks';
6
+ export { claimTask } from './claimTask';
7
+ export { assignTask } from './assignTask';
8
+ export { unclaimTask } from './unclaimTask';
9
+ export { startTask } from './startTask';
10
+ export { completeTask } from './completeTask';
11
+ export { blockTask } from './blockTask';
@@ -0,0 +1,12 @@
1
+ // CRUD
2
+ export { createTask } from './createTask';
3
+ export { getTask } from './getTask';
4
+ export { getTasksByPlan } from './getTasksByPlan';
5
+ export { getPendingTasks } from './getPendingTasks';
6
+ // Workflow
7
+ export { claimTask } from './claimTask';
8
+ export { assignTask } from './assignTask';
9
+ export { unclaimTask } from './unclaimTask';
10
+ export { startTask } from './startTask';
11
+ export { completeTask } from './completeTask';
12
+ export { blockTask } from './blockTask';
@@ -0,0 +1,5 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ /**
3
+ * Mark a task as in_progress
4
+ */
5
+ export declare function startTask(db: Database, taskId: string): boolean;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Mark a task as in_progress
3
+ */
4
+ export function startTask(db, taskId) {
5
+ const stmt = db.prepare(`
6
+ UPDATE tasks
7
+ SET status = 'in_progress'
8
+ WHERE id = ? AND status = 'claimed'
9
+ `);
10
+ const result = stmt.run(taskId);
11
+ return result.changes > 0;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'bun:test';
2
+ import { createTestDb, cleanupAllTestDbs } from '../test/setup';
3
+ import { buildAgent, buildPlan, buildTask } from '../test/factories';
4
+ import { createTask } from './createTask';
5
+ import { getTask } from './getTask';
6
+ import { getTasksByPlan } from './getTasksByPlan';
7
+ import { getPendingTasks } from './getPendingTasks';
8
+ import { claimTask } from './claimTask';
9
+ import { assignTask } from './assignTask';
10
+ import { unclaimTask } from './unclaimTask';
11
+ import { startTask } from './startTask';
12
+ import { completeTask } from './completeTask';
13
+ import { blockTask } from './blockTask';
14
+ describe('Tasks Module', () => {
15
+ let testDb;
16
+ beforeEach(() => {
17
+ testDb = createTestDb();
18
+ });
19
+ afterEach(() => {
20
+ testDb.cleanup();
21
+ });
22
+ afterAll(() => {
23
+ cleanupAllTestDbs();
24
+ });
25
+ describe('createTask', () => {
26
+ it('creates a task with auto-generated seq', () => {
27
+ const { plan } = buildPlan(testDb.db);
28
+ const task = createTask(testDb.db, {
29
+ plan_id: plan.id,
30
+ title: 'Implement login',
31
+ });
32
+ expect(task.id).toBe(`tsk_${plan.hex}_001`);
33
+ expect(task.title).toBe('Implement login');
34
+ expect(task.status).toBe('pending');
35
+ expect(task.plan_id).toBe(plan.id);
36
+ });
37
+ it('increments sequence for multiple tasks', () => {
38
+ const { plan } = buildPlan(testDb.db);
39
+ const task1 = createTask(testDb.db, { plan_id: plan.id, title: 'Task 1' });
40
+ const task2 = createTask(testDb.db, { plan_id: plan.id, title: 'Task 2' });
41
+ const task3 = createTask(testDb.db, { plan_id: plan.id, title: 'Task 3' });
42
+ expect(task1.seq).toBe('001');
43
+ expect(task2.seq).toBe('002');
44
+ expect(task3.seq).toBe('003');
45
+ });
46
+ it('creates task with label', () => {
47
+ const { plan } = buildPlan(testDb.db);
48
+ const task = createTask(testDb.db, {
49
+ plan_id: plan.id,
50
+ title: 'Setup Database',
51
+ label: 'setup',
52
+ });
53
+ expect(task.id).toBe(`tsk_${plan.hex}_001_setup`);
54
+ expect(task.label).toBe('setup');
55
+ });
56
+ it('creates task with description', () => {
57
+ const { plan } = buildPlan(testDb.db);
58
+ const task = createTask(testDb.db, {
59
+ plan_id: plan.id,
60
+ title: 'Complex Task',
61
+ description: 'This task requires multiple steps.',
62
+ });
63
+ expect(task.description).toBe('This task requires multiple steps.');
64
+ });
65
+ it('creates task with branch', () => {
66
+ const { plan } = buildPlan(testDb.db);
67
+ const task = createTask(testDb.db, {
68
+ plan_id: plan.id,
69
+ title: 'Feature Task',
70
+ branch: 'feature/specific',
71
+ });
72
+ expect(task.branch).toBe('feature/specific');
73
+ });
74
+ });
75
+ describe('getTask', () => {
76
+ it('retrieves existing task', () => {
77
+ const { task: created } = buildTask(testDb.db, { title: 'Test Task' });
78
+ const retrieved = getTask(testDb.db, created.id);
79
+ expect(retrieved).toBeDefined();
80
+ expect(retrieved.id).toBe(created.id);
81
+ expect(retrieved.title).toBe('Test Task');
82
+ });
83
+ it('returns null for non-existent task', () => {
84
+ const task = getTask(testDb.db, 'tsk_nonexistent_01');
85
+ expect(task).toBeNull();
86
+ });
87
+ });
88
+ describe('getTasksByPlan', () => {
89
+ it('returns all tasks for plan', () => {
90
+ const { plan } = buildPlan(testDb.db);
91
+ buildTask(testDb.db, { plan, title: 'Task 1' });
92
+ buildTask(testDb.db, { plan, title: 'Task 2' });
93
+ buildTask(testDb.db, { plan, title: 'Task 3' });
94
+ const tasks = getTasksByPlan(testDb.db, plan.id);
95
+ expect(tasks).toHaveLength(3);
96
+ });
97
+ it('returns tasks in sequence order', () => {
98
+ const { plan } = buildPlan(testDb.db);
99
+ buildTask(testDb.db, { plan, title: 'First' });
100
+ buildTask(testDb.db, { plan, title: 'Second' });
101
+ buildTask(testDb.db, { plan, title: 'Third' });
102
+ const tasks = getTasksByPlan(testDb.db, plan.id);
103
+ expect(tasks[0].title).toBe('First');
104
+ expect(tasks[1].title).toBe('Second');
105
+ expect(tasks[2].title).toBe('Third');
106
+ });
107
+ });
108
+ describe('getPendingTasks', () => {
109
+ it('returns only pending tasks', () => {
110
+ const { agent } = buildAgent(testDb.db, { label: 'test' });
111
+ const { plan } = buildPlan(testDb.db, { agent });
112
+ const { task: task1 } = buildTask(testDb.db, { plan, title: 'Pending' });
113
+ const { task: task2 } = buildTask(testDb.db, { plan, title: 'To be claimed' });
114
+ // Claim task2 so it's no longer pending
115
+ claimTask(testDb.db, task2.id, agent.id);
116
+ const pending = getPendingTasks(testDb.db);
117
+ expect(pending).toHaveLength(1);
118
+ expect(pending[0].id).toBe(task1.id);
119
+ });
120
+ });
121
+ describe('claimTask', () => {
122
+ it('claims task for agent', () => {
123
+ const { agent } = buildAgent(testDb.db);
124
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
125
+ const claimed = claimTask(testDb.db, task.id, agent.id);
126
+ expect(claimed).toBeDefined();
127
+ expect(claimed.claimed_by).toBe(agent.id);
128
+ expect(claimed.claimed_at).toBeDefined();
129
+ });
130
+ it('returns null if task already claimed', () => {
131
+ const { agent: agent1 } = buildAgent(testDb.db, { label: 'one' });
132
+ const { agent: agent2 } = buildAgent(testDb.db, { label: 'two' });
133
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
134
+ claimTask(testDb.db, task.id, agent1.id);
135
+ const result = claimTask(testDb.db, task.id, agent2.id);
136
+ expect(result).toBeNull();
137
+ });
138
+ });
139
+ describe('assignTask', () => {
140
+ it('assigns task to agent (even if claimed)', () => {
141
+ const { agent: agent1 } = buildAgent(testDb.db, { label: 'one' });
142
+ const { agent: agent2 } = buildAgent(testDb.db, { label: 'two' });
143
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
144
+ claimTask(testDb.db, task.id, agent1.id);
145
+ const assigned = assignTask(testDb.db, task.id, agent2.id);
146
+ expect(assigned).toBeDefined();
147
+ expect(assigned.claimed_by).toBe(agent2.id);
148
+ });
149
+ });
150
+ describe('unclaimTask', () => {
151
+ it('releases task claim', () => {
152
+ const { agent } = buildAgent(testDb.db);
153
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
154
+ claimTask(testDb.db, task.id, agent.id);
155
+ const success = unclaimTask(testDb.db, task.id);
156
+ expect(success).toBe(true);
157
+ // Verify the task was updated
158
+ const updated = getTask(testDb.db, task.id);
159
+ expect(updated.claimed_by).toBeNull();
160
+ expect(updated.claimed_at).toBeNull();
161
+ expect(updated.status).toBe('pending');
162
+ });
163
+ });
164
+ describe('startTask', () => {
165
+ it('changes status to in_progress', () => {
166
+ const { agent } = buildAgent(testDb.db);
167
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
168
+ // Must claim first since startTask requires 'claimed' status
169
+ claimTask(testDb.db, task.id, agent.id);
170
+ const success = startTask(testDb.db, task.id);
171
+ expect(success).toBe(true);
172
+ // Verify the task was updated
173
+ const updated = getTask(testDb.db, task.id);
174
+ expect(updated.status).toBe('in_progress');
175
+ });
176
+ });
177
+ describe('completeTask', () => {
178
+ it('changes status to done', () => {
179
+ const { agent } = buildAgent(testDb.db);
180
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
181
+ // Must claim first since completeTask requires 'claimed' or 'in_progress'
182
+ claimTask(testDb.db, task.id, agent.id);
183
+ const completed = completeTask(testDb.db, task.id);
184
+ expect(completed).toBeDefined();
185
+ expect(completed.status).toBe('done');
186
+ expect(completed.completed_at).toBeDefined();
187
+ });
188
+ it('stores outcome', () => {
189
+ const { agent } = buildAgent(testDb.db);
190
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
191
+ claimTask(testDb.db, task.id, agent.id);
192
+ const completed = completeTask(testDb.db, task.id, 'Successfully implemented');
193
+ expect(completed.outcome).toBe('Successfully implemented');
194
+ });
195
+ });
196
+ describe('blockTask', () => {
197
+ it('changes status to blocked', () => {
198
+ const { agent } = buildAgent(testDb.db);
199
+ const { task } = buildTask(testDb.db, { title: 'Test Task' });
200
+ // Must claim first since blockTask requires 'claimed' or 'in_progress'
201
+ claimTask(testDb.db, task.id, agent.id);
202
+ const success = blockTask(testDb.db, task.id, 'Waiting for API');
203
+ expect(success).toBe(true);
204
+ // Verify the task was updated
205
+ const updated = getTask(testDb.db, task.id);
206
+ expect(updated.status).toBe('blocked');
207
+ });
208
+ });
209
+ });