mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
Merge remote-tracking branch 'origin/main' into chore/node-22-floor
This commit is contained in:
@@ -17,6 +17,7 @@ const AQE_DIR = path.join(PROJECT_ROOT, '.agentic-qe');
|
||||
const RVF_PATH = path.join(AQE_DIR, 'aqe.rvf');
|
||||
const DB_PATH = path.join(AQE_DIR, 'memory.db');
|
||||
const MAX_AGE_HOURS = 24;
|
||||
const AQE_BIN = process.env.AQE_HOOK_BIN || (process.platform === 'win32' ? 'aqe.cmd' : 'aqe');
|
||||
|
||||
// Mount-local kill switch. `brain export` (below) spawns a native RVF writer
|
||||
// that, on a macOS Docker virtiofs bind mount, deadlocks in a futex and IGNORES
|
||||
@@ -37,7 +38,7 @@ function exportBrain() {
|
||||
const idmap = RVF_PATH + '.idmap.json';
|
||||
if (fs.existsSync(idmap)) fs.unlinkSync(idmap);
|
||||
const result = execFileSync(
|
||||
'npx', ['agentic-qe', 'brain', 'export', '-o', RVF_PATH, '--format', 'rvf'],
|
||||
AQE_BIN, ['brain', 'export', '-o', RVF_PATH, '--format', 'rvf'],
|
||||
{ timeout: 60000, encoding: 'utf-8' }
|
||||
);
|
||||
const m = result.match(/Patterns:\s+(\d+)/);
|
||||
|
||||
+13
-13
@@ -14,9 +14,9 @@
|
||||
* polluting the transcript, and a crash/slow init can surface a hook error.
|
||||
*
|
||||
* Contract:
|
||||
* * resolve a PROJECT-LOCAL AQE bundle and run it via THIS node (no shell, no
|
||||
* .bin/.cmd wrapper) — or fall back to `npx agentic-qe` for npx-only installs
|
||||
* (disable that fallback with AQE_HOOK_NPX=0);
|
||||
* * resolve a PROJECT-LOCAL AQE bundle and run it via THIS node (no shell),
|
||||
* or invoke an already-installed `aqe` binary without package-manager or
|
||||
* network resolution;
|
||||
* * if nothing resolves, no-op (a hook must never block a turn);
|
||||
* * keep stderr OUT of the transcript, but tee fatal native-init markers
|
||||
* (e.g. better-sqlite3 `invalid ELF header`) to a throttled, durable
|
||||
@@ -49,14 +49,14 @@ const args = process.argv.slice(2); // hook subcommand + its args
|
||||
// 2. local source build (this repo: ./dist)
|
||||
// -> run either directly via process.execPath (this node): no shell, no
|
||||
// node_modules/.bin/aqe(.cmd) wrapper, fully cross-platform.
|
||||
// 3. npx fallback (npx-only installs with no project-local
|
||||
// bundle) — `npx -y --prefer-offline agentic-qe hooks ...`. Disable with
|
||||
// AQE_HOOK_NPX=0 (used by tests and by anyone who never wants a network
|
||||
// reach). When disabled and no local bundle exists, we no-op.
|
||||
// 3. installed `aqe` binary (global/managed installs without a local
|
||||
// bundle). AQE_HOOK_BIN is a test/managed-install seam; no package manager
|
||||
// is ever invoked from a lifecycle hook.
|
||||
const candidates = [
|
||||
process.env.AQE_HOOK_BUNDLE,
|
||||
path.join(PROJECT, 'node_modules', 'agentic-qe', 'dist', 'cli', 'bundle.js'),
|
||||
path.join(PROJECT, 'dist', 'cli', 'bundle.js'),
|
||||
];
|
||||
].filter(Boolean);
|
||||
|
||||
let cmd;
|
||||
let cmdArgs;
|
||||
@@ -67,11 +67,9 @@ for (const p of candidates) {
|
||||
if (bundle) {
|
||||
cmd = process.execPath;
|
||||
cmdArgs = [bundle, 'hooks', ...args];
|
||||
} else if (process.env.AQE_HOOK_NPX !== '0') {
|
||||
cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
cmdArgs = ['-y', '--prefer-offline', 'agentic-qe', 'hooks', ...args];
|
||||
} else {
|
||||
process.exit(0); // no project-local AQE and npx disabled -> no-op, never block
|
||||
cmd = process.env.AQE_HOOK_BIN || (process.platform === 'win32' ? 'aqe.cmd' : 'aqe');
|
||||
cmdArgs = ['hooks', ...args];
|
||||
}
|
||||
|
||||
// Fatal init markers that mean the hook ran but its persistence layer is dead
|
||||
@@ -94,7 +92,9 @@ const FATAL_MARKERS = [
|
||||
// harness timeout vs. this shim's own admitted ~30-60s cold start left
|
||||
// routing_outcomes writes silently dropped for a month — nothing recorded
|
||||
// the mismatch because nothing was watching for it.)
|
||||
const SPAWN_TIMEOUT_MS = Number(process.env.AQE_HOOK_TIMEOUT_MS) || 18000;
|
||||
const FAST_HOOKS = new Set(['guard', 'pre-command']);
|
||||
const SPAWN_TIMEOUT_MS = Number(process.env.AQE_HOOK_TIMEOUT_MS)
|
||||
|| (FAST_HOOKS.has(args[0]) ? 2500 : 4500);
|
||||
|
||||
function recordHookHealth(line) {
|
||||
try {
|
||||
|
||||
@@ -252,6 +252,20 @@ npx agentic-qe init --auto --with-codex
|
||||
npx agentic-qe platform setup codex
|
||||
```
|
||||
|
||||
Codex guidance is independent from MCP, hooks, and skills. Choose the eager
|
||||
`AGENTS.md` payload with `--codex-guidance full|compact|none` on either command.
|
||||
`full` is the default. `compact` keeps only the safety and verification
|
||||
contract and is capped at 512 UTF-8 bytes including AQE's ownership sentinels
|
||||
(a conservative ceiling of 171 planning tokens at three bytes per token).
|
||||
`none` removes only well-formed AQE-owned sentinel blocks. It leaves all other
|
||||
`AGENTS.md` bytes untouched and still provisions MCP, hooks, and skills.
|
||||
|
||||
```bash
|
||||
npx agentic-qe init --auto --with-codex --codex-guidance compact
|
||||
npx agentic-qe platform setup codex --codex-guidance none
|
||||
npx agentic-qe platform verify codex --codex-guidance none
|
||||
```
|
||||
|
||||
Ruflo integration is deliberately opt-in. Add it only when you want its
|
||||
development-time coordination guidance and lifecycle hooks:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* verify - Verify a platform's configuration
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { Command, Option } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -93,6 +93,15 @@ export interface PlatformVerificationResult {
|
||||
export interface PlatformVerificationOptions {
|
||||
expectMcp?: boolean;
|
||||
expectRuflo?: boolean;
|
||||
guidancePolicy?: 'full' | 'compact' | 'none';
|
||||
}
|
||||
|
||||
function detectCodexGuidance(content: string): 'full' | 'compact' | 'none' | 'malformed' {
|
||||
const start = '<!-- BEGIN AGENTIC-QE CODEX -->';
|
||||
const end = '<!-- END AGENTIC-QE CODEX -->';
|
||||
const blocks = content.match(/<!-- BEGIN AGENTIC-QE CODEX -->[\s\S]*?<!-- END AGENTIC-QE CODEX -->/g) || [];
|
||||
if (blocks.length === 0) return content.includes(start) || content.includes(end) ? 'malformed' : 'none';
|
||||
return blocks[0]!.includes('Discover AQE tools and skills from their live schemas') ? 'compact' : 'full';
|
||||
}
|
||||
|
||||
/** Inspect the complete installed surface for a platform without changing it. */
|
||||
@@ -125,11 +134,14 @@ export function verifyPlatformConfiguration(
|
||||
|
||||
const rulesPath = path.join(projectRoot, platform.rulesPath);
|
||||
const rulesExists = existsSync(rulesPath);
|
||||
add('Behavioral rules', rulesExists, rulesExists ? platform.rulesPath : `missing: ${platform.rulesPath}`);
|
||||
if (platformId === 'codex' && rulesExists) {
|
||||
const rules = readFileSync(rulesPath, 'utf-8');
|
||||
add('AQE instructions', rules.includes('Quality Engineering Standards (Agentic QE)'),
|
||||
'AGENTS.md contains generated AQE guidance');
|
||||
if (platformId === 'codex') {
|
||||
const expected = options.guidancePolicy ?? 'full';
|
||||
const actual = rulesExists ? detectCodexGuidance(readFileSync(rulesPath, 'utf-8')) : 'none';
|
||||
add('Behavioral rules', expected === 'none' || rulesExists,
|
||||
expected === 'none' ? 'intentionally disabled' : (rulesExists ? platform.rulesPath : `missing: ${platform.rulesPath}`));
|
||||
add('Codex guidance', actual === expected, `selected=${expected}, detected=${actual}`);
|
||||
} else {
|
||||
add('Behavioral rules', rulesExists, rulesExists ? platform.rulesPath : `missing: ${platform.rulesPath}`);
|
||||
}
|
||||
|
||||
if (platformId === 'codex') {
|
||||
@@ -234,7 +246,10 @@ export function createPlatformCommand(): Command {
|
||||
.description('Set up a specific platform configuration')
|
||||
.option('--overwrite', 'Overwrite existing configuration files')
|
||||
.option('--with-ruflo', 'Add optional Ruflo guidance and lifecycle hooks (Codex only)')
|
||||
.action(async (name: string, options: { overwrite?: boolean; withRuflo?: boolean }) => {
|
||||
.addOption(new Option('--codex-guidance <mode>', 'Codex AGENTS.md guidance policy')
|
||||
.choices(['full', 'compact', 'none'])
|
||||
.default('full'))
|
||||
.action(async (name: string, options: { overwrite?: boolean; withRuflo?: boolean; codexGuidance?: 'full' | 'compact' | 'none' }) => {
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
if (!isValidPlatformId(name)) {
|
||||
@@ -264,7 +279,7 @@ export function createPlatformCommand(): Command {
|
||||
`create${capitalize(name)}Installer`,
|
||||
];
|
||||
|
||||
let factory: ((opts: { projectRoot: string; overwrite?: boolean }) => { install: () => Promise<unknown> }) | undefined;
|
||||
let factory: ((opts: { projectRoot: string; overwrite?: boolean; guidancePolicy?: 'full' | 'compact' | 'none' }) => { install: () => Promise<unknown> }) | undefined;
|
||||
for (const fn of possibleNames) {
|
||||
if (typeof installerModule[fn] === 'function') {
|
||||
factory = installerModule[fn];
|
||||
@@ -290,7 +305,10 @@ export function createPlatformCommand(): Command {
|
||||
const installer = factory({
|
||||
projectRoot,
|
||||
overwrite: options.overwrite,
|
||||
...(name === 'codex' ? { includeRuflo: options.withRuflo } : {}),
|
||||
...(name === 'codex' ? {
|
||||
includeRuflo: options.withRuflo,
|
||||
guidancePolicy: options.codexGuidance,
|
||||
} : {}),
|
||||
});
|
||||
|
||||
const result = await installer.install() as {
|
||||
@@ -339,7 +357,10 @@ export function createPlatformCommand(): Command {
|
||||
.description('Verify a platform configuration is correct')
|
||||
.option('--no-mcp', 'Verify a deliberately MCP-free platform installation')
|
||||
.option('--with-ruflo', 'Require optional Ruflo guidance and lifecycle hooks (Codex only)')
|
||||
.action(async (name: string, options: { mcp?: boolean; withRuflo?: boolean }) => {
|
||||
.addOption(new Option('--codex-guidance <mode>', 'Expected Codex AGENTS.md guidance policy')
|
||||
.choices(['full', 'compact', 'none'])
|
||||
.default('full'))
|
||||
.action(async (name: string, options: { mcp?: boolean; withRuflo?: boolean; codexGuidance?: 'full' | 'compact' | 'none' }) => {
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
if (!isValidPlatformId(name)) {
|
||||
@@ -359,6 +380,7 @@ export function createPlatformCommand(): Command {
|
||||
const verification = verifyPlatformConfiguration(projectRoot, name, {
|
||||
expectMcp: options.mcp !== false,
|
||||
expectRuflo: options.withRuflo,
|
||||
guidancePolicy: options.codexGuidance,
|
||||
});
|
||||
for (const check of verification.checks) {
|
||||
const message = ` [${check.passed ? 'pass' : 'fail'}] ${check.label}: ${check.detail}`;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles the 'aqe init' command for system initialization.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { Command, Option } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { createRequire } from 'node:module';
|
||||
import { ICommandHandler, CLIContext } from './interfaces.js';
|
||||
@@ -64,6 +64,9 @@ export class InitHandler implements ICommandHandler {
|
||||
.option('--with-kilocode', 'Include Kilo Code MCP config and custom QE mode')
|
||||
.option('--with-roocode', 'Include Roo Code MCP config and custom QE mode')
|
||||
.option('--with-codex', 'Include OpenAI Codex CLI MCP, AGENTS.md, hooks, and QE skills')
|
||||
.addOption(new Option('--codex-guidance <mode>', 'Codex AGENTS.md guidance policy')
|
||||
.choices(['full', 'compact', 'none'])
|
||||
.default('full'))
|
||||
.option('--with-ruflo', 'Add optional Ruflo guidance and lifecycle hooks to the Codex setup')
|
||||
.option('--with-windsurf', 'Include Windsurf MCP config and rules')
|
||||
.option('--with-continuedev', 'Include Continue.dev MCP config and rules')
|
||||
@@ -212,6 +215,7 @@ export class InitHandler implements ICommandHandler {
|
||||
withRooCode: options.withRoocode,
|
||||
withCodex: options.withCodex,
|
||||
withRuflo: options.withRuflo,
|
||||
codexGuidance: options.codexGuidance,
|
||||
withWindsurf: options.withWindsurf,
|
||||
withContinueDev: options.withContinuedev,
|
||||
noMcp: options.noMcp && !options.withMcp,
|
||||
@@ -535,6 +539,7 @@ Options:
|
||||
--with-n8n Install n8n workflow testing agents and skills
|
||||
--with-opencode Include OpenCode agent/skill provisioning
|
||||
--with-codex Include OpenAI Codex hooks, instructions, skills, and MCP
|
||||
--codex-guidance <mode> Codex AGENTS.md guidance: full, compact, or none
|
||||
--with-ruflo Opt into Ruflo guidance and lifecycle hooks for Codex
|
||||
--auto-migrate Automatically migrate from v2 if detected
|
||||
--with-claude-flow Force Claude Flow integration setup
|
||||
@@ -610,10 +615,14 @@ export interface InitJsonOutput {
|
||||
mcpConfigured: boolean;
|
||||
claudeMdGenerated: boolean;
|
||||
workersStarted: number;
|
||||
n8nInstalled?: {
|
||||
agents: number;
|
||||
skills: number;
|
||||
};
|
||||
n8nInstalled?: {
|
||||
agents: number;
|
||||
skills: number;
|
||||
};
|
||||
codexGuidance?: {
|
||||
policy: 'full' | 'compact' | 'none';
|
||||
ownedBytes: number;
|
||||
};
|
||||
};
|
||||
totalDurationMs: number;
|
||||
timestamp: string;
|
||||
@@ -643,6 +652,7 @@ interface InitOptions {
|
||||
withRoocode?: boolean;
|
||||
withCodex?: boolean;
|
||||
withRuflo?: boolean;
|
||||
codexGuidance?: 'full' | 'compact' | 'none';
|
||||
withWindsurf?: boolean;
|
||||
withContinuedev?: boolean;
|
||||
withAllPlatforms?: boolean;
|
||||
|
||||
@@ -81,6 +81,14 @@ export interface BrowserEngineInstallResult {
|
||||
platformHint?: PlatformHint;
|
||||
}
|
||||
|
||||
export type BrowserEngineDetectionStatus = 'ready' | 'cli-missing' | 'payload-missing';
|
||||
|
||||
export interface BrowserEngineDetectionResult {
|
||||
status: BrowserEngineDetectionStatus;
|
||||
version?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PlatformHint {
|
||||
/**
|
||||
* Short machine-readable tag for the condition being reported. Consumers
|
||||
@@ -201,6 +209,31 @@ export function detectVibium(spawner: Spawner = defaultSpawner, timeoutMs = 5_00
|
||||
return match ? match[1] : raw.split(/\s+/)[0] || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Vibium readiness without changing the host. A usable browser engine
|
||||
* requires both the CLI and Vibium's same-revision Chrome/chromedriver payload.
|
||||
*/
|
||||
export function detectBrowserEngine(
|
||||
spawner: Spawner = defaultSpawner,
|
||||
timeoutMs = 5_000
|
||||
): BrowserEngineDetectionResult {
|
||||
const version = detectVibium(spawner, timeoutMs);
|
||||
if (!version) return { status: 'cli-missing' };
|
||||
|
||||
const payload = tryRun(spawner, 'vibium', ['is-installed'], timeoutMs);
|
||||
if (payload.status === 0) return { status: 'ready', version };
|
||||
|
||||
return {
|
||||
status: 'payload-missing',
|
||||
version,
|
||||
message:
|
||||
payload.stderr?.trim() ||
|
||||
payload.stdout?.trim() ||
|
||||
toErrorMessage(payload.error) ||
|
||||
'Vibium Chrome/chromedriver payload is missing or incomplete',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Vibium via `npm install -g`. Returns a structured result the
|
||||
* assets phase can log/summarize — never throws for expected failures.
|
||||
@@ -217,11 +250,11 @@ export function installBrowserEngine(
|
||||
const platformProbe = options.platformProbe || defaultPlatformProbe;
|
||||
const platformHint = diagnosePlatform(platformProbe);
|
||||
|
||||
const alreadyInstalled = detectVibium(spawner);
|
||||
if (alreadyInstalled) {
|
||||
const existing = detectBrowserEngine(spawner);
|
||||
if (existing.status === 'ready') {
|
||||
return {
|
||||
status: 'already-installed',
|
||||
version: alreadyInstalled,
|
||||
version: existing.version,
|
||||
packageSpec,
|
||||
platformHint,
|
||||
};
|
||||
@@ -252,6 +285,18 @@ export function installBrowserEngine(
|
||||
};
|
||||
}
|
||||
|
||||
const version = detectVibium(spawner) || 'unknown';
|
||||
return { status: 'installed', version, packageSpec, platformHint };
|
||||
const verified = detectBrowserEngine(spawner);
|
||||
if (verified.status !== 'ready') {
|
||||
const reason = verified.message ? `: ${verified.message}` : '';
|
||||
return {
|
||||
status: 'install-failed',
|
||||
packageSpec,
|
||||
platformHint,
|
||||
message:
|
||||
`npm completed, but Vibium readiness verification failed (${verified.status})${reason}. ` +
|
||||
`Run \`npm install -g ${packageSpec}\`, then verify with \`vibium is-installed\`.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'installed', version: verified.version, packageSpec, platformHint };
|
||||
}
|
||||
|
||||
+102
-31
@@ -35,6 +35,8 @@ export interface CodexInstallerOptions {
|
||||
installMcp?: boolean;
|
||||
/** Install optional Ruflo guidance, adapter, runtime, and lifecycle groups. */
|
||||
includeRuflo?: boolean;
|
||||
/** Eager AGENTS.md guidance policy. Defaults to full. */
|
||||
guidancePolicy?: CodexGuidancePolicy;
|
||||
/**
|
||||
* Memory backend for this install. 'memory' => database-free: the MCP config
|
||||
* is written to run in-memory (AQE_MEMORY_BACKEND=memory, no AQE_MEMORY_PATH). (#533)
|
||||
@@ -42,6 +44,13 @@ export interface CodexInstallerOptions {
|
||||
memoryBackend?: 'memory' | 'sqlite' | 'agentdb' | 'hybrid';
|
||||
}
|
||||
|
||||
export type CodexGuidancePolicy = 'full' | 'compact' | 'none';
|
||||
export const CODEX_COMPACT_GUIDANCE_MAX_BYTES = 512;
|
||||
|
||||
const COMPACT_CODEX_GUIDANCE = `# Agentic QE
|
||||
|
||||
Preserve project data, especially .agentic-qe/memory.db. Read affected code and tests before editing, validate inputs at system boundaries, and run focused checks before broader gates. Discover AQE tools and skills from their live schemas.`;
|
||||
|
||||
export interface CodexInstallResult {
|
||||
success: boolean;
|
||||
mcpConfigured: boolean;
|
||||
@@ -53,6 +62,8 @@ export interface CodexInstallResult {
|
||||
agentsMdPath: string;
|
||||
hooksPath: string;
|
||||
skillsPath: string;
|
||||
guidancePolicy: CodexGuidancePolicy;
|
||||
ownedGuidanceBytes: number;
|
||||
/** Per-component outcomes. Legacy booleans above remain supported. */
|
||||
components: {
|
||||
mcp: CodexComponentOutcome;
|
||||
@@ -99,6 +110,8 @@ export class CodexInstaller {
|
||||
agentsMdPath: '',
|
||||
hooksPath: join(this.projectRoot, '.codex', 'hooks.json'),
|
||||
skillsPath: join(this.projectRoot, '.agents', 'skills'),
|
||||
guidancePolicy: this.options.guidancePolicy ?? 'full',
|
||||
ownedGuidanceBytes: 0,
|
||||
components: {
|
||||
mcp: { status: 'skipped' },
|
||||
rules: { status: 'skipped' },
|
||||
@@ -137,23 +150,49 @@ export class CodexInstaller {
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate AGENTS.md behavioral rules
|
||||
const policy = this.options.guidancePolicy ?? 'full';
|
||||
const rules = this.generator.generateBehavioralRules('codex');
|
||||
const agentsMdPath = join(this.projectRoot, rules.path);
|
||||
result.agentsMdPath = agentsMdPath;
|
||||
|
||||
const agentsMdPath = join(this.projectRoot, rules.path);
|
||||
result.agentsMdPath = agentsMdPath;
|
||||
|
||||
const rulesExist = existsSync(agentsMdPath);
|
||||
if (!rulesExist || this.overwrite) {
|
||||
if (rulesExist && this.overwrite) {
|
||||
const merged = this.mergeExistingAgentsMd(agentsMdPath, rules.content);
|
||||
writeFileSync(agentsMdPath, merged);
|
||||
} else {
|
||||
writeFileSync(agentsMdPath, this.markAgentsSection(rules.content));
|
||||
if (policy === 'none') {
|
||||
if (!rulesExist) {
|
||||
result.components.rules.status = 'skipped';
|
||||
} else {
|
||||
const existing = readFileSync(agentsMdPath, 'utf-8');
|
||||
const updated = this.removeOwnedAgentsSections(existing);
|
||||
if (updated !== existing) {
|
||||
writeFileSync(agentsMdPath, updated);
|
||||
result.components.rules.status = 'updated';
|
||||
} else {
|
||||
result.components.rules.status = 'preserved';
|
||||
}
|
||||
}
|
||||
} else if (!rulesExist) {
|
||||
const content = policy === 'compact' ? COMPACT_CODEX_GUIDANCE : rules.content;
|
||||
const marked = this.markAgentsSection(content);
|
||||
writeFileSync(agentsMdPath, marked);
|
||||
result.agentsMdInstalled = true;
|
||||
result.components.rules.status = rulesExist ? 'updated' : 'installed';
|
||||
result.ownedGuidanceBytes = Buffer.byteLength(marked);
|
||||
result.components.rules.status = 'installed';
|
||||
} else if (this.overwrite || policy === 'compact') {
|
||||
const content = policy === 'compact' ? COMPACT_CODEX_GUIDANCE : rules.content;
|
||||
const existing = readFileSync(agentsMdPath, 'utf-8');
|
||||
const merged = this.mergeExistingAgentsMdContent(existing, content);
|
||||
if (merged !== existing) {
|
||||
writeFileSync(agentsMdPath, merged);
|
||||
result.agentsMdInstalled = true;
|
||||
result.components.rules.status = 'updated';
|
||||
} else {
|
||||
result.components.rules.status = 'preserved';
|
||||
}
|
||||
result.ownedGuidanceBytes = this.measureOwnedAgentsSection(merged);
|
||||
} else {
|
||||
result.components.rules.status = 'preserved';
|
||||
result.ownedGuidanceBytes = this.measureOwnedAgentsSection(
|
||||
readFileSync(agentsMdPath, 'utf-8'),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.recordComponentFailure(result, 'rules', error);
|
||||
@@ -232,13 +271,22 @@ export class CodexInstaller {
|
||||
description?: string;
|
||||
hooks?: Record<string, unknown[]>;
|
||||
};
|
||||
const isRufloGroup = (value: unknown): boolean =>
|
||||
JSON.stringify(value).includes('ruflo-codex-hook.cjs');
|
||||
const withoutRufloHooks = (groups: unknown[]): unknown[] => groups.flatMap((group) => {
|
||||
if (!group || typeof group !== 'object') return [group];
|
||||
const candidate = group as { hooks?: unknown[] };
|
||||
if (!Array.isArray(candidate.hooks)) {
|
||||
return JSON.stringify(group).includes('ruflo-codex-hook.cjs') ? [] : [group];
|
||||
}
|
||||
const hooks = candidate.hooks.filter(
|
||||
(hook) => !JSON.stringify(hook).includes('ruflo-codex-hook.cjs'),
|
||||
);
|
||||
return hooks.length > 0 ? [{ ...candidate, hooks }] : [];
|
||||
});
|
||||
const generated = {
|
||||
...generatedSource,
|
||||
hooks: Object.fromEntries(Object.entries(generatedSource.hooks || {}).map(([event, groups]) => [
|
||||
event,
|
||||
this.options.includeRuflo ? groups : groups.filter((group) => !isRufloGroup(group)),
|
||||
this.options.includeRuflo ? groups : withoutRufloHooks(groups),
|
||||
])),
|
||||
};
|
||||
const targetConfig = join(targetCodexDir, 'hooks.json');
|
||||
@@ -370,25 +418,48 @@ export class CodexInstaller {
|
||||
* Merge AQE section into existing AGENTS.md.
|
||||
* Replaces a previously marked AQE section or appends a new marked section.
|
||||
*/
|
||||
private mergeExistingAgentsMd(agentsMdPath: string, newContent: string): string {
|
||||
const existing = readFileSync(agentsMdPath, 'utf-8');
|
||||
|
||||
const marked = this.markAgentsSection(newContent);
|
||||
const start = CodexInstaller.AGENTS_START;
|
||||
const end = CodexInstaller.AGENTS_END;
|
||||
const startIndex = existing.indexOf(start);
|
||||
if (startIndex >= 0) {
|
||||
const endIndex = existing.indexOf(end, startIndex);
|
||||
if (endIndex >= 0) {
|
||||
return existing.slice(0, startIndex) + marked
|
||||
+ existing.slice(endIndex + end.length);
|
||||
}
|
||||
}
|
||||
return existing.trimEnd() + '\n\n---\n\n' + marked;
|
||||
private mergeExistingAgentsMdContent(existing: string, newContent: string): string {
|
||||
this.assertOwnedAgentsSectionsWellFormed(existing);
|
||||
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
||||
const marked = this.markAgentsSection(newContent, eol);
|
||||
let replaced = false;
|
||||
const merged = existing.replace(this.ownedAgentsPattern(), () => {
|
||||
if (replaced) return '';
|
||||
replaced = true;
|
||||
return marked;
|
||||
});
|
||||
if (replaced) return merged;
|
||||
if (existing.length === 0) return marked;
|
||||
return existing.trimEnd() + `${eol}${eol}---${eol}${eol}` + marked;
|
||||
}
|
||||
|
||||
private markAgentsSection(content: string): string {
|
||||
return `${CodexInstaller.AGENTS_START}\n${content.trim()}\n${CodexInstaller.AGENTS_END}\n`;
|
||||
private removeOwnedAgentsSections(existing: string): string {
|
||||
this.assertOwnedAgentsSectionsWellFormed(existing);
|
||||
return existing.replace(this.ownedAgentsPattern(), '');
|
||||
}
|
||||
|
||||
private measureOwnedAgentsSection(content: string): number {
|
||||
this.assertOwnedAgentsSectionsWellFormed(content);
|
||||
const match = content.match(this.ownedAgentsPattern());
|
||||
return match ? Buffer.byteLength(match[0]) : 0;
|
||||
}
|
||||
|
||||
private assertOwnedAgentsSectionsWellFormed(content: string): void {
|
||||
const starts = content.match(/<!-- BEGIN AGENTIC-QE CODEX -->/g)?.length ?? 0;
|
||||
const ends = content.match(/<!-- END AGENTIC-QE CODEX -->/g)?.length ?? 0;
|
||||
const complete = content.match(this.ownedAgentsPattern())?.length ?? 0;
|
||||
if (starts !== ends || complete !== starts) {
|
||||
throw new Error('Malformed Agentic QE Codex sentinel in AGENTS.md; file was preserved');
|
||||
}
|
||||
}
|
||||
|
||||
private ownedAgentsPattern(): RegExp {
|
||||
return /<!-- BEGIN AGENTIC-QE CODEX -->[\s\S]*?<!-- END AGENTIC-QE CODEX -->(?:\r?\n)?/g;
|
||||
}
|
||||
|
||||
private markAgentsSection(content: string, eol = '\n'): string {
|
||||
const normalized = content.trim().replace(/\r?\n/g, eol);
|
||||
return `${CodexInstaller.AGENTS_START}${eol}${normalized}${eol}${CodexInstaller.AGENTS_END}${eol}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" guard --file "$TOOL_INPUT_file_path" --json',
|
||||
timeout: 3000,
|
||||
timeout: 3,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -79,7 +79,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" pre-edit --file "$TOOL_INPUT_file_path" --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -90,7 +90,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" pre-command --command "$TOOL_INPUT_command" --json',
|
||||
timeout: 3000,
|
||||
timeout: 3,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -101,7 +101,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" pre-task --description "$TOOL_INPUT_prompt" --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -114,7 +114,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-edit --file "$TOOL_INPUT_file_path" --success --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -125,7 +125,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-command --command "$TOOL_INPUT_command" --success true --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -136,7 +136,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-task --task-id "$TOOL_RESULT_agent_id" --agent "$TOOL_INPUT_subagent_type" --success true --description "$TOOL_INPUT_prompt" --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -151,7 +151,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
// var, so we let the CLI read stdin directly.
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" route --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -163,7 +163,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" session-start --session-id "$SESSION_ID" --json',
|
||||
timeout: 10000,
|
||||
timeout: 10,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -175,7 +175,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" session-end --save-state --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -188,7 +188,7 @@ export async function configureHooks(projectRoot: string, config: AQEInitConfig)
|
||||
// sentinels accumulate at quality_score=-1 indefinitely.
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-route --success true --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -64,6 +64,7 @@ export class ModularInitOrchestrator {
|
||||
withRooCode: options.withRooCode || allPlatforms,
|
||||
withCodex: options.withCodex || allPlatforms,
|
||||
withRuflo: options.withRuflo,
|
||||
codexGuidance: options.codexGuidance,
|
||||
withWindsurf: options.withWindsurf || allPlatforms,
|
||||
withContinueDev: options.withContinueDev || allPlatforms,
|
||||
withAllPlatforms: options.withAllPlatforms,
|
||||
@@ -191,6 +192,10 @@ export class ModularInitOrchestrator {
|
||||
mcpConfigured: (mcpResult?.data as Record<string, unknown> | undefined)?.configured as boolean ?? false,
|
||||
claudeMdGenerated: (claudeMdResult?.data as Record<string, unknown> | undefined)?.generated as boolean ?? false,
|
||||
workersStarted: (workersResult?.data as Record<string, unknown> | undefined)?.workersConfigured as number ?? 0,
|
||||
codexGuidance: (assetsResult?.data as { codexGuidance?: {
|
||||
policy: 'full' | 'compact' | 'none';
|
||||
ownedBytes: number;
|
||||
} } | undefined)?.codexGuidance,
|
||||
},
|
||||
totalDurationMs: Date.now() - startTime,
|
||||
timestamp: new Date(),
|
||||
|
||||
+19
-18
@@ -353,7 +353,7 @@ export class HooksPhase extends BasePhase<HooksResult> {
|
||||
/**
|
||||
* Install the resilient hook shim (aqe-hook.cjs) into .claude/hooks/.
|
||||
* The generated settings.json hooks run `node .../aqe-hook.cjs <cmd>` — the
|
||||
* shim resolves a project-local AQE bundle (or falls back to npx), strips
|
||||
* shim resolves a project-local AQE bundle (or an installed aqe binary), strips
|
||||
* init noise from stdout, swallows stderr, and always exits 0.
|
||||
*/
|
||||
private installHookShim(projectRoot: string, context: InitContext): void {
|
||||
@@ -386,7 +386,7 @@ export class HooksPhase extends BasePhase<HooksResult> {
|
||||
|
||||
// Should not happen for a real install (the file ships in the package).
|
||||
context.services.log(
|
||||
' ⚠ aqe-hook.cjs source not found — hooks will fall back to `npx agentic-qe`',
|
||||
' ⚠ aqe-hook.cjs source not found — lifecycle hooks cannot run until AQE is reinstalled',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -449,6 +449,7 @@ const AQE_DIR = path.join(PROJECT_ROOT, '.agentic-qe');
|
||||
const RVF_PATH = path.join(AQE_DIR, 'aqe.rvf');
|
||||
const DB_PATH = path.join(AQE_DIR, 'memory.db');
|
||||
const MAX_AGE_HOURS = 24;
|
||||
const AQE_BIN = process.env.AQE_HOOK_BIN || (process.platform === 'win32' ? 'aqe.cmd' : 'aqe');
|
||||
|
||||
function log(msg) { process.stderr.write('[brain-checkpoint] ' + msg + '\\n'); }
|
||||
|
||||
@@ -456,7 +457,7 @@ function exportBrain() {
|
||||
if (!fs.existsSync(DB_PATH)) { log('No memory.db, skipping'); return { exported: false }; }
|
||||
try {
|
||||
const result = execFileSync(
|
||||
'npx', ['agentic-qe', 'brain', 'export', '-o', RVF_PATH, '--format', 'rvf'],
|
||||
AQE_BIN, ['brain', 'export', '-o', RVF_PATH, '--format', 'rvf'],
|
||||
{ timeout: 60000, encoding: 'utf-8' }
|
||||
);
|
||||
const m = result.match(/Patterns:\\s+(\\d+)/);
|
||||
@@ -491,8 +492,8 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
/**
|
||||
* Generate hooks configuration
|
||||
*
|
||||
* Uses `npx agentic-qe` for portability - works without global installation.
|
||||
* All hooks use --json output for structured data and fail silently with continueOnError.
|
||||
* Timeout values use Claude Code's native seconds unit. The shim itself uses
|
||||
* a smaller millisecond budget so it can record failures before the host stops it.
|
||||
*/
|
||||
private generateHooksConfig(_config: AQEInitConfig, _projectRoot: string): Record<string, unknown[]> {
|
||||
// Shell injection safety: env vars like $TOOL_INPUT_file_path are set by
|
||||
@@ -511,7 +512,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" guard --file "$TOOL_INPUT_file_path" --json',
|
||||
timeout: 3000,
|
||||
timeout: 3,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -523,7 +524,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" pre-edit --file "$TOOL_INPUT_file_path" --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -535,7 +536,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" pre-command --command "$TOOL_INPUT_command" --json',
|
||||
timeout: 3000,
|
||||
timeout: 3,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -547,7 +548,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" pre-task --description "$TOOL_INPUT_prompt" --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -561,7 +562,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-edit --file "$TOOL_INPUT_file_path" --success --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -572,7 +573,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-command --command "$TOOL_INPUT_command" --success true --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -583,7 +584,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-task --task-id "$TOOL_RESULT_agent_id" --agent "$TOOL_INPUT_subagent_type" --success true --description "$TOOL_INPUT_prompt" --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -599,7 +600,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
// var, so we let the CLI read stdin directly.
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" route --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -612,7 +613,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" session-start --session-id "$SESSION_ID" --json',
|
||||
timeout: 10000,
|
||||
timeout: 10,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -622,7 +623,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'sh -c \'exec node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/brain-checkpoint.cjs" verify --json\'',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -635,7 +636,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" session-end --save-state --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -649,7 +650,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
// rows forever in direct-work sessions (no Task/Agent spawned).
|
||||
type: 'command',
|
||||
command: 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" post-route --success true --json',
|
||||
timeout: 5000,
|
||||
timeout: 5,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
@@ -659,7 +660,7 @@ if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(result)
|
||||
{
|
||||
type: 'command',
|
||||
command: 'sh -c \'exec node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/brain-checkpoint.cjs" export --json\'',
|
||||
timeout: 60000,
|
||||
timeout: 60,
|
||||
continueOnError: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,14 +13,16 @@ import { createSkillsInstaller } from '../skills-installer.js';
|
||||
import { createAgentsInstaller } from '../agents-installer.js';
|
||||
import { createN8nInstaller } from '../n8n-installer.js';
|
||||
import {
|
||||
installBrowserEngine,
|
||||
installBrowserEngine,
|
||||
detectBrowserEngine,
|
||||
diagnosePlatform,
|
||||
DEFAULT_VIBIUM_SPEC,
|
||||
type BrowserEngineInstallResult,
|
||||
} from '../browser-engine-installer.js';
|
||||
import { initializeOverlays } from '../../routing/qe-agent-registry.js';
|
||||
import type { AQEInitConfig } from '../types.js';
|
||||
|
||||
export interface AssetsResult {
|
||||
export interface AssetsResult {
|
||||
skillsInstalled: number;
|
||||
agentsInstalled: number;
|
||||
n8nAgents: number;
|
||||
@@ -31,7 +33,8 @@ export interface AssetsResult {
|
||||
kiroSkills: number;
|
||||
kiroHooks: number;
|
||||
platformsConfigured: string[];
|
||||
browserEngine?: BrowserEngineInstallResult;
|
||||
browserEngine?: BrowserEngineInstallResult;
|
||||
codexGuidance?: { policy: 'full' | 'compact' | 'none'; ownedBytes: number };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +66,8 @@ export class AssetsPhase extends BasePhase<AssetsResult> {
|
||||
let openCodeSkills = 0;
|
||||
let kiroAgents = 0;
|
||||
let kiroSkills = 0;
|
||||
let kiroHooks = 0;
|
||||
let kiroHooks = 0;
|
||||
let codexGuidance: AssetsResult['codexGuidance'];
|
||||
|
||||
if (options.upgrade) {
|
||||
context.services.log(` Upgrade mode: overwriting existing files`);
|
||||
@@ -128,15 +132,14 @@ export class AssetsPhase extends BasePhase<AssetsResult> {
|
||||
try {
|
||||
// Pre-flight check: if vibium is already on PATH, skip the loud
|
||||
// banner so we don't scare users on the common path.
|
||||
const alreadyHere = installBrowserEngine({
|
||||
skip: false,
|
||||
// Use a tiny timeout for the pre-flight detect-only call. The
|
||||
// installer will short-circuit on already-installed without
|
||||
// ever invoking npm.
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
if (alreadyHere.status === 'already-installed') {
|
||||
browserEngine = alreadyHere;
|
||||
const detected = detectBrowserEngine();
|
||||
if (detected.status === 'ready') {
|
||||
browserEngine = {
|
||||
status: 'already-installed',
|
||||
version: detected.version,
|
||||
packageSpec: DEFAULT_VIBIUM_SPEC,
|
||||
platformHint: diagnosePlatform(),
|
||||
};
|
||||
context.services.log(
|
||||
` Browser engine: vibium ${browserEngine.version} (already installed)`
|
||||
);
|
||||
@@ -341,9 +344,11 @@ export class AssetsPhase extends BasePhase<AssetsResult> {
|
||||
overwrite: shouldOverwrite,
|
||||
installMcp: !options.noMcp,
|
||||
includeRuflo: options.withRuflo,
|
||||
guidancePolicy: options.codexGuidance,
|
||||
memoryBackend: options.memoryBackend === 'memory' ? 'memory' : undefined,
|
||||
});
|
||||
const res = await installer.install();
|
||||
const res = await installer.install();
|
||||
codexGuidance = { policy: res.guidancePolicy, ownedBytes: res.ownedGuidanceBytes };
|
||||
if (res.mcpConfigured) platformsConfigured.push('codex');
|
||||
if (res.errors.length > 0) context.services.warn(`Codex warnings: ${res.errors.join(', ')}`);
|
||||
if (res.mcpConfigured) context.services.log(` Codex MCP: ${res.configPath}`);
|
||||
@@ -390,7 +395,8 @@ export class AssetsPhase extends BasePhase<AssetsResult> {
|
||||
kiroSkills,
|
||||
kiroHooks,
|
||||
platformsConfigured,
|
||||
browserEngine,
|
||||
browserEngine,
|
||||
codexGuidance,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,8 @@ export interface InitOptions {
|
||||
withCodex?: boolean;
|
||||
/** Include optional Ruflo guidance and Codex lifecycle integration */
|
||||
withRuflo?: boolean;
|
||||
/** Codex AGENTS.md guidance policy. */
|
||||
codexGuidance?: 'full' | 'compact' | 'none';
|
||||
/** Install Windsurf MCP config and rules */
|
||||
withWindsurf?: boolean;
|
||||
/** Install Continue.dev MCP config and rules */
|
||||
|
||||
+8
-4
@@ -263,10 +263,14 @@ export interface InitResult {
|
||||
claudeMdGenerated: boolean;
|
||||
workersStarted: number;
|
||||
// Platform integration results
|
||||
n8nInstalled?: {
|
||||
agents: number;
|
||||
skills: number;
|
||||
};
|
||||
n8nInstalled?: {
|
||||
agents: number;
|
||||
skills: number;
|
||||
};
|
||||
codexGuidance?: {
|
||||
policy: 'full' | 'compact' | 'none';
|
||||
ownedBytes: number;
|
||||
};
|
||||
};
|
||||
|
||||
totalDurationMs: number;
|
||||
|
||||
@@ -92,6 +92,21 @@ describe('Init Command', () => {
|
||||
});
|
||||
|
||||
describe('Init Options Validation', () => {
|
||||
it('should_parseCompactCodexGuidance_when_requested', async () => {
|
||||
const program = new Command();
|
||||
const handler = new InitHandler(async () => undefined as never);
|
||||
const context = {} as CLIContext;
|
||||
const execute = vi.spyOn(handler, 'execute').mockResolvedValue(undefined);
|
||||
handler.register(program, context);
|
||||
|
||||
await program.parseAsync(['node', 'aqe', 'init', '--with-codex', '--codex-guidance', 'compact']);
|
||||
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ withCodex: true, codexGuidance: 'compact' }),
|
||||
context,
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse --no-statusline as an explicit opt-out', async () => {
|
||||
const program = new Command();
|
||||
const handler = new InitHandler(async () => undefined as never);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('verifyPlatformConfiguration', () => {
|
||||
|
||||
function arrangeCompleteCodexInstall(projectRoot: string): void {
|
||||
write(projectRoot, '.codex/config.toml', '[mcp_servers.agentic-qe]\ncommand = "npx"\n');
|
||||
write(projectRoot, 'AGENTS.md', '# Quality Engineering Standards (Agentic QE)\n');
|
||||
write(projectRoot, 'AGENTS.md', '<!-- BEGIN AGENTIC-QE CODEX -->\n# Quality Engineering Standards (Agentic QE)\n<!-- END AGENTIC-QE CODEX -->\n');
|
||||
write(projectRoot, '.codex/hooks.json', JSON.stringify({
|
||||
hooks: {
|
||||
SessionStart: [{ hooks: [{ command: 'node .codex/hooks/aqe-codex-hook.cjs' }] }],
|
||||
@@ -51,7 +51,7 @@ describe('verifyPlatformConfiguration', () => {
|
||||
'Config syntax',
|
||||
'AQE MCP entry',
|
||||
'Behavioral rules',
|
||||
'AQE instructions',
|
||||
'Codex guidance',
|
||||
'Lifecycle hooks',
|
||||
'Hook adapters',
|
||||
'Hook runtimes',
|
||||
@@ -94,6 +94,36 @@ describe('verifyPlatformConfiguration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should_pass_when_compactCodexGuidanceIsSelected', () => {
|
||||
const projectRoot = root();
|
||||
arrangeCompleteCodexInstall(projectRoot);
|
||||
write(projectRoot, 'AGENTS.md', '<!-- BEGIN AGENTIC-QE CODEX -->\n# Agentic QE\nDiscover AQE tools and skills from their live schemas.\n<!-- END AGENTIC-QE CODEX -->\n');
|
||||
|
||||
const result = verifyPlatformConfiguration(projectRoot, 'codex', { guidancePolicy: 'compact' });
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.checks).toContainEqual({
|
||||
label: 'Codex guidance',
|
||||
passed: true,
|
||||
detail: 'selected=compact, detected=compact',
|
||||
});
|
||||
});
|
||||
|
||||
it('should_pass_when_CodexGuidanceIsIntentionallyDisabled', () => {
|
||||
const projectRoot = root();
|
||||
arrangeCompleteCodexInstall(projectRoot);
|
||||
rmSync(join(projectRoot, 'AGENTS.md'));
|
||||
|
||||
const result = verifyPlatformConfiguration(projectRoot, 'codex', { guidancePolicy: 'none' });
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.checks).toContainEqual({
|
||||
label: 'Behavioral rules',
|
||||
passed: true,
|
||||
detail: 'intentionally disabled',
|
||||
});
|
||||
});
|
||||
|
||||
it('should_requireRufloSkill_only_when_RufloIsRequested', () => {
|
||||
const projectRoot = root();
|
||||
arrangeCompleteCodexInstall(projectRoot);
|
||||
|
||||
@@ -29,9 +29,7 @@ function runShim(
|
||||
try {
|
||||
const stdout = execFileSync('node', [SHIM, ...args], {
|
||||
cwd: REPO_ROOT,
|
||||
// AQE_HOOK_NPX=0 disables the npx fallback so the no-local-bundle cases
|
||||
// no-op deterministically instead of reaching the network.
|
||||
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, AQE_HOOK_NPX: '0', ...extraEnv },
|
||||
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, AQE_HOOK_BIN: '/definitely/missing/aqe', ...extraEnv },
|
||||
encoding: 'utf-8',
|
||||
timeout: 60_000,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@@ -47,12 +45,29 @@ describe('aqe-hook.cjs resilient shim (#510 item 5)', () => {
|
||||
expect(existsSync(SHIM)).toBe(true);
|
||||
});
|
||||
|
||||
it('exits 0 and emits nothing when no project-local AQE exists (never blocks a turn)', () => {
|
||||
it('exits 0 and emits nothing when no AQE executable exists (never blocks a turn)', () => {
|
||||
const r = runShim(['route', '--task', 'x', '--json'], '/nonexistent-project-root');
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should_useResolvedManagedBundle_withoutInvokingPackageManager_when_projectBundleIsAbsent', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'aqe-shim-bin-'));
|
||||
try {
|
||||
const fakeAqe = join(tmp, 'managed-aqe-bundle.cjs');
|
||||
writeFileSync(fakeAqe, `console.log(JSON.stringify({ argv: process.argv.slice(2) }));`);
|
||||
|
||||
const r = runShim(['session-end', '--json'], tmp, {
|
||||
AQE_HOOK_BUNDLE: fakeAqe,
|
||||
});
|
||||
|
||||
expect(r.code).toBe(0);
|
||||
expect(JSON.parse(r.stdout)).toEqual({ argv: ['hooks', 'session-end', '--json'] });
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('exits 0 on an unknown subcommand and never leaks stderr to the caller', () => {
|
||||
// Copy the REAL built bundle into a tmpdir rather than pointing projectDir
|
||||
// at '.' (the repo) — using the repo root here would make this test's
|
||||
@@ -199,7 +214,7 @@ describe('aqe-hook.cjs resilient shim (#510 item 5)', () => {
|
||||
);
|
||||
const stdout = execFileSync('node', [SHIM, 'route', '--json'], {
|
||||
cwd: REPO_ROOT,
|
||||
env: { ...process.env, CLAUDE_PROJECT_DIR: tmp, AQE_HOOK_NPX: '0' },
|
||||
env: { ...process.env, CLAUDE_PROJECT_DIR: tmp, AQE_HOOK_BIN: '/definitely/missing/aqe' },
|
||||
input: JSON.stringify({ prompt: 'find flaky tests in the coverage module' }),
|
||||
encoding: 'utf-8',
|
||||
timeout: 60_000,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const repoRoot = resolve(__dirname, '../../..');
|
||||
|
||||
describe('Claude lifecycle hook contract', () => {
|
||||
it('should_avoidPackageManagerResolution_when_hooksRun', () => {
|
||||
const hookShim = readFileSync(resolve(repoRoot, '.claude/hooks/aqe-hook.cjs'), 'utf8');
|
||||
const checkpoint = readFileSync(resolve(repoRoot, '.claude/helpers/brain-checkpoint.cjs'), 'utf8');
|
||||
|
||||
expect(hookShim).not.toMatch(/spawnSync\(['"]npx/);
|
||||
expect(hookShim).not.toContain('--prefer-offline');
|
||||
expect(checkpoint).not.toMatch(/execFileSync\(\s*['"]npx/);
|
||||
});
|
||||
|
||||
it('should_keepInternalHookBudgetBelowClaudeStopBudget', () => {
|
||||
const hookShim = readFileSync(resolve(repoRoot, '.claude/hooks/aqe-hook.cjs'), 'utf8');
|
||||
|
||||
expect(hookShim).toContain("FAST_HOOKS.has(args[0]) ? 2500 : 4500");
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import type { SpawnSyncReturns } from 'node:child_process';
|
||||
|
||||
import {
|
||||
installBrowserEngine,
|
||||
detectBrowserEngine,
|
||||
detectVibium,
|
||||
diagnosePlatform,
|
||||
DEFAULT_VIBIUM_SPEC,
|
||||
@@ -61,6 +62,50 @@ function makeSpawner(
|
||||
}
|
||||
|
||||
describe('browser-engine-installer', () => {
|
||||
describe('detectBrowserEngine', () => {
|
||||
it('should_reportReady_when_cliAndBrowserPayloadAreUsable', () => {
|
||||
const { spawner, calls } = makeSpawner((call) => {
|
||||
if (call.args[0] === '--version') return canned({ stdout: 'v26.5.31\n' });
|
||||
if (call.args[0] === 'is-installed') return canned({ stdout: 'installed\n' });
|
||||
throw new Error(`unexpected call: ${call.bin} ${call.args.join(' ')}`);
|
||||
});
|
||||
|
||||
const result = detectBrowserEngine(spawner);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', version: '26.5.31' });
|
||||
expect(calls).toEqual([
|
||||
{ bin: 'vibium', args: ['--version'] },
|
||||
{ bin: 'vibium', args: ['is-installed'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should_reportCliMissing_when_versionProbeCannotStart', () => {
|
||||
const { spawner, calls } = makeSpawner(() => enoent());
|
||||
|
||||
const result = detectBrowserEngine(spawner);
|
||||
|
||||
expect(result.status).toBe('cli-missing');
|
||||
expect(calls).toEqual([{ bin: 'vibium', args: ['--version'] }]);
|
||||
});
|
||||
|
||||
it('should_reportPayloadMissing_when_cliExistsWithoutBrowserPair', () => {
|
||||
const { spawner, calls } = makeSpawner((call) =>
|
||||
call.args[0] === '--version'
|
||||
? canned({ stdout: 'v26.5.31\n' })
|
||||
: canned({ status: 1, stderr: 'Chrome and chromedriver revision mismatch' })
|
||||
);
|
||||
|
||||
const result = detectBrowserEngine(spawner);
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'payload-missing',
|
||||
version: '26.5.31',
|
||||
message: 'Chrome and chromedriver revision mismatch',
|
||||
});
|
||||
expect(calls.every((call) => call.bin === 'vibium')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectVibium', () => {
|
||||
it('extracts semver from stdout v-prefixed output', () => {
|
||||
const { spawner, calls } = makeSpawner(() => canned({ stdout: 'v26.3.18\n' }));
|
||||
@@ -122,15 +167,20 @@ describe('browser-engine-installer', () => {
|
||||
expect(DEFAULT_VIBIUM_SPEC).toMatch(/^vibium@/);
|
||||
});
|
||||
|
||||
it('returns already-installed when vibium is on PATH', () => {
|
||||
const { spawner, calls } = makeSpawner(() => canned({ stdout: 'v26.3.18\n' }));
|
||||
it('returns already-installed when vibium and its browser payload are ready', () => {
|
||||
const { spawner, calls } = makeSpawner((call) =>
|
||||
call.args[0] === '--version'
|
||||
? canned({ stdout: 'v26.3.18\n' })
|
||||
: canned({ stdout: 'installed\n' })
|
||||
);
|
||||
const result = installBrowserEngine({ spawner });
|
||||
expect(result.status).toBe('already-installed');
|
||||
// H1 fix: detectVibium now extracts the bare semver, dropping the v prefix.
|
||||
expect(result.version).toBe('26.3.18');
|
||||
// Only the detection call, no npm install attempt.
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]).toEqual({ bin: 'vibium', args: ['--version'] });
|
||||
expect(calls).toEqual([
|
||||
{ bin: 'vibium', args: ['--version'] },
|
||||
{ bin: 'vibium', args: ['is-installed'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns npm-unavailable when npm is missing', () => {
|
||||
@@ -147,17 +197,19 @@ describe('browser-engine-installer', () => {
|
||||
});
|
||||
|
||||
it('installs via npm when vibium is missing but npm works', () => {
|
||||
let vibiumChecks = 0;
|
||||
let installed = false;
|
||||
const { spawner, calls } = makeSpawner((call) => {
|
||||
if (call.bin === 'vibium') {
|
||||
vibiumChecks += 1;
|
||||
// First call: not installed. Second call (post-install): installed.
|
||||
return vibiumChecks === 1 ? enoent() : canned({ stdout: 'v26.3.18' });
|
||||
if (call.bin === 'vibium' && call.args[0] === '--version') {
|
||||
return installed ? canned({ stdout: 'v26.3.18' }) : enoent();
|
||||
}
|
||||
if (call.bin === 'vibium' && call.args[0] === 'is-installed') {
|
||||
return canned({ stdout: 'installed' });
|
||||
}
|
||||
if (call.bin === 'npm' && call.args[0] === '--version') {
|
||||
return canned({ stdout: '10.2.0' });
|
||||
}
|
||||
if (call.bin === 'npm' && call.args[0] === 'install') {
|
||||
installed = true;
|
||||
return canned({ stdout: 'added 1 package' });
|
||||
}
|
||||
throw new Error(`unexpected call: ${call.bin} ${call.args.join(' ')}`);
|
||||
@@ -167,11 +219,49 @@ describe('browser-engine-installer', () => {
|
||||
expect(result.status).toBe('installed');
|
||||
// H1 fix: detectVibium now extracts the bare semver.
|
||||
expect(result.version).toBe('26.3.18');
|
||||
// vibium check, npm check, npm install, vibium re-check
|
||||
expect(calls).toHaveLength(4);
|
||||
// version check, npm check, npm install, version + payload verification
|
||||
expect(calls).toHaveLength(5);
|
||||
expect(calls[2]).toEqual({ bin: 'npm', args: ['install', '-g', DEFAULT_VIBIUM_SPEC] });
|
||||
});
|
||||
|
||||
it('should_reinstall_when_cliExistsButBrowserPayloadIsMissing', () => {
|
||||
let payloadChecks = 0;
|
||||
const { spawner, calls } = makeSpawner((call) => {
|
||||
if (call.bin === 'vibium' && call.args[0] === '--version') {
|
||||
return canned({ stdout: 'v26.5.31' });
|
||||
}
|
||||
if (call.bin === 'vibium' && call.args[0] === 'is-installed') {
|
||||
payloadChecks += 1;
|
||||
return payloadChecks === 1
|
||||
? canned({ status: 1, stderr: 'browser assets missing' })
|
||||
: canned({ stdout: 'installed' });
|
||||
}
|
||||
if (call.bin === 'npm' && call.args[0] === '--version') return canned({ stdout: '10.2.0' });
|
||||
if (call.bin === 'npm' && call.args[0] === 'install') return canned({ stdout: 'added 1 package' });
|
||||
throw new Error(`unexpected call: ${call.bin} ${call.args.join(' ')}`);
|
||||
});
|
||||
|
||||
const result = installBrowserEngine({ spawner });
|
||||
|
||||
expect(result.status).toBe('installed');
|
||||
expect(calls.filter((call) => call.bin === 'npm' && call.args[0] === 'install')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should_reportInstallFailed_when_npmSucceedsButCliVerificationFails', () => {
|
||||
const { spawner } = makeSpawner((call) => {
|
||||
if (call.bin === 'vibium') return enoent();
|
||||
if (call.bin === 'npm' && call.args[0] === '--version') return canned({ stdout: '10.2.0' });
|
||||
if (call.bin === 'npm' && call.args[0] === 'install') return canned({ stdout: 'added 1 package' });
|
||||
throw new Error(`unexpected call: ${call.bin} ${call.args.join(' ')}`);
|
||||
});
|
||||
|
||||
const result = installBrowserEngine({ spawner });
|
||||
|
||||
expect(result.status).toBe('install-failed');
|
||||
expect(result.version).toBeUndefined();
|
||||
expect(result.message).toMatch(/npm install -g vibium/i);
|
||||
});
|
||||
|
||||
it('returns install-failed when npm install exits non-zero', () => {
|
||||
const { spawner } = makeSpawner((call) => {
|
||||
if (call.bin === 'vibium') return enoent();
|
||||
@@ -190,7 +280,8 @@ describe('browser-engine-installer', () => {
|
||||
const result = installBrowserEngine({ spawner, packageSpec: 'vibium@26.3.18' });
|
||||
expect(result.packageSpec).toBe('vibium@26.3.18');
|
||||
// Already-installed detection path should not trigger npm install.
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls.every((call) => call.bin === 'vibium')).toBe(true);
|
||||
});
|
||||
|
||||
describe('Linux ARM64 platform hint (GAP-05)', () => {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
CODEX_COMPACT_GUIDANCE_MAX_BYTES,
|
||||
createCodexInstaller,
|
||||
} from '../../../src/init/codex-installer.js';
|
||||
|
||||
const START = '<!-- BEGIN AGENTIC-QE CODEX -->';
|
||||
const END = '<!-- END AGENTIC-QE CODEX -->';
|
||||
|
||||
describe('Codex guidance policy', () => {
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function projectRoot(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'aqe-codex-guidance-'));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
it('should_writeCompactGuidance_withinDocumentedByteBudget', async () => {
|
||||
const root = projectRoot();
|
||||
|
||||
const result = await createCodexInstaller({
|
||||
projectRoot: root,
|
||||
installMcp: false,
|
||||
guidancePolicy: 'compact',
|
||||
}).install();
|
||||
|
||||
const content = readFileSync(join(root, 'AGENTS.md'), 'utf8');
|
||||
expect(result.guidancePolicy).toBe('compact');
|
||||
expect(result.ownedGuidanceBytes).toBe(Buffer.byteLength(content));
|
||||
expect(result.ownedGuidanceBytes).toBeLessThanOrEqual(CODEX_COMPACT_GUIDANCE_MAX_BYTES);
|
||||
expect(content).toContain('Discover AQE tools and skills from their live schemas');
|
||||
});
|
||||
|
||||
it('should_removeEveryWellFormedOwnedBlock_andPreserveForeignBytes_when_policyIsNone', async () => {
|
||||
const root = projectRoot();
|
||||
const existing = `alpha\r\n${START}\r\nold one\r\n${END}\r\nbeta\r\n${START}\r\nold two\r\n${END}\r\nomega\r\n`;
|
||||
writeFileSync(join(root, 'AGENTS.md'), existing);
|
||||
|
||||
const result = await createCodexInstaller({
|
||||
projectRoot: root,
|
||||
installMcp: false,
|
||||
guidancePolicy: 'none',
|
||||
}).install();
|
||||
|
||||
expect(readFileSync(join(root, 'AGENTS.md'), 'utf8')).toBe('alpha\r\nbeta\r\nomega\r\n');
|
||||
expect(result.ownedGuidanceBytes).toBe(0);
|
||||
expect(result.components.rules.status).toBe('updated');
|
||||
});
|
||||
|
||||
it('should_notCreateAgentsFile_when_nonePolicyHasNoOwnedBlock', async () => {
|
||||
const root = projectRoot();
|
||||
|
||||
const result = await createCodexInstaller({
|
||||
projectRoot: root,
|
||||
installMcp: false,
|
||||
guidancePolicy: 'none',
|
||||
}).install();
|
||||
|
||||
expect(existsSync(join(root, 'AGENTS.md'))).toBe(false);
|
||||
expect(result.ownedGuidanceBytes).toBe(0);
|
||||
});
|
||||
|
||||
it('should_writeOnlyCompactBlock_when_existingAgentsFileIsEmpty', async () => {
|
||||
const root = projectRoot();
|
||||
writeFileSync(join(root, 'AGENTS.md'), '');
|
||||
|
||||
await createCodexInstaller({
|
||||
projectRoot: root,
|
||||
installMcp: false,
|
||||
guidancePolicy: 'compact',
|
||||
}).install();
|
||||
|
||||
expect(readFileSync(join(root, 'AGENTS.md'), 'utf8')).toMatch(/^<!-- BEGIN AGENTIC-QE CODEX -->/);
|
||||
});
|
||||
|
||||
it('should_collapseDuplicateOwnedBlocks_when_compactPolicyIsApplied', async () => {
|
||||
const root = projectRoot();
|
||||
writeFileSync(join(root, 'AGENTS.md'), `before\n${START}\nold one\n${END}\nmiddle\n${START}\nold two\n${END}\nafter\n`);
|
||||
|
||||
await createCodexInstaller({
|
||||
projectRoot: root,
|
||||
installMcp: false,
|
||||
guidancePolicy: 'compact',
|
||||
}).install();
|
||||
|
||||
const content = readFileSync(join(root, 'AGENTS.md'), 'utf8');
|
||||
expect(content.match(/BEGIN AGENTIC-QE CODEX/g)).toHaveLength(1);
|
||||
expect(content).toContain('before\n');
|
||||
expect(content).toContain('middle\n');
|
||||
expect(content).toContain('after\n');
|
||||
});
|
||||
|
||||
it('should_preserveMalformedSentinel_when_policyIsNone', async () => {
|
||||
const root = projectRoot();
|
||||
const malformed = `user bytes\n${START}\nunterminated user-visible text\n`;
|
||||
writeFileSync(join(root, 'AGENTS.md'), malformed);
|
||||
|
||||
const result = await createCodexInstaller({
|
||||
projectRoot: root,
|
||||
installMcp: false,
|
||||
guidancePolicy: 'none',
|
||||
}).install();
|
||||
|
||||
expect(readFileSync(join(root, 'AGENTS.md'), 'utf8')).toBe(malformed);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.components.rules.status).toBe('failed');
|
||||
expect(result.errors[0]).toContain('Malformed Agentic QE Codex sentinel');
|
||||
});
|
||||
|
||||
it('should_remainByteIdempotent_withCrLf_when_compactPolicyRunsAgain', async () => {
|
||||
const root = projectRoot();
|
||||
writeFileSync(join(root, 'AGENTS.md'), 'user line\r\n');
|
||||
const options = { projectRoot: root, installMcp: false, guidancePolicy: 'compact' as const };
|
||||
|
||||
await createCodexInstaller(options).install();
|
||||
const first = readFileSync(join(root, 'AGENTS.md'), 'utf8');
|
||||
await createCodexInstaller(options).install();
|
||||
const second = readFileSync(join(root, 'AGENTS.md'), 'utf8');
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(second).toContain('\r\n');
|
||||
expect(second.match(/BEGIN AGENTIC-QE CODEX/g)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -219,6 +219,32 @@ describe('CodexInstaller', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps AQE hooks when a generated group also contains Ruflo hooks', async () => {
|
||||
mockExistsSync.mockImplementation((value: unknown) => {
|
||||
const file = String(value);
|
||||
if (file.startsWith(projectRoot)) return false;
|
||||
return file.endsWith('/.codex/hooks.json') || file.endsWith('/.codex/hooks');
|
||||
});
|
||||
mockReaddirSync.mockReturnValue([]);
|
||||
mockReadFileSync.mockImplementation((value: unknown) => String(value).endsWith('.codex/hooks.json')
|
||||
? JSON.stringify({ hooks: { SessionStart: [{ matcher: 'startup', hooks: [
|
||||
{ command: 'node .codex/hooks/aqe-codex-hook.cjs session-start' },
|
||||
{ command: 'node .codex/hooks/ruflo-codex-hook.cjs session-restore' },
|
||||
] }] } })
|
||||
: '');
|
||||
|
||||
const { createCodexInstaller } = await import('../../../src/init/codex-installer.js');
|
||||
await createCodexInstaller({ projectRoot, installMcp: false }).install();
|
||||
|
||||
const hooksWrite = mockWriteFileSync.mock.calls.find(
|
||||
(c: unknown[]) => String(c[0]) === join(projectRoot, '.codex', 'hooks.json'),
|
||||
);
|
||||
const generated = JSON.parse(hooksWrite![1] as string);
|
||||
expect(generated.hooks.SessionStart).toEqual([{ matcher: 'startup', hooks: [
|
||||
{ command: 'node .codex/hooks/aqe-codex-hook.cjs session-start' },
|
||||
] }]);
|
||||
});
|
||||
|
||||
it('does not write MCP config when installMcp is false', async () => {
|
||||
const { createCodexInstaller } = await import('../../../src/init/codex-installer.js');
|
||||
const result = await createCodexInstaller({ projectRoot, installMcp: false }).install();
|
||||
|
||||
@@ -623,7 +623,8 @@ describe('InitOrchestrator', () => {
|
||||
expect(settingsContent.hooks.PreToolUse).toBeDefined();
|
||||
expect(settingsContent.hooks.PostToolUse).toBeDefined();
|
||||
expect(settingsContent.hooks.SessionStart).toBeDefined();
|
||||
expect(settingsContent.hooks.Stop).toBeDefined(); // Claude Code uses 'Stop' for session end
|
||||
expect(settingsContent.hooks.Stop).toBeDefined(); // Claude Code uses 'Stop' for session end
|
||||
expect(settingsContent.hooks.Stop.map((group: any) => group.hooks[0].timeout)).toEqual([5, 5]);
|
||||
expect(settingsContent.aqe).toBeDefined();
|
||||
expect(settingsContent.aqe.hooksConfigured).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user