diff --git a/packages/client/src/api/hermes/kanban.ts b/packages/client/src/api/hermes/kanban.ts index e058191b..11fd40c0 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 855b34a4..60338330 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') }}