@brainfile/cli 0.17.0 → 0.17.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 (93) hide show
  1. package/.brainfile/brainfile.md.v1.bak +171 -0
  2. package/.brainfile/logs/task-1.md +206 -0
  3. package/.brainfile/logs/task-2.md +482 -0
  4. package/.brainfile/logs/task-3.md +314 -0
  5. package/.github/workflows/release.yml +35 -0
  6. package/README.md +3 -5
  7. package/dist/cli.js +29 -53
  8. package/dist/cli.js.map +1 -1
  9. package/dist/commands/add.d.ts +2 -0
  10. package/dist/commands/add.d.ts.map +1 -1
  11. package/dist/commands/add.js +16 -2
  12. package/dist/commands/add.js.map +1 -1
  13. package/dist/commands/complete.d.ts +5 -7
  14. package/dist/commands/complete.d.ts.map +1 -1
  15. package/dist/commands/complete.js +76 -127
  16. package/dist/commands/complete.js.map +1 -1
  17. package/dist/commands/contract.d.ts +46 -2
  18. package/dist/commands/contract.d.ts.map +1 -1
  19. package/dist/commands/contract.js +499 -2
  20. package/dist/commands/contract.js.map +1 -1
  21. package/dist/commands/mcp.d.ts.map +1 -1
  22. package/dist/commands/mcp.js +1037 -1763
  23. package/dist/commands/mcp.js.map +1 -1
  24. package/dist/commands/migrate.d.ts +0 -2
  25. package/dist/commands/migrate.d.ts.map +1 -1
  26. package/dist/commands/migrate.js +23 -89
  27. package/dist/commands/migrate.js.map +1 -1
  28. package/dist/commands/move.js.map +1 -1
  29. package/dist/commands/schema.d.ts.map +1 -1
  30. package/dist/commands/schema.js +6 -1
  31. package/dist/commands/schema.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/lib/contractRunner.d.ts.map +1 -1
  37. package/dist/lib/contractRunner.js +76 -49
  38. package/dist/lib/contractRunner.js.map +1 -1
  39. package/dist/mcp/tools/contract.d.ts +25 -0
  40. package/dist/mcp/tools/contract.d.ts.map +1 -0
  41. package/dist/mcp/tools/contract.js +33 -0
  42. package/dist/mcp/tools/contract.js.map +1 -0
  43. package/dist/utils/config.d.ts.map +1 -1
  44. package/dist/utils/config.js +22 -9
  45. package/dist/utils/config.js.map +1 -1
  46. package/dist/utils/contractSpec.d.ts +2 -0
  47. package/dist/utils/contractSpec.d.ts.map +1 -1
  48. package/dist/utils/contractSpec.js +5 -1
  49. package/dist/utils/contractSpec.js.map +1 -1
  50. package/dist/utils/hook-settings.d.ts.map +1 -1
  51. package/dist/utils/hook-settings.js +5 -1
  52. package/dist/utils/hook-settings.js.map +1 -1
  53. package/dist/validation/command-lint.d.ts +11 -0
  54. package/dist/validation/command-lint.d.ts.map +1 -0
  55. package/dist/validation/command-lint.js +88 -0
  56. package/dist/validation/command-lint.js.map +1 -0
  57. package/package.json +2 -2
  58. package/dist/commands/context.d.ts +0 -19
  59. package/dist/commands/context.d.ts.map +0 -1
  60. package/dist/commands/context.js +0 -103
  61. package/dist/commands/context.js.map +0 -1
  62. package/dist/commands/history.d.ts +0 -18
  63. package/dist/commands/history.d.ts.map +0 -1
  64. package/dist/commands/history.js +0 -92
  65. package/dist/commands/history.js.map +0 -1
  66. package/dist/commands/ledger-rebuild.d.ts +0 -17
  67. package/dist/commands/ledger-rebuild.d.ts.map +0 -1
  68. package/dist/commands/ledger-rebuild.js +0 -287
  69. package/dist/commands/ledger-rebuild.js.map +0 -1
  70. package/dist/commands/ledger.d.ts +0 -20
  71. package/dist/commands/ledger.d.ts.map +0 -1
  72. package/dist/commands/ledger.js +0 -111
  73. package/dist/commands/ledger.js.map +0 -1
  74. package/dist/commands/migrate-ledger.d.ts +0 -17
  75. package/dist/commands/migrate-ledger.d.ts.map +0 -1
  76. package/dist/commands/migrate-ledger.js +0 -127
  77. package/dist/commands/migrate-ledger.js.map +0 -1
  78. package/dist/commands/stats.d.ts +0 -20
  79. package/dist/commands/stats.d.ts.map +0 -1
  80. package/dist/commands/stats.js +0 -151
  81. package/dist/commands/stats.js.map +0 -1
  82. package/dist/tui/components/ArchivePanel.d.ts +0 -16
  83. package/dist/tui/components/ArchivePanel.d.ts.map +0 -1
  84. package/dist/tui/components/ArchivePanel.js +0 -115
  85. package/dist/tui/components/ArchivePanel.js.map +0 -1
  86. package/dist/utils/date-helpers.d.ts +0 -20
  87. package/dist/utils/date-helpers.d.ts.map +0 -1
  88. package/dist/utils/date-helpers.js +0 -54
  89. package/dist/utils/date-helpers.js.map +0 -1
  90. package/dist/utils/v2-tasks.d.ts +0 -121
  91. package/dist/utils/v2-tasks.d.ts.map +0 -1
  92. package/dist/utils/v2-tasks.js +0 -384
  93. package/dist/utils/v2-tasks.js.map +0 -1
@@ -39,7 +39,6 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
39
39
  const zod_1 = require("zod");
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
- const child_process_1 = require("child_process");
43
42
  const core_1 = require("@brainfile/core");
44
43
  const errorHandler_1 = require("../utils/errorHandler");
45
44
  const contractSpec_1 = require("../utils/contractSpec");
@@ -49,9 +48,9 @@ const github_auth_1 = require("../utils/github-auth");
49
48
  const linear_auth_1 = require("../utils/linear-auth");
50
49
  const core_2 = require("@brainfile/core");
51
50
  const contractRunner_1 = require("../lib/contractRunner");
51
+ const contract_1 = require("../mcp/tools/contract");
52
52
  const archive_1 = require("../utils/archive");
53
53
  const core_3 = require("@brainfile/core");
54
- const dist_1 = require("../../../core/dist");
55
54
  const v2_detect_1 = require("../utils/v2-detect");
56
55
  function sanitizeTypesConfig(raw) {
57
56
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
@@ -131,80 +130,6 @@ function findGitRoot(startDir) {
131
130
  }
132
131
  return null;
133
132
  }
134
- const LEDGER_CONTRACT_STATUSES = [
135
- 'ready',
136
- 'in_progress',
137
- 'delivered',
138
- 'done',
139
- 'failed',
140
- 'blocked',
141
- ];
142
- function resolveLedgerLogsDir(filePath) {
143
- if ((0, v2_detect_1.isV2)(filePath)) {
144
- return (0, v2_detect_1.getV2Dirs)(filePath).logsDir;
145
- }
146
- const absolutePath = path.resolve(filePath);
147
- const parentDir = path.dirname(absolutePath);
148
- const stateDir = path.basename(parentDir) === '.brainfile'
149
- ? parentDir
150
- : path.join(parentDir, '.brainfile');
151
- return path.join(stateDir, 'logs');
152
- }
153
- function normalizePositiveInt(value, fallback) {
154
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
155
- return fallback;
156
- }
157
- return Math.floor(value);
158
- }
159
- function normalizeNonNegativeInt(value, fallback) {
160
- if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
161
- return fallback;
162
- }
163
- return Math.floor(value);
164
- }
165
- function toIsoDaysAgo(days) {
166
- const now = Date.now();
167
- const msPerDay = 24 * 60 * 60 * 1000;
168
- return new Date(now - (days * msPerDay)).toISOString();
169
- }
170
- function inferFilesChangedFromGit(filePath) {
171
- const repoRoot = findGitRoot(path.dirname(path.resolve(filePath)));
172
- if (!repoRoot) {
173
- return [];
174
- }
175
- const inferred = new Set();
176
- const commands = ['git diff --name-only -- .', 'git diff --name-only --cached -- .'];
177
- for (const command of commands) {
178
- try {
179
- const stdout = (0, child_process_1.execSync)(command, {
180
- cwd: repoRoot,
181
- encoding: 'utf-8',
182
- stdio: ['ignore', 'pipe', 'ignore'],
183
- });
184
- for (const line of stdout.split('\n')) {
185
- const trimmed = line.trim();
186
- if (trimmed) {
187
- inferred.add(trimmed.replace(/\\/g, '/'));
188
- }
189
- }
190
- }
191
- catch {
192
- // Ignore git inference errors and fall back to core defaults.
193
- }
194
- }
195
- return Array.from(inferred);
196
- }
197
- function median(values) {
198
- if (values.length === 0) {
199
- return 0;
200
- }
201
- const sorted = [...values].sort((a, b) => a - b);
202
- const mid = Math.floor(sorted.length / 2);
203
- const rawMedian = sorted.length % 2 === 0
204
- ? (sorted[mid - 1] + sorted[mid]) / 2
205
- : sorted[mid];
206
- return Number(rawMedian.toFixed(3));
207
- }
208
133
  async function mcpCommand(options) {
209
134
  // Auto-discover brainfile if not specified
210
135
  let defaultFile = options.file;
@@ -392,72 +317,107 @@ async function mcpCommand(options) {
392
317
  content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
393
318
  };
394
319
  });
