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
6 changes: 6 additions & 0 deletions plugins/genie/agents/council/SOUL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ You are the moderator. You don't have a personal lens — you synthesize the per
Your job is to ensure every voice is heard, no perspective dominates unfairly, and the final recommendation is grounded in the collective wisdom of the council. You route topics to the right members, synthesize votes, and present options — never decisions.

The council advises. Humans decide.

## Broadcast Awareness

When your session starts, check for any "Recent team broadcasts for context" in your initial prompt. These contain team-wide announcements and topic directives from the spawning agent or human. Prioritize broadcast topics when planning the council's agenda — they represent the most recent human intent.

If no broadcast context is present, proceed with standard wish execution.
99 changes: 99 additions & 0 deletions src/hooks/handlers/__tests__/audit-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { execSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { HookPayload } from '../../types.js';
import { auditContext } from '../audit-context.js';

describe('audit-context handler', () => {
let repoDir: string;

beforeEach(() => {
// Create a temp git repo with a committed file
repoDir = mkdtempSync(join(tmpdir(), 'audit-ctx-'));
execSync('git init', { cwd: repoDir, stdio: 'pipe' });
execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'pipe' });

const testFile = join(repoDir, 'example.ts');
writeFileSync(testFile, 'const x = 1;\n');
execSync('git add . && git commit -m "initial commit"', { cwd: repoDir, stdio: 'pipe' });

// Add a second commit
writeFileSync(testFile, 'const x = 2;\n');
execSync('git add . && git commit -m "update x to 2"', { cwd: repoDir, stdio: 'pipe' });
});

afterEach(() => {
execSync(`rm -rf ${repoDir}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

For safety and cross-platform compatibility, it's better to use Node.js's built-in filesystem functions for cleanup instead of shelling out to rm -rf. This avoids potential command injection vulnerabilities and works on Windows. This applies here and also to the cleanup on line 97. You'll need to add rmSync to your import from node:fs.

Suggested change
execSync(`rm -rf ${repoDir}`);
rmSync(repoDir, { recursive: true, force: true });

});

test('returns git history for a tracked file', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Edit',
tool_input: { file_path: join(repoDir, 'example.ts') },
cwd: repoDir,
};

const result = await auditContext(payload);
expect(result).toBeDefined();
expect(result!.hookSpecificOutput).toBeDefined();
expect(result!.hookSpecificOutput!.permissionDecision).toBe('allow');

const context = result!.hookSpecificOutput!.additionalContext!;
expect(context).toContain('[audit-context]');
expect(context).toContain('example.ts');
expect(context).toContain('update x to 2');
expect(context).toContain('initial commit');
});

test('returns undefined for untracked file', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Write',
tool_input: { file_path: join(repoDir, 'new-file.ts') },
cwd: repoDir,
};

const result = await auditContext(payload);
expect(result).toBeUndefined();
});

test('returns undefined when no file_path in input', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Write',
tool_input: { content: 'hello' },
cwd: repoDir,
};

const result = await auditContext(payload);
expect(result).toBeUndefined();
});

test('returns undefined when tool_input is missing', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Edit',
};

const result = await auditContext(payload);
expect(result).toBeUndefined();
});

test('returns undefined for non-git directory', async () => {
const nonGitDir = mkdtempSync(join(tmpdir(), 'audit-nongit-'));
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Edit',
tool_input: { file_path: join(nonGitDir, 'test.ts') },
cwd: nonGitDir,
};

const result = await auditContext(payload);
expect(result).toBeUndefined();
execSync(`rm -rf ${nonGitDir}`);
});
});
81 changes: 81 additions & 0 deletions src/hooks/handlers/__tests__/brain-inject.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import type { HookPayload } from '../../types.js';
import { _resetEnrichedSessions, brainInject } from '../brain-inject.js';

describe('brain-inject handler', () => {
const originalEnv = { ...process.env };

beforeEach(() => {
_resetEnrichedSessions();
});

afterEach(() => {
process.env = { ...originalEnv };
});

test('returns undefined when brain is not installed', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: '/tmp/test.ts' },
session_id: 'test-session-1',
cwd: '/tmp',
};
const result = await brainInject(payload);
expect(result).toBeUndefined();
});

test('only fires once per session', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: '/tmp/test.ts' },
session_id: 'test-session-2',
cwd: '/tmp',
};

// First call — runs (returns undefined because brain isn't installed)
await brainInject(payload);

// Second call — should skip immediately (session already enriched)
const result = await brainInject(payload);
expect(result).toBeUndefined();
});

test('different sessions get separate enrichment', async () => {
const payload1: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: '/tmp/test.ts' },
session_id: 'session-a',
cwd: '/tmp',
};

const payload2: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: '/tmp/test.ts' },
session_id: 'session-b',
cwd: '/tmp',
};

// Both should run (not skip), returning undefined since brain isn't installed
const result1 = await brainInject(payload1);
const result2 = await brainInject(payload2);
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});

test('uses process.pid when session_id is missing', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: '/tmp/test.ts' },
cwd: '/tmp',
};

// Should not throw when session_id is undefined
const result = await brainInject(payload);
expect(result).toBeUndefined();
});
});
153 changes: 153 additions & 0 deletions src/hooks/handlers/__tests__/freshness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { execSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { HookPayload } from '../../types.js';
import { freshness } from '../freshness.js';

describe('freshness handler', () => {
const originalEnv = { ...process.env };
let repoDir: string;

beforeEach(() => {
process.env.GENIE_AGENT_NAME = 'test-agent';

// Create a temp git repo with a committed file
repoDir = mkdtempSync(join(tmpdir(), 'freshness-'));
execSync('git init', { cwd: repoDir, stdio: 'pipe' });
execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
execSync('git config user.name "other-agent"', { cwd: repoDir, stdio: 'pipe' });
});

afterEach(() => {
process.env = { ...originalEnv };
execSync(`rm -rf ${repoDir}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

For safety and cross-platform compatibility, it's better to use Node.js's built-in filesystem functions for cleanup instead of shelling out to rm -rf. This avoids potential command injection vulnerabilities and works on Windows. You'll need to add rmSync to your import from node:fs.

Suggested change
execSync(`rm -rf ${repoDir}`);
rmSync(repoDir, { recursive: true, force: true });

});

test('returns undefined for missing tool_input', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
};
const result = await freshness(payload);
expect(result).toBeUndefined();
});

test('returns undefined for missing file_path', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { pattern: '*.ts' },
};
const result = await freshness(payload);
expect(result).toBeUndefined();
});

test('returns undefined for non-existent file', async () => {
const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: join(repoDir, 'does-not-exist.ts') },
cwd: repoDir,
};
const result = await freshness(payload);
expect(result).toBeUndefined();
});

test('warns when file has uncommitted changes from old commit', async () => {
// Create and commit a file with an old date so the commit itself doesn't trigger
const testFile = join(repoDir, 'target.ts');
writeFileSync(testFile, 'const x = 1;\n');
execSync('git add .', { cwd: repoDir, stdio: 'pipe' });
execSync('GIT_COMMITTER_DATE="2020-01-01T00:00:00" git commit -m "old initial" --date="2020-01-01T00:00:00"', {
cwd: repoDir,
stdio: 'pipe',
});

// Modify without committing (simulates another agent's uncommitted edit)
writeFileSync(testFile, 'const x = 2;\n');

const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: testFile },
cwd: repoDir,
};

const result = await freshness(payload);
// File was just modified on disk and has uncommitted changes
expect(result).toBeDefined();
expect(result!.hookSpecificOutput).toBeDefined();
expect(result!.hookSpecificOutput!.additionalContext).toContain('[freshness]');
expect(result!.hookSpecificOutput!.additionalContext).toContain('uncommitted changes');
expect(result!.hookSpecificOutput!.permissionDecision).toBe('allow');
});

test('warns for recently committed file by another agent', async () => {
// Create and commit a file very recently (within threshold)
const testFile = join(repoDir, 'recent.ts');
writeFileSync(testFile, 'const y = 1;\n');
execSync('git add . && git commit -m "recent change"', { cwd: repoDir, stdio: 'pipe' });

const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: testFile },
cwd: repoDir,
};

const result = await freshness(payload);
// The commit was just made (< 120s ago) by "other-agent" (not "test-agent")
if (result) {
expect(result.hookSpecificOutput).toBeDefined();
expect(result.hookSpecificOutput!.additionalContext).toContain('[freshness]');
expect(result.hookSpecificOutput!.permissionDecision).toBe('allow');
}
// May also be undefined if the commit age check is borderline — both are valid
});

test('returns undefined for old files', async () => {
// Create a file that was committed long ago — use GIT_COMMITTER_DATE
const testFile = join(repoDir, 'old.ts');
writeFileSync(testFile, 'const old = true;\n');
execSync('git add .', { cwd: repoDir, stdio: 'pipe' });
execSync('GIT_COMMITTER_DATE="2020-01-01T00:00:00" git commit -m "old commit" --date="2020-01-01T00:00:00"', {
cwd: repoDir,
stdio: 'pipe',
});

// Touch the file to set mtime to a distant past
execSync(`touch -t 202001010000 ${testFile}`, { stdio: 'pipe' });

const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: testFile },
cwd: repoDir,
};

const result = await freshness(payload);
expect(result).toBeUndefined();
});

test('skips warning when current agent is the author', async () => {
// Set git user to match GENIE_AGENT_NAME
execSync('git config user.name "test-agent"', { cwd: repoDir, stdio: 'pipe' });

const testFile = join(repoDir, 'mine.ts');
writeFileSync(testFile, 'const mine = true;\n');
execSync('git add . && git commit -m "my change"', { cwd: repoDir, stdio: 'pipe' });

const payload: HookPayload = {
hook_event_name: 'PreToolUse',
tool_name: 'Read',
tool_input: { file_path: testFile },
cwd: repoDir,
};

const result = await freshness(payload);
// Should not warn because the current agent made the commit
expect(result).toBeUndefined();
});
});
Loading
Loading