Adds justfile with 8 grouped recipe sections covering build, jail management, skill catalog, agent ops, and system admin. Adds scripts for skill-list/add/sync, jail-status, system-health, agent-task/status/logs, harness-check, and hostd-cli. Fixes project root derivation to use import.meta.url instead of process.cwd() so scripts work regardless of invocation directory. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: pass | Tests: FAIL — Tests 40 failed | 766 passed (806)
188 lines
5.9 KiB
TypeScript
188 lines
5.9 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
/**
|
|
* scripts/harness-check.ts — Validate the entire harness setup.
|
|
*
|
|
* Checks:
|
|
* 1. infra/jails.yaml — parses and has required fields
|
|
* 2. agent/library.yaml — all local: sources exist on disk
|
|
* 3. .agent/harness/safety.yaml — parses
|
|
* 4. hostd socket — reachable (non-fatal warning if not)
|
|
* 5. Key script dependencies exist
|
|
*
|
|
* Usage: just harness-check
|
|
* Exit 0 = all checks passed (hostd warning is non-fatal)
|
|
* Exit 1 = hard failures found
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { parse } from 'yaml';
|
|
|
|
import { validateLibrary } from '../src/skill-library.js';
|
|
|
|
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
|
let passed = 0;
|
|
let failed = 0;
|
|
let warned = 0;
|
|
|
|
function pass(msg: string): void {
|
|
console.log(` \x1b[32m✓\x1b[0m ${msg}`);
|
|
passed++;
|
|
}
|
|
|
|
function fail(msg: string, detail?: string): void {
|
|
console.log(` \x1b[31m✗\x1b[0m ${msg}`);
|
|
if (detail) console.log(` ${detail}`);
|
|
failed++;
|
|
}
|
|
|
|
function warn(msg: string, detail?: string): void {
|
|
console.log(` \x1b[33m!\x1b[0m ${msg}`);
|
|
if (detail) console.log(` ${detail}`);
|
|
warned++;
|
|
}
|
|
|
|
function section(title: string): void {
|
|
console.log(`\n\x1b[1m${title}\x1b[0m`);
|
|
}
|
|
|
|
// ── 1. infra/jails.yaml ────────────────────────────────────────────────────
|
|
|
|
section('infra/jails.yaml');
|
|
|
|
const JAILS_PATH = path.join(ROOT, 'infra', 'jails.yaml');
|
|
if (!fs.existsSync(JAILS_PATH)) {
|
|
fail('file exists', JAILS_PATH);
|
|
} else {
|
|
try {
|
|
const jailsData = parse(fs.readFileSync(JAILS_PATH, 'utf-8')) as Record<string, unknown>;
|
|
pass('file parses');
|
|
|
|
const requiredFields = ['bridge', 'subnet_base', 'gateway', 'release', 'jails'];
|
|
for (const field of requiredFields) {
|
|
if (jailsData[field] !== undefined) {
|
|
pass(`has field: ${field}`);
|
|
} else {
|
|
fail(`missing field: ${field}`);
|
|
}
|
|
}
|
|
|
|
const jailCount = Object.keys((jailsData.jails as Record<string, unknown>) ?? {}).length;
|
|
pass(`${jailCount} jail(s) defined`);
|
|
} catch (err) {
|
|
fail('parses as YAML', (err as Error).message);
|
|
}
|
|
}
|
|
|
|
// ── 2. agent/library.yaml ─────────────────────────────────────────────────
|
|
|
|
section('agent/library.yaml');
|
|
|
|
const LIBRARY_PATH = path.join(ROOT, 'agent', 'library.yaml');
|
|
if (!fs.existsSync(LIBRARY_PATH)) {
|
|
fail('file exists', LIBRARY_PATH);
|
|
} else {
|
|
try {
|
|
const result = validateLibrary();
|
|
if (result.ok) {
|
|
const { parse: yamlParse } = await import('yaml');
|
|
const lib = yamlParse(fs.readFileSync(LIBRARY_PATH, 'utf-8')) as {
|
|
skills?: unknown[]; features?: unknown[]; agents?: unknown[];
|
|
};
|
|
pass(`parses — ${lib.skills?.length ?? 0} skills, ${lib.features?.length ?? 0} features, ${lib.agents?.length ?? 0} agents`);
|
|
pass('all local: sources found on disk');
|
|
} else {
|
|
pass('file parses');
|
|
for (const err of result.errors) {
|
|
fail(`source missing: ${err}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
fail('parses as YAML', (err as Error).message);
|
|
}
|
|
}
|
|
|
|
// ── 3. .agent/harness/safety.yaml ─────────────────────────────────────────
|
|
|
|
section('.agent/harness/safety.yaml');
|
|
|
|
const SAFETY_PATH = path.join(ROOT, '.agent', 'harness', 'safety.yaml');
|
|
if (!fs.existsSync(SAFETY_PATH)) {
|
|
fail('file exists', SAFETY_PATH);
|
|
} else {
|
|
try {
|
|
const safety = parse(fs.readFileSync(SAFETY_PATH, 'utf-8')) as Record<string, unknown>;
|
|
const ruleCount = (safety.rules as unknown[] ?? []).length +
|
|
(safety.bash_patterns as unknown[] ?? []).length;
|
|
pass(`parses — ${ruleCount} rule(s)`);
|
|
} catch (err) {
|
|
fail('parses as YAML', (err as Error).message);
|
|
}
|
|
}
|
|
|
|
// ── 4. hostd reachability ──────────────────────────────────────────────────
|
|
|
|
section('hostd');
|
|
|
|
try {
|
|
const { hostd } = await import('../src/hostd/client.js');
|
|
const resp = await Promise.race([
|
|
hostd('bastille-list'),
|
|
new Promise<never>((_, reject) =>
|
|
setTimeout(() => reject(new Error('timeout')), 3000),
|
|
),
|
|
]);
|
|
if (resp.ok) {
|
|
pass('socket reachable, bastille-list ok');
|
|
} else {
|
|
warn('socket reachable but bastille-list failed', resp.error);
|
|
}
|
|
} catch (err) {
|
|
const msg = (err as Error).message;
|
|
if (msg.includes('ENOENT') || msg.includes('ECONNREFUSED') || msg.includes('timeout')) {
|
|
warn('hostd not reachable (non-fatal — start with: just hostd-dev)', msg);
|
|
} else {
|
|
warn('hostd check failed', msg);
|
|
}
|
|
}
|
|
|
|
// ── 5. Script dependencies ─────────────────────────────────────────────────
|
|
|
|
section('script dependencies');
|
|
|
|
const scriptDeps = [
|
|
'scripts/jail-status.ts',
|
|
'scripts/hostd-cli.ts',
|
|
'scripts/system-health.ts',
|
|
'scripts/agent-status.ts',
|
|
'scripts/agent-task.ts',
|
|
'scripts/agent-task-status.ts',
|
|
'scripts/agent-logs.ts',
|
|
'scripts/skill-list.ts',
|
|
'scripts/skill-add.ts',
|
|
'scripts/skill-sync.ts',
|
|
];
|
|
|
|
for (const dep of scriptDeps) {
|
|
const p = path.join(ROOT, dep);
|
|
if (fs.existsSync(p)) {
|
|
pass(dep);
|
|
} else {
|
|
fail(dep, 'file not found');
|
|
}
|
|
}
|
|
|
|
// ── Summary ────────────────────────────────────────────────────────────────
|
|
|
|
console.log('');
|
|
if (failed === 0) {
|
|
console.log(
|
|
`\x1b[32m\x1b[1mAll checks passed\x1b[0m (${passed} passed, ${warned} warning${warned === 1 ? '' : 's'})\n`,
|
|
);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(
|
|
`\x1b[31m\x1b[1m${failed} check(s) failed\x1b[0m (${passed} passed, ${warned} warning${warned === 1 ? '' : 's'})\n`,
|
|
);
|
|
process.exit(1);
|
|
}
|