2026-05-08 11:32:47 +08:00
import { execFile } from 'child_process'
import { promisify } from 'util'
import { logger } from '../logger'
const execFileAsync = promisify ( execFile )
const execOpts = { windowsHide : true }
2026-05-11 15:26:24 +02:00
const BOARD_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
const NO_WORKER_LOG_PATTERNS = [
/^\(no log for [^)]+?\s+—\s+task may not have spawned yet\)$/i ,
/^no worker log(?: for [^\n]+)?$/i ,
]
2026-05-08 11:32:47 +08:00
function resolveHermesBin ( ) : string {
const envBin = process . env . HERMES_BIN ? . trim ( )
if ( envBin ) return envBin
return 'hermes'
}
const HERMES_BIN = resolveHermesBin ( )
2026-05-10 13:58:44 +02:00
export function normalizeBoardSlug ( board? : string | null ) : string {
2026-05-11 15:26:24 +02:00
if ( board === undefined || board === null ) return 'default'
const trimmed = board . trim ( ) . toLowerCase ( )
if ( ! trimmed ) throw new Error ( 'Invalid kanban board slug' )
2026-05-10 13:58:44 +02:00
if ( ! BOARD_SLUG_RE . test ( trimmed ) ) {
throw new Error ( 'Invalid kanban board slug' )
}
return trimmed
}
function boardArgs ( board? : string | null ) : string [ ] {
return [ 'kanban' , '--board' , normalizeBoardSlug ( board ) ]
}
2026-05-08 11:32:47 +08:00
// ─── Types ──────────────────────────────────────────────────────
export type KanbanTaskStatus = 'triage' | 'todo' | 'ready' | 'running' | 'blocked' | 'done' | 'archived'
export interface KanbanTask {
id : string
title : string
body : string | null
assignee : string | null
status : KanbanTaskStatus
priority : number
created_by : string | null
created_at : number
started_at : number | null
completed_at : number | null
workspace_kind : string
workspace_path : string | null
tenant : string | null
result : string | null
skills : string [ ] | null
}
export interface KanbanRun {
id : number
task_id : string
profile : string | null
status : string
started_at : number
ended_at : number | null
outcome : string | null
summary : string | null
error : string | null
}
export interface KanbanComment {
id : number
task_id : string
author : string
body : string
created_at : number
}
export interface KanbanEvent {
id : number
task_id : string
kind : string
payload : Record < string , unknown > | null
created_at : number
run_id : number | null
}
export interface KanbanTaskDetail {
task : KanbanTask
comments : KanbanComment [ ]
events : KanbanEvent [ ]
runs : KanbanRun [ ]
}
export interface KanbanStats {
by_status : Record < string , number >
by_assignee : Record < string , number >
total : number
}
export interface KanbanAssignee {
name : string
on_disk : boolean
counts : Record < string , number > | null
}
2026-05-10 13:58:44 +02:00
export interface KanbanBoard {
slug : string
name : string
description : string
icon : string
color : string
created_at : number | null
archived : boolean
db_path? : string
is_current? : boolean
counts : Record < string , number >
total : number
}
export interface KanbanBoardCreateOptions {
slug : string
name? : string
description? : string
icon? : string
color? : string
switchCurrent? : boolean
}
export interface KanbanCapabilities {
source : 'hermes-cli'
supports : Record < string , boolean >
missing : string [ ]
2026-05-11 15:26:24 +02:00
capabilities : KanbanCapabilityStatus [ ]
}
export interface KanbanTaskLog {
task_id : string
path : string | null
exists : boolean
size_bytes : number
content : string
truncated : boolean
}
export interface KanbanCapabilityStatus {
key : string
status : 'supported' | 'partial' | 'missing'
reason? : string
canonicalRoute? : string
canonicalCommand? : string
requiresBoard : boolean
2026-05-10 13:58:44 +02:00
}
export interface KanbanBoardOptions {
board? : string
}
2026-05-08 11:32:47 +08:00
// ─── CLI wrappers ───────────────────────────────────────────────
2026-05-10 13:58:44 +02:00
export async function listBoards ( opts ? : { includeArchived? : boolean } ) : Promise < KanbanBoard [ ] > {
const args = [ 'kanban' , 'boards' , 'list' , '--json' ]
if ( opts ? . includeArchived ) args . push ( '--all' )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban boards list failed' )
throw new Error ( ` Failed to list kanban boards: ${ err . message } ` )
}
}
async function findBoard ( slug : string , includeArchived = true ) : Promise < KanbanBoard | null > {
const boards = await listBoards ( { includeArchived } )
return boards . find ( board = > board . slug === slug ) || null
}
export async function createBoard ( opts : KanbanBoardCreateOptions ) : Promise < KanbanBoard > {
const slug = normalizeBoardSlug ( opts . slug )
const args = [ 'kanban' , 'boards' , 'create' , slug ]
if ( opts . name ? . trim ( ) ) args . push ( '--name' , opts . name . trim ( ) )
if ( opts . description ? . trim ( ) ) args . push ( '--description' , opts . description . trim ( ) )
if ( opts . icon ? . trim ( ) ) args . push ( '--icon' , opts . icon . trim ( ) )
if ( opts . color ? . trim ( ) ) args . push ( '--color' , opts . color . trim ( ) )
if ( opts . switchCurrent ) args . push ( '--switch' )
try {
await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
const board = await findBoard ( slug )
if ( ! board ) throw new Error ( 'created board was not returned by boards list' )
return board
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban boards create failed' )
throw new Error ( ` Failed to create kanban board: ${ err . message } ` )
}
}
export async function archiveBoard ( slugInput : string ) : Promise < void > {
const slug = normalizeBoardSlug ( slugInput )
if ( slug === 'default' ) throw new Error ( 'Cannot archive the default kanban board' )
try {
await execFileAsync ( HERMES_BIN , [ 'kanban' , 'boards' , 'rm' , slug ] , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban boards archive failed' )
throw new Error ( ` Failed to archive kanban board: ${ err . message } ` )
}
}
export async function getCapabilities ( ) : Promise < KanbanCapabilities > {
2026-05-11 15:26:24 +02:00
const capabilities : KanbanCapabilityStatus [ ] = [
{ key : 'explicitBoard' , status : 'supported' , canonicalCommand : '--board' , requiresBoard : true } ,
{ key : 'boardsList' , status : 'supported' , canonicalRoute : '/boards' , canonicalCommand : 'boards list' , requiresBoard : false } ,
{ key : 'boardCreate' , status : 'supported' , canonicalRoute : '/boards' , canonicalCommand : 'boards create' , requiresBoard : false } ,
{ key : 'boardArchive' , status : 'supported' , canonicalRoute : '/boards/{slug}' , canonicalCommand : 'boards rm' , requiresBoard : false } ,
{ key : 'cliCurrentSwitch' , status : 'partial' , reason : 'Backend keeps explicit board context and does not expose a WUI route for mutating canonical CLI current board' , canonicalRoute : '/boards/{slug}/switch' , canonicalCommand : 'boards switch' , requiresBoard : false } ,
{ key : 'taskCrudLite' , status : 'supported' , canonicalRoute : '/tasks' , canonicalCommand : 'list/show/create/complete/block/unblock/assign' , requiresBoard : true } ,
{ key : 'commentsWrite' , status : 'supported' , canonicalRoute : '/tasks/{task_id}/comments' , canonicalCommand : 'comment' , requiresBoard : true } ,
{ key : 'commentsRead' , status : 'supported' , reason : 'Comments are returned on task detail responses' , canonicalRoute : '/tasks/{task_id}' , canonicalCommand : 'show --json' , requiresBoard : true } ,
{ key : 'taskLog' , status : 'supported' , canonicalRoute : '/tasks/{task_id}/log' , canonicalCommand : 'log' , requiresBoard : true } ,
{ key : 'diagnostics' , status : 'supported' , canonicalRoute : '/diagnostics' , canonicalCommand : 'diagnostics' , requiresBoard : true } ,
{ key : 'reclaim' , status : 'supported' , canonicalRoute : '/tasks/{task_id}/reclaim' , canonicalCommand : 'reclaim' , requiresBoard : true } ,
{ key : 'reassign' , status : 'supported' , canonicalRoute : '/tasks/{task_id}/reassign' , canonicalCommand : 'reassign' , requiresBoard : true } ,
{ key : 'specify' , status : 'supported' , canonicalRoute : '/tasks/{task_id}/specify' , canonicalCommand : 'specify' , requiresBoard : true } ,
{ key : 'dispatch' , status : 'supported' , canonicalRoute : '/dispatch' , canonicalCommand : 'dispatch' , requiresBoard : true } ,
{ key : 'links' , status : 'missing' , reason : 'Deferred from current WUI parity batch' , canonicalRoute : '/links' , canonicalCommand : 'link/unlink' , requiresBoard : true } ,
{ key : 'bulk' , status : 'missing' , reason : 'Deferred from current WUI parity batch' , canonicalRoute : '/tasks/bulk' , canonicalCommand : 'bulk-equivalent' , requiresBoard : true } ,
{ key : 'events' , status : 'missing' , reason : 'Streaming strategy not selected for WUI yet' , canonicalRoute : '/events' , canonicalCommand : 'watch' , requiresBoard : true } ,
{ key : 'homeSubscriptions' , status : 'missing' , reason : 'Deferred from current WUI parity batch' , canonicalRoute : '/home-channels and subscription routes' , canonicalCommand : 'notify-*' , requiresBoard : true } ,
]
const supports = Object . fromEntries ( capabilities . map ( capability = > [ capability . key , capability . status === 'supported' ] ) ) as Record < string , boolean >
const missing = capabilities
. filter ( capability = > capability . status !== 'supported' )
. map ( capability = > capability . key )
return { source : 'hermes-cli' , supports , missing , capabilities }
}
function parseJsonPayload ( stdout : string ) : unknown [ ] {
const trimmed = stdout . trim ( )
if ( ! trimmed ) return [ ]
const parsed = JSON . parse ( trimmed )
if ( Array . isArray ( parsed ) ) return parsed
return [ parsed ]
}
function isNoWorkerLogError ( err : any ) : boolean {
const lines = [ err ? . stderr , err ? . stdout , err ? . message ]
. filter ( Boolean )
. flatMap ( value = > String ( value ) . split ( /\r?\n/ ) . map ( line = > line . trim ( ) ) . filter ( Boolean ) )
return lines . some ( line = > NO_WORKER_LOG_PATTERNS . some ( pattern = > pattern . test ( line ) ) )
}
function pushOptional ( args : string [ ] , flag : string , value? : string | number | null ) : void {
if ( value !== undefined && value !== null && String ( value ) . trim ( ) !== '' ) args . push ( flag , String ( value ) )
}
export async function addComment ( taskId : string , body : string , opts? : KanbanBoardOptions & { author? : string } ) : Promise < { ok : boolean ; output : string } > {
const args = [ . . . boardArgs ( opts ? . board ) , 'comment' , taskId , body ]
pushOptional ( args , '--author' , opts ? . author )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return { ok : true , output : stdout }
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban comment failed' )
throw new Error ( ` Failed to comment on kanban task: ${ err . message } ` )
}
}
export async function getTaskLog ( taskId : string , opts? : KanbanBoardOptions & { tail? : number } ) : Promise < KanbanTaskLog > {
const args = [ . . . boardArgs ( opts ? . board ) , 'log' , taskId ]
pushOptional ( args , '--tail' , opts ? . tail )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
const sizeBytes = Buffer . byteLength ( stdout , 'utf8' )
return {
task_id : taskId ,
path : null ,
exists : true ,
size_bytes : sizeBytes ,
content : stdout ,
truncated : opts?.tail !== undefined && sizeBytes >= opts . tail ,
}
} catch ( err : any ) {
const detail = await getTask ( taskId , opts )
if ( ! detail ) throw new Error ( 'Kanban task not found' )
if ( ( err . code === 1 || err . status === 1 ) && isNoWorkerLogError ( err ) ) {
return {
task_id : taskId ,
path : null ,
exists : false ,
size_bytes : 0 ,
content : '' ,
truncated : false ,
}
}
logger . error ( err , 'Hermes CLI: kanban log failed' )
throw new Error ( ` Failed to read kanban task log: ${ err . message } ` )
}
}
export async function getDiagnostics ( opts? : KanbanBoardOptions & { task? : string ; severity? : string } ) : Promise < unknown [ ] > {
const args = [ . . . boardArgs ( opts ? . board ) , 'diagnostics' , '--json' ]
pushOptional ( args , '--task' , opts ? . task )
pushOptional ( args , '--severity' , opts ? . severity )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban diagnostics failed' )
throw new Error ( ` Failed to get kanban diagnostics: ${ err . message } ` )
}
}
export async function reclaimTask ( taskId : string , opts? : KanbanBoardOptions & { reason? : string } ) : Promise < { ok : boolean ; output : string } > {
const args = [ . . . boardArgs ( opts ? . board ) , 'reclaim' , taskId ]
pushOptional ( args , '--reason' , opts ? . reason )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return { ok : true , output : stdout }
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban reclaim failed' )
throw new Error ( ` Failed to reclaim kanban task: ${ err . message } ` )
}
}
export async function reassignTask ( taskId : string , profile : string , opts? : KanbanBoardOptions & { reclaim? : boolean ; reason? : string } ) : Promise < { ok : boolean ; output : string } > {
const args = [ . . . boardArgs ( opts ? . board ) , 'reassign' , taskId , profile ]
if ( opts ? . reclaim ) args . push ( '--reclaim' )
pushOptional ( args , '--reason' , opts ? . reason )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return { ok : true , output : stdout }
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban reassign failed' )
throw new Error ( ` Failed to reassign kanban task: ${ err . message } ` )
}
}
export async function specifyTask ( taskId : string , opts? : KanbanBoardOptions & { author? : string } ) : Promise < unknown [ ] > {
const args = [ . . . boardArgs ( opts ? . board ) , 'specify' , taskId , '--json' ]
pushOptional ( args , '--author' , opts ? . author )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return parseJsonPayload ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban specify failed' )
throw new Error ( ` Failed to specify kanban task: ${ err . message } ` )
}
}
export async function dispatch ( opts? : KanbanBoardOptions & { dryRun? : boolean ; max? : number ; failureLimit? : number } ) : Promise < unknown > {
const args = [ . . . boardArgs ( opts ? . board ) , 'dispatch' , '--json' ]
if ( opts ? . dryRun ) args . push ( '--dry-run' )
pushOptional ( args , '--max' , opts ? . max )
pushOptional ( args , '--failure-limit' , opts ? . failureLimit )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban dispatch failed' )
throw new Error ( ` Failed to dispatch kanban tasks: ${ err . message } ` )
2026-05-10 13:58:44 +02:00
}
}
2026-05-08 11:32:47 +08:00
export async function listTasks ( opts ? : {
2026-05-10 13:58:44 +02:00
board? : string
2026-05-08 11:32:47 +08:00
status? : string
assignee? : string
tenant? : string
2026-05-11 15:09:58 +02:00
includeArchived? : boolean
2026-05-08 11:32:47 +08:00
} ) : Promise < KanbanTask [ ] > {
2026-05-10 13:58:44 +02:00
const args = [ . . . boardArgs ( opts ? . board ) , 'list' , '--json' ]
2026-05-11 15:09:58 +02:00
if ( opts ? . includeArchived ) args . push ( '--archived' )
2026-05-08 11:32:47 +08:00
if ( opts ? . status ) args . push ( '--status' , opts . status )
if ( opts ? . assignee ) args . push ( '--assignee' , opts . assignee )
if ( opts ? . tenant ) args . push ( '--tenant' , opts . tenant )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban list failed' )
throw new Error ( ` Failed to list kanban tasks: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function getTask ( taskId : string , opts? : KanbanBoardOptions ) : Promise < KanbanTaskDetail | null > {
2026-05-08 11:32:47 +08:00
try {
2026-05-10 13:58:44 +02:00
const { stdout } = await execFileAsync ( HERMES_BIN , [ . . . boardArgs ( opts ? . board ) , 'show' , taskId , '--json' ] , {
2026-05-08 11:32:47 +08:00
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
if ( err . code === 1 || err . status === 1 ) return null
logger . error ( err , 'Hermes CLI: kanban show failed' )
throw new Error ( ` Failed to get kanban task: ${ err . message } ` )
}
}
export async function createTask (
title : string ,
opts ? : {
2026-05-10 13:58:44 +02:00
board? : string
2026-05-08 11:32:47 +08:00
body? : string
assignee? : string
priority? : number
tenant? : string
} ,
) : Promise < KanbanTask > {
2026-05-10 13:58:44 +02:00
const args = [ . . . boardArgs ( opts ? . board ) , 'create' , title , '--json' ]
2026-05-08 11:32:47 +08:00
if ( opts ? . body ) args . push ( '--body' , opts . body )
if ( opts ? . assignee ) args . push ( '--assignee' , opts . assignee )
if ( opts ? . priority !== undefined ) args . push ( '--priority' , String ( opts . priority ) )
if ( opts ? . tenant ) args . push ( '--tenant' , opts . tenant )
try {
const { stdout } = await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban create failed' )
throw new Error ( ` Failed to create kanban task: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function completeTasks ( taskIds : string [ ] , summary? : string , opts? : KanbanBoardOptions ) : Promise < void > {
const args = [ . . . boardArgs ( opts ? . board ) , 'complete' , . . . taskIds ]
2026-05-08 11:32:47 +08:00
if ( summary ) args . push ( '--summary' , summary )
try {
await execFileAsync ( HERMES_BIN , args , {
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban complete failed' )
throw new Error ( ` Failed to complete kanban tasks: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function blockTask ( taskId : string , reason : string , opts? : KanbanBoardOptions ) : Promise < void > {
2026-05-08 11:32:47 +08:00
try {
2026-05-10 13:58:44 +02:00
await execFileAsync ( HERMES_BIN , [ . . . boardArgs ( opts ? . board ) , 'block' , taskId , reason ] , {
2026-05-08 11:32:47 +08:00
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban block failed' )
throw new Error ( ` Failed to block kanban task: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function unblockTasks ( taskIds : string [ ] , opts? : KanbanBoardOptions ) : Promise < void > {
2026-05-08 11:32:47 +08:00
try {
2026-05-10 13:58:44 +02:00
await execFileAsync ( HERMES_BIN , [ . . . boardArgs ( opts ? . board ) , 'unblock' , . . . taskIds ] , {
2026-05-08 11:32:47 +08:00
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban unblock failed' )
throw new Error ( ` Failed to unblock kanban tasks: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function assignTask ( taskId : string , profile : string , opts? : KanbanBoardOptions ) : Promise < void > {
2026-05-08 11:32:47 +08:00
try {
2026-05-10 13:58:44 +02:00
await execFileAsync ( HERMES_BIN , [ . . . boardArgs ( opts ? . board ) , 'assign' , taskId , profile ] , {
2026-05-08 11:32:47 +08:00
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban assign failed' )
throw new Error ( ` Failed to assign kanban task: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function getStats ( opts? : KanbanBoardOptions ) : Promise < KanbanStats > {
2026-05-08 11:32:47 +08:00
try {
2026-05-10 13:58:44 +02:00
const { stdout } = await execFileAsync ( HERMES_BIN , [ . . . boardArgs ( opts ? . board ) , 'stats' , '--json' ] , {
2026-05-08 11:32:47 +08:00
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
2026-05-11 15:09:58 +02:00
const stats = JSON . parse ( stdout ) as KanbanStats
const archivedTasks = await listTasks ( { board : opts?.board , status : 'archived' , includeArchived : true } )
const existingArchived = stats . by_status ? . archived || 0
const archivedCount = archivedTasks . length
stats . by_status = { . . . ( stats . by_status || { } ) , archived : archivedCount }
stats . total = ( stats . total || 0 ) + Math . max ( 0 , archivedCount - existingArchived )
return stats
2026-05-08 11:32:47 +08:00
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban stats failed' )
throw new Error ( ` Failed to get kanban stats: ${ err . message } ` )
}
}
2026-05-10 13:58:44 +02:00
export async function getAssignees ( opts? : KanbanBoardOptions ) : Promise < KanbanAssignee [ ] > {
2026-05-08 11:32:47 +08:00
try {
2026-05-10 13:58:44 +02:00
const { stdout } = await execFileAsync ( HERMES_BIN , [ . . . boardArgs ( opts ? . board ) , 'assignees' , '--json' ] , {
2026-05-08 11:32:47 +08:00
maxBuffer : 50 * 1024 * 1024 ,
timeout : 30000 ,
. . . execOpts ,
} )
return JSON . parse ( stdout )
} catch ( err : any ) {
logger . error ( err , 'Hermes CLI: kanban assignees failed' )
throw new Error ( ` Failed to get kanban assignees: ${ err . message } ` )
}
}