hzl-core 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 (177) hide show
  1. package/dist/__tests__/backup/backup-restore.test.d.ts +2 -0
  2. package/dist/__tests__/backup/backup-restore.test.d.ts.map +1 -0
  3. package/dist/__tests__/backup/backup-restore.test.js +200 -0
  4. package/dist/__tests__/backup/backup-restore.test.js.map +1 -0
  5. package/dist/__tests__/backup/import-export.test.d.ts +2 -0
  6. package/dist/__tests__/backup/import-export.test.d.ts.map +1 -0
  7. package/dist/__tests__/backup/import-export.test.js +341 -0
  8. package/dist/__tests__/backup/import-export.test.js.map +1 -0
  9. package/dist/__tests__/concurrency/stress.test.d.ts +2 -0
  10. package/dist/__tests__/concurrency/stress.test.d.ts.map +1 -0
  11. package/dist/__tests__/concurrency/stress.test.js +274 -0
  12. package/dist/__tests__/concurrency/stress.test.js.map +1 -0
  13. package/dist/__tests__/concurrency/worker.d.ts +2 -0
  14. package/dist/__tests__/concurrency/worker.d.ts.map +1 -0
  15. package/dist/__tests__/concurrency/worker.js +84 -0
  16. package/dist/__tests__/concurrency/worker.js.map +1 -0
  17. package/dist/__tests__/migrations/upgrade.test.d.ts +2 -0
  18. package/dist/__tests__/migrations/upgrade.test.d.ts.map +1 -0
  19. package/dist/__tests__/migrations/upgrade.test.js +203 -0
  20. package/dist/__tests__/migrations/upgrade.test.js.map +1 -0
  21. package/dist/__tests__/projections/rebuild-equivalence.test.d.ts +2 -0
  22. package/dist/__tests__/projections/rebuild-equivalence.test.d.ts.map +1 -0
  23. package/dist/__tests__/projections/rebuild-equivalence.test.js +276 -0
  24. package/dist/__tests__/projections/rebuild-equivalence.test.js.map +1 -0
  25. package/dist/__tests__/properties/invariants.test.d.ts +2 -0
  26. package/dist/__tests__/properties/invariants.test.d.ts.map +1 -0
  27. package/dist/__tests__/properties/invariants.test.js +314 -0
  28. package/dist/__tests__/properties/invariants.test.js.map +1 -0
  29. package/dist/db/connection.d.ts +13 -0
  30. package/dist/db/connection.d.ts.map +1 -0
  31. package/dist/db/connection.js +52 -0
  32. package/dist/db/connection.js.map +1 -0
  33. package/dist/db/connection.test.d.ts +2 -0
  34. package/dist/db/connection.test.d.ts.map +1 -0
  35. package/dist/db/connection.test.js +63 -0
  36. package/dist/db/connection.test.js.map +1 -0
  37. package/dist/db/migrations/v2.d.ts +2 -0
  38. package/dist/db/migrations/v2.d.ts.map +1 -0
  39. package/dist/db/migrations/v2.js +4 -0
  40. package/dist/db/migrations/v2.js.map +1 -0
  41. package/dist/db/migrations.d.ts +4 -0
  42. package/dist/db/migrations.d.ts.map +1 -0
  43. package/dist/db/migrations.js +45 -0
  44. package/dist/db/migrations.js.map +1 -0
  45. package/dist/db/migrations.test.d.ts +2 -0
  46. package/dist/db/migrations.test.d.ts.map +1 -0
  47. package/dist/db/migrations.test.js +75 -0
  48. package/dist/db/migrations.test.js.map +1 -0
  49. package/dist/db/schema.d.ts +3 -0
  50. package/dist/db/schema.d.ts.map +1 -0
  51. package/dist/db/schema.js +114 -0
  52. package/dist/db/schema.js.map +1 -0
  53. package/dist/events/store.d.ts +33 -0
  54. package/dist/events/store.d.ts.map +1 -0
  55. package/dist/events/store.js +81 -0
  56. package/dist/events/store.js.map +1 -0
  57. package/dist/events/store.test.d.ts +2 -0
  58. package/dist/events/store.test.d.ts.map +1 -0
  59. package/dist/events/store.test.js +138 -0
  60. package/dist/events/store.test.js.map +1 -0
  61. package/dist/events/types.d.ts +106 -0
  62. package/dist/events/types.d.ts.map +1 -0
  63. package/dist/events/types.js +87 -0
  64. package/dist/events/types.js.map +1 -0
  65. package/dist/events/validation.test.d.ts +2 -0
  66. package/dist/events/validation.test.d.ts.map +1 -0
  67. package/dist/events/validation.test.js +83 -0
  68. package/dist/events/validation.test.js.map +1 -0
  69. package/dist/fixtures/sample-data.d.ts +16 -0
  70. package/dist/fixtures/sample-data.d.ts.map +1 -0
  71. package/dist/fixtures/sample-data.js +148 -0
  72. package/dist/fixtures/sample-data.js.map +1 -0
  73. package/dist/index.d.ts +27 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +44 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/index.test.d.ts +2 -0
  78. package/dist/index.test.d.ts.map +1 -0
  79. package/dist/index.test.js +102 -0
  80. package/dist/index.test.js.map +1 -0
  81. package/dist/projections/comments-checkpoints.d.ts +11 -0
  82. package/dist/projections/comments-checkpoints.d.ts.map +1 -0
  83. package/dist/projections/comments-checkpoints.js +33 -0
  84. package/dist/projections/comments-checkpoints.js.map +1 -0
  85. package/dist/projections/comments-checkpoints.test.d.ts +2 -0
  86. package/dist/projections/comments-checkpoints.test.d.ts.map +1 -0
  87. package/dist/projections/comments-checkpoints.test.js +72 -0
  88. package/dist/projections/comments-checkpoints.test.js.map +1 -0
  89. package/dist/projections/dependencies.d.ts +12 -0
  90. package/dist/projections/dependencies.d.ts.map +1 -0
  91. package/dist/projections/dependencies.js +39 -0
  92. package/dist/projections/dependencies.js.map +1 -0
  93. package/dist/projections/dependencies.test.d.ts +2 -0
  94. package/dist/projections/dependencies.test.d.ts.map +1 -0
  95. package/dist/projections/dependencies.test.js +97 -0
  96. package/dist/projections/dependencies.test.js.map +1 -0
  97. package/dist/projections/engine.d.ts +18 -0
  98. package/dist/projections/engine.d.ts.map +1 -0
  99. package/dist/projections/engine.js +56 -0
  100. package/dist/projections/engine.js.map +1 -0
  101. package/dist/projections/engine.test.d.ts +2 -0
  102. package/dist/projections/engine.test.d.ts.map +1 -0
  103. package/dist/projections/engine.test.js +92 -0
  104. package/dist/projections/engine.test.js.map +1 -0
  105. package/dist/projections/rebuild.d.ts +4 -0
  106. package/dist/projections/rebuild.d.ts.map +1 -0
  107. package/dist/projections/rebuild.js +26 -0
  108. package/dist/projections/rebuild.js.map +1 -0
  109. package/dist/projections/rebuild.test.d.ts +2 -0
  110. package/dist/projections/rebuild.test.d.ts.map +1 -0
  111. package/dist/projections/rebuild.test.js +59 -0
  112. package/dist/projections/rebuild.test.js.map +1 -0
  113. package/dist/projections/search.d.ts +11 -0
  114. package/dist/projections/search.d.ts.map +1 -0
  115. package/dist/projections/search.js +39 -0
  116. package/dist/projections/search.js.map +1 -0
  117. package/dist/projections/search.test.d.ts +2 -0
  118. package/dist/projections/search.test.d.ts.map +1 -0
  119. package/dist/projections/search.test.js +78 -0
  120. package/dist/projections/search.test.js.map +1 -0
  121. package/dist/projections/tags.d.ts +12 -0
  122. package/dist/projections/tags.d.ts.map +1 -0
  123. package/dist/projections/tags.js +41 -0
  124. package/dist/projections/tags.js.map +1 -0
  125. package/dist/projections/tags.test.d.ts +2 -0
  126. package/dist/projections/tags.test.d.ts.map +1 -0
  127. package/dist/projections/tags.test.js +69 -0
  128. package/dist/projections/tags.test.js.map +1 -0
  129. package/dist/projections/tasks-current.d.ts +14 -0
  130. package/dist/projections/tasks-current.d.ts.map +1 -0
  131. package/dist/projections/tasks-current.js +110 -0
  132. package/dist/projections/tasks-current.js.map +1 -0
  133. package/dist/projections/tasks-current.test.d.ts +2 -0
  134. package/dist/projections/tasks-current.test.d.ts.map +1 -0
  135. package/dist/projections/tasks-current.test.js +215 -0
  136. package/dist/projections/tasks-current.test.js.map +1 -0
  137. package/dist/projections/types.d.ts +13 -0
  138. package/dist/projections/types.d.ts.map +1 -0
  139. package/dist/projections/types.js +2 -0
  140. package/dist/projections/types.js.map +1 -0
  141. package/dist/services/backup-service.d.ts +16 -0
  142. package/dist/services/backup-service.d.ts.map +1 -0
  143. package/dist/services/backup-service.js +114 -0
  144. package/dist/services/backup-service.js.map +1 -0
  145. package/dist/services/search-service.d.ts +27 -0
  146. package/dist/services/search-service.d.ts.map +1 -0
  147. package/dist/services/search-service.js +33 -0
  148. package/dist/services/search-service.js.map +1 -0
  149. package/dist/services/search-service.test.d.ts +2 -0
  150. package/dist/services/search-service.test.d.ts.map +1 -0
  151. package/dist/services/search-service.test.js +66 -0
  152. package/dist/services/search-service.test.js.map +1 -0
  153. package/dist/services/task-service.d.ts +147 -0
  154. package/dist/services/task-service.d.ts.map +1 -0
  155. package/dist/services/task-service.js +442 -0
  156. package/dist/services/task-service.js.map +1 -0
  157. package/dist/services/task-service.test.d.ts +2 -0
  158. package/dist/services/task-service.test.d.ts.map +1 -0
  159. package/dist/services/task-service.test.js +399 -0
  160. package/dist/services/task-service.test.js.map +1 -0
  161. package/dist/services/validation-service.d.ts +29 -0
  162. package/dist/services/validation-service.d.ts.map +1 -0
  163. package/dist/services/validation-service.js +74 -0
  164. package/dist/services/validation-service.js.map +1 -0
  165. package/dist/services/validation-service.test.d.ts +2 -0
  166. package/dist/services/validation-service.test.d.ts.map +1 -0
  167. package/dist/services/validation-service.test.js +67 -0
  168. package/dist/services/validation-service.test.js.map +1 -0
  169. package/dist/utils/id.d.ts +3 -0
  170. package/dist/utils/id.d.ts.map +1 -0
  171. package/dist/utils/id.js +12 -0
  172. package/dist/utils/id.js.map +1 -0
  173. package/dist/utils/id.test.d.ts +2 -0
  174. package/dist/utils/id.test.d.ts.map +1 -0
  175. package/dist/utils/id.test.js +24 -0
  176. package/dist/utils/id.test.js.map +1 -0
  177. package/package.json +83 -0
