chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
+239
View File
@@ -0,0 +1,239 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { Command } from 'commander';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { registerMacpCommand } from './cli.js';
describe('registerMacpCommand', () => {
function buildProgram(): Command {
const program = new Command();
program.exitOverride(); // prevent process.exit in tests
registerMacpCommand(program);
return program;
}
it('registers a "macp" command on the parent', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp');
expect(macpCmd).toBeDefined();
});
it('registers "macp tasks" subcommand group', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const tasksCmd = macpCmd.commands.find((c) => c.name() === 'tasks');
expect(tasksCmd).toBeDefined();
});
it('registers "macp tasks list" subcommand with --status and --type flags', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const tasksCmd = macpCmd.commands.find((c) => c.name() === 'tasks')!;
const listCmd = tasksCmd.commands.find((c) => c.name() === 'list');
expect(listCmd).toBeDefined();
const optionNames = listCmd!.options.map((o) => o.long);
expect(optionNames).toContain('--status');
expect(optionNames).toContain('--type');
});
it('registers "macp submit" subcommand', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const submitCmd = macpCmd.commands.find((c) => c.name() === 'submit');
expect(submitCmd).toBeDefined();
});
it('registers "macp gate" subcommand with --fail-on flag', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const gateCmd = macpCmd.commands.find((c) => c.name() === 'gate');
expect(gateCmd).toBeDefined();
const optionNames = gateCmd!.options.map((o) => o.long);
expect(optionNames).toContain('--fail-on');
});
it('registers "macp events" subcommand group', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const eventsCmd = macpCmd.commands.find((c) => c.name() === 'events');
expect(eventsCmd).toBeDefined();
});
it('registers "macp events tail" subcommand', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const eventsCmd = macpCmd.commands.find((c) => c.name() === 'events')!;
const tailCmd = eventsCmd.commands.find((c) => c.name() === 'tail');
expect(tailCmd).toBeDefined();
});
it('has all required top-level subcommands', () => {
const program = buildProgram();
const macpCmd = program.commands.find((c) => c.name() === 'macp')!;
const topLevel = macpCmd.commands.map((c) => c.name());
expect(topLevel).toContain('tasks');
expect(topLevel).toContain('submit');
expect(topLevel).toContain('gate');
expect(topLevel).toContain('events');
});
});
/**
* RI-N2 fail-closed CLI behavior: an unimplemented capability is a failure,
* never a success. Every stub exits nonzero with a typed message, and the
* implemented `macp gate` mirrors the typed gate-runner states.
*/
describe('registerMacpCommand fail-closed (RI-N2)', () => {
let tmpDir: string;
function buildProgram(): Command {
const program = new Command();
program.exitOverride();
program.configureOutput({ writeErr: () => {} });
registerMacpCommand(program);
return program;
}
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'macp-cli-failclosed-'));
process.exitCode = 0;
});
afterEach(() => {
process.exitCode = 0;
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('macp tasks list exits nonzero (unimplemented capability)', async () => {
const program = buildProgram();
await program.parseAsync(['macp', 'tasks', 'list'], { from: 'user' });
expect(process.exitCode).not.toBe(0);
});
it('macp submit exits nonzero with a typed MACP_NOT_IMPLEMENTED message', async () => {
const program = buildProgram();
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
await program.parseAsync(['macp', 'submit', 'spec.json'], { from: 'user' });
expect(process.exitCode).not.toBe(0);
const errText = errSpy.mock.calls.map((c) => String(c[0])).join('\n');
expect(errText).toContain('MACP_NOT_IMPLEMENTED');
} finally {
errSpy.mockRestore();
}
});
it('macp events tail exits nonzero (unimplemented capability)', async () => {
const program = buildProgram();
await program.parseAsync(['macp', 'events', 'tail'], { from: 'user' });
expect(process.exitCode).not.toBe(0);
});
it('macp gate runs a green inline command and exits 0', async () => {
const program = buildProgram();
await program.parseAsync(
[
'macp',
'gate',
'exit 0',
'--cwd',
tmpDir,
'--log',
path.join(tmpDir, 'g.log'),
'--timeout',
'10',
],
{ from: 'user' },
);
expect(process.exitCode).toBe(0);
});
it('macp gate exits nonzero on a failing command', async () => {
const program = buildProgram();
await program.parseAsync(
[
'macp',
'gate',
'exit 9',
'--cwd',
tmpDir,
'--log',
path.join(tmpDir, 'g.log'),
'--timeout',
'10',
],
{ from: 'user' },
);
expect(process.exitCode).not.toBe(0);
});
it('macp gate with an unimplemented ci-pipeline capability exits nonzero', async () => {
const program = buildProgram();
const specPath = path.join(tmpDir, 'gates.json');
fs.writeFileSync(specPath, JSON.stringify([{ type: 'ci-pipeline' }]));
await program.parseAsync(
[
'macp',
'gate',
specPath,
'--cwd',
tmpDir,
'--log',
path.join(tmpDir, 'g.log'),
'--timeout',
'10',
],
{ from: 'user' },
);
expect(process.exitCode).not.toBe(0);
});
it('macp gate --simulate completes (exit 0) but reports simulated results', async () => {
const program = buildProgram();
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
try {
await program.parseAsync(
[
'macp',
'gate',
'exit 0',
'--simulate',
'--cwd',
tmpDir,
'--log',
path.join(tmpDir, 'g.log'),
'--timeout',
'10',
],
{ from: 'user' },
);
// completes only because the caller explicitly asked to simulate
expect(process.exitCode).toBe(0);
const outText = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
expect(outText).toContain('simulated');
expect(outText).toContain('SIMULATED');
} finally {
logSpy.mockRestore();
}
});
it('macp gate with an empty spec exits nonzero with a typed error', async () => {
const program = buildProgram();
await program.parseAsync(
[
'macp',
'gate',
' ',
'--cwd',
tmpDir,
'--log',
path.join(tmpDir, 'g.log'),
'--timeout',
'10',
],
{ from: 'user' },
);
expect(process.exitCode).not.toBe(0);
});
});
+202
View File
@@ -0,0 +1,202 @@
import { existsSync, readFileSync } from 'node:fs';
import type { Command } from 'commander';
import { runGates } from './gate-runner.js';
import { MACPCapabilityError, type MacpErrorCode } from './errors.js';
/**
* Load gates from a spec: an existing file (JSON gates array, a JSON object
* with `quality_gates`, a JSON gate object, or one command per line) or an
* inline command string. Fails closed with a typed capability error when the
* spec contains no executable gate definition.
*/
function loadGateSpec(spec: string): unknown[] {
if (existsSync(spec)) {
const raw = readFileSync(spec, 'utf-8');
try {
const parsed = JSON.parse(raw) as unknown;
if (Array.isArray(parsed)) {
if (parsed.length === 0) {
throw new MACPCapabilityError(
'MACP_NO_COMMAND',
'gate-spec',
`gate spec file '${spec}' contains an empty gates array`,
);
}
return parsed;
}
if (typeof parsed === 'object' && parsed !== null) {
const obj = parsed as Record<string, unknown>;
if (Array.isArray(obj['quality_gates'])) {
return obj['quality_gates'];
}
return [parsed];
}
throw new MACPCapabilityError(
'MACP_NO_COMMAND',
'gate-spec',
`gate spec file '${spec}' parsed to ${typeof parsed} — expected a gates array, a task with quality_gates, or a gate object`,
);
} catch (exc) {
if (exc instanceof MACPCapabilityError) throw exc;
// Not JSON — treat each non-empty line as a command gate.
const lines = raw
.split('\n')
.map((l) => l.trim())
.filter((l) => l.length > 0);
if (lines.length > 0) return lines;
throw new MACPCapabilityError(
'MACP_NO_COMMAND',
'gate-spec',
`gate spec file '${spec}' contains no gates`,
);
}
}
if (spec.trim().length > 0) return [spec];
throw new MACPCapabilityError('MACP_NO_COMMAND', 'gate-spec', 'gate spec is empty');
}
/** Print a typed not-implemented failure and exit nonzero (RI-N2 fail-closed). */
function notImplemented(subcommand: string, capability: string, hint: string): void {
const err = new MACPCapabilityError(
'MACP_NOT_IMPLEMENTED',
capability,
`${subcommand} is not implemented in @mosaicstack/macp yet (${capability} capability absent) — ${hint}`,
);
console.error(`[macp] ${subcommand}: ${err.message} [${err.code}]`);
process.exitCode = 1;
}
/**
* Register macp subcommands on an existing Commander program.
* This avoids cross-package Commander version mismatches by using the
* caller's Command instance directly.
*/
export function registerMacpCommand(parent: Command): void {
const macp = parent.command('macp').description('MACP task and gate management');
// ─── tasks ───────────────────────────────────────────────────────────────
const tasks = macp.command('tasks').description('Manage MACP tasks');
tasks
.command('list')
.description('List MACP tasks')
.option(
'--status <status>',
'Filter by task status (pending|running|gated|completed|failed|escalated)',
)
.option(
'--type <type>',
'Filter by task type (coding|deploy|research|review|documentation|infrastructure)',
)
.action((opts: { status?: string; type?: string }) => {
// unimplemented capability — a failure, never a success (RI-N2)
if (opts.status) {
console.log(` status filter: ${opts.status}`);
}
if (opts.type) {
console.log(` type filter: ${opts.type}`);
}
notImplemented('tasks list', 'task-persistence', 'use the macp package programmatically');
});
// ─── submit ──────────────────────────────────────────────────────────────
macp
.command('submit <path>')
.description('Submit a task from a JSON/YAML spec file')
.action((specPath: string) => {
// unimplemented capability — a failure, never a success (RI-N2)
console.log(` spec path: ${specPath}`);
console.log(' task id: (unavailable — no MACP server connected)');
console.log(' status: (unavailable — no MACP server connected)');
notImplemented('submit', 'macp-server', 'use the macp package programmatically');
});
// ─── gate ────────────────────────────────────────────────────────────────
macp
.command('gate <spec>')
.description('Run a gate from a spec string or file path (wraps runGate/runGates)')
.option('--fail-on <mode>', 'Gate fail-on mode: ai|fail|both|none', 'fail')
.option('--cwd <path>', 'Working directory for gate execution', process.cwd())
.option('--log <path>', 'Path to write gate log output', '/tmp/macp-gate.log')
.option('--timeout <seconds>', 'Gate timeout in seconds', '60')
.option(
'--simulate',
'Simulate gates instead of executing them; results are typed simulated and never satisfy a check',
)
.action(
(
spec: string,
opts: { failOn: string; cwd: string; log: string; timeout: string; simulate?: boolean },
) => {
let gates: unknown[];
try {
gates = loadGateSpec(spec);
} catch (exc) {
if (exc instanceof MACPCapabilityError) {
console.error(`[macp] gate: ${exc.message} [${exc.code}]`);
} else {
console.error(`[macp] gate: ${String(exc)}`);
}
process.exitCode = 1;
return;
}
const timeoutSec = Number.parseInt(opts.timeout, 10) || 60;
const eventsPath = `${opts.log}.events.ndjson`;
const { state, gateResults } = runGates(
gates,
opts.cwd,
opts.log,
timeoutSec,
eventsPath,
'macp-cli-gate',
{
simulate: opts.simulate,
},
);
for (const r of gateResults) {
const label = r.command || r.type;
const reason = r.reason ? `${r.reason}` : '';
console.log(`[macp] gate ${r.status}: ${label}${reason}`);
}
if (opts.simulate) {
console.log(
'[macp] SIMULATED run — every result is typed simulated and can never satisfy a gate, dependency, or release check',
);
}
// Simulated runs may complete (exit 0) only because the caller
// explicitly passed --simulate; the typed state stays 'simulated'.
process.exitCode = state === 'passed' || state === 'simulated' ? 0 : 1;
},
);
// ─── events ──────────────────────────────────────────────────────────────
const events = macp.command('events').description('Stream MACP events');
events
.command('tail')
.description('Tail MACP events from the event log (wraps event emitter)')
.option('--file <path>', 'Path to the MACP events NDJSON file')
.option('--follow', 'Follow the file for new events (like tail -f)')
.action((opts: { file?: string; follow?: boolean }) => {
// unimplemented capability — a failure, never a success (RI-N2)
if (opts.file) {
console.log(` file: ${opts.file}`);
}
if (opts.follow) {
console.log(' mode: follow');
}
notImplemented('events tail', 'event-source', 'use the macp package programmatically');
});
}
// Re-export so CLI consumers can surface typed capability codes.
export type { MacpErrorCode };
+236
View File
@@ -0,0 +1,236 @@
import { existsSync, readFileSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import { CredentialError } from './types.js';
import type { ProviderRegistry } from './types.js';
export const DEFAULT_CREDENTIALS_DIR = resolve(join(homedir(), '.config', 'mosaic', 'credentials'));
export const OC_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
export const REDACTED_MARKER = '__OPENCLAW_REDACTED__';
export const PROVIDER_REGISTRY: ProviderRegistry = {
anthropic: {
credential_file: 'anthropic.env',
env_var: 'ANTHROPIC_API_KEY',
oc_env_key: 'ANTHROPIC_API_KEY',
oc_provider_path: 'anthropic',
},
openai: {
credential_file: 'openai.env',
env_var: 'OPENAI_API_KEY',
oc_env_key: 'OPENAI_API_KEY',
oc_provider_path: 'openai',
},
zai: {
credential_file: 'zai.env',
env_var: 'ZAI_API_KEY',
oc_env_key: 'ZAI_API_KEY',
oc_provider_path: 'zai',
},
};
export function extractProvider(modelRef: string): string {
const provider = String(modelRef).trim().split('/')[0]?.trim().toLowerCase() ?? '';
if (!provider) {
throw new CredentialError(`Unable to resolve provider from model reference: '${modelRef}'`);
}
if (!(provider in PROVIDER_REGISTRY)) {
throw new CredentialError(`Unsupported credential provider: ${provider}`);
}
return provider;
}
export function parseDotenv(content: string): Record<string, string> {
const parsed: Record<string, string> = {};
for (const rawLine of content.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
if (!line.includes('=')) continue;
const eqIdx = line.indexOf('=');
const key = line.slice(0, eqIdx).trim();
if (!key) continue;
let value = line.slice(eqIdx + 1).trim();
if (
value.length >= 2 &&
value[0] === value[value.length - 1] &&
(value[0] === '"' || value[0] === "'")
) {
value = value.slice(1, -1);
}
parsed[key] = value;
}
return parsed;
}
function loadCredentialFile(path: string): Record<string, string> {
if (!existsSync(path)) return {};
return parseDotenv(readFileSync(path, 'utf-8'));
}
export function stripJSON5Extensions(content: string): string {
const strings: string[] = [];
const MARKER = '\x00OCSTR';
// 1. Remove full-line comments
content = content.replace(/^\s*\/\/[^\n]*$/gm, '');
// 2. Protect single-quoted strings
content = content.replace(/'([^']*)'/g, (_m, g1: string) => {
const idx = strings.length;
strings.push(g1);
return `${MARKER}${idx}\x00`;
});
// 3. Protect double-quoted strings
content = content.replace(/"([^"]*)"/g, (_m, g1: string) => {
const idx = strings.length;
strings.push(g1);
return `${MARKER}${idx}\x00`;
});
// 4. Structural transforms — safe because strings are now placeholders
content = content.replace(/,\s*([}\]])/g, '$1');
content = content.replace(/\b(\w[\w-]*)\b(?=\s*:)/g, '"$1"');
// 5. Restore string values with proper JSON escaping
for (let i = 0; i < strings.length; i++) {
content = content.replace(`${MARKER}${i}\x00`, JSON.stringify(strings[i]!));
}
return content;
}
export interface PermissionCheckOptions {
ocConfigPath?: string;
}
export function checkOCConfigPermissions(path: string, opts?: { getuid?: () => number }): boolean {
if (!existsSync(path)) return false;
const stat = statSync(path);
const mode = stat.mode & 0o777;
if (mode & 0o077) {
// world/group readable — log warning (matches Python behavior)
}
const getuid = opts?.getuid ?? process.getuid?.bind(process);
if (getuid && stat.uid !== getuid()) {
return false;
}
return true;
}
export function isValidCredential(value: string): boolean {
const stripped = String(value).trim();
return stripped.length > 0 && stripped !== REDACTED_MARKER;
}
function loadOCConfigCredentials(
provider: string,
envVar: string,
ocConfigPath?: string,
): Record<string, string> {
const configPath = ocConfigPath ?? OC_CONFIG_PATH;
if (!existsSync(configPath)) return {};
try {
if (!checkOCConfigPermissions(configPath)) return {};
const rawContent = readFileSync(configPath, 'utf-8');
const config = JSON.parse(stripJSON5Extensions(rawContent)) as Record<string, unknown>;
const providerMeta = PROVIDER_REGISTRY[provider];
const ocEnvKey = providerMeta?.oc_env_key ?? envVar;
const envBlock = config['env'];
if (typeof envBlock === 'object' && envBlock !== null && !Array.isArray(envBlock)) {
const envValue = (envBlock as Record<string, unknown>)[ocEnvKey];
if (typeof envValue === 'string' && isValidCredential(envValue)) {
return { [envVar]: envValue.trim() };
}
}
const models = config['models'];
const providers =
typeof models === 'object' && models !== null && !Array.isArray(models)
? ((models as Record<string, unknown>)['providers'] as Record<string, unknown> | undefined)
: undefined;
const ocProviderPath = providerMeta?.oc_provider_path ?? provider;
if (typeof providers === 'object' && providers !== null && !Array.isArray(providers)) {
const providerConfig = providers[ocProviderPath];
if (
typeof providerConfig === 'object' &&
providerConfig !== null &&
!Array.isArray(providerConfig)
) {
const apiKey = (providerConfig as Record<string, unknown>)['apiKey'];
if (typeof apiKey === 'string' && isValidCredential(apiKey)) {
return { [envVar]: apiKey.trim() };
}
}
}
} catch {
return {};
}
return {};
}
function resolveTargetEnvVar(
provider: string,
taskConfig?: Record<string, unknown> | null,
): string {
const providerMeta = PROVIDER_REGISTRY[provider]!;
const rawCredentials =
typeof taskConfig === 'object' && taskConfig !== null
? (taskConfig['credentials'] as Record<string, unknown> | undefined)
: undefined;
const credentials =
typeof rawCredentials === 'object' && rawCredentials !== null ? rawCredentials : {};
const envVar = String(credentials['provider_key_env'] || providerMeta.env_var).trim();
if (!envVar) {
throw new CredentialError(`Invalid credential env var override for provider: ${provider}`);
}
return envVar;
}
export interface ResolveCredentialsOptions {
taskConfig?: Record<string, unknown> | null;
credentialsDir?: string;
ocConfigPath?: string;
}
export function resolveCredentials(
modelRef: string,
opts?: ResolveCredentialsOptions,
): Record<string, string> {
const provider = extractProvider(modelRef);
const providerMeta = PROVIDER_REGISTRY[provider]!;
const envVar = resolveTargetEnvVar(provider, opts?.taskConfig);
const credentialRoot = resolve(opts?.credentialsDir ?? DEFAULT_CREDENTIALS_DIR);
const credentialFile = join(credentialRoot, providerMeta.credential_file);
// 1. Mosaic credential file
const fileValues = loadCredentialFile(credentialFile);
const fileValue = (fileValues[envVar] ?? '').trim();
if (fileValue) {
return { [envVar]: fileValue };
}
// 2. OpenClaw config
const ocValues = loadOCConfigCredentials(provider, envVar, opts?.ocConfigPath);
if (Object.keys(ocValues).length > 0) {
return ocValues;
}
// 3. Ambient environment
const ambientValue = String(process.env[envVar] ?? '').trim();
if (ambientValue) {
return { [envVar]: ambientValue };
}
throw new CredentialError(
`Missing required credential ${envVar} for provider ${provider} ` +
`(checked ${credentialFile}, OC config, then ambient environment)`,
);
}
+35
View File
@@ -0,0 +1,35 @@
/** Typed error code from the closed MACP_ERROR_CODES set. */
export type MacpErrorCode = (typeof MACP_ERROR_CODES)[number];
/**
* Typed fail-closed capability errors (RI-N2, SDLC-D-035).
*
* MACP must fail closed when a required capability (executor, reviewer,
* command, CI provider, human authority) is absent. These typed codes mirror
* the Forge failure vocabulary (FORGE_NO_*) so both packages speak the same
* language: an unimplemented capability is a failure, never a stub success.
*/
/** Closed set of typed MACP capability error codes. */
export const MACP_ERROR_CODES = [
'MACP_NOT_IMPLEMENTED',
'MACP_NO_COMMAND',
'MACP_NO_REVIEWER',
'MACP_NO_CI_PIPELINE',
'MACP_NO_PROVIDER',
'MACP_AUTHORITY_REQUIRED',
] as const;
/** Raised when a required capability is missing and execution must fail closed. */
export class MACPCapabilityError extends Error {
/** Typed error code from the closed MACP_ERROR_CODES set. */
readonly code: MacpErrorCode;
/** The missing capability, e.g. `ci-provider`, `task-persistence`, `command`. */
readonly capability: string;
constructor(code: MacpErrorCode, capability: string, message: string) {
super(message);
this.name = 'MACPCapabilityError';
this.code = code;
this.capability = capability;
}
}
+35
View File
@@ -0,0 +1,35 @@
import { randomUUID } from 'node:crypto';
import { appendFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import type { MACPEvent } from './types.js';
export function nowISO(): string {
return new Date().toISOString();
}
export function appendEvent(eventsPath: string, event: MACPEvent): void {
mkdirSync(dirname(eventsPath), { recursive: true });
appendFileSync(eventsPath, JSON.stringify(event) + '\n', 'utf-8');
}
export function emitEvent(
eventsPath: string,
eventType: string,
taskId: string,
status: string,
source: string,
message: string,
metadata?: Record<string, unknown>,
): void {
appendEvent(eventsPath, {
event_id: randomUUID(),
event_type: eventType,
task_id: taskId,
status,
timestamp: nowISO(),
source,
message,
metadata: metadata ?? {},
});
}
+429
View File
@@ -0,0 +1,429 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { countAIFindings, normalizeGate, runGate, runGates } from './gate-runner.js';
function makeTmpDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'macp-gate-'));
}
describe('normalizeGate', () => {
it('normalizes a string to mechanical gate', () => {
expect(normalizeGate('echo test')).toEqual({
command: 'echo test',
type: 'mechanical',
fail_on: 'blocker',
});
});
it('normalizes an object gate with defaults', () => {
expect(normalizeGate({ command: 'lint' })).toEqual({
command: 'lint',
type: 'mechanical',
fail_on: 'blocker',
});
});
it('preserves explicit type and fail_on', () => {
expect(normalizeGate({ command: 'review', type: 'ai-review', fail_on: 'any' })).toEqual({
command: 'review',
type: 'ai-review',
fail_on: 'any',
});
});
it('handles non-string/non-object input', () => {
expect(normalizeGate(42)).toEqual({ command: '', type: 'mechanical', fail_on: 'blocker' });
expect(normalizeGate(null)).toEqual({ command: '', type: 'mechanical', fail_on: 'blocker' });
});
});
describe('countAIFindings', () => {
it('returns zeros for non-object', () => {
expect(countAIFindings(null)).toEqual({ blockers: 0, total: 0 });
expect(countAIFindings('string')).toEqual({ blockers: 0, total: 0 });
expect(countAIFindings([])).toEqual({ blockers: 0, total: 0 });
});
it('counts from stats block', () => {
const output = { stats: { blockers: 2, should_fix: 3, suggestions: 1 } };
expect(countAIFindings(output)).toEqual({ blockers: 2, total: 6 });
});
it('counts from findings array when stats has no blockers', () => {
const output = {
stats: { blockers: 0 },
findings: [{ severity: 'blocker' }, { severity: 'warning' }, { severity: 'blocker' }],
};
expect(countAIFindings(output)).toEqual({ blockers: 2, total: 3 });
});
it('uses stats blockers over findings array when stats has blockers', () => {
const output = {
stats: { blockers: 5 },
findings: [{ severity: 'blocker' }, { severity: 'warning' }],
};
// stats.blockers = 5, total from stats = 5+0+0 = 5, findings not used for total since stats total is non-zero
expect(countAIFindings(output)).toEqual({ blockers: 5, total: 5 });
});
it('counts findings length as total when stats has zero total', () => {
const output = {
findings: [{ severity: 'warning' }, { severity: 'info' }],
};
expect(countAIFindings(output)).toEqual({ blockers: 0, total: 2 });
});
});
describe('runGate', () => {
let tmp: string;
let logPath: string;
beforeEach(() => {
tmp = makeTmpDir();
logPath = path.join(tmp, 'gate.log');
});
afterEach(() => {
fs.rmSync(tmp, { recursive: true, force: true });
});
it('passes mechanical gate on exit 0', () => {
const result = runGate('echo hello', tmp, logPath, 30);
expect(result.passed).toBe(true);
expect(result.exit_code).toBe(0);
expect(result.type).toBe('mechanical');
expect(result.output).toContain('hello');
});
it('fails mechanical gate on non-zero exit', () => {
const result = runGate('exit 1', tmp, logPath, 30);
expect(result.passed).toBe(false);
expect(result.exit_code).toBe(1);
});
it('ci-pipeline fails closed without a CI provider (no placeholder pass)', () => {
const result = runGate({ command: 'anything', type: 'ci-pipeline' }, tmp, logPath, 30);
expect(result.passed).toBe(false);
expect(result.status).toBe('capability_failure');
expect(result.capability_code).toBe('MACP_NO_CI_PIPELINE');
expect(result.type).toBe('ci-pipeline');
expect(result.output).not.toBe('CI pipeline gate placeholder');
});
it('empty command is a typed capability failure, never a pass', () => {
const result = runGate({ command: '' }, tmp, logPath, 30);
expect(result.passed).toBe(false);
expect(result.status).toBe('capability_failure');
expect(result.capability_code).toBe('MACP_NO_COMMAND');
});
it('ai-review gate parses JSON output', () => {
const json = JSON.stringify({ stats: { blockers: 0, should_fix: 1 } });
const result = runGate({ command: `echo '${json}'`, type: 'ai-review' }, tmp, logPath, 30);
expect(result.passed).toBe(true);
expect(result.blockers).toBe(0);
expect(result.findings).toBe(1);
});
it('ai-review gate fails on blockers', () => {
const json = JSON.stringify({ stats: { blockers: 2 } });
const result = runGate({ command: `echo '${json}'`, type: 'ai-review' }, tmp, logPath, 30);
expect(result.passed).toBe(false);
expect(result.blockers).toBe(2);
});
it('ai-review gate with fail_on=any fails on any findings', () => {
const json = JSON.stringify({ stats: { blockers: 0, should_fix: 1 } });
const result = runGate(
{ command: `echo '${json}'`, type: 'ai-review', fail_on: 'any' },
tmp,
logPath,
30,
);
expect(result.passed).toBe(false);
expect(result.fail_on).toBe('any');
});
it('ai-review gate fails on invalid JSON output', () => {
const result = runGate({ command: 'echo "not json"', type: 'ai-review' }, tmp, logPath, 30);
expect(result.passed).toBe(false);
expect(result.parse_error).toBeDefined();
});
it('writes to log file', () => {
runGate('echo logged', tmp, logPath, 30);
const log = fs.readFileSync(logPath, 'utf-8');
expect(log).toContain('COMMAND: echo logged');
expect(log).toContain('logged');
expect(log).toContain('EXIT:');
});
});
describe('runGates', () => {
let tmp: string;
let logPath: string;
let eventsPath: string;
beforeEach(() => {
tmp = makeTmpDir();
logPath = path.join(tmp, 'gates.log');
eventsPath = path.join(tmp, 'events.ndjson');
});
afterEach(() => {
fs.rmSync(tmp, { recursive: true, force: true });
});
it('runs multiple gates and returns results', () => {
const { allPassed, gateResults } = runGates(
['echo one', 'echo two'],
tmp,
logPath,
30,
eventsPath,
'task-1',
);
expect(allPassed).toBe(true);
expect(gateResults).toHaveLength(2);
});
it('reports failure when any gate fails', () => {
const { allPassed, gateResults } = runGates(
['echo ok', 'exit 1'],
tmp,
logPath,
30,
eventsPath,
'task-2',
);
expect(allPassed).toBe(false);
expect(gateResults[0]!.passed).toBe(true);
expect(gateResults[1]!.passed).toBe(false);
});
it('emits events for each gate', () => {
runGates(['echo test'], tmp, logPath, 30, eventsPath, 'task-3');
const events = fs
.readFileSync(eventsPath, 'utf-8')
.trim()
.split('\n')
.map((l) => JSON.parse(l));
expect(events).toHaveLength(2); // started + passed
expect(events[0].event_type).toBe('rail.check.started');
expect(events[1].event_type).toBe('rail.check.passed');
});
it('does not silently skip gates with empty command — they become capability failures', () => {
const { gateResults, allPassed, state } = runGates(
[{ command: '', type: 'mechanical' }, 'echo real'],
tmp,
logPath,
30,
eventsPath,
'task-4',
);
expect(gateResults).toHaveLength(2);
expect(gateResults[0]!.status).toBe('capability_failure');
expect(gateResults[1]!.status).toBe('passed');
expect(allPassed).toBe(false);
expect(state).toBe('capability_failure');
});
it('does not skip ci-pipeline even with empty command — typed capability failure', () => {
const { gateResults, allPassed, state } = runGates(
[{ command: '', type: 'ci-pipeline' }],
tmp,
logPath,
30,
eventsPath,
'task-5',
);
expect(gateResults).toHaveLength(1);
expect(gateResults[0]!.passed).toBe(false);
expect(gateResults[0]!.status).toBe('capability_failure');
expect(allPassed).toBe(false);
expect(state).toBe('capability_failure');
});
it('emits failed event with correct message', () => {
runGates(['exit 42'], tmp, logPath, 30, eventsPath, 'task-6');
const events = fs
.readFileSync(eventsPath, 'utf-8')
.trim()
.split('\n')
.map((l) => JSON.parse(l));
const failEvent = events.find(
(e: Record<string, unknown>) => e.event_type === 'rail.check.failed',
);
expect(failEvent).toBeDefined();
expect(failEvent.message).toContain('Gate failed (');
});
});
/**
* RI-N2 / SDLC-D-035 fail-closed controls for the MACP gate runner.
*
* Invariant under test: `passed: true` occurs ONLY when a gate really executed
* and really exited green (`status === 'passed'`). Absent capabilities,
* manual sign-offs, and simulated runs are typed distinctly and can never
* make the aggregate `passed`.
*/
describe('gate-runner fail-closed (RI-N2)', () => {
let tmpDir: string;
let logPath: string;
let eventsPath: string;
beforeEach(() => {
tmpDir = makeTmpDir();
logPath = path.join(tmpDir, 'gate.log');
eventsPath = path.join(tmpDir, 'events.ndjson');
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function run(gates: unknown[], options?: { simulate?: boolean }) {
return runGates(gates, tmpDir, logPath, 10, eventsPath, 'spec-task', options);
}
// ─── positive controls ───────────────────────────────────────────────────
it('a really-executed green command gate still passes', () => {
const result = run([{ command: 'exit 0', type: 'mechanical' }]);
expect(result.gateResults[0]!.status).toBe('passed');
expect(result.gateResults[0]!.passed).toBe(true);
expect(result.allPassed).toBe(true);
expect(result.state).toBe('passed');
});
it('explicit simulate completes and types every result simulated', () => {
const result = run([{ command: 'exit 0', type: 'mechanical' }, 'echo hello'], {
simulate: true,
});
expect(result.gateResults).toHaveLength(2);
for (const gate of result.gateResults) {
expect(gate.status).toBe('simulated');
expect(gate.passed).toBe(false);
}
expect(result.state).toBe('simulated');
});
it('a really-executed red command gate fails with typed status failed', () => {
const result = run([{ command: 'exit 3', type: 'mechanical' }]);
expect(result.gateResults[0]!.status).toBe('failed');
expect(result.gateResults[0]!.passed).toBe(false);
expect(result.allPassed).toBe(false);
expect(result.state).toBe('failed');
});
// ─── negative controls — each asserts typed status AND aggregate not passed ──
it('an empty-command gate is a capability_failure, not skipped and not passed', () => {
const result = run([{ command: '', type: 'mechanical' }]);
// runGates must not silently skip it — it produces a typed result
expect(result.gateResults).toHaveLength(1);
const gate = result.gateResults[0]!;
expect(gate.status).toBe('capability_failure');
expect(gate.capability_code).toBe('MACP_NO_COMMAND');
expect(gate.passed).toBe(false);
// aggregate is not passed
expect(result.allPassed).toBe(false);
expect(result.state).toBe('capability_failure');
expect(result.state).not.toBe('passed');
});
it('a commandless ai-review gate is a typed MACP_NO_REVIEWER capability_failure', () => {
const result = run([{ command: '', type: 'ai-review' }]);
expect(result.gateResults[0]!.status).toBe('capability_failure');
expect(result.gateResults[0]!.capability_code).toBe('MACP_NO_REVIEWER');
expect(result.allPassed).toBe(false);
expect(result.state).not.toBe('passed');
});
it('a ci-pipeline gate without a provider implementation is a capability_failure, never a placeholder pass', () => {
const result = run([{ command: '', type: 'ci-pipeline' }]);
const gate = result.gateResults[0]!;
expect(gate.status).toBe('capability_failure');
expect(gate.capability_code).toBe('MACP_NO_CI_PIPELINE');
expect(gate.passed).toBe(false);
// the old false-success placeholder must be gone
expect(gate.output).not.toBe('CI pipeline gate placeholder');
expect(result.allPassed).toBe(false);
expect(result.state).not.toBe('passed');
});
it('a ci-pipeline gate fails closed even alongside an otherwise green run', () => {
const result = run(['exit 0', { type: 'ci-pipeline', command: 'fake-ci' }]);
expect(result.gateResults[1]!.status).toBe('capability_failure');
expect(result.gateResults[0]!.status).toBe('passed');
expect(result.allPassed).toBe(false);
expect(result.state).toBe('capability_failure');
});
it('a manual gate with no automation enters typed waiting — neither pass nor fail', () => {
const result = run([{ type: 'manual' }]);
const gate = result.gateResults[0]!;
expect(gate.status).toBe('waiting');
expect(gate.passed).toBe(false);
expect(gate.exit_code).toBe(0);
// aggregate is not passed while any gate is waiting
expect(result.allPassed).toBe(false);
expect(result.state).toBe('waiting');
expect(result.state).not.toBe('passed');
});
it('a simulated result can never make the aggregate passed', () => {
const result = run(['exit 0', 'exit 0'], { simulate: true });
expect(result.gateResults.every((g) => g.status === 'simulated')).toBe(true);
expect(result.allPassed).toBe(false);
expect(result.state).toBe('simulated');
expect(result.state).not.toBe('passed');
});
it('waiting dominates an otherwise green aggregate', () => {
const result = run(['exit 0', { type: 'manual' }]);
expect(result.allPassed).toBe(false);
expect(result.state).toBe('waiting');
});
});
describe('runGate fail-closed (RI-N2)', () => {
let tmpDir: string;
let logPath: string;
beforeEach(() => {
tmpDir = makeTmpDir();
logPath = path.join(tmpDir, 'gate.log');
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('simulate: true returns a typed simulated result without executing', () => {
const result = runGate('this-command-does-not-exist-xyz', tmpDir, logPath, 10, {
simulate: true,
});
expect(result.status).toBe('simulated');
expect(result.passed).toBe(false);
expect(result.exit_code).toBe(0);
});
it('normal mode executes for real and types a green gate passed', () => {
const result = runGate('echo ok', tmpDir, logPath, 10);
expect(result.status).toBe('passed');
expect(result.passed).toBe(true);
expect(result.output).toContain('ok');
});
it('a bare string gate normalizes to mechanical and executes', () => {
const result = runGate('exit 7', tmpDir, logPath, 10);
expect(result.type).toBe('mechanical');
expect(result.status).toBe('failed');
expect(result.passed).toBe(false);
});
});
+363
View File
@@ -0,0 +1,363 @@
import { spawnSync } from 'node:child_process';
import { appendFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { emitEvent } from './event-emitter.js';
import { nowISO } from './event-emitter.js';
import type { GateResult, GateStatus, RunGatesResult } from './types.js';
/** Typed reason stamped on every simulated gate result. */
export const SIMULATED_GATE_REASON =
'simulated execution (explicit simulate opt-in): gate was not evaluated by a real implementation';
/** Options for gate execution (RI-N2 fail-closed / explicit simulation). */
export interface RunGateOptions {
/**
* Explicit caller opt-in to simulation. Simulated gates are NOT executed;
* every result is typed `simulated` and never satisfies anything.
*/
simulate?: boolean;
}
export interface NormalizedGate {
command: string;
type: string;
fail_on: string;
}
export function normalizeGate(gate: unknown): NormalizedGate {
if (typeof gate === 'string') {
return { command: gate, type: 'mechanical', fail_on: 'blocker' };
}
if (typeof gate === 'object' && gate !== null && !Array.isArray(gate)) {
const g = gate as Record<string, unknown>;
return {
command: String(g['command'] ?? ''),
type: String(g['type'] ?? 'mechanical'),
fail_on: String(g['fail_on'] ?? 'blocker'),
};
}
return { command: '', type: 'mechanical', fail_on: 'blocker' };
}
export function runShell(
command: string,
cwd: string,
logPath: string,
timeoutSec: number,
): { exitCode: number; output: string; timedOut: boolean } {
mkdirSync(dirname(logPath), { recursive: true });
const header = `\n[${nowISO()}] COMMAND: ${command}\n`;
appendFileSync(logPath, header, 'utf-8');
let exitCode: number;
let output = '';
let timedOut = false;
try {
const result = spawnSync('sh', ['-c', command], {
cwd,
timeout: Math.max(1, timeoutSec) * 1000,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
output = (result.stdout ?? '') + (result.stderr ?? '');
if (result.error && (result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
timedOut = true;
exitCode = 124;
appendFileSync(logPath, `[${nowISO()}] TIMEOUT: exceeded ${timeoutSec}s\n`, 'utf-8');
} else {
exitCode = result.status ?? 1;
}
} catch {
exitCode = 1;
}
if (output) appendFileSync(logPath, output, 'utf-8');
appendFileSync(logPath, `[${nowISO()}] EXIT: ${exitCode}\n`, 'utf-8');
return { exitCode, output, timedOut };
}
export function countAIFindings(parsedOutput: unknown): { blockers: number; total: number } {
if (typeof parsedOutput !== 'object' || parsedOutput === null || Array.isArray(parsedOutput)) {
return { blockers: 0, total: 0 };
}
const obj = parsedOutput as Record<string, unknown>;
const stats = obj['stats'];
let blockers = 0;
let total = 0;
if (typeof stats === 'object' && stats !== null && !Array.isArray(stats)) {
const s = stats as Record<string, unknown>;
blockers = Number(s['blockers']) || 0;
total = blockers + (Number(s['should_fix']) || 0) + (Number(s['suggestions']) || 0);
}
const findings = obj['findings'];
if (Array.isArray(findings)) {
if (blockers === 0) {
blockers = findings.filter(
(f) =>
typeof f === 'object' &&
f !== null &&
(f as Record<string, unknown>)['severity'] === 'blocker',
).length;
}
if (total === 0) {
total = findings.length;
}
}
return { blockers, total };
}
function simulatedResult(gateEntry: NormalizedGate): GateResult {
return {
command: gateEntry.command,
exit_code: 0,
type: gateEntry.type,
output: SIMULATED_GATE_REASON,
timed_out: false,
passed: false,
status: 'simulated',
reason: SIMULATED_GATE_REASON,
};
}
function capabilityFailureResult(
gateEntry: NormalizedGate,
code: GateResult['capability_code'],
reason: string,
): GateResult {
return {
command: gateEntry.command,
exit_code: 1,
type: gateEntry.type,
output: '',
timed_out: false,
passed: false,
status: 'capability_failure',
capability_code: code,
reason,
};
}
function waitingResult(gateEntry: NormalizedGate, reason: string): GateResult {
return {
command: gateEntry.command,
exit_code: 0,
type: gateEntry.type,
output: '',
timed_out: false,
passed: false,
status: 'waiting',
capability_code: 'MACP_AUTHORITY_REQUIRED',
reason,
};
}
export function runGate(
gate: unknown,
cwd: string,
logPath: string,
timeoutSec: number,
options: RunGateOptions = {},
): GateResult {
const gateEntry = normalizeGate(gate);
const gateType = gateEntry.type;
const command = gateEntry.command;
// Explicit simulation only: never executes, typed simulated, never satisfying.
if (options.simulate) {
return simulatedResult(gateEntry);
}
// Fail closed: no CI provider implementation exists in @mosaicstack/macp,
// so a ci-pipeline gate is an absent capability — never a placeholder pass.
if (gateType === 'ci-pipeline') {
return capabilityFailureResult(
gateEntry,
'MACP_NO_CI_PIPELINE',
`ci-pipeline gate '${gateEntry.command || gateType}' has no CI provider implementation wired — refusing placeholder pass`,
);
}
if (!command) {
// A manual gate with no automation waits for human sign-off: not pass, not fail.
if (gateType === 'manual') {
return waitingResult(
gateEntry,
`manual gate has no automation — waiting for human sign-off (type: ${gateType})`,
);
}
// Any other commandless gate is an absent capability — never a vacuous pass.
return capabilityFailureResult(
gateEntry,
gateType === 'ai-review' ? 'MACP_NO_REVIEWER' : 'MACP_NO_COMMAND',
`gate of type '${gateType}' has no command to execute — refusing empty-command pass`,
);
}
const { exitCode, output, timedOut } = runShell(command, cwd, logPath, timeoutSec);
const result: GateResult = {
command,
exit_code: exitCode,
type: gateType,
output,
timed_out: timedOut,
passed: false,
status: 'failed',
};
if (gateType !== 'ai-review') {
result.passed = exitCode === 0;
result.status = result.passed ? 'passed' : 'failed';
return result;
}
const failOn = gateEntry.fail_on || 'blocker';
let parsedOutput: unknown = undefined;
let blockers = 0;
let findingsCount = 0;
let parseError: string | undefined;
try {
parsedOutput = output.trim() ? JSON.parse(output) : {};
const counts = countAIFindings(parsedOutput);
blockers = counts.blockers;
findingsCount = counts.total;
} catch (exc) {
parseError = String(exc instanceof Error ? exc.message : exc);
}
if (failOn === 'any') {
result.passed = exitCode === 0 && findingsCount === 0 && !timedOut && parseError === undefined;
} else {
result.passed = exitCode === 0 && blockers === 0 && !timedOut && parseError === undefined;
}
result.status = result.passed ? 'passed' : 'failed';
result.fail_on = failOn;
result.blockers = blockers;
result.findings = findingsCount;
if (parsedOutput !== undefined) {
result.parsed_output = parsedOutput;
}
if (parseError !== undefined) {
result.parse_error = parseError;
}
return result;
}
export function runGates(
gates: unknown[],
cwd: string,
logPath: string,
timeoutSec: number,
eventsPath: string,
taskId: string,
options: RunGateOptions = {},
): RunGatesResult {
const gateResults: GateResult[] = [];
let hasCapabilityFailure = false;
let hasSimulated = false;
let hasFailed = false;
let hasWaiting = false;
for (const gate of gates) {
const gateEntry = normalizeGate(gate);
const gateCmd = gateEntry.command;
const label = gateCmd || gateEntry.type;
// NOTE: no silent skip — every gate produces a typed result (RI-N2).
emitEvent(
eventsPath,
'rail.check.started',
taskId,
'gated',
'quality-gate',
`Running gate: ${label}`,
);
const result = runGate(gate, cwd, logPath, timeoutSec, options);
gateResults.push(result);
if (result.status === 'passed') {
emitEvent(
eventsPath,
'rail.check.passed',
taskId,
'gated',
'quality-gate',
`Gate passed: ${label}`,
);
continue;
}
if (result.status === 'waiting') {
hasWaiting = true;
emitEvent(
eventsPath,
'rail.check.waiting',
taskId,
'gated',
'quality-gate',
`Gate waiting: ${label}${result.reason ?? 'manual gate awaits sign-off'}`,
);
continue;
}
if (result.status === 'simulated') {
hasSimulated = true;
emitEvent(
eventsPath,
'rail.check.simulated',
taskId,
'gated',
'quality-gate',
`Gate simulated (non-satisfying): ${label}`,
);
continue;
}
if (result.status === 'capability_failure') {
hasCapabilityFailure = true;
emitEvent(
eventsPath,
'rail.check.failed',
taskId,
'gated',
'quality-gate',
`Gate capability failure (${result.capability_code ?? 'MACP_NO_PROVIDER'}): ${label}${result.reason ?? 'required capability is absent'}`,
);
continue;
}
hasFailed = true;
let message: string;
if (result.timed_out) {
message = `Gate timed out after ${timeoutSec}s: ${label}`;
} else if (result.type === 'ai-review' && result.parse_error) {
message = `AI review gate output was not valid JSON: ${label}`;
} else {
message = `Gate failed (${result.exit_code}): ${label}`;
}
emitEvent(eventsPath, 'rail.check.failed', taskId, 'gated', 'quality-gate', message);
}
const state: GateStatus = hasCapabilityFailure
? 'capability_failure'
: hasSimulated
? 'simulated'
: hasFailed
? 'failed'
: hasWaiting
? 'waiting'
: 'passed';
return { allPassed: state === 'passed', gateResults, state };
}
+65
View File
@@ -0,0 +1,65 @@
// Types
export type {
TaskStatus,
TaskType,
DispatchMode,
DependsOnPolicy,
GateType,
GateFailOn,
GateStatus,
GateEntry,
Task,
EventType,
MACPEvent,
GateResult,
RunGatesResult,
TaskResult,
ProviderMeta,
ProviderRegistry,
} from './types.js';
export { CredentialError } from './types.js';
// Typed fail-closed capability errors (RI-N2, SDLC-D-035)
export { MACP_ERROR_CODES, MACPCapabilityError } from './errors.js';
export type { MacpErrorCode } from './errors.js';
// Credential resolver
export {
DEFAULT_CREDENTIALS_DIR,
OC_CONFIG_PATH,
REDACTED_MARKER,
PROVIDER_REGISTRY,
extractProvider,
parseDotenv,
stripJSON5Extensions,
checkOCConfigPermissions,
isValidCredential,
resolveCredentials,
} from './credential-resolver.js';
export type { ResolveCredentialsOptions } from './credential-resolver.js';
// Gate runner
export {
normalizeGate,
runShell,
countAIFindings,
runGate,
runGates,
SIMULATED_GATE_REASON,
} from './gate-runner.js';
export type { NormalizedGate, RunGateOptions } from './gate-runner.js';
// Risk-floor (agent reflection loop — diff review classifier)
export { evaluateRiskFloor, DEFAULT_RISK_THRESHOLD } from './risk-floor.js';
export type { ReviewSurface, RiskFloorInput, RiskFloorVerdict } from './risk-floor.js';
// Event emitter
export { nowISO, appendEvent, emitEvent } from './event-emitter.js';
// CLI
export { registerMacpCommand } from './cli.js';
+87
View File
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { DEFAULT_RISK_THRESHOLD, evaluateRiskFloor, type ReviewSurface } from './risk-floor.js';
describe('evaluateRiskFloor', () => {
it('returns a no-review "none" verdict for an empty diff', () => {
const v = evaluateRiskFloor({ filesChanged: [] });
expect(v).toEqual({
needs_review: false,
score: 0,
surface: 'none',
reason: 'no files changed',
});
});
it('ignores empty/non-string entries', () => {
const v = evaluateRiskFloor({ filesChanged: ['', ' ' as unknown as string].filter(Boolean) });
// only the whitespace string survives the Boolean filter; it classifies to none
expect(v.surface).toBe('none');
expect(v.needs_review).toBe(false);
});
it.each<[string, string, ReviewSurface, boolean]>([
['auth', 'apps/api/src/auth/session.guard.ts', 'auth', true],
['data', 'packages/db/migrations/0007_add_users.sql', 'data', true],
['infra', '.woodpecker/deploy.yml', 'infra', true],
['build', 'packages/types/tsconfig.json', 'build', true],
['ui', 'apps/web/src/components/Button.tsx', 'ui', false],
['test', 'packages/macp/src/risk-floor.spec.ts', 'test', false],
['docs', 'docs/plans/agent-reflection-loop-PRD.md', 'docs', false],
['none', 'README', 'none', false],
])(
'classifies a single %s file → surface=%s needs_review=%s',
(_label, file, surface, needsReview) => {
const v = evaluateRiskFloor({ filesChanged: [file] });
expect(v.surface).toBe(surface);
expect(v.needs_review).toBe(needsReview);
expect(v.reason).toContain(
file === 'README' ? 'no sensitive surface' : surface === 'none' ? '' : surface,
);
},
);
it('lets the highest-risk surface dominate a mixed diff', () => {
const v = evaluateRiskFloor({
filesChanged: [
'docs/readme.md',
'apps/web/src/components/Nav.tsx',
'apps/api/src/auth/token.service.ts',
],
});
expect(v.surface).toBe('auth');
expect(v.score).toBe(1.0);
expect(v.needs_review).toBe(true);
expect(v.reason).toContain('token.service.ts');
expect(v.reason).not.toContain('readme.md');
});
it('names every file that ties at the dominant surface', () => {
const v = evaluateRiskFloor({
filesChanged: ['src/login.ts', 'src/permission-check.ts'],
});
expect(v.surface).toBe('auth');
expect(v.reason).toContain('src/login.ts');
expect(v.reason).toContain('src/permission-check.ts');
});
it('treats docs+test-only diffs as below the floor', () => {
const v = evaluateRiskFloor({
filesChanged: ['docs/guide.md', 'packages/x/src/x.test.ts'],
});
expect(v.needs_review).toBe(false);
expect(v.surface).toBe('test'); // higher weight than docs
});
it('honors a custom threshold', () => {
const docsOnly = { filesChanged: ['docs/guide.md'] };
expect(evaluateRiskFloor(docsOnly, 0.05).needs_review).toBe(true);
expect(evaluateRiskFloor(docsOnly, DEFAULT_RISK_THRESHOLD).needs_review).toBe(false);
});
it('is deterministic across call order', () => {
const a = evaluateRiskFloor({ filesChanged: ['a.md', 'auth/x.ts', 'b.tsx'] });
const b = evaluateRiskFloor({ filesChanged: ['b.tsx', 'a.md', 'auth/x.ts'] });
expect(a).toEqual(b);
});
});
+138
View File
@@ -0,0 +1,138 @@
/**
* Diff risk-floor — deterministic review-need classifier.
*
* Given the set of changed files in a diff, derive a *minimum* review
* requirement ("floor") from the change surface. This is the mechanical half
* of the agent reflection loop (design §6): risky surfaces (auth, data, infra)
* trip a review requirement regardless of what the agent self-reports.
*
* Precedence (authoritative ordering, see design §5):
* CI/tests > human merge > reviewer verdict > self-reflection
* This module sits at the *floor*. It NEVER overrides CI or a human; a
* `needs_review: false` verdict means "no surface tripped the floor", not
* "safe to merge". Consumers MUST keep CI/tests authoritative above it.
*
* Pure and deterministic: no IO, no clock, no randomness. Same input → same
* verdict. Safe to call from a Stop hook via `node -e` or to port inline.
*/
/** Review surfaces, ordered most- to least-sensitive. */
export type ReviewSurface = 'auth' | 'data' | 'infra' | 'build' | 'ui' | 'test' | 'docs' | 'none';
export interface RiskFloorInput {
/** Paths of changed files, repo-relative. Order-insensitive. */
filesChanged: string[];
/** Optional diff size signals; reserved for future weighting. */
insertions?: number;
deletions?: number;
}
export interface RiskFloorVerdict {
/** True when the change surface meets/exceeds the review threshold. */
needs_review: boolean;
/** Aggregate risk score in [0, 1] — the max surface weight across files. */
score: number;
/** The dominant (highest-weight) surface across all changed files. */
surface: ReviewSurface;
/** Human-readable explanation naming the surface and tripping files. */
reason: string;
}
/** Default review threshold; `score >= THRESHOLD` ⇒ `needs_review`. */
export const DEFAULT_RISK_THRESHOLD = 0.5;
interface SurfaceRule {
surface: ReviewSurface;
weight: number;
/** Case-insensitive regex matched against the file path. */
pattern: RegExp;
}
/**
* Surface classification rules, evaluated highest-weight first. The first
* rule whose pattern matches a path classifies that file; the file's surface
* is the highest-risk surface it matches (rules are pre-sorted by weight).
*/
const SURFACE_RULES: readonly SurfaceRule[] = [
{
surface: 'auth',
weight: 1.0,
pattern: /auth|login|session|token|permission|rbac|credential|secret/i,
},
{
surface: 'data',
weight: 0.9,
pattern: /migration|prisma|schema|\.sql|entity|repository|seed/i,
},
{
surface: 'infra',
weight: 0.85,
pattern: /docker|\.woodpecker|compose|traefik|deploy|helm|k8s|terraform/i,
},
{
surface: 'build',
weight: 0.6,
pattern: /package\.json|tsconfig|turbo\.json|pnpm-|\.config\.|eslint|vite/i,
},
{ surface: 'ui', weight: 0.4, pattern: /\.tsx|\.css|components\/|apps\/web\// },
{ surface: 'test', weight: 0.2, pattern: /\.spec\.|\.test\.|__tests__\// },
{ surface: 'docs', weight: 0.1, pattern: /\.md$|docs\// },
];
const NONE_WEIGHT = 0.0;
/** Classify a single path to its highest-risk surface and weight. */
function classify(path: string): { surface: ReviewSurface; weight: number } {
for (const rule of SURFACE_RULES) {
if (rule.pattern.test(path)) {
return { surface: rule.surface, weight: rule.weight };
}
}
return { surface: 'none', weight: NONE_WEIGHT };
}
/**
* Evaluate the review risk-floor for a diff.
*
* @param input changed files (+ optional size signals)
* @param threshold review cutoff; defaults to {@link DEFAULT_RISK_THRESHOLD}
*/
export function evaluateRiskFloor(
input: RiskFloorInput,
threshold: number = DEFAULT_RISK_THRESHOLD,
): RiskFloorVerdict {
const files = (input.filesChanged ?? []).filter((f) => typeof f === 'string' && f.length > 0);
if (files.length === 0) {
return {
needs_review: false,
score: 0,
surface: 'none',
reason: 'no files changed',
};
}
let topSurface: ReviewSurface = 'none';
let topWeight = NONE_WEIGHT;
const tripping: string[] = [];
for (const file of files) {
const { surface, weight } = classify(file);
if (weight > topWeight) {
topWeight = weight;
topSurface = surface;
tripping.length = 0;
tripping.push(file);
} else if (weight === topWeight && surface === topSurface && surface !== 'none') {
tripping.push(file);
}
}
const needs_review = topWeight >= threshold;
const reason =
topSurface === 'none'
? `no sensitive surface in ${files.length} changed file(s)`
: `${topSurface} surface (weight ${topWeight}) in: ${tripping.join(', ')}`;
return { needs_review, score: topWeight, surface: topSurface, reason };
}
@@ -0,0 +1,105 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mosaicstack.dev/schemas/reflection/reflection.v1.schema.json",
"title": "Agent Reflection (v1)",
"description": "End-of-run reflection sidecar. Mechanical fields are written by the Stop hook; self-reported fields are merged from an optional agent-supplied input and are null when absent (provenance.degraded=true).",
"type": "object",
"required": [
"schema",
"task_ref",
"agent",
"session_id",
"timestamp",
"repo",
"risk",
"files_changed",
"provenance"
],
"properties": {
"schema": {
"const": "reflection.v1"
},
"task_ref": {
"type": "string",
"description": "Canonical task ref; derived from REFLECTION_TASK_REF or repo+branch."
},
"agent": {
"type": "string",
"description": "Persona/runtime id (REFLECTION_AGENT or 'unknown')."
},
"session_id": {
"type": "string",
"description": "From the Stop payload session_id, else 'unknown'."
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO-8601 UTC capture time."
},
"repo": {
"type": "string",
"description": "Repo root basename."
},
"confidence": {
"type": ["number", "null"],
"minimum": 0,
"maximum": 1,
"description": "SELF-REPORTED. Agent's overall confidence; null when not supplied."
},
"most_likely_wrong": {
"type": ["object", "null"],
"description": "SELF-REPORTED. The single most-likely way the work is wrong.",
"required": ["surface", "description"],
"properties": {
"surface": { "$ref": "#/$defs/surface" },
"description": { "type": "string" }
},
"additionalProperties": false
},
"known_not_in_diff": {
"type": ["string", "null"],
"description": "SELF-REPORTED. What the agent knows that isn't visible in the diff."
},
"risk": {
"type": "object",
"description": "MECHANICAL. Output of the diff risk-floor.",
"required": ["needs_review", "score", "surface", "reason"],
"properties": {
"needs_review": { "type": "boolean" },
"score": { "type": "number", "minimum": 0, "maximum": 1 },
"surface": { "$ref": "#/$defs/surface" },
"reason": { "type": "string" }
},
"additionalProperties": false
},
"files_changed": {
"type": "array",
"items": { "type": "string" },
"description": "MECHANICAL. git diff name-only."
},
"provenance": {
"type": "object",
"required": ["source", "reflection_attempt", "degraded", "reflection_mode"],
"properties": {
"source": { "const": "stop-hook" },
"reflection_attempt": { "type": "integer", "minimum": 1 },
"degraded": {
"type": "boolean",
"description": "True when self-report inputs were missing/unreadable."
},
"reflection_mode": {
"type": "string",
"enum": ["off", "solo", "orchestrated"]
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"$defs": {
"surface": {
"type": "string",
"enum": ["auth", "data", "infra", "build", "ui", "test", "docs", "none"]
}
}
}
@@ -0,0 +1,123 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mosaicstack.dev/schemas/orchestrator/task.schema.json",
"title": "Mosaic Orchestrator Task",
"type": "object",
"required": ["id", "title", "status"],
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"status": {
"type": "string",
"enum": ["pending", "running", "gated", "completed", "failed", "escalated"]
},
"type": {
"type": "string",
"enum": ["coding", "deploy", "research", "review", "documentation", "infrastructure"],
"description": "Task type - determines dispatch strategy and gate requirements"
},
"dispatch": {
"type": "string",
"enum": ["yolo", "acp", "exec"],
"description": "Execution backend: yolo=mosaic yolo (full system), acp=OpenClaw sessions_spawn (sandboxed), exec=direct shell"
},
"runtime": {
"type": "string",
"description": "Preferred worker runtime, e.g. codex, claude, opencode"
},
"worktree": {
"type": "string",
"description": "Path to git worktree for this task, e.g. ~/src/repo-worktrees/task-042"
},
"branch": {
"type": "string",
"description": "Git branch name for this task"
},
"brief_path": {
"type": "string",
"description": "Path to markdown task brief relative to repo root"
},
"result_path": {
"type": "string",
"description": "Path to JSON result file relative to .mosaic/orchestrator/"
},
"issue": {
"type": "string",
"description": "Issue reference (e.g. #42)"
},
"pr": {
"type": ["string", "null"],
"description": "PR number/URL once opened"
},
"depends_on": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of task IDs this task depends on"
},
"depends_on_policy": {
"type": "string",
"enum": ["all", "any", "all_terminal"],
"default": "all",
"description": "How to evaluate dependency satisfaction"
},
"max_attempts": {
"type": "integer",
"minimum": 1,
"default": 1
},
"attempts": {
"type": "integer",
"minimum": 0,
"default": 0
},
"timeout_seconds": {
"type": "integer",
"description": "Override default timeout for this task"
},
"command": {
"type": "string",
"description": "Worker command to execute for this task"
},
"quality_gates": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"command": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["mechanical", "ai-review", "ci-pipeline"]
},
"fail_on": {
"type": "string",
"enum": ["blocker", "any"]
}
},
"required": ["command"],
"additionalProperties": true
}
]
}
},
"metadata": {
"type": "object"
}
},
"additionalProperties": true
}
+164
View File
@@ -0,0 +1,164 @@
import type { MacpErrorCode } from './errors.js';
/** Task status values. */
export type TaskStatus = 'pending' | 'running' | 'gated' | 'completed' | 'failed' | 'escalated';
/** Task type — determines dispatch strategy and gate requirements. */
export type TaskType =
| 'coding'
| 'deploy'
| 'research'
| 'review'
| 'documentation'
| 'infrastructure';
/** Execution backend. */
export type DispatchMode = 'yolo' | 'acp' | 'exec';
/** Dependency evaluation policy. */
export type DependsOnPolicy = 'all' | 'any' | 'all_terminal';
/** Quality gate type. */
export type GateType = 'mechanical' | 'ai-review' | 'ci-pipeline' | 'manual';
/**
* Typed execution state of a gate — closed set (RI-N2, SDLC-D-035).
*
* Only `passed` means "really executed and green". `simulated` is produced
* exclusively under an explicit simulate opt-in and never satisfies anything.
* `capability_failure` means a required executor/provider/command was absent.
* `waiting` means a manual gate awaits human sign-off (neither pass nor fail).
*/
export type GateStatus = 'passed' | 'failed' | 'simulated' | 'waiting' | 'capability_failure';
/** Gate fail_on mode. */
export type GateFailOn = 'blocker' | 'any';
/** Quality gate definition — either a bare command string or a structured object. */
export interface GateEntry {
command: string;
type?: GateType;
fail_on?: GateFailOn;
[key: string]: unknown;
}
/** MACP task. */
export interface Task {
id: string;
title: string;
status: TaskStatus;
description?: string;
type?: TaskType;
dispatch?: DispatchMode;
runtime?: string;
worktree?: string;
branch?: string;
brief_path?: string;
result_path?: string;
issue?: string;
pr?: string | null;
depends_on?: string[];
depends_on_policy?: DependsOnPolicy;
max_attempts?: number;
attempts?: number;
timeout_seconds?: number;
command?: string;
quality_gates?: (string | GateEntry)[];
metadata?: Record<string, unknown>;
[key: string]: unknown;
}
/** Event types emitted by the MACP protocol. */
export type EventType =
| 'task.assigned'
| 'task.started'
| 'task.completed'
| 'task.failed'
| 'task.escalated'
| 'task.gated'
| 'task.retry.scheduled'
| 'rail.check.started'
| 'rail.check.passed'
| 'rail.check.failed'
| 'rail.check.waiting'
| 'rail.check.simulated';
/** Structured event record. */
export interface MACPEvent {
event_id: string;
event_type: EventType | string;
task_id: string;
status: string;
timestamp: string;
source: string;
message: string;
metadata: Record<string, unknown>;
}
/** Result from running a single quality gate. */
export interface GateResult {
command: string;
exit_code: number;
type: string;
output: string;
timed_out: boolean;
/** Back-compat boolean view — true ONLY when `status === 'passed'`. */
passed: boolean;
/** Typed discriminator — the authoritative gate outcome (RI-N2). */
status: GateStatus;
/** Typed capability error code, set when `status === 'capability_failure'`. */
capability_code?: MacpErrorCode;
/** Why a non-executed state (simulated/waiting/capability_failure) was reached. */
reason?: string;
fail_on?: string;
blockers?: number;
findings?: number;
parsed_output?: unknown;
parse_error?: string;
}
/**
* Aggregate outcome of `runGates` (RI-N2).
*
* `state` is the typed aggregate: it is `passed` only when every gate really
* executed green. A `simulated` result makes the aggregate `simulated` (never
* `passed`); a `waiting` manual gate keeps the aggregate `waiting`; a missing
* capability makes it `capability_failure`. `allPassed` is exactly
* `state === 'passed'`, so a simulated or waiting result can never satisfy a
* dependency, acceptance criterion, gate, merge, or release check.
*/
export interface RunGatesResult {
allPassed: boolean;
gateResults: GateResult[];
state: GateStatus;
}
/** Result from a completed task. */
export interface TaskResult {
task_id: string;
status: TaskStatus;
completed_at: string;
exit_code: number;
gate_results: GateResult[];
files_changed?: string[];
[key: string]: unknown;
}
/** Provider registry entry. */
export interface ProviderMeta {
credential_file: string;
env_var: string;
oc_env_key: string;
oc_provider_path: string;
}
/** Provider registry mapping. */
export type ProviderRegistry = Record<string, ProviderMeta>;
/** Raised when required provider credentials cannot be resolved. */
export class CredentialError extends Error {
constructor(message: string) {
super(message);
this.name = 'CredentialError';
}
}