@brainfile/cli 0.13.3 → 0.15.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 (154) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +129 -354
  3. package/dist/cli.js +91 -5
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/add.d.ts +3 -0
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +163 -3
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/adr.d.ts +22 -0
  10. package/dist/commands/adr.d.ts.map +1 -0
  11. package/dist/commands/adr.js +182 -0
  12. package/dist/commands/adr.js.map +1 -0
  13. package/dist/commands/archive.d.ts.map +1 -1
  14. package/dist/commands/archive.js +147 -0
  15. package/dist/commands/archive.js.map +1 -1
  16. package/dist/commands/complete.d.ts +30 -0
  17. package/dist/commands/complete.d.ts.map +1 -0
  18. package/dist/commands/complete.js +254 -0
  19. package/dist/commands/complete.js.map +1 -0
  20. package/dist/commands/contract.d.ts.map +1 -1
  21. package/dist/commands/contract.js +2 -0
  22. package/dist/commands/contract.js.map +1 -1
  23. package/dist/commands/delete.d.ts.map +1 -1
  24. package/dist/commands/delete.js +29 -1
  25. package/dist/commands/delete.js.map +1 -1
  26. package/dist/commands/init.d.ts.map +1 -1
  27. package/dist/commands/init.js +38 -19
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/list.d.ts +1 -0
  30. package/dist/commands/list.d.ts.map +1 -1
  31. package/dist/commands/list.js +35 -12
  32. package/dist/commands/list.js.map +1 -1
  33. package/dist/commands/log.d.ts +52 -0
  34. package/dist/commands/log.d.ts.map +1 -0
  35. package/dist/commands/log.js +246 -0
  36. package/dist/commands/log.js.map +1 -0
  37. package/dist/commands/mcp.d.ts.map +1 -1
  38. package/dist/commands/mcp.js +864 -44
  39. package/dist/commands/mcp.js.map +1 -1
  40. package/dist/commands/migrate.d.ts +4 -3
  41. package/dist/commands/migrate.d.ts.map +1 -1
  42. package/dist/commands/migrate.js +225 -33
  43. package/dist/commands/migrate.js.map +1 -1
  44. package/dist/commands/move.d.ts.map +1 -1
  45. package/dist/commands/move.js +90 -0
  46. package/dist/commands/move.js.map +1 -1
  47. package/dist/commands/patch.d.ts.map +1 -1
  48. package/dist/commands/patch.js +85 -13
  49. package/dist/commands/patch.js.map +1 -1
  50. package/dist/commands/search.d.ts +33 -0
  51. package/dist/commands/search.d.ts.map +1 -0
  52. package/dist/commands/search.js +209 -0
  53. package/dist/commands/search.js.map +1 -0
  54. package/dist/commands/show.d.ts.map +1 -1
  55. package/dist/commands/show.js +75 -1
  56. package/dist/commands/show.js.map +1 -1
  57. package/dist/commands/subtask.d.ts.map +1 -1
  58. package/dist/commands/subtask.js +72 -5
  59. package/dist/commands/subtask.js.map +1 -1
  60. package/dist/commands/tui.d.ts.map +1 -1
  61. package/dist/commands/tui.js +10 -0
  62. package/dist/commands/tui.js.map +1 -1
  63. package/dist/commands/types.d.ts +40 -0
  64. package/dist/commands/types.d.ts.map +1 -0
  65. package/dist/commands/types.js +242 -0
  66. package/dist/commands/types.js.map +1 -0
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +3 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/lib/contractRunner.d.ts.map +1 -1
  72. package/dist/lib/contractRunner.js +177 -12
  73. package/dist/lib/contractRunner.js.map +1 -1
  74. package/dist/schemas/board.json +105 -18
  75. package/dist/tui/BrainfileTUI.d.ts.map +1 -1
  76. package/dist/tui/BrainfileTUI.js +23 -20
  77. package/dist/tui/BrainfileTUI.js.map +1 -1
  78. package/dist/tui/actions.d.ts +5 -5
  79. package/dist/tui/actions.d.ts.map +1 -1
  80. package/dist/tui/actions.js +335 -47
  81. package/dist/tui/actions.js.map +1 -1
  82. package/dist/tui/components/ArchivePanel.js +1 -1
  83. package/dist/tui/components/ArchivePanel.js.map +1 -1
  84. package/dist/tui/components/ColumnTabs.d.ts.map +1 -1
  85. package/dist/tui/components/ColumnTabs.js +6 -2
  86. package/dist/tui/components/ColumnTabs.js.map +1 -1
  87. package/dist/tui/components/HelpOverlay.js +3 -3
  88. package/dist/tui/components/HelpOverlay.js.map +1 -1
  89. package/dist/tui/components/LogsPanel.d.ts +16 -0
  90. package/dist/tui/components/LogsPanel.d.ts.map +1 -0
  91. package/dist/tui/components/LogsPanel.js +115 -0
  92. package/dist/tui/components/LogsPanel.js.map +1 -0
  93. package/dist/tui/components/MainPanelTabs.d.ts +2 -2
  94. package/dist/tui/components/MainPanelTabs.d.ts.map +1 -1
  95. package/dist/tui/components/MainPanelTabs.js +3 -3
  96. package/dist/tui/components/MainPanelTabs.js.map +1 -1
  97. package/dist/tui/components/StackedTaskList.d.ts +2 -1
  98. package/dist/tui/components/StackedTaskList.d.ts.map +1 -1
  99. package/dist/tui/components/StackedTaskList.js +9 -9
  100. package/dist/tui/components/StackedTaskList.js.map +1 -1
  101. package/dist/tui/components/TaskCard.d.ts +2 -1
  102. package/dist/tui/components/TaskCard.d.ts.map +1 -1
  103. package/dist/tui/components/TaskCard.js +41 -5
  104. package/dist/tui/components/TaskCard.js.map +1 -1
  105. package/dist/tui/components/TaskCardMeasure.d.ts +2 -16
  106. package/dist/tui/components/TaskCardMeasure.d.ts.map +1 -1
  107. package/dist/tui/components/TaskCardMeasure.js +30 -25
  108. package/dist/tui/components/TaskCardMeasure.js.map +1 -1
  109. package/dist/tui/components/TaskDetail.d.ts +2 -3
  110. package/dist/tui/components/TaskDetail.d.ts.map +1 -1
  111. package/dist/tui/components/TaskDetail.js +35 -12
  112. package/dist/tui/components/TaskDetail.js.map +1 -1
  113. package/dist/tui/components/TaskList.d.ts +2 -1
  114. package/dist/tui/components/TaskList.d.ts.map +1 -1
  115. package/dist/tui/components/TaskList.js +5 -5
  116. package/dist/tui/components/TaskList.js.map +1 -1
  117. package/dist/tui/components/index.d.ts +2 -2
  118. package/dist/tui/components/index.d.ts.map +1 -1
  119. package/dist/tui/components/index.js +3 -3
  120. package/dist/tui/components/index.js.map +1 -1
  121. package/dist/tui/hooks/useBrainfileLoader.d.ts.map +1 -1
  122. package/dist/tui/hooks/useBrainfileLoader.js +97 -31
  123. package/dist/tui/hooks/useBrainfileLoader.js.map +1 -1
  124. package/dist/tui/hooks/useKeyboardNavigation.d.ts.map +1 -1
  125. package/dist/tui/hooks/useKeyboardNavigation.js +47 -47
  126. package/dist/tui/hooks/useKeyboardNavigation.js.map +1 -1
  127. package/dist/tui/types.d.ts +7 -7
  128. package/dist/tui/types.d.ts.map +1 -1
  129. package/dist/utils/board-types.d.ts +13 -0
  130. package/dist/utils/board-types.d.ts.map +1 -0
  131. package/dist/utils/board-types.js +7 -0
  132. package/dist/utils/board-types.js.map +1 -0
  133. package/dist/utils/dot-brainfile.d.ts +9 -0
  134. package/dist/utils/dot-brainfile.d.ts.map +1 -0
  135. package/dist/utils/dot-brainfile.js +74 -0
  136. package/dist/utils/dot-brainfile.js.map +1 -0
  137. package/dist/utils/strict-validation.d.ts +8 -0
  138. package/dist/utils/strict-validation.d.ts.map +1 -0
  139. package/dist/utils/strict-validation.js +41 -0
  140. package/dist/utils/strict-validation.js.map +1 -0
  141. package/dist/utils/v2-detect.d.ts +28 -0
  142. package/dist/utils/v2-detect.d.ts.map +1 -0
  143. package/dist/utils/v2-detect.js +112 -0
  144. package/dist/utils/v2-detect.js.map +1 -0
  145. package/dist/utils/v2-tasks.d.ts +121 -0
  146. package/dist/utils/v2-tasks.d.ts.map +1 -0
  147. package/dist/utils/v2-tasks.js +384 -0
  148. package/dist/utils/v2-tasks.js.map +1 -0
  149. package/dist/utils/workspace-format.d.ts +25 -0
  150. package/dist/utils/workspace-format.d.ts.map +1 -0
  151. package/dist/utils/workspace-format.js +106 -0
  152. package/dist/utils/workspace-format.js.map +1 -0
  153. package/package.json +3 -3
  154. package/state.json +3 -0
