Skip to content
Merged

Dev #2521

Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2141287
chore(version): bump to 5.260705.1 [auto-version]
github-actions[bot] Jul 5, 2026
399ac1c
fix(config): resolve genie config dir via GENIE_HOME, lazily
namastex888 Jul 5, 2026
40a1e93
Merge pull request #2519 from automagik-dev/fix/genie-config-genie-home
namastex888 Jul 5, 2026
a8541ae
chore(version): bump to 5.260705.2 [auto-version]
github-actions[bot] Jul 5, 2026
5c69a75
fix(omni-runner): reaction guard, -- terminator, stderr on failure
namastex888 Jul 5, 2026
eee864b
Merge pull request #2520 from automagik-dev/fix/omni-route-reaction-g…
namastex888 Jul 5, 2026
6655415
chore(version): bump to 5.260705.3 [auto-version]
github-actions[bot] Jul 5, 2026
1a140dc
fix(hermes-plugin): install into the active Hermes profile's plugins dir
namastex888 Jul 5, 2026
d78f541
chore(wish): close out hermes-khaw-native-surface bookkeeping
namastex888 Jul 5, 2026
af2614d
Merge pull request #2522 from automagik-dev/fix/install-hermes-profil…
namastex888 Jul 5, 2026
b66d804
Merge pull request #2523 from automagik-dev/chore/wish-hermes-khaw-bo…
namastex888 Jul 5, 2026
c5013f3
chore(version): bump to 5.260705.4 [auto-version]
github-actions[bot] Jul 5, 2026
1ca6f3e
fix(omni-runner): sequence route status acks so the final reaction ne…
namastex888 Jul 5, 2026
396db08
Merge pull request #2524 from automagik-dev/fix/route-ack-ordering
namastex888 Jul 5, 2026
61c1024
fix: close version-race, purge dead v4 verbs from guard nudges and so…
namastex888 Jul 5, 2026
3c4ab5b
Merge pull request #2525 from automagik-dev/fix/release-race-and-v4-n…
namastex888 Jul 5, 2026
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "genie",
"version": "5.260704.3",
"version": "5.260705.3",
"source": "./plugins/genie",
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@automagik/genie",
"version": "5.260704.3",
"version": "5.260705.3",
"description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 — install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion plugins/genie/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genie",
"version": "5.260704.3",
"version": "5.260705.3",
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
"author": {
"name": "Namastex Labs"
Expand Down
2 changes: 1 addition & 1 deletion plugins/genie/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genie-plugin",
"version": "5.260704.3",
"version": "5.260705.3",
"private": true,
"description": "Runtime dependencies for genie bundled CLIs",
"type": "module",
Expand Down
15 changes: 12 additions & 3 deletions src/hooks/__tests__/dispatch-fail-closed-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,30 @@
* the plugin should route additional events is a separate registration concern,
* not this gate's subject.
*/
import { beforeAll, describe, expect, test } from 'bun:test';
import { existsSync } from 'node:fs';
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const REPO_ROOT = join(import.meta.dir, '..', '..', '..');
const DIST = join(REPO_ROOT, 'dist', 'genie.js');

// Isolated global state so "default path" means default CONFIG on every host —
// a real ~/.genie/config.json (e.g. omni approvals enabled) would otherwise
// change the dispatcher's registry and this gate would no longer test the default.
const ISOLATED_HOME = mkdtempSync(join(tmpdir(), 'genie-dispatch-regression-'));
afterAll(() => rmSync(ISOLATED_HOME, { recursive: true, force: true }));
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A5 "driveDispatch\(" src/hooks/__tests__/dispatch-fail-closed-regression.test.ts

Repository: automagik-dev/genie

Length of output: 881


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dispatch-fail-closed-regression.test.ts =="
cat -n src/hooks/__tests__/dispatch-fail-closed-regression.test.ts | sed -n '1,220p'

echo
echo "== nearby tests using isolated home =="
rg -n -A4 -B4 "GENIE_HOME|mkdtempSync|beforeEach|afterEach|afterAll" src/hooks/__tests__/*.ts

Repository: automagik-dev/genie

Length of output: 21916


Use a fresh GENIE_HOME per test. ISOLATED_HOME is shared across both cases and only cleaned up in afterAll, so any dispatch-side writes can leak state between tests. Match the beforeEach/afterEach isolation pattern used in the other hook tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/__tests__/dispatch-fail-closed-regression.test.ts` around lines 44
- 45, The test setup for dispatch fail-closed regression is sharing one
ISOLATED_HOME across both cases, which can leak state between tests. Update the
suite to create a fresh GENIE_HOME per test using the same beforeEach/afterEach
isolation pattern used in the other hook tests, and ensure each test tears down
its own temporary home instead of relying on a single afterAll cleanup.

Source: Coding guidelines


/**
* Drive the built CLI's real hook-dispatch entry point as a subprocess and
* return its stdout. No GENIE_* flags are set — this is the default path CC
* return its stdout. No behavior flags are set — this is the default path CC
* uses, so a fall-open default would show up here as an empty (allow) decision.
* GENIE_HOME only relocates global state, keeping the run host-independent.
*/
async function driveDispatch(payload: unknown): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(['bun', DIST, 'hook', 'dispatch'], {
cwd: REPO_ROOT,
env: { ...process.env, GENIE_HOME: ISOLATED_HOME },
stdin: Buffer.from(JSON.stringify(payload)),
stdout: 'pipe',
stderr: 'pipe',
Expand Down
11 changes: 11 additions & 0 deletions src/hooks/__tests__/omni-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
* byte-identical to a build with no Omni.
*/
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { orchestrationGuard } from '../handlers/orchestration-guard.js';
import { buildOmniRegistry, dispatch, getRegistry, installDispatchRegistry, setRegistry } from '../index.js';
import type { Handler, HandlerResult } from '../types.js';
Expand Down Expand Up @@ -67,19 +70,27 @@ describe('config-gated omni-approval registry', () => {
describe('installDispatchRegistry — config-gated boot seam', () => {
let original: ReadonlyArray<Handler>;
let prev: Record<string, string | undefined>;
let isolatedHome: string;
beforeEach(() => {
original = getRegistry();
prev = {
enabled: process.env.OMNI_APPROVALS_ENABLED,
instance: process.env.OMNI_INSTANCE,
chat: process.env.OMNI_APPROVAL_CHAT,
home: process.env.GENIE_HOME,
};
// Isolate global state: "default config" must not mean the host's real
// ~/.genie/config.json, which may have omni approvals enabled.
isolatedHome = mkdtempSync(join(tmpdir(), 'genie-omni-dispatch-'));
process.env.GENIE_HOME = isolatedHome;
});
afterEach(() => {
setRegistry(original);
restoreEnv('OMNI_APPROVALS_ENABLED', prev.enabled);
restoreEnv('OMNI_INSTANCE', prev.instance);
restoreEnv('OMNI_APPROVAL_CHAT', prev.chat);
restoreEnv('GENIE_HOME', prev.home);
rmSync(isolatedHome, { recursive: true, force: true });
});

test('disabled (no env, default config) → registry has NO omni-approval handler', async () => {
Expand Down
52 changes: 52 additions & 0 deletions src/lib/genie-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import {
genieConfigExists,
getGenieConfigPath,
getGenieDir,
loadGenieConfig,
saveGenieConfig,
} from './genie-config.js';

describe('genie-config GENIE_HOME resolution', () => {
let dir: string;
let prevGenieHome: string | undefined;

beforeEach(() => {
prevGenieHome = process.env.GENIE_HOME;
dir = mkdtempSync(join(tmpdir(), 'genie-config-'));
process.env.GENIE_HOME = dir;
});

afterEach(() => {
if (prevGenieHome === undefined) {
Reflect.deleteProperty(process.env, 'GENIE_HOME');
} else {
process.env.GENIE_HOME = prevGenieHome;
}
rmSync(dir, { recursive: true, force: true });
});

test('config dir and path honor GENIE_HOME lazily (set after import)', () => {
expect(getGenieDir()).toBe(dir);
expect(getGenieConfigPath()).toBe(join(dir, 'config.json'));
});

test('load returns isolated defaults, save/load round-trips inside GENIE_HOME', async () => {
expect(genieConfigExists()).toBe(false);
const config = await loadGenieConfig();
config.setupComplete = true;
await saveGenieConfig(config);
expect(genieConfigExists()).toBe(true);
const reloaded = await loadGenieConfig();
expect(reloaded.setupComplete).toBe(true);
});

test('falls back to ~/.genie when GENIE_HOME is unset', () => {
Reflect.deleteProperty(process.env, 'GENIE_HOME');
expect(getGenieDir()).toBe(join(homedir(), '.genie'));
expect(getGenieConfigPath()).toBe(join(homedir(), '.genie', 'config.json'));
});
});
26 changes: 14 additions & 12 deletions src/lib/genie-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,52 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { type GenieConfig, GenieConfigSchema, type ShortcutsConfig } from '../types/genie-config.js';

const GENIE_DIR = join(homedir(), '.genie');
const GENIE_CONFIG_FILE = join(GENIE_DIR, 'config.json');
import { genieHome } from './workspace.js';

/**
* Get the path to the genie config directory
* Get the path to the genie config directory.
* Honors GENIE_HOME (which relocates ALL global state) and resolves lazily so
* env overrides in tests and spawned subprocesses take effect.
*/
export function getGenieDir(): string {
return GENIE_DIR;
return genieHome();
}

/**
* Get the path to the genie config file
*/
export function getGenieConfigPath(): string {
return GENIE_CONFIG_FILE;
return join(genieHome(), 'config.json');
}
Comment on lines 19 to 21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse getGenieDir() instead of re-deriving genieHome().

getGenieConfigPath() re-calls genieHome() directly rather than composing with getGenieDir(), which already exists for this purpose. Minor duplication of the path-resolution logic.

♻️ Proposed refactor
 export function getGenieConfigPath(): string {
-  return join(genieHome(), 'config.json');
+  return join(getGenieDir(), 'config.json');
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getGenieConfigPath(): string {
return GENIE_CONFIG_FILE;
return join(genieHome(), 'config.json');
}
export function getGenieConfigPath(): string {
return join(getGenieDir(), 'config.json');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/genie-config.ts` around lines 19 - 21, `getGenieConfigPath()` is
duplicating path resolution by calling `genieHome()` directly instead of reusing
the existing `getGenieDir()` helper. Update `getGenieConfigPath()` to compose
the config path from `getGenieDir()` so the path logic stays centralized and
consistent, and keep the change localized to the
`getGenieConfigPath`/`getGenieDir` functions in this module.


/**
* Check if genie config exists
*/
export function genieConfigExists(): boolean {
return existsSync(GENIE_CONFIG_FILE);
return existsSync(getGenieConfigPath());
}

/**
* Ensure the genie config directory exists
*/
function ensureGenieDir(): void {
if (!existsSync(GENIE_DIR)) {
mkdirSync(GENIE_DIR, { recursive: true });
const dir = getGenieDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}

/**
* Load genie config, returning defaults if not found
*/
export async function loadGenieConfig(): Promise<GenieConfig> {
if (!existsSync(GENIE_CONFIG_FILE)) {
const configPath = getGenieConfigPath();
if (!existsSync(configPath)) {
return GenieConfigSchema.parse({});
}

try {
const content = readFileSync(GENIE_CONFIG_FILE, 'utf-8');
const content = readFileSync(configPath, 'utf-8');
const data = JSON.parse(content);
return GenieConfigSchema.parse(data);
} catch (error) {
Expand All @@ -64,7 +66,7 @@ export async function saveGenieConfig(config: GenieConfig): Promise<void> {
try {
const validated = GenieConfigSchema.parse(config);
const content = JSON.stringify(validated, null, 2);
writeFileSync(GENIE_CONFIG_FILE, content, 'utf-8');
writeFileSync(getGenieConfigPath(), content, 'utf-8');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to save genie config: ${message}`);
Expand Down
Loading
Loading