digital-tasks 2.0.1 → 2.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # digital-tasks
2
2
 
3
+ ## 2.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [6beb531]
8
+ - ai-functions@2.1.1
9
+ - digital-tools@2.1.1
10
+ - digital-workers@2.1.1
11
+
12
+ ## 2.0.3
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies
17
+ - rpc.do@0.2.0
18
+ - ai-functions@2.0.3
19
+ - digital-tools@2.0.3
20
+ - digital-workers@2.0.3
21
+
22
+ ## 2.0.2
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies
27
+ - ai-functions@2.0.2
28
+ - digital-tools@2.0.2
29
+ - digital-workers@2.0.2
30
+
3
31
  ## 2.0.1
4
32
 
5
33
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "digital-tasks",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "Task management primitives for digital workers (agents and humans)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,10 +24,9 @@
24
24
  "clean": "rm -rf dist"
25
25
  },
26
26
  "dependencies": {
27
- "ai-functions": "workspace:*",
28
- "digital-tools": "workspace:*",
29
- "digital-workers": "workspace:*",
30
- "rpc.do": "^0.1.0"
27
+ "ai-functions": "2.1.1",
28
+ "digital-tools": "2.1.1",
29
+ "digital-workers": "2.1.1"
31
30
  },
32
31
  "keywords": [
33
32
  "ai",
package/src/index.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * digital-tasks - Task management primitives for digital workers
3
+ *
4
+ * Task = Function + metadata (status, progress, assignment, dependencies)
5
+ *
6
+ * Every task wraps a function (code, generative, agentic, or human)
7
+ * with lifecycle management, worker assignment, and dependency tracking.
8
+ *
9
+ * ## Quick Start
10
+ *
11
+ * ```ts
12
+ * import { Task, createTask, taskQueue } from 'digital-tasks'
13
+ *
14
+ * // Create a task from a function
15
+ * const task = await createTask({
16
+ * function: {
17
+ * type: 'generative',
18
+ * name: 'summarize',
19
+ * args: { text: 'The text to summarize' },
20
+ * output: 'string',
21
+ * promptTemplate: 'Summarize: {{text}}',
22
+ * },
23
+ * input: { text: 'Long article...' },
24
+ * priority: 'high',
25
+ * })
26
+ *
27
+ * // Start and complete
28
+ * await startTask(task.id, { type: 'agent', id: 'agent_1' })
29
+ * await completeTask(task.id, 'Summary of the article...')
30
+ * ```
31
+ *
32
+ * ## Projects with Parallel/Sequential Tasks
33
+ *
34
+ * ```ts
35
+ * import { createProject, task, parallel, sequential, toMarkdown } from 'digital-tasks'
36
+ *
37
+ * const project = createProject({
38
+ * name: 'Launch Feature',
39
+ * tasks: [
40
+ * parallel(
41
+ * task('Design mockups'),
42
+ * task('Write tech spec'),
43
+ * ),
44
+ * sequential(
45
+ * task('Implement backend'),
46
+ * task('Implement frontend'),
47
+ * task('Write tests'),
48
+ * ),
49
+ * ],
50
+ * })
51
+ *
52
+ * // Convert to markdown
53
+ * const md = toMarkdown(project)
54
+ * // # Launch Feature
55
+ * // - [ ] Design mockups
56
+ * // - [ ] Write tech spec
57
+ * // 1. [ ] Implement backend
58
+ * // 2. [ ] Implement frontend
59
+ * // 3. [ ] Write tests
60
+ * ```
61
+ *
62
+ * @packageDocumentation
63
+ */
64
+ // ============================================================================
65
+ // Task Queue
66
+ // ============================================================================
67
+ export { taskQueue, createTaskQueue } from './queue.js';
68
+ // ============================================================================
69
+ // Task Management
70
+ // ============================================================================
71
+ export { createTask, getTask, startTask, updateProgress, completeTask, failTask, cancelTask, addComment, createSubtask, getSubtasks, waitForTask, } from './task.js';
72
+ export { task, parallel, sequential, createProject, workflow, materializeProject, getDependants, getDependencies, getReadyTasks, hasCycles, sortTasks, } from './project.js';
73
+ export { parseMarkdown, toMarkdown, syncStatusFromMarkdown, } from './markdown.js';
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Markdown Task List Parser and Serializer
3
+ *
4
+ * Bidirectional conversion between markdown task lists and Task objects.
5
+ *
6
+ * ## Syntax
7
+ *
8
+ * - `- [ ]` = Unordered/parallel tasks (can run simultaneously)
9
+ * - `1. [ ]` = Ordered/sequential tasks (must run in order)
10
+ * - `- [x]` or `1. [x]` = Completed task
11
+ * - `- [-]` = In progress task
12
+ * - `- [~]` = Blocked task
13
+ * - `- [!]` = Failed task
14
+ * - Indentation (2 spaces) = Subtasks
15
+ * - `# Heading` = Project name
16
+ * - `## Heading` = Task group/section
17
+ *
18
+ * ## Example
19
+ *
20
+ * ```markdown
21
+ * # Launch Feature
22
+ *
23
+ * ## Planning (parallel)
24
+ * - [ ] Design mockups
25
+ * - [ ] Write technical spec
26
+ * - [x] Create project board
27
+ *
28
+ * ## Implementation (sequential)
29
+ * 1. [ ] Implement backend API
30
+ * 2. [-] Implement frontend UI
31
+ * - [ ] Create components
32
+ * - [ ] Add state management
33
+ * 3. [ ] Write tests
34
+ *
35
+ * ## Deployment
36
+ * 1. [ ] Deploy to staging
37
+ * 2. [ ] QA testing
38
+ * 3. [ ] Deploy to production
39
+ * ```
40
+ *
41
+ * @packageDocumentation
42
+ */
43
+ import { task, parallel, sequential, createProject } from './project.js';
44
+ // ============================================================================
45
+ // Status Markers
46
+ // ============================================================================
47
+ /**
48
+ * Markdown checkbox markers and their task status
49
+ */
50
+ const STATUS_MARKERS = {
51
+ ' ': 'pending',
52
+ 'x': 'completed',
53
+ 'X': 'completed',
54
+ '-': 'in_progress',
55
+ '~': 'blocked',
56
+ '!': 'failed',
57
+ '/': 'cancelled',
58
+ '?': 'review',
59
+ };
60
+ /**
61
+ * Reverse mapping: task status to marker
62
+ */
63
+ const STATUS_TO_MARKER = {
64
+ pending: ' ',
65
+ queued: ' ',
66
+ assigned: '-',
67
+ in_progress: '-',
68
+ blocked: '~',
69
+ review: '?',
70
+ completed: 'x',
71
+ failed: '!',
72
+ cancelled: '/',
73
+ };
74
+ /**
75
+ * Priority markers (can be added after checkbox)
76
+ */
77
+ const PRIORITY_MARKERS = {
78
+ '!!': 'critical',
79
+ '!': 'urgent',
80
+ '^': 'high',
81
+ '': 'normal',
82
+ 'v': 'low',
83
+ };
84
+ // ============================================================================
85
+ // Parser
86
+ // ============================================================================
87
+ /**
88
+ * Parse a single line of markdown
89
+ */
90
+ function parseLine(line) {
91
+ const raw = line;
92
+ // Count leading spaces for indent (2 spaces = 1 level)
93
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length || 0;
94
+ const indent = Math.floor(leadingSpaces / 2);
95
+ const trimmed = line.slice(leadingSpaces);
96
+ // Check for heading
97
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
98
+ if (headingMatch) {
99
+ return {
100
+ indent,
101
+ isTask: false,
102
+ isOrdered: false,
103
+ title: headingMatch[2].trim(),
104
+ isHeading: true,
105
+ headingLevel: headingMatch[1].length,
106
+ raw,
107
+ };
108
+ }
109
+ // Check for unordered task: - [ ] or - [x] etc.
110
+ const unorderedMatch = trimmed.match(/^[-*]\s+\[([^\]]*)\]\s*(.*)$/);
111
+ if (unorderedMatch) {
112
+ const marker = unorderedMatch[1];
113
+ let title = unorderedMatch[2].trim();
114
+ let priority = 'normal';
115
+ // Check for priority marker at start of title
116
+ if (title.startsWith('!!')) {
117
+ priority = 'critical';
118
+ title = title.slice(2).trim();
119
+ }
120
+ else if (title.startsWith('!')) {
121
+ priority = 'urgent';
122
+ title = title.slice(1).trim();
123
+ }
124
+ else if (title.startsWith('^')) {
125
+ priority = 'high';
126
+ title = title.slice(1).trim();
127
+ }
128
+ else if (title.startsWith('v')) {
129
+ priority = 'low';
130
+ title = title.slice(1).trim();
131
+ }
132
+ return {
133
+ indent,
134
+ isTask: true,
135
+ isOrdered: false,
136
+ status: STATUS_MARKERS[marker] || 'pending',
137
+ priority,
138
+ title,
139
+ isHeading: false,
140
+ raw,
141
+ };
142
+ }
143
+ // Check for ordered task: 1. [ ] or 1. [x] etc.
144
+ const orderedMatch = trimmed.match(/^(\d+)\.\s+\[([^\]]*)\]\s*(.*)$/);
145
+ if (orderedMatch) {
146
+ const marker = orderedMatch[2];
147
+ let title = orderedMatch[3].trim();
148
+ let priority = 'normal';
149
+ // Check for priority marker
150
+ if (title.startsWith('!!')) {
151
+ priority = 'critical';
152
+ title = title.slice(2).trim();
153
+ }
154
+ else if (title.startsWith('!')) {
155
+ priority = 'urgent';
156
+ title = title.slice(1).trim();
157
+ }
158
+ else if (title.startsWith('^')) {
159
+ priority = 'high';
160
+ title = title.slice(1).trim();
161
+ }
162
+ else if (title.startsWith('v')) {
163
+ priority = 'low';
164
+ title = title.slice(1).trim();
165
+ }
166
+ return {
167
+ indent,
168
+ isTask: true,
169
+ isOrdered: true,
170
+ orderNumber: parseInt(orderedMatch[1], 10),
171
+ status: STATUS_MARKERS[marker] || 'pending',
172
+ priority,
173
+ title,
174
+ isHeading: false,
175
+ raw,
176
+ };
177
+ }
178
+ // Plain text line
179
+ return {
180
+ indent,
181
+ isTask: false,
182
+ isOrdered: false,
183
+ title: trimmed,
184
+ isHeading: false,
185
+ raw,
186
+ };
187
+ }
188
+ /**
189
+ * Parse tasks at a specific indent level
190
+ */
191
+ function parseTasksAtIndent(lines, startIndex, baseIndent) {
192
+ const tasks = [];
193
+ let index = startIndex;
194
+ let mode = 'parallel'; // Default based on first task type
195
+ let modeSet = false;
196
+ while (index < lines.length) {
197
+ const line = lines[index];
198
+ // Stop if we've gone back to a lower indent level
199
+ if (line.indent < baseIndent && (line.isTask || line.isHeading)) {
200
+ break;
201
+ }
202
+ // Skip lines at lower indent (they belong to parent)
203
+ if (line.indent < baseIndent) {
204
+ index++;
205
+ continue;
206
+ }
207
+ // Process tasks at our indent level
208
+ if (line.indent === baseIndent && line.isTask) {
209
+ // Set mode based on first task
210
+ if (!modeSet) {
211
+ mode = line.isOrdered ? 'sequential' : 'parallel';
212
+ modeSet = true;
213
+ }
214
+ // Parse subtasks
215
+ const { tasks: subtasks, nextIndex } = parseTasksAtIndent(lines, index + 1, baseIndent + 1);
216
+ const taskDef = task(line.title, {
217
+ priority: line.priority,
218
+ subtasks: subtasks.length > 0 ? subtasks : undefined,
219
+ metadata: {
220
+ _originalStatus: line.status,
221
+ _lineNumber: index,
222
+ },
223
+ });
224
+ tasks.push(taskDef);
225
+ index = nextIndex;
226
+ }
227
+ else {
228
+ index++;
229
+ }
230
+ }
231
+ return { tasks, nextIndex: index, mode };
232
+ }
233
+ /**
234
+ * Parse a markdown string into a Project
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * const markdown = `
239
+ * # My Project
240
+ *
241
+ * - [ ] Task 1
242
+ * - [ ] Task 2
243
+ *
244
+ * ## Sequential Work
245
+ * 1. [ ] Step 1
246
+ * 2. [ ] Step 2
247
+ * `
248
+ *
249
+ * const project = parseMarkdown(markdown)
250
+ * ```
251
+ */
252
+ export function parseMarkdown(markdown) {
253
+ // Normalize line endings (handle Windows \r\n and old Mac \r)
254
+ const normalizedMarkdown = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
255
+ const rawLines = normalizedMarkdown.split('\n');
256
+ const lines = rawLines.map(parseLine);
257
+ let projectName = 'Untitled Project';
258
+ let projectDescription;
259
+ const allTasks = [];
260
+ // Find project name from first h1
261
+ const h1Index = lines.findIndex(l => l.isHeading && l.headingLevel === 1);
262
+ if (h1Index !== -1) {
263
+ projectName = lines[h1Index].title;
264
+ }
265
+ // Process sections and tasks
266
+ let currentSection = null;
267
+ let index = 0;
268
+ while (index < lines.length) {
269
+ const line = lines[index];
270
+ // New section (h2)
271
+ if (line.isHeading && line.headingLevel === 2) {
272
+ // Save previous section
273
+ if (currentSection && currentSection.tasks.length > 0) {
274
+ if (currentSection.mode === 'sequential') {
275
+ allTasks.push(sequential(...currentSection.tasks));
276
+ }
277
+ else {
278
+ allTasks.push(parallel(...currentSection.tasks));
279
+ }
280
+ }
281
+ // Detect mode from section name (e.g., "## Implementation (sequential)")
282
+ let sectionName = line.title;
283
+ let sectionMode = 'parallel';
284
+ const modeMatch = sectionName.match(/\((parallel|sequential)\)\s*$/i);
285
+ if (modeMatch) {
286
+ sectionMode = modeMatch[1].toLowerCase();
287
+ sectionName = sectionName.replace(/\s*\((parallel|sequential)\)\s*$/i, '');
288
+ }
289
+ currentSection = { name: sectionName, mode: sectionMode, tasks: [] };
290
+ index++;
291
+ continue;
292
+ }
293
+ // Task at root level or in section
294
+ if (line.isTask && line.indent === 0) {
295
+ const { tasks, nextIndex, mode } = parseTasksAtIndent(lines, index, 0);
296
+ if (currentSection) {
297
+ currentSection.tasks.push(...tasks);
298
+ // Update section mode based on first task if not explicitly set
299
+ if (currentSection.tasks.length === tasks.length) {
300
+ currentSection.mode = mode;
301
+ }
302
+ }
303
+ else {
304
+ // No section, add to root with appropriate grouping
305
+ if (mode === 'sequential') {
306
+ allTasks.push(sequential(...tasks));
307
+ }
308
+ else {
309
+ allTasks.push(parallel(...tasks));
310
+ }
311
+ }
312
+ index = nextIndex;
313
+ continue;
314
+ }
315
+ index++;
316
+ }
317
+ // Add final section
318
+ if (currentSection && currentSection.tasks.length > 0) {
319
+ if (currentSection.mode === 'sequential') {
320
+ allTasks.push(sequential(...currentSection.tasks));
321
+ }
322
+ else {
323
+ allTasks.push(parallel(...currentSection.tasks));
324
+ }
325
+ }
326
+ return createProject({
327
+ name: projectName,
328
+ description: projectDescription,
329
+ tasks: allTasks,
330
+ });
331
+ }
332
+ /**
333
+ * Serialize a task node to markdown lines
334
+ */
335
+ function serializeTaskNode(node, indent, options, isSequential) {
336
+ const lines = [];
337
+ const indentStr = ' '.repeat(indent * (options.indentSize || 2));
338
+ if (node.__type === 'task') {
339
+ const taskDef = node;
340
+ const status = taskDef.metadata?._originalStatus || 'pending';
341
+ const marker = options.includeStatus !== false ? STATUS_TO_MARKER[status] : ' ';
342
+ let prefix;
343
+ if (isSequential) {
344
+ // Use numbered list for sequential
345
+ const num = taskDef.metadata?._sequenceNumber || 1;
346
+ prefix = `${num}. [${marker}]`;
347
+ }
348
+ else {
349
+ // Use bullet for parallel
350
+ prefix = `- [${marker}]`;
351
+ }
352
+ let title = taskDef.title;
353
+ if (options.includePriority && taskDef.priority && taskDef.priority !== 'normal') {
354
+ const priorityMarker = taskDef.priority === 'critical' ? '!!'
355
+ : taskDef.priority === 'urgent' ? '!'
356
+ : taskDef.priority === 'high' ? '^'
357
+ : taskDef.priority === 'low' ? 'v'
358
+ : '';
359
+ title = `${priorityMarker}${title}`;
360
+ }
361
+ lines.push(`${indentStr}${prefix} ${title}`);
362
+ // Serialize subtasks
363
+ if (taskDef.subtasks && taskDef.subtasks.length > 0) {
364
+ for (const subtask of taskDef.subtasks) {
365
+ lines.push(...serializeTaskNode(subtask, indent + 1, options, false));
366
+ }
367
+ }
368
+ }
369
+ else if (node.__type === 'parallel') {
370
+ const group = node;
371
+ let seqNum = 1;
372
+ for (const child of group.tasks) {
373
+ if (child.__type === 'task') {
374
+ child.metadata = {
375
+ ...child.metadata,
376
+ _sequenceNumber: seqNum++,
377
+ };
378
+ }
379
+ lines.push(...serializeTaskNode(child, indent, options, false));
380
+ }
381
+ }
382
+ else if (node.__type === 'sequential') {
383
+ const group = node;
384
+ let seqNum = 1;
385
+ for (const child of group.tasks) {
386
+ if (child.__type === 'task') {
387
+ child.metadata = {
388
+ ...child.metadata,
389
+ _sequenceNumber: seqNum++,
390
+ };
391
+ }
392
+ lines.push(...serializeTaskNode(child, indent, options, true));
393
+ }
394
+ }
395
+ return lines;
396
+ }
397
+ /**
398
+ * Serialize a Project to markdown
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * const project = createProject({
403
+ * name: 'My Project',
404
+ * tasks: [
405
+ * parallel(
406
+ * task('Task 1'),
407
+ * task('Task 2'),
408
+ * ),
409
+ * sequential(
410
+ * task('Step 1'),
411
+ * task('Step 2'),
412
+ * ),
413
+ * ],
414
+ * })
415
+ *
416
+ * const markdown = toMarkdown(project)
417
+ * // # My Project
418
+ * //
419
+ * // - [ ] Task 1
420
+ * // - [ ] Task 2
421
+ * //
422
+ * // 1. [ ] Step 1
423
+ * // 2. [ ] Step 2
424
+ * ```
425
+ */
426
+ export function toMarkdown(project, options = {}) {
427
+ const lines = [];
428
+ // Project title
429
+ lines.push(`# ${project.name}`);
430
+ lines.push('');
431
+ if (project.description) {
432
+ lines.push(project.description);
433
+ lines.push('');
434
+ }
435
+ // Tasks
436
+ for (const node of project.tasks) {
437
+ const taskLines = serializeTaskNode(node, 0, options, false);
438
+ lines.push(...taskLines);
439
+ // Add blank line between top-level groups
440
+ if (taskLines.length > 0) {
441
+ lines.push('');
442
+ }
443
+ }
444
+ return lines.join('\n').trim() + '\n';
445
+ }
446
+ // ============================================================================
447
+ // Conversion Utilities
448
+ // ============================================================================
449
+ /**
450
+ * Update task statuses in a project from markdown
451
+ * (Useful for syncing when markdown is edited externally)
452
+ */
453
+ export function syncStatusFromMarkdown(project, markdown) {
454
+ const parsed = parseMarkdown(markdown);
455
+ // Build a map of task titles to statuses from parsed markdown
456
+ const statusMap = new Map();
457
+ function collectStatuses(node) {
458
+ if (node.__type === 'task') {
459
+ const taskDef = node;
460
+ const status = taskDef.metadata?._originalStatus;
461
+ if (status) {
462
+ statusMap.set(taskDef.title, status);
463
+ }
464
+ if (taskDef.subtasks) {
465
+ taskDef.subtasks.forEach(collectStatuses);
466
+ }
467
+ }
468
+ else if (node.__type === 'parallel' || node.__type === 'sequential') {
469
+ const group = node;
470
+ group.tasks.forEach(collectStatuses);
471
+ }
472
+ }
473
+ parsed.tasks.forEach(collectStatuses);
474
+ // Update statuses in original project
475
+ function updateStatuses(node) {
476
+ if (node.__type === 'task') {
477
+ const taskDef = node;
478
+ const newStatus = statusMap.get(taskDef.title);
479
+ return {
480
+ ...taskDef,
481
+ metadata: {
482
+ ...taskDef.metadata,
483
+ _originalStatus: newStatus || taskDef.metadata?._originalStatus,
484
+ },
485
+ subtasks: taskDef.subtasks?.map(updateStatuses),
486
+ };
487
+ }
488
+ else if (node.__type === 'parallel') {
489
+ const group = node;
490
+ return {
491
+ ...group,
492
+ tasks: group.tasks.map(updateStatuses),
493
+ };
494
+ }
495
+ else if (node.__type === 'sequential') {
496
+ const group = node;
497
+ return {
498
+ ...group,
499
+ tasks: group.tasks.map(updateStatuses),
500
+ };
501
+ }
502
+ return node;
503
+ }
504
+ return {
505
+ ...project,
506
+ tasks: project.tasks.map(updateStatuses),
507
+ updatedAt: new Date(),
508
+ };
509
+ }