@@ -0,0 +1,66 @@
1
+ // packages/hzl-core/src/services/search-service.test.ts
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import Database from 'better-sqlite3';
4
+ import { SearchService } from './search-service.js';
5
+ import { runMigrations } from '../db/migrations.js';
6
+ import { EventStore } from '../events/store.js';
7
+ import { ProjectionEngine } from '../projections/engine.js';
8
+ import { TasksCurrentProjector } from '../projections/tasks-current.js';
9
+ import { SearchProjector } from '../projections/search.js';
10
+ import { EventType } from '../events/types.js';
11
+ describe('SearchService', () => {
12
+ let db;
13
+ let eventStore;
14
+ let engine;
15
+ let searchService;
16
+ beforeEach(() => {
17
+ db = new Database(':memory:');
18
+ runMigrations(db);
19
+ eventStore = new EventStore(db);
20
+ engine = new ProjectionEngine(db);
21
+ engine.register(new TasksCurrentProjector());
22
+ engine.register(new SearchProjector());
23
+ searchService = new SearchService(db);
24
+ });
25
+ afterEach(() => { db.close(); });
26
+ function createTask(taskId, title, project, description) {
27
+ const event = eventStore.append({ task_id: taskId, type: EventType.TaskCreated, data: { title, project, description } });
28
+ engine.applyEvent(event);
29
+ }
30
+ describe('search', () => {
31
+ it('finds tasks by title match', () => {
32
+ createTask('TASK1', 'Implement authentication', 'project-a');
33
+ createTask('TASK2', 'Write documentation', 'project-a');
34
+ const results = searchService.search('authentication');
35
+ expect(results.tasks).toHaveLength(1);
36
+ expect(results.tasks[0].task_id).toBe('TASK1');
37
+ });
38
+ it('finds tasks by description match', () => {
39
+ createTask('TASK1', 'Backend task', 'project-a', 'Implement OAuth2');
40
+ const results = searchService.search('OAuth2');
41
+ expect(results.tasks).toHaveLength(1);
42
+ });
43
+ it('supports project filter', () => {
44
+ createTask('TASK1', 'Auth for A', 'project-a');
45
+ createTask('TASK2', 'Auth for B', 'project-b');
46
+ const results = searchService.search('Auth', { project: 'project-a' });
47
+ expect(results.tasks).toHaveLength(1);
48
+ expect(results.tasks[0].task_id).toBe('TASK1');
49
+ });
50
+ it('supports limit and offset pagination', () => {
51
+ for (let i = 0; i < 5; i++)
52
+ createTask(`TASK${i}`, `Test task ${i}`, 'inbox');
53
+ const page1 = searchService.search('Test', { limit: 2, offset: 0 });
54
+ const page2 = searchService.search('Test', { limit: 2, offset: 2 });
55
+ expect(page1.tasks).toHaveLength(2);
56
+ expect(page2.tasks).toHaveLength(2);
57
+ expect(page1.total).toBe(5);
58
+ });
59
+ it('handles empty query', () => {
60
+ createTask('TASK1', 'Test', 'inbox');
61
+ const results = searchService.search('');
62
+ expect(results.tasks).toHaveLength(0);
63
+ });
64
+ });
65
+ });
66
+ //# sourceMappingURL=search-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-service.test.js","sourceRoot":"","sources":["../../src/services/search-service.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ import type Database from 'better-sqlite3';
2
+ import { EventStore } from '../events/store.js';
3
+ import { TaskStatus } from '../events/types.js';
4
+ import { ProjectionEngine } from '../projections/engine.js';
5
+ export interface CreateTaskInput {
6
+ title: string;
7
+ project: string;
8
+ parent_id?: string;
9
+ description?: string;
10
+ links?: string[];
11
+ depends_on?: string[];
12
+ tags?: string[];
13
+ priority?: number;
14
+ due_at?: string;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ export interface EventContext {
18
+ author?: string;
19
+ agent_id?: string;
20
+ session_id?: string;
21
+ correlation_id?: string;
22
+ causation_id?: string;
23
+ }
24
+ export interface ClaimTaskOptions extends EventContext {
25
+ lease_until?: string;
26
+ }
27
+ export interface ClaimNextOptions {
28
+ author?: string;
29
+ agent_id?: string;
30
+ project?: string;
31
+ tags?: string[];
32
+ lease_until?: string;
33
+ }
34
+ export interface StealOptions {
35
+ ifExpired?: boolean;
36
+ force?: boolean;
37
+ author?: string;
38
+ agent_id?: string;
39
+ lease_until?: string;
40
+ }
41
+ export interface StealResult {
42
+ success: boolean;
43
+ error?: string;
44
+ }
45
+ export interface StuckTask {
46
+ task_id: string;
47
+ title: string;
48
+ project: string;
49
+ claimed_at: string;
50
+ claimed_by_author: string | null;
51
+ claimed_by_agent_id: string | null;
52
+ lease_until: string | null;
53
+ }
54
+ export interface AvailableTask {
55
+ task_id: string;
56
+ title: string;
57
+ project: string;
58
+ status: TaskStatus;
59
+ priority: number;
60
+ created_at: string;
61
+ tags: string[];
62
+ }
63
+ export interface Comment {
64
+ event_rowid: number;
65
+ task_id: string;
66
+ author?: string;
67
+ agent_id?: string;
68
+ text: string;
69
+ timestamp: string;
70
+ }
71
+ export interface Checkpoint {
72
+ event_rowid: number;
73
+ task_id: string;
74
+ name: string;
75
+ data: Record<string, unknown>;
76
+ timestamp: string;
77
+ }
78
+ export interface Task {
79
+ task_id: string;
80
+ title: string;
81
+ project: string;
82
+ status: TaskStatus;
83
+ parent_id: string | null;
84
+ description: string | null;
85
+ links: string[];
86
+ tags: string[];
87
+ priority: number;
88
+ due_at: string | null;
89
+ metadata: Record<string, unknown>;
90
+ claimed_at: string | null;
91
+ claimed_by_author: string | null;
92
+ claimed_by_agent_id: string | null;
93
+ lease_until: string | null;
94
+ created_at: string;
95
+ updated_at: string;
96
+ }
97
+ export declare class TaskNotFoundError extends Error {
98
+ constructor(taskId: string);
99
+ }
100
+ export declare class TaskNotClaimableError extends Error {
101
+ constructor(taskId: string, reason: string);
102
+ }
103
+ export declare class DependenciesNotDoneError extends Error {
104
+ constructor(taskId: string, pendingDeps: string[]);
105
+ }
106
+ export declare class TaskService {
107
+ private db;
108
+ private eventStore;
109
+ private projectionEngine;
110
+ private getIncompleteDepsStmt;
111
+ constructor(db: Database.Database, eventStore: EventStore, projectionEngine: ProjectionEngine);
112
+ createTask(input: CreateTaskInput, ctx?: EventContext): Task;
113
+ claimTask(taskId: string, opts?: ClaimTaskOptions): Task;
114
+ setStatus(taskId: string, toStatus: TaskStatus, ctx?: EventContext): Task;
115
+ completeTask(taskId: string, ctx?: EventContext): Task;
116
+ claimNext(opts?: ClaimNextOptions): Task | null;
117
+ releaseTask(taskId: string, opts?: {
118
+ reason?: string;
119
+ } & EventContext): Task;
120
+ archiveTask(taskId: string, opts?: {
121
+ reason?: string;
122
+ } & EventContext): Task;
123
+ reopenTask(taskId: string, opts?: {
124
+ to_status?: TaskStatus.Ready | TaskStatus.Backlog;
125
+ reason?: string;
126
+ } & EventContext): Task;
127
+ stealTask(taskId: string, opts: StealOptions): StealResult;
128
+ getStuckTasks(opts: {
129
+ project?: string;
130
+ olderThan: number;
131
+ }): StuckTask[];
132
+ areAllDepsDone(taskId: string): boolean;
133
+ isTaskAvailable(taskId: string): boolean;
134
+ getAvailableTasks(opts: {
135
+ project?: string;
136
+ tagsAny?: string[];
137
+ tagsAll?: string[];
138
+ limit?: number;
139
+ }): AvailableTask[];
140
+ getTaskById(taskId: string): Task | null;
141
+ addComment(taskId: string, text: string, opts?: EventContext): Comment;
142
+ addCheckpoint(taskId: string, name: string, data?: Record<string, unknown>, opts?: EventContext): Checkpoint;
143
+ getComments(taskId: string): Comment[];
144
+ getCheckpoints(taskId: string): Checkpoint[];
145
+ private rowToTask;
146
+ }
147
+ //# sourceMappingURL=task-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-service.d.ts","sourceRoot":"","sources":["../../src/services/task-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,442 @@
1
+ import { EventType, TaskStatus } from '../events/types.js';
2
+ import { withWriteTransaction } from '../db/connection.js';
3
+ import { generateId } from '../utils/id.js';
4
+ export class TaskNotFoundError extends Error {
5
+ constructor(taskId) {
6
+ super(`Task not found: ${taskId}`);
7
+ }
8
+ }
9
+ export class TaskNotClaimableError extends Error {
10
+ constructor(taskId, reason) {
11
+ super(`Task ${taskId} is not claimable: ${reason}`);
12
+ }
13
+ }
14
+ export class DependenciesNotDoneError extends Error {
15
+ constructor(taskId, pendingDeps) {
16
+ super(`Task ${taskId} has dependencies not done: ${pendingDeps.join(', ')}`);
17
+ }
18
+ }
19
+ export class TaskService {
20
+ db;
21
+ eventStore;
22
+ projectionEngine;
23
+ getIncompleteDepsStmt;
24
+ constructor(db, eventStore, projectionEngine) {
25
+ this.db = db;
26
+ this.eventStore = eventStore;
27
+ this.projectionEngine = projectionEngine;
28
+ this.getIncompleteDepsStmt = db.prepare(`
29
+ SELECT td.depends_on_id
30
+ FROM task_dependencies td
31
+ LEFT JOIN tasks_current tc ON tc.task_id = td.depends_on_id
32
+ WHERE td.task_id = ?
33
+ AND (tc.status IS NULL OR tc.status != 'done')
34
+ `);
35
+ }
36
+ createTask(input, ctx) {
37
+ const taskId = generateId();
38
+ const eventData = {
39
+ title: input.title,
40
+ project: input.project,
41
+ parent_id: input.parent_id,
42
+ description: input.description,
43
+ links: input.links,
44
+ depends_on: input.depends_on,
45
+ tags: input.tags,
46
+ priority: input.priority,
47
+ due_at: input.due_at,
48
+ metadata: input.metadata,
49
+ };
50
+ Object.keys(eventData).forEach((key) => {
51
+ if (eventData[key] === undefined) {
52
+ delete eventData[key];
53
+ }
54
+ });
55
+ const task = withWriteTransaction(this.db, () => {
56
+ const event = this.eventStore.append({
57
+ task_id: taskId,
58
+ type: EventType.TaskCreated,
59
+ data: eventData,
60
+ author: ctx?.author,
61
+ agent_id: ctx?.agent_id,
62
+ session_id: ctx?.session_id,
63
+ correlation_id: ctx?.correlation_id,
64
+ causation_id: ctx?.causation_id,
65
+ });
66
+ this.projectionEngine.applyEvent(event);
67
+ return this.getTaskById(taskId);
68
+ });
69
+ if (!task) {
70
+ throw new Error(`Failed to create task: task not found after creation`);
71
+ }
72
+ return task;
73
+ }
74
+ claimTask(taskId, opts) {
75
+ return withWriteTransaction(this.db, () => {
76
+ const task = this.getTaskById(taskId);
77
+ if (!task)
78
+ throw new TaskNotFoundError(taskId);
79
+ if (task.status !== TaskStatus.Ready) {
80
+ throw new TaskNotClaimableError(taskId, `status is ${task.status}, must be ready`);
81
+ }
82
+ const incompleteDeps = this.getIncompleteDepsStmt.all(taskId);
83
+ if (incompleteDeps.length > 0) {
84
+ throw new DependenciesNotDoneError(taskId, incompleteDeps.map(d => d.depends_on_id));
85
+ }
86
+ const eventData = {
87
+ from: TaskStatus.Ready,
88
+ to: TaskStatus.InProgress,
89
+ };
90
+ if (opts?.lease_until)
91
+ eventData.lease_until = opts.lease_until;
92
+ const event = this.eventStore.append({
93
+ task_id: taskId,
94
+ type: EventType.StatusChanged,
95
+ data: eventData,
96
+ author: opts?.author,
97
+ agent_id: opts?.agent_id,
98
+ });
99
+ this.projectionEngine.applyEvent(event);
100
+ return this.getTaskById(taskId);
101
+ });
102
+ }
103
+ setStatus(taskId, toStatus, ctx) {
104
+ return withWriteTransaction(this.db, () => {
105
+ const task = this.getTaskById(taskId);
106
+ if (!task)
107
+ throw new TaskNotFoundError(taskId);
108
+ const event = this.eventStore.append({
109
+ task_id: taskId,
110
+ type: EventType.StatusChanged,
111
+ data: { from: task.status, to: toStatus },
112
+ author: ctx?.author,
113
+ agent_id: ctx?.agent_id,
114
+ });
115
+ this.projectionEngine.applyEvent(event);
116
+ return this.getTaskById(taskId);
117
+ });
118
+ }
119
+ completeTask(taskId, ctx) {
120
+ return withWriteTransaction(this.db, () => {
121
+ const task = this.getTaskById(taskId);
122
+ if (!task)
123
+ throw new TaskNotFoundError(taskId);
124
+ if (task.status !== TaskStatus.InProgress) {
125
+ throw new Error(`Cannot complete: status is ${task.status}, must be in_progress`);
126
+ }
127
+ const event = this.eventStore.append({
128
+ task_id: taskId,
129
+ type: EventType.StatusChanged,
130
+ data: { from: TaskStatus.InProgress, to: TaskStatus.Done },
131
+ author: ctx?.author,
132
+ agent_id: ctx?.agent_id,
133
+ });
134
+ this.projectionEngine.applyEvent(event);
135
+ return this.getTaskById(taskId);
136
+ });
137
+ }
138
+ claimNext(opts = {}) {
139
+ return withWriteTransaction(this.db, () => {
140
+ let candidate;
141
+ if (opts.tags && opts.tags.length > 0) {
142
+ const tagPlaceholders = opts.tags.map(() => '?').join(', ');
143
+ const tagCount = opts.tags.length;
144
+ let query = `
145
+ SELECT tc.task_id FROM tasks_current tc
146
+ WHERE tc.status = 'ready'
147
+ AND NOT EXISTS (
148
+ SELECT 1 FROM task_dependencies td
149
+ JOIN tasks_current dep ON td.depends_on_id = dep.task_id
150
+ WHERE td.task_id = tc.task_id AND dep.status != 'done'
151
+ )
152
+ AND (SELECT COUNT(DISTINCT tag) FROM task_tags WHERE task_id = tc.task_id AND tag IN (${tagPlaceholders})) = ?
153
+ `;
154
+ const params = [...opts.tags, tagCount];
155
+ if (opts.project) {
156
+ query += ' AND tc.project = ?';
157
+ params.push(opts.project);
158
+ }
159
+ query += ' ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC LIMIT 1';
160
+ candidate = this.db.prepare(query).get(...params);
161
+ }
162
+ else if (opts.project) {
163
+ candidate = this.db.prepare(`
164
+ SELECT tc.task_id FROM tasks_current tc
165
+ WHERE tc.status = 'ready' AND tc.project = ?
166
+ AND NOT EXISTS (
167
+ SELECT 1 FROM task_dependencies td
168
+ JOIN tasks_current dep ON td.depends_on_id = dep.task_id
169
+ WHERE td.task_id = tc.task_id AND dep.status != 'done'
170
+ )
171
+ ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC LIMIT 1
172
+ `).get(opts.project);
173
+ }
174
+ else {
175
+ candidate = this.db.prepare(`
176
+ SELECT tc.task_id FROM tasks_current tc
177
+ WHERE tc.status = 'ready'
178
+ AND NOT EXISTS (
179
+ SELECT 1 FROM task_dependencies td
180
+ JOIN tasks_current dep ON td.depends_on_id = dep.task_id
181
+ WHERE td.task_id = tc.task_id AND dep.status != 'done'
182
+ )
183
+ ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC LIMIT 1
184
+ `).get();
185
+ }
186
+ if (!candidate)
187
+ return null;
188
+ const event = this.eventStore.append({
189
+ task_id: candidate.task_id,
190
+ type: EventType.StatusChanged,
191
+ data: { from: TaskStatus.Ready, to: TaskStatus.InProgress, lease_until: opts.lease_until },
192
+ author: opts.author,
193
+ agent_id: opts.agent_id,
194
+ });
195
+ this.projectionEngine.applyEvent(event);
196
+ return this.getTaskById(candidate.task_id);
197
+ });
198
+ }
199
+ releaseTask(taskId, opts) {
200
+ return withWriteTransaction(this.db, () => {
201
+ const task = this.getTaskById(taskId);
202
+ if (!task)
203
+ throw new TaskNotFoundError(taskId);
204
+ if (task.status !== TaskStatus.InProgress) {
205
+ throw new Error(`Cannot release: status is ${task.status}, expected in_progress`);
206
+ }
207
+ const event = this.eventStore.append({
208
+ task_id: taskId,
209
+ type: EventType.StatusChanged,
210
+ data: { from: TaskStatus.InProgress, to: TaskStatus.Ready, reason: opts?.reason },
211
+ author: opts?.author,
212
+ agent_id: opts?.agent_id,
213
+ });
214
+ this.projectionEngine.applyEvent(event);
215
+ return this.getTaskById(taskId);
216
+ });
217
+ }
218
+ archiveTask(taskId, opts) {
219
+ return withWriteTransaction(this.db, () => {
220
+ const task = this.getTaskById(taskId);
221
+ if (!task)
222
+ throw new TaskNotFoundError(taskId);
223
+ if (task.status === TaskStatus.Archived) {
224
+ throw new Error('Task is already archived');
225
+ }
226
+ const event = this.eventStore.append({
227
+ task_id: taskId,
228
+ type: EventType.TaskArchived,
229
+ data: { reason: opts?.reason },
230
+ author: opts?.author,
231
+ agent_id: opts?.agent_id,
232
+ });
233
+ this.projectionEngine.applyEvent(event);
234
+ return this.getTaskById(taskId);
235
+ });
236
+ }
237
+ reopenTask(taskId, opts) {
238
+ return withWriteTransaction(this.db, () => {
239
+ const task = this.getTaskById(taskId);
240
+ if (!task)
241
+ throw new TaskNotFoundError(taskId);
242
+ if (task.status !== TaskStatus.Done) {
243
+ throw new Error(`Cannot reopen: status is ${task.status}, expected done`);
244
+ }
245
+ const toStatus = opts?.to_status ?? TaskStatus.Ready;
246
+ const event = this.eventStore.append({
247
+ task_id: taskId,
248
+ type: EventType.StatusChanged,
249
+ data: { from: TaskStatus.Done, to: toStatus, reason: opts?.reason },
250
+ author: opts?.author,
251
+ agent_id: opts?.agent_id,
252
+ });
253
+ this.projectionEngine.applyEvent(event);
254
+ return this.getTaskById(taskId);
255
+ });
256
+ }
257
+ stealTask(taskId, opts) {
258
+ return withWriteTransaction(this.db, () => {
259
+ const task = this.getTaskById(taskId);
260
+ if (!task)
261
+ return { success: false, error: `Task ${taskId} not found` };
262
+ if (task.status !== TaskStatus.InProgress) {
263
+ return { success: false, error: `Task ${taskId} is not in_progress` };
264
+ }
265
+ if (!opts.force) {
266
+ if (opts.ifExpired) {
267
+ const now = new Date().toISOString();
268
+ if (task.lease_until && task.lease_until >= now) {
269
+ return { success: false, error: `Task ${taskId} lease has not expired` };
270
+ }
271
+ }
272
+ else {
273
+ return { success: false, error: 'Must specify either force=true or ifExpired=true' };
274
+ }
275
+ }
276
+ const event = this.eventStore.append({
277
+ task_id: taskId,
278
+ type: EventType.StatusChanged,
279
+ data: { from: TaskStatus.InProgress, to: TaskStatus.InProgress, reason: 'stolen', lease_until: opts.lease_until },
280
+ author: opts.author,
281
+ agent_id: opts.agent_id,
282
+ });
283
+ this.projectionEngine.applyEvent(event);
284
+ // Update claim info
285
+ this.db.prepare(`
286
+ UPDATE tasks_current SET
287
+ claimed_at = ?, claimed_by_author = ?, claimed_by_agent_id = ?, lease_until = ?, updated_at = ?, last_event_id = ?
288
+ WHERE task_id = ?
289
+ `).run(new Date().toISOString(), opts.author ?? null, opts.agent_id ?? null, opts.lease_until ?? null, new Date().toISOString(), event.rowid, taskId);
290
+ return { success: true };
291
+ });
292
+ }
293
+ getStuckTasks(opts) {
294
+ const cutoffTime = new Date(Date.now() - opts.olderThan).toISOString();
295
+ let query = `
296
+ SELECT task_id, title, project, claimed_at, claimed_by_author, claimed_by_agent_id, lease_until
297
+ FROM tasks_current WHERE status = 'in_progress' AND claimed_at < ?
298
+ `;
299
+ const params = [cutoffTime];
300
+ if (opts.project) {
301
+ query += ' AND project = ?';
302
+ params.push(opts.project);
303
+ }
304
+ query += ' ORDER BY claimed_at ASC';
305
+ return this.db.prepare(query).all(...params);
306
+ }
307
+ areAllDepsDone(taskId) {
308
+ const result = this.db.prepare(`
309
+ SELECT COUNT(*) as count FROM task_dependencies td
310
+ JOIN tasks_current tc ON td.depends_on_id = tc.task_id
311
+ WHERE td.task_id = ? AND tc.status != 'done'
312
+ `).get(taskId);
313
+ return result.count === 0;
314
+ }
315
+ isTaskAvailable(taskId) {
316
+ const task = this.getTaskById(taskId);
317
+ if (!task)
318
+ return false;
319
+ if (task.status !== TaskStatus.Ready)
320
+ return false;
321
+ return this.areAllDepsDone(taskId);
322
+ }
323
+ getAvailableTasks(opts) {
324
+ let query = `
325
+ SELECT tc.task_id, tc.title, tc.project, tc.status, tc.priority, tc.created_at, tc.tags
326
+ FROM tasks_current tc
327
+ WHERE tc.status = 'ready'
328
+ AND NOT EXISTS (
329
+ SELECT 1 FROM task_dependencies td
330
+ JOIN tasks_current dep ON td.depends_on_id = dep.task_id
331
+ WHERE td.task_id = tc.task_id AND dep.status != 'done'
332
+ )
333
+ `;
334
+ const params = [];
335
+ if (opts.project) {
336
+ query += ' AND tc.project = ?';
337
+ params.push(opts.project);
338
+ }
339
+ if (opts.tagsAny?.length) {
340
+ query += ` AND EXISTS (SELECT 1 FROM task_tags tt WHERE tt.task_id = tc.task_id AND tt.tag IN (${opts.tagsAny.map(() => '?').join(',')}))`;
341
+ params.push(...opts.tagsAny);
342
+ }
343
+ if (opts.tagsAll?.length) {
344
+ query += ` AND (SELECT COUNT(DISTINCT tt.tag) FROM task_tags tt WHERE tt.task_id = tc.task_id AND tt.tag IN (${opts.tagsAll.map(() => '?').join(',')})) = ?`;
345
+ params.push(...opts.tagsAll, opts.tagsAll.length);
346
+ }
347
+ query += ' ORDER BY tc.priority DESC, tc.created_at ASC, tc.task_id ASC';
348
+ if (opts.limit) {
349
+ query += ' LIMIT ?';
350
+ params.push(opts.limit);
351
+ }
352
+ const rows = this.db.prepare(query).all(...params);
353
+ return rows.map(row => ({
354
+ task_id: row.task_id,
355
+ title: row.title,
356
+ project: row.project,
357
+ status: row.status,
358
+ priority: row.priority,
359
+ created_at: row.created_at,
360
+ tags: JSON.parse(row.tags || '[]'),
361
+ }));
362
+ }
363
+ getTaskById(taskId) {
364
+ const row = this.db.prepare('SELECT * FROM tasks_current WHERE task_id = ?').get(taskId);
365
+ if (!row)
366
+ return null;
367
+ return this.rowToTask(row);
368
+ }
369
+ addComment(taskId, text, opts) {
370
+ if (!text?.trim())
371
+ throw new Error('Comment text cannot be empty');
372
+ const task = this.getTaskById(taskId);
373
+ if (!task)
374
+ throw new TaskNotFoundError(taskId);
375
+ return withWriteTransaction(this.db, () => {
376
+ const event = this.eventStore.append({
377
+ task_id: taskId,
378
+ type: EventType.CommentAdded,
379
+ data: { text },
380
+ author: opts?.author,
381
+ agent_id: opts?.agent_id,
382
+ });
383
+ this.projectionEngine.applyEvent(event);
384
+ return { event_rowid: event.rowid, task_id: taskId, author: opts?.author, agent_id: opts?.agent_id, text, timestamp: event.timestamp };
385
+ });
386
+ }
387
+ addCheckpoint(taskId, name, data, opts) {
388
+ if (!name?.trim())
389
+ throw new Error('Checkpoint name cannot be empty');
390
+ const task = this.getTaskById(taskId);
391
+ if (!task)
392
+ throw new TaskNotFoundError(taskId);
393
+ const checkpointData = data ?? {};
394
+ return withWriteTransaction(this.db, () => {
395
+ const event = this.eventStore.append({
396
+ task_id: taskId,
397
+ type: EventType.CheckpointRecorded,
398
+ data: { name, data: checkpointData },
399
+ author: opts?.author,
400
+ agent_id: opts?.agent_id,
401
+ });
402
+ this.projectionEngine.applyEvent(event);
403
+ return { event_rowid: event.rowid, task_id: taskId, name, data: checkpointData, timestamp: event.timestamp };
404
+ });
405
+ }
406
+ getComments(taskId) {
407
+ const rows = this.db.prepare(`
408
+ SELECT event_rowid, task_id, author, agent_id, text, timestamp
409
+ FROM task_comments WHERE task_id = ? ORDER BY event_rowid ASC
410
+ `).all(taskId);
411
+ return rows.map(r => ({ event_rowid: r.event_rowid, task_id: r.task_id, author: r.author ?? undefined, agent_id: r.agent_id ?? undefined, text: r.text, timestamp: r.timestamp }));
412
+ }
413
+ getCheckpoints(taskId) {
414
+ const rows = this.db.prepare(`
415
+ SELECT event_rowid, task_id, name, data, timestamp
416
+ FROM task_checkpoints WHERE task_id = ? ORDER BY event_rowid ASC
417
+ `).all(taskId);
418
+ return rows.map(r => ({ event_rowid: r.event_rowid, task_id: r.task_id, name: r.name, data: JSON.parse(r.data), timestamp: r.timestamp }));
419
+ }
420
+ rowToTask(row) {
421
+ return {
422
+ task_id: row.task_id,
423
+ title: row.title,
424
+ project: row.project,
425
+ status: row.status,
426
+ parent_id: row.parent_id,
427
+ description: row.description,
428
+ links: JSON.parse(row.links),
429
+ tags: JSON.parse(row.tags),
430
+ priority: row.priority,
431
+ due_at: row.due_at,
432
+ metadata: JSON.parse(row.metadata),
433
+ claimed_at: row.claimed_at,
434
+ claimed_by_author: row.claimed_by_author,
435
+ claimed_by_agent_id: row.claimed_by_agent_id,
436
+ lease_until: row.lease_until,
437
+ created_at: row.created_at,
438
+ updated_at: row.updated_at,
439
+ };
440
+ }
441
+ }
442
+ //# sourceMappingURL=task-service.js.map