Apply room skill overrides to runner spawn
This commit is contained in:
@@ -108,6 +108,15 @@ const group: RegisteredGroup = {
|
|||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function writeSkill(dir: string, name: string): void {
|
||||||
|
const skillDir = path.join(dir, name);
|
||||||
|
fs.mkdirSync(skillDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(skillDir, 'SKILL.md'),
|
||||||
|
`---\nname: ${name}\ndescription: ${name} skill\n---\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe('prepareGroupEnvironment codex auth handling', () => {
|
describe('prepareGroupEnvironment codex auth handling', () => {
|
||||||
let tempRoot: string;
|
let tempRoot: string;
|
||||||
let previousCwd: string;
|
let previousCwd: string;
|
||||||
@@ -370,6 +379,133 @@ describe('prepareGroupEnvironment codex auth handling', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('prepareGroupEnvironment room skill overrides', () => {
|
||||||
|
let tempRoot: string;
|
||||||
|
let previousCwd: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-agent-skills-'));
|
||||||
|
previousCwd = process.cwd();
|
||||||
|
process.chdir(tempRoot);
|
||||||
|
process.env.EJ_TEST_ROOT = tempRoot;
|
||||||
|
process.env.EJ_TEST_HOME = path.join(tempRoot, 'home');
|
||||||
|
fs.mkdirSync(path.join(process.env.EJ_TEST_HOME, '.codex'), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
mockReadEnvFile.mockReset();
|
||||||
|
mockGetActiveCodexAuthPath.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
delete process.env.EJ_TEST_ROOT;
|
||||||
|
delete process.env.EJ_TEST_HOME;
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters Claude session skills with room overrides at spawn time', () => {
|
||||||
|
mockReadEnvFile.mockReturnValue({});
|
||||||
|
const homeSkills = path.join(
|
||||||
|
process.env.EJ_TEST_HOME!,
|
||||||
|
'.claude',
|
||||||
|
'skills',
|
||||||
|
);
|
||||||
|
const workDir = path.join(tempRoot, 'workdir');
|
||||||
|
const runnerSkills = path.join(tempRoot, 'runners', 'skills');
|
||||||
|
writeSkill(homeSkills, 'claude-keep');
|
||||||
|
writeSkill(path.join(workDir, '.claude', 'skills'), 'workdir-keep');
|
||||||
|
writeSkill(runnerSkills, 'runner-keep');
|
||||||
|
writeSkill(runnerSkills, 'runner-off');
|
||||||
|
|
||||||
|
prepareGroupEnvironment(
|
||||||
|
{ ...group, agentType: 'claude-code', workDir },
|
||||||
|
false,
|
||||||
|
'dc:test',
|
||||||
|
{
|
||||||
|
skillOverrides: [
|
||||||
|
{
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'runner-off',
|
||||||
|
enabled: false,
|
||||||
|
createdAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionSkills = path.join(
|
||||||
|
tempRoot,
|
||||||
|
'sessions',
|
||||||
|
group.folder,
|
||||||
|
'services',
|
||||||
|
'codex-main',
|
||||||
|
'.claude',
|
||||||
|
'skills',
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'claude-keep'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'workdir-keep'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'runner-keep'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'runner-off'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses a session-scoped Codex home when room overrides disable skills', () => {
|
||||||
|
mockReadEnvFile.mockReturnValue({});
|
||||||
|
const codexSkills = path.join(
|
||||||
|
process.env.EJ_TEST_HOME!,
|
||||||
|
'.agents',
|
||||||
|
'skills',
|
||||||
|
);
|
||||||
|
const runnerSkills = path.join(tempRoot, 'runners', 'skills');
|
||||||
|
writeSkill(codexSkills, 'codex-keep');
|
||||||
|
writeSkill(codexSkills, 'codex-off');
|
||||||
|
writeSkill(runnerSkills, 'runner-keep');
|
||||||
|
writeSkill(runnerSkills, 'runner-off');
|
||||||
|
|
||||||
|
const prepared = prepareGroupEnvironment(group, false, 'dc:test', {
|
||||||
|
skillOverrides: [
|
||||||
|
{
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'codex-user',
|
||||||
|
skillName: 'codex-off',
|
||||||
|
enabled: false,
|
||||||
|
createdAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'runner-off',
|
||||||
|
enabled: false,
|
||||||
|
createdAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionRoot = path.join(
|
||||||
|
tempRoot,
|
||||||
|
'sessions',
|
||||||
|
group.folder,
|
||||||
|
'services',
|
||||||
|
'codex-main',
|
||||||
|
);
|
||||||
|
const sessionHome = path.join(sessionRoot, 'home');
|
||||||
|
const sessionSkills = path.join(sessionHome, '.agents', 'skills');
|
||||||
|
expect(prepared.env.HOME).toBe(sessionHome);
|
||||||
|
expect(prepared.env.CODEX_HOME).toBe(path.join(sessionRoot, '.codex'));
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'codex-keep'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'runner-keep'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'codex-off'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(sessionSkills, 'runner-off'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(codexSkills, 'codex-off'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('prepareGroupEnvironment Codex goals handling', () => {
|
describe('prepareGroupEnvironment Codex goals handling', () => {
|
||||||
let tempRoot: string;
|
let tempRoot: string;
|
||||||
let previousCwd: string;
|
let previousCwd: string;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
hasReviewerLease,
|
hasReviewerLease,
|
||||||
} from './service-routing.js';
|
} from './service-routing.js';
|
||||||
import type { AgentType, RegisteredGroup } from './types.js';
|
import type { AgentType, RegisteredGroup } from './types.js';
|
||||||
|
import type { StoredRoomSkillOverride } from './db/rooms.js';
|
||||||
|
|
||||||
// writeCodexApiKeyAuth removed — Codex uses OAuth only.
|
// writeCodexApiKeyAuth removed — Codex uses OAuth only.
|
||||||
// API key auth caused unintended billing.
|
// API key auth caused unintended billing.
|
||||||
@@ -53,6 +54,72 @@ function syncDirectoryEntries(sources: string[], destination: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SkillSyncScope = 'codex-user' | 'claude-user' | 'runner' | 'workdir';
|
||||||
|
|
||||||
|
interface SkillSyncSource {
|
||||||
|
dir: string;
|
||||||
|
scope: SkillSyncScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisabledSkillNamesByScope(
|
||||||
|
overrides: StoredRoomSkillOverride[] | undefined,
|
||||||
|
agentType: AgentType,
|
||||||
|
): Map<SkillSyncScope, Set<string>> {
|
||||||
|
const disabled = new Map<SkillSyncScope, Set<string>>();
|
||||||
|
for (const override of overrides ?? []) {
|
||||||
|
if (override.agentType !== agentType || override.enabled !== false)
|
||||||
|
continue;
|
||||||
|
if (
|
||||||
|
override.skillScope !== 'codex-user' &&
|
||||||
|
override.skillScope !== 'claude-user' &&
|
||||||
|
override.skillScope !== 'runner'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const names = disabled.get(override.skillScope) ?? new Set<string>();
|
||||||
|
names.add(override.skillName);
|
||||||
|
disabled.set(override.skillScope, names);
|
||||||
|
}
|
||||||
|
return disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDisabledSkillOverrides(
|
||||||
|
overrides: StoredRoomSkillOverride[] | undefined,
|
||||||
|
agentType: AgentType,
|
||||||
|
): boolean {
|
||||||
|
return (overrides ?? []).some(
|
||||||
|
(override) =>
|
||||||
|
override.agentType === agentType && override.enabled === false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRoomSkillDirectories(args: {
|
||||||
|
sources: SkillSyncSource[];
|
||||||
|
destination: string;
|
||||||
|
agentType: AgentType;
|
||||||
|
overrides?: StoredRoomSkillOverride[];
|
||||||
|
}): void {
|
||||||
|
const disabledNamesByScope = getDisabledSkillNamesByScope(
|
||||||
|
args.overrides,
|
||||||
|
args.agentType,
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.rmSync(args.destination, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(args.destination, { recursive: true });
|
||||||
|
|
||||||
|
for (const source of args.sources) {
|
||||||
|
if (!fs.existsSync(source.dir)) continue;
|
||||||
|
for (const entry of fs.readdirSync(source.dir, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
if (disabledNamesByScope.get(source.scope)?.has(entry.name)) continue;
|
||||||
|
|
||||||
|
const srcPath = path.join(source.dir, entry.name);
|
||||||
|
const dstPath = path.join(args.destination, entry.name);
|
||||||
|
fs.cpSync(srcPath, dstPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readOptionalPromptFile(
|
function readOptionalPromptFile(
|
||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
@@ -301,6 +368,7 @@ function prepareCodexSessionEnvironment(args: {
|
|||||||
isPairedRoom: boolean;
|
isPairedRoom: boolean;
|
||||||
useFailoverPromptPack: boolean;
|
useFailoverPromptPack: boolean;
|
||||||
memoryBriefing?: string;
|
memoryBriefing?: string;
|
||||||
|
skillOverrides?: StoredRoomSkillOverride[];
|
||||||
}): void {
|
}): void {
|
||||||
// API key auth intentionally removed — Codex uses OAuth only.
|
// API key auth intentionally removed — Codex uses OAuth only.
|
||||||
// Never pass any API key to Codex child process to prevent API billing.
|
// Never pass any API key to Codex child process to prevent API billing.
|
||||||
@@ -385,7 +453,30 @@ function prepareCodexSessionEnvironment(args: {
|
|||||||
|
|
||||||
// Codex reads skills from ~/.agents/skills/ (user-level) and
|
// Codex reads skills from ~/.agents/skills/ (user-level) and
|
||||||
// {workDir}/.agents/skills/ (project-level), NOT from .codex/skills/.
|
// {workDir}/.agents/skills/ (project-level), NOT from .codex/skills/.
|
||||||
// Sync to the user-level path so all Codex sessions can discover them.
|
if (hasDisabledSkillOverrides(args.skillOverrides, 'codex')) {
|
||||||
|
const sessionHomeDir = path.join(args.sessionRootDir, 'home');
|
||||||
|
args.env.HOME = sessionHomeDir;
|
||||||
|
syncRoomSkillDirectories({
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
dir: path.join(os.homedir(), '.claude', 'skills'),
|
||||||
|
scope: 'codex-user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dir: path.join(os.homedir(), '.agents', 'skills'),
|
||||||
|
scope: 'codex-user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dir: path.join(args.projectRoot, 'runners', 'skills'),
|
||||||
|
scope: 'runner',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
destination: path.join(sessionHomeDir, '.agents', 'skills'),
|
||||||
|
agentType: 'codex',
|
||||||
|
overrides: args.skillOverrides,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Preserve the historical global sync path when no room override exists.
|
||||||
const codexSkillsDir = path.join(os.homedir(), '.agents', 'skills');
|
const codexSkillsDir = path.join(os.homedir(), '.agents', 'skills');
|
||||||
syncDirectoryEntries(
|
syncDirectoryEntries(
|
||||||
[
|
[
|
||||||
@@ -394,6 +485,7 @@ function prepareCodexSessionEnvironment(args: {
|
|||||||
],
|
],
|
||||||
codexSkillsDir,
|
codexSkillsDir,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const mcpServerPath = path.join(
|
const mcpServerPath = path.join(
|
||||||
args.projectRoot,
|
args.projectRoot,
|
||||||
@@ -411,7 +503,7 @@ function prepareCodexSessionEnvironment(args: {
|
|||||||
chatJid: args.chatJid,
|
chatJid: args.chatJid,
|
||||||
groupFolder: args.group.folder,
|
groupFolder: args.group.folder,
|
||||||
isMain: args.isMain,
|
isMain: args.isMain,
|
||||||
agentType: args.group.agentType || 'claude-code',
|
agentType: 'codex',
|
||||||
workDir:
|
workDir:
|
||||||
args.env.EJCLAW_WORK_DIR || args.group.workDir || args.projectRoot,
|
args.env.EJCLAW_WORK_DIR || args.group.workDir || args.projectRoot,
|
||||||
});
|
});
|
||||||
@@ -430,6 +522,10 @@ export interface PreparedGroupEnvironment {
|
|||||||
runnerDir: string;
|
runnerDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreparedReadonlySessionEnvironment {
|
||||||
|
codexHomeDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function prepareGroupEnvironment(
|
export function prepareGroupEnvironment(
|
||||||
group: RegisteredGroup,
|
group: RegisteredGroup,
|
||||||
isMain: boolean,
|
isMain: boolean,
|
||||||
@@ -438,6 +534,7 @@ export function prepareGroupEnvironment(
|
|||||||
memoryBriefing?: string;
|
memoryBriefing?: string;
|
||||||
runtimeTaskId?: string;
|
runtimeTaskId?: string;
|
||||||
useTaskScopedSession?: boolean;
|
useTaskScopedSession?: boolean;
|
||||||
|
skillOverrides?: StoredRoomSkillOverride[];
|
||||||
},
|
},
|
||||||
): PreparedGroupEnvironment {
|
): PreparedGroupEnvironment {
|
||||||
const projectRoot = process.cwd();
|
const projectRoot = process.cwd();
|
||||||
@@ -463,12 +560,26 @@ export function prepareGroupEnvironment(
|
|||||||
const workDirClaude = group.workDir
|
const workDirClaude = group.workDir
|
||||||
? path.join(group.workDir, '.claude')
|
? path.join(group.workDir, '.claude')
|
||||||
: null;
|
: null;
|
||||||
const skillSources = [
|
syncRoomSkillDirectories({
|
||||||
path.join(os.homedir(), '.claude', 'skills'),
|
sources: [
|
||||||
...(workDirClaude ? [path.join(workDirClaude, 'skills')] : []),
|
{
|
||||||
path.join(projectRoot, 'runners', 'skills'),
|
dir: path.join(os.homedir(), '.claude', 'skills'),
|
||||||
];
|
scope: 'claude-user',
|
||||||
syncDirectoryEntries(skillSources, path.join(groupSessionsDir, 'skills'));
|
},
|
||||||
|
...(workDirClaude
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
dir: path.join(workDirClaude, 'skills'),
|
||||||
|
scope: 'workdir' as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ dir: path.join(projectRoot, 'runners', 'skills'), scope: 'runner' },
|
||||||
|
],
|
||||||
|
destination: path.join(groupSessionsDir, 'skills'),
|
||||||
|
agentType: 'claude-code',
|
||||||
|
overrides: options?.skillOverrides,
|
||||||
|
});
|
||||||
|
|
||||||
const groupIpcDir = runtimeTaskId
|
const groupIpcDir = runtimeTaskId
|
||||||
? resolveTaskRuntimeIpcPath(group.folder, runtimeTaskId)
|
? resolveTaskRuntimeIpcPath(group.folder, runtimeTaskId)
|
||||||
@@ -566,6 +677,7 @@ export function prepareGroupEnvironment(
|
|||||||
isPairedRoom,
|
isPairedRoom,
|
||||||
useFailoverPromptPack: useCodexReviewFailoverPromptPack,
|
useFailoverPromptPack: useCodexReviewFailoverPromptPack,
|
||||||
memoryBriefing: options?.memoryBriefing,
|
memoryBriefing: options?.memoryBriefing,
|
||||||
|
skillOverrides: options?.skillOverrides,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
prepareClaudeEnvironment({ env, envVars, group });
|
prepareClaudeEnvironment({ env, envVars, group });
|
||||||
@@ -594,7 +706,8 @@ export function prepareReadonlySessionEnvironment(args: {
|
|||||||
ipcDir?: string;
|
ipcDir?: string;
|
||||||
hostIpcDir?: string;
|
hostIpcDir?: string;
|
||||||
workDir?: string;
|
workDir?: string;
|
||||||
}): void {
|
skillOverrides?: StoredRoomSkillOverride[];
|
||||||
|
}): PreparedReadonlySessionEnvironment {
|
||||||
const {
|
const {
|
||||||
sessionDir,
|
sessionDir,
|
||||||
chatJid,
|
chatJid,
|
||||||
@@ -606,6 +719,7 @@ export function prepareReadonlySessionEnvironment(args: {
|
|||||||
ipcDir = '/workspace/ipc',
|
ipcDir = '/workspace/ipc',
|
||||||
hostIpcDir = ipcDir,
|
hostIpcDir = ipcDir,
|
||||||
workDir = '/workspace/project',
|
workDir = '/workspace/project',
|
||||||
|
skillOverrides,
|
||||||
} = args;
|
} = args;
|
||||||
const projectRoot = process.cwd();
|
const projectRoot = process.cwd();
|
||||||
|
|
||||||
@@ -614,11 +728,40 @@ export function prepareReadonlySessionEnvironment(args: {
|
|||||||
ensureClaudeGlobalSettingsFile(sessionDir);
|
ensureClaudeGlobalSettingsFile(sessionDir);
|
||||||
|
|
||||||
// Sync skills from host
|
// Sync skills from host
|
||||||
const skillSources = [
|
syncRoomSkillDirectories({
|
||||||
path.join(os.homedir(), '.claude', 'skills'),
|
sources: [
|
||||||
path.join(projectRoot, 'runners', 'skills'),
|
{
|
||||||
];
|
dir: path.join(os.homedir(), '.claude', 'skills'),
|
||||||
syncDirectoryEntries(skillSources, path.join(sessionDir, 'skills'));
|
scope: agentType === 'codex' ? 'codex-user' : 'claude-user',
|
||||||
|
},
|
||||||
|
{ dir: path.join(projectRoot, 'runners', 'skills'), scope: 'runner' },
|
||||||
|
],
|
||||||
|
destination: path.join(sessionDir, 'skills'),
|
||||||
|
agentType,
|
||||||
|
overrides: skillOverrides,
|
||||||
|
});
|
||||||
|
const codexHomeDir =
|
||||||
|
agentType === 'codex' && hasDisabledSkillOverrides(skillOverrides, 'codex')
|
||||||
|
? sessionDir
|
||||||
|
: undefined;
|
||||||
|
if (codexHomeDir) {
|
||||||
|
syncRoomSkillDirectories({
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
dir: path.join(os.homedir(), '.claude', 'skills'),
|
||||||
|
scope: 'codex-user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dir: path.join(os.homedir(), '.agents', 'skills'),
|
||||||
|
scope: 'codex-user',
|
||||||
|
},
|
||||||
|
{ dir: path.join(projectRoot, 'runners', 'skills'), scope: 'runner' },
|
||||||
|
],
|
||||||
|
destination: path.join(codexHomeDir, '.agents', 'skills'),
|
||||||
|
agentType,
|
||||||
|
overrides: skillOverrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Build CLAUDE.md with role-appropriate prompts (reviewer or arbiter)
|
// Build CLAUDE.md with role-appropriate prompts (reviewer or arbiter)
|
||||||
const claudePlatformPrompt = readPlatformPrompt('claude-code', projectRoot);
|
const claudePlatformPrompt = readPlatformPrompt('claude-code', projectRoot);
|
||||||
@@ -688,4 +831,5 @@ export function prepareReadonlySessionEnvironment(args: {
|
|||||||
} else if (fs.existsSync(sessionClaudeMdPath)) {
|
} else if (fs.existsSync(sessionClaudeMdPath)) {
|
||||||
fs.unlinkSync(sessionClaudeMdPath);
|
fs.unlinkSync(sessionClaudeMdPath);
|
||||||
}
|
}
|
||||||
|
return { codexHomeDir };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ vi.mock('./service-routing.js', () => ({
|
|||||||
hasReviewerLease: vi.fn(() => true),
|
hasReviewerLease: vi.fn(() => true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('./db.js', () => ({
|
||||||
|
getStoredRoomSkillOverrides: vi.fn(() => []),
|
||||||
|
}));
|
||||||
|
|
||||||
// Create a controllable fake ChildProcess
|
// Create a controllable fake ChildProcess
|
||||||
function createFakeProcess() {
|
function createFakeProcess() {
|
||||||
const proc = new EventEmitter() as EventEmitter & {
|
const proc = new EventEmitter() as EventEmitter & {
|
||||||
@@ -108,6 +112,7 @@ vi.mock('child_process', async () => {
|
|||||||
|
|
||||||
import { runAgentProcess, AgentOutput } from './agent-runner.js';
|
import { runAgentProcess, AgentOutput } from './agent-runner.js';
|
||||||
import * as agentRunnerEnvironment from './agent-runner-environment.js';
|
import * as agentRunnerEnvironment from './agent-runner-environment.js';
|
||||||
|
import { getStoredRoomSkillOverrides } from './db.js';
|
||||||
import type { RegisteredGroup } from './types.js';
|
import type { RegisteredGroup } from './types.js';
|
||||||
|
|
||||||
const testGroup: RegisteredGroup = {
|
const testGroup: RegisteredGroup = {
|
||||||
@@ -681,10 +686,61 @@ OUROBOROS_LLM_BACKEND = "codex"
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('agent-runner room skill overrides', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(getStoredRoomSkillOverrides).mockReturnValue([]);
|
||||||
|
fakeProc = createFakeProcess();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes room skill overrides into runner environment setup', async () => {
|
||||||
|
const skillOverride = {
|
||||||
|
chatJid: testInput.chatJid,
|
||||||
|
agentType: 'claude-code' as const,
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'runner-off',
|
||||||
|
enabled: false,
|
||||||
|
createdAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
vi.mocked(getStoredRoomSkillOverrides).mockReturnValue([skillOverride]);
|
||||||
|
const prepareGroupEnvironmentSpy = vi.spyOn(
|
||||||
|
agentRunnerEnvironment,
|
||||||
|
'prepareGroupEnvironment',
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultPromise = runAgentProcess(
|
||||||
|
testGroup,
|
||||||
|
testInput,
|
||||||
|
() => {},
|
||||||
|
async () => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
fakeProc.emit('close', 0);
|
||||||
|
const result = await resultPromise;
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(getStoredRoomSkillOverrides).toHaveBeenCalledWith(testInput.chatJid);
|
||||||
|
expect(prepareGroupEnvironmentSpy).toHaveBeenCalledWith(
|
||||||
|
testGroup,
|
||||||
|
testInput.isMain,
|
||||||
|
testInput.chatJid,
|
||||||
|
expect.objectContaining({
|
||||||
|
skillOverrides: [skillOverride],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('agent-runner Codex goals', () => {
|
describe('agent-runner Codex goals', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(getStoredRoomSkillOverrides).mockReturnValue([]);
|
||||||
fakeProc = createFakeProcess();
|
fakeProc = createFakeProcess();
|
||||||
vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => {
|
vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => {
|
||||||
const str = String(p);
|
const str = String(p);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
prepareGroupEnvironment,
|
prepareGroupEnvironment,
|
||||||
} from './agent-runner-environment.js';
|
} from './agent-runner-environment.js';
|
||||||
import { runSpawnedAgentProcess } from './agent-runner-process.js';
|
import { runSpawnedAgentProcess } from './agent-runner-process.js';
|
||||||
|
import { getStoredRoomSkillOverrides } from './db.js';
|
||||||
export {
|
export {
|
||||||
type AvailableGroup,
|
type AvailableGroup,
|
||||||
writeGroupsSnapshot,
|
writeGroupsSnapshot,
|
||||||
@@ -20,6 +21,7 @@ export {
|
|||||||
} from './agent-runner-snapshot.js';
|
} from './agent-runner-snapshot.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { RegisteredGroup, RoomRoleContext } from './types.js';
|
import { RegisteredGroup, RoomRoleContext } from './types.js';
|
||||||
|
import type { StoredRoomSkillOverride } from './db/rooms.js';
|
||||||
|
|
||||||
export interface AgentInput {
|
export interface AgentInput {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
@@ -50,6 +52,23 @@ export interface AgentOutput {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readRoomSkillOverridesForRunner(
|
||||||
|
chatJid: string,
|
||||||
|
): StoredRoomSkillOverride[] {
|
||||||
|
try {
|
||||||
|
return getStoredRoomSkillOverrides(chatJid);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
err,
|
||||||
|
chatJid,
|
||||||
|
},
|
||||||
|
'Failed to read room skill overrides; falling back to default skills',
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function runAgentProcess(
|
export async function runAgentProcess(
|
||||||
group: RegisteredGroup,
|
group: RegisteredGroup,
|
||||||
input: AgentInput,
|
input: AgentInput,
|
||||||
@@ -65,6 +84,7 @@ export async function runAgentProcess(
|
|||||||
|
|
||||||
// ── Host process mode (owner) ───────────────────────────────────
|
// ── Host process mode (owner) ───────────────────────────────────
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const skillOverrides = readRoomSkillOverridesForRunner(input.chatJid);
|
||||||
const { env, groupDir, runnerDir } = prepareGroupEnvironment(
|
const { env, groupDir, runnerDir } = prepareGroupEnvironment(
|
||||||
group,
|
group,
|
||||||
input.isMain,
|
input.isMain,
|
||||||
@@ -73,6 +93,7 @@ export async function runAgentProcess(
|
|||||||
memoryBriefing: input.memoryBriefing,
|
memoryBriefing: input.memoryBriefing,
|
||||||
runtimeTaskId: input.runtimeTaskId,
|
runtimeTaskId: input.runtimeTaskId,
|
||||||
useTaskScopedSession: input.useTaskScopedSession,
|
useTaskScopedSession: input.useTaskScopedSession,
|
||||||
|
skillOverrides,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,7 +109,7 @@ export async function runAgentProcess(
|
|||||||
(input.roomRoleContext?.role === 'reviewer' ||
|
(input.roomRoleContext?.role === 'reviewer' ||
|
||||||
input.roomRoleContext?.role === 'arbiter')
|
input.roomRoleContext?.role === 'arbiter')
|
||||||
) {
|
) {
|
||||||
prepareReadonlySessionEnvironment({
|
const readonlySession = prepareReadonlySessionEnvironment({
|
||||||
sessionDir: envOverrides.CLAUDE_CONFIG_DIR,
|
sessionDir: envOverrides.CLAUDE_CONFIG_DIR,
|
||||||
chatJid: input.chatJid,
|
chatJid: input.chatJid,
|
||||||
isMain: input.isMain,
|
isMain: input.isMain,
|
||||||
@@ -99,9 +120,13 @@ export async function runAgentProcess(
|
|||||||
ipcDir: env.EJCLAW_IPC_DIR,
|
ipcDir: env.EJCLAW_IPC_DIR,
|
||||||
hostIpcDir: env.EJCLAW_HOST_IPC_DIR,
|
hostIpcDir: env.EJCLAW_HOST_IPC_DIR,
|
||||||
workDir: envOverrides.EJCLAW_WORK_DIR || env.EJCLAW_WORK_DIR,
|
workDir: envOverrides.EJCLAW_WORK_DIR || env.EJCLAW_WORK_DIR,
|
||||||
|
skillOverrides,
|
||||||
});
|
});
|
||||||
if ((group.agentType || 'claude-code') === 'codex') {
|
if ((group.agentType || 'claude-code') === 'codex') {
|
||||||
env.CODEX_HOME = path.join(envOverrides.CLAUDE_CONFIG_DIR, '.codex');
|
env.CODEX_HOME = path.join(envOverrides.CLAUDE_CONFIG_DIR, '.codex');
|
||||||
|
if (readonlySession.codexHomeDir) {
|
||||||
|
env.HOME = readonlySession.codexHomeDir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (input.runId) {
|
if (input.runId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user