@opentask/taskin-task-manager 2.0.1 → 3.0.1

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 (41) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.js +4 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/metrics.types.d.ts +64 -73
  6. package/dist/provider-stats.test.js.map +1 -1
  7. package/dist/task-manager.d.ts +23 -7
  8. package/dist/task-manager.js +31 -3
  9. package/dist/task-manager.js.map +1 -1
  10. package/dist/task-manager.mock.d.ts +3 -4
  11. package/dist/task-manager.mock.js +2 -3
  12. package/dist/task-manager.mock.js.map +1 -1
  13. package/dist/task-manager.test.js +58 -17
  14. package/dist/task-manager.test.js.map +1 -1
  15. package/dist/task-manager.types.d.ts +49 -37
  16. package/dist/user-registry.contract.d.ts +13 -0
  17. package/dist/user-registry.contract.js +130 -0
  18. package/dist/user-registry.contract.js.map +1 -0
  19. package/dist/user-registry.types.d.ts +51 -0
  20. package/dist/user-registry.types.js +2 -0
  21. package/dist/user-registry.types.js.map +1 -0
  22. package/package.json +17 -9
  23. package/src/index.ts +3 -3
  24. package/src/metrics.types.ts +2 -11
  25. package/src/provider-stats.test.ts +6 -3
  26. package/src/task-manager.mock.ts +4 -6
  27. package/src/task-manager.test.ts +83 -34
  28. package/src/task-manager.ts +44 -10
  29. package/src/task-manager.types.ts +56 -38
  30. package/src/user-registry.contract.ts +151 -0
  31. package/src/user-registry.types.ts +55 -0
  32. package/tsconfig.json +1 -0
  33. package/.turbo/turbo-build.log +0 -5
  34. package/.turbo/turbo-format.log +0 -18
  35. package/.turbo/turbo-install.log +0 -6
  36. package/.turbo/turbo-lint$colon$fix.log +0 -12
  37. package/.turbo/turbo-lint.log +0 -13
  38. package/.turbo/turbo-test.log +0 -15
  39. package/.turbo/turbo-typecheck.log +0 -5
  40. package/eslint.config.js +0 -8
  41. package/tsconfig.tsbuildinfo +0 -1
@@ -1,9 +1,4 @@
1
- import type {
2
- StatsQuery,
3
- TaskStats,
4
- TeamStats,
5
- UserStats,
6
- } from '@opentask/taskin-types';
1
+ import type { StatsQuery, TaskStats, TeamStats, UserStats } from '@opentask/taskin-types';
7
2
 
