481 lines
13 KiB
JavaScript
481 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
// @ts-nocheck
|
|
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const APPROVED_DECISIONS = new Set([
|
|
'promote-skill',
|
|
'promote-instruction',
|
|
'promote-agent',
|
|
'promote-hook',
|
|
'promote-script',
|
|
'promote-prompt',
|
|
'template-only',
|
|
'docs-only',
|
|
]);
|
|
|
|
const DRAFTABLE_DECISIONS = new Set([
|
|
'promote-skill',
|
|
'promote-instruction',
|
|
'promote-agent',
|
|
'promote-prompt',
|
|
]);
|
|
|
|
function usage() {
|
|
console.error(
|
|
[
|
|
'Usage: prepare-audit-promotions.mjs [options]',
|
|
'',
|
|
'Options:',
|
|
' --audit-dir <path> Use a specific audit directory.',
|
|
' --machine-id <id> Limit latest-run discovery to one machine id.',
|
|
' --repo-root <path> Override the repository root.',
|
|
' --help Show this help text.',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
auditDir: '',
|
|
machineId: '',
|
|
repoRoot: path.resolve(__dirname, '../..'),
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === '--help' || arg === '-h') {
|
|
usage();
|
|
process.exit(0);
|
|
}
|
|
if (arg === '--audit-dir') {
|
|
options.auditDir = argv[index + 1] ?? '';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === '--machine-id') {
|
|
options.machineId = argv[index + 1] ?? '';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === '--repo-root') {
|
|
options.repoRoot = path.resolve(argv[index + 1] ?? options.repoRoot);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function ensureDir(dirPath) {
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
|
|
function sanitizeMachineId(input) {
|
|
return String(input || os.hostname() || 'unknown-machine')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '') || 'unknown-machine';
|
|
}
|
|
|
|
function safeStat(filePath) {
|
|
try {
|
|
return fs.statSync(filePath);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function listDirectories(rootPath) {
|
|
if (!fs.existsSync(rootPath)) {
|
|
return [];
|
|
}
|
|
|
|
return fs.readdirSync(rootPath, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => path.join(rootPath, entry.name));
|
|
}
|
|
|
|
function findLatestAuditDir(repoRoot, machineId) {
|
|
const auditRoot = path.join(repoRoot, '.local', 'audits');
|
|
const preferredMachineId = sanitizeMachineId(machineId);
|
|
const candidateMachineDirs = [];
|
|
|
|
if (machineId) {
|
|
const machineDir = path.join(auditRoot, preferredMachineId);
|
|
if (fs.existsSync(machineDir)) {
|
|
candidateMachineDirs.push(machineDir);
|
|
}
|
|
} else {
|
|
const localMachineDir = path.join(auditRoot, sanitizeMachineId(os.hostname()));
|
|
if (fs.existsSync(localMachineDir)) {
|
|
candidateMachineDirs.push(localMachineDir);
|
|
}
|
|
for (const machineDir of listDirectories(auditRoot)) {
|
|
if (!candidateMachineDirs.includes(machineDir)) {
|
|
candidateMachineDirs.push(machineDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
let latestRun = null;
|
|
|
|
for (const machineDir of candidateMachineDirs) {
|
|
for (const runDir of listDirectories(machineDir)) {
|
|
const stat = safeStat(runDir);
|
|
if (!stat) {
|
|
continue;
|
|
}
|
|
|
|
if (!latestRun || stat.mtimeMs > latestRun.mtimeMs) {
|
|
latestRun = { path: runDir, mtimeMs: stat.mtimeMs };
|
|
}
|
|
}
|
|
}
|
|
|
|
return latestRun?.path ?? null;
|
|
}
|
|
|
|
function parseTsv(filePath) {
|
|
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
if (lines.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const header = lines[0].split('\t');
|
|
return lines.slice(1).map((line) => {
|
|
const values = line.split('\t');
|
|
const row = {};
|
|
for (let index = 0; index < header.length; index += 1) {
|
|
row[header[index]] = values[index] ?? '';
|
|
}
|
|
return row;
|
|
});
|
|
}
|
|
|
|
function slugify(value) {
|
|
return String(value || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.replace(/-{2,}/g, '-');
|
|
}
|
|
|
|
function yamlString(value) {
|
|
return JSON.stringify(String(value ?? ''));
|
|
}
|
|
|
|
function preferredStem(row) {
|
|
return slugify(row.title || row.id || 'draft-resource') || 'draft-resource';
|
|
}
|
|
|
|
function relativeAuditPath(auditDir, filePath) {
|
|
return path.relative(auditDir, filePath) || '.';
|
|
}
|
|
|
|
function buildSourceBlock(row, detailRelativePath) {
|
|
const lines = [
|
|
`- Candidate ID: ${row.id}`,
|
|
`- Decision: ${row.decision}`,
|
|
`- Suggested action: ${row.suggested_action}`,
|
|
`- Category: ${row.category}`,
|
|
`- Value rank: ${row.value_rank}`,
|
|
];
|
|
|
|
if (detailRelativePath) {
|
|
lines.push(`- Detail file: ${detailRelativePath}`);
|
|
}
|
|
|
|
if (row.review_note) {
|
|
lines.push(`- Review note: ${row.review_note}`);
|
|
}
|
|
|
|
if (row.notes) {
|
|
lines.push(`- Audit notes: ${row.notes}`);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function renderSkillDraft(row, detailRelativePath) {
|
|
const name = preferredStem(row);
|
|
return [
|
|
'---',
|
|
`name: ${yamlString(name)}`,
|
|
`description: ${yamlString(`Draft generated from audit candidate ${row.id}.`)}`,
|
|
'argument-hint: ""',
|
|
'---',
|
|
'',
|
|
`# ${row.title}`,
|
|
'',
|
|
'Use this draft to turn the audited pattern into a portable shared skill.',
|
|
'',
|
|
'## Source',
|
|
'',
|
|
buildSourceBlock(row, detailRelativePath),
|
|
'',
|
|
'## Draft Workflow',
|
|
'',
|
|
'Replace this placeholder with the reusable workflow distilled from the audit evidence bundle.',
|
|
'',
|
|
'## Validation',
|
|
'',
|
|
'- Confirm the pattern is portable across repositories.',
|
|
'- Add any required docs, prompts, or scripts before publishing.',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function renderInstructionDraft(row, detailRelativePath) {
|
|
return [
|
|
'---',
|
|
`description: ${yamlString(`Draft generated from audit candidate ${row.id}.`)}`,
|
|
'applyTo: ""',
|
|
'---',
|
|
'',
|
|
'# Draft Instruction',
|
|
'',
|
|
'- Replace this placeholder with portable instruction text distilled from the audit evidence.',
|
|
'',
|
|
'## Source',
|
|
'',
|
|
buildSourceBlock(row, detailRelativePath),
|
|
'',
|
|
'## Guardrails',
|
|
'',
|
|
'- Remove repo-specific assumptions before publishing.',
|
|
'- Keep the instruction concise and reusable.',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function renderAgentDraft(row, detailRelativePath) {
|
|
return [
|
|
'---',
|
|
`name: ${yamlString(row.title)}`,
|
|
`description: ${yamlString(`Draft generated from audit candidate ${row.id}.`)}`,
|
|
'tools: [read, search, edit, execute]',
|
|
'---',
|
|
'',
|
|
`# ${row.title}`,
|
|
'',
|
|
'Use this draft agent to package the audited pattern into a reusable interaction mode.',
|
|
'',
|
|
'## Source',
|
|
'',
|
|
buildSourceBlock(row, detailRelativePath),
|
|
'',
|
|
'## Behavior',
|
|
'',
|
|
'- Replace this placeholder with the agent workflow distilled from the audit evidence.',
|
|
'- Specify when to use the agent and which tradeoffs it should enforce.',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function renderPromptDraft(row, detailRelativePath) {
|
|
const name = preferredStem(row);
|
|
return [
|
|
'---',
|
|
`name: ${yamlString(name)}`,
|
|
`description: ${yamlString(`Draft generated from audit candidate ${row.id}.`)}`,
|
|
'agent: "agent"',
|
|
'tools: [read, search, edit, execute]',
|
|
'argument-hint: ""',
|
|
'---',
|
|
'',
|
|
'Translate the audited pattern into a reusable prompt adapter.',
|
|
'',
|
|
'## Source',
|
|
'',
|
|
buildSourceBlock(row, detailRelativePath),
|
|
'',
|
|
'Requirements:',
|
|
'',
|
|
'- Replace this placeholder guidance with the portable workflow.',
|
|
'- Recheck the evidence bundle before publishing.',
|
|
'- Remove any repo-specific assumptions.',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function suggestedTarget(decision, stem) {
|
|
if (decision === 'promote-script') {
|
|
return `resources/scripts/${stem}.sh and resources/scripts/${stem}.ps1`;
|
|
}
|
|
if (decision === 'promote-hook') {
|
|
return `resources/hooks/${stem}.json`;
|
|
}
|
|
if (decision === 'template-only') {
|
|
return `templates/<target>/${stem}`;
|
|
}
|
|
if (decision === 'docs-only') {
|
|
return `docs/${stem}.md`;
|
|
}
|
|
|
|
return 'manual follow-up';
|
|
}
|
|
|
|
function renderStagingNote(row, detailRelativePath) {
|
|
const stem = preferredStem(row);
|
|
return [
|
|
`# ${row.title}`,
|
|
'',
|
|
'This approved audit row still needs manual design work before it should be published into the shared repository.',
|
|
'',
|
|
'## Source',
|
|
'',
|
|
buildSourceBlock(row, detailRelativePath),
|
|
'',
|
|
'## Suggested Target',
|
|
'',
|
|
`- ${suggestedTarget(row.decision, stem)}`,
|
|
'',
|
|
'## Next Steps',
|
|
'',
|
|
'- Distill the evidence bundle into a portable implementation plan.',
|
|
'- Decide whether this should stay a note, become docs, or become a concrete shared artifact.',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function buildDraftSpec(auditDir, row) {
|
|
const stem = preferredStem(row);
|
|
const detailRelativePath = row.detail_file || '';
|
|
|
|
if (row.decision === 'promote-skill') {
|
|
return {
|
|
kind: 'draft',
|
|
outputPath: path.join(auditDir, 'draft-resources', 'resources', 'skills', stem, 'SKILL.md'),
|
|
content: renderSkillDraft(row, detailRelativePath),
|
|
};
|
|
}
|
|
|
|
if (row.decision === 'promote-instruction') {
|
|
return {
|
|
kind: 'draft',
|
|
outputPath: path.join(auditDir, 'draft-resources', 'resources', 'instructions', `${stem}.instructions.md`),
|
|
content: renderInstructionDraft(row, detailRelativePath),
|
|
};
|
|
}
|
|
|
|
if (row.decision === 'promote-agent') {
|
|
return {
|
|
kind: 'draft',
|
|
outputPath: path.join(auditDir, 'draft-resources', 'resources', 'agents', `${stem}.agent.md`),
|
|
content: renderAgentDraft(row, detailRelativePath),
|
|
};
|
|
}
|
|
|
|
if (row.decision === 'promote-prompt') {
|
|
return {
|
|
kind: 'draft',
|
|
outputPath: path.join(auditDir, 'draft-resources', 'resources', 'prompts', `${stem}.prompt.md`),
|
|
content: renderPromptDraft(row, detailRelativePath),
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: 'note',
|
|
outputPath: path.join(auditDir, 'staging-notes', `${stem}.md`),
|
|
content: renderStagingNote(row, detailRelativePath),
|
|
};
|
|
}
|
|
|
|
function writeArtifacts(auditDir, approvedRows) {
|
|
const generated = [];
|
|
|
|
for (const row of approvedRows) {
|
|
const artifact = buildDraftSpec(auditDir, row);
|
|
ensureDir(path.dirname(artifact.outputPath));
|
|
fs.writeFileSync(artifact.outputPath, artifact.content, 'utf8');
|
|
generated.push({
|
|
decision: row.decision,
|
|
id: row.id,
|
|
kind: artifact.kind,
|
|
outputPath: artifact.outputPath,
|
|
});
|
|
}
|
|
|
|
return generated;
|
|
}
|
|
|
|
function writeSummary(auditDir, manifestPath, approvedRows, generatedArtifacts) {
|
|
const summaryPath = path.join(auditDir, 'promotion-summary.md');
|
|
const draftArtifacts = generatedArtifacts.filter((artifact) => artifact.kind === 'draft');
|
|
const noteArtifacts = generatedArtifacts.filter((artifact) => artifact.kind === 'note');
|
|
const lines = [
|
|
'# Audit Promotion Summary',
|
|
'',
|
|
`- Generated: ${new Date().toISOString()}`,
|
|
`- Audit directory: ${auditDir}`,
|
|
`- Selection manifest: ${manifestPath}`,
|
|
`- Approved rows: ${approvedRows.length}`,
|
|
`- Draft resources: ${draftArtifacts.length}`,
|
|
`- Staging notes: ${noteArtifacts.length}`,
|
|
'',
|
|
'## Draft Resources',
|
|
'',
|
|
];
|
|
|
|
if (draftArtifacts.length === 0) {
|
|
lines.push('- None');
|
|
} else {
|
|
for (const artifact of draftArtifacts) {
|
|
lines.push(`- ${artifact.decision} :: ${artifact.id} :: ${relativeAuditPath(auditDir, artifact.outputPath)}`);
|
|
}
|
|
}
|
|
|
|
lines.push('', '## Staging Notes', '');
|
|
|
|
if (noteArtifacts.length === 0) {
|
|
lines.push('- None');
|
|
} else {
|
|
for (const artifact of noteArtifacts) {
|
|
lines.push(`- ${artifact.decision} :: ${artifact.id} :: ${relativeAuditPath(auditDir, artifact.outputPath)}`);
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(summaryPath, `${lines.join('\n')}\n`, 'utf8');
|
|
return summaryPath;
|
|
}
|
|
|
|
function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const auditDir = options.auditDir
|
|
? path.resolve(options.auditDir)
|
|
: findLatestAuditDir(options.repoRoot, options.machineId);
|
|
|
|
if (!auditDir) {
|
|
throw new Error('No audit directory was found. Run the audit first or pass --audit-dir.');
|
|
}
|
|
|
|
const manifestPath = path.join(auditDir, 'selection-manifest.tsv');
|
|
if (!fs.existsSync(manifestPath)) {
|
|
throw new Error(`Selection manifest not found: ${manifestPath}`);
|
|
}
|
|
|
|
const rows = parseTsv(manifestPath);
|
|
const approvedRows = rows.filter((row) => APPROVED_DECISIONS.has(row.decision));
|
|
const generatedArtifacts = writeArtifacts(auditDir, approvedRows);
|
|
const summaryPath = writeSummary(auditDir, manifestPath, approvedRows, generatedArtifacts);
|
|
|
|
console.log('Audit promotion preparation complete.');
|
|
console.log(`Audit directory: ${auditDir}`);
|
|
console.log(`Approved rows: ${approvedRows.length}`);
|
|
console.log(`Draft resources: ${generatedArtifacts.filter((artifact) => artifact.kind === 'draft').length}`);
|
|
console.log(`Staging notes: ${generatedArtifacts.filter((artifact) => artifact.kind === 'note').length}`);
|
|
console.log(`Summary: ${summaryPath}`);
|
|
}
|
|
|
|
main(); |