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
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hermes-orchestrator",
"version": "0.4.0",
"version": "0.5.0",
"description": "Supervisor framework for AI agents - classify, spawn, watch, intervene, learn. The Hermes pattern (Claude CLI subprocess) generalised.",
"type": "module",
"main": "./dist/index.js",
Expand All @@ -25,6 +25,14 @@
"./patterns/hermes-bug-fix": {
"types": "./dist/patterns/hermes-bug-fix.d.ts",
"import": "./dist/patterns/hermes-bug-fix.js"
},
"./patterns/research-doc": {
"types": "./dist/patterns/research-doc.d.ts",
"import": "./dist/patterns/research-doc.js"
},
"./patterns/meeting-capture": {
"types": "./dist/patterns/meeting-capture.d.ts",
"import": "./dist/patterns/meeting-capture.js"
}
},
"files": ["dist", "README.md", "LICENSE", "docs"],
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export { HermesRunner, type HermesRunnerOptions } from './adapters/hermes-runner
export { BonfireMemory, type BonfireMemoryOptions } from './adapters/bonfire-memory.js'
export { FileMemory, type FileMemoryOptions } from './adapters/file-memory.js'
export { hermesBugFix } from './patterns/hermes-bug-fix.js'
export { researchDoc } from './patterns/research-doc.js'
export { meetingCapture } from './patterns/meeting-capture.js'

