Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { readFile, readdir } from 'node:fs/promises';
import { loadConstraints } from '../memory/constraints.js';
import { kicadCliVersion } from '../kicad/cli.js';
import { branchName, headCommit, uncommittedCount } from '../util/git.js';
import type { CopperheadConfig, ModelSource } from '../config.js';
/** Caller-supplied run identity: facts the loop cannot probe for itself. */
export interface RunMetaInput {
command?: 'do' | 'create' | 'sync' | 'repl';
modelSource?: ModelSource;
version?: string;
kicadCliVersion?: string;
stage?: { name: string; index: number; total: number };
brief?: { path: string; sha256: string };
}
/**
* Everything a run needs to be self-describing (AC-8.1). Collected once,
* rendered onto three surfaces: run-start event, summary.md ## Environment,
* and the live CLI header. Probe failures are nulls, never errors (AC-8.3).
*/
export interface RunMeta {
request: string;
model: string;
provider: string;
modelSource: ModelSource | null;
runId: string;
startedAt: string;
command: 'do' | 'create' | 'sync' | 'repl' | null;
interactive: boolean;
stage: { name: string; index: number; total: number } | null;
brief: { path: string; sha256: string } | null;
versions: {
copperhead: string | null;
installPath: string | null;
kicadCli: string | null;
node: string;
platform: string;
};
config: {
schematic: string | null;
board: string | null;
docs: string;
maxTurns: number;
maxRepairCycles: number;
budgets: Record<string, number>;
};
git: {
commit: string | null;
branch: string | null;
dirty: boolean | null;
uncommittedFiles: number | null;
preCommitHookInstalled: boolean | null;
};
openConstraints: number | null;
priorRuns: number | null;
}
/** A metadata probe must never fail the run it describes (design D4). */
async function probe<T>(fn: () => Promise<T> | T): Promise<T | null> {
try {
return await fn();
} catch {
return null;
}
}
function packageRoot(): string {
// src/agent/ and dist/agent/ both sit two levels below the package root.
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
}
function ownVersion(): string {
const { version } = createRequire(import.meta.url)('../../package.json') as { version: string };
return version;
}
export interface CollectRunMetaOptions {
repoRoot: string;
config: CopperheadConfig;
/** Effective turn budget for this run (flag override already applied). */
maxTurns: number;
runId: string;
request: string;
model: string;
provider: string;
interactive: boolean;
input?: RunMetaInput | undefined;
}
export async function collectRunMeta(opts: CollectRunMetaOptions): Promise<RunMeta> {
const { repoRoot, config, input } = opts;
const [copperhead, kicadCli, commit, branch, uncommitted, hook, openConstraints, priorRuns] = await Promise.all([
probe(() => input?.version ?? ownVersion()),
probe(() => input?.kicadCliVersion ?? kicadCliVersion()),
probe(() => headCommit(repoRoot)),
probe(() => branchName(repoRoot)),
probe(() => uncommittedCount(repoRoot)),
probe(async () => {
const hookText = await readFile(path.join(repoRoot, '.git', 'hooks', 'pre-commit'), 'utf8');
return hookText.includes('copperhead');
}).then((v) => v ?? false),
probe(async () => Object.keys(await loadConstraints(repoRoot)).length),
probe(async () => {
const entries = await readdir(path.join(repoRoot, '.copperhead', 'runs'));
return entries.filter((e) => e !== opts.runId).length;
}).then((v) => v ?? 0),
]);
return {
request: opts.request,
model: opts.model,
provider: opts.provider,
modelSource: input?.modelSource ?? null,
runId: opts.runId,
startedAt: new Date().toISOString(),
command: input?.command ?? null,
interactive: opts.interactive,
stage: input?.stage ?? null,
brief: input?.brief ?? null,
versions: {
copperhead,
installPath: await probe(packageRoot),
kicadCli,
node: process.version,
platform: `${process.platform}-${process.arch}`,
},
config: {
schematic: config.schematic,
board: config.board,
docs: config.docs,
maxTurns: opts.maxTurns,
maxRepairCycles: config.maxRepairCycles,
budgets: config.budgets,
},
git: {
commit,
branch,
dirty: uncommitted === null ? null : uncommitted > 0,
uncommittedFiles: uncommitted,
preCommitHookInstalled: hook,
},
openConstraints,
priorRuns,
};
}
const unk = (v: string | null | undefined): string => v ?? 'unknown';
/** ≤ 2 lines, printed before the first turn (AC-8.4).
* Live header stays compact: install path / platform live in summary.md only. */
export function renderCliHeader(meta: RunMeta): string[] {
const v = meta.versions;
const line1 = [
`copperhead v${unk(v.copperhead)}`,
`kicad-cli ${unk(v.kicadCli)}`,
`node ${v.node}`,
].join(' · ');
const repoState =
meta.git.dirty === null
? 'unknown'
: meta.git.dirty
? `dirty(${meta.git.uncommittedFiles})`
: 'clean';
const line2 = [
unk(meta.command),
...(meta.stage ? [`stage ${meta.stage.name} (${meta.stage.index}/${meta.stage.total})`] : []),
`model ${meta.model} (${meta.provider}, via ${unk(meta.modelSource)})`,
`turns ≤${meta.config.maxTurns}`,
`${unk(meta.git.branch)}@${meta.git.commit?.slice(0, 7) ?? 'unknown'} ${repoState}`,
].join(' · ');
return [line1, line2];
}
/** The `## Environment` section of summary.md; values mirror the run-start event (AC-8.4). */
export function renderEnvironmentSection(meta: RunMeta): string[] {
const v = meta.versions;
const c = meta.config;
const g = meta.git;
return [
`## Environment`,
``,
`- **Run:** ${meta.runId} · ${unk(meta.command)} · started ${meta.startedAt} · ${meta.interactive ? 'interactive' : 'autonomous'}`,
...(meta.stage ? [`- **Stage:** ${meta.stage.name} (${meta.stage.index}/${meta.stage.total})`] : []),
...(meta.brief ? [`- **Brief:** ${meta.brief.path} (sha256 ${meta.brief.sha256.slice(0, 12)}…)`] : []),
`- **Model:** ${meta.model} (${meta.provider}, via ${unk(meta.modelSource)})`,
`- **copperhead:** v${unk(v.copperhead)}${v.installPath ? ` at ${v.installPath}` : ''}`,
`- **Tooling:** kicad-cli ${unk(v.kicadCli)} · node ${v.node} · ${v.platform}`,
`- **Config:** schematic ${c.schematic ?? 'null'} · board ${c.board ?? 'null'} · docs ${c.docs} · maxTurns ${c.maxTurns} · maxRepairCycles ${c.maxRepairCycles} · budgets ${JSON.stringify(c.budgets)}`,
`- **Repo:** ${unk(g.branch)}@${g.commit ?? 'unknown'} · ${g.dirty === null ? 'unknown' : g.dirty ? `dirty (${g.uncommittedFiles} uncommitted)` : 'clean'} · pre-commit hook ${g.preCommitHookInstalled === null ? 'unknown' : g.preCommitHookInstalled ? 'installed' : 'absent'}`,
`- **Memory:** ${meta.openConstraints ?? 'unknown'} open constraint(s) · ${meta.priorRuns ?? 'unknown'} prior run(s)`,
];
}
ff4fe56b2This file is pinned to the selected release. Download the original to open it in your design tool.