395
- // Search tasks tool
396
- server.registerTool('search_tasks', {
397
- title: 'Search Tasks',
398
- description: 'Search tasks by title, description, or other fields',
320
+ // Search tool (tasks + logs)
321
+ server.registerTool('search', {
322
+ title: 'Search',
323
+ description: 'Search tasks and logs by query, list recent logs, or view one task/log entry',
399
324
  inputSchema: {
400
325
  file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
401
- query: zod_1.z.string().describe('Search query (matches title, description, tags)'),
326
+ query: zod_1.z.string().optional().describe('Search query (matches title, description, tags, and log text in v2)'),
402
327
  column: zod_1.z.string().optional().describe('Filter by column ID or name'),
403
328
  priority: zod_1.z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Filter by priority'),
404
- assignee: zod_1.z.string().optional().describe('Filter by assignee')
329
+ assignee: zod_1.z.string().optional().describe('Filter by assignee'),
330
+ recent: zod_1.z.boolean().optional().describe('List recently completed tasks (v2 only)'),
331
+ task: zod_1.z.string().optional().describe('View a specific task/log entry (v2 only)'),
405
332
  }
406
- }, async ({ file, query, column, priority, assignee }) => {
333
+ }, async ({ file, query, column, priority, assignee, recent, task }) => {
407
334
  const filePath = file || defaultFile;
335
+ if (task) {
336
+ if (!(0, v2_detect_1.isV2)(filePath)) {
337
+ return { content: [{ type: 'text', text: 'Error: task lookup in search requires v2 per-task file architecture.' }], isError: true };
338
+ }
339
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
340
+ const found = (0, v2_detect_1.findV2Task)(dirs, task, true);
341
+ if (!found) {
342
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
343
+ }
344
+ const output = {
345
+ id: found.doc.task.id,
346
+ title: found.doc.task.title,
347
+ completedAt: found.doc.task.completedAt,
348
+ isLog: found.isLog,
349
+ description: (0, v2_detect_1.extractDescription)(found.doc.body),
350
+ log: (0, v2_detect_1.extractLog)(found.doc.body),
351
+ };
352
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
353
+ }
354
+ if (recent) {
355
+ if (!(0, v2_detect_1.isV2)(filePath)) {
356
+ return { content: [{ type: 'text', text: 'Error: recent log listing requires v2 per-task file architecture.' }], isError: true };
357
+ }
358
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
359
+ const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
360
+ logDocs.sort((a, b) => (b.task.completedAt || '').localeCompare(a.task.completedAt || ''));
361
+ const logs = logDocs.slice(0, 20).map(doc => ({
362
+ id: doc.task.id,
363
+ title: doc.task.title,
364
+ completedAt: doc.task.completedAt,
365
+ }));
366
+ return { content: [{ type: 'text', text: JSON.stringify({ logs, count: logs.length }, null, 2) }] };
367
+ }
368
+ if (!query) {
369
+ return { content: [{ type: 'text', text: 'Error: query is required unless recent or task is provided' }], isError: true };
370
+ }
371
+ const queryLower = query.toLowerCase();
408
372
  // V2: search per-task files
409
373
  if ((0, v2_detect_1.isV2)(filePath)) {
410
374
  const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
411
- const queryLower = query.toLowerCase();
412
- let matches = [];
413
- const taskDocs = (0, core_3.readTasksDir)(dirs.boardDir);
414
- for (const doc of taskDocs) {
415
- const task = doc.task;
416
- if (column && task.column !== column)
417
- continue;
418
- if (priority && task.priority !== priority)
419
- continue;
420
- if (assignee && task.assignee !== assignee)
421
- continue;
375
+ const matches = [];
376
+ const scoreDoc = (doc, includeLogText) => {
377
+ const t = doc.task;
422
378
  let score = 0;
423
- if (task.title.toLowerCase().includes(queryLower)) {
379
+ if (t.title.toLowerCase().includes(queryLower)) {
424
380
  score += 10;
425
- if (task.title.toLowerCase().startsWith(queryLower))
381
+ if (t.title.toLowerCase().startsWith(queryLower))
426
382
  score += 5;
427
383
  }
428
- if (task.description?.toLowerCase().includes(queryLower))
384
+ if (t.description?.toLowerCase().includes(queryLower))
385
+ score += 5;
386
+ if ((0, v2_detect_1.extractDescription)(doc.body)?.toLowerCase().includes(queryLower))
429
387
  score += 5;
430
- if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
388
+ if (t.tags?.some(tag => tag.toLowerCase().includes(queryLower)))
431
389
  score += 3;
432
- if (task.id.toLowerCase() === queryLower)
390
+ if (includeLogText && (0, v2_detect_1.extractLog)(doc.body)?.toLowerCase().includes(queryLower))
391
+ score += 2;
392
+ if (t.id.toLowerCase() === queryLower)
433
393
  score += 20;
394
+ return score;
395
+ };
396
+ const taskDocs = (0, core_3.readTasksDir)(dirs.boardDir);
397
+ for (const doc of taskDocs) {
398
+ const t = doc.task;
399
+ if (column && t.column !== column)
400
+ continue;
401
+ if (priority && t.priority !== priority)
402
+ continue;
403
+ if (assignee && t.assignee !== assignee)
404
+ continue;
405
+ const score = scoreDoc(doc, false);
434
406
  if (score > 0) {
435
- matches.push({ id: task.id, title: task.title, column: task.column, priority: task.priority, tags: task.tags, assignee: task.assignee, score });
407
+ matches.push({ id: t.id, title: t.title, column: t.column, priority: t.priority, tags: t.tags, assignee: t.assignee, score });
436
408
  }
437
409
  }
438
- // Also search logs
439
410
  if (!column) {
440
411
  const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
441
412
  for (const doc of logDocs) {
442
- const task = doc.task;
443
- if (priority && task.priority !== priority)
413
+ const t = doc.task;
414
+ if (priority && t.priority !== priority)
444
415
  continue;
445
- if (assignee && task.assignee !== assignee)
416
+ if (assignee && t.assignee !== assignee)
446
417
  continue;
447
- let score = 0;
448
- if (task.title.toLowerCase().includes(queryLower)) {
449
- score += 10;
450
- if (task.title.toLowerCase().startsWith(queryLower))
451
- score += 5;
452
- }
453
- if (task.description?.toLowerCase().includes(queryLower))
454
- score += 5;
455
- if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
456
- score += 3;
457
- if (task.id.toLowerCase() === queryLower)
458
- score += 20;
418
+ const score = scoreDoc(doc, true);
459
419
  if (score > 0) {
460
- matches.push({ id: task.id, title: task.title, column: 'Completed', priority: task.priority, tags: task.tags, assignee: task.assignee, score, isLog: true });
420
+ matches.push({ id: t.id, title: t.title, column: 'Completed', priority: t.priority, tags: t.tags, assignee: t.assignee, score, isLog: true });
461
421
  }
462
422
  }
463
423
  }
@@ -470,8 +430,7 @@ async function mcpCommand(options) {
470
430
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
471
431
  }
472
432
  const { board } = result;
473
- const queryLower = query.toLowerCase();
474
- let matches = [];
433
+ const matches = [];
475
434
  for (const col of board.columns) {
476
435
  if (column) {
477
436
  const matchesId = col.id === column;
@@ -479,34 +438,33 @@ async function mcpCommand(options) {
479
438
  if (!matchesId && !matchesName)
480
439
  continue;
481
440
  }
482
- for (const task of col.tasks) {
483
- if (priority && task.priority !== priority)
441
+ for (const t of col.tasks) {
442
+ if (priority && t.priority !== priority)
484
443
  continue;
485
- if (assignee && task.assignee !== assignee)
444
+ if (assignee && t.assignee !== assignee)
486
445
  continue;
487
446
  let score = 0;
488
- if (task.title.toLowerCase().includes(queryLower)) {
447
+ if (t.title.toLowerCase().includes(queryLower)) {
489
448
  score += 10;
490
- if (task.title.toLowerCase().startsWith(queryLower))
449
+ if (t.title.toLowerCase().startsWith(queryLower))
491
450
  score += 5;
492
451
  }
493
- if (task.description?.toLowerCase().includes(queryLower))
452
+ if (t.description?.toLowerCase().includes(queryLower))
494
453
  score += 5;
495
- if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
454
+ if (t.tags?.some(tag => tag.toLowerCase().includes(queryLower)))
496
455
  score += 3;
497
- if (task.id.toLowerCase() === queryLower)
456
+ if (t.id.toLowerCase() === queryLower)
498
457
  score += 20;
499
458
  if (score > 0) {
500
- matches.push({ id: task.id, title: task.title, column: col.title, priority: task.priority, tags: task.tags, assignee: task.assignee, score });
459
+ matches.push({ id: t.id, title: t.title, column: col.title, priority: t.priority, tags: t.tags, assignee: t.assignee, score });
501
460
  }
502
461
  }
503
462
  }
504
463
  matches.sort((a, b) => b.score - a.score);
505
- const output = { results: matches, count: matches.length };
506
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
464
+ return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
507
465
  });
508
466
  // Add task tool
509
- server.registerTool('add_task', {
467
+ server.registerTool('task_add', {
510
468
  title: 'Add Task',
511
469
  description: 'Add a new task to a column in the brainfile',
512
470
  inputSchema: {
@@ -522,7 +480,8 @@ async function mcpCommand(options) {
522
480
  relatedFiles: zod_1.z.array(zod_1.z.string()).optional().describe('Related file paths'),
523
481
  type: zod_1.z.string().optional().describe('Document type (e.g., epic, adr). Determines ID prefix. Default: task'),
524
482
  // Contract creation (optional)
525
- with_contract: zod_1.z.boolean().optional().describe('Attach a contract to the new task (status=ready)'),
483
+ with_contract: zod_1.z.boolean().optional().describe('Attach a contract to the new task (default status=draft; use ready:true to make immediately dispatchable)'),
484
+ ready: zod_1.z.boolean().optional().describe('When true, contract status is set to ready instead of draft'),
526
485
  deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('Contract deliverables: type:path:description'),
527
486
  validation_commands: zod_1.z.array(zod_1.z.string()).optional().describe('Contract validation commands'),
528
487
  constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Contract constraints'),
@@ -530,7 +489,7 @@ async function mcpCommand(options) {
530
489
  withContract: zod_1.z.boolean().optional().describe('Alias of with_contract'),
531
490
  validationCommands: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of validation_commands'),
532
491
  }
533
- }, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, type: docType, with_contract, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
492
+ }, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, type: docType, with_contract, ready: contractReady, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
534
493
  const filePath = file || defaultFile;
535
494
  // V2: add task as individual file
536
495
  if ((0, v2_detect_1.isV2)(filePath)) {
@@ -570,7 +529,7 @@ async function mcpCommand(options) {
570
529
  ...(builtSubtasks && { subtasks: builtSubtasks }),
571
530
  createdAt: new Date().toISOString(),
572
531
  };
573
- // Optionally attach contract
532
+ // Optionally attach contract (default status=draft; ready:true → status=ready)
574
533
  const wantsContract = Boolean(with_contract ?? withContract) ||
575
534
  Boolean(deliverables && deliverables.length > 0) ||
576
535
  Boolean(validation_commands && validation_commands.length > 0) ||
@@ -581,6 +540,7 @@ async function mcpCommand(options) {
581
540
  deliverableSpecs: deliverables,
582
541
  validationCommands: validation_commands ?? validationCommands,
583
542
  constraints,
543
+ status: contractReady ? 'ready' : 'draft',
584
544
  });
585
545
  task.contract = contract;
586
546
  }
@@ -636,6 +596,7 @@ async function mcpCommand(options) {
636
596
  deliverableSpecs: deliverables,
637
597
  validationCommands: validation_commands ?? validationCommands,
638
598
  constraints,
599
+ status: contractReady ? 'ready' : 'draft',
639
600
  });
640
601
  const contractResult = (0, core_1.setTaskContract)(nextBoard, newTask.id, contract);
641
602
  if (!contractResult.success || !contractResult.board) {
@@ -652,93 +613,27 @@ async function mcpCommand(options) {
652
613
  content: [{ type: 'text', text: `Task added successfully: ${newTask.id} - ${newTask.title}` }]
653
614
  };
654
615
  });
655
- // Attach contract tool
656
- server.registerTool('attach_contract', {
657
- title: 'Attach Contract',
658
- description: 'Attach a new contract to an existing task (status=ready)',
659
- inputSchema: {
660
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
661
- task: zod_1.z.string().optional().describe('Task ID to attach contract to'),
662
- task_id: zod_1.z.string().optional().describe('Alias of task'),
663
- deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('Contract deliverables: type:path:description'),
664
- validation_commands: zod_1.z.array(zod_1.z.string()).optional().describe('Contract validation commands'),
665
- constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Contract constraints'),
666
- validationCommands: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of validation_commands'),
667
- }
668
- }, async ({ file, task, task_id, deliverables, validation_commands, constraints, validationCommands }) => {
669
- const filePath = file || defaultFile;
670
- const resolvedTaskId = task || task_id;
671
- if (!resolvedTaskId) {
672
- return { content: [{ type: 'text', text: 'Error: task is required' }], isError: true };
673
- }
674
- // V2: update task file directly
675
- if ((0, v2_detect_1.isV2)(filePath)) {
676
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
677
- const found = (0, v2_detect_1.findV2Task)(dirs, resolvedTaskId);
678
- if (!found) {
679
- return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
680
- }
681
- try {
682
- const contract = (0, contractSpec_1.buildContract)({
683
- deliverableSpecs: deliverables,
684
- validationCommands: validation_commands ?? validationCommands,
685
- constraints,
686
- });
687
- found.doc.task.contract = contract;
688
- found.doc.task.updatedAt = new Date().toISOString();
689
- (0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
690
- return { content: [{ type: 'text', text: `Contract attached: ${resolvedTaskId}` }] };
691
- }
692
- catch (e) {
693
- return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
694
- }
695
- }
696
- // V1: use board
697
- const result = readBoard(filePath);
698
- if ('error' in result) {
699
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
700
- }
701
- let { board } = result;
702
- const taskInfo = (0, core_1.findTaskById)(board, resolvedTaskId);
703
- if (!taskInfo) {
704
- return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
705
- }
706
- try {
707
- const contract = (0, contractSpec_1.buildContract)({
708
- deliverableSpecs: deliverables,
709
- validationCommands: validation_commands ?? validationCommands,
710
- constraints,
711
- });
712
- const contractResult = (0, core_1.setTaskContract)(board, resolvedTaskId, contract);
713
- if (!contractResult.success || !contractResult.board) {
714
- return { content: [{ type: 'text', text: `Error: ${contractResult.error || 'Failed to attach contract'}` }], isError: true };
715
- }
716
- writeBoard(filePath, contractResult.board);
717
- return { content: [{ type: 'text', text: `Contract attached: ${resolvedTaskId}` }] };
718
- }
719
- catch (e) {
720
- return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
721
- }
722
- });
723
616
  // Move task tool
724
- server.registerTool('move_task', {
617
+ server.registerTool('task_move', {
725
618
  title: 'Move Task',
726
619
  description: 'Move a task to a different column',
727
620
  inputSchema: {
728
621
  file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
729
- task: zod_1.z.string().describe('Task ID to move'),
622
+ taskId: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional().describe('Task ID or array of task IDs to move'),
623
+ task: zod_1.z.string().optional().describe('Alias of taskId for single task move'),
730
624
  column: zod_1.z.string().describe('Target column ID or name')
731
625
  }
732
- }, async ({ file, task, column }) => {
626
+ }, async ({ file, taskId, task, column }) => {
733
627
  const filePath = file || defaultFile;
628
+ const rawTaskIds = taskId ?? task;
629
+ if (!rawTaskIds) {
630
+ return { content: [{ type: 'text', text: 'Error: taskId is required' }], isError: true };
631
+ }
632
+ const taskIds = Array.isArray(rawTaskIds) ? rawTaskIds : [rawTaskIds];
633
+ const isBatch = taskIds.length > 1;
734
634
  // V2: update task file
735
635
  if ((0, v2_detect_1.isV2)(filePath)) {
736
636
  const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
737
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
738
- const doc = (0, core_3.readTaskFile)(taskPath);
739
- if (!doc) {
740
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
741
- }
742
637
  const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
743
638
  let targetColumn = board.columns.find(c => c.id === column);
744
639
  if (!targetColumn)
@@ -749,38 +644,60 @@ async function mcpCommand(options) {
749
644
  return mcpStructuredError(columnValidation.error || `Invalid column: ${targetColumnId}`, 'column', targetColumnId);
750
645
  }
751
646
  const resolvedTargetColumn = targetColumn || { id: column, title: column, tasks: [] };
752
- const sourceColumn = doc.task.column || '';
753
- const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === resolvedTargetColumn.id);
754
- doc.task.column = resolvedTargetColumn.id;
755
- doc.task.position = targetTasks.length;
756
- doc.task.updatedAt = new Date().toISOString();
757
- (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
758
- const shouldAutoComplete = resolvedTargetColumn.completionColumn === true && isTaskCompletable(doc.task.type, board.types);
759
- if (shouldAutoComplete) {
760
- const completeResult = (0, dist_1.completeTaskFile)(taskPath, dirs.logsDir);
761
- if (!completeResult.success) {
762
- return { content: [{ type: 'text', text: `Error: ${completeResult.error || `Failed to complete task: ${task}`}` }], isError: true };
763
- }
764
- }
765
- const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(doc.task, resolvedTargetColumn);
766
- let message = `Task ${task} moved from "${sourceColumn}" to "${resolvedTargetColumn.title}"`;
767
- if (shouldAutoComplete) {
768
- message += '\nTask auto-completed and moved to logs/.';
647
+ let nextPosition = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === resolvedTargetColumn.id).length;
648
+ const results = [];
649
+ for (const id of taskIds) {
650
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(id));
651
+ const doc = (0, core_3.readTaskFile)(taskPath);
652
+ if (!doc) {
653
+ results.push({ taskId: id, success: false, error: `Task not found: ${id}` });
654
+ continue;
655
+ }
656
+ const sourceColumn = doc.task.column || '';
657
+ doc.task.column = resolvedTargetColumn.id;
658
+ doc.task.position = nextPosition++;
659
+ doc.task.updatedAt = new Date().toISOString();
660
+ (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
661
+ const shouldAutoComplete = resolvedTargetColumn.completionColumn === true &&
662
+ isTaskCompletable(doc.task.type, board.types);
663
+ if (shouldAutoComplete) {
664
+ const completeResult = (0, core_3.completeTaskFile)(taskPath, dirs.logsDir);
665
+ if (!completeResult.success) {
666
+ results.push({ taskId: id, success: false, error: completeResult.error || `Failed to complete task: ${id}` });
667
+ continue;
668
+ }
669
+ }
670
+ const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(doc.task, resolvedTargetColumn);
671
+ let message = `Task ${id} moved from "${sourceColumn}" to "${resolvedTargetColumn.title}"`;
672
+ if (shouldAutoComplete) {
673
+ message += '\nTask auto-completed and moved to logs/.';
674
+ }
675
+ results.push({ taskId: id, success: true, message, warning: warning?.warning });
769
676
  }
770
- if (warning)
771
- message += `\n\n${warning.warning}`;
772
- return { content: [{ type: 'text', text: message }] };
677
+ if (!isBatch) {
678
+ const single = results[0];
679
+ if (!single?.success) {
680
+ return { content: [{ type: 'text', text: `Error: ${single?.error || 'Move failed'}` }], isError: true };
681
+ }
682
+ let text = single.message || `Task ${taskIds[0]} moved`;
683
+ if (single.warning)
684
+ text += `\n\n${single.warning}`;
685
+ return { content: [{ type: 'text', text }] };
686
+ }
687
+ const successCount = results.filter(r => r.success).length;
688
+ const failureCount = results.length - successCount;
689
+ const output = { success: failureCount === 0, successCount, failureCount, results };
690
+ return {
691
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
692
+ isError: failureCount > 0 && successCount === 0,
693
+ };
773
694
  }
774
695
  // V1: use board
775
696
  const result = readBoard(filePath);
776
697
  if ('error' in result) {
777
698
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
778
699
  }
779
- let { board } = result;
780
- const taskInfo = (0, core_1.findTaskById)(board, task);
781
- if (!taskInfo) {
782
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
783
- }
700
+ const { board } = result;
784
701
  let targetColumn = (0, core_1.findColumnById)(board, column);
785
702
  if (!targetColumn) {
786
703
  targetColumn = (0, core_1.findColumnByName)(board, column);
@@ -788,27 +705,64 @@ async function mcpCommand(options) {
788
705
  if (!targetColumn) {
789
706
  return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
790
707
  }
791
- const moveResult = (0, core_1.moveTask)(board, task, taskInfo.column.id, targetColumn.id, targetColumn.tasks.length);
792
- if (!moveResult.success) {
793
- return { content: [{ type: 'text', text: `Error: ${moveResult.error}` }], isError: true };
708
+ if (!isBatch) {
709
+ const id = taskIds[0];
710
+ const taskInfo = (0, core_1.findTaskById)(board, id);
711
+ if (!taskInfo) {
712
+ return { content: [{ type: 'text', text: `Error: Task not found: ${id}` }], isError: true };
713
+ }
714
+ const moveResult = (0, core_1.moveTask)(board, id, taskInfo.column.id, targetColumn.id, targetColumn.tasks.length);
715
+ if (!moveResult.success) {
716
+ return { content: [{ type: 'text', text: `Error: ${moveResult.error}` }], isError: true };
717
+ }
718
+ writeBoard(filePath, moveResult.board);
719
+ const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
720
+ let message = `Task ${id} moved from "${taskInfo.column.title}" to "${targetColumn.title}"`;
721
+ if (warning)
722
+ message += `\n\n${warning.warning}`;
723
+ return { content: [{ type: 'text', text: message }] };
724
+ }
725
+ const tasksWithIncomplete = [];
726
+ for (const id of taskIds) {
727
+ const taskInfo = (0, core_1.findTaskById)(board, id);
728
+ if (taskInfo) {
729
+ const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
730
+ if (warning?.incompleteSubtasks) {
731
+ tasksWithIncomplete.push({
732
+ id,
733
+ incomplete: warning.incompleteSubtasks.incomplete.length,
734
+ total: warning.incompleteSubtasks.total,
735
+ });
736
+ }
737
+ }
738
+ }
739
+ const bulkResult = (0, core_1.moveTasks)(board, taskIds, targetColumn.id);
740
+ if (bulkResult.board) {
741
+ writeBoard(filePath, bulkResult.board);
794
742
  }
795
- writeBoard(filePath, moveResult.board);
796
- const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
797
- let message = `Task ${task} moved from "${taskInfo.column.title}" to "${targetColumn.title}"`;
798
- if (warning) {
799
- message += `\n\n${warning.warning}`;
743
+ const output = {
744
+ success: bulkResult.success,
745
+ successCount: bulkResult.successCount,
746
+ failureCount: bulkResult.failureCount,
747
+ results: bulkResult.results,
748
+ };
749
+ if (tasksWithIncomplete.length > 0) {
750
+ output.warning = `${tasksWithIncomplete.length} task(s) moved to "${targetColumn.title}" have incomplete subtasks`;
751
+ output.tasksWithIncompleteSubtasks = tasksWithIncomplete;
800
752
  }
801
753
  return {
802
- content: [{ type: 'text', text: message }]
754
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
755
+ isError: !bulkResult.success,
803
756
  };
804
757
  });
805
758
  // Patch task tool
806
- server.registerTool('patch_task', {
759
+ server.registerTool('task_patch', {
807
760
  title: 'Patch Task',
808
761
  description: 'Update specific fields of a task. Set fields to null to remove them.',
809
762
  inputSchema: {
810
763
  file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
811
- task: zod_1.z.string().describe('Task ID to update'),
764
+ taskId: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional().describe('Task ID or array of task IDs to update'),
765
+ task: zod_1.z.string().optional().describe('Alias of taskId for single task update'),
812
766
  title: zod_1.z.string().optional().describe('New task title'),
813
767
  description: zod_1.z.string().nullable().optional().describe('New description (null to remove)'),
814
768
  priority: zod_1.z.enum(['low', 'medium', 'high', 'critical']).nullable().optional().describe('New priority (null to remove)'),
@@ -817,59 +771,83 @@ async function mcpCommand(options) {
817
771
  dueDate: zod_1.z.string().nullable().optional().describe('New due date (null to remove)'),
818
772
  relatedFiles: zod_1.z.array(zod_1.z.string()).nullable().optional().describe('Related file paths (null to remove)')
819
773
  }
820
- }, async ({ file, task, title, description, priority, tags, assignee, dueDate, relatedFiles }) => {
774
+ }, async ({ file, taskId, task, title, description, priority, tags, assignee, dueDate, relatedFiles }) => {
821
775
  const filePath = file || defaultFile;
776
+ const rawTaskIds = taskId ?? task;
777
+ if (!rawTaskIds) {
778
+ return { content: [{ type: 'text', text: 'Error: taskId is required' }], isError: true };
779
+ }
780
+ const taskIds = Array.isArray(rawTaskIds) ? rawTaskIds : [rawTaskIds];
781
+ const isBatch = taskIds.length > 1;
822
782
  const isNull = (v) => v === null || v === 'null';
823
783
  // V2: update task file directly
824
784
  if ((0, v2_detect_1.isV2)(filePath)) {
825
785
  const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
826
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
827
- const doc = (0, core_3.readTaskFile)(taskPath);
828
- if (!doc) {
829
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
786
+ const results = [];
787
+ for (const id of taskIds) {
788
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(id));
789
+ const doc = (0, core_3.readTaskFile)(taskPath);
790
+ if (!doc) {
791
+ results.push({ taskId: id, success: false, error: 'Task not found' });
792
+ continue;
793
+ }
794
+ const t = doc.task;
795
+ if (title !== undefined)
796
+ t.title = title;
797
+ if (description !== undefined) {
798
+ if (isNull(description))
799
+ delete t.description;
800
+ else
801
+ t.description = description;
802
+ }
803
+ if (priority !== undefined) {
804
+ if (isNull(priority))
805
+ delete t.priority;
806
+ else
807
+ t.priority = priority;
808
+ }
809
+ if (tags !== undefined) {
810
+ if (isNull(tags))
811
+ delete t.tags;
812
+ else
813
+ t.tags = tags;
814
+ }
815
+ if (assignee !== undefined) {
816
+ if (isNull(assignee))
817
+ delete t.assignee;
818
+ else
819
+ t.assignee = assignee;
820
+ }
821
+ if (dueDate !== undefined) {
822
+ if (isNull(dueDate))
823
+ delete t.dueDate;
824
+ else
825
+ t.dueDate = dueDate;
826
+ }
827
+ if (relatedFiles !== undefined) {
828
+ if (isNull(relatedFiles))
829
+ delete t.relatedFiles;
830
+ else
831
+ t.relatedFiles = relatedFiles;
832
+ }
833
+ t.updatedAt = new Date().toISOString();
834
+ (0, core_3.writeTaskFile)(taskPath, t, doc.body);
835
+ results.push({ taskId: id, success: true });
830
836
  }
831
- const t = doc.task;
832
- if (title !== undefined)
833
- t.title = title;
834
- if (description !== undefined) {
835
- if (isNull(description))
836
- delete t.description;
837
- else
838
- t.description = description;
839
- }
840
- if (priority !== undefined) {
841
- if (isNull(priority))
842
- delete t.priority;
843
- else
844
- t.priority = priority;
845
- }
846
- if (tags !== undefined) {
847
- if (isNull(tags))
848
- delete t.tags;
849
- else
850
- t.tags = tags;
851
- }
852
- if (assignee !== undefined) {
853
- if (isNull(assignee))
854
- delete t.assignee;
855
- else
856
- t.assignee = assignee;
857
- }
858
- if (dueDate !== undefined) {
859
- if (isNull(dueDate))
860
- delete t.dueDate;
861
- else
862
- t.dueDate = dueDate;
863
- }
864
- if (relatedFiles !== undefined) {
865
- if (isNull(relatedFiles))
866
- delete t.relatedFiles;
867
- else
868
- t.relatedFiles = relatedFiles;
869
- }
870
- t.updatedAt = new Date().toISOString();
871
- (0, core_3.writeTaskFile)(taskPath, t, doc.body);
872
- return { content: [{ type: 'text', text: `Task ${task} updated successfully` }] };
837
+ if (!isBatch) {
838
+ const single = results[0];
839
+ if (!single?.success) {
840
+ return { content: [{ type: 'text', text: `Error: ${single?.error || 'Task not found'}` }], isError: true };
841
+ }
842
+ return { content: [{ type: 'text', text: `Task ${taskIds[0]} updated successfully` }] };
843
+ }
844
+ const successCount = results.filter(r => r.success).length;
845
+ const failureCount = results.length - successCount;
846
+ const output = { success: failureCount === 0, successCount, failureCount, results };
847
+ return {
848
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
849
+ isError: failureCount > 0 && successCount === 0,
850
+ };
873
851
  }
874
852
  // V1: use board
875
853
  const result = readBoard(filePath);
@@ -892,17 +870,32 @@ async function mcpCommand(options) {
892
870
  patch.dueDate = isNull(dueDate) ? undefined : dueDate;
893
871
  if (relatedFiles !== undefined)
894
872
  patch.relatedFiles = isNull(relatedFiles) ? undefined : relatedFiles;
895
- const patchResult = (0, core_1.patchTask)(board, task, patch);
896
- if (!patchResult.success) {
897
- return { content: [{ type: 'text', text: `Error: ${patchResult.error}` }], isError: true };
873
+ if (!isBatch) {
874
+ const id = taskIds[0];
875
+ const patchResult = (0, core_1.patchTask)(board, id, patch);
876
+ if (!patchResult.success) {
877
+ return { content: [{ type: 'text', text: `Error: ${patchResult.error}` }], isError: true };
878
+ }
879
+ writeBoard(filePath, patchResult.board);
880
+ return { content: [{ type: 'text', text: `Task ${id} updated successfully` }] };
881
+ }
882
+ const bulkResult = (0, core_1.patchTasks)(board, taskIds, patch);
883
+ if (bulkResult.board) {
884
+ writeBoard(filePath, bulkResult.board);
898
885
  }
899
- writeBoard(filePath, patchResult.board);
886
+ const output = {
887
+ success: bulkResult.success,
888
+ successCount: bulkResult.successCount,
889
+ failureCount: bulkResult.failureCount,
890
+ results: bulkResult.results,
891
+ };
900
892
  return {
901
- content: [{ type: 'text', text: `Task ${task} updated successfully` }]
893
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
894
+ isError: !bulkResult.success,
902
895
  };
903
896
  });
904
897
  // Delete task tool
905
- server.registerTool('delete_task', {
898
+ server.registerTool('task_delete', {
906
899
  title: 'Delete Task',
907
900
  description: 'Permanently delete a task from the brainfile',
908
901
  inputSchema: {
@@ -946,212 +939,790 @@ async function mcpCommand(options) {
946
939
  content: [{ type: 'text', text: `Task ${task} deleted successfully` }]
947
940
  };
948
941
  });
949
- // Archive task tool
950
- server.registerTool('archive_task', {
951
- title: 'Archive Task',
952
- description: 'Archive a task locally or to an external service (GitHub Issues, Linear). If no destination is specified, uses the project default from brainfile.md, then user default from ~/.config/brainfile/config.json, then falls back to local.',
942
+ // Unified subtask tool (action-based)
943
+ server.registerTool('subtask', {
944
+ title: 'Subtask',
945
+ description: 'Unified subtask tool for add/toggle/delete/update with single, array, or all targeting',
953
946
  inputSchema: {
947
+ action: zod_1.z.enum(['add', 'toggle', 'delete', 'update']).describe('Subtask action'),
954
948
  file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
955
- task: zod_1.z.string().describe('Task ID to archive'),
956
- destination: zod_1.z.enum(['local', 'github', 'linear']).optional().describe('Archive destination: local (default), github (creates closed issue), or linear (creates completed issue)')
957
- }
958
- }, async ({ file, task, destination }) => {
949
+ task: zod_1.z.string().describe('Parent task ID'),
950
+ subtask: zod_1.z.string().optional().describe('Single subtask title/id depending on action'),
951
+ subtasks: zod_1.z.array(zod_1.z.string()).optional().describe('Subtask titles/ids depending on action'),
952
+ title: zod_1.z.string().optional().describe('New title for update action'),
953
+ titles: zod_1.z.array(zod_1.z.string()).optional().describe('Optional titles for batch update action'),
954
+ completed: zod_1.z.boolean().optional().describe('For toggle action: set explicit completed state (true/false) instead of flipping'),
955
+ all: zod_1.z.boolean().optional().describe('For toggle/delete action: target all subtasks in the task'),
956
+ }
957
+ }, async ({ action, file, task, subtask, subtasks, title, titles, completed, all }) => {
959
958
  const filePath = file || defaultFile;
960
- // V2: archive means move task from board/ to logs/
961
- if ((0, v2_detect_1.isV2)(filePath)) {
962
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
963
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
964
- if (!found) {
965
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
959
+ const listParam = subtasks ?? (subtask ? [subtask] : []);
960
+ const useAll = all === true;
961
+ const resolveUpdateTitles = (ids) => {
962
+ if (titles && titles.length > 0) {
963
+ if (titles.length !== ids.length && titles.length !== 1) {
964
+ return { ok: false, error: 'titles length must match subtasks length (or provide a single title to apply to all)' };
965
+ }
966
+ const values = titles.length === 1 ? ids.map(() => titles[0]) : titles;
967
+ return { ok: true, values };
966
968
  }
967
- // Determine effective destination
968
- const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
969
- const brainfileDestination = board.archive?.destination;
970
- const effectiveDestination = destination || (0, config_1.getEffectiveArchiveDestination)(brainfileDestination);
971
- // For GitHub/Linear, format and send before removing
972
- if (effectiveDestination === 'github') {
973
- if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
974
- return { content: [{ type: 'text', text: `Error: Not authenticated with GitHub.\n\nTo authenticate, run:\n npx @brainfile/cli auth github\n\nOr fall back to local archive:\n Use destination: "local"` }], isError: true };
969
+ if (title !== undefined) {
970
+ return { ok: true, values: ids.map(() => title) };
971
+ }
972
+ return { ok: false, error: 'title or titles is required for action=update' };
973
+ };
974
+ // ── add ────────────────────────────────────────────────────────────────
975
+ if (action === 'add') {
976
+ const titlesToAdd = listParam.map(value => value.trim()).filter(Boolean);
977
+ if (titlesToAdd.length === 0) {
978
+ return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=add' }], isError: true };
979
+ }
980
+ if ((0, v2_detect_1.isV2)(filePath)) {
981
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
982
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
983
+ if (!found) {
984
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
975
985
  }
976
- const config = (0, config_1.getArchiveConfig)();
977
- if (!config.github?.owner || !config.github?.repo) {
978
- return { content: [{ type: 'text', text: `Error: GitHub repository not configured.\n\nTo configure, run:\n npx @brainfile/cli config set archive.github.owner <owner>\n npx @brainfile/cli config set archive.github.repo <repo>` }], isError: true };
986
+ const t = found.doc.task;
987
+ if (!t.subtasks)
988
+ t.subtasks = [];
989
+ let nextIndex = t.subtasks.length > 0
990
+ ? Math.max(...t.subtasks.map(st => parseInt(st.id.split('-').pop() || '0', 10))) + 1
991
+ : 1;
992
+ const added = [];
993
+ for (const value of titlesToAdd) {
994
+ const id = `${task}-${nextIndex++}`;
995
+ const newSubtask = { id, title: value, completed: false };
996
+ t.subtasks.push(newSubtask);
997
+ added.push({ id: newSubtask.id, title: newSubtask.title });
979
998
  }
980
- const payload = (0, core_2.formatTaskForGitHub)(found.doc.task, {
981
- includeMeta: true, includeSubtasks: true, includeRelatedFiles: true,
982
- boardTitle: board.title, fromColumn: found.doc.task.column || 'unknown',
983
- extraLabels: config.github.labels,
984
- });
985
- const ghResult = await (0, github_auth_1.createGitHubIssue)({ owner: config.github.owner, repo: config.github.repo, title: payload.title, body: payload.body, labels: payload.labels, state: 'closed' });
986
- if (!ghResult.success) {
987
- return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
999
+ t.updatedAt = new Date().toISOString();
1000
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1001
+ if (added.length === 1) {
1002
+ return { content: [{ type: 'text', text: `Subtask added: ${added[0].id} - ${added[0].title}` }] };
988
1003
  }
989
- fs.unlinkSync(found.filePath);
990
- return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
991
- }
992
- if (effectiveDestination === 'linear') {
993
- if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
994
- return { content: [{ type: 'text', text: `Error: Not authenticated with Linear.\n\nTo authenticate, run:\n npx @brainfile/cli auth linear --token <api-key>` }], isError: true };
1004
+ return { content: [{ type: 'text', text: JSON.stringify({ added, count: added.length }, null, 2) }] };
1005
+ }
1006
+ const result = readBoard(filePath);
1007
+ if ('error' in result) {
1008
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1009
+ }
1010
+ let board = result.board;
1011
+ const added = [];
1012
+ for (const value of titlesToAdd) {
1013
+ const addResult = (0, core_1.addSubtask)(board, task, value);
1014
+ if (!addResult.success || !addResult.board) {
1015
+ return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
995
1016
  }
996
- const config = (0, config_1.getArchiveConfig)();
997
- let teamId = config.linear?.teamId;
998
- if (!teamId) {
999
- const teams = await (0, linear_auth_1.getLinearTeams)();
1000
- if (teams.length === 0) {
1001
- return { content: [{ type: 'text', text: `Error: No Linear teams found.` }], isError: true };
1002
- }
1003
- if (teams.length === 1) {
1004
- teamId = teams[0].id;
1005
- }
1006
- else {
1007
- const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
1008
- return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
1009
- }
1017
+ board = addResult.board;
1018
+ const updatedTask = (0, core_1.findTaskById)(board, task)?.task;
1019
+ const created = updatedTask?.subtasks?.slice(-1)[0];
1020
+ if (created)
1021
+ added.push({ id: created.id, title: created.title });
1022
+ }
1023
+ writeBoard(filePath, board);
1024
+ if (added.length === 1) {
1025
+ return { content: [{ type: 'text', text: `Subtask added: ${added[0].id} - ${added[0].title}` }] };
1026
+ }
1027
+ return { content: [{ type: 'text', text: JSON.stringify({ added, count: added.length }, null, 2) }] };
1028
+ }
1029
+ // ── delete ─────────────────────────────────────────────────────────────
1030
+ if (action === 'delete') {
1031
+ if ((0, v2_detect_1.isV2)(filePath)) {
1032
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1033
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1034
+ if (!found) {
1035
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1010
1036
  }
1011
- const payload = (0, core_2.formatTaskForLinear)(found.doc.task, {
1012
- includeMeta: true, includeSubtasks: true, includeRelatedFiles: true,
1013
- boardTitle: board.title, fromColumn: found.doc.task.column || 'unknown', stateName: 'Done',
1014
- });
1015
- const linearResult = await (0, linear_auth_1.createLinearIssue)({ teamId, title: payload.title, description: payload.description, priority: payload.priority, labelNames: payload.labelNames, stateName: 'Done' });
1016
- if (!linearResult.success) {
1017
- return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
1037
+ const t = found.doc.task;
1038
+ if (!t.subtasks || t.subtasks.length === 0) {
1039
+ return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
1018
1040
  }
1019
- fs.unlinkSync(found.filePath);
1020
- return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
1041
+ const targetIds = useAll ? t.subtasks.map(st => st.id) : listParam;
1042
+ if (targetIds.length === 0) {
1043
+ return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=delete (unless all=true)' }], isError: true };
1044
+ }
1045
+ const existing = new Set(t.subtasks.map(st => st.id));
1046
+ const deleted = targetIds.filter(id => existing.has(id));
1047
+ const missing = targetIds.filter(id => !existing.has(id));
1048
+ if (deleted.length === 0) {
1049
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
1050
+ }
1051
+ const deleteSet = new Set(deleted);
1052
+ t.subtasks = t.subtasks.filter(st => !deleteSet.has(st.id));
1053
+ t.updatedAt = new Date().toISOString();
1054
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1055
+ if (!useAll && deleted.length === 1 && missing.length === 0) {
1056
+ return { content: [{ type: 'text', text: `Subtask ${deleted[0]} deleted successfully` }] };
1057
+ }
1058
+ return { content: [{ type: 'text', text: JSON.stringify({ deleted, missing, count: deleted.length }, null, 2) }] };
1021
1059
  }
1022
- // Local archive: move from board/ to logs/
1023
- const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
1024
- found.doc.task.completedAt = found.doc.task.completedAt || new Date().toISOString();
1025
- delete found.doc.task.column;
1026
- delete found.doc.task.position;
1027
- (0, core_3.writeTaskFile)(logPath, found.doc.task, found.doc.body);
1028
- fs.unlinkSync(found.filePath);
1029
- return { content: [{ type: 'text', text: `Task ${task} archived to logs/` }] };
1030
- }
1031
- // V1: use board
1032
- const result = readBoard(filePath);
1033
- if ('error' in result) {
1034
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1060
+ const result = readBoard(filePath);
1061
+ if ('error' in result) {
1062
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1063
+ }
1064
+ let board = result.board;
1065
+ const taskInfo = (0, core_1.findTaskById)(board, task);
1066
+ if (!taskInfo) {
1067
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1068
+ }
1069
+ const targetIds = useAll ? (taskInfo.task.subtasks || []).map(st => st.id) : listParam;
1070
+ if (targetIds.length === 0) {
1071
+ return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=delete (unless all=true)' }], isError: true };
1072
+ }
1073
+ const deleted = [];
1074
+ const missing = [];
1075
+ for (const id of targetIds) {
1076
+ const deleteResult = (0, core_1.deleteSubtask)(board, task, id);
1077
+ if (!deleteResult.success || !deleteResult.board) {
1078
+ missing.push(id);
1079
+ continue;
1080
+ }
1081
+ board = deleteResult.board;
1082
+ deleted.push(id);
1083
+ }
1084
+ if (deleted.length === 0) {
1085
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
1086
+ }
1087
+ writeBoard(filePath, board);
1088
+ if (!useAll && deleted.length === 1 && missing.length === 0) {
1089
+ return { content: [{ type: 'text', text: `Subtask ${deleted[0]} deleted successfully` }] };
1090
+ }
1091
+ return { content: [{ type: 'text', text: JSON.stringify({ deleted, missing, count: deleted.length }, null, 2) }] };
1035
1092
  }
1036
- let { board } = result;
1037
- // Find the task to get its column
1038
- const taskInfo = (0, core_1.findTaskById)(board, task);
1039
- if (!taskInfo) {
1040
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1093
+ // ── toggle ─────────────────────────────────────────────────────────────
1094
+ if (action === 'toggle') {
1095
+ if ((0, v2_detect_1.isV2)(filePath)) {
1096
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1097
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1098
+ if (!found) {
1099
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1100
+ }
1101
+ const t = found.doc.task;
1102
+ if (!t.subtasks || t.subtasks.length === 0) {
1103
+ return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
1104
+ }
1105
+ const targetIds = useAll ? t.subtasks.map(st => st.id) : listParam;
1106
+ if (targetIds.length === 0) {
1107
+ return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=toggle (unless all=true)' }], isError: true };
1108
+ }
1109
+ const targetSet = new Set(targetIds);
1110
+ const updated = [];
1111
+ for (const st of t.subtasks) {
1112
+ if (!targetSet.has(st.id))
1113
+ continue;
1114
+ st.completed = completed !== undefined ? completed : !st.completed;
1115
+ updated.push({ id: st.id, completed: st.completed });
1116
+ }
1117
+ if (updated.length === 0) {
1118
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
1119
+ }
1120
+ t.updatedAt = new Date().toISOString();
1121
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1122
+ if (!useAll && updated.length === 1) {
1123
+ const status = updated[0].completed ? 'completed' : 'incomplete';
1124
+ return { content: [{ type: 'text', text: `Subtask ${updated[0].id} marked as ${status}` }] };
1125
+ }
1126
+ return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
1127
+ }
1128
+ const result = readBoard(filePath);
1129
+ if ('error' in result) {
1130
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1131
+ }
1132
+ let board = result.board;
1133
+ const taskInfo = (0, core_1.findTaskById)(board, task);
1134
+ if (!taskInfo) {
1135
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1136
+ }
1137
+ const targetIds = useAll ? (taskInfo.task.subtasks || []).map(st => st.id) : listParam;
1138
+ if (targetIds.length === 0) {
1139
+ return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=toggle (unless all=true)' }], isError: true };
1140
+ }
1141
+ const updated = [];
1142
+ if (useAll && completed !== undefined) {
1143
+ const setResult = (0, core_1.setAllSubtasksCompleted)(board, task, completed);
1144
+ if (!setResult.success || !setResult.board) {
1145
+ return { content: [{ type: 'text', text: `Error: ${setResult.error}` }], isError: true };
1146
+ }
1147
+ board = setResult.board;
1148
+ const updatedTask = (0, core_1.findTaskById)(board, task)?.task;
1149
+ for (const st of updatedTask?.subtasks || []) {
1150
+ updated.push({ id: st.id, completed: st.completed });
1151
+ }
1152
+ }
1153
+ else if (completed !== undefined && targetIds.length > 0) {
1154
+ const setResult = (0, core_1.setSubtasksCompleted)(board, task, targetIds, completed);
1155
+ if (!setResult.success || !setResult.board) {
1156
+ return { content: [{ type: 'text', text: `Error: ${setResult.error}` }], isError: true };
1157
+ }
1158
+ board = setResult.board;
1159
+ const updatedTask = (0, core_1.findTaskById)(board, task)?.task;
1160
+ const targetSet = new Set(targetIds);
1161
+ for (const st of updatedTask?.subtasks || []) {
1162
+ if (targetSet.has(st.id))
1163
+ updated.push({ id: st.id, completed: st.completed });
1164
+ }
1165
+ }
1166
+ else {
1167
+ for (const id of targetIds) {
1168
+ const toggleResult = (0, core_1.toggleSubtask)(board, task, id);
1169
+ if (!toggleResult.success || !toggleResult.board) {
1170
+ continue;
1171
+ }
1172
+ board = toggleResult.board;
1173
+ const st = (0, core_1.findTaskById)(board, task)?.task.subtasks?.find(entry => entry.id === id);
1174
+ if (st)
1175
+ updated.push({ id: st.id, completed: st.completed });
1176
+ }
1177
+ }
1178
+ if (updated.length === 0) {
1179
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
1180
+ }
1181
+ writeBoard(filePath, board);
1182
+ if (!useAll && updated.length === 1) {
1183
+ const status = updated[0].completed ? 'completed' : 'incomplete';
1184
+ return { content: [{ type: 'text', text: `Subtask ${updated[0].id} marked as ${status}` }] };
1185
+ }
1186
+ return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
1187
+ }
1188
+ // ── update ─────────────────────────────────────────────────────────────
1189
+ if (action === 'update') {
1190
+ const targetIds = listParam;
1191
+ if (targetIds.length === 0) {
1192
+ return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=update' }], isError: true };
1193
+ }
1194
+ const resolvedTitles = resolveUpdateTitles(targetIds);
1195
+ if (!resolvedTitles.ok) {
1196
+ return { content: [{ type: 'text', text: `Error: ${resolvedTitles.error}` }], isError: true };
1197
+ }
1198
+ if ((0, v2_detect_1.isV2)(filePath)) {
1199
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1200
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1201
+ if (!found) {
1202
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1203
+ }
1204
+ const t = found.doc.task;
1205
+ if (!t.subtasks || t.subtasks.length === 0) {
1206
+ return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
1207
+ }
1208
+ const updates = new Map();
1209
+ targetIds.forEach((id, i) => updates.set(id, resolvedTitles.values[i]));
1210
+ const updated = [];
1211
+ for (const st of t.subtasks) {
1212
+ const nextTitle = updates.get(st.id);
1213
+ if (nextTitle === undefined)
1214
+ continue;
1215
+ st.title = nextTitle;
1216
+ updated.push({ id: st.id, title: st.title });
1217
+ }
1218
+ if (updated.length === 0) {
1219
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
1220
+ }
1221
+ t.updatedAt = new Date().toISOString();
1222
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1223
+ if (updated.length === 1) {
1224
+ return { content: [{ type: 'text', text: `Subtask ${updated[0].id} updated to "${updated[0].title}"` }] };
1225
+ }
1226
+ return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
1227
+ }
1228
+ const result = readBoard(filePath);
1229
+ if ('error' in result) {
1230
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1231
+ }
1232
+ let board = result.board;
1233
+ const updated = [];
1234
+ targetIds.forEach((id, idx) => {
1235
+ const updateResult = (0, core_1.updateSubtask)(board, task, id, resolvedTitles.values[idx]);
1236
+ if (!updateResult.success || !updateResult.board)
1237
+ return;
1238
+ board = updateResult.board;
1239
+ const st = (0, core_1.findTaskById)(board, task)?.task.subtasks?.find(entry => entry.id === id);
1240
+ if (st)
1241
+ updated.push({ id: st.id, title: st.title });
1242
+ });
1243
+ if (updated.length === 0) {
1244
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
1245
+ }
1246
+ writeBoard(filePath, board);
1247
+ if (updated.length === 1) {
1248
+ return { content: [{ type: 'text', text: `Subtask ${updated[0].id} updated to "${updated[0].title}"` }] };
1249
+ }
1250
+ return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
1251
+ }
1252
+ return { content: [{ type: 'text', text: `Error: Unknown action: ${action}` }], isError: true };
1253
+ });
1254
+ // Unified contract tool (action-based)
1255
+ server.registerTool('contract', {
1256
+ title: 'Contract',
1257
+ description: [
1258
+ 'Unified action-based contract tool.',
1259
+ 'action=attach — Attach a new contract to a task (default status=draft; pass ready:true for immediate dispatch)',
1260
+ 'action=pickup — Claim a contract (status → in_progress), returns agent context markdown',
1261
+ 'action=deliver — Mark contract as delivered (status → delivered)',
1262
+ 'action=validate — Validate deliverables + commands (status → done/failed)',
1263
+ 'action=graph — Attach contracts to multiple tasks atomically with dependsOn DAG edges (tasks array only)',
1264
+ 'action=activate — Flip draft → ready for one task (task param) or all children of a parent (parentId param)',
1265
+ ].join('\n'),
1266
+ inputSchema: {
1267
+ action: zod_1.z.enum(['attach', 'pickup', 'deliver', 'validate', 'graph', 'activate']).describe('Contract action'),
1268
+ file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1269
+ task: zod_1.z.string().optional().describe('Task ID (required for attach, pickup, deliver, validate, and single-task activate)'),
1270
+ parentId: zod_1.z.string().optional().describe('For activate: activate all draft contracts whose parentId matches this value'),
1271
+ // attach fields
1272
+ ready: zod_1.z.boolean().optional().describe('attach only: when true, status=ready instead of draft'),
1273
+ deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('attach only: type:path:description'),
1274
+ validation_commands: zod_1.z.array(zod_1.z.string()).optional().describe('attach only: validation shell commands'),
1275
+ constraints: zod_1.z.array(zod_1.z.string()).optional().describe('attach only: constraint strings'),
1276
+ tasks: zod_1.z.array(zod_1.z.object({
1277
+ task: zod_1.z.string(),
1278
+ deliverables: zod_1.z.array(zod_1.z.object({
1279
+ type: zod_1.z.enum(['file', 'test', 'docs', 'design', 'research']),
1280
+ path: zod_1.z.string(),
1281
+ description: zod_1.z.string().optional(),
1282
+ })).optional(),
1283
+ validation_commands: zod_1.z.array(zod_1.z.string()).optional(),
1284
+ constraints: zod_1.z.array(zod_1.z.string()).optional(),
1285
+ dependsOn: zod_1.z.array(zod_1.z.string()).optional(),
1286
+ })).optional().describe('graph only: array of contract graph task specs'),
1287
+ activate: zod_1.z.boolean().optional().describe('graph only: when true, attached contracts start in ready instead of draft'),
1288
+ }
1289
+ }, async ({ action, file, task, parentId, ready: attachReady, deliverables, validation_commands, constraints, tasks, activate }) => {
1290
+ const filePath = file || defaultFile;
1291
+ // ── attach ─────────────────────────────────────────────────────────────
1292
+ if (action === 'attach') {
1293
+ if (!task) {
1294
+ return { content: [{ type: 'text', text: 'Error: task is required for action=attach' }], isError: true };
1295
+ }
1296
+ if ((0, v2_detect_1.isV2)(filePath)) {
1297
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1298
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1299
+ if (!found) {
1300
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1301
+ }
1302
+ try {
1303
+ const contract = (0, contractSpec_1.buildContract)({
1304
+ deliverableSpecs: deliverables,
1305
+ validationCommands: validation_commands,
1306
+ constraints,
1307
+ status: attachReady ? 'ready' : 'draft',
1308
+ });
1309
+ found.doc.task.contract = contract;
1310
+ found.doc.task.updatedAt = new Date().toISOString();
1311
+ (0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
1312
+ return { content: [{ type: 'text', text: `Contract attached (${contract.status}): ${task}` }] };
1313
+ }
1314
+ catch (e) {
1315
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
1316
+ }
1317
+ }
1318
+ const readResult = readBoard(filePath);
1319
+ if ('error' in readResult) {
1320
+ return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
1321
+ }
1322
+ const taskInfo = (0, core_1.findTaskById)(readResult.board, task);
1323
+ if (!taskInfo) {
1324
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1325
+ }
1326
+ try {
1327
+ const contract = (0, contractSpec_1.buildContract)({
1328
+ deliverableSpecs: deliverables,
1329
+ validationCommands: validation_commands,
1330
+ constraints,
1331
+ status: attachReady ? 'ready' : 'draft',
1332
+ });
1333
+ const contractResult = (0, core_1.setTaskContract)(readResult.board, task, contract);
1334
+ if (!contractResult.success || !contractResult.board) {
1335
+ return { content: [{ type: 'text', text: `Error: ${contractResult.error || 'Failed to attach contract'}` }], isError: true };
1336
+ }
1337
+ writeBoard(filePath, contractResult.board);
1338
+ return { content: [{ type: 'text', text: `Contract attached (${contract.status}): ${task}` }] };
1339
+ }
1340
+ catch (e) {
1341
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
1342
+ }
1343
+ }
1344
+ // ── pickup ─────────────────────────────────────────────────────────────
1345
+ if (action === 'pickup') {
1346
+ if (!task) {
1347
+ return { content: [{ type: 'text', text: 'Error: task is required for action=pickup' }], isError: true };
1348
+ }
1349
+ const result = (0, contractRunner_1.pickupContract)({ filePath, taskId: task });
1350
+ if ('error' in result) {
1351
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1352
+ }
1353
+ return { content: [{ type: 'text', text: result.markdown }] };
1354
+ }
1355
+ // ── deliver ────────────────────────────────────────────────────────────
1356
+ if (action === 'deliver') {
1357
+ if (!task) {
1358
+ return { content: [{ type: 'text', text: 'Error: task is required for action=deliver' }], isError: true };
1359
+ }
1360
+ const result = (0, contractRunner_1.deliverContract)({ filePath, taskId: task });
1361
+ if ('error' in result) {
1362
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1363
+ }
1364
+ return { content: [{ type: 'text', text: `Contract delivered: ${task}` }] };
1041
1365
  }
1042
- // Determine effective destination
1043
- const brainfileDestination = board.archive?.destination;
1044
- const effectiveDestination = destination || (0, config_1.getEffectiveArchiveDestination)(brainfileDestination);
1045
- // Handle local archive (to separate brainfile-archive.md file)
1046
- if (effectiveDestination === 'local') {
1047
- const archiveResult = (0, archive_1.archiveTaskToFile)(filePath, board, taskInfo.column.id, task);
1048
- if (!archiveResult.success) {
1049
- return { content: [{ type: 'text', text: `Error: ${archiveResult.error}` }], isError: true };
1050
- }
1051
- const archivePath = (0, archive_1.getArchivePath)(filePath);
1366
+ // ── validate ───────────────────────────────────────────────────────────
1367
+ if (action === 'validate') {
1368
+ if (!task) {
1369
+ return { content: [{ type: 'text', text: 'Error: task is required for action=validate' }], isError: true };
1370
+ }
1371
+ const result = (0, contractRunner_1.validateContract)({ filePath, taskId: task });
1372
+ if ('error' in result) {
1373
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1374
+ }
1375
+ const output = {
1376
+ ok: result.ok,
1377
+ status: result.ok ? 'done' : 'failed',
1378
+ deliverables: result.deliverableChecks,
1379
+ commands: result.commandResults,
1380
+ warnings: result.warnings,
1381
+ };
1052
1382
  return {
1053
- content: [{ type: 'text', text: `Task ${task} archived to ${path.basename(archivePath)}` }]
1383
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
1384
+ isError: !result.ok,
1054
1385
  };
1055
1386
  }
1056
- // Handle GitHub archive
1057
- if (effectiveDestination === 'github') {
1058
- // Check authentication
1059
- if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
1387
+ // ── graph ──────────────────────────────────────────────────────────────
1388
+ if (action === 'graph') {
1389
+ if (!tasks || tasks.length === 0) {
1390
+ return { content: [{ type: 'text', text: 'Error: tasks is required for action=graph and must be a non-empty array' }], isError: true };
1391
+ }
1392
+ try {
1393
+ const result = (0, contract_1.executeContractGraphMcpAction)({
1394
+ file: filePath,
1395
+ tasks,
1396
+ activate,
1397
+ });
1060
1398
  return {
1061
- content: [{
1062
- type: 'text',
1063
- text: `Error: Not authenticated with GitHub.\n\nTo authenticate, run:\n npx @brainfile/cli auth github\n\nOr fall back to local archive:\n Use destination: "local"`
1064
- }],
1065
- isError: true
1399
+ content: [{ type: 'text', text: JSON.stringify({
1400
+ attached: result.attached,
1401
+ count: result.count,
1402
+ order: result.order,
1403
+ graph: result.graph,
1404
+ }, null, 2) }],
1066
1405
  };
1067
1406
  }
1068
- // Check configuration
1069
- const config = (0, config_1.getArchiveConfig)();
1070
- if (!config.github?.owner || !config.github?.repo) {
1407
+ catch (error) {
1071
1408
  return {
1072
- content: [{
1073
- type: 'text',
1074
- text: `Error: GitHub repository not configured.\n\nTo configure, run:\n npx @brainfile/cli config set archive.github.owner <owner>\n npx @brainfile/cli config set archive.github.repo <repo>\n\nOr fall back to local archive:\n Use destination: "local"`
1075
- }],
1076
- isError: true
1409
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
1410
+ isError: true,
1077
1411
  };
1078
1412
  }
1079
- // Format and create GitHub issue
1080
- const payload = (0, core_2.formatTaskForGitHub)(taskInfo.task, {
1081
- includeMeta: true,
1082
- includeSubtasks: true,
1083
- includeRelatedFiles: true,
1084
- boardTitle: board.title,
1085
- fromColumn: taskInfo.column.title,
1086
- extraLabels: config.github.labels,
1087
- });
1088
- const ghResult = await (0, github_auth_1.createGitHubIssue)({
1089
- owner: config.github.owner,
1090
- repo: config.github.repo,
1091
- title: payload.title,
1092
- body: payload.body,
1093
- labels: payload.labels,
1094
- state: 'closed',
1095
- });
1096
- if (!ghResult.success) {
1097
- return {
1098
- content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }],
1099
- isError: true
1413
+ }
1414
+ // ── activate ───────────────────────────────────────────────────────────
1415
+ if (action === 'activate') {
1416
+ if (!task && !parentId) {
1417
+ return { content: [{ type: 'text', text: 'Error: task or parentId is required for action=activate' }], isError: true };
1418
+ }
1419
+ const activated = [];
1420
+ if ((0, v2_detect_1.isV2)(filePath)) {
1421
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1422
+ if (task) {
1423
+ const found = (0, v2_detect_1.findV2Task)(dirs, task, false);
1424
+ if (!found || found.isLog) {
1425
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1426
+ }
1427
+ if (!found.doc.task.contract) {
1428
+ return { content: [{ type: 'text', text: `Error: Task ${task} has no contract` }], isError: true };
1429
+ }
1430
+ if (found.doc.task.contract.status !== 'draft') {
1431
+ return { content: [{ type: 'text', text: `Error: Contract is not in draft status (current: ${found.doc.task.contract.status})` }], isError: true };
1432
+ }
1433
+ const readyAt = new Date().toISOString();
1434
+ found.doc.task.contract = {
1435
+ ...found.doc.task.contract,
1436
+ status: 'ready',
1437
+ metrics: {
1438
+ ...(found.doc.task.contract.metrics ?? {}),
1439
+ readyAt,
1440
+ },
1441
+ };
1442
+ found.doc.task.updatedAt = readyAt;
1443
+ (0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
1444
+ activated.push(task);
1445
+ }
1446
+ else {
1447
+ // Bulk by parentId
1448
+ const allTasks = (0, core_3.readTasksDir)(dirs.boardDir);
1449
+ for (const doc of allTasks) {
1450
+ const t = doc.task;
1451
+ if (t.parentId !== parentId)
1452
+ continue;
1453
+ if (!t.contract || t.contract.status !== 'draft')
1454
+ continue;
1455
+ const readyAt = new Date().toISOString();
1456
+ t.contract = {
1457
+ ...t.contract,
1458
+ status: 'ready',
1459
+ metrics: {
1460
+ ...(t.contract.metrics ?? {}),
1461
+ readyAt,
1462
+ },
1463
+ };
1464
+ t.updatedAt = readyAt;
1465
+ (0, core_3.writeTaskFile)(path.join(dirs.boardDir, (0, core_3.taskFileName)(t.id)), t, doc.body);
1466
+ activated.push(t.id);
1467
+ }
1468
+ }
1469
+ const output = { activated, count: activated.length };
1470
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
1471
+ }
1472
+ // V1
1473
+ const readResult = readBoard(filePath);
1474
+ if ('error' in readResult) {
1475
+ return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
1476
+ }
1477
+ let board = readResult.board;
1478
+ if (task) {
1479
+ const taskInfo = (0, core_1.findTaskById)(board, task);
1480
+ if (!taskInfo) {
1481
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1482
+ }
1483
+ if (!taskInfo.task.contract) {
1484
+ return { content: [{ type: 'text', text: `Error: Task ${task} has no contract` }], isError: true };
1485
+ }
1486
+ if (taskInfo.task.contract.status !== 'draft') {
1487
+ return { content: [{ type: 'text', text: `Error: Contract is not in draft status (current: ${taskInfo.task.contract.status})` }], isError: true };
1488
+ }
1489
+ const readyAt = new Date().toISOString();
1490
+ const updatedContract = {
1491
+ ...taskInfo.task.contract,
1492
+ status: 'ready',
1493
+ metrics: {
1494
+ ...(taskInfo.task.contract.metrics ?? {}),
1495
+ readyAt,
1496
+ },
1100
1497
  };
1498
+ const contractResult = (0, core_1.setTaskContract)(board, task, updatedContract);
1499
+ if (!contractResult.success || !contractResult.board) {
1500
+ return { content: [{ type: 'text', text: `Error: ${contractResult.error || 'Failed to activate contract'}` }], isError: true };
1501
+ }
1502
+ board = contractResult.board;
1503
+ activated.push(task);
1101
1504
  }
1102
- // Remove task from board
1103
- const deleteResult = (0, core_1.deleteTask)(board, taskInfo.column.id, task);
1104
- if (deleteResult.success) {
1105
- writeBoard(filePath, deleteResult.board);
1505
+ else {
1506
+ // Bulk by parentId
1507
+ for (const col of board.columns) {
1508
+ for (const t of col.tasks) {
1509
+ const taskAny = t;
1510
+ if (taskAny.parentId !== parentId)
1511
+ continue;
1512
+ if (!t.contract || t.contract.status !== 'draft')
1513
+ continue;
1514
+ const readyAt = new Date().toISOString();
1515
+ const updatedContract = {
1516
+ ...t.contract,
1517
+ status: 'ready',
1518
+ metrics: {
1519
+ ...(t.contract.metrics ?? {}),
1520
+ readyAt,
1521
+ },
1522
+ };
1523
+ const contractResult = (0, core_1.setTaskContract)(board, t.id, updatedContract);
1524
+ if (contractResult.success && contractResult.board) {
1525
+ board = contractResult.board;
1526
+ activated.push(t.id);
1527
+ }
1528
+ }
1529
+ }
1106
1530
  }
1107
- return {
1108
- content: [{
1109
- type: 'text',
1110
- text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}`
1111
- }]
1112
- };
1531
+ writeBoard(filePath, board);
1532
+ const output = { activated, count: activated.length };
1533
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
1113
1534
  }
1114
- // Handle Linear archive
1115
- if (effectiveDestination === 'linear') {
1116
- // Check authentication
1117
- if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
1535
+ return { content: [{ type: 'text', text: `Error: Unknown action: ${action}` }], isError: true };
1536
+ });
1537
+ // Task complete tool (also supports archive destinations)
1538
+ server.registerTool('task_complete', {
1539
+ title: 'Complete Task',
1540
+ description: 'Complete a task or archive it to local/GitHub/Linear destination',
1541
+ inputSchema: {
1542
+ file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1543
+ task: zod_1.z.string().describe('Task ID to complete'),
1544
+ destination: zod_1.z.enum(['local', 'github', 'linear']).optional().describe('Optional archive destination. If omitted, performs normal completion flow.'),
1545
+ }
1546
+ }, async ({ file, task, destination }) => {
1547
+ const filePath = file || defaultFile;
1548
+ try {
1549
+ // Default behavior: normal complete flow (v2 -> logs, v1 -> done column)
1550
+ if (!destination) {
1551
+ const { completeCommand } = await Promise.resolve().then(() => __importStar(require('./complete')));
1552
+ const result = completeCommand({ file: filePath, task }, { log: () => { }, warn: () => { }, error: () => { }, info: () => { } });
1553
+ return {
1554
+ content: [{ type: 'text', text: `Task ${task} completed at ${result.completedAt}` }]
1555
+ };
1556
+ }
1557
+ // Local archive keeps legacy archive_task behavior for v1
1558
+ if (destination === 'local') {
1559
+ if ((0, v2_detect_1.isV2)(filePath)) {
1560
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1561
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1562
+ if (!found) {
1563
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1564
+ }
1565
+ const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
1566
+ found.doc.task.completedAt = found.doc.task.completedAt || new Date().toISOString();
1567
+ delete found.doc.task.column;
1568
+ delete found.doc.task.position;
1569
+ (0, core_3.writeTaskFile)(logPath, found.doc.task, found.doc.body);
1570
+ fs.unlinkSync(found.filePath);
1571
+ return { content: [{ type: 'text', text: `Task ${task} archived to logs/` }] };
1572
+ }
1573
+ const readResult = readBoard(filePath);
1574
+ if ('error' in readResult) {
1575
+ return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
1576
+ }
1577
+ const taskInfo = (0, core_1.findTaskById)(readResult.board, task);
1578
+ if (!taskInfo) {
1579
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1580
+ }
1581
+ const archiveResult = (0, archive_1.archiveTaskToFile)(filePath, readResult.board, taskInfo.column.id, task);
1582
+ if (!archiveResult.success) {
1583
+ return { content: [{ type: 'text', text: `Error: ${archiveResult.error}` }], isError: true };
1584
+ }
1118
1585
  return {
1119
- content: [{
1120
- type: 'text',
1121
- text: `Error: Not authenticated with Linear.\n\nTo authenticate, run:\n npx @brainfile/cli auth linear --token <api-key>\n\nGet your API key from: https://linear.app/settings/api\n\nOr fall back to local archive:\n Use destination: "local"`
1122
- }],
1123
- isError: true
1586
+ content: [{ type: 'text', text: `Task ${task} archived to ${path.basename((0, archive_1.getArchivePath)(filePath))}` }]
1124
1587
  };
1125
1588
  }
1126
- // Check/get team configuration
1589
+ // External archive destinations: legacy archive behavior
1590
+ if ((0, v2_detect_1.isV2)(filePath)) {
1591
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1592
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1593
+ if (!found) {
1594
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1595
+ }
1596
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1597
+ if (destination === 'github') {
1598
+ if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
1599
+ return { content: [{ type: 'text', text: 'Error: Not authenticated with GitHub.' }], isError: true };
1600
+ }
1601
+ const config = (0, config_1.getArchiveConfig)();
1602
+ if (!config.github?.owner || !config.github?.repo) {
1603
+ return { content: [{ type: 'text', text: 'Error: GitHub repository not configured.' }], isError: true };
1604
+ }
1605
+ const payload = (0, core_2.formatTaskForGitHub)(found.doc.task, {
1606
+ includeMeta: true,
1607
+ includeSubtasks: true,
1608
+ includeRelatedFiles: true,
1609
+ boardTitle: board.title,
1610
+ fromColumn: found.doc.task.column || 'unknown',
1611
+ extraLabels: config.github.labels,
1612
+ });
1613
+ const ghResult = await (0, github_auth_1.createGitHubIssue)({
1614
+ owner: config.github.owner,
1615
+ repo: config.github.repo,
1616
+ title: payload.title,
1617
+ body: payload.body,
1618
+ labels: payload.labels,
1619
+ state: 'closed',
1620
+ });
1621
+ if (!ghResult.success) {
1622
+ return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
1623
+ }
1624
+ fs.unlinkSync(found.filePath);
1625
+ return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
1626
+ }
1627
+ if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
1628
+ return { content: [{ type: 'text', text: 'Error: Not authenticated with Linear.' }], isError: true };
1629
+ }
1630
+ const config = (0, config_1.getArchiveConfig)();
1631
+ let teamId = config.linear?.teamId;
1632
+ if (!teamId) {
1633
+ const teams = await (0, linear_auth_1.getLinearTeams)();
1634
+ if (teams.length === 0) {
1635
+ return { content: [{ type: 'text', text: 'Error: No Linear teams found.' }], isError: true };
1636
+ }
1637
+ if (teams.length === 1) {
1638
+ teamId = teams[0].id;
1639
+ }
1640
+ else {
1641
+ const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
1642
+ return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
1643
+ }
1644
+ }
1645
+ const payload = (0, core_2.formatTaskForLinear)(found.doc.task, {
1646
+ includeMeta: true,
1647
+ includeSubtasks: true,
1648
+ includeRelatedFiles: true,
1649
+ boardTitle: board.title,
1650
+ fromColumn: found.doc.task.column || 'unknown',
1651
+ stateName: 'Done',
1652
+ });
1653
+ const linearResult = await (0, linear_auth_1.createLinearIssue)({
1654
+ teamId,
1655
+ title: payload.title,
1656
+ description: payload.description,
1657
+ priority: payload.priority,
1658
+ labelNames: payload.labelNames,
1659
+ stateName: 'Done',
1660
+ });
1661
+ if (!linearResult.success) {
1662
+ return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
1663
+ }
1664
+ fs.unlinkSync(found.filePath);
1665
+ return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
1666
+ }
1667
+ const readResult = readBoard(filePath);
1668
+ if ('error' in readResult) {
1669
+ return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
1670
+ }
1671
+ const { board } = readResult;
1672
+ const taskInfo = (0, core_1.findTaskById)(board, task);
1673
+ if (!taskInfo) {
1674
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1675
+ }
1676
+ if (destination === 'github') {
1677
+ if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
1678
+ return { content: [{ type: 'text', text: 'Error: Not authenticated with GitHub.' }], isError: true };
1679
+ }
1680
+ const config = (0, config_1.getArchiveConfig)();
1681
+ if (!config.github?.owner || !config.github?.repo) {
1682
+ return { content: [{ type: 'text', text: 'Error: GitHub repository not configured.' }], isError: true };
1683
+ }
1684
+ const payload = (0, core_2.formatTaskForGitHub)(taskInfo.task, {
1685
+ includeMeta: true,
1686
+ includeSubtasks: true,
1687
+ includeRelatedFiles: true,
1688
+ boardTitle: board.title,
1689
+ fromColumn: taskInfo.column.title,
1690
+ extraLabels: config.github.labels,
1691
+ });
1692
+ const ghResult = await (0, github_auth_1.createGitHubIssue)({
1693
+ owner: config.github.owner,
1694
+ repo: config.github.repo,
1695
+ title: payload.title,
1696
+ body: payload.body,
1697
+ labels: payload.labels,
1698
+ state: 'closed',
1699
+ });
1700
+ if (!ghResult.success) {
1701
+ return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
1702
+ }
1703
+ const deleteResult = (0, core_1.deleteTask)(board, taskInfo.column.id, task);
1704
+ if (deleteResult.success)
1705
+ writeBoard(filePath, deleteResult.board);
1706
+ return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
1707
+ }
1708
+ if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
1709
+ return { content: [{ type: 'text', text: 'Error: Not authenticated with Linear.' }], isError: true };
1710
+ }
1127
1711
  const config = (0, config_1.getArchiveConfig)();
1128
1712
  let teamId = config.linear?.teamId;
1129
1713
  if (!teamId) {
1130
1714
  const teams = await (0, linear_auth_1.getLinearTeams)();
1131
1715
  if (teams.length === 0) {
1132
- return {
1133
- content: [{
1134
- type: 'text',
1135
- text: `Error: No Linear teams found.\n\nVerify your authentication:\n npx @brainfile/cli auth status`
1136
- }],
1137
- isError: true
1138
- };
1716
+ return { content: [{ type: 'text', text: 'Error: No Linear teams found.' }], isError: true };
1139
1717
  }
1140
1718
  if (teams.length === 1) {
1141
1719
  teamId = teams[0].id;
1142
1720
  }
1143
1721
  else {
1144
1722
  const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
1145
- return {
1146
- content: [{
1147
- type: 'text',
1148
- text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}\n\nTo configure, run:\n npx @brainfile/cli config set archive.linear.teamId <team-id>\n\nOr fall back to local archive:\n Use destination: "local"`
1149
- }],
1150
- isError: true
1151
- };
1723
+ return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
1152
1724
  }
1153
1725
  }
1154
- // Format and create Linear issue
1155
1726
  const payload = (0, core_2.formatTaskForLinear)(taskInfo.task, {
1156
1727
  includeMeta: true,
1157
1728
  includeSubtasks: true,
@@ -1169,1314 +1740,17 @@ async function mcpCommand(options) {
1169
1740
  stateName: 'Done',
1170
1741
  });
1171
1742
  if (!linearResult.success) {
1172
- return {
1173
- content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }],
1174
- isError: true
1175
- };
1743
+ return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
1176
1744
  }
1177
- // Remove task from board
1178
1745
  const deleteResult = (0, core_1.deleteTask)(board, taskInfo.column.id, task);
1179
- if (deleteResult.success) {
1746
+ if (deleteResult.success)
1180
1747
  writeBoard(filePath, deleteResult.board);
1181
- }
1182
- return {
1183
- content: [{
1184
- type: 'text',
1185
- text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}`
1186
- }]
1187
- };
1188
- }
1189
- return {
1190
- content: [{ type: 'text', text: `Error: Unknown destination: ${effectiveDestination}` }],
1191
- isError: true
1192
- };
1193
- });
1194
- // Restore task tool
1195
- server.registerTool('restore_task', {
1196
- title: 'Restore Task',
1197
- description: 'Restore a task from the archive to a column',
1198
- inputSchema: {
1199
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1200
- task: zod_1.z.string().describe('Task ID to restore'),
1201
- column: zod_1.z.string().describe('Target column ID or name')
1202
- }
1203
- }, async ({ file, task, column }) => {
1204
- const filePath = file || defaultFile;
1205
- // V2: restore means move from logs/ to board/
1206
- if ((0, v2_detect_1.isV2)(filePath)) {
1207
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1208
- const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1209
- let targetColumn = board.columns.find(c => c.id === column);
1210
- if (!targetColumn)
1211
- targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
1212
- if (!targetColumn) {
1213
- return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
1214
- }
1215
- // Look in logs
1216
- const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
1217
- const doc = (0, core_3.readTaskFile)(logPath);
1218
- if (!doc) {
1219
- return { content: [{ type: 'text', text: `Error: Task not found in logs: ${task}` }], isError: true };
1220
- }
1221
- // Move to board/ with column info
1222
- const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === targetColumn.id);
1223
- doc.task.column = targetColumn.id;
1224
- doc.task.position = targetTasks.length;
1225
- delete doc.task.completedAt;
1226
- doc.task.updatedAt = new Date().toISOString();
1227
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
1228
- (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
1229
- fs.unlinkSync(logPath);
1230
- return { content: [{ type: 'text', text: `Task ${task} restored to "${targetColumn.title}"` }] };
1231
- }
1232
- // V1: use board
1233
- const result = readBoard(filePath);
1234
- if ('error' in result) {
1235
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1236
- }
1237
- let { board } = result;
1238
- // Find target column
1239
- let targetColumn = (0, core_1.findColumnById)(board, column);
1240
- if (!targetColumn) {
1241
- targetColumn = (0, core_1.findColumnByName)(board, column);
1242
- }
1243
- if (!targetColumn) {
1244
- return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
1245
- }
1246
- // Restore from separate archive file
1247
- const restoreResult = (0, archive_1.restoreFromArchive)(filePath, task, targetColumn.id);
1248
- if (!restoreResult.success) {
1249
- // Provide helpful error if archive is empty
1250
- const { tasks } = (0, archive_1.loadArchivedTasks)(filePath);
1251
- if (tasks.length === 0) {
1252
- return {
1253
- content: [{ type: 'text', text: `Error: Archive is empty (${path.basename((0, archive_1.getArchivePath)(filePath))})` }],
1254
- isError: true
1255
- };
1256
- }
1257
- return { content: [{ type: 'text', text: `Error: ${restoreResult.error}` }], isError: true };
1258
- }
1259
- return {
1260
- content: [{ type: 'text', text: `Task ${task} restored to "${targetColumn.title}"` }]
1261
- };
1262
- });
1263
- // Add subtask tool
1264
- server.registerTool('add_subtask', {
1265
- title: 'Add Subtask',
1266
- description: 'Add a subtask to a task',
1267
- inputSchema: {
1268
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1269
- task: zod_1.z.string().describe('Parent task ID'),
1270
- title: zod_1.z.string().describe('Subtask title')
1271
- }
1272
- }, async ({ file, task, title }) => {
1273
- const filePath = file || defaultFile;
1274
- // V2: update task file directly
1275
- if ((0, v2_detect_1.isV2)(filePath)) {
1276
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1277
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
1278
- if (!found) {
1279
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1280
- }
1281
- const t = found.doc.task;
1282
- if (!t.subtasks)
1283
- t.subtasks = [];
1284
- const nextId = t.subtasks.length > 0
1285
- ? `${task}-${Math.max(...t.subtasks.map(s => parseInt(s.id.split('-').pop() || '0', 10))) + 1}`
1286
- : `${task}-1`;
1287
- const newSubtask = { id: nextId, title, completed: false };
1288
- t.subtasks.push(newSubtask);
1289
- t.updatedAt = new Date().toISOString();
1290
- (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1291
- return { content: [{ type: 'text', text: `Subtask added: ${newSubtask.id} - ${newSubtask.title}` }] };
1292
- }
1293
- // V1: use board
1294
- const result = readBoard(filePath);
1295
- if ('error' in result) {
1296
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1297
- }
1298
- let { board } = result;
1299
- const addResult = (0, core_1.addSubtask)(board, task, title);
1300
- if (!addResult.success) {
1301
- return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
1302
- }
1303
- writeBoard(filePath, addResult.board);
1304
- // Get the new subtask ID
1305
- const updatedTask = (0, core_1.findTaskById)(addResult.board, task).task;
1306
- const newSubtask = updatedTask.subtasks.slice(-1)[0];
1307
- return {
1308
- content: [{ type: 'text', text: `Subtask added: ${newSubtask.id} - ${newSubtask.title}` }]
1309
- };
1310
- });
1311
- // Delete subtask tool
1312
- server.registerTool('delete_subtask', {
1313
- title: 'Delete Subtask',
1314
- description: 'Delete a subtask from a task',
1315
- inputSchema: {
1316
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1317
- task: zod_1.z.string().describe('Parent task ID'),
1318
- subtask: zod_1.z.string().describe('Subtask ID to delete')
1319
- }
1320
- }, async ({ file, task, subtask }) => {
1321
- const filePath = file || defaultFile;
1322
- // V2: update task file directly
1323
- if ((0, v2_detect_1.isV2)(filePath)) {
1324
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1325
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
1326
- if (!found) {
1327
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1328
- }
1329
- const t = found.doc.task;
1330
- if (!t.subtasks || !t.subtasks.some(s => s.id === subtask)) {
1331
- return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
1332
- }
1333
- t.subtasks = t.subtasks.filter(s => s.id !== subtask);
1334
- t.updatedAt = new Date().toISOString();
1335
- (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1336
- return { content: [{ type: 'text', text: `Subtask ${subtask} deleted successfully` }] };
1337
- }
1338
- // V1: use board
1339
- const result = readBoard(filePath);
1340
- if ('error' in result) {
1341
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1342
- }
1343
- let { board } = result;
1344
- const deleteResult = (0, core_1.deleteSubtask)(board, task, subtask);
1345
- if (!deleteResult.success) {
1346
- return { content: [{ type: 'text', text: `Error: ${deleteResult.error}` }], isError: true };
1347
- }
1348
- writeBoard(filePath, deleteResult.board);
1349
- return {
1350
- content: [{ type: 'text', text: `Subtask ${subtask} deleted successfully` }]
1351
- };
1352
- });
1353
- // Toggle subtask tool
1354
- server.registerTool('toggle_subtask', {
1355
- title: 'Toggle Subtask',
1356
- description: 'Toggle a subtask completion status',
1357
- inputSchema: {
1358
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1359
- task: zod_1.z.string().describe('Parent task ID'),
1360
- subtask: zod_1.z.string().describe('Subtask ID to toggle')
1361
- }
1362
- }, async ({ file, task, subtask }) => {
1363
- const filePath = file || defaultFile;
1364
- // V2: update task file directly
1365
- if ((0, v2_detect_1.isV2)(filePath)) {
1366
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1367
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
1368
- if (!found) {
1369
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1370
- }
1371
- const t = found.doc.task;
1372
- const st = t.subtasks?.find(s => s.id === subtask);
1373
- if (!st) {
1374
- return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
1375
- }
1376
- const wasCompleted = st.completed;
1377
- st.completed = !st.completed;
1378
- t.updatedAt = new Date().toISOString();
1379
- (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1380
- const newStatus = wasCompleted ? 'incomplete' : 'completed';
1381
- return { content: [{ type: 'text', text: `Subtask ${subtask} marked as ${newStatus}` }] };
1382
- }
1383
- // V1: use board
1384
- const result = readBoard(filePath);
1385
- if ('error' in result) {
1386
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1387
- }
1388
- let { board } = result;
1389
- // Get current status
1390
- const taskInfo = (0, core_1.findTaskById)(board, task);
1391
- const currentSubtask = taskInfo?.task.subtasks?.find(st => st.id === subtask);
1392
- const wasCompleted = currentSubtask?.completed || false;
1393
- const toggleResult = (0, core_1.toggleSubtask)(board, task, subtask);
1394
- if (!toggleResult.success) {
1395
- return { content: [{ type: 'text', text: `Error: ${toggleResult.error}` }], isError: true };
1396
- }
1397
- writeBoard(filePath, toggleResult.board);
1398
- const newStatus = wasCompleted ? 'incomplete' : 'completed';
1399
- return {
1400
- content: [{ type: 'text', text: `Subtask ${subtask} marked as ${newStatus}` }]
1401
- };
1402
- });
1403
- // Update subtask tool
1404
- server.registerTool('update_subtask', {
1405
- title: 'Update Subtask',
1406
- description: 'Update a subtask title',
1407
- inputSchema: {
1408
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1409
- task: zod_1.z.string().describe('Parent task ID'),
1410
- subtask: zod_1.z.string().describe('Subtask ID to update'),
1411
- title: zod_1.z.string().describe('New subtask title')
1412
- }
1413
- }, async ({ file, task, subtask, title }) => {
1414
- const filePath = file || defaultFile;
1415
- // V2: update task file directly
1416
- if ((0, v2_detect_1.isV2)(filePath)) {
1417
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1418
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
1419
- if (!found) {
1420
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1421
- }
1422
- const t = found.doc.task;
1423
- const st = t.subtasks?.find(s => s.id === subtask);
1424
- if (!st) {
1425
- return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
1426
- }
1427
- st.title = title;
1428
- t.updatedAt = new Date().toISOString();
1429
- (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1430
- return { content: [{ type: 'text', text: `Subtask ${subtask} updated to "${title}"` }] };
1431
- }
1432
- // V1: use board
1433
- const result = readBoard(filePath);
1434
- if ('error' in result) {
1435
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1436
- }
1437
- let { board } = result;
1438
- const updateResult = (0, core_1.updateSubtask)(board, task, subtask, title);
1439
- if (!updateResult.success) {
1440
- return { content: [{ type: 'text', text: `Error: ${updateResult.error}` }], isError: true };
1441
- }
1442
- writeBoard(filePath, updateResult.board);
1443
- return {
1444
- content: [{ type: 'text', text: `Subtask ${subtask} updated to "${title}"` }]
1445
- };
1446
- });
1447
- // Bulk set subtasks completed tool
1448
- server.registerTool('bulk_set_subtasks', {
1449
- title: 'Bulk Set Subtasks',
1450
- description: 'Set multiple subtasks to completed or incomplete in a single atomic operation',
1451
- inputSchema: {
1452
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1453
- task: zod_1.z.string().describe('Parent task ID'),
1454
- subtasks: zod_1.z.array(zod_1.z.string()).describe('Array of subtask IDs to update'),
1455
- completed: zod_1.z.boolean().describe('Whether to mark as completed (true) or incomplete (false)')
1456
- }
1457
- }, async ({ file, task, subtasks, completed }) => {
1458
- const filePath = file || defaultFile;
1459
- // V2: update task file directly
1460
- if ((0, v2_detect_1.isV2)(filePath)) {
1461
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1462
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
1463
- if (!found) {
1464
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1465
- }
1466
- const t = found.doc.task;
1467
- if (!t.subtasks) {
1468
- return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
1469
- }
1470
- const subtaskSet = new Set(subtasks);
1471
- for (const st of t.subtasks) {
1472
- if (subtaskSet.has(st.id)) {
1473
- st.completed = completed;
1474
- }
1475
- }
1476
- t.updatedAt = new Date().toISOString();
1477
- (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1478
- const status = completed ? 'completed' : 'incomplete';
1479
- return { content: [{ type: 'text', text: `${subtasks.length} subtasks marked as ${status}` }] };
1480
- }
1481
- // V1: use board
1482
- const result = readBoard(filePath);
1483
- if ('error' in result) {
1484
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1485
- }
1486
- let { board } = result;
1487
- const bulkResult = (0, core_1.setSubtasksCompleted)(board, task, subtasks, completed);
1488
- if (!bulkResult.success) {
1489
- return { content: [{ type: 'text', text: `Error: ${bulkResult.error}` }], isError: true };
1490
- }
1491
- writeBoard(filePath, bulkResult.board);
1492
- const status = completed ? 'completed' : 'incomplete';
1493
- return {
1494
- content: [{ type: 'text', text: `${subtasks.length} subtasks marked as ${status}` }]
1495
- };
1496
- });
1497
- // Complete all subtasks tool
1498
- server.registerTool('complete_all_subtasks', {
1499
- title: 'Complete All Subtasks',
1500
- description: 'Mark all subtasks in a task as completed or incomplete',
1501
- inputSchema: {
1502
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1503
- task: zod_1.z.string().describe('Parent task ID'),
1504
- completed: zod_1.z.boolean().optional().default(true).describe('Whether to mark as completed (default: true) or incomplete (false)')
1505
- }
1506
- }, async ({ file, task, completed }) => {
1507
- const filePath = file || defaultFile;
1508
- const markCompleted = completed ?? true;
1509
- // V2: update task file directly
1510
- if ((0, v2_detect_1.isV2)(filePath)) {
1511
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1512
- const found = (0, v2_detect_1.findV2Task)(dirs, task);
1513
- if (!found) {
1514
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1515
- }
1516
- const t = found.doc.task;
1517
- const count = t.subtasks?.length || 0;
1518
- if (t.subtasks) {
1519
- for (const st of t.subtasks) {
1520
- st.completed = markCompleted;
1521
- }
1522
- }
1523
- t.updatedAt = new Date().toISOString();
1524
- (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1525
- const status = markCompleted ? 'completed' : 'incomplete';
1526
- return { content: [{ type: 'text', text: `All ${count} subtasks in ${task} marked as ${status}` }] };
1527
- }
1528
- // V1: use board
1529
- const result = readBoard(filePath);
1530
- if ('error' in result) {
1531
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1532
- }
1533
- let { board } = result;
1534
- const bulkResult = (0, core_1.setAllSubtasksCompleted)(board, task, markCompleted);
1535
- if (!bulkResult.success) {
1536
- return { content: [{ type: 'text', text: `Error: ${bulkResult.error}` }], isError: true };
1537
- }
1538
- writeBoard(filePath, bulkResult.board);
1539
- // Count subtasks for the message
1540
- const taskInfo = (0, core_1.findTaskById)(bulkResult.board, task);
1541
- const count = taskInfo?.task.subtasks?.length || 0;
1542
- const status = markCompleted ? 'completed' : 'incomplete';
1543
- return {
1544
- content: [{ type: 'text', text: `All ${count} subtasks in ${task} marked as ${status}` }]
1545
- };
1546
- });
1547
- // ==========================================================================
1548
- // BULK OPERATIONS
1549
- // ==========================================================================
1550
- // Bulk move tasks tool
1551
- server.registerTool('bulk_move_tasks', {
1552
- title: 'Bulk Move Tasks',
1553
- description: 'Move multiple tasks to a target column in a single operation',
1554
- inputSchema: {
1555
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1556
- tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to move'),
1557
- column: zod_1.z.string().describe('Target column ID or name')
1558
- }
1559
- }, async ({ file, tasks, column }) => {
1560
- const filePath = file || defaultFile;
1561
- // V2: update each task file
1562
- if ((0, v2_detect_1.isV2)(filePath)) {
1563
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1564
- const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1565
- let targetColumn = board.columns.find(c => c.id === column);
1566
- if (!targetColumn)
1567
- targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
1568
- if (!targetColumn) {
1569
- return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
1570
- }
1571
- const results = [];
1572
- let successCount = 0;
1573
- let failureCount = 0;
1574
- for (const taskId of tasks) {
1575
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
1576
- const doc = (0, core_3.readTaskFile)(taskPath);
1577
- if (!doc) {
1578
- results.push({ taskId, success: false, error: 'Task not found' });
1579
- failureCount++;
1580
- continue;
1581
- }
1582
- doc.task.column = targetColumn.id;
1583
- doc.task.updatedAt = new Date().toISOString();
1584
- (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
1585
- results.push({ taskId, success: true });
1586
- successCount++;
1587
- }
1588
- const output = { success: failureCount === 0, successCount, failureCount, results };
1589
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1590
- }
1591
- // V1: use board
1592
- const result = readBoard(filePath);
1593
- if ('error' in result) {
1594
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1595
- }
1596
- let { board } = result;
1597
- // Find target column
1598
- let targetColumn = (0, core_1.findColumnById)(board, column);
1599
- if (!targetColumn) {
1600
- targetColumn = (0, core_1.findColumnByName)(board, column);
1601
- }
1602
- if (!targetColumn) {
1603
- return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
1604
- }
1605
- // Check for incomplete subtasks before move (for warning)
1606
- const tasksWithIncomplete = [];
1607
- for (const taskId of tasks) {
1608
- const taskInfo = (0, core_1.findTaskById)(board, taskId);
1609
- if (taskInfo) {
1610
- const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
1611
- if (warning?.incompleteSubtasks) {
1612
- tasksWithIncomplete.push({
1613
- id: taskId,
1614
- incomplete: warning.incompleteSubtasks.incomplete.length,
1615
- total: warning.incompleteSubtasks.total
1616
- });
1617
- }
1618
- }
1619
- }
1620
- const bulkResult = (0, core_1.moveTasks)(board, tasks, targetColumn.id);
1621
- if (bulkResult.board) {
1622
- writeBoard(filePath, bulkResult.board);
1623
- }
1624
- const output = {
1625
- success: bulkResult.success,
1626
- successCount: bulkResult.successCount,
1627
- failureCount: bulkResult.failureCount,
1628
- results: bulkResult.results
1629
- };
1630
- // Add warning about incomplete subtasks if any
1631
- if (tasksWithIncomplete.length > 0) {
1632
- output.warning = `${tasksWithIncomplete.length} task(s) moved to "${targetColumn.title}" have incomplete subtasks`;
1633
- output.tasksWithIncompleteSubtasks = tasksWithIncomplete;
1634
- }
1635
- return {
1636
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
1637
- isError: !bulkResult.success
1638
- };
1639
- });
1640
- // Bulk patch tasks tool
1641
- server.registerTool('bulk_patch_tasks', {
1642
- title: 'Bulk Patch Tasks',
1643
- description: 'Apply the same patch to multiple tasks in a single operation',
1644
- inputSchema: {
1645
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1646
- tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to patch'),
1647
- priority: zod_1.z.enum(['low', 'medium', 'high', 'critical']).nullable().optional().describe('New priority (null to remove)'),
1648
- tags: zod_1.z.array(zod_1.z.string()).nullable().optional().describe('New tags (null to remove)'),
1649
- assignee: zod_1.z.string().nullable().optional().describe('New assignee (null to remove)')
1650
- }
1651
- }, async ({ file, tasks, priority, tags, assignee }) => {
1652
- const filePath = file || defaultFile;
1653
- const isNull = (v) => v === null || v === 'null';
1654
- // V2: update each task file
1655
- if ((0, v2_detect_1.isV2)(filePath)) {
1656
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1657
- const results = [];
1658
- let successCount = 0;
1659
- let failureCount = 0;
1660
- for (const taskId of tasks) {
1661
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
1662
- const doc = (0, core_3.readTaskFile)(taskPath);
1663
- if (!doc) {
1664
- results.push({ taskId, success: false, error: 'Task not found' });
1665
- failureCount++;
1666
- continue;
1667
- }
1668
- const t = doc.task;
1669
- if (priority !== undefined) {
1670
- if (isNull(priority))
1671
- delete t.priority;
1672
- else
1673
- t.priority = priority;
1674
- }
1675
- if (tags !== undefined) {
1676
- if (isNull(tags))
1677
- delete t.tags;
1678
- else
1679
- t.tags = tags;
1680
- }
1681
- if (assignee !== undefined) {
1682
- if (isNull(assignee))
1683
- delete t.assignee;
1684
- else
1685
- t.assignee = assignee;
1686
- }
1687
- t.updatedAt = new Date().toISOString();
1688
- (0, core_3.writeTaskFile)(taskPath, t, doc.body);
1689
- results.push({ taskId, success: true });
1690
- successCount++;
1691
- }
1692
- const output = { success: failureCount === 0, successCount, failureCount, results };
1693
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1694
- }
1695
- // V1: use board
1696
- const result = readBoard(filePath);
1697
- if ('error' in result) {
1698
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1699
- }
1700
- let { board } = result;
1701
- const patch = {};
1702
- if (priority !== undefined)
1703
- patch.priority = isNull(priority) ? undefined : priority;
1704
- if (tags !== undefined)
1705
- patch.tags = isNull(tags) ? undefined : tags;
1706
- if (assignee !== undefined)
1707
- patch.assignee = isNull(assignee) ? undefined : assignee;
1708
- const bulkResult = (0, core_1.patchTasks)(board, tasks, patch);
1709
- if (bulkResult.board) {
1710
- writeBoard(filePath, bulkResult.board);
1711
- }
1712
- const output = {
1713
- success: bulkResult.success,
1714
- successCount: bulkResult.successCount,
1715
- failureCount: bulkResult.failureCount,
1716
- results: bulkResult.results
1717
- };
1718
- return {
1719
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
1720
- isError: !bulkResult.success
1721
- };
1722
- });
1723
- // Bulk delete tasks tool
1724
- server.registerTool('bulk_delete_tasks', {
1725
- title: 'Bulk Delete Tasks',
1726
- description: 'Permanently delete multiple tasks in a single operation',
1727
- inputSchema: {
1728
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1729
- tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to delete')
1730
- }
1731
- }, async ({ file, tasks }) => {
1732
- const filePath = file || defaultFile;
1733
- // V2: delete task files
1734
- if ((0, v2_detect_1.isV2)(filePath)) {
1735
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1736
- const results = [];
1737
- let successCount = 0;
1738
- let failureCount = 0;
1739
- for (const taskId of tasks) {
1740
- const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
1741
- if (!found) {
1742
- results.push({ taskId, success: false, error: 'Task not found' });
1743
- failureCount++;
1744
- continue;
1745
- }
1746
- try {
1747
- fs.unlinkSync(found.filePath);
1748
- results.push({ taskId, success: true });
1749
- successCount++;
1750
- }
1751
- catch (e) {
1752
- results.push({ taskId, success: false, error: e.message });
1753
- failureCount++;
1754
- }
1755
- }
1756
- const output = { success: failureCount === 0, successCount, failureCount, results };
1757
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1758
- }
1759
- // V1: use board
1760
- const result = readBoard(filePath);
1761
- if ('error' in result) {
1762
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1763
- }
1764
- let { board } = result;
1765
- const bulkResult = (0, core_1.deleteTasks)(board, tasks);
1766
- if (bulkResult.board) {
1767
- writeBoard(filePath, bulkResult.board);
1768
- }
1769
- const output = {
1770
- success: bulkResult.success,
1771
- successCount: bulkResult.successCount,
1772
- failureCount: bulkResult.failureCount,
1773
- results: bulkResult.results
1774
- };
1775
- return {
1776
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
1777
- isError: !bulkResult.success
1778
- };
1779
- });
1780
- // Bulk archive tasks tool
1781
- server.registerTool('bulk_archive_tasks', {
1782
- title: 'Bulk Archive Tasks',
1783
- description: 'Archive multiple tasks to the separate archive file',
1784
- inputSchema: {
1785
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1786
- tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to archive')
1787
- }
1788
- }, async ({ file, tasks }) => {
1789
- const filePath = file || defaultFile;
1790
- // V2: move task files from board/ to logs/
1791
- if ((0, v2_detect_1.isV2)(filePath)) {
1792
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1793
- const results = [];
1794
- let successCount = 0;
1795
- let failureCount = 0;
1796
- for (const taskId of tasks) {
1797
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
1798
- const doc = (0, core_3.readTaskFile)(taskPath);
1799
- if (!doc) {
1800
- results.push({ taskId, success: false, error: 'Task not found' });
1801
- failureCount++;
1802
- continue;
1803
- }
1804
- try {
1805
- const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(taskId));
1806
- doc.task.completedAt = doc.task.completedAt || new Date().toISOString();
1807
- delete doc.task.column;
1808
- delete doc.task.position;
1809
- (0, core_3.writeTaskFile)(logPath, doc.task, doc.body);
1810
- fs.unlinkSync(taskPath);
1811
- results.push({ taskId, success: true });
1812
- successCount++;
1813
- }
1814
- catch (e) {
1815
- results.push({ taskId, success: false, error: e.message });
1816
- failureCount++;
1817
- }
1818
- }
1819
- const output = { success: failureCount === 0, successCount, failureCount, results };
1820
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1821
- }
1822
- // V1: use board
1823
- const results = [];
1824
- let successCount = 0;
1825
- let failureCount = 0;
1826
- // Archive each task individually (need to re-read board after each archive)
1827
- for (const taskId of tasks) {
1828
- const boardResult = readBoard(filePath);
1829
- if ('error' in boardResult) {
1830
- results.push({ taskId, success: false, error: boardResult.error });
1831
- failureCount++;
1832
- continue;
1833
- }
1834
- const { board } = boardResult;
1835
- const taskInfo = (0, core_1.findTaskById)(board, taskId);
1836
- if (!taskInfo) {
1837
- results.push({ taskId, success: false, error: 'Task not found' });
1838
- failureCount++;
1839
- continue;
1840
- }
1841
- const archiveResult = (0, archive_1.archiveTaskToFile)(filePath, board, taskInfo.column.id, taskId);
1842
- if (archiveResult.success) {
1843
- results.push({ taskId, success: true });
1844
- successCount++;
1845
- }
1846
- else {
1847
- results.push({ taskId, success: false, error: archiveResult.error });
1848
- failureCount++;
1849
- }
1850
- }
1851
- const output = {
1852
- success: failureCount === 0,
1853
- successCount,
1854
- failureCount,
1855
- results,
1856
- archiveFile: path.basename((0, archive_1.getArchivePath)(filePath))
1857
- };
1858
- return {
1859
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
1860
- isError: failureCount > 0 && successCount === 0
1861
- };
1862
- });
1863
- // ==========================================================================
1864
- // CONTRACTS
1865
- // ==========================================================================
1866
- server.registerTool('contract_pickup', {
1867
- title: 'Contract Pickup',
1868
- description: 'Claim a task contract (sets status to in_progress) and return agent context as markdown',
1869
- inputSchema: {
1870
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1871
- task: zod_1.z.string().describe('Task ID to pick up'),
1872
- }
1873
- }, async ({ file, task }) => {
1874
- const filePath = file || defaultFile;
1875
- const result = (0, contractRunner_1.pickupContract)({ filePath, taskId: task });
1876
- if ('error' in result) {
1877
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1878
- }
1879
- return { content: [{ type: 'text', text: result.markdown }] };
1880
- });
1881
- server.registerTool('contract_deliver', {
1882
- title: 'Contract Deliver',
1883
- description: 'Mark a task contract as delivered (sets status to delivered)',
1884
- inputSchema: {
1885
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1886
- task: zod_1.z.string().describe('Task ID to deliver'),
1887
- }
1888
- }, async ({ file, task }) => {
1889
- const filePath = file || defaultFile;
1890
- const result = (0, contractRunner_1.deliverContract)({ filePath, taskId: task });
1891
- if ('error' in result) {
1892
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1893
- }
1894
- return {
1895
- content: [{
1896
- type: 'text',
1897
- text: `Contract delivered: ${task}\nReminder: prepare summary and filesChanged for the upcoming complete_task call.`
1898
- }]
1899
- };
1900
- });
1901
- server.registerTool('contract_validate', {
1902
- title: 'Contract Validate',
1903
- description: 'Validate contract deliverables + commands; sets status to done/failed',
1904
- inputSchema: {
1905
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1906
- task: zod_1.z.string().describe('Task ID to validate'),
1907
- }
1908
- }, async ({ file, task }) => {
1909
- const filePath = file || defaultFile;
1910
- const result = (0, contractRunner_1.validateContract)({ filePath, taskId: task });
1911
- if ('error' in result) {
1912
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1913
- }
1914
- const output = {
1915
- ok: result.ok,
1916
- status: result.ok ? 'done' : 'failed',
1917
- deliverables: result.deliverableChecks,
1918
- commands: result.commandResults,
1919
- warnings: result.warnings,
1920
- };
1921
- return {
1922
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
1923
- isError: !result.ok
1924
- };
1925
- });
1926
- // ==========================================================================
1927
- // TYPES
1928
- // ==========================================================================
1929
- server.registerTool('list_types', {
1930
- title: 'List Types',
1931
- description: 'List board strict mode and custom type configuration',
1932
- inputSchema: {
1933
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1934
- }
1935
- }, async ({ file }) => {
1936
- const filePath = file || defaultFile;
1937
- if ((0, v2_detect_1.isV2)(filePath)) {
1938
- try {
1939
- const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1940
- const boardConfig = board;
1941
- const output = {
1942
- strict: boardConfig.strict === true,
1943
- types: sanitizeTypesConfig(boardConfig.types),
1944
- };
1945
- return {
1946
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
1947
- };
1948
- }
1949
- catch (e) {
1950
- return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
1951
- }
1952
- }
1953
- const result = readBoard(filePath);
1954
- if ('error' in result) {
1955
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1956
- }
1957
- const boardConfig = result.board;
1958
- const strict = boardConfig.strict === true;
1959
- const types = sanitizeTypesConfig(boardConfig.types);
1960
- return {
1961
- content: [{
1962
- type: 'text',
1963
- text: JSON.stringify({ strict, types }, null, 2)
1964
- }]
1965
- };
1966
- });
1967
- // ==========================================================================
1968
- // RULES
1969
- // ==========================================================================
1970
- // List rules tool
1971
- server.registerTool('list_rules', {
1972
- title: 'List Rules',
1973
- description: 'List all project rules (always, never, prefer, context) from the brainfile',
1974
- inputSchema: {
1975
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1976
- category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).optional().describe('Filter by rule category')
1977
- }
1978
- }, async ({ file, category }) => {
1979
- const filePath = file || defaultFile;
1980
- const result = readBoard(filePath);
1981
- if ('error' in result) {
1982
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1983
- }
1984
- const { board } = result;
1985
- const rules = board.rules || {};
1986
- // Filter by category if specified
1987
- let outputRules = rules;
1988
- if (category) {
1989
- outputRules = { [category]: rules[category] || [] };
1990
- }
1991
- // Count total rules
1992
- const countRules = (r) => (r.always?.length || 0) +
1993
- (r.never?.length || 0) +
1994
- (r.prefer?.length || 0) +
1995
- (r.context?.length || 0);
1996
- const output = {
1997
- rules: outputRules,
1998
- totalCount: category
1999
- ? (outputRules[category]?.length || 0)
2000
- : countRules(rules),
2001
- };
2002
- return {
2003
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
2004
- };
2005
- });
2006
- // Add rule tool
2007
- server.registerTool('add_rule', {
2008
- title: 'Add Rule',
2009
- description: 'Add a new project rule to the brainfile',
2010
- inputSchema: {
2011
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2012
- category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).describe('Rule category'),
2013
- text: zod_1.z.string().describe('Rule text/description')
2014
- }
2015
- }, async ({ file, category, text }) => {
2016
- const filePath = file || defaultFile;
2017
- const result = readBoard(filePath);
2018
- if ('error' in result) {
2019
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
2020
- }
2021
- let { board } = result;
2022
- const addResult = (0, core_1.addRule)(board, category, text);
2023
- if (!addResult.success || !addResult.board) {
2024
- return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
2025
- }
2026
- writeBoard(filePath, addResult.board);
2027
- // Find the newly added rule (last one in the category)
2028
- const newRules = addResult.board.rules?.[category] || [];
2029
- const newRule = newRules[newRules.length - 1];
2030
- const output = {
2031
- success: true,
2032
- category,
2033
- rule: newRule,
2034
- };
2035
- return {
2036
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
2037
- };
2038
- });
2039
- // Delete rule tool
2040
- server.registerTool('delete_rule', {
2041
- title: 'Delete Rule',
2042
- description: 'Delete a project rule from the brainfile by category and ID',
2043
- inputSchema: {
2044
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2045
- category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).describe('Rule category'),
2046
- id: zod_1.z.number().describe('Rule ID to delete')
2047
- }
2048
- }, async ({ file, category, id }) => {
2049
- const filePath = file || defaultFile;
2050
- const result = readBoard(filePath);
2051
- if ('error' in result) {
2052
- return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
2053
- }
2054
- let { board } = result;
2055
- // Find the rule being deleted for the response
2056
- const existingRules = board.rules?.[category] || [];
2057
- const ruleToDelete = existingRules.find((r) => r.id === id);
2058
- if (!ruleToDelete) {
2059
- const availableIds = existingRules.map((r) => r.id).join(', ');
2060
- return {
2061
- content: [{
2062
- type: 'text',
2063
- text: `Error: Rule ${id} not found in ${category}. ${availableIds ? `Available IDs: ${availableIds}` : `No rules in ${category} category`}`
2064
- }],
2065
- isError: true
2066
- };
2067
- }
2068
- const deleteResult = (0, core_1.deleteRule)(board, category, id);
2069
- if (!deleteResult.success || !deleteResult.board) {
2070
- return { content: [{ type: 'text', text: `Error: ${deleteResult.error}` }], isError: true };
2071
- }
2072
- writeBoard(filePath, deleteResult.board);
2073
- const output = {
2074
- success: true,
2075
- category,
2076
- deletedRule: ruleToDelete,
2077
- };
2078
- return {
2079
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
2080
- };
2081
- });
2082
- // ==========================================================================
2083
- // LEDGER
2084
- // ==========================================================================
2085
- server.registerTool('get_task_context', {
2086
- title: 'Get Task Context',
2087
- description: 'Get scoped recent ledger history for a task based on relatedFiles and contract deliverables',
2088
- inputSchema: {
2089
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2090
- taskId: zod_1.z.string().optional().describe('Task ID to scope context for'),
2091
- maxEntries: zod_1.z.number().optional().describe('Maximum matching entries to return (default: 5)'),
2092
- maxAgeDays: zod_1.z.number().optional().describe('Maximum age window in days (default: 90)'),
2093
- task_id: zod_1.z.string().optional().describe('Alias of taskId'),
2094
- max_entries: zod_1.z.number().optional().describe('Alias of maxEntries'),
2095
- max_age_days: zod_1.z.number().optional().describe('Alias of maxAgeDays'),
2096
- }
2097
- }, async ({ file, taskId, maxEntries, maxAgeDays, task_id, max_entries, max_age_days }) => {
2098
- const filePath = file || defaultFile;
2099
- const resolvedTaskId = taskId || task_id;
2100
- if (!resolvedTaskId) {
2101
- return { content: [{ type: 'text', text: 'Error: taskId is required' }], isError: true };
2102
- }
2103
- let relatedFiles = [];
2104
- let deliverables;
2105
- if ((0, v2_detect_1.isV2)(filePath)) {
2106
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
2107
- const found = (0, v2_detect_1.findV2Task)(dirs, resolvedTaskId, true);
2108
- if (!found) {
2109
- return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
2110
- }
2111
- relatedFiles = found.doc.task.relatedFiles || [];
2112
- deliverables = found.doc.task.contract?.deliverables;
2113
- }
2114
- else {
2115
- const boardResult = readBoard(filePath);
2116
- if ('error' in boardResult) {
2117
- return { content: [{ type: 'text', text: `Error: ${boardResult.error}` }], isError: true };
2118
- }
2119
- const taskInfo = (0, core_1.findTaskById)(boardResult.board, resolvedTaskId);
2120
- if (!taskInfo) {
2121
- return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
2122
- }
2123
- relatedFiles = taskInfo.task.relatedFiles || [];
2124
- deliverables = taskInfo.task.contract?.deliverables;
2125
- }
2126
- const limit = normalizePositiveInt(maxEntries ?? max_entries, 5);
2127
- const ageDays = normalizeNonNegativeInt(maxAgeDays ?? max_age_days, 90);
2128
- const since = toIsoDaysAgo(ageDays);
2129
- const logsDir = resolveLedgerLogsDir(filePath);
2130
- const entries = (0, dist_1.getTaskContext)(logsDir, relatedFiles, deliverables, {
2131
- limit,
2132
- dateRange: { from: since },
2133
- });
2134
- const scopeFiles = new Set();
2135
- for (const value of relatedFiles) {
2136
- const trimmed = value.trim();
2137
- if (trimmed) {
2138
- scopeFiles.add(trimmed);
2139
- }
2140
- }
2141
- for (const deliverable of deliverables || []) {
2142
- const rawPath = typeof deliverable === 'string' ? deliverable : deliverable.path;
2143
- const trimmed = rawPath.trim();
2144
- if (trimmed) {
2145
- scopeFiles.add(trimmed);
2146
- }
2147
- }
2148
- const output = {
2149
- taskId: resolvedTaskId,
2150
- count: entries.length,
2151
- maxEntries: limit,
2152
- maxAgeDays: ageDays,
2153
- since,
2154
- scopeFiles: Array.from(scopeFiles),
2155
- entries,
2156
- };
2157
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2158
- });
2159
- server.registerTool('query_ledger', {
2160
- title: 'Query Ledger',
2161
- description: 'Query ledger records by assignee, tags, date range, files, and contract status',
2162
- inputSchema: {
2163
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2164
- assignee: zod_1.z.string().optional().describe('Filter by assignee'),
2165
- tags: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by tags (OR match)'),
2166
- since: zod_1.z.string().optional().describe('Filter completions since this ISO date/time'),
2167
- until: zod_1.z.string().optional().describe('Filter completions until this ISO date/time'),
2168
- files: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by file paths touched'),
2169
- contractStatus: zod_1.z.union([
2170
- zod_1.z.enum(LEDGER_CONTRACT_STATUSES),
2171
- zod_1.z.array(zod_1.z.enum(LEDGER_CONTRACT_STATUSES)),
2172
- ]).optional().describe('Filter by contract status'),
2173
- contract_status: zod_1.z.union([
2174
- zod_1.z.enum(LEDGER_CONTRACT_STATUSES),
2175
- zod_1.z.array(zod_1.z.enum(LEDGER_CONTRACT_STATUSES)),
2176
- ]).optional().describe('Alias of contractStatus'),
2177
- }
2178
- }, async ({ file, assignee, tags, since, until, files, contractStatus, contract_status }) => {
2179
- const filePath = file || defaultFile;
2180
- const logsDir = resolveLedgerLogsDir(filePath);
2181
- const contractFilter = contractStatus ?? contract_status;
2182
- const dateRange = (since || until) ? { from: since, to: until } : undefined;
2183
- const records = (0, dist_1.queryLedger)(logsDir, {
2184
- assignee,
2185
- tags,
2186
- dateRange,
2187
- files,
2188
- contractStatus: contractFilter,
2189
- });
2190
- const output = {
2191
- count: records.length,
2192
- filters: {
2193
- assignee,
2194
- tags,
2195
- since,
2196
- until,
2197
- files,
2198
- contractStatus: contractFilter,
2199
- },
2200
- records,
2201
- };
2202
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2203
- });
2204
- server.registerTool('get_stats', {
2205
- title: 'Get Stats',
2206
- description: 'Get aggregate completion analytics from the ledger with optional filters',
2207
- inputSchema: {
2208
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2209
- assignee: zod_1.z.string().optional().describe('Filter analytics by assignee'),
2210
- since: zod_1.z.string().optional().describe('Only include completions since this ISO date/time'),
2211
- tag: zod_1.z.string().optional().describe('Filter analytics by a single tag'),
2212
- }
2213
- }, async ({ file, assignee, since, tag }) => {
2214
- const filePath = file || defaultFile;
2215
- const logsDir = resolveLedgerLogsDir(filePath);
2216
- const records = (0, dist_1.queryLedger)(logsDir, {
2217
- assignee,
2218
- tags: tag ? [tag] : undefined,
2219
- dateRange: since ? { from: since } : undefined,
2220
- });
2221
- const cycleTimes = records
2222
- .map((record) => record.cycleTimeHours)
2223
- .filter((hours) => typeof hours === 'number' && Number.isFinite(hours) && hours >= 0);
2224
- const cycleTimeTotal = Number(cycleTimes.reduce((sum, value) => sum + value, 0).toFixed(3));
2225
- const cycleTimeAverage = cycleTimes.length > 0
2226
- ? Number((cycleTimeTotal / cycleTimes.length).toFixed(3))
2227
- : 0;
2228
- const cycleTimeMin = cycleTimes.length > 0 ? Number(Math.min(...cycleTimes).toFixed(3)) : 0;
2229
- const cycleTimeMax = cycleTimes.length > 0 ? Number(Math.max(...cycleTimes).toFixed(3)) : 0;
2230
- const byType = {};
2231
- const byContractStatus = {};
2232
- const byAssignee = {};
2233
- const byTag = {};
2234
- const fileCounts = new Map();
2235
- let firstCompletionAt = null;
2236
- let lastCompletionAt = null;
2237
- let firstCompletionMs = null;
2238
- let lastCompletionMs = null;
2239
- for (const record of records) {
2240
- byType[record.type] = (byType[record.type] || 0) + 1;
2241
- if (record.contractStatus) {
2242
- byContractStatus[record.contractStatus] = (byContractStatus[record.contractStatus] || 0) + 1;
2243
- }
2244
- if (record.assignee) {
2245
- byAssignee[record.assignee] = (byAssignee[record.assignee] || 0) + 1;
2246
- }
2247
- for (const recordTag of record.tags || []) {
2248
- byTag[recordTag] = (byTag[recordTag] || 0) + 1;
2249
- }
2250
- for (const changedFile of record.filesChanged || []) {
2251
- fileCounts.set(changedFile, (fileCounts.get(changedFile) || 0) + 1);
2252
- }
2253
- const completedMs = Date.parse(record.completedAt);
2254
- if (Number.isFinite(completedMs)) {
2255
- if (firstCompletionMs === null || completedMs < firstCompletionMs) {
2256
- firstCompletionMs = completedMs;
2257
- firstCompletionAt = record.completedAt;
2258
- }
2259
- if (lastCompletionMs === null || completedMs > lastCompletionMs) {
2260
- lastCompletionMs = completedMs;
2261
- lastCompletionAt = record.completedAt;
2262
- }
2263
- }
2264
- }
2265
- const topFiles = Array.from(fileCounts.entries())
2266
- .sort((a, b) => b[1] - a[1])
2267
- .slice(0, 10)
2268
- .map(([filePath, count]) => ({ filePath, count }));
2269
- const output = {
2270
- filters: { assignee, since, tag },
2271
- totals: {
2272
- completed: records.length,
2273
- uniqueFilesTouched: fileCounts.size,
2274
- },
2275
- cycleTimeHours: {
2276
- total: cycleTimeTotal,
2277
- average: cycleTimeAverage,
2278
- median: median(cycleTimes),
2279
- min: cycleTimeMin,
2280
- max: cycleTimeMax,
2281
- },
2282
- breakdown: {
2283
- byType,
2284
- byContractStatus,
2285
- byAssignee,
2286
- byTag,
2287
- },
2288
- topFiles,
2289
- window: {
2290
- firstCompletionAt,
2291
- lastCompletionAt,
2292
- },
2293
- };
2294
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2295
- });
2296
- server.registerTool('get_file_history', {
2297
- title: 'Get File History',
2298
- description: 'Get recent completed records that touched a specific file path',
2299
- inputSchema: {
2300
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2301
- filePath: zod_1.z.string().optional().describe('File path to query'),
2302
- last: zod_1.z.number().optional().describe('Maximum records to return (default: 10)'),
2303
- since: zod_1.z.string().optional().describe('Only include completions since this ISO date/time'),
2304
- file_path: zod_1.z.string().optional().describe('Alias of filePath'),
2305
- }
2306
- }, async ({ file, filePath: targetFilePath, last, since, file_path }) => {
2307
- const filePath = file || defaultFile;
2308
- const resolvedFilePath = targetFilePath || file_path;
2309
- if (!resolvedFilePath) {
2310
- return { content: [{ type: 'text', text: 'Error: filePath is required' }], isError: true };
2311
- }
2312
- const logsDir = resolveLedgerLogsDir(filePath);
2313
- const limit = normalizePositiveInt(last, 10);
2314
- const records = (0, dist_1.getFileHistory)(logsDir, resolvedFilePath, {
2315
- limit,
2316
- dateRange: since ? { from: since } : undefined,
2317
- });
2318
- const output = {
2319
- filePath: resolvedFilePath,
2320
- count: records.length,
2321
- last: limit,
2322
- since,
2323
- records,
2324
- };
2325
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2326
- });
2327
- // ==========================================================================
2328
- // V2 NEW TOOLS: complete_task, search_logs, append_log
2329
- // ==========================================================================
2330
- // Complete task tool
2331
- server.registerTool('complete_task', {
2332
- title: 'Complete Task',
2333
- description: 'Complete a task - in v2, moves task file from board/ to logs/ with completedAt timestamp. In v1, moves to done column.',
2334
- inputSchema: {
2335
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2336
- task: zod_1.z.string().describe('Task ID to complete'),
2337
- summary: zod_1.z.string().optional().describe('Completion summary (default: Completed: {title})'),
2338
- filesChanged: zod_1.z.array(zod_1.z.string()).optional().describe('Files changed during implementation (optional; inferred from git diff if omitted)'),
2339
- files_changed: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of filesChanged'),
2340
- }
2341
- }, async ({ file, task, summary, filesChanged, files_changed }) => {
2342
- const filePath = file || defaultFile;
2343
- if ((0, v2_detect_1.isV2)(filePath)) {
2344
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
2345
- const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
2346
- const doc = (0, core_3.readTaskFile)(taskPath);
2347
- if (!doc) {
2348
- return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
2349
- }
2350
- const resolvedSummary = summary?.trim() || `Completed: ${doc.task.title}`;
2351
- const rawFiles = filesChanged ?? files_changed ?? [];
2352
- const providedFiles = rawFiles
2353
- .map((entry) => entry.trim())
2354
- .filter((entry) => entry.length > 0);
2355
- const inferredFiles = providedFiles.length > 0 ? providedFiles : inferFilesChangedFromGit(filePath);
2356
- const filesSource = providedFiles.length > 0
2357
- ? 'input'
2358
- : inferredFiles.length > 0
2359
- ? 'git_diff'
2360
- : 'core_default';
2361
- const completeResult = (0, dist_1.completeTaskFile)(taskPath, dirs.logsDir, {
2362
- summary: resolvedSummary,
2363
- filesChanged: inferredFiles.length > 0 ? inferredFiles : undefined,
2364
- });
2365
- if (!completeResult.success || !completeResult.task) {
2366
- return { content: [{ type: 'text', text: `Error: ${completeResult.error || `Failed to complete task: ${task}`}` }], isError: true };
2367
- }
2368
- const output = {
2369
- task,
2370
- completedAt: completeResult.task.completedAt,
2371
- summary: resolvedSummary,
2372
- filesChanged: inferredFiles,
2373
- filesSource,
2374
- };
2375
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2376
- }
2377
- try {
2378
- const { completeCommand } = await Promise.resolve().then(() => __importStar(require('./complete')));
2379
- const result = completeCommand({ file: filePath, task }, { log: () => { }, warn: () => { }, error: () => { }, info: () => { } });
2380
- const hasLedgerOnlyParams = Boolean(summary) || ((filesChanged ?? files_changed)?.length || 0) > 0;
2381
- return {
2382
- content: [{
2383
- type: 'text',
2384
- text: `Task ${task} completed at ${result.completedAt}${hasLedgerOnlyParams ? `\nNote: summary/filesChanged are only applied in v2 ledger mode.` : ''}`
2385
- }]
2386
- };
1748
+ return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
2387
1749
  }
2388
1750
  catch (e) {
2389
1751
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
2390
1752
  }
2391
1753
  });
2392
- // Search logs tool
2393
- server.registerTool('search_logs', {
2394
- title: 'Search Logs',
2395
- description: 'Search across completed task logs. Requires v2 per-task file architecture.',
2396
- inputSchema: {
2397
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2398
- query: zod_1.z.string().optional().describe('Search query to match against log content'),
2399
- recent: zod_1.z.boolean().optional().describe('List recently completed tasks'),
2400
- task: zod_1.z.string().optional().describe('View a specific task log'),
2401
- }
2402
- }, async ({ file, query, recent, task: taskId }) => {
2403
- const filePath = file || defaultFile;
2404
- if (!(0, v2_detect_1.isV2)(filePath)) {
2405
- return { content: [{ type: 'text', text: 'Error: search_logs requires v2 per-task file architecture. Run: brainfile migrate' }], isError: true };
2406
- }
2407
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
2408
- // View specific task log
2409
- if (taskId) {
2410
- const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
2411
- if (!found) {
2412
- return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
2413
- }
2414
- const output = {
2415
- id: found.doc.task.id,
2416
- title: found.doc.task.title,
2417
- completedAt: found.doc.task.completedAt,
2418
- description: (0, v2_detect_1.extractDescription)(found.doc.body),
2419
- log: (0, v2_detect_1.extractLog)(found.doc.body),
2420
- };
2421
- return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2422
- }
2423
- // Search logs
2424
- if (query) {
2425
- const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
2426
- const queryLower = query.toLowerCase();
2427
- const matches = [];
2428
- for (const doc of logDocs) {
2429
- const task = doc.task;
2430
- const desc = (0, v2_detect_1.extractDescription)(doc.body) || '';
2431
- const log = (0, v2_detect_1.extractLog)(doc.body) || '';
2432
- const fullText = [task.title, task.description || '', desc, log].join(' ').toLowerCase();
2433
- if (fullText.includes(queryLower)) {
2434
- matches.push({ id: task.id, title: task.title, completedAt: task.completedAt });
2435
- }
2436
- }
2437
- return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
2438
- }
2439
- // Recent logs (default)
2440
- const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
2441
- logDocs.sort((a, b) => (b.task.completedAt || '').localeCompare(a.task.completedAt || ''));
2442
- const recent20 = logDocs.slice(0, 20).map(doc => ({
2443
- id: doc.task.id,
2444
- title: doc.task.title,
2445
- completedAt: doc.task.completedAt,
2446
- }));
2447
- return { content: [{ type: 'text', text: JSON.stringify({ logs: recent20, count: recent20.length }, null, 2) }] };
2448
- });
2449
- // Append log tool
2450
- server.registerTool('append_log', {
2451
- title: 'Append Log',
2452
- description: 'Append a timestamped entry to a task log section. Works on both active tasks and completed logs. Requires v2 per-task file architecture.',
2453
- inputSchema: {
2454
- file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2455
- task: zod_1.z.string().describe('Task ID to append log to'),
2456
- message: zod_1.z.string().describe('Log message to append'),
2457
- agent: zod_1.z.string().optional().describe('Agent name for attribution'),
2458
- }
2459
- }, async ({ file, task: taskId, message, agent }) => {
2460
- const filePath = file || defaultFile;
2461
- if (!(0, v2_detect_1.isV2)(filePath)) {
2462
- return { content: [{ type: 'text', text: 'Error: append_log requires v2 per-task file architecture. Run: brainfile migrate' }], isError: true };
2463
- }
2464
- const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
2465
- const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
2466
- if (!found) {
2467
- return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
2468
- }
2469
- const { doc, filePath: taskFilePath } = found;
2470
- const timestamp = new Date().toISOString();
2471
- const agentPrefix = agent ? `[${agent}] ` : '';
2472
- const entry = `- ${timestamp}: ${agentPrefix}${message}`;
2473
- const existingDescription = (0, v2_detect_1.extractDescription)(doc.body);
2474
- const existingLog = (0, v2_detect_1.extractLog)(doc.body) || '';
2475
- const newLog = existingLog ? `${existingLog}\n${entry}` : entry;
2476
- const newBody = (0, v2_detect_1.composeBody)(existingDescription, newLog);
2477
- (0, core_3.writeTaskFile)(taskFilePath, doc.task, newBody);
2478
- return { content: [{ type: 'text', text: `Log entry added to ${taskId}: ${entry}` }] };
2479
- });
2480
1754
  // Connect via stdio
2481
1755
  const transport = new stdio_js_1.StdioServerTransport();
2482
1756
  await server.connect(transport);