/*
Maintains packages/os/site-kernel-handoff/src/mission/snapshot-auto-regen.ts as an authored site-kernel-handoff authored module so agents can evolve it without rediscovering local boundaries.
- Do not add build pipeline orchestration logic — this helper only handles SNAP-01 detection, snapshot regeneration, git commit, and optional rebuild via dependency injection.
- ADR-0085: guard autoRegenerateSnapshotOnSnap01 against closed workpieces — the direct executeKernelCommand path bypasses the pipeline-level .closed skip, so regeneration would write a tracked file that RFC-0878 can never commit.
- ADR-0087: replace hand-rolled .closed sentinel check with isClosedWorkpiece from kernel/runtime/closed-workpiece.ts — the executor now enforces the same guard, this early-exit remains for a clear orchestrator result.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
- ADR-0096: bootstrapCommittedSnapshot — committed-presence pre-check + scoped bookkeeping commit before build.post to prevent SNAP-02 deadlock
- ADR-0096: dedupe CHANGE_SUMMARY item, use SNAPSHOT_FILENAME in CACHE_CLONE_ONLY_PATHS
RFC-0689, RFC-0697
*/
import path from "node:path";
import type { KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import { executeKernelCommand } from "@warpgogol/werkstatt-engine/kernel";
import { isClosedWorkpiece } from "../kernel/runtime/closed-workpiece.ts";
import { resolveMissionDir } from "./mission-io.ts";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
interface SnapshotDiagnostics {
diagnostics?: { ruleId: string }[];
}
export interface AutoRegenerateOptions {
workspaceRoot: string;
systemId: string;
missionId: string;
logger: { info: (msg: string) => void; warn?: (msg: string) => void };
context?: KernelRuntimeContext;
}
export const SNAPSHOT_FILENAME = "behavior.snapshot.generated.yaml";
export interface AutoRegenerateResult {
regenerated: boolean;
error?: string;
}
export function detectSnap01(data: unknown): boolean {
const diagnostics = (data as SnapshotDiagnostics | undefined)?.diagnostics;
if (!diagnostics || !Array.isArray(diagnostics)) return false;
return diagnostics.some((d) => d.ruleId === "SNAP-01");
}
export async function autoRegenerateSnapshotOnSnap01(
opts: AutoRegenerateOptions,
): Promise {
const { workspaceRoot, systemId, missionId, logger } = opts;
// ADR-0085/0087: a closed workpiece is immutable — regeneration would write
// a tracked file that RFC-0878 can never commit, producing pure churn.
// executeRegisteredCommand enforces the same guard (ADR-0087); this early
// exit stays so the orchestrator gets a clear `regenerated: false` result
// instead of a skipped-report chain through behavior.snapshot.generate.
const io = opts.context?.io ?? createDefaultIO().io;
const workpieceDir = path.join(
await resolveMissionDir(workspaceRoot, missionId, io),
"workpiece",
);
if (await isClosedWorkpiece(workpieceDir, io)) {
const error =
`snapshot auto-regeneration skipped: workpiece is closed (ADR-0085) — ` +
`mission '${missionId}' cannot accept commits`;
logger.info(` ${error}`);
return { regenerated: false, error };
}
logger.info(` SNAP-01 detected — auto-regenerating behavior snapshot…`);
try {
await executeKernelCommand({
workspaceRoot,
commandName: "behavior.snapshot.generate",
siteName: systemId,
});
await executeKernelCommand({
workspaceRoot,
commandName: "mission.git.commit",
argv: [`--mission=${missionId}`, "--message=chore: auto-regenerate behavior snapshot"],
});
logger.info(` Behavior snapshot regenerated and committed`);
return { regenerated: true };
} catch (regenErr) {
const error = `snapshot auto-regeneration failed: ${regenErr instanceof Error ? regenErr.message : String(regenErr)}`;
logger.info(` ${error}`);
return { regenerated: false, error };
}
}
export interface Snap01OrchestrationOptions {
workspaceRoot: string;
systemId: string;
missionId: string;
logger: { info: (msg: string) => void; warn?: (msg: string) => void };
validateFn: () => Promise;
rebuildFn?: () => Promise;
}
export interface Snap01OrchestrationResult {
regenerated: boolean;
rebuildSucceeded?: boolean;
error?: string;
}
export async function orchestrateSnap01Recovery(
opts: Snap01OrchestrationOptions,
): Promise {
const { workspaceRoot, systemId, missionId, logger, validateFn, rebuildFn } = opts;
try {
const validateData = await validateFn();
if (!detectSnap01(validateData)) {
return { regenerated: false };
}
const regenResult = await autoRegenerateSnapshotOnSnap01({
workspaceRoot,
systemId,
missionId,
logger,
});
if (!regenResult.regenerated) {
return { regenerated: false, error: regenResult.error };
}
if (rebuildFn) {
try {
await rebuildFn();
return { regenerated: true, rebuildSucceeded: true };
} catch (rebuildErr) {
const error = `rebuild failed after snapshot regeneration: ${rebuildErr instanceof Error ? rebuildErr.message : String(rebuildErr)}`;
logger.info(` ${error}`);
return { regenerated: true, rebuildSucceeded: false, error };
}
}
return { regenerated: true };
} catch (validateErr) {
const error = `SNAP-01 validation check failed: ${validateErr instanceof Error ? validateErr.message : String(validateErr)}`;
logger.info(` ${error}`);
return { regenerated: false, error };
}
}
export interface SnapshotBootstrapResult {
/** True when a bookkeeping commit created the committed snapshot baseline. */
committed: boolean;
/** True when the snapshot was already committed — no action taken. */
alreadyCommitted?: boolean;
error?: string;
}
/**
* ADR-0096: ensure a git-committed `behavior.snapshot.generated.yaml` exists in
* the workpiece before `build.post` runs — `behavior.snapshot.validate` diffs
* against the committed baseline and emits SNAP-02 when none exists, deadlocking
* first-time sites. The bookkeeping commit is scoped via `mission.git.commit
* --scope-files` so it stages only the snapshot file and cannot be blocked by
* unrelated dirty content.
*/
export async function bootstrapCommittedSnapshot(
opts: AutoRegenerateOptions,
): Promise {
const { workspaceRoot, systemId, missionId, logger } = opts;
const io = opts.context?.io ?? createDefaultIO().io;
const workpieceDir = path.join(
await resolveMissionDir(workspaceRoot, missionId, io),
"workpiece",
);
// ADR-0085/0087: closed workpieces are immutable — bootstrap is only
// meaningful while the mission is open and can accept commits.
if (await isClosedWorkpiece(workpieceDir, io)) {
return { committed: false };
}
const lsResult = await io.exec("git", ["ls-files", "--error-unmatch", SNAPSHOT_FILENAME], {
cwd: workpieceDir,
});
if (lsResult.exitCode === 0) {
return { committed: false, alreadyCommitted: true };
}
logger.info(` No committed ${SNAPSHOT_FILENAME} — bootstrapping committed baseline (ADR-0096)…`);
try {
await executeKernelCommand({
workspaceRoot,
commandName: "behavior.snapshot.generate",
siteName: systemId,
});
const commitResult = await executeKernelCommand({
workspaceRoot,
commandName: "mission.git.commit",
argv: [
`--mission=${missionId}`,
"--message=chore: bootstrap committed behavior snapshot (ADR-0096)",
`--scope-files=${SNAPSHOT_FILENAME}`,
],
});
const commitSingle = Array.isArray(commitResult) ? commitResult[0] : commitResult;
if (commitSingle && commitSingle.ok === false) {
const error = `snapshot bootstrap commit failed: ${commitSingle.summary ?? "unknown error"}`;
logger.info(` ${error}`);
return { committed: false, error };
}
logger.info(` Committed ${SNAPSHOT_FILENAME} baseline`);
return { committed: true };
} catch (err) {
const error = `snapshot bootstrap failed: ${err instanceof Error ? err.message : String(err)}`;
logger.info(` ${error}`);
return { committed: false, error };
}
}