@goodandready/dsh-kanban 0.2.2 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
28
+ <br><br>
29
+ 🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
@@ -286,3 +296,17 @@ deregistration.
286
296
  ## 📄 License
287
297
 
288
298
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
299
+
300
+ ## Public composition service
301
+
302
+ The server half exposes an optional dshKanban composition service. Its
303
+ createTask({ title, body, board, column, labels, owner, repo, issueNumber,
304
+ issueUrl, externalRef }) method validates the target board and column, stores
305
+ the task through the existing SQLite-backed path, and returns { ok, task,
306
+ taskId, alreadyExists }. A repeated externalRef returns the original card,
307
+ including after a restart.
308
+
309
+ This service only creates a card. It does not launch an agent, create a branch,
310
+ create a worktree, or accept credentials from a consumer. The canonical
311
+ consumer contract for task provisioning is
312
+ dsh-drives.task-provision.v1.
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
28
+ <br><br>
29
+ 🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
28
+ <br><br>
29
+ 🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
@@ -171,3 +181,14 @@ npm test
171
181
  ## 📄 开源许可证
172
182
 
173
183
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
184
+
185
+ ## 公共组合服务
186
+
187
+ 服务器端暴露可选的 dshKanban 组合服务。其
188
+ createTask({ title, body, board, column, labels, owner, repo, issueNumber,
189
+ issueUrl, externalRef }) 方法校验目标看板和列,通过现有的 SQLite 存储路径
190
+ 保存任务,并返回 { ok, task, taskId, alreadyExists }。重复的 externalRef
191
+ 会返回原有卡片,重启后仍然有效。
192
+
193
+ 该服务只创建卡片,不启动代理、不创建分支或 worktree,也不接受调用方传入的
194
+ 凭据。任务供应的规范消费者契约是 dsh-drives.task-provision.v1。
package/lib/board-tool.js CHANGED
@@ -367,3 +367,154 @@ export function boardCommentAddDefinition({ store }) {
367
367
  },
368
368
  }
369
369
  }