@@ -42,12 +42,46 @@ const path = __importStar(require("path"));
42
42
  const core_1 = require("@brainfile/core");
43
43
  const errorHandler_1 = require("../utils/errorHandler");
44
44
  const contractSpec_1 = require("../utils/contractSpec");
45
+ const strict_validation_1 = require("../utils/strict-validation");
45
46
  const config_1 = require("../utils/config");
46
47
  const github_auth_1 = require("../utils/github-auth");
47
48
  const linear_auth_1 = require("../utils/linear-auth");
48
49
  const core_2 = require("@brainfile/core");
49
50
  const contractRunner_1 = require("../lib/contractRunner");
50
51
  const archive_1 = require("../utils/archive");
52
+ const core_3 = require("@brainfile/core");
53
+ const v2_detect_1 = require("../utils/v2-detect");
54
+ function sanitizeTypesConfig(raw) {
55
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
56
+ return {};
57
+ }
58
+ const out = {};
59
+ for (const [name, value] of Object.entries(raw)) {
60
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
61
+ continue;
62
+ }
63
+ const entry = value;
64
+ const idPrefix = typeof entry.idPrefix === 'string' && entry.idPrefix.trim()
65
+ ? entry.idPrefix.trim()
66
+ : name;
67
+ const normalized = { idPrefix };
68
+ if (typeof entry.completable === 'boolean')
69
+ normalized.completable = entry.completable;
70
+ if (typeof entry.schema === 'string' && entry.schema.trim())
71
+ normalized.schema = entry.schema.trim();
72
+ out[name] = normalized;
73
+ }
74
+ return out;
75
+ }
76
+ function isTaskCompletable(taskType, rawTypes) {
77
+ const resolvedType = taskType || 'task';
78
+ if (resolvedType === 'task') {
79
+ return true;
80
+ }
81
+ const types = sanitizeTypesConfig(rawTypes);
82
+ const typeConfig = types[resolvedType];
83
+ return typeConfig?.completable !== false;
84
+ }
51
85
  function resolveBrainfile(filePath) {
52
86
  return path.resolve(filePath);
53
87
  }
@@ -68,6 +102,15 @@ function writeBoard(filePath, board) {
68
102
  const content = core_1.Brainfile.serialize(board);
69
103
  fs.writeFileSync(resolvedPath, content, 'utf-8');
70
104
  }
105
+ function mcpStructuredError(message, field, value) {
106
+ return {
107
+ content: [{
108
+ type: 'text',
109
+ text: JSON.stringify({ error: { code: 'VALIDATION_ERROR', message, field, value } }, null, 2)
110
+ }],
111
+ isError: true
112
+ };
113
+ }
71
114
  /**
72
115
  * Find git repository root by walking up directory tree
73
116
  */
