@alpacachen/dsh-kanban 1.3.7 → 1.5.0

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
@@ -27,7 +27,7 @@ You can take over at any time—drag a card, refine a note, reorder a list—and
27
27
 
28
28
  | 💬 Plan naturally | 🧭 Stay in control | ⚡ Start instantly |
29
29
  | --- | --- | --- |
30
- | Manage the board with 14 built-in `kanban_*` tools. | Edit, drag, prioritize, label, and filter from the UI. | Install the prebuilt plugin—no frontend setup required. |
30
+ | Manage the board with 15 built-in `kanban_*` tools. | Edit, comment, drag, prioritize, label, and filter from the UI. | Install the prebuilt plugin—no frontend setup required. |
31
31
  | **🗂️ Keep work separate** | **🌓 Feel at home** | **🤝 Share one source of truth** |
32
32
  | Every DSH workspace gets its own board. | Follows your DSH language, theme, and dark mode. | You and the agent always see the same tasks and status. |
33
33
 
@@ -67,8 +67,9 @@ The cards appear directly on your board. Continue in conversation, or switch to
67
67
 
68
68
  - Create, edit, delete, and drag cards between lists
69
69
  - Add notes, labels, and P0 / P1 / P2 priorities
70
+ - Exchange persistent card comments with the agent and see comment counts at a glance
70
71
  - Open a card's activity timeline to see when it changed, what changed, and whether you or the agent made the change
71
- - Track card creation, moves, title and note edits, label changes, and priority changes
72
+ - Track card creation, comments, moves, title and note edits, label changes, and priority changes
72
73
  - Send any card to the agent in your current conversation or a new one
73
74
  - Create, rename, reorder, and remove workflow lists
74
75
  - Create color-coded labels for fast visual scanning
@@ -85,7 +86,7 @@ The plugin automatically gives the agent these tools in every workspace:
85
86
  | Area | Tools |
86
87
  | --- | --- |
87
88
  | **Board** | `kanban_get` |
88
- | **Cards** | `kanban_get_card`, `kanban_add_card`, `kanban_update_card`, `kanban_delete_card`, `kanban_move_card` |
89
+ | **Cards** | `kanban_get_card`, `kanban_add_comment`, `kanban_add_card`, `kanban_update_card`, `kanban_delete_card`, `kanban_move_card` |
89
90
  | **Lists** | `kanban_add_column`, `kanban_rename_column`, `kanban_delete_column`, `kanban_move_column` |
90
91
  | **Labels** | `kanban_get_label`, `kanban_add_label`, `kanban_update_label`, `kanban_delete_label` |
91
92
 
package/README.zh.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  | 💬 自然对话 | 🧭 随时掌控 | ⚡ 开箱即用 |
29
29
  | --- | --- | --- |
30
- | 通过 14 个 `kanban_*` 工具直接管理任务。 | 在界面中编辑、拖拽、标记和筛选卡片。 | 浏览器端已预构建,无需配置前端环境。 |
30
+ | 通过 15 个 `kanban_*` 工具直接管理任务。 | 在界面中编辑、评论、拖拽、标记和筛选卡片。 | 浏览器端已预构建,无需配置前端环境。 |
31
31
  | **🗂️ 项目隔离** | **🌓 原生体验** | **🤝 实时协作** |
32
32
  | 每个 DSH 工作区都有自己的独立看板。 | 自动跟随 DSH 的语言、主题与暗色模式。 | 你做的调整,Agent 立即可见;反之亦然。 |
33
33
 
@@ -67,8 +67,9 @@ dsh --profile web --dump-config
67
67
 
68
68
  - 创建、编辑、删除卡片,并将卡片拖到其他列表
69
69
  - 为卡片补充备注、标签和 P0 / P1 / P2 优先级
70
+ - 与 Agent 在卡片下持续评论,并在看板上快速查看评论数量
70
71
  - 打开卡片活动时间线,查看变更时间、具体内容,以及操作来自你还是 Agent
71
- - 追踪卡片创建、移动、标题与备注编辑、标签变化和优先级变化
72
+ - 追踪卡片创建、评论、移动、标题与备注编辑、标签变化和优先级变化
72
73
  - 从任意卡片发起对话,在当前会话或新会话中让 Agent 接着处理
73
74
  - 创建、重命名、排序和删除工作流列表
74
75
  - 创建彩色标签,让不同任务一眼可辨
