@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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/agents/agents.test.d.ts +1 -0
- package/dist/agents/agents.test.js +167 -0
- package/dist/agents/getActiveAgents.d.ts +6 -0
- package/dist/agents/getActiveAgents.js +11 -0
- package/dist/agents/getAgent.d.ts +6 -0
- package/dist/agents/getAgent.js +7 -0
- package/dist/agents/getAgentBySessionId.d.ts +10 -0
- package/dist/agents/getAgentBySessionId.js +17 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents/markAgentDead.d.ts +6 -0
- package/dist/agents/markAgentDead.js +26 -0
- package/dist/agents/markAgentIdle.d.ts +5 -0
- package/dist/agents/markAgentIdle.js +12 -0
- package/dist/agents/registerAgent.d.ts +6 -0
- package/dist/agents/registerAgent.js +29 -0
- package/dist/agents/types.d.ts +30 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/unregisterAgent.d.ts +5 -0
- package/dist/agents/unregisterAgent.js +8 -0
- package/dist/agents/updateAgentContext.d.ts +5 -0
- package/dist/agents/updateAgentContext.js +12 -0
- package/dist/agents/updateAgentTask.d.ts +5 -0
- package/dist/agents/updateAgentTask.js +12 -0
- package/dist/agents/updateAgentWorktree.d.ts +5 -0
- package/dist/agents/updateAgentWorktree.js +12 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/init.d.ts +14 -0
- package/dist/cli/init.js +71 -0
- package/dist/cli/install.d.ts +8 -0
- package/dist/cli/install.js +47 -0
- package/dist/cli/join.d.ts +9 -0
- package/dist/cli/join.js +38 -0
- package/dist/cli/registerMcp.d.ts +28 -0
- package/dist/cli/registerMcp.js +138 -0
- package/dist/cli/status.d.ts +8 -0
- package/dist/cli/status.js +82 -0
- package/dist/cli/watch.d.ts +6 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +49 -0
- package/dist/coordinator/coordinator.test.d.ts +1 -0
- package/dist/coordinator/coordinator.test.js +171 -0
- package/dist/coordinator/index.d.ts +16 -0
- package/dist/coordinator/index.js +166 -0
- package/dist/coordinator/spawn.d.ts +22 -0
- package/dist/coordinator/spawn.js +66 -0
- package/dist/datetime/datetime.test.d.ts +1 -0
- package/dist/datetime/datetime.test.js +63 -0
- package/dist/datetime/formatDate.d.ts +6 -0
- package/dist/datetime/formatDate.js +11 -0
- package/dist/datetime/formatDatetime.d.ts +6 -0
- package/dist/datetime/formatDatetime.js +12 -0
- package/dist/datetime/formatTime.d.ts +6 -0
- package/dist/datetime/formatTime.js +11 -0
- package/dist/datetime/index.d.ts +4 -0
- package/dist/datetime/index.js +7 -0
- package/dist/datetime/isStale.d.ts +10 -0
- package/dist/datetime/isStale.js +18 -0
- package/dist/datetime/now.d.ts +6 -0
- package/dist/datetime/now.js +9 -0
- package/dist/datetime/parseDatetime.d.ts +7 -0
- package/dist/datetime/parseDatetime.js +28 -0
- package/dist/db/constants.d.ts +4 -0
- package/dist/db/constants.js +6 -0
- package/dist/db/db.test.d.ts +1 -0
- package/dist/db/db.test.js +141 -0
- package/dist/db/ensureProjectDirs.d.ts +4 -0
- package/dist/db/ensureProjectDirs.js +12 -0
- package/dist/db/getConnection.d.ts +19 -0
- package/dist/db/getConnection.js +51 -0
- package/dist/db/getCurrentProject.d.ts +8 -0
- package/dist/db/getCurrentProject.js +14 -0
- package/dist/db/getProjectPaths.d.ts +21 -0
- package/dist/db/getProjectPaths.js +26 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +13 -0
- package/dist/db/initializeDb.d.ts +7 -0
- package/dist/db/initializeDb.js +23 -0
- package/dist/db/nextEventSeq.d.ts +5 -0
- package/dist/db/nextEventSeq.js +13 -0
- package/dist/db/nextSubtaskSeq.d.ts +5 -0
- package/dist/db/nextSubtaskSeq.js +12 -0
- package/dist/db/nextTaskSeq.d.ts +5 -0
- package/dist/db/nextTaskSeq.js +13 -0
- package/dist/db/resetDb.d.ts +10 -0
- package/dist/db/resetDb.js +36 -0
- package/dist/events/emit.d.ts +6 -0
- package/dist/events/emit.js +31 -0
- package/dist/events/events.test.d.ts +1 -0
- package/dist/events/events.test.js +145 -0
- package/dist/events/getEventsByAgent.d.ts +6 -0
- package/dist/events/getEventsByAgent.js +14 -0
- package/dist/events/getEventsByBranch.d.ts +6 -0
- package/dist/events/getEventsByBranch.js +12 -0
- package/dist/events/getEventsByPlan.d.ts +6 -0
- package/dist/events/getEventsByPlan.js +14 -0
- package/dist/events/getEventsByWorktree.d.ts +6 -0
- package/dist/events/getEventsByWorktree.js +12 -0
- package/dist/events/getEventsSince.d.ts +12 -0
- package/dist/events/getEventsSince.js +47 -0
- package/dist/events/getRecentEvents.d.ts +6 -0
- package/dist/events/getRecentEvents.js +12 -0
- package/dist/events/index.d.ts +8 -0
- package/dist/events/index.js +9 -0
- package/dist/events/types.d.ts +34 -0
- package/dist/events/types.js +1 -0
- package/dist/git/getBranch.d.ts +4 -0
- package/dist/git/getBranch.js +14 -0
- package/dist/git/getCurrentWorktree.d.ts +5 -0
- package/dist/git/getCurrentWorktree.js +15 -0
- package/dist/git/getGitInfo.d.ts +10 -0
- package/dist/git/getGitInfo.js +23 -0
- package/dist/git/getRepoName.d.ts +4 -0
- package/dist/git/getRepoName.js +32 -0
- package/dist/git/getRepoRoot.d.ts +4 -0
- package/dist/git/getRepoRoot.js +14 -0
- package/dist/git/getWorktrees.d.ts +10 -0
- package/dist/git/getWorktrees.js +39 -0
- package/dist/git/index.d.ts +9 -0
- package/dist/git/index.js +7 -0
- package/dist/git/isGitRepo.d.ts +4 -0
- package/dist/git/isGitRepo.js +13 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/sessionStart.d.ts +21 -0
- package/dist/hooks/sessionStart.js +93 -0
- package/dist/ids/generateHex.d.ts +4 -0
- package/dist/ids/generateHex.js +7 -0
- package/dist/ids/getParentTaskId.d.ts +7 -0
- package/dist/ids/getParentTaskId.js +15 -0
- package/dist/ids/getPlanHexFromTaskId.d.ts +6 -0
- package/dist/ids/getPlanHexFromTaskId.js +9 -0
- package/dist/ids/ids.test.d.ts +1 -0
- package/dist/ids/ids.test.js +215 -0
- package/dist/ids/index.d.ts +16 -0
- package/dist/ids/index.js +17 -0
- package/dist/ids/isSubtask.d.ts +7 -0
- package/dist/ids/isSubtask.js +11 -0
- package/dist/ids/isValidId.d.ts +9 -0
- package/dist/ids/isValidId.js +22 -0
- package/dist/ids/makeAgentId.d.ts +8 -0
- package/dist/ids/makeAgentId.js +15 -0
- package/dist/ids/makeEventId.d.ts +11 -0
- package/dist/ids/makeEventId.js +12 -0
- package/dist/ids/makePlanId.d.ts +11 -0
- package/dist/ids/makePlanId.js +15 -0
- package/dist/ids/makeSubtaskId.d.ts +8 -0
- package/dist/ids/makeSubtaskId.js +15 -0
- package/dist/ids/makeTaskId.d.ts +8 -0
- package/dist/ids/makeTaskId.js +14 -0
- package/dist/ids/makeWorktreeId.d.ts +5 -0
- package/dist/ids/makeWorktreeId.js +12 -0
- package/dist/ids/parseId.d.ts +11 -0
- package/dist/ids/parseId.js +26 -0
- package/dist/ids/sanitizeLabel.d.ts +7 -0
- package/dist/ids/sanitizeLabel.js +12 -0
- package/dist/ids/typedIds.d.ts +34 -0
- package/dist/ids/typedIds.js +22 -0
- package/dist/ids/types.d.ts +14 -0
- package/dist/ids/types.js +1 -0
- package/dist/init/claudeConfig.d.ts +39 -0
- package/dist/init/claudeConfig.js +161 -0
- package/dist/llm/extractTasks.d.ts +28 -0
- package/dist/llm/extractTasks.js +108 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.js +2 -0
- package/dist/llm/reconcileTasks.d.ts +21 -0
- package/dist/llm/reconcileTasks.js +82 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +100 -0
- package/dist/mcp/tools/emitEvent.d.ts +62 -0
- package/dist/mcp/tools/emitEvent.js +84 -0
- package/dist/mcp/tools/events.d.ts +55 -0
- package/dist/mcp/tools/events.js +56 -0
- package/dist/mcp/tools/index.d.ts +18 -0
- package/dist/mcp/tools/index.js +13 -0
- package/dist/mcp/tools/query.d.ts +54 -0
- package/dist/mcp/tools/query.js +70 -0
- package/dist/mcp/tools/register.d.ts +47 -0
- package/dist/mcp/tools/register.js +79 -0
- package/dist/mcp/tools/reset.d.ts +38 -0
- package/dist/mcp/tools/reset.js +56 -0
- package/dist/mcp/tools/setup.d.ts +42 -0
- package/dist/mcp/tools/setup.js +75 -0
- package/dist/mcp/tools/status.d.ts +44 -0
- package/dist/mcp/tools/status.js +74 -0
- package/dist/mcp/tools/tasks.d.ts +116 -0
- package/dist/mcp/tools/tasks.js +143 -0
- package/dist/mcp/tools/worktreeCleanup.d.ts +38 -0
- package/dist/mcp/tools/worktreeCleanup.js +67 -0
- package/dist/plans/createPlan.d.ts +6 -0
- package/dist/plans/createPlan.js +29 -0
- package/dist/plans/getActivePlans.d.ts +6 -0
- package/dist/plans/getActivePlans.js +11 -0
- package/dist/plans/getPlan.d.ts +6 -0
- package/dist/plans/getPlan.js +7 -0
- package/dist/plans/index.d.ts +5 -0
- package/dist/plans/index.js +5 -0
- package/dist/plans/plans.test.d.ts +1 -0
- package/dist/plans/plans.test.js +107 -0
- package/dist/plans/types.d.ts +32 -0
- package/dist/plans/types.js +1 -0
- package/dist/plans/updatePlanStatus.d.ts +6 -0
- package/dist/plans/updatePlanStatus.js +8 -0
- package/dist/tasks/assignTask.d.ts +7 -0
- package/dist/tasks/assignTask.js +20 -0
- package/dist/tasks/blockTask.d.ts +5 -0
- package/dist/tasks/blockTask.js +12 -0
- package/dist/tasks/claimTask.d.ts +7 -0
- package/dist/tasks/claimTask.js +21 -0
- package/dist/tasks/completeTask.d.ts +6 -0
- package/dist/tasks/completeTask.js +18 -0
- package/dist/tasks/createTask.d.ts +6 -0
- package/dist/tasks/createTask.js +36 -0
- package/dist/tasks/getPendingTasks.d.ts +6 -0
- package/dist/tasks/getPendingTasks.js +19 -0
- package/dist/tasks/getTask.d.ts +6 -0
- package/dist/tasks/getTask.js +7 -0
- package/dist/tasks/getTasksByPlan.d.ts +6 -0
- package/dist/tasks/getTasksByPlan.js +11 -0
- package/dist/tasks/index.d.ts +11 -0
- package/dist/tasks/index.js +12 -0
- package/dist/tasks/startTask.d.ts +5 -0
- package/dist/tasks/startTask.js +12 -0
- package/dist/tasks/tasks.test.d.ts +1 -0
- package/dist/tasks/tasks.test.js +209 -0
- package/dist/tasks/types.d.ts +36 -0
- package/dist/tasks/types.js +1 -0
- package/dist/tasks/unclaimTask.d.ts +5 -0
- package/dist/tasks/unclaimTask.js +12 -0
- package/dist/test/factories/agentFactory.d.ts +13 -0
- package/dist/test/factories/agentFactory.js +5 -0
- package/dist/test/factories/eventFactory.d.ts +20 -0
- package/dist/test/factories/eventFactory.js +16 -0
- package/dist/test/factories/factories.test.d.ts +1 -0
- package/dist/test/factories/factories.test.js +101 -0
- package/dist/test/factories/index.d.ts +4 -0
- package/dist/test/factories/index.js +4 -0
- package/dist/test/factories/planFactory.d.ts +15 -0
- package/dist/test/factories/planFactory.js +14 -0
- package/dist/test/factories/taskFactory.d.ts +22 -0
- package/dist/test/factories/taskFactory.js +44 -0
- package/dist/test/multiAgentDemo.d.ts +16 -0
- package/dist/test/multiAgentDemo.js +20 -0
- package/dist/test/multiAgentDemo.test.d.ts +1 -0
- package/dist/test/multiAgentDemo.test.js +14 -0
- package/dist/test/setup.d.ts +9 -0
- package/dist/test/setup.js +50 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/math.d.ts +6 -0
- package/dist/utils/math.js +10 -0
- package/dist/utils/math.test.d.ts +1 -0
- package/dist/utils/math.test.js +26 -0
- package/dist/utils/string.d.ts +11 -0
- package/dist/utils/string.js +17 -0
- package/dist/utils/string.test.d.ts +1 -0
- package/dist/utils/string.test.js +30 -0
- package/dist/watcher/index.d.ts +1 -0
- package/dist/watcher/index.js +1 -0
- package/dist/watcher/planWatcher.d.ts +31 -0
- package/dist/watcher/planWatcher.js +171 -0
- package/dist/worktrees/getAllWorktrees.d.ts +6 -0
- package/dist/worktrees/getAllWorktrees.js +10 -0
- package/dist/worktrees/getWorktreeById.d.ts +6 -0
- package/dist/worktrees/getWorktreeById.js +7 -0
- package/dist/worktrees/getWorktreeByPath.d.ts +6 -0
- package/dist/worktrees/getWorktreeByPath.js +7 -0
- package/dist/worktrees/index.d.ts +9 -0
- package/dist/worktrees/index.js +13 -0
- package/dist/worktrees/registerWorktree.d.ts +6 -0
- package/dist/worktrees/registerWorktree.js +28 -0
- package/dist/worktrees/removeWorktree.d.ts +6 -0
- package/dist/worktrees/removeWorktree.js +9 -0
- package/dist/worktrees/syncWorktreesFromGit.d.ts +7 -0
- package/dist/worktrees/syncWorktreesFromGit.js +51 -0
- package/dist/worktrees/types.d.ts +29 -0
- package/dist/worktrees/types.js +1 -0
- package/dist/worktrees/updateWorktreeCommit.d.ts +5 -0
- package/dist/worktrees/updateWorktreeCommit.js +13 -0
- package/dist/worktrees/updateWorktreeSeen.d.ts +5 -0
- package/dist/worktrees/updateWorktreeSeen.js +13 -0
- 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,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,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,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,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,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,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,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
|
+
});
|