@@ -159,10 +202,37 @@ async function mcpCommand(options) {
159
202
  inputSchema: {
160
203
  file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
161
204
  column: zod_1.z.string().optional().describe('Filter by column ID or name'),
162
- tag: zod_1.z.string().optional().describe('Filter by tag')
205
+ tag: zod_1.z.string().optional().describe('Filter by tag'),
206
+ type: zod_1.z.string().optional().describe('Filter by document type (e.g., epic, adr). Only returns tasks matching this type.'),
163
207
  }
164
- }, async ({ file, column, tag }) => {
208
+ }, async ({ file, column, tag, type: filterType }) => {
165
209
  const filePath = file || defaultFile;
210
+ // V2: use per-task files
211
+ if ((0, v2_detect_1.isV2)(filePath)) {
212
+ const board = (0, v2_detect_1.buildBoardFromV2)(filePath);
213
+ let tasks = [];
214
+ for (const col of board.columns) {
215
+ if (column) {
216
+ const matchesId = col.id === column;
217
+ const matchesName = col.title.toLowerCase() === column.toLowerCase();
218
+ if (!matchesId && !matchesName)
219
+ continue;
220
+ }
221
+ for (const task of col.tasks) {
222
+ if (tag && (!task.tags || !task.tags.includes(tag)))
223
+ continue;
224
+ // Filter by type: match explicit type field, or treat missing/undefined as "task"
225
+ if (filterType) {
226
+ const taskType = task.type || 'task';
227
+ if (taskType !== filterType)
228
+ continue;
229
+ }
230
+ tasks.push({ id: task.id, title: task.title, column: col.title, priority: task.priority, tags: task.tags, assignee: task.assignee });
231
+ }
232
+ }
233
+ return { content: [{ type: 'text', text: JSON.stringify({ tasks, count: tasks.length }, null, 2) }] };
234
+ }
235
+ // V1: use board
166
236
  const result = readBoard(filePath);
167
237
  if ('error' in result) {
168
238
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -170,7 +240,6 @@ async function mcpCommand(options) {
170
240
  const { board } = result;
171
241
  let tasks = [];
172
242
  for (const col of board.columns) {
173
- // Filter by column if specified
174
243
  if (column) {
175
244
  const matchesId = col.id === column;
176
245
  const matchesName = col.title.toLowerCase() === column.toLowerCase();
@@ -178,9 +247,14 @@ async function mcpCommand(options) {
178
247
  continue;
179
248
  }
180
249
  for (const task of col.tasks) {
181
- // Filter by tag if specified
182
250
  if (tag && (!task.tags || !task.tags.includes(tag)))
183
251
  continue;
252
+ // Filter by type: match explicit type field, or treat missing/undefined as "task"
253
+ if (filterType) {
254
+ const taskType = task.type || 'task';
255
+ if (taskType !== filterType)
256
+ continue;
257
+ }
184
258
  tasks.push({
185
259
  id: task.id,
186
260
  title: task.title,
@@ -206,6 +280,24 @@ async function mcpCommand(options) {
206
280
  }
207
281
  }, async ({ file, task }) => {
208
282
  const filePath = file || defaultFile;
283
+ // V2: use per-task files
284
+ if ((0, v2_detect_1.isV2)(filePath)) {
285
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
286
+ const found = (0, v2_detect_1.findV2Task)(dirs, task, true);
287
+ if (!found) {
288
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
289
+ }
290
+ const { doc, isLog } = found;
291
+ const description = (0, v2_detect_1.extractDescription)(doc.body);
292
+ const output = {
293
+ ...doc.task,
294
+ ...(description && !doc.task.description && { description }),
295
+ column: isLog ? 'Completed' : (doc.task.column || 'unknown'),
296
+ ...(isLog && { archived: true }),
297
+ };
298
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
299
+ }
300
+ // V1: use board
209
301
  const result = readBoard(filePath);
210
302
  if ('error' in result) {
211
303
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -237,6 +329,66 @@ async function mcpCommand(options) {
237
329
  }
238
330
  }, async ({ file, query, column, priority, assignee }) => {
239
331
  const filePath = file || defaultFile;
332
+ // V2: search per-task files
333
+ if ((0, v2_detect_1.isV2)(filePath)) {
334
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
335
+ const queryLower = query.toLowerCase();
336
+ let matches = [];
337
+ const taskDocs = (0, core_3.readTasksDir)(dirs.boardDir);
338
+ for (const doc of taskDocs) {
339
+ const task = doc.task;
340
+ if (column && task.column !== column)
341
+ continue;
342
+ if (priority && task.priority !== priority)
343
+ continue;
344
+ if (assignee && task.assignee !== assignee)
345
+ continue;
346
+ let score = 0;
347
+ if (task.title.toLowerCase().includes(queryLower)) {
348
+ score += 10;
349
+ if (task.title.toLowerCase().startsWith(queryLower))
350
+ score += 5;
351
+ }
352
+ if (task.description?.toLowerCase().includes(queryLower))
353
+ score += 5;
354
+ if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
355
+ score += 3;
356
+ if (task.id.toLowerCase() === queryLower)
357
+ score += 20;
358
+ if (score > 0) {
359
+ matches.push({ id: task.id, title: task.title, column: task.column, priority: task.priority, tags: task.tags, assignee: task.assignee, score });
360
+ }
361
+ }
362
+ // Also search logs
363
+ if (!column) {
364
+ const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
365
+ for (const doc of logDocs) {
366
+ const task = doc.task;
367
+ if (priority && task.priority !== priority)
368
+ continue;
369
+ if (assignee && task.assignee !== assignee)
370
+ continue;
371
+ let score = 0;
372
+ if (task.title.toLowerCase().includes(queryLower)) {
373
+ score += 10;
374
+ if (task.title.toLowerCase().startsWith(queryLower))
375
+ score += 5;
376
+ }
377
+ if (task.description?.toLowerCase().includes(queryLower))
378
+ score += 5;
379
+ if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
380
+ score += 3;
381
+ if (task.id.toLowerCase() === queryLower)
382
+ score += 20;
383
+ if (score > 0) {
384
+ matches.push({ id: task.id, title: task.title, column: 'Completed', priority: task.priority, tags: task.tags, assignee: task.assignee, score, isLog: true });
385
+ }
386
+ }
387
+ }
388
+ matches.sort((a, b) => b.score - a.score);
389
+ return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
390
+ }
391
+ // V1: use board
240
392
  const result = readBoard(filePath);
241
393
  if ('error' in result) {
242
394
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -245,7 +397,6 @@ async function mcpCommand(options) {
245
397
  const queryLower = query.toLowerCase();
246
398
  let matches = [];
247
399
  for (const col of board.columns) {
248
- // Filter by column if specified
249
400
  if (column) {
250
401
  const matchesId = col.id === column;
251
402
  const matchesName = col.title.toLowerCase() === column.toLowerCase();
@@ -253,51 +404,30 @@ async function mcpCommand(options) {
253
404
  continue;
254
405
  }
255
406
  for (const task of col.tasks) {
256
- // Filter by priority if specified
257
407
  if (priority && task.priority !== priority)
258
408
  continue;
259
- // Filter by assignee if specified
260
409
  if (assignee && task.assignee !== assignee)
261
410
  continue;
262
- // Calculate search score
263
411
  let score = 0;
264
- // Title match (highest weight)
265
412
  if (task.title.toLowerCase().includes(queryLower)) {
266
413
  score += 10;
267
414
  if (task.title.toLowerCase().startsWith(queryLower))
268
415
  score += 5;
269
416
  }
270
- // Description match
271
- if (task.description?.toLowerCase().includes(queryLower)) {
417
+ if (task.description?.toLowerCase().includes(queryLower))
272
418
  score += 5;
273
- }
274
- // Tag match
275
- if (task.tags?.some(t => t.toLowerCase().includes(queryLower))) {
419
+ if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
276
420
  score += 3;
277
- }
278
- // ID exact match
279
- if (task.id.toLowerCase() === queryLower) {
421
+ if (task.id.toLowerCase() === queryLower)
280
422
  score += 20;
281
- }
282
423
  if (score > 0) {
283
- matches.push({
284
- id: task.id,
285
- title: task.title,
286
- column: col.title,
287
- priority: task.priority,
288
- tags: task.tags,
289
- assignee: task.assignee,
290
- score
291
- });
424
+ matches.push({ id: task.id, title: task.title, column: col.title, priority: task.priority, tags: task.tags, assignee: task.assignee, score });
292
425
  }
293
426
  }
294
427
  }
295
- // Sort by score descending
296
428
  matches.sort((a, b) => b.score - a.score);
297
429
  const output = { results: matches, count: matches.length };
298
- return {
299
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
300
- };
430
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
301
431
  });
302
432
  // Add task tool
303
433
  server.registerTool('add_task', {
@@ -314,6 +444,7 @@ async function mcpCommand(options) {
314
444
  dueDate: zod_1.z.string().optional().describe('Due date (YYYY-MM-DD)'),
315
445
  subtasks: zod_1.z.array(zod_1.z.string()).optional().describe('Subtask titles (IDs auto-generated)'),
316
446
  relatedFiles: zod_1.z.array(zod_1.z.string()).optional().describe('Related file paths'),
447
+ type: zod_1.z.string().optional().describe('Document type (e.g., epic, adr). Determines ID prefix. Default: task'),
317
448
  // Contract creation (optional)
318
449
  with_contract: zod_1.z.boolean().optional().describe('Attach a contract to the new task (status=ready)'),
319
450
  deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('Contract deliverables: type:path:description'),
@@ -323,14 +454,75 @@ async function mcpCommand(options) {
323
454
  withContract: zod_1.z.boolean().optional().describe('Alias of with_contract'),
324
455
  validationCommands: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of validation_commands'),
325
456
  }
326
- }, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, with_contract, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
457
+ }, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, type: docType, with_contract, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
327
458
  const filePath = file || defaultFile;
459
+ // V2: add task as individual file
460
+ if ((0, v2_detect_1.isV2)(filePath)) {
461
+ try {
462
+ const dirs = (0, v2_detect_1.ensureV2Dirs)(filePath);
463
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
464
+ const typePrefix = docType || 'task';
465
+ const typeValidation = (0, strict_validation_1.validateType)(board, typePrefix);
466
+ if (!typeValidation.valid) {
467
+ return mcpStructuredError(typeValidation.error || `Invalid type: ${typePrefix}`, 'type', typePrefix);
468
+ }
469
+ let targetColumn = board.columns.find(c => c.id === column);
470
+ if (!targetColumn)
471
+ targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
472
+ if (!targetColumn) {
473
+ const available = board.columns.map(c => `${c.id} (${c.title})`).join(', ');
474
+ return { content: [{ type: 'text', text: `Error: Column not found: ${column}. Available: ${available}` }], isError: true };
475
+ }
476
+ const taskId = (0, core_3.generateNextFileTaskId)(dirs.boardDir, dirs.logsDir, typePrefix);
477
+ const existingTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === targetColumn.id);
478
+ const position = existingTasks.length;
479
+ const builtSubtasks = subtasks && subtasks.length > 0
480
+ ? subtasks.map((st, i) => ({ id: `${taskId}-${i + 1}`, title: st.trim(), completed: false }))
481
+ : undefined;
482
+ const task = {
483
+ id: taskId,
484
+ title,
485
+ ...(docType && docType !== 'task' && { type: docType }),
486
+ column: targetColumn.id,
487
+ position,
488
+ ...(description && { description }),
489
+ ...(priority && { priority }),
490
+ ...(tags && tags.length > 0 && { tags }),
491
+ ...(assignee && { assignee }),
492
+ ...(dueDate && { dueDate }),
493
+ ...(relatedFiles && relatedFiles.length > 0 && { relatedFiles }),
494
+ ...(builtSubtasks && { subtasks: builtSubtasks }),
495
+ createdAt: new Date().toISOString(),
496
+ };
497
+ // Optionally attach contract
498
+ const wantsContract = Boolean(with_contract ?? withContract) ||
499
+ Boolean(deliverables && deliverables.length > 0) ||
500
+ Boolean(validation_commands && validation_commands.length > 0) ||
501
+ Boolean(validationCommands && validationCommands.length > 0) ||
502
+ Boolean(constraints && constraints.length > 0);
503
+ if (wantsContract) {
504
+ const contract = (0, contractSpec_1.buildContract)({
505
+ deliverableSpecs: deliverables,
506
+ validationCommands: validation_commands ?? validationCommands,
507
+ constraints,
508
+ });
509
+ task.contract = contract;
510
+ }
511
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
512
+ const body = description ? (0, v2_detect_1.composeBody)(description) : '';
513
+ (0, core_3.writeTaskFile)(taskPath, task, body);
514
+ return { content: [{ type: 'text', text: `Task added successfully: ${taskId} - ${title}` }] };
515
+ }
516
+ catch (e) {
517
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
518
+ }
519
+ }
520
+ // V1: use board
328
521
  const result = readBoard(filePath);
329
522
  if ('error' in result) {
330
523
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
331
524
  }
332
525
  let { board } = result;
333
- // Find target column
334
526
  let targetColumn = (0, core_1.findColumnById)(board, column);
335
527
  if (!targetColumn) {
336
528
  targetColumn = (0, core_1.findColumnByName)(board, column);
@@ -353,11 +545,9 @@ async function mcpCommand(options) {
353
545
  if (!addResult.success) {
354
546
  return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
355
547
  }
356
- // Get the new task
357
548
  const newTask = addResult.board.columns
358
549
  .find(c => c.id === targetColumn.id)
359
550
  .tasks.slice(-1)[0];
360
- // Optionally attach a contract (status=ready)
361
551
  const wantsContract = Boolean(with_contract ?? withContract) ||
362
552
  Boolean(deliverables && deliverables.length > 0) ||
363
553
  Boolean(validation_commands && validation_commands.length > 0) ||
@@ -405,6 +595,29 @@ async function mcpCommand(options) {
405
595
  if (!resolvedTaskId) {
406
596
  return { content: [{ type: 'text', text: 'Error: task is required' }], isError: true };
407
597
  }
598
+ // V2: update task file directly
599
+ if ((0, v2_detect_1.isV2)(filePath)) {
600
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
601
+ const found = (0, v2_detect_1.findV2Task)(dirs, resolvedTaskId);
602
+ if (!found) {
603
+ return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
604
+ }
605
+ try {
606
+ const contract = (0, contractSpec_1.buildContract)({
607
+ deliverableSpecs: deliverables,
608
+ validationCommands: validation_commands ?? validationCommands,
609
+ constraints,
610
+ });
611
+ found.doc.task.contract = contract;
612
+ found.doc.task.updatedAt = new Date().toISOString();
613
+ (0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
614
+ return { content: [{ type: 'text', text: `Contract attached: ${resolvedTaskId}` }] };
615
+ }
616
+ catch (e) {
617
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
618
+ }
619
+ }
620
+ // V1: use board
408
621
  const result = readBoard(filePath);
409
622
  if ('error' in result) {
410
623
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -442,17 +655,56 @@ async function mcpCommand(options) {
442
655
  }
443
656
  }, async ({ file, task, column }) => {
444
657
  const filePath = file || defaultFile;
658
+ // V2: update task file
659
+ if ((0, v2_detect_1.isV2)(filePath)) {
660
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
661
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
662
+ const doc = (0, core_3.readTaskFile)(taskPath);
663
+ if (!doc) {
664
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
665
+ }
666
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
667
+ let targetColumn = board.columns.find(c => c.id === column);
668
+ if (!targetColumn)
669
+ targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
670
+ const targetColumnId = targetColumn?.id || column;
671
+ const columnValidation = (0, strict_validation_1.validateColumn)(board, targetColumnId);
672
+ if (!columnValidation.valid) {
673
+ return mcpStructuredError(columnValidation.error || `Invalid column: ${targetColumnId}`, 'column', targetColumnId);
674
+ }
675
+ const resolvedTargetColumn = targetColumn || { id: column, title: column, tasks: [] };
676
+ const sourceColumn = doc.task.column || '';
677
+ const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === resolvedTargetColumn.id);
678
+ doc.task.column = resolvedTargetColumn.id;
679
+ doc.task.position = targetTasks.length;
680
+ doc.task.updatedAt = new Date().toISOString();
681
+ (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
682
+ const shouldAutoComplete = resolvedTargetColumn.completionColumn === true && isTaskCompletable(doc.task.type, board.types);
683
+ if (shouldAutoComplete) {
684
+ const completeResult = (0, core_3.completeTaskFile)(taskPath, dirs.logsDir);
685
+ if (!completeResult.success) {
686
+ return { content: [{ type: 'text', text: `Error: ${completeResult.error || `Failed to complete task: ${task}`}` }], isError: true };
687
+ }
688
+ }
689
+ const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(doc.task, resolvedTargetColumn);
690
+ let message = `Task ${task} moved from "${sourceColumn}" to "${resolvedTargetColumn.title}"`;
691
+ if (shouldAutoComplete) {
692
+ message += '\nTask auto-completed and moved to logs/.';
693
+ }
694
+ if (warning)
695
+ message += `\n\n${warning.warning}`;
696
+ return { content: [{ type: 'text', text: message }] };
697
+ }
698
+ // V1: use board
445
699
  const result = readBoard(filePath);
446
700
  if ('error' in result) {
447
701
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
448
702
  }
449
703
  let { board } = result;
450
- // Find the task
451
704
  const taskInfo = (0, core_1.findTaskById)(board, task);
452
705
  if (!taskInfo) {
453
706
  return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
454
707
  }
455
- // Find target column
456
708
  let targetColumn = (0, core_1.findColumnById)(board, column);
457
709
  if (!targetColumn) {
458
710
  targetColumn = (0, core_1.findColumnByName)(board, column);
@@ -465,7 +717,6 @@ async function mcpCommand(options) {
465
717
  return { content: [{ type: 'text', text: `Error: ${moveResult.error}` }], isError: true };
466
718
  }
467
719
  writeBoard(filePath, moveResult.board);
468
- // Check for incomplete subtasks warning when moving to done-like column
469
720
  const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
470
721
  let message = `Task ${task} moved from "${taskInfo.column.title}" to "${targetColumn.title}"`;
471
722
  if (warning) {
@@ -492,13 +743,64 @@ async function mcpCommand(options) {
492
743
  }
493
744
  }, async ({ file, task, title, description, priority, tags, assignee, dueDate, relatedFiles }) => {
494
745
  const filePath = file || defaultFile;
746
+ const isNull = (v) => v === null || v === 'null';
747
+ // V2: update task file directly
748
+ if ((0, v2_detect_1.isV2)(filePath)) {
749
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
750
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
751
+ const doc = (0, core_3.readTaskFile)(taskPath);
752
+ if (!doc) {
753
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
754
+ }
755
+ const t = doc.task;
756
+ if (title !== undefined)
757
+ t.title = title;
758
+ if (description !== undefined) {
759
+ if (isNull(description))
760
+ delete t.description;
761
+ else
762
+ t.description = description;
763
+ }
764
+ if (priority !== undefined) {
765
+ if (isNull(priority))
766
+ delete t.priority;
767
+ else
768
+ t.priority = priority;
769
+ }
770
+ if (tags !== undefined) {
771
+ if (isNull(tags))
772
+ delete t.tags;
773
+ else
774
+ t.tags = tags;
775
+ }
776
+ if (assignee !== undefined) {
777
+ if (isNull(assignee))
778
+ delete t.assignee;
779
+ else
780
+ t.assignee = assignee;
781
+ }
782
+ if (dueDate !== undefined) {
783
+ if (isNull(dueDate))
784
+ delete t.dueDate;
785
+ else
786
+ t.dueDate = dueDate;
787
+ }
788
+ if (relatedFiles !== undefined) {
789
+ if (isNull(relatedFiles))
790
+ delete t.relatedFiles;
791
+ else
792
+ t.relatedFiles = relatedFiles;
793
+ }
794
+ t.updatedAt = new Date().toISOString();
795
+ (0, core_3.writeTaskFile)(taskPath, t, doc.body);
796
+ return { content: [{ type: 'text', text: `Task ${task} updated successfully` }] };
797
+ }
798
+ // V1: use board
495
799
  const result = readBoard(filePath);
496
800
  if ('error' in result) {
497
801
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
498
802
  }
499
803
  let { board } = result;
500
- // Helper to check for null or "null" string (MCP clients may send either)
501
- const isNull = (v) => v === null || v === 'null';
502
804
  const patch = {};
503
805
  if (title !== undefined)
504
806
  patch.title = title;
@@ -533,6 +835,22 @@ async function mcpCommand(options) {
533
835
  }
534
836
  }, async ({ file, task }) => {
535
837
  const filePath = file || defaultFile;
838
+ // V2: delete task file
839
+ if ((0, v2_detect_1.isV2)(filePath)) {
840
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
841
+ const found = (0, v2_detect_1.findV2Task)(dirs, task, true);
842
+ if (!found) {
843
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
844
+ }
845
+ try {
846
+ fs.unlinkSync(found.filePath);
847
+ return { content: [{ type: 'text', text: `Task ${task} deleted successfully` }] };
848
+ }
849
+ catch (e) {
850
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
851
+ }
852
+ }
853
+ // V1: use board
536
854
  const result = readBoard(filePath);
537
855
  if ('error' in result) {
538
856
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -563,6 +881,78 @@ async function mcpCommand(options) {
563
881
  }
564
882
  }, async ({ file, task, destination }) => {
565
883
  const filePath = file || defaultFile;
884
+ // V2: archive means move task from board/ to logs/
885
+ if ((0, v2_detect_1.isV2)(filePath)) {
886
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
887
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
888
+ if (!found) {
889
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
890
+ }
891
+ // Determine effective destination
892
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
893
+ const brainfileDestination = board.archive?.destination;
894
+ const effectiveDestination = destination || (0, config_1.getEffectiveArchiveDestination)(brainfileDestination);
895
+ // For GitHub/Linear, format and send before removing
896
+ if (effectiveDestination === 'github') {
897
+ if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
898
+ 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 };
899
+ }
900
+ const config = (0, config_1.getArchiveConfig)();
901
+ if (!config.github?.owner || !config.github?.repo) {
902
+ 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 };
903
+ }
904
+ const payload = (0, core_2.formatTaskForGitHub)(found.doc.task, {
905
+ includeMeta: true, includeSubtasks: true, includeRelatedFiles: true,
906
+ boardTitle: board.title, fromColumn: found.doc.task.column || 'unknown',
907
+ extraLabels: config.github.labels,
908
+ });
909
+ 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' });
910
+ if (!ghResult.success) {
911
+ return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
912
+ }
913
+ fs.unlinkSync(found.filePath);
914
+ return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
915
+ }
916
+ if (effectiveDestination === 'linear') {
917
+ if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
918
+ 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 };
919
+ }
920
+ const config = (0, config_1.getArchiveConfig)();
921
+ let teamId = config.linear?.teamId;
922
+ if (!teamId) {
923
+ const teams = await (0, linear_auth_1.getLinearTeams)();
924
+ if (teams.length === 0) {
925
+ return { content: [{ type: 'text', text: `Error: No Linear teams found.` }], isError: true };
926
+ }
927
+ if (teams.length === 1) {
928
+ teamId = teams[0].id;
929
+ }
930
+ else {
931
+ const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
932
+ return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
933
+ }
934
+ }
935
+ const payload = (0, core_2.formatTaskForLinear)(found.doc.task, {
936
+ includeMeta: true, includeSubtasks: true, includeRelatedFiles: true,
937
+ boardTitle: board.title, fromColumn: found.doc.task.column || 'unknown', stateName: 'Done',
938
+ });
939
+ const linearResult = await (0, linear_auth_1.createLinearIssue)({ teamId, title: payload.title, description: payload.description, priority: payload.priority, labelNames: payload.labelNames, stateName: 'Done' });
940
+ if (!linearResult.success) {
941
+ return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
942
+ }
943
+ fs.unlinkSync(found.filePath);
944
+ return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
945
+ }
946
+ // Local archive: move from board/ to logs/
947
+ const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
948
+ found.doc.task.completedAt = found.doc.task.completedAt || new Date().toISOString();
949
+ delete found.doc.task.column;
950
+ delete found.doc.task.position;
951
+ (0, core_3.writeTaskFile)(logPath, found.doc.task, found.doc.body);
952
+ fs.unlinkSync(found.filePath);
953
+ return { content: [{ type: 'text', text: `Task ${task} archived to logs/` }] };
954
+ }
955
+ // V1: use board
566
956
  const result = readBoard(filePath);
567
957
  if ('error' in result) {
568
958
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -736,6 +1126,34 @@ async function mcpCommand(options) {
736
1126
  }
737
1127
  }, async ({ file, task, column }) => {
738
1128
  const filePath = file || defaultFile;
1129
+ // V2: restore means move from logs/ to board/
1130
+ if ((0, v2_detect_1.isV2)(filePath)) {
1131
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1132
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1133
+ let targetColumn = board.columns.find(c => c.id === column);
1134
+ if (!targetColumn)
1135
+ targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
1136
+ if (!targetColumn) {
1137
+ return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
1138
+ }
1139
+ // Look in logs
1140
+ const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
1141
+ const doc = (0, core_3.readTaskFile)(logPath);
1142
+ if (!doc) {
1143
+ return { content: [{ type: 'text', text: `Error: Task not found in logs: ${task}` }], isError: true };
1144
+ }
1145
+ // Move to board/ with column info
1146
+ const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === targetColumn.id);
1147
+ doc.task.column = targetColumn.id;
1148
+ doc.task.position = targetTasks.length;
1149
+ delete doc.task.completedAt;
1150
+ doc.task.updatedAt = new Date().toISOString();
1151
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
1152
+ (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
1153
+ fs.unlinkSync(logPath);
1154
+ return { content: [{ type: 'text', text: `Task ${task} restored to "${targetColumn.title}"` }] };
1155
+ }
1156
+ // V1: use board
739
1157
  const result = readBoard(filePath);
740
1158
  if ('error' in result) {
741
1159
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -777,6 +1195,26 @@ async function mcpCommand(options) {
777
1195
  }
778
1196
  }, async ({ file, task, title }) => {
779
1197
  const filePath = file || defaultFile;
1198
+ // V2: update task file directly
1199
+ if ((0, v2_detect_1.isV2)(filePath)) {
1200
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1201
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1202
+ if (!found) {
1203
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1204
+ }
1205
+ const t = found.doc.task;
1206
+ if (!t.subtasks)
1207
+ t.subtasks = [];
1208
+ const nextId = t.subtasks.length > 0
1209
+ ? `${task}-${Math.max(...t.subtasks.map(s => parseInt(s.id.split('-').pop() || '0', 10))) + 1}`
1210
+ : `${task}-1`;
1211
+ const newSubtask = { id: nextId, title, completed: false };
1212
+ t.subtasks.push(newSubtask);
1213
+ t.updatedAt = new Date().toISOString();
1214
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1215
+ return { content: [{ type: 'text', text: `Subtask added: ${newSubtask.id} - ${newSubtask.title}` }] };
1216
+ }
1217
+ // V1: use board
780
1218
  const result = readBoard(filePath);
781
1219
  if ('error' in result) {
782
1220
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -805,6 +1243,23 @@ async function mcpCommand(options) {
805
1243
  }
806
1244
  }, async ({ file, task, subtask }) => {
807
1245
  const filePath = file || defaultFile;
1246
+ // V2: update task file directly
1247
+ if ((0, v2_detect_1.isV2)(filePath)) {
1248
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1249
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1250
+ if (!found) {
1251
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1252
+ }
1253
+ const t = found.doc.task;
1254
+ if (!t.subtasks || !t.subtasks.some(s => s.id === subtask)) {
1255
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
1256
+ }
1257
+ t.subtasks = t.subtasks.filter(s => s.id !== subtask);
1258
+ t.updatedAt = new Date().toISOString();
1259
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1260
+ return { content: [{ type: 'text', text: `Subtask ${subtask} deleted successfully` }] };
1261
+ }
1262
+ // V1: use board
808
1263
  const result = readBoard(filePath);
809
1264
  if ('error' in result) {
810
1265
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -830,6 +1285,26 @@ async function mcpCommand(options) {
830
1285
  }
831
1286
  }, async ({ file, task, subtask }) => {
832
1287
  const filePath = file || defaultFile;
1288
+ // V2: update task file directly
1289
+ if ((0, v2_detect_1.isV2)(filePath)) {
1290
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1291
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1292
+ if (!found) {
1293
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1294
+ }
1295
+ const t = found.doc.task;
1296
+ const st = t.subtasks?.find(s => s.id === subtask);
1297
+ if (!st) {
1298
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
1299
+ }
1300
+ const wasCompleted = st.completed;
1301
+ st.completed = !st.completed;
1302
+ t.updatedAt = new Date().toISOString();
1303
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1304
+ const newStatus = wasCompleted ? 'incomplete' : 'completed';
1305
+ return { content: [{ type: 'text', text: `Subtask ${subtask} marked as ${newStatus}` }] };
1306
+ }
1307
+ // V1: use board
833
1308
  const result = readBoard(filePath);
834
1309
  if ('error' in result) {
835
1310
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -861,6 +1336,24 @@ async function mcpCommand(options) {
861
1336
  }
862
1337
  }, async ({ file, task, subtask, title }) => {
863
1338
  const filePath = file || defaultFile;
1339
+ // V2: update task file directly
1340
+ if ((0, v2_detect_1.isV2)(filePath)) {
1341
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1342
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1343
+ if (!found) {
1344
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1345
+ }
1346
+ const t = found.doc.task;
1347
+ const st = t.subtasks?.find(s => s.id === subtask);
1348
+ if (!st) {
1349
+ return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
1350
+ }
1351
+ st.title = title;
1352
+ t.updatedAt = new Date().toISOString();
1353
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1354
+ return { content: [{ type: 'text', text: `Subtask ${subtask} updated to "${title}"` }] };
1355
+ }
1356
+ // V1: use board
864
1357
  const result = readBoard(filePath);
865
1358
  if ('error' in result) {
866
1359
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -887,6 +1380,29 @@ async function mcpCommand(options) {
887
1380
  }
888
1381
  }, async ({ file, task, subtasks, completed }) => {
889
1382
  const filePath = file || defaultFile;
1383
+ // V2: update task file directly
1384
+ if ((0, v2_detect_1.isV2)(filePath)) {
1385
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1386
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1387
+ if (!found) {
1388
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1389
+ }
1390
+ const t = found.doc.task;
1391
+ if (!t.subtasks) {
1392
+ return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
1393
+ }
1394
+ const subtaskSet = new Set(subtasks);
1395
+ for (const st of t.subtasks) {
1396
+ if (subtaskSet.has(st.id)) {
1397
+ st.completed = completed;
1398
+ }
1399
+ }
1400
+ t.updatedAt = new Date().toISOString();
1401
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1402
+ const status = completed ? 'completed' : 'incomplete';
1403
+ return { content: [{ type: 'text', text: `${subtasks.length} subtasks marked as ${status}` }] };
1404
+ }
1405
+ // V1: use board
890
1406
  const result = readBoard(filePath);
891
1407
  if ('error' in result) {
892
1408
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -913,13 +1429,32 @@ async function mcpCommand(options) {
913
1429
  }
914
1430
  }, async ({ file, task, completed }) => {
915
1431
  const filePath = file || defaultFile;
1432
+ const markCompleted = completed ?? true;
1433
+ // V2: update task file directly
1434
+ if ((0, v2_detect_1.isV2)(filePath)) {
1435
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1436
+ const found = (0, v2_detect_1.findV2Task)(dirs, task);
1437
+ if (!found) {
1438
+ return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
1439
+ }
1440
+ const t = found.doc.task;
1441
+ const count = t.subtasks?.length || 0;
1442
+ if (t.subtasks) {
1443
+ for (const st of t.subtasks) {
1444
+ st.completed = markCompleted;
1445
+ }
1446
+ }
1447
+ t.updatedAt = new Date().toISOString();
1448
+ (0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
1449
+ const status = markCompleted ? 'completed' : 'incomplete';
1450
+ return { content: [{ type: 'text', text: `All ${count} subtasks in ${task} marked as ${status}` }] };
1451
+ }
1452
+ // V1: use board
916
1453
  const result = readBoard(filePath);
917
1454
  if ('error' in result) {
918
1455
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
919
1456
  }
920
1457
  let { board } = result;
921
- // Default to true if not specified
922
- const markCompleted = completed ?? true;
923
1458
  const bulkResult = (0, core_1.setAllSubtasksCompleted)(board, task, markCompleted);
924
1459
  if (!bulkResult.success) {
925
1460
  return { content: [{ type: 'text', text: `Error: ${bulkResult.error}` }], isError: true };
@@ -947,6 +1482,37 @@ async function mcpCommand(options) {
947
1482
  }
948
1483
  }, async ({ file, tasks, column }) => {
949
1484
  const filePath = file || defaultFile;
1485
+ // V2: update each task file
1486
+ if ((0, v2_detect_1.isV2)(filePath)) {
1487
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1488
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1489
+ let targetColumn = board.columns.find(c => c.id === column);
1490
+ if (!targetColumn)
1491
+ targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
1492
+ if (!targetColumn) {
1493
+ return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
1494
+ }
1495
+ const results = [];
1496
+ let successCount = 0;
1497
+ let failureCount = 0;
1498
+ for (const taskId of tasks) {
1499
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
1500
+ const doc = (0, core_3.readTaskFile)(taskPath);
1501
+ if (!doc) {
1502
+ results.push({ taskId, success: false, error: 'Task not found' });
1503
+ failureCount++;
1504
+ continue;
1505
+ }
1506
+ doc.task.column = targetColumn.id;
1507
+ doc.task.updatedAt = new Date().toISOString();
1508
+ (0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
1509
+ results.push({ taskId, success: true });
1510
+ successCount++;
1511
+ }
1512
+ const output = { success: failureCount === 0, successCount, failureCount, results };
1513
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1514
+ }
1515
+ // V1: use board
950
1516
  const result = readBoard(filePath);
951
1517
  if ('error' in result) {
952
1518
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -1008,13 +1574,54 @@ async function mcpCommand(options) {
1008
1574
  }
1009
1575
  }, async ({ file, tasks, priority, tags, assignee }) => {
1010
1576
  const filePath = file || defaultFile;
1577
+ const isNull = (v) => v === null || v === 'null';
1578
+ // V2: update each task file
1579
+ if ((0, v2_detect_1.isV2)(filePath)) {
1580
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1581
+ const results = [];
1582
+ let successCount = 0;
1583
+ let failureCount = 0;
1584
+ for (const taskId of tasks) {
1585
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
1586
+ const doc = (0, core_3.readTaskFile)(taskPath);
1587
+ if (!doc) {
1588
+ results.push({ taskId, success: false, error: 'Task not found' });
1589
+ failureCount++;
1590
+ continue;
1591
+ }
1592
+ const t = doc.task;
1593
+ if (priority !== undefined) {
1594
+ if (isNull(priority))
1595
+ delete t.priority;
1596
+ else
1597
+ t.priority = priority;
1598
+ }
1599
+ if (tags !== undefined) {
1600
+ if (isNull(tags))
1601
+ delete t.tags;
1602
+ else
1603
+ t.tags = tags;
1604
+ }
1605
+ if (assignee !== undefined) {
1606
+ if (isNull(assignee))
1607
+ delete t.assignee;
1608
+ else
1609
+ t.assignee = assignee;
1610
+ }
1611
+ t.updatedAt = new Date().toISOString();
1612
+ (0, core_3.writeTaskFile)(taskPath, t, doc.body);
1613
+ results.push({ taskId, success: true });
1614
+ successCount++;
1615
+ }
1616
+ const output = { success: failureCount === 0, successCount, failureCount, results };
1617
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1618
+ }
1619
+ // V1: use board
1011
1620
  const result = readBoard(filePath);
1012
1621
  if ('error' in result) {
1013
1622
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1014
1623
  }
1015
1624
  let { board } = result;
1016
- // Helper to check for null or "null" string
1017
- const isNull = (v) => v === null || v === 'null';
1018
1625
  const patch = {};
1019
1626
  if (priority !== undefined)
1020
1627
  patch.priority = isNull(priority) ? undefined : priority;
@@ -1047,6 +1654,33 @@ async function mcpCommand(options) {
1047
1654
  }
1048
1655
  }, async ({ file, tasks }) => {
1049
1656
  const filePath = file || defaultFile;
1657
+ // V2: delete task files
1658
+ if ((0, v2_detect_1.isV2)(filePath)) {
1659
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1660
+ const results = [];
1661
+ let successCount = 0;
1662
+ let failureCount = 0;
1663
+ for (const taskId of tasks) {
1664
+ const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
1665
+ if (!found) {
1666
+ results.push({ taskId, success: false, error: 'Task not found' });
1667
+ failureCount++;
1668
+ continue;
1669
+ }
1670
+ try {
1671
+ fs.unlinkSync(found.filePath);
1672
+ results.push({ taskId, success: true });
1673
+ successCount++;
1674
+ }
1675
+ catch (e) {
1676
+ results.push({ taskId, success: false, error: e.message });
1677
+ failureCount++;
1678
+ }
1679
+ }
1680
+ const output = { success: failureCount === 0, successCount, failureCount, results };
1681
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1682
+ }
1683
+ // V1: use board
1050
1684
  const result = readBoard(filePath);
1051
1685
  if ('error' in result) {
1052
1686
  return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
@@ -1077,6 +1711,39 @@ async function mcpCommand(options) {
1077
1711
  }
1078
1712
  }, async ({ file, tasks }) => {
1079
1713
  const filePath = file || defaultFile;
1714
+ // V2: move task files from board/ to logs/
1715
+ if ((0, v2_detect_1.isV2)(filePath)) {
1716
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
1717
+ const results = [];
1718
+ let successCount = 0;
1719
+ let failureCount = 0;
1720
+ for (const taskId of tasks) {
1721
+ const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
1722
+ const doc = (0, core_3.readTaskFile)(taskPath);
1723
+ if (!doc) {
1724
+ results.push({ taskId, success: false, error: 'Task not found' });
1725
+ failureCount++;
1726
+ continue;
1727
+ }
1728
+ try {
1729
+ const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(taskId));
1730
+ doc.task.completedAt = doc.task.completedAt || new Date().toISOString();
1731
+ delete doc.task.column;
1732
+ delete doc.task.position;
1733
+ (0, core_3.writeTaskFile)(logPath, doc.task, doc.body);
1734
+ fs.unlinkSync(taskPath);
1735
+ results.push({ taskId, success: true });
1736
+ successCount++;
1737
+ }
1738
+ catch (e) {
1739
+ results.push({ taskId, success: false, error: e.message });
1740
+ failureCount++;
1741
+ }
1742
+ }
1743
+ const output = { success: failureCount === 0, successCount, failureCount, results };
1744
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
1745
+ }
1746
+ // V1: use board
1080
1747
  const results = [];
1081
1748
  let successCount = 0;
1082
1749
  let failureCount = 0;
@@ -1176,6 +1843,47 @@ async function mcpCommand(options) {
1176
1843
  };
1177
1844
  });
1178
1845
  // ==========================================================================
1846
+ // TYPES
1847
+ // ==========================================================================
1848
+ server.registerTool('list_types', {
1849
+ title: 'List Types',
1850
+ description: 'List board strict mode and custom type configuration',
1851
+ inputSchema: {
1852
+ file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
1853
+ }
1854
+ }, async ({ file }) => {
1855
+ const filePath = file || defaultFile;
1856
+ if ((0, v2_detect_1.isV2)(filePath)) {
1857
+ try {
1858
+ const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
1859
+ const boardConfig = board;
1860
+ const output = {
1861
+ strict: boardConfig.strict === true,
1862
+ types: sanitizeTypesConfig(boardConfig.types),
1863
+ };
1864
+ return {
1865
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
1866
+ };
1867
+ }
1868
+ catch (e) {
1869
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
1870
+ }
1871
+ }
1872
+ const result = readBoard(filePath);
1873
+ if ('error' in result) {
1874
+ return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
1875
+ }
1876
+ const boardConfig = result.board;
1877
+ const strict = boardConfig.strict === true;
1878
+ const types = sanitizeTypesConfig(boardConfig.types);
1879
+ return {
1880
+ content: [{
1881
+ type: 'text',
1882
+ text: JSON.stringify({ strict, types }, null, 2)
1883
+ }]
1884
+ };
1885
+ });
1886
+ // ==========================================================================
1179
1887
  // RULES
1180
1888
  // ==========================================================================
1181
1889
  // List rules tool
@@ -1290,6 +1998,118 @@ async function mcpCommand(options) {
1290
1998
  content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
1291
1999
  };
1292
2000
  });
2001
+ // ==========================================================================
2002
+ // V2 NEW TOOLS: complete_task, search_logs, append_log
2003
+ // ==========================================================================
2004
+ // Complete task tool
2005
+ server.registerTool('complete_task', {
2006
+ title: 'Complete Task',
2007
+ description: 'Complete a task - in v2, moves task file from board/ to logs/ with completedAt timestamp. In v1, moves to done column.',
2008
+ inputSchema: {
2009
+ file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2010
+ task: zod_1.z.string().describe('Task ID to complete'),
2011
+ }
2012
+ }, async ({ file, task }) => {
2013
+ const filePath = file || defaultFile;
2014
+ try {
2015
+ const { completeCommand } = await Promise.resolve().then(() => __importStar(require('./complete')));
2016
+ const result = completeCommand({ file: filePath, task }, { log: () => { }, warn: () => { }, error: () => { }, info: () => { } });
2017
+ return {
2018
+ content: [{ type: 'text', text: `Task ${task} completed at ${result.completedAt}` }]
2019
+ };
2020
+ }
2021
+ catch (e) {
2022
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
2023
+ }
2024
+ });
2025
+ // Search logs tool
2026
+ server.registerTool('search_logs', {
2027
+ title: 'Search Logs',
2028
+ description: 'Search across completed task logs. Requires v2 per-task file architecture.',
2029
+ inputSchema: {
2030
+ file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2031
+ query: zod_1.z.string().optional().describe('Search query to match against log content'),
2032
+ recent: zod_1.z.boolean().optional().describe('List recently completed tasks'),
2033
+ task: zod_1.z.string().optional().describe('View a specific task log'),
2034
+ }
2035
+ }, async ({ file, query, recent, task: taskId }) => {
2036
+ const filePath = file || defaultFile;
2037
+ if (!(0, v2_detect_1.isV2)(filePath)) {
2038
+ return { content: [{ type: 'text', text: 'Error: search_logs requires v2 per-task file architecture. Run: brainfile migrate' }], isError: true };
2039
+ }
2040
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
2041
+ // View specific task log
2042
+ if (taskId) {
2043
+ const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
2044
+ if (!found) {
2045
+ return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
2046
+ }
2047
+ const output = {
2048
+ id: found.doc.task.id,
2049
+ title: found.doc.task.title,
2050
+ completedAt: found.doc.task.completedAt,
2051
+ description: (0, v2_detect_1.extractDescription)(found.doc.body),
2052
+ log: (0, v2_detect_1.extractLog)(found.doc.body),
2053
+ };
2054
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
2055
+ }
2056
+ // Search logs
2057
+ if (query) {
2058
+ const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
2059
+ const queryLower = query.toLowerCase();
2060
+ const matches = [];
2061
+ for (const doc of logDocs) {
2062
+ const task = doc.task;
2063
+ const desc = (0, v2_detect_1.extractDescription)(doc.body) || '';
2064
+ const log = (0, v2_detect_1.extractLog)(doc.body) || '';
2065
+ const fullText = [task.title, task.description || '', desc, log].join(' ').toLowerCase();
2066
+ if (fullText.includes(queryLower)) {
2067
+ matches.push({ id: task.id, title: task.title, completedAt: task.completedAt });
2068
+ }
2069
+ }
2070
+ return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
2071
+ }
2072
+ // Recent logs (default)
2073
+ const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
2074
+ logDocs.sort((a, b) => (b.task.completedAt || '').localeCompare(a.task.completedAt || ''));
2075
+ const recent20 = logDocs.slice(0, 20).map(doc => ({
2076
+ id: doc.task.id,
2077
+ title: doc.task.title,
2078
+ completedAt: doc.task.completedAt,
2079
+ }));
2080
+ return { content: [{ type: 'text', text: JSON.stringify({ logs: recent20, count: recent20.length }, null, 2) }] };
2081
+ });
2082
+ // Append log tool
2083
+ server.registerTool('append_log', {
2084
+ title: 'Append Log',
2085
+ description: 'Append a timestamped entry to a task log section. Works on both active tasks and completed logs. Requires v2 per-task file architecture.',
2086
+ inputSchema: {
2087
+ file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
2088
+ task: zod_1.z.string().describe('Task ID to append log to'),
2089
+ message: zod_1.z.string().describe('Log message to append'),
2090
+ agent: zod_1.z.string().optional().describe('Agent name for attribution'),
2091
+ }
2092
+ }, async ({ file, task: taskId, message, agent }) => {
2093
+ const filePath = file || defaultFile;
2094
+ if (!(0, v2_detect_1.isV2)(filePath)) {
2095
+ return { content: [{ type: 'text', text: 'Error: append_log requires v2 per-task file architecture. Run: brainfile migrate' }], isError: true };
2096
+ }
2097
+ const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
2098
+ const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
2099
+ if (!found) {
2100
+ return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
2101
+ }
2102
+ const { doc, filePath: taskFilePath } = found;
2103
+ const timestamp = new Date().toISOString();
2104
+ const agentPrefix = agent ? `[${agent}] ` : '';
2105
+ const entry = `- ${timestamp}: ${agentPrefix}${message}`;
2106
+ const existingDescription = (0, v2_detect_1.extractDescription)(doc.body);
2107
+ const existingLog = (0, v2_detect_1.extractLog)(doc.body) || '';
2108
+ const newLog = existingLog ? `${existingLog}\n${entry}` : entry;
2109
+ const newBody = (0, v2_detect_1.composeBody)(existingDescription, newLog);
2110
+ (0, core_3.writeTaskFile)(taskFilePath, doc.task, newBody);
2111
+ return { content: [{ type: 'text', text: `Log entry added to ${taskId}: ${entry}` }] };
2112
+ });
1293
2113
  // Connect via stdio
1294
2114
  const transport = new stdio_js_1.StdioServerTransport();
1295
2115
  await server.connect(transport);