8
3
  /**
9
4
  * Manages metrics and statistics for users, teams, and tasks.
@@ -89,8 +84,4 @@ export interface IMetricsManager {
89
84
  getTaskMetrics(taskId: string, query?: StatsQuery): Promise<TaskStats>;
90
85
  }
91
86
 
92
- export type {
93
- TaskStats as TaskMetrics,
94
- TeamStats as TeamMetrics,
95
- UserStats as UserMetrics,
96
- };
87
+ export type { TaskStats as TaskMetrics, TeamStats as TeamMetrics, UserStats as UserMetrics };
@@ -4,9 +4,12 @@ import type { IMetricsManager } from './metrics.types';
4
4
  describe('IMetricsManager contract', () => {
5
5
  it('accepts an object implementing IMetricsManager', async () => {
6
6
  const metrics: IMetricsManager = {
7
- getUserMetrics: async (userId: string) => ({ userId }) as any,
8
- getTeamMetrics: async (teamId: string) => ({ teamId }) as any,
9
- getTaskMetrics: async (taskId: string) => ({ taskId }) as any,
7
+ getUserMetrics: async (userId: string) =>
8
+ ({ userId }) as unknown as Awaited<ReturnType<IMetricsManager['getUserMetrics']>>,
9
+ getTeamMetrics: async (teamId: string) =>
10
+ ({ teamId }) as unknown as Awaited<ReturnType<IMetricsManager['getTeamMetrics']>>,
11
+ getTaskMetrics: async (taskId: string) =>
12
+ ({ taskId }) as unknown as Awaited<ReturnType<IMetricsManager['getTaskMetrics']>>,
10
13
  };
11
14
 
12
15
  expect(typeof metrics.getUserMetrics).toBe('function');
@@ -1,13 +1,11 @@
1
- import type { TaskId } from '@opentask/taskin-types';
1
+ import { parseTaskId, type Task } from '@opentask/taskin-types';
2
2
  import { vi } from 'vitest';
3
- import type { ITaskProvider, TaskFile } from './task-manager.types';
3
+ import type { ITaskProvider } from './task-manager.types';
4
4
 
5
- export const createMockTask = (overrides?: Partial<TaskFile>): TaskFile => ({
6
- content: '# Task 001 - Implement feature',
5
+ export const createMockTask = (overrides?: Partial<Task>): Task => ({
7
6
  createdAt: new Date().toISOString(),
8
7
  description: 'A test feature',
9
- filePath: '/tasks/task-001.md',
10
- id: 'task-001' satisfies string as TaskId,
8
+ id: parseTaskId('001'),
11
9
  status: 'pending',
12
10
  title: 'Implement feature',
13
11
  type: 'feat',
@@ -1,13 +1,14 @@
1
+ import { parseTaskId, type Task } from '@opentask/taskin-types';
1
2
  import type { Mock } from 'vitest';
2
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
4
  import { TaskManager } from './task-manager';
4
5
  import { createMockTask, createMockTaskProvider } from './task-manager.mock';
5
- import type { ITaskProvider, TaskFile } from './task-manager.types';
6
+ import type { ITaskProvider } from './task-manager.types';
6
7
 
7
8
  describe('TaskManager', () => {
8
9
  let taskManager: TaskManager;
9
10
  let mockTaskProvider: ITaskProvider;
10
- let mockTask: TaskFile;
11
+ let mockTask: Task;
11
12
 
12
13
  beforeEach(() => {
13
14
  vi.clearAllMocks();
@@ -20,30 +21,58 @@ describe('TaskManager', () => {
20
21
  it('should start a pending task', async () => {
21
22
  (mockTaskProvider.findTask as Mock).mockResolvedValue(mockTask);
22
23
 
23
- const updatedTask = await taskManager.startTask('task-001');
24
+ const updatedTask = await taskManager.startTask(parseTaskId('001'));
24
25
 
25
- expect(mockTaskProvider.findTask).toHaveBeenCalledWith('task-001');
26
- expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
27
- expect.objectContaining({ status: 'in-progress' }),
28
- );
26
+ expect(mockTaskProvider.findTask).toHaveBeenCalledWith('001');
27
+ expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'in-progress' }));
29
28
  expect(updatedTask.status).toBe('in-progress');
30
29
  });
31
30
 
32
31
  it('should throw an error if task is not found', async () => {
33
32
  (mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
34
- await expect(taskManager.startTask('not-found')).rejects.toThrow(
35
- "Task with ID 'not-found' not found.",
36
- );
33
+ await expect(taskManager.startTask(parseTaskId('999'))).rejects.toThrow("Task with ID '999' not found.");
37
34
  });
38
35
 
39
36
  it('should throw an error if task is already in progress', async () => {
40
37
  const inProgressTask = { ...mockTask, status: 'in-progress' as const };
41
38
  (mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
42
39
 
43
- await expect(taskManager.startTask('task-001')).rejects.toThrow(
44
- "Task 'task-001' is already in progress.",
40
+ await expect(taskManager.startTask(parseTaskId('001'))).rejects.toThrow("Task '001' is already in progress.");
41
+ });
42
+ });
43
+
44
+ describe('pauseTask', () => {
45
+ it('should pause an in-progress task', async () => {
46
+ const inProgressTask = { ...mockTask, status: 'in-progress' as const };
47
+ (mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
48
+
49
+ const updatedTask = await taskManager.pauseTask(parseTaskId('001'));
50
+
51
+ expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'paused' }));
52
+ expect(updatedTask.status).toBe('paused');
53
+ });
54
+
55
+ it('should throw an error if task is not found', async () => {
56
+ (mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
57
+
58
+ await expect(taskManager.pauseTask(parseTaskId('999'))).rejects.toThrow("Task with ID '999' not found.");
59
+ });
60
+
61
+ it('should throw an error if task is not in-progress', async () => {
62
+ (mockTaskProvider.findTask as Mock).mockResolvedValue({ ...mockTask, status: 'pending' as const });
63
+
64
+ await expect(taskManager.pauseTask(parseTaskId('001'))).rejects.toThrow(
65
+ "Task '001' must be in 'in-progress' status to be paused",
45
66
  );
46
67
  });
68
+
69
+ it('should let startTask resume a paused task', async () => {
70
+ (mockTaskProvider.findTask as Mock).mockResolvedValue({ ...mockTask, status: 'paused' as const });
71
+
72
+ const updatedTask = await taskManager.startTask(parseTaskId('001'));
73
+
74
+ expect(updatedTask.status).toBe('in-progress');
75
+ });
47
76
  });
48
77
 
49
78
  describe('finishTask', () => {
@@ -51,11 +80,9 @@ describe('TaskManager', () => {
51
80
  const inProgressTask = { ...mockTask, status: 'in-progress' as const };
52
81
  (mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
53
82
 
54
- const updatedTask = await taskManager.finishTask('task-001');
83
+ const updatedTask = await taskManager.finishTask(parseTaskId('001'));
55
84
 
56
- expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
57
- expect.objectContaining({ status: 'done' }),
58
- );
85
+ expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'done' }));
59
86
  expect(updatedTask.status).toBe('done');
60
87
  });
61
88
  });
@@ -65,29 +92,25 @@ describe('TaskManager', () => {
65
92
  const inProgressTask = { ...mockTask, status: 'in-progress' as const };
66
93
  (mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
67
94
 
68
- const updatedTask = await taskManager.reviewTask('task-001');
95
+ const updatedTask = await taskManager.reviewTask(parseTaskId('001'));
69
96
 
70
- expect(mockTaskProvider.findTask).toHaveBeenCalledWith('task-001');
71
- expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
72
- expect.objectContaining({ status: 'in-review' }),
73
- );
97
+ expect(mockTaskProvider.findTask).toHaveBeenCalledWith('001');
98
+ expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'in-review' }));
74
99
  expect(updatedTask.status).toBe('in-review');
75
100
  });
76
101
 
77
102
  it('should throw an error if task is not found', async () => {
78
103
  (mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
79
104
 
80
- await expect(taskManager.reviewTask('not-found')).rejects.toThrow(
81
- "Task with ID 'not-found' not found.",
82
- );
105
+ await expect(taskManager.reviewTask(parseTaskId('999'))).rejects.toThrow("Task with ID '999' not found.");
83
106
  });
84
107
 
85
108
  it('should throw an error if task is not in-progress', async () => {
86
109
  const pendingTask = { ...mockTask, status: 'pending' as const };
87
110
  (mockTaskProvider.findTask as Mock).mockResolvedValue(pendingTask);
88
111
 
89
- await expect(taskManager.reviewTask('task-001')).rejects.toThrow(
90
- "Task 'task-001' must be in 'in-progress' status to be reviewed",
112
+ await expect(taskManager.reviewTask(parseTaskId('001'))).rejects.toThrow(
113
+ "Task '001' must be in 'in-progress' status to be reviewed",
91
114
  );
92
115
  });
93
116
 
@@ -95,8 +118,8 @@ describe('TaskManager', () => {
95
118
  const doneTask = { ...mockTask, status: 'done' as const };
96
119
  (mockTaskProvider.findTask as Mock).mockResolvedValue(doneTask);
97
120
 
98
- await expect(taskManager.reviewTask('task-001')).rejects.toThrow(
99
- "Task 'task-001' must be in 'in-progress' status to be reviewed",
121
+ await expect(taskManager.reviewTask(parseTaskId('001'))).rejects.toThrow(
122
+ "Task '001' must be in 'in-progress' status to be reviewed",
100
123
  );
101
124
  });
102
125
 
@@ -104,18 +127,42 @@ describe('TaskManager', () => {
104
127
  const reviewTask = { ...mockTask, status: 'in-review' as const };
105
128
  (mockTaskProvider.findTask as Mock).mockResolvedValue(reviewTask);
106
129
 
107
- await expect(taskManager.reviewTask('task-001')).rejects.toThrow(
108
- "Task 'task-001' must be in 'in-progress' status to be reviewed",
130
+ await expect(taskManager.reviewTask(parseTaskId('001'))).rejects.toThrow(
131
+ "Task '001' must be in 'in-progress' status to be reviewed",
109
132
  );
110
133
  });
111
134
  });
112
135
 
136
+ describe('provider-specific fields', () => {
137
+ // The manager is generic over the provider's task shape, so extra fields a
138
+ // provider carries (filePath/content for the file system, an issue number
139
+ // for a tracker) must survive a status transition untouched.
140
+ type ProviderTask = Task & { filePath: string; content: string };
141
+
142
+ it('should preserve provider fields through a transition', async () => {
143
+ const providerTask: ProviderTask = {
144
+ ...createMockTask(),
145
+ filePath: '/tasks/task-001.md',
146
+ content: '# Task 001',
147
+ };
148
+ const provider = createMockTaskProvider() as unknown as ITaskProvider<ProviderTask>;
149
+ (provider.findTask as Mock).mockResolvedValue(providerTask);
150
+
151
+ const manager = new TaskManager(provider);
152
+ const updated = await manager.startTask(parseTaskId('001'));
153
+
154
+ expect(updated.filePath).toBe('/tasks/task-001.md');
155
+ expect(updated.content).toBe('# Task 001');
156
+ expect(updated.status).toBe('in-progress');
157
+ expect(provider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ filePath: '/tasks/task-001.md' }));
158
+ });
159
+ });
160
+
113
161
  describe('createTask', () => {
114
162
  it('should delegate task creation to provider', async () => {
115
163
  const createOptions = { title: 'New Task', type: 'feat' as const };
116
164
  const createResult = {
117
165
  task: mockTask,
118
- taskId: '001',
119
166
  filePath: '/tasks/task-001.md',
120
167
  };
121
168
 
@@ -134,9 +181,11 @@ describe('TaskManager', () => {
134
181
  description: 'Fix something',
135
182
  assignee: 'john-doe',
136
183
  };
184
+ // Antes o assert batia no campo `taskId` do result, que dizia '002'
185
+ // enquanto o `task` dizia outro id. Com um campo so, o teste tem que
186
+ // devolver a task que ele afirma ter criado.
137
187
  const createResult = {
138
- task: mockTask,
139
- taskId: '002',
188
+ task: createMockTask({ id: parseTaskId('002') }),
140
189
  filePath: '/tasks/task-002.md',
141
190
  };
142
191
 
@@ -145,7 +194,7 @@ describe('TaskManager', () => {
145
194
  const result = await taskManager.createTask(createOptions);
146
195
 
147
196
  expect(mockTaskProvider.createTask).toHaveBeenCalledWith(createOptions);
148
- expect(result.taskId).toBe('002');
197
+ expect(result.task.id).toBe('002');
149
198
  });
150
199
  });
151
200
 
@@ -1,16 +1,33 @@
1
+ import type { Task, TaskId, TaskStatus } from '@opentask/taskin-types';
1
2
  import type {
2
3
  CreateTaskOptions,
3
4
  CreateTaskResult,
4
5
  ITaskManager,
5
6
  ITaskProvider,
6
7
  LintResult,
7
- TaskFile,
8
8
  } from './task-manager.types';
9
9
 
10
- export class TaskManager implements ITaskManager {
11
- constructor(private taskProvider: ITaskProvider) {}
10
+ /**
11
+ * Orchestrates task state transitions on top of any {@link ITaskProvider}.
12
+ *
13
+ * `TTask` is inferred from the provider passed to the constructor, so callers
14
+ * get their provider's task shape back without this class ever naming it.
15
+ */
16
+ export class TaskManager<TTask extends Task = Task> implements ITaskManager<TTask> {
17
+ constructor(private taskProvider: ITaskProvider<TTask>) {}
18
+
19
+ /**
20
+ * Returns a copy of `task` with a new status.
21
+ *
22
+ * TypeScript cannot prove that spreading a generic yields that same generic,
23
+ * so the assertion is required. It is sound here: `status` is a known key of
24
+ * `Task`, and every other field is carried over untouched.
25
+ */
26
+ private withStatus(task: TTask, status: TaskStatus): TTask {
27
+ return { ...task, status } as TTask;
28
+ }
12
29
 
13
- async startTask(taskId: string): Promise<TaskFile> {
30
+ async startTask(taskId: TaskId): Promise<TTask> {
14
31
  const task = await this.taskProvider.findTask(taskId);
15
32
 
16
33
  if (!task) {
@@ -25,26 +42,43 @@ export class TaskManager implements ITaskManager {
25
42
  throw new Error(`Task '${taskId}' is already done.`);
26
43
  }
27
44
 
28
- const updatedTask: TaskFile = { ...task, status: 'in-progress' };
45
+ const updatedTask = this.withStatus(task, 'in-progress');
46
+ await this.taskProvider.updateTask(updatedTask);
47
+
48
+ return updatedTask;
49
+ }
50
+
51
+ async pauseTask(taskId: TaskId): Promise<TTask> {
52
+ const task = await this.taskProvider.findTask(taskId);
53
+
54
+ if (!task) {
55
+ throw new Error(`Task with ID '${taskId}' not found.`);
56
+ }
57
+
58
+ if (task.status !== 'in-progress') {
59
+ throw new Error(`Task '${taskId}' must be in 'in-progress' status to be paused. Current status: ${task.status}`);
60
+ }
61
+
62
+ const updatedTask = this.withStatus(task, 'paused');
29
63
  await this.taskProvider.updateTask(updatedTask);
30
64
 
31
65
  return updatedTask;
32
66
  }
33
67
 
34
- async finishTask(taskId: string): Promise<TaskFile> {
68
+ async finishTask(taskId: TaskId): Promise<TTask> {
35
69
  const task = await this.taskProvider.findTask(taskId);
36
70
 
37
71
  if (!task) {
38
72
  throw new Error(`Task with ID '${taskId}' not found.`);
39
73
  }
40
74
 
41
- const updatedTask: TaskFile = { ...task, status: 'done' };
75
+ const updatedTask = this.withStatus(task, 'done');
42
76
  await this.taskProvider.updateTask(updatedTask);
43
77
 
44
78
  return updatedTask;
45
79
  }
46
80
 
47
- async reviewTask(taskId: string): Promise<TaskFile> {
81
+ async reviewTask(taskId: TaskId): Promise<TTask> {
48
82
  const task = await this.taskProvider.findTask(taskId);
49
83
 
50
84
  if (!task) {
@@ -57,13 +91,13 @@ export class TaskManager implements ITaskManager {
57
91
  );
58
92
  }
59
93
 
60
- const updatedTask: TaskFile = { ...task, status: 'in-review' };
94
+ const updatedTask = this.withStatus(task, 'in-review');
61
95
  await this.taskProvider.updateTask(updatedTask);
62
96
 
63
97
  return updatedTask;
64
98
  }
65
99
 
66
- async createTask(options: CreateTaskOptions): Promise<CreateTaskResult> {
100
+ async createTask(options: CreateTaskOptions): Promise<CreateTaskResult<TTask>> {
67
101
  return this.taskProvider.createTask(options);
68
102
  }
69
103
 
@@ -1,4 +1,4 @@
1
- import type { Task, TaskType, User } from '@opentask/taskin-types';
1
+ import type { Task, TaskId, TaskType } from '@opentask/taskin-types';
2
2
 
3
3
  /**
4
4
  * Options for creating a new task
@@ -16,16 +16,18 @@ export interface CreateTaskOptions {
16
16
  }
17
17
 
18
18
  /**
19
- * Result of creating a new task
19
+ * Result of creating a new task.
20
+ *
21
+ * Providers are free to return a wider object (e.g. the file system provider
22
+ * adds `filePath`); returning extra fields is allowed because the result is
23
+ * only ever consumed through this contract.
24
+ *
25
+ * @typeParam TTask - The task shape produced by the provider
20
26
  * @public
21
27
  */
22
- export interface CreateTaskResult {
28
+ export interface CreateTaskResult<TTask extends Task = Task> {
23
29
  /** The created task */
24
- task: TaskFile;
25
- /** The generated task ID */
26
- taskId: string;
27
- /** The file path where the task was created */
28
- filePath: string;
30
+ task: TTask;
29
31
  }
30
32
 
31
33
  /**
@@ -68,60 +70,58 @@ export interface LintResult {
68
70
  infoCount: number;
69
71
  }
70
72
 
71
- /**
72
- * A task with additional file system metadata.
73
- * Extends the base Task type with content and path information.
74
- * @public
75
- */
76
- export type TaskFile = Task & {
77
- /** The raw markdown content of the task file */
78
- content: string;
79
- /** Absolute or relative path to the task file */
80
- filePath: string;
81
- /** The type of work this task represents */
82
- type: TaskType;
83
- /** Optional user assigned to this task */
84
- assignee?: User;
85
- };
86
-
87
73
  /**
88
74
  * Interface for task storage providers.
89
75
  * Implementations handle reading and writing tasks from different sources
90
- * (e.g., file system, database, API).
76
+ * (e.g., file system, GitHub issues, Redmine).
77
+ *
78
+ * The task shape is a type parameter so that a provider can enrich `Task` with
79
+ * whatever its backing store requires — the file system provider carries
80
+ * `content`/`filePath`, a Redmine provider would carry its own fields — without
81
+ * that shape leaking into this package. Consumers that do not care about the
82
+ * extra fields can simply use the default and work with plain `Task`.
83
+ *
84
+ * @typeParam TTask - The task shape this provider reads and writes
91
85
  * @public
92
86
  */
93
- export interface ITaskProvider {
87
+ /*
88
+ * Os membros sao propriedades de funcao, nao metodos, de proposito: TypeScript
89
+ * trata metodos como bivariantes mesmo com `strictFunctionTypes`, e isso
90
+ * deixava `ITaskProvider<TaskFile>` ser atribuido a `ITaskProvider<Task>` — o
91
+ * que compila e depois quebra em `updateTask`, que le `task.filePath`.
92
+ */
93
+ export interface ITaskProvider<TTask extends Task = Task> {
94
94
  /**
95
95
  * Initialize the provider, performing any necessary setup or loading.
96
96
  * This may involve reading existing tasks, setting up connections, etc.
97
97
  */
98
- initialize(): Promise<void>;
98
+ initialize: () => Promise<void>;
99
99
 
100
100
  /**
101
101
  * Find a specific task by its ID.
102
102
  * @param taskId - The unique identifier of the task
103
103
  * @returns The task if found, undefined otherwise
104
104
  */
105
- findTask(taskId: string): Promise<TaskFile | undefined>;
105
+ findTask: (taskId: TaskId) => Promise<TTask | undefined>;
106
106
 
107
107
  /**
108
108
  * Retrieve all tasks from the provider.
109
109
  * @returns Array of all tasks
110
110
  */
111
- getAllTasks(): Promise<TaskFile[]>;
111
+ getAllTasks: () => Promise<TTask[]>;
112
112
 
113
113
  /**
114
114
  * Update an existing task.
115
115
  * @param task - The task with updated information
116
116
  */
117
- updateTask(task: TaskFile): Promise<void>;
117
+ updateTask: (task: TTask) => Promise<void>;
118
118
 
119
119
  /**
120
120
  * Create a new task.
121
121
  * @param options - Options for creating the task
122
122
  * @returns The created task information
123
123
  */
124
- createTask(options: CreateTaskOptions): Promise<CreateTaskResult>;
124
+ createTask: (options: CreateTaskOptions) => Promise<CreateTaskResult<TTask>>;
125
125
 
126
126
  /**
127
127
  * Validate all tasks managed by this provider.
@@ -129,15 +129,22 @@ export interface ITaskProvider {
129
129
  * @param fix - If true, attempt to automatically fix validation issues
130
130
  * @returns The lint result with any validation issues found
131
131
  */
132
- lint(fix?: boolean): Promise<LintResult>;
132
+ lint: (fix?: boolean) => Promise<LintResult>;
133
133
  }
134
134
 
135
135
  /**
136
136
  * Interface for task management operations.
137
137
  * Provides high-level methods for managing task workflow and state transitions.
138
+ *
139
+ * Mirrors the provider's task shape: a manager built on top of the file system
140
+ * provider hands back the provider's richer task, while a manager built on any
141
+ * other provider hands back that provider's shape. This package never needs to
142
+ * know which one it is.
143
+ *
144
+ * @typeParam TTask - The task shape produced by the underlying provider
138
145
  * @public
139
146
  */
140
- export interface ITaskManager {
147
+ export interface ITaskManager<TTask extends Task = Task> {
141
148
  /**
142
149
  * Mark a task as finished.
143
150
  * Transitions the task to 'done' status.
@@ -145,7 +152,7 @@ export interface ITaskManager {
145
152
  * @returns The updated task
146
153
  * @throws Error if task is not found
147
154
  */
148
- finishTask(taskId: string): Promise<TaskFile>;
155
+ finishTask: (taskId: TaskId) => Promise<TTask>;
149
156
 
150
157
  /**
151
158
  * Mark a task as ready for review.
@@ -154,23 +161,34 @@ export interface ITaskManager {
154
161
  * @returns The updated task
155
162
  * @throws Error if task is not found or not in 'in-progress' status
156
163
  */
157
- reviewTask(taskId: string): Promise<TaskFile>;
164
+ reviewTask: (taskId: TaskId) => Promise<TTask>;
158
165
 
159
166
  /**
160
167
  * Start working on a task.
161
168
  * Transitions the task to 'in-progress' status.
169
+ * Also used to resume a paused task.
162
170
  * @param taskId - The unique identifier of the task
163
171
  * @returns The updated task
164
172
  * @throws Error if task is not found, already in progress, or already done
165
173
  */
166
- startTask(taskId: string): Promise<TaskFile>;
174
+ startTask: (taskId: TaskId) => Promise<TTask>;
175
+
176
+ /**
177
+ * Pause work on a task.
178
+ * Transitions the task from 'in-progress' to 'paused' status.
179
+ * Resume with {@link ITaskManager.startTask}.
180
+ * @param taskId - The unique identifier of the task
181
+ * @returns The updated task
182
+ * @throws Error if task is not found or not in 'in-progress' status
183
+ */
184
+ pauseTask: (taskId: TaskId) => Promise<TTask>;
167
185
 
168
186
  /**
169
187
  * Create a new task.
170
188
  * @param options - Options for creating the task
171
189
  * @returns The created task information
172
190
  */
173
- createTask(options: CreateTaskOptions): Promise<CreateTaskResult>;
191
+ createTask: (options: CreateTaskOptions) => Promise<CreateTaskResult<TTask>>;
174
192
 
175
193
  /**
176
194
  * Validate all tasks in the system.
@@ -178,5 +196,5 @@ export interface ITaskManager {
178
196
  * @param fix - If true, attempt to automatically fix validation issues
179
197
  * @returns The lint result with any validation issues found
180
198
  */
181
- lint(fix?: boolean): Promise<LintResult>;
199
+ lint: (fix?: boolean) => Promise<LintResult>;
182
200
  }