@@ -85,7 +86,7 @@ dsh --profile web --dump-config
85
86
  | 范围 | 工具 |
86
87
  | --- | --- |
87
88
  | **看板** | `kanban_get` |
88
- | **卡片** | `kanban_get_card`、`kanban_add_card`、`kanban_update_card`、`kanban_delete_card`、`kanban_move_card` |
89
+ | **卡片** | `kanban_get_card`、`kanban_add_comment`、`kanban_add_card`、`kanban_update_card`、`kanban_delete_card`、`kanban_move_card` |
89
90
  | **列表** | `kanban_add_column`、`kanban_rename_column`、`kanban_delete_column`、`kanban_move_column` |
90
91
  | **标签** | `kanban_get_label`、`kanban_add_label`、`kanban_update_label`、`kanban_delete_label` |
91
92
 
package/index.js CHANGED
@@ -7,14 +7,15 @@
7
7
  * 职责:
8
8
  * - 按工作区(项目)隔离:boards 以 workspaceId 为键,每个工作区一块独立看板
9
9
  * - 磁盘持久化:经 ctx.fs 写入 <workspace.path>/.dsh-kanban.json
10
- * - 模型工具:经 ctx.tools.register 注册 14 个 kanban_* 工具
10
+ * - 模型工具:经 ctx.tools.register 注册 15 个 kanban_* 工具
11
11
  * - 浏览器数据层:经 ctx.get('webServer') 注册 /api/kanban 前缀路由
12
12
  *
13
13
  * 数据模型(每工作区,磁盘文件带 schemaVersion):
14
- * schemaVersion: 2
14
+ * schemaVersion: 3
15
15
  * columns: [{ id, title }]
16
16
  * labels: [{ name, color }] —— 标签与颜色绑定,name 为唯一键
17
- * cards: [{ id, columnId, title, note, label, priority, createdAt, createdBy }]
17
+ * cards: [{ id, columnId, title, note, label, priority, createdAt, createdBy,
18
+ * comments: [{ id, content, source, createdAt }] }]
18
19
  * activities: [{ id, ts, cardId, type, source, field?, from?, to?, meta? }] —— 追加式活动日志
19
20
  *
20
21
  * 数据安全:
@@ -44,12 +45,21 @@ export const inject = ['tools']
44
45
  // 旧文件在首次打开时自动沿迁移链升级,升级前原文件先备份。
45
46
  // ---------------------------------------------------------------------------
46
47
 
47
- export const SCHEMA_VERSION = 2
48
+ export const SCHEMA_VERSION = 3
48
49
  export const LEGACY_VERSION = 0
49
50
 
50
51
  // 活动日志:追加式只读记录,随看板一起落盘;超过上限丢弃最旧事件,防止日志无界增长。
51
52
  const ACTIVITY_LIMIT = 5000
52
53
 
54
+ // 输入长度上限:与工具 schema 描述、客户端编辑器(CardDialog)保持一致。
55
+ // 超长输入在写入前被截断,并通过看板 warnings 通道发出一次性警告(见 clampText),
56
+ // 因此 agent 与用户都能感知到内容被裁剪,而不是静默丢失。
57
+ export const TITLE_LIMIT = 120
58
+ export const NOTE_LIMIT = 2000
59
+ export const LABEL_LIMIT = 20
60
+ export const COLUMN_TITLE_LIMIT = 40
61
+ export const COMMENT_LIMIT = 2000
62
+
53
63
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
54
64
  const strField = (v, fb) => (typeof v === 'string' && v ? v : fb)
55
65
  const normColor = (v) => (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : '#94a3b8')
@@ -104,6 +114,20 @@ export const MIGRATIONS = {
104
114
  activities: [],
105
115
  }
106
116
  },
117
+ 2: (data) => {
118
+ // v2 → v3:卡片新增评论数组;历史卡片默认没有评论
119
+ const src = isObj(data) ? data : {}
120
+ const pick = (arr) => (Array.isArray(arr) ? arr : [])
121
+ return {
122
+ schemaVersion: 3,
123
+ columns: pick(src.columns),
124
+ labels: pick(src.labels),
125
+ cards: pick(src.cards)
126
+ .map((c) => (isObj(c) ? { ...c, comments: [] } : null))
127
+ .filter(Boolean),
128
+ activities: pick(src.activities),
129
+ }
130
+ },
107
131
  }
108
132
 
109
133
  /**
@@ -177,8 +201,9 @@ export function validateBoard(data) {
177
201
 
178
202
  if (Array.isArray(data.cards)) {
179
203
  const seen = new Set()
204
+ const commentIds = new Set()
180
205
  for (const c of data.cards) {
181
- if (!isObj(c) || typeof c.id !== 'string' || !c.id || typeof c.columnId !== 'string' || typeof c.title !== 'string') {
206
+ if (!isObj(c) || typeof c.id !== 'string' || !c.id || typeof c.columnId !== 'string' || typeof c.title !== 'string' || !Array.isArray(c.comments)) {
182
207
  errors.push('cards contain an invalid entry')
183
208
  break
184
209
  }
@@ -189,6 +214,17 @@ export function validateBoard(data) {
189
214
  if (!columnIds.has(c.columnId)) errors.push('card references missing column: ' + c.id)
190
215
  if (c.label != null && !labelNames.has(c.label)) errors.push('card references missing label: ' + c.id)
191
216
  if (c.priority != null && !['high', 'medium', 'low'].includes(c.priority)) errors.push('card has invalid priority: ' + c.id)
217
+ for (const comment of c.comments) {
218
+ if (!isObj(comment) || typeof comment.id !== 'string' || !comment.id || typeof comment.content !== 'string' || !comment.content.trim() || comment.content.length > 2000 || !['human', 'agent'].includes(comment.source) || typeof comment.createdAt !== 'string' || !comment.createdAt) {
219
+ errors.push('card comments contain an invalid entry: ' + c.id)
220
+ break
221
+ }
222
+ if (commentIds.has(comment.id)) {
223
+ errors.push('duplicate comment id: ' + comment.id)
224
+ break
225
+ }
226
+ commentIds.add(comment.id)
227
+ }
192
228
  seen.add(c.id)
193
229
  }
194
230
  }
@@ -444,7 +480,10 @@ export function apply(ctx) {
444
480
  }
445
481
  }
446
482
  for (const col of board.columns) bumpSeq(col.id)
447
- for (const card of board.cards) bumpSeq(card.id)
483
+ for (const card of board.cards) {
484
+ bumpSeq(card.id)
485
+ for (const comment of card.comments) bumpSeq(comment.id)
486
+ }
448
487
  for (const act of board.activities) bumpSeq(act.id)
449
488
  if (board.columns.length === 0) {
450
489
  for (const title of DEFAULT_COLUMNS) board.columns.push({ id: nextId('c'), title })
@@ -496,6 +535,16 @@ export function apply(ctx) {
496
535
 
497
536
  // ---- 校验 / 查找 / 序列化 ----
498
537
  const str = (v, fb) => (typeof v === 'string' ? v : fb)
538
+
539
+ // 截断辅助:把输入裁到 limit 以内;一旦实际发生截断,就经看板 warnings 通道
540
+ // 发出一次性警告(同时写入宿主日志),避免内容被静默丢弃。
541
+ const clampText = (value, limit, field, board) => {
542
+ const s = str(value, '')
543
+ if (s.length <= limit) return s
544
+ warn(board, field + ' truncated to ' + limit + ' characters')
545
+ return s.slice(0, limit)
546
+ }
547
+
499
548
  const PRIORITIES = ['high', 'medium', 'low']
500
549
  const normPriority = (v) => (typeof v === 'string' && PRIORITIES.includes(v) ? v : undefined)
501
550
  const normColor = (v) => (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : undefined)
@@ -515,6 +564,14 @@ export function apply(ctx) {
515
564
  priority: c.priority ?? null,
516
565
  createdAt: typeof c.createdAt === 'string' ? c.createdAt : null,
517
566
  createdBy: typeof c.createdBy === 'string' ? c.createdBy : null,
567
+ comments: Array.isArray(c.comments)
568
+ ? c.comments.map((comment) => ({
569
+ id: comment.id,
570
+ content: comment.content,
571
+ source: comment.source,
572
+ createdAt: comment.createdAt,
573
+ }))
574
+ : [],
518
575
  })),
519
576
  activities: Array.isArray(b.activities) ? b.activities.map((a) => ({ ...a })) : [],
520
577
  })
@@ -531,6 +588,7 @@ export function apply(ctx) {
531
588
  title: c.title,
532
589
  label: c.label ?? null,
533
590
  priority: c.priority ?? null,
591
+ commentCount: c.comments.length,
534
592
  })),
535
593
  })
536
594
 
@@ -572,17 +630,18 @@ export function apply(ctx) {
572
630
  case 'addCard': {
573
631
  const col = findColumn(board, str(a.columnId, '')) || board.columns[0]
574
632
  if (!col) return result({ error: 'No list available' })
575
- const label = typeof a.label === 'string' ? a.label.slice(0, 20) : undefined
633
+ const label = typeof a.label === 'string' ? clampText(a.label, LABEL_LIMIT, 'Label', board) : undefined
576
634
  if (label && !findLabel(board, label)) return result({ error: 'Label not found: ' + label })
577
635
  const card = {
578
636
  id: nextId('k'),
579
637
  columnId: col.id,
580
- title: str(a.title, '').slice(0, 120) || 'Untitled card',
581
- note: str(a.note, '').slice(0, 500),
638
+ title: clampText(a.title, TITLE_LIMIT, 'Title', board) || 'Untitled card',
639
+ note: clampText(a.note, NOTE_LIMIT, 'Note', board),
582
640
  label,
583
641
  priority: normPriority(a.priority),
584
642
  createdAt: new Date().toISOString(),
585
643
  createdBy: actor,
644
+ comments: [],
586
645
  }
587
646
  board.cards.push(card)
588
647
  record(board, {
@@ -603,7 +662,7 @@ export function apply(ctx) {
603
662
  case 'updateCard': {
604
663
  const card = findCard(board, str(a.id, ''))
605
664
  if (card) {
606
- const nextLabel = typeof a.label === 'string' ? a.label.slice(0, 20) : undefined
665
+ const nextLabel = typeof a.label === 'string' ? clampText(a.label, LABEL_LIMIT, 'Label', board) : undefined
607
666
  if (nextLabel && !findLabel(board, nextLabel)) return result({ error: 'Label not found: ' + nextLabel })
608
667
  const before = {
609
668
  title: card.title,
@@ -611,8 +670,8 @@ export function apply(ctx) {
611
670
  label: card.label ?? null,
612
671
  priority: card.priority ?? null,
613
672
  }
614
- if (typeof a.title === 'string') card.title = a.title.slice(0, 120) || card.title
615
- if (typeof a.note === 'string') card.note = a.note.slice(0, 500)
673
+ if (typeof a.title === 'string') card.title = clampText(a.title, TITLE_LIMIT, 'Title', board) || card.title
674
+ if (typeof a.note === 'string') card.note = clampText(a.note, NOTE_LIMIT, 'Note', board)
616
675
  if (typeof a.label === 'string') card.label = nextLabel || undefined
617
676
  if (typeof a.priority === 'string') card.priority = normPriority(a.priority)
618
677
  const after = {
@@ -640,6 +699,24 @@ export function apply(ctx) {
640
699
  : result({ error: 'Card not found: ' + str(a.id, '') })
641
700
  }
642
701
 
702
+ case 'addComment': {
703
+ const card = findCard(board, str(a.id, ''))
704
+ if (!card) return result({ error: 'Card not found: ' + str(a.id, '') })
705
+ const content = str(a.content, '').trim()
706
+ if (!content) return result({ error: 'Comment content required' })
707
+ if (content.length > COMMENT_LIMIT) return result({ error: 'Comment exceeds ' + COMMENT_LIMIT + ' characters' })
708
+ const comment = {
709
+ id: nextId('m'),
710
+ content,
711
+ source: actor,
712
+ createdAt: new Date().toISOString(),
713
+ }
714
+ card.comments.push(comment)
715
+ record(board, { cardId: card.id, type: 'card_comment_added', source: actor, meta: { title: card.title } })
716
+ await save(workspace, session)
717
+ return result({ message: 'Comment added' })
718
+ }
719
+
643
720
  case 'deleteCard': {
644
721
  const id = str(a.id, '')
645
722
  const card = findCard(board, id)
@@ -673,7 +750,7 @@ export function apply(ctx) {
673
750
  }
674
751
 
675
752
  case 'addColumn': {
676
- const title = str(a.title, '').slice(0, 40) || 'New list'
753
+ const title = clampText(a.title, COLUMN_TITLE_LIMIT, 'List title', board) || 'New list'
677
754
  board.columns.push({ id: nextId('c'), title })
678
755
  record(board, { cardId: null, type: 'column_added', source: actor, meta: { column: title } })
679
756
  await save(workspace, session)
@@ -684,7 +761,7 @@ export function apply(ctx) {
684
761
  const col = findColumn(board, str(a.id, ''))
685
762
  if (col && typeof a.title === 'string') {
686
763
  const before = col.title
687
- col.title = a.title.slice(0, 40) || col.title
764
+ col.title = clampText(a.title, COLUMN_TITLE_LIMIT, 'List title', board) || col.title
688
765
  if (col.title !== before) {
689
766
  record(board, { cardId: null, type: 'column_renamed', source: actor, field: 'title', from: before, to: col.title, meta: { column: col.title } })
690
767
  }
@@ -726,7 +803,7 @@ export function apply(ctx) {
726
803
  }
727
804
 
728
805
  case 'addLabel': {
729
- const name = str(a.name, '').slice(0, 20)
806
+ const name = clampText(a.name, LABEL_LIMIT, 'Label name', board)
730
807
  if (!name) return result({ error: 'Label name required' })
731
808
  if (findLabel(board, name)) return result({ error: 'Label already exists' })
732
809
  board.labels.push({ name, color: normColor(a.color) || '#94a3b8' })
@@ -739,7 +816,7 @@ export function apply(ctx) {
739
816
  const name = str(a.name, '')
740
817
  const label = findLabel(board, name)
741
818
  if (!label) return result({ error: 'Label not found' })
742
- const newName = str(a.newName, '').slice(0, 20)
819
+ const newName = clampText(a.newName, LABEL_LIMIT, 'Label name', board)
743
820
  if (newName && newName !== name) {
744
821
  if (findLabel(board, newName)) return result({ error: 'Label name already exists' })
745
822
  label.name = newName
@@ -1033,7 +1110,8 @@ export function apply(ctx) {
1033
1110
  ' - [' + card.id + '] ' +
1034
1111
  (card.priority ? '[' + card.priority + '] ' : '') +
1035
1112
  (card.label ? '[' + card.label + '] ' : '') +
1036
- card.title,
1113
+ card.title +
1114
+ (card.commentCount ? ' (' + card.commentCount + ' comments)' : ''),
1037
1115
  )
1038
1116
  }
1039
1117
  }
@@ -1054,7 +1132,7 @@ export function apply(ctx) {
1054
1132
  },
1055
1133
  {
1056
1134
  name: 'kanban_get_card',
1057
- description: "Read one card's full details (title, note, label, priority) by id from the current project (workspace) board. Use kanban_get first to discover card ids, then this tool to read a card's complete note and fields.",
1135
+ description: "Read one card's full details (title, note, label, priority and comments) by id from the current project (workspace) board. Use kanban_get first to discover card ids, then this tool to read the complete card and comment history.",
1058
1136
  parameters: {
1059
1137
  type: 'object',
1060
1138
  properties: {
@@ -1086,6 +1164,12 @@ export function apply(ctx) {
1086
1164
  if (c.priority) lines.push('Priority: ' + c.priority)
1087
1165
  if (c.label) lines.push('Label: ' + c.label)
1088
1166
  if (c.note) lines.push('Note: ' + c.note)
1167
+ if (Array.isArray(c.comments) && c.comments.length > 0) {
1168
+ lines.push('Comments:')
1169
+ for (const comment of c.comments) {
1170
+ lines.push(' - [' + comment.createdAt + '] ' + comment.source + ': ' + comment.content)
1171
+ }
1172
+ }
1089
1173
  }
1090
1174
  return [{ type: 'text', text: lines.join('\n') }]
1091
1175
  },
@@ -1099,16 +1183,32 @@ export function apply(ctx) {
1099
1183
  return { ok: true, message: 'Card ' + String(args.id || '') + ' details', card: r.card, warnings: Array.isArray(r.warnings) ? r.warnings : [] }
1100
1184
  },
1101
1185
  },
1186
+ {
1187
+ name: 'kanban_add_comment',
1188
+ description: 'Add a comment to a card in the current project (workspace). Use it to leave progress updates, decisions, questions, or review feedback.',
1189
+ parameters: {
1190
+ type: 'object',
1191
+ properties: {
1192
+ id: { type: 'string', description: 'Card id' },
1193
+ content: { type: 'string', description: 'Comment text (1-2000 characters)' },
1194
+ },
1195
+ required: ['id', 'content'],
1196
+ },
1197
+ output: output(renderBoard),
1198
+ async execute(args, exec) {
1199
+ return runTool('addComment', args, exec)
1200
+ },
1201
+ },
1102
1202
  {
1103
1203
  name: 'kanban_add_card',
1104
1204
  description: 'Add a card to the current project (workspace) kanban board. Write feature breakdowns and plans to the board: one card per task step.',
1105
1205
  parameters: {
1106
1206
  type: 'object',
1107
1207
  properties: {
1108
- title: { type: 'string', description: 'Card title (task name, concise and actionable)' },
1208
+ title: { type: 'string', description: `Card title (task name, concise and actionable; max ${TITLE_LIMIT} chars)` },
1109
1209
  columnId: { type: 'string', description: 'Target list id; defaults to the first list (Todo)' },
1110
- note: { type: 'string', description: 'Note: background, acceptance criteria or breakdown details (optional)' },
1111
- label: { type: 'string', description: 'Label name (optional): e.g. New Feature, bug, Feedback' },
1210
+ note: { type: 'string', description: `Note: background, acceptance criteria or breakdown details (optional; max ${NOTE_LIMIT} chars)` },
1211
+ label: { type: 'string', description: `Label name (optional; max ${LABEL_LIMIT} chars): e.g. New Feature, bug, Feedback` },
1112
1212
  priority: { type: 'string', enum: ['high', 'medium', 'low'], description: 'Priority (optional): high=P0 / medium=P1 / low=P2' },
1113
1213
  },
1114
1214
  required: ['title'],
@@ -1125,9 +1225,9 @@ export function apply(ctx) {
1125
1225
  type: 'object',
1126
1226
  properties: {
1127
1227
  id: { type: 'string', description: 'Card id' },
1128
- title: { type: 'string', description: 'New title (optional)' },
1129
- note: { type: 'string', description: 'New note (optional)' },
1130
- label: { type: 'string', description: 'New label name (optional); pass empty string to clear' },
1228
+ title: { type: 'string', description: `New title (optional; max ${TITLE_LIMIT} chars)` },
1229
+ note: { type: 'string', description: `New note (optional; max ${NOTE_LIMIT} chars)` },
1230
+ label: { type: 'string', description: `New label name (optional; max ${LABEL_LIMIT} chars); pass empty string to clear` },
1131
1231
  priority: { type: 'string', enum: ['high', 'medium', 'low', ''], description: 'New priority (optional): high=P0 / medium=P1 / low=P2; pass empty string to clear' },
1132
1232
  },
1133
1233
  required: ['id'],
@@ -1172,7 +1272,7 @@ export function apply(ctx) {
1172
1272
  description: 'Add a list (column) to the board. Use when a new workflow stage (e.g. Review, Blocked) is needed.',
1173
1273
  parameters: {
1174
1274
  type: 'object',
1175
- properties: { title: { type: 'string', description: 'List name' } },
1275
+ properties: { title: { type: 'string', description: `List name (max ${COLUMN_TITLE_LIMIT} chars)` } },
1176
1276
  required: ['title'],
1177
1277
  },
1178
1278
  output: output(renderBoard),
@@ -1187,7 +1287,7 @@ export function apply(ctx) {
1187
1287
  type: 'object',
1188
1288
  properties: {
1189
1289
  id: { type: 'string', description: 'List id' },
1190
- title: { type: 'string', description: 'New name' },
1290
+ title: { type: 'string', description: `New name (max ${COLUMN_TITLE_LIMIT} chars)` },
1191
1291
  },
1192
1292
  required: ['id', 'title'],
1193
1293
  },
@@ -1231,7 +1331,7 @@ export function apply(ctx) {
1231
1331
  parameters: {
1232
1332
  type: 'object',
1233
1333
  properties: {
1234
- name: { type: 'string', description: 'Label name (unique, e.g. Urgent, Refactor)' },
1334
+ name: { type: 'string', description: `Label name (unique, max ${LABEL_LIMIT} chars; e.g. Urgent, Refactor)` },
1235
1335
  color: { type: 'string', description: 'Label color (optional): #rrggbb hex; defaults to gray' },
1236
1336
  },
1237
1337
  required: ['name'],
@@ -1248,7 +1348,7 @@ export function apply(ctx) {
1248
1348
  type: 'object',
1249
1349
  properties: {
1250
1350
  name: { type: 'string', description: 'Current label name' },
1251
- newName: { type: 'string', description: 'New name (optional)' },
1351
+ newName: { type: 'string', description: `New name (optional; max ${LABEL_LIMIT} chars)` },
1252
1352
  color: { type: 'string', description: 'New color (optional): #rrggbb hex' },
1253
1353
  },
1254
1354
  required: ['name'],