From 6ff1c18ee2a4a4408c5cff3919db78b37d255ff2 Mon Sep 17 00:00:00 2001 From: Zhicheng Han <43314240+hanzckernel@users.noreply.github.com> Date: Mon, 11 May 2026 15:26:24 +0200 Subject: [PATCH] =?UTF-8?q?Kanban=EF=BC=9A=E8=A1=A5=E9=BD=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=93=8D=E4=BD=9C=E9=93=BE=E8=B7=AF=EF=BC=8C=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E8=83=BD=E5=8A=9B=E8=BE=B9=E7=95=8C=20(#615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [verified] fix(kanban): harden WUI parity bridge - Align board slug normalization with canonical underscore/lowercase/64-char rules - Validate malformed Kanban action bodies before CLI shell-out - Narrow task log no-log handling and expose phase-1 capabilities - Extend client/server regression coverage for parity actions * fix(kanban): guard archived task detail actions --------- Co-authored-by: ekko <152005280+EKKOLearnAI@users.noreply.github.com> --- packages/client/src/api/hermes/kanban.ts | 104 ++++++ .../hermes/kanban/KanbanTaskDrawer.vue | 9 +- packages/client/src/stores/hermes/kanban.ts | 79 ++++- .../server/src/controllers/hermes/kanban.ts | 325 ++++++++++++++---- packages/server/src/routes/hermes/kanban.ts | 7 + .../src/services/hermes/hermes-kanban.ts | 226 ++++++++++-- tests/client/kanban-api.test.ts | 36 ++ tests/client/kanban-store.test.ts | 78 ++++- tests/client/kanban-task-drawer.test.ts | 29 ++ tests/server/hermes-kanban-service.test.ts | 97 +++++- tests/server/kanban-controller.test.ts | 166 ++++++++- tests/server/kanban-routes.test.ts | 14 + 12 files changed, 1079 insertions(+), 91 deletions(-) diff --git a/packages/client/src/api/hermes/kanban.ts b/packages/client/src/api/hermes/kanban.ts index e058191..11fd40c 100644 --- a/packages/client/src/api/hermes/kanban.ts +++ b/packages/client/src/api/hermes/kanban.ts @@ -121,10 +121,29 @@ export interface KanbanBoardCreateRequest { switchCurrent?: boolean } +export interface KanbanCapabilityStatus { + key: string + status: 'supported' | 'partial' | 'missing' + reason?: string + canonicalRoute?: string + canonicalCommand?: string + requiresBoard: boolean +} + export interface KanbanCapabilities { source: 'hermes-cli' supports: Record missing: string[] + capabilities?: KanbanCapabilityStatus[] +} + +export interface KanbanTaskLog { + task_id: string + path: string | null + exists: boolean + size_bytes: number + content: string + truncated: boolean } export interface KanbanCreateRequest { @@ -146,6 +165,39 @@ export interface KanbanListOptions extends KanbanBoardOptions { includeArchived?: boolean } +export interface KanbanCommentCreateRequest { + body: string + author?: string +} + +export interface KanbanTaskLogOptions extends KanbanBoardOptions { + tail?: number +} + +export interface KanbanDiagnosticsOptions extends KanbanBoardOptions { + task?: string + severity?: 'warning' | 'error' | 'critical' +} + +export interface KanbanReclaimOptions extends KanbanBoardOptions { + reason?: string +} + +export interface KanbanReassignOptions extends KanbanBoardOptions { + reclaim?: boolean + reason?: string +} + +export interface KanbanSpecifyOptions extends KanbanBoardOptions { + author?: string +} + +export interface KanbanDispatchOptions extends KanbanBoardOptions { + dryRun?: boolean + max?: number + failureLimit?: number +} + function normalizedBoard(board?: string): string { const trimmed = board?.trim() return trimmed || 'default' @@ -240,6 +292,58 @@ export async function assignTask(taskId: string, profile: string, opts?: KanbanB }) } +export async function addComment(taskId: string, data: KanbanCommentCreateRequest, opts?: KanbanBoardOptions): Promise<{ ok: boolean; output?: string }> { + return request<{ ok: boolean; output?: string }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/comments`, boardParams(opts?.board)), { + method: 'POST', + body: JSON.stringify(data), + }) +} + +export async function getTaskLog(taskId: string, opts?: KanbanTaskLogOptions): Promise { + const params = boardParams(opts?.board) + if (opts?.tail !== undefined) params.set('tail', String(opts.tail)) + return request(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/log`, params)) +} + +export async function getDiagnostics(opts?: KanbanDiagnosticsOptions): Promise { + const params = boardParams(opts?.board) + if (opts?.task) params.set('task', opts.task) + if (opts?.severity) params.set('severity', opts.severity) + const res = await request<{ diagnostics: unknown[] }>(appendQuery('/api/hermes/kanban/diagnostics', params)) + return res.diagnostics +} + +export async function reclaimTask(taskId: string, opts?: KanbanReclaimOptions): Promise<{ ok: boolean; output?: string }> { + return request<{ ok: boolean; output?: string }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/reclaim`, boardParams(opts?.board)), { + method: 'POST', + body: JSON.stringify({ reason: opts?.reason }), + }) +} + +export async function reassignTask(taskId: string, profile: string, opts?: KanbanReassignOptions): Promise<{ ok: boolean; output?: string }> { + return request<{ ok: boolean; output?: string }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/reassign`, boardParams(opts?.board)), { + method: 'POST', + body: JSON.stringify({ profile, reclaim: opts?.reclaim, reason: opts?.reason }), + }) +} + +export async function specifyTask(taskId: string, opts?: KanbanSpecifyOptions): Promise { + const res = await request<{ results: unknown[] }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/specify`, boardParams(opts?.board)), { + method: 'POST', + body: JSON.stringify({ author: opts?.author }), + }) + return res.results +} + +export async function dispatch(opts?: KanbanDispatchOptions): Promise { + const params = boardParams(opts?.board) + const res = await request<{ result: unknown }>(appendQuery('/api/hermes/kanban/dispatch', params), { + method: 'POST', + body: JSON.stringify({ dryRun: opts?.dryRun, max: opts?.max, failureLimit: opts?.failureLimit }), + }) + return res.result +} + export async function getStats(opts?: KanbanBoardOptions): Promise { const res = await request<{ stats: KanbanStats }>(appendQuery('/api/hermes/kanban/stats', boardParams(opts?.board))) return res.stats diff --git a/packages/client/src/components/hermes/kanban/KanbanTaskDrawer.vue b/packages/client/src/components/hermes/kanban/KanbanTaskDrawer.vue index 855b34a..6033833 100644 --- a/packages/client/src/components/hermes/kanban/KanbanTaskDrawer.vue +++ b/packages/client/src/components/hermes/kanban/KanbanTaskDrawer.vue @@ -43,6 +43,11 @@ const localizedTaskStatus = computed(() => { return t(`kanban.columns.${detail.value.task.status}`, detail.value.task.status) }) +const canMutateTask = computed(() => { + const status = detail.value?.task.status + return status !== 'done' && status !== 'archived' +}) + const sessionResults = ref([]) const sessionLoading = ref(false) const showSessions = ref(false) @@ -243,8 +248,8 @@ async function handleAssign() {
{{ completionSummary }}
- -
+ +
{{ t('kanban.action.title') }}