Skip to content
Merged
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: 5 additions & 5 deletions src/cli/commands/brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ experiment conclusions so mutual thinking compounds. See docs/guides/BRAIN.md.
const ctx = openBrain();
try {
const q: BrainQuery = {
text: query,
org: !!options.org,
agent: options.agent,
kind: options.kind as BrainKind | undefined,
limit: parseInt(options.limit, 10),
includeSuperseded: !!options.all,
...(query ? { text: query } : {}),
...(options.agent ? { agent: options.agent } : {}),
...(options.kind ? { kind: options.kind as BrainKind } : {}),
};
const results = ctx.store.recall(q);
if (options.json) {
Expand Down Expand Up @@ -266,8 +266,8 @@ experiment conclusions so mutual thinking compounds. See docs/guides/BRAIN.md.
return cmd;
}

function splitList(v?: string): string[] | undefined {
if (!v) return undefined;
function splitList(v?: string): string[] {
if (!v) return [];
return v
.split(',')
.map((s) => s.trim())
Expand Down
26 changes: 14 additions & 12 deletions src/cli/commands/vision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,16 @@ limits (maxIterations, maxConsecutiveFailures, …). See docs/guides/VISION.md.
.action((text, options) => {
const p = paths(process.cwd());
const inbox = new SignalInbox(p.signalsPath);
const refs = options.refs
? String(options.refs)
.split(',')
.map((r: string) => r.trim())
: undefined;
const s = inbox.add({
text,
severity: options.severity as SignalSeverity,
source: options.source,
refs: options.refs
? String(options.refs)
.split(',')
.map((r: string) => r.trim())
: undefined,
...(refs ? { refs } : {}),
});
console.log(
chalk.green('✓ signal queued'),
Expand Down Expand Up @@ -228,15 +229,16 @@ limits (maxIterations, maxConsecutiveFailures, …). See docs/guides/VISION.md.
)
);
}
const max = options.once
? 1
: options.max
? parseInt(options.max, 10)
: undefined;
await runLoop({
dryRun,
max: options.once
? 1
: options.max
? parseInt(options.max, 10)
: undefined,
delegateCmd: options.delegateCmd,
timeoutMs: parseInt(options.timeout, 10) * 1000,
...(max !== undefined ? { max } : {}),
...(options.delegateCmd ? { delegateCmd: options.delegateCmd } : {}),
});
});

Expand Down Expand Up @@ -284,7 +286,7 @@ async function runLoop(opts: {

const result = await loop.run({
dryRun: opts.dryRun,
maxIterations: opts.max,
...(opts.max !== undefined ? { maxIterations: opts.max } : {}),
});
for (const d of result.decisions) console.log(fmtDecision(d));

Expand Down
29 changes: 23 additions & 6 deletions src/cli/hermes-sm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ const DEFAULT_CONFIG: HermesSMConfig = {
function loadConfig(): HermesSMConfig {
try {
if (fs.existsSync(HERMES_CONFIG_PATH)) {
return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(HERMES_CONFIG_PATH, 'utf8')) };
return {
...DEFAULT_CONFIG,
...JSON.parse(fs.readFileSync(HERMES_CONFIG_PATH, 'utf8')),
};
}
} catch {
// Use defaults
Expand Down Expand Up @@ -121,9 +124,13 @@ function ensureDaemon(): void {

function writeSessionHeartbeat(instanceId: string): NodeJS.Timeout {
const sessionsDir = path.join(SM_DIR, 'sessions');
if (!fs.existsSync(sessionsDir)) fs.mkdirSync(sessionsDir, { recursive: true });
if (!fs.existsSync(sessionsDir))
fs.mkdirSync(sessionsDir, { recursive: true });

const heartbeatFile = path.join(sessionsDir, `session-${Date.now()}.heartbeat`);
const heartbeatFile = path.join(
sessionsDir,
`session-${Date.now()}.heartbeat`
);
fs.writeFileSync(heartbeatFile, instanceId);

// Update heartbeat every 60s
Expand Down Expand Up @@ -154,7 +161,11 @@ class HermesSM {
const { instanceId, tracingEnabled, verboseTracing } = this.config;

console.log(chalk.cyan('╭─ hermes-sm ─────────────────────────────╮'));
console.log(chalk.cyan(`│ Instance: ${instanceId.slice(0, 8)} │`));
console.log(
chalk.cyan(
`│ Instance: ${instanceId.slice(0, 8)} │`
)
);
console.log(chalk.cyan('╰──────────────────────────────────────────╯'));

// 1. Ensure daemon is running
Expand Down Expand Up @@ -193,7 +204,11 @@ class HermesSM {
const handoff = store.getLatestHandoff(projectId);
if (handoff) {
handoffContext = handoff.content || '';
console.log(chalk.dim(` ↳ Restored handoff: ${handoff.summary?.slice(0, 60) || 'previous session'}`));
console.log(
chalk.dim(
` ↳ Restored handoff: ${handoff.summary?.slice(0, 60) || 'previous session'}`
)
);
}
} catch {
// No handoff available
Expand Down Expand Up @@ -282,7 +297,9 @@ const smConfig = loadConfig();

program
.name('hermes-smd')
.description('Hermes with StackMemory context persistence, daemon auto-start, and desire-path tracking')
.description(
'Hermes with StackMemory context persistence, daemon auto-start, and desire-path tracking'
)
.argument('[prompt...]', 'Initial prompt for hermes')
.option('--resume <session>', 'Resume a Hermes session by ID')
.option('-m, --model <model>', 'Model to use')
Expand Down
4 changes: 2 additions & 2 deletions src/core/brain/__tests__/brain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ describe('BrainStore', () => {

const results = store.recall({ text: 'jitter' });
expect(results).toHaveLength(1);
expect(results[0].conclusion).toBe('errors dropped 60%');
expect(results[0].agent).toBe('codex');
expect(results[0]?.conclusion).toBe('errors dropped 60%');
expect(results[0]?.agent).toBe('codex');
});

it('does not leak entries across repos by default', () => {
Expand Down
5 changes: 3 additions & 2 deletions src/core/brain/brain-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class BrainStore {
}

function rowToEntry(row: BrainRow): BrainEntry {
return {
const entry: BrainEntry = {
entryId: row.entry_id,
workspaceId: row.workspace_id,
projectId: row.project_id,
Expand All @@ -220,10 +220,11 @@ function rowToEntry(row: BrainRow): BrainEntry {
refs: safeParse(row.refs),
confidence: row.confidence,
status: row.status as BrainEntry['status'],
supersededBy: row.superseded_by ?? undefined,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
if (row.superseded_by) entry.supersededBy = row.superseded_by;
return entry;
}

function safeParse(json: string): string[] {
Expand Down
8 changes: 5 additions & 3 deletions src/core/brain/brain-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,13 @@ export class BrainSync {
async sync(): Promise<BrainSyncResult> {
const pushed = await this.push();
const pulled = await this.pull();
const error = pushed.error ?? pulled.error;
return {
success: pushed.success && pulled.success,
pushed: pushed.pushed,
pulled: pulled.pulled,
applied: pulled.applied,
error: pushed.error ?? pulled.error,
...(error ? { error } : {}),
};
}

Expand Down Expand Up @@ -229,7 +230,7 @@ function toWireEntry(row: Record<string, unknown>): BrainEntry {
return [];
}
};
return {
const entry: BrainEntry = {
entryId: String(row['entry_id']),
workspaceId: String(row['workspace_id'] ?? ''),
projectId: String(row['project_id']),
Expand All @@ -242,10 +243,11 @@ function toWireEntry(row: Record<string, unknown>): BrainEntry {
refs: parse(row['refs']),
confidence: Number(row['confidence'] ?? 0.7),
status: String(row['status'] ?? 'active') as BrainEntry['status'],
supersededBy: (row['superseded_by'] as string | null) ?? undefined,
createdAt: Number(row['created_at']),
updatedAt: Number(row['updated_at']),
};
if (row['superseded_by']) entry.supersededBy = String(row['superseded_by']);
return entry;
}

function errMsg(err: unknown): string {
Expand Down
7 changes: 4 additions & 3 deletions src/core/vision/__tests__/vision.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ describe('parseVision', () => {
]);
expect(v.scope).toEqual(['src/**']);
expect(v.objectives).toHaveLength(3);
expect(v.objectives[1].done).toBe(true);
expect(v.objectives[0].done).toBe(false);
expect(v.objectives[1]?.done).toBe(true);
expect(v.objectives[0]?.done).toBe(false);
expect(v.limits.maxIterations).toBe(5);
expect(v.limits.maxConsecutiveFailures).toBe(2);
expect(v.limits.requireApproval).toBe(false);
Expand All @@ -85,6 +85,7 @@ describe('scaffold + toggle', () => {
writeFileSync(p, SAMPLE);
const v = parseVision(SAMPLE);
const target = v.objectives[0];
if (!target) throw new Error('expected an objective');
expect(setObjectiveDone(p, target.id, true)).toBe(true);
const after = parseVision(readFileSync(p, 'utf-8'));
expect(after.objectives.find((o) => o.id === target.id)?.done).toBe(true);
Expand All @@ -98,7 +99,7 @@ describe('SignalInbox', () => {
inbox.add({ text: 'critical thing', severity: 'critical' });
inbox.add({ text: 'medium thing', severity: 'medium' });
const pending = inbox.pending();
expect(pending[0].text).toBe('critical thing');
expect(pending[0]?.text).toBe('critical thing');
expect(pending).toHaveLength(3);
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/vision/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export class SignalInbox {
source: input.source ?? 'manual',
severity: input.severity ?? 'medium',
text: input.text,
refs: input.refs,
createdAt: Date.now(),
...(input.refs ? { refs: input.refs } : {}),
};
appendFileSync(this.path, JSON.stringify(signal) + '\n');
return signal;
Expand Down
21 changes: 11 additions & 10 deletions src/core/vision/vision-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function splitSections(text: string): {

for (const line of lines) {
const h2 = line.match(/^##\s+(.+?)\s*$/);
if (h2) {
if (h2?.[1]) {
current = { body: [] };
sections.set(h2[1].toLowerCase(), current);
continue;
Expand All @@ -69,22 +69,21 @@ function splitSections(text: string): {

function bulletLines(body: string[]): string[] {
return body
.map((l) => l.match(/^\s*[-*]\s+(.*\S)\s*$/))
.filter((m): m is RegExpMatchArray => !!m)
.map((m) => m[1].trim())
.map((l) => l.match(/^\s*[-*]\s+(.*\S)\s*$/)?.[1]?.trim())
.filter((s): s is string => !!s)
.filter((s) => !/^\[[ xX]\]/.test(s)); // checklist handled separately
}

function parseObjectives(body: string[]): Objective[] {
const objectives: Objective[] = [];
for (const line of body) {
const m = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.*\S)\s*$/);
if (!m) continue;
if (!m?.[2]) continue;
const text = m[2].trim();
objectives.push({
id: objectiveId(text),
text,
done: m[1].toLowerCase() === 'x',
done: (m[1] ?? '').toLowerCase() === 'x',
});
}
return objectives;
Expand All @@ -94,7 +93,7 @@ function parseLimits(body: string[]): VisionLimits {
const limits: VisionLimits = { ...DEFAULT_LIMITS };
for (const line of body) {
const m = line.match(/^\s*([a-zA-Z]+)\s*:\s*(.+?)\s*$/);
if (!m) continue;
if (!m?.[1] || m[2] === undefined) continue;
const key = m[1] as keyof VisionLimits;
const raw = m[2];
if (!(key in limits)) continue;
Expand Down Expand Up @@ -142,10 +141,12 @@ export function setObjectiveDone(
const lines = readFileSync(path, 'utf-8').split(/\r?\n/);
let changed = false;
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^(\s*[-*]\s+)\[([ xX])\]\s+(.*\S)\s*$/);
if (!m) continue;
const line = lines[i];
if (line === undefined) continue;
const m = line.match(/^(\s*[-*]\s+)\[([ xX])\]\s+(.*\S)\s*$/);
if (!m?.[3]) continue;
if (objectiveId(m[3].trim()) === objId) {
lines[i] = `${m[1]}[${done ? 'x' : ' '}] ${m[3].trim()}`;
lines[i] = `${m[1] ?? ''}[${done ? 'x' : ' '}] ${m[3].trim()}`;
changed = true;
break;
}
Expand Down
4 changes: 2 additions & 2 deletions src/core/vision/vision-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export class VisionLoop {
};
}
const idx = vision.objectives.findIndex((o) => !o.done);
if (idx >= 0) {
const o = vision.objectives[idx];
const o = idx >= 0 ? vision.objectives[idx] : undefined;
if (o) {
return {
kind: 'objective',
id: o.id,
Expand Down
15 changes: 12 additions & 3 deletions src/daemon/daemon-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,18 @@ export const DEFAULT_DAEMON_CONFIG: DaemonConfig = {
enabled: true, // opt-out via STACKMEMORY_RESEARCH_STREAM=0
interval: 360, // every 6 hours
keywords: [
'agent', 'ai', 'llm', 'mcp', 'context', 'memory',
'orchestration', 'skill', 'workflow', 'automation',
'browser agent', 'coding assistant',
'agent',
'ai',
'llm',
'mcp',
'context',
'memory',
'orchestration',
'skill',
'workflow',
'automation',
'browser agent',
'coding assistant',
],
maxSignalsPerScan: 50,
},
Expand Down
Loading
Loading