370
+
371
+ /** Определение board_decompose (#248) */
372
+ export function boardDecomposeDefinition({ store, gitea }) {
373
+ return {
374
+ name: 'board_decompose',
375
+ description: 'Decompose a task into child subtasks, converting the task into an Epic with linked progress tracking.',
376
+ parameters: {
377
+ subtasks: {
378
+ type: 'array',
379
+ items: { type: 'string' },
380
+ description: 'List of child subtask titles to create.',
381
+ required: true,
382
+ },
383
+ create_gitea_issues: {
384
+ type: 'boolean',
385
+ description: 'Whether to create linked Gitea issues for each subtask if repository is linked.',
386
+ },
387
+ task: {
388
+ type: 'string',
389
+ description: 'Task reference (`repo#number` or card id). Required if this session carries multiple tasks.',
390
+ },
391
+ },
392
+ output: {
393
+ schema: { type: 'string', description: 'Status and list of created subtasks.' },
394
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
395
+ },
396
+ async execute({ subtasks, create_gitea_issues, task: ref }, exec) {
397
+ const picked = pickTask(store, exec, ref)
398
+ if (picked.error !== undefined) return picked.error
399
+ const task = picked.task
400
+ if (!Array.isArray(subtasks) || subtasks.length === 0) {
401
+ return 'At least one subtask title is required in `subtasks`.'
402
+ }
403
+
404
+ store.updateTask(task.id, { isEpic: true })
405
+ const created = []
406
+ const giteaClient = typeof gitea === 'function' ? gitea() : gitea
407
+ const createGitea = create_gitea_issues === true && Boolean(task.owner && task.repo && giteaClient)
408
+
409
+ for (const item of subtasks) {
410
+ const title = (typeof item === 'string' ? item : item?.title || '').trim()
411
+ if (!title) continue
412
+ const body = typeof item === 'object' && item?.body ? String(item.body).trim() : ''
413
+ let issueNumber = null
414
+ let issueUrl = null
415
+
416
+ if (createGitea) {
417
+ try {
418
+ const issueBody = body ? `${body}\n\nParent epic: #${task.issueNumber || task.id}` : `Parent epic: #${task.issueNumber || task.id}`
419
+ const giteaIssue = await giteaClient.createIssue({
420
+ owner: task.owner,
421
+ repo: task.repo,
422
+ title,
423
+ body: issueBody,
424
+ })
425
+ if (giteaIssue?.number) {
426
+ issueNumber = giteaIssue.number
427
+ issueUrl = giteaIssue.html_url || null
428
+ }
429
+ } catch { /* gitea issue creation best-effort */ }
430
+ }
431
+
432
+ const sub = store.createTask({
433
+ board: task.board || 'main',
434
+ column: 'backlog',
435
+ title,
436
+ body,
437
+ parentId: task.id,
438
+ owner: task.owner || null,
439
+ repo: task.repo || null,
440
+ issueNumber,
441
+ issueUrl,
442
+ workspaceId: task.workspaceId || '',
443
+ })
444
+ created.push(sub)
445
+ }
446
+
447
+ store.addTransition({
448
+ taskId: task.id,
449
+ fromCol: task.column,
450
+ toCol: task.column,
451
+ source: 'tool',
452
+ detail: `agent decomposed task into ${created.length} subtasks`,
453
+ })
454
+
455
+ return `Decomposed task "${task.title}" into ${created.length} subtasks:\n`
456
+ + created.map((s, i) => `${i + 1}. [${s.id.slice(0, 8)}] ${s.title}${s.issueNumber ? ` (#${s.issueNumber})` : ''}`).join('\n')
457
+ },
458
+ }
459
+ }
460
+
461
+ /** Определение board_checklist_item (#248) */
462
+ export function boardChecklistItemDefinition({ store }) {
463
+ return {
464
+ name: 'board_checklist_item',
465
+ description: 'Toggle or check a specific DoD checklist item by text or index without replacing the entire list.',
466
+ parameters: {
467
+ text: {
468
+ type: 'string',
469
+ description: 'Text substring matching the checklist item to toggle.',
470
+ },
471
+ index: {
472
+ type: 'number',
473
+ description: 'Zero-based index of the checklist item to toggle.',
474
+ },
475
+ done: {
476
+ type: 'boolean',
477
+ description: 'Explicit completion state (true to mark done, false to unmark). If omitted, toggles state.',
478
+ },
479
+ task: {
480
+ type: 'string',
481
+ description: 'Task reference (`repo#number` or card id). Required if this session carries multiple tasks.',
482
+ },
483
+ },
484
+ output: {
485
+ schema: { type: 'string', description: 'Status confirmation.' },
486
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
487
+ },
488
+ async execute({ text, index, done, task: ref }, exec) {
489
+ const picked = pickTask(store, exec, ref)
490
+ if (picked.error !== undefined) return picked.error
491
+ const task = picked.task
492
+ const checklist = Array.isArray(task.checklist) ? [...task.checklist] : []
493
+ let targetIndex = -1
494
+
495
+ if (typeof index === 'number' && index >= 0 && index < checklist.length) {
496
+ targetIndex = index
497
+ } else if (typeof text === 'string' && text.trim()) {
498
+ const q = text.trim().toLowerCase()
499
+ targetIndex = checklist.findIndex((item) => {
500
+ const t = typeof item === 'string' ? item : item?.text
501
+ return typeof t === 'string' && t.toLowerCase().includes(q)
502
+ })
503
+ }
504
+
505
+ if (targetIndex === -1) {
506
+ return `Checklist item matching index=${index} / text="${text}" not found.`
507
+ }
508
+
509
+ const item = checklist[targetIndex]
510
+ const oldDone = typeof item === 'object' && item !== null ? Boolean(item.done) : false
511
+ const newDone = done !== undefined ? Boolean(done) : !oldDone
512
+ const title = typeof item === 'string' ? item : (item?.text || '')
513
+ checklist[targetIndex] = { text: title, done: newDone }
514
+
515
+ store.updateChecklist(task.id, checklist)
516
+ return `Checklist item #${targetIndex + 1} "${title}" marked as ${newDone ? 'DONE' : 'PENDING'}.`
517
+ },
518
+ }
519
+ }
520
+