clawdie-ai/scripts/harness-check.ts
Clawdie AI af659e7c56 feat(phase4): just front door — 55 recipes + 10 CLI helper scripts
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)
2026-04-13 23:26:22 +00:00

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);
}