export type {
Task,
Expand Down
74 changes: 74 additions & 0 deletions src/patterns/meeting-capture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { PatternAdapter, Task, MemoryHit, RunnerInput } from '../types.js'

const KEYWORDS = [
'meeting',
'transcribe',
'transcript',
'recap',
'process this call',
'process the call',
'extract todos',
'extract action items',
'voice memo',
'recording',
'standup',
]

const FILE_EXTENSIONS = ['.mp4', '.m4a', '.mov', '.mp3', '.wav', '.opus', '.flac']

const SYSTEM_PROMPT = `You are a meeting-capture agent. Turn one recording or transcript into one durable recap.

Workflow:
1. Acquire the transcript: if a media file, transcribe it (local mlx-whisper preferred). If a paste, use it directly.
2. Run multi-pass extraction (NOT a single monolithic prompt):
A. Metadata: date, duration, title, attendees, platform.
B. Decisions: explicit + verbatim-anchored. Carry a confidence (high/medium/low).
C. Actions: concrete follow-ups with one owner each. Confidence + due if stated.
D. Quotes: 3-8 load-bearing verbatim quotes.
E. Research seeds + memory updates. Cross-check existing entities BEFORE adding.
3. Produce: a recap doc with the schema below, plus a separate transcript file.

Rules:
- Verbatim where possible. No paraphrasing of decisions.
- Every decision + action carries a confidence field.
- Ambiguous owner = "Both" + confidence: low, surface it for the operator.
- Never invent dates. Relative ("by Thursday") -> absolute, anchored to the meeting date.
- If an entity is already documented, LINK it. Do not re-introduce.

Doc structure:
- Frontmatter (date, attendees, project, doc-type: meeting-recap)
- TL;DR (3-5 bullets)
- Decisions (table with id, text, owner, confidence)
- Actions (table with title, owner, due, category, confidence)
- Key quotes (3-8)
- Transcript link (separate file)
- Next Actions (link to trackers + PRs)`

export const meetingCapture: PatternAdapter = {
name: 'meeting-capture',
defaultRunner: 'hermes',
costCap: 3.0,

matches(task: Task): boolean {
const t = task.text.toLowerCase()
if (KEYWORDS.some((k) => t.includes(k))) return true
if (FILE_EXTENSIONS.some((ext) => t.includes(ext))) return true
return false
},

prepare(task: Task, memory: MemoryHit[]): RunnerInput {
const fewshot =
memory.length > 0
? `\n\nPast meeting recaps for similar contexts (most relevant first):\n${memory
.map((m, i) => `${i + 1}. ${m.body}`)
.join('\n')}`
: ''
return {
prompt: task.text,
systemPrompt: SYSTEM_PROMPT + fewshot,
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob'],
maxCostUsd: 3.0,
metadata: { pattern: 'meeting-capture' },
}
},
}
54 changes: 54 additions & 0 deletions src/patterns/research-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { PatternAdapter, Task, MemoryHit, RunnerInput } from '../types.js'

const KEYWORDS = [
'research',
'investigate',
'audit',
'look into',
'find out about',
'survey',
'compare',
'evaluate',
]

const SYSTEM_PROMPT = `You are a research agent. Your job is to produce one durable research doc, not a chat answer.

Workflow:
1. Search the existing research library first (grep over README.md files) to avoid duplicating work and to find related docs.
2. Fetch the actual source - climb the ladder WebFetch -> exa web_fetch -> Playwright -> Wayback. Do not write off a search snippet.
3. Classify every source FULL / PARTIAL / FAILED. Escalate PARTIAL/FAILED through the full ladder before writing.
4. Write the doc with required frontmatter: topic, type, status, last-validated, related-docs, original-query, tier.
5. End with a Next Actions table linking to concrete todos / PRs / calendar items.

Rules:
- Be specific. Include at least 3 numbers (versions, prices, dates, counts).
- No vague language: never use "consider", "it might be worth", "you could explore". State the decision.
- Recommendations FIRST in a Key Decisions table at the top.
- Cite every source URL. Mark each FULL/PARTIAL/FAILED.`

export const researchDoc: PatternAdapter = {
name: 'research-doc',
defaultRunner: 'hermes',
costCap: 5.0,

matches(task: Task): boolean {
const t = task.text.toLowerCase()
return KEYWORDS.some((k) => t.includes(k))
},

prepare(task: Task, memory: MemoryHit[]): RunnerInput {
const fewshot =
memory.length > 0
? `\n\nPast research outputs for similar topics (most relevant first):\n${memory
.map((m, i) => `${i + 1}. ${m.body}`)
.join('\n')}`
: ''
return {
prompt: task.text,
systemPrompt: SYSTEM_PROMPT + fewshot,
allowedTools: ['WebFetch', 'WebSearch', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
maxCostUsd: 5.0,
metadata: { pattern: 'research-doc' },
}
},
}
102 changes: 102 additions & 0 deletions tests/patterns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest'
import { researchDoc } from '../src/patterns/research-doc.js'
import { meetingCapture } from '../src/patterns/meeting-capture.js'
import { hermesBugFix } from '../src/patterns/hermes-bug-fix.js'
import { classify } from '../src/router.js'
import type { Task, MemoryHit } from '../src/types.js'

function task(text: string): Task {
return { id: 't-1', text, createdAt: new Date().toISOString() }
}

describe('researchDoc pattern', () => {
it('matches on "research", "investigate", "audit"', () => {
expect(researchDoc.matches(task('research vector dbs for ZAO'))).toBe(true)
expect(researchDoc.matches(task('Investigate the Bonfire API limits'))).toBe(true)
expect(researchDoc.matches(task('audit the agent stack'))).toBe(true)
})

it('does NOT match on bug-fix or meeting language', () => {
expect(researchDoc.matches(task('fix the type error in foo.ts'))).toBe(false)
expect(researchDoc.matches(task('process this meeting recording'))).toBe(false)
})

it('prepare returns a research-shaped RunnerInput with WebFetch allowed', () => {
const input = researchDoc.prepare(task('research vector dbs'), [])
expect(input.allowedTools).toContain('WebFetch')
expect(input.allowedTools).toContain('WebSearch')
expect(input.systemPrompt).toContain('research agent')
expect(input.systemPrompt).toContain('Next Actions')
expect(input.maxCostUsd).toBe(5.0)
expect(input.metadata?.pattern).toBe('research-doc')
})

it('few-shot injects past memory hits into the systemPrompt', () => {
const past: MemoryHit[] = [
{
name: 'research-doc:t-prev',
body: 'Past task: "research vector dbs", Outcome: doc 200 recommends pgvector',
sourceTag: 'hermes:research-doc:completed',
},
]
const input = researchDoc.prepare(task('research vector dbs again'), past)
expect(input.systemPrompt).toContain('Past research outputs')
expect(input.systemPrompt).toContain('doc 200 recommends pgvector')
})
})

describe('meetingCapture pattern', () => {
it('matches on meeting / transcript / recap keywords', () => {
expect(meetingCapture.matches(task('process this meeting recording'))).toBe(true)
expect(meetingCapture.matches(task('Recap that call'))).toBe(true)
expect(meetingCapture.matches(task('extract action items from the standup'))).toBe(true)
})

it('matches on media file extensions', () => {
expect(meetingCapture.matches(task('/Users/me/Downloads/call.mp4'))).toBe(true)
expect(meetingCapture.matches(task('process /tmp/voice-memo.m4a'))).toBe(true)
expect(meetingCapture.matches(task('here is the recording.wav'))).toBe(true)
})

it('does NOT match on plain research or bug-fix', () => {
expect(meetingCapture.matches(task('research vector dbs'))).toBe(false)
expect(meetingCapture.matches(task('fix the type error in foo.ts'))).toBe(false)
})

it('prepare returns a capture-shaped RunnerInput', () => {
const input = meetingCapture.prepare(task('process the call'), [])
expect(input.allowedTools).toContain('Bash')
expect(input.allowedTools).toContain('Write')
expect(input.systemPrompt).toContain('meeting-capture agent')
expect(input.systemPrompt).toContain('Decisions')
expect(input.systemPrompt).toContain('Actions')
expect(input.maxCostUsd).toBe(3.0)
expect(input.metadata?.pattern).toBe('meeting-capture')
})
})

describe('router picks the right pattern when multiple are registered', () => {
const patterns = [hermesBugFix, researchDoc, meetingCapture]

it('picks hermes-bug-fix for fix/bug language', async () => {
const d = await classify(task('fix the type error in src/foo.ts'), { patterns })
expect(d.pattern).toBe('hermes-bug-fix')
})

it('picks research-doc for research language', async () => {
const d = await classify(task('research the best vector db for our agents'), {
patterns,
})
expect(d.pattern).toBe('research-doc')
})

it('picks meeting-capture for media file paths', async () => {
const d = await classify(task('process /tmp/Arthur-x-Zaal.mp4'), { patterns })
expect(d.pattern).toBe('meeting-capture')
})

it('returns unknown when no pattern matches', async () => {
const d = await classify(task('write a haiku about ducks'), { patterns })
expect(d.pattern).toBe('unknown')
})
})