aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
191 lines (174 loc) • 6.62 kB
JavaScript
/**
* Verify Hermes-source citations in AIWG documentation.
*
* Walks docs/integrations/hermes-*.md, docs/providers/hermes-*.md, and
* .aiwg/research/parity/hermes/*.md for file:line citations to Hermes
* source files. For each citation, fetches the cited line range from
* the pinned Hermes version and checks that key symbols/text still
* appear nearby.
*
* Pins to HERMES_VERIFIED_VERSION below. Update the pin (and re-run this
* script) whenever AIWG is verified against a new Hermes minor release.
*
* Usage:
* node tools/verify-hermes-citations.mjs # walk + report
* node tools/verify-hermes-citations.mjs --strict # exit non-zero on any drift
*
* Optionally set HERMES_REPO=/path/to/hermes-clone to use a local
* checkout instead of cloning fresh.
*
* @issues #1330 (S23)
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const HERMES_VERIFIED_VERSION = '0.13.0';
const HERMES_REPO_URL = 'https://github.com/NousResearch/hermes-agent';
const SCAN_DOCS_GLOB = [
'docs/integrations/hermes-*.md',
'docs/providers/hermes-*.md',
'.aiwg/research/parity/hermes/*.md',
];
// Citation pattern: matches `<path>:<line>` or `<path>:<line>-<line>`
const CITATION_RE = /(hermes_cli|agent|tools)\/[a-zA-Z0-9_.\/-]+\.py:(\d+)(?:-(\d+))?/g;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
function spawnP(cmd, args, { cwd } = {}) {
return new Promise((resolve, reject) => {
const p = spawn(cmd, args, { cwd, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
let out = '', err = '';
p.stdout.on('data', (d) => { out += d; });
p.stderr.on('data', (d) => { err += d; });
p.on('close', (c) => resolve({ stdout: out, stderr: err, code: c ?? -1 }));
p.on('error', reject);
});
}
async function pathExists(p) {
try { await fs.stat(p); return true; } catch { return false; }
}
async function ensureHermesClone() {
if (process.env.HERMES_REPO) {
if (await pathExists(process.env.HERMES_REPO)) {
return process.env.HERMES_REPO;
}
console.error(`HERMES_REPO points to ${process.env.HERMES_REPO} which does not exist`);
process.exit(2);
}
// Cache under /tmp keyed by version
const cacheDir = `/tmp/hermes-verify-${HERMES_VERIFIED_VERSION}`;
if (await pathExists(path.join(cacheDir, '.git'))) {
return cacheDir;
}
// Try existing /tmp/hermes-agent first (from earlier vendor research)
if (await pathExists('/tmp/hermes-agent/.git')) {
return '/tmp/hermes-agent';
}
console.error(`Cloning Hermes v${HERMES_VERIFIED_VERSION} to ${cacheDir} ...`);
const { code, stderr } = await spawnP('git', [
'clone', '--depth', '1',
'--branch', `v${HERMES_VERIFIED_VERSION}`,
HERMES_REPO_URL, cacheDir,
]);
if (code !== 0) {
// Fall back to default branch shallow clone
console.error(`Tag v${HERMES_VERIFIED_VERSION} not found; falling back to default branch`);
const { code: c2 } = await spawnP('git', ['clone', '--depth', '1', HERMES_REPO_URL, cacheDir]);
if (c2 !== 0) {
console.error(`Failed to clone Hermes: ${stderr}`);
process.exit(2);
}
}
return cacheDir;
}
async function globFiles(pattern) {
// Simple glob using fs — we only need flat directory matches
const [dir, file] = (() => {
const idx = pattern.lastIndexOf('/');
return [pattern.slice(0, idx), pattern.slice(idx + 1)];
})();
const re = new RegExp('^' + file.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
try {
const entries = await fs.readdir(path.join(REPO_ROOT, dir), { withFileTypes: true });
return entries
.filter((e) => e.isFile() && re.test(e.name))
.map((e) => path.join(REPO_ROOT, dir, e.name));
} catch {
return [];
}
}
async function collectCitations() {
const results = [];
for (const pattern of SCAN_DOCS_GLOB) {
const files = await globFiles(pattern);
for (const docPath of files) {
const content = await fs.readFile(docPath, 'utf-8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const match of line.matchAll(CITATION_RE)) {
results.push({
doc: path.relative(REPO_ROOT, docPath),
docLine: i + 1,
citation: match[0],
srcPath: match[0].split(':')[0],
startLine: parseInt(match[2], 10),
endLine: match[3] ? parseInt(match[3], 10) : parseInt(match[2], 10),
});
}
}
}
}
return results;
}
async function verifyCitation(hermesRoot, c) {
const full = path.join(hermesRoot, c.srcPath);
try {
const content = await fs.readFile(full, 'utf-8');
const lines = content.split('\n');
if (c.endLine > lines.length) {
return { ok: false, reason: `cited end-line ${c.endLine} exceeds file length ${lines.length}` };
}
// We don't try to verify semantic match — just that the range exists
// and looks structurally reasonable (non-empty range). A future
// enhancement could check that the citing doc paragraph still
// references the same symbol present in the range.
const snippet = lines.slice(c.startLine - 1, c.endLine).join('\n');
if (snippet.trim() === '') {
return { ok: false, reason: 'cited range is empty whitespace' };
}
return { ok: true, snippet };
} catch (err) {
return { ok: false, reason: `cannot read ${c.srcPath}: ${err.code || err.message}` };
}
}
async function main() {
const strict = process.argv.includes('--strict');
const hermesRoot = await ensureHermesClone();
console.log(`Hermes pin: v${HERMES_VERIFIED_VERSION}`);
console.log(`Verifying against: ${hermesRoot}`);
const citations = await collectCitations();
console.log(`Found ${citations.length} citations across ${SCAN_DOCS_GLOB.length} doc patterns\n`);
const issues = [];
for (const c of citations) {
const r = await verifyCitation(hermesRoot, c);
if (!r.ok) {
issues.push({ ...c, reason: r.reason });
}
}
if (issues.length === 0) {
console.log('✓ All citations verified');
process.exit(0);
}
console.log(`✗ ${issues.length} drifted citation(s):\n`);
for (const i of issues) {
console.log(` ${i.doc}:${i.docLine} cite=${i.citation}`);
console.log(` reason: ${i.reason}`);
}
process.exit(strict ? 1 : 0);
}
main().catch((err) => {
console.error(`verify-hermes-citations: ${err.message}`);
process.exit(2);
});