Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,28 @@ export function appendExternalEvent(from: string, to: string, message: string):
try {
const dir = getStateDir()
mkdirSync(dir, { recursive: true })
const event = JSON.stringify({ type: 'message', from, to, message, ts: new Date().toISOString() })
const refs = externalEventRefs()
const event = JSON.stringify({
version: 1,
type: 'message',
from,
to,
text: message,
message,
ts: Date.now(),
...(Object.keys(refs).length > 0 ? { refs } : {}),
})
appendFileSync(join(dir, 'events.jsonl'), event + '\n', 'utf-8')
} catch {
// Best-effort — never throw from event logging
}
}

function externalEventRefs(): Record<string, string> {
const refs: Record<string, string> = {}
for (const key of ['HERMES_KANBAN_TASK', 'HERMES_KANBAN_RUN_ID', 'HERMES_KANBAN_PARENT_REF']) {
const value = process.env[key]
if (value && value.length > 0) refs[key] = value
}
return refs
}
34 changes: 33 additions & 1 deletion tests/unit/external-orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ describe('sendDirect — external parent routing', () => {
const lines = readFileSync(eventsPath, 'utf-8').trim().split('\n')
expect(lines.length).toBe(1)
const evt = JSON.parse(lines[0])
expect(evt.version).toBe(1)
expect(evt.type).toBe('message')
expect(evt.from).toBe('my-agent')
expect(evt.to).toBe('hermes')
expect(evt.text).toBe('done with task')
expect(evt.message).toBe('done with task')
expect(typeof evt.ts).toBe('string')
expect(typeof evt.ts).toBe('number')
})

it('does NOT write to events.jsonl when parent is human', async () => {
Expand Down Expand Up @@ -194,9 +196,12 @@ describe('appendExternalEvent', () => {
const eventsPath = join(tmpHome, '.flt', 'events.jsonl')
expect(existsSync(eventsPath)).toBe(true)
const evt = JSON.parse(readFileSync(eventsPath, 'utf-8').trim())
expect(evt.version).toBe(1)
expect(evt.from).toBe('agent-x')
expect(evt.to).toBe('hermes')
expect(evt.text).toBe('test message')
expect(evt.message).toBe('test message')
expect(typeof evt.ts).toBe('number')
})

it('appends multiple events as separate lines', () => {
Expand All @@ -208,4 +213,31 @@ describe('appendExternalEvent', () => {
expect(JSON.parse(lines[0]).message).toBe('first')
expect(JSON.parse(lines[1]).message).toBe('second')
})

it('forwards Hermes kanban refs opaquely when present', () => {
const prevTask = process.env.HERMES_KANBAN_TASK
const prevRun = process.env.HERMES_KANBAN_RUN_ID
const prevParent = process.env.HERMES_KANBAN_PARENT_REF
process.env.HERMES_KANBAN_TASK = 't_abc123'
process.env.HERMES_KANBAN_RUN_ID = '42'
process.env.HERMES_KANBAN_PARENT_REF = '{"kind":"flt-event-sink","board":"kanban-flt-events"}'
try {
appendExternalEvent('agent-x', 'hermes', 'done')
} finally {
if (prevTask === undefined) delete process.env.HERMES_KANBAN_TASK
else process.env.HERMES_KANBAN_TASK = prevTask
if (prevRun === undefined) delete process.env.HERMES_KANBAN_RUN_ID
else process.env.HERMES_KANBAN_RUN_ID = prevRun
if (prevParent === undefined) delete process.env.HERMES_KANBAN_PARENT_REF
else process.env.HERMES_KANBAN_PARENT_REF = prevParent
}

const eventsPath = join(tmpHome, '.flt', 'events.jsonl')
const evt = JSON.parse(readFileSync(eventsPath, 'utf-8').trim())
expect(evt.refs).toEqual({
HERMES_KANBAN_TASK: 't_abc123',
HERMES_KANBAN_RUN_ID: '42',
HERMES_KANBAN_PARENT_REF: '{"kind":"flt-event-sink","board":"kanban-flt-events"}',
})
})
})
Loading