UNPKG

@sellable/install

Version:

One-command installer for Sellable MCP in Claude Code, Codex, and Hermes

2,872 lines • 96.9 kB
import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
  chmodSync,
  closeSync,
  constants,
  copyFileSync,
  existsSync,
  fsyncSync,
  linkSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  openSync,
  readFileSync,
  readdirSync,
  renameSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { promisify } from "node:util";
import { parseDocument } from "yaml";
import { withProfileSoulLock } from "./fly-soul-bridge.mjs";

export const HERMES_SNAPSHOT_HOOKS = Object.freeze({
  post_tool_call: Object.freeze([
    Object.freeze({
      matcher: "^(cronjob|memory|skill_manage)$",
      command: "/opt/sellable/bin/hermes-memory-dirty",
      // Node startup can exceed one second when a shared-CPU customer is also
      // serving Slack and an operator Desktop. Missing this hook defers a real
      // user memory change until the daily fallback. Keep the hook bounded,
      // but give the marker helper enough CPU time to complete under load.
      timeout: 10,
    }),
  ]),
});
export const HERMES_SNAPSHOT_HOOK_ALLOWLIST = Object.freeze({
  approvals: Object.freeze([
    Object.freeze({
      event: "post_tool_call",
      command: "/opt/sellable/bin/hermes-memory-dirty",
    }),
  ]),
});
export const HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES = `${JSON.stringify(
  HERMES_SNAPSHOT_HOOK_ALLOWLIST,
  null,
  2
)}\n`;
export const HERMES_SNAPSHOT_CRON = Object.freeze({
  name: "sellable-hermes-backup-daily",
  schedule: "0 3 * * *",
  script: "hermes-memory-reconcile.sh",
  installedScript: "scripts/hermes-memory-reconcile.sh",
  noAgent: true,
});
export const HERMES_OPPORTUNITY_SEQUENCES_SKILL =
  "sellable-admin-opportunity-sequences";
const HERMES_OPPORTUNITY_SEQUENCES_SKILL_GROUP = "sellable-admin";
export const HERMES_OPPORTUNITY_SEQUENCES_CRON = Object.freeze({
  name: "sellable-opportunity-sequences",
  schedule: "every 30m",
  deliver: "slack",
  skills: Object.freeze([HERMES_OPPORTUNITY_SEQUENCES_SKILL]),
  enabledToolsets: Object.freeze([
    "file",
    "terminal",
    "safe",
    "sellable",
    "mcp",
  ]),
  noAgent: false,
});
export const HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL =
  "first-campaign-onboarding";
const HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL_GROUP = "sellable-defaults";
export const HERMES_CAMPAIGN_DAILY_REVIEW_SKILL =
  "sellable-campaign-daily-review";
// Each job ticks every hour of every day so that a customer in any time zone is
// reached. The product's due check decides: it wakes the agent only on the
// customer's local working days (weekly: Mondays), from the job's report time
// (review 08:00, results and weekly 09:00) for four hours, and never for a
// finished period, so the later ticks in that window are the retries. Every
// other tick is a cheap "not due" answer and the agent stays asleep.
export const HERMES_CUSTOMER_PROGRAM_CRONS = Object.freeze([
  Object.freeze({
    jobKey: "campaign_daily_review",
    name: "sellable-default-campaign-daily-review",
    schedule: "0 * * * *",
    script: "customer-program-due-campaign-daily-review.py",
    skills: Object.freeze([
      HERMES_CAMPAIGN_DAILY_REVIEW_SKILL,
      "sellable-create-campaign",
      "sellable-refill-sends",
    ]),
    prompt: Object.freeze([
      "Run the installed campaign-daily-review skill.",
      "trigger: scheduled",
      "Use the fenced customer_program claim from this wake and move exactly one useful next step forward.",
      "Keep strict customer_program inputs separate: the review claim uses only action, trigger, and leaseOwner; later outcome and delivery calls use only their documented fields.",
      "Completion contract: never return while the review run has deliveryState pending or deliveryRequired true. Stage the complete Slack text with the same effectId and fence, then return exactly the delivery result required by the skill.",
      "Reply [SILENT] exactly for healthy maintain outcomes and routine refill completion.",
    ]),
  }),
  Object.freeze({
    jobKey: "campaign_daily_results",
    name: "sellable-default-campaign-daily-results",
    schedule: "0 * * * *",
    script: "customer-program-due-campaign-daily-results.py",
    skills: Object.freeze(["sellable-campaign-daily-results"]),
    prompt: Object.freeze([
      "Run the installed campaign-daily-results skill.",
      "trigger: scheduled",
      "Call daily_results with only action, trigger, and the wake's exact leaseOwner; use effectId and fence only for the documented delivery calls.",
      "Publish the prior complete local day once when the active customer program has results.",
    ]),
  }),
  Object.freeze({
    jobKey: "campaign_weekly_review",
    name: "sellable-default-campaign-weekly-review",
    schedule: "0 * * * *",
    script: "customer-program-due-campaign-weekly-review.py",
    skills: Object.freeze([
      "sellable-campaign-weekly-review",
      "sellable-create-campaign",
      "sellable-refill-sends",
    ]),
    prompt: Object.freeze([
      "Run the installed campaign-weekly-review skill.",
      "trigger: scheduled",
      "Call weekly_review with only action, trigger, and the wake's exact leaseOwner; use effectId and fence only for the documented delivery calls.",
      "Publish one combined complete-week review and daily snapshot, then recommend one useful next step.",
    ]),
  }),
]);
// Built-in weekday refill on every agent (proven on the Admin image first). It
// posts nothing (deliver local, reply [SILENT]); the daily review names anything
// only the customer can fix and the results update reports what was added.
// Refill observes capacity itself and does nothing when queues are full, so a
// single weekday tick is enough and a missed one is harmless.
export const HERMES_REFILL_SENDS_CRON = Object.freeze({
  name: "sellable-default-refill-sends",
  deliver: "local",
  skills: Object.freeze(["sellable-refill-sends"]),
  enabledToolsets: Object.freeze(["safe", "sellable", "mcp"]),
});
export const HERMES_CAMPAIGN_DAILY_REVIEW_CRON =
  HERMES_CUSTOMER_PROGRAM_CRONS[0];
export const HERMES_CUSTOMER_PROGRAM_DUE_TEMPLATE_PATH =
  "/usr/local/lib/sellable-agent/default-profile-bundles/shared/scripts/customer-program-due.py";

export const HERMES_FIRST_CAMPAIGN_ONBOARDING_CRON = Object.freeze({
  name: "sellable-first-campaign-onboarding",
  schedule: "0 1 * * *",
  deliver: "slack",
  skills: Object.freeze([HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL]),
  enabledToolsets: Object.freeze(["safe", "sellable", "mcp"]),
  noAgent: false,
  maxReminderDays: 14,
});
export const HERMES_MANAGED_CRON_TOMBSTONES = Object.freeze({
  relativePath: "cron/managed-job-tombstones.json",
  schemaVersion: "sellable-managed-cron-tombstones/v1",
  names: Object.freeze([HERMES_FIRST_CAMPAIGN_ONBOARDING_CRON.name]),
});
export const HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT =
  "/opt/data/.sellable-agent/admin-runtime/opportunity-sequences";
export const HERMES_CLIENT_RELATIONSHIPS_SKILL =
  "sellable-admin-client-relationships";
export const HERMES_CLIENT_DIGEST_CRON_PREFIX = "sellable-client-digest-";
// One job per active client (each with its own gateway delivery target) plus
// one internal chase job. `enabledToolsets` is byte-identical to the
// opportunity-sequences sibling on purpose: there is no agent-callable Slack
// toolset, and `mcp` is what reaches the Admin MCP.
export const HERMES_CLIENT_RELATIONSHIPS_CRONS = Object.freeze({
  digestSchedule: "0 13 * * 1-5",
  chaseName: "sellable-committed-deals-chase",
  chaseSchedule: "30 13 * * 1-5",
  deliver: "slack",
  skills: Object.freeze([HERMES_CLIENT_RELATIONSHIPS_SKILL]),
  enabledToolsets: Object.freeze([
    "file",
    "terminal",
    "safe",
    "sellable",
    "mcp",
  ]),
  noAgent: false,
});
export const CLIENT_RELATIONSHIPS_ASSETS = Object.freeze([
  "SKILL.md",
  "references/aida-client-digest-v1.md",
  "references/shermeen-committed-deal-chase-v1.md",
]);
const LEGACY_HERMES_SNAPSHOT_SCRIPT = "scripts/hermes-memory-reconcile.sh";

const HERMES = "/opt/hermes/.venv/bin/hermes";
const HERMES_PYTHON = "/opt/hermes/.venv/bin/python";
const VERSION = "0.21.5";
const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
const BOUNDED_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
const SHA256 = /^[a-f0-9]{64}$/;
// Native state.db is durable conversation/thread state and grows over time.
// The previous 128 MiB ceiling left the Admin recovery ZIP only ~1.5 MiB of
// headroom in production and eventually made every protected Backup/Refresh
// fail after Hermes had already created the archive. Keep this bounded for
// memory/storage safety, but large enough for an established profile.
export const HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
const MAX_PLUGIN_DATA_BYTES = 10 * 1024 * 1024;
const MAX_PORTABLE_EXTERNAL_BYTES = 32 * 1024 * 1024;
const MAX_PORTABLE_CONFIG_BYTES = 64 * 1024;
// Portable snapshots contain only state that compounds (see
// HERMES_SNAPSHOT_USER_STATE_ENTRIES), a few megabytes. If native Hermes cannot
// package that within two minutes, abort the helper and let the persistent
// retry scheduler back off instead of contending with the live Slack gateway.
const NATIVE_COMMAND_TIMEOUT_MS = 120_000;
const TRANSIENT_PROFILE_ROOTS = new Set([
  ".artifact-venv",
  ".cache",
  // npm/npx installs executable shims as ordinary symlinks under
  // `.npm/_npx/*/node_modules/.bin`. The cache is disposable runtime state,
  // not Hermes memory, and copying it into every staged Admin profile both
  // trips the closed symlink fence and wastes persistent-volume capacity.
  ".npm",
]);
const TRANSIENT_PROFILE_PATHS = new Set([
  ".sellable-admin/.venvs",
  // Hermes creates and removes this root lock with the live gateway process.
  // A Restart can enumerate it immediately before the old gateway exits, so
  // attempting to lstat/copy it races normal shutdown. It is process-local
  // coordination, never durable profile state.
  "gateway.lock",
  // Hermes removes its PID file during gateway shutdown too. Exclude it
  // before lstat so profile promotion cannot race that normal unlink.
  "gateway.pid",
  // PulseAudio records its per-boot server socket as a link into /tmp. It is
  // process-local runtime state, not portable Hermes memory; carrying it into
  // a replacement Machine would both be stale and violate the closed symlink
  // fence. Skip only Pulse's bounded config subtree while every other profile
  // symlink remains rejected below.
  ".config/pulse",
]);

export function isHermesTransientProfilePath(relativePath) {
  const [topLevel] = relativePath.split("/", 1);
  return (
    TRANSIENT_PROFILE_ROOTS.has(topLevel) ||
    TRANSIENT_PROFILE_PATHS.has(relativePath)
  );
}
// Durable profile entries the pre-profile layout kept directly under
// HERMES_HOME. The one-time legacy move carries all of them, run history
// included, because it moves live state on the volume rather than packing a
// recovery archive.
const HERMES_LEGACY_USER_STATE_ENTRIES = Object.freeze([
  "AGENTS.md",
  "SOUL.md",
  "artifacts",
  "cron",
  "home",
  "hooks",
  "kanban.db",
  "memories",
  "memory",
  "plans",
  "plugin-data",
  "plugins",
  "projects",
  "projects.db",
  "sandboxes",
  "scripts",
  "sessions",
  "skins",
  "skills",
  "state.db",
  "workspace",
]);
// The encrypted Fly volume remains the authoritative full profile and keeps
// everything across an in-place Upgrade. The portable archive, used only to
// rebuild an Agent on a replacement volume, carries just the state that
// compounds: who the Agent is (SOUL, AGENTS), what it remembers, the skills it
// learned, its scheduled work (cron and the scripts cron runs), hooks and
// plugins. Run history (state.db, sessions), task boards and scratch areas
// never travel: a rebuilt Agent starts fresh conversations with all of the
// above. Caches, dependency trees, secrets and live logs remain excluded.
export const HERMES_SNAPSHOT_USER_STATE_ENTRIES = Object.freeze([
  "AGENTS.md",
  "SOUL.md",
  "cron",
  "hooks",
  "memories",
  "memory",
  "plugin-data",
  "plugins",
  "scripts",
  "skills",
]);
export const HERMES_SNAPSHOT_RETRY_BACKOFF_MS = Object.freeze([
  15 * 60_000,
  60 * 60_000,
  6 * 60 * 60_000,
  24 * 60 * 60_000,
]);
export const HERMES_HOSTED_BACKGROUND_REVIEW_POLICY = Object.freeze({
  memoryNudgeInterval: 0,
  skillCreationNudgeInterval: 0,
});
const SNAPSHOT_TRANSIENT_DIRECTORIES = new Set([
  ".cache",
  ".git",
  ".mypy_cache",
  ".pytest_cache",
  ".ruff_cache",
  ".tox",
  ".venv",
  "__pycache__",
  "audio_cache",
  "backups",
  "checkpoints",
  "image_cache",
  "lazy-packages",
  "logs",
  "node_modules",
  "site-packages",
  "venv",
]);
const SNAPSHOT_SENSITIVE_FILE_NAMES = new Set([
  "cookies",
  "cookies.json",
  "credentials.json",
  "login data",
  "local state",
  "storage-state.json",
  "token.json",
  "tokens.json",
  "web data",
]);
const SNAPSHOT_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
  "disabled",
  "platform_disabled",
]);
const SNAPSHOT_LIVE_PLUGIN_CONFIG_KEYS = Object.freeze(["enabled", "disabled"]);
export const HERMES_SNAPSHOT_EXTERNAL_USER_STATE = Object.freeze([
  Object.freeze({
    sourceName: "stanley-ideate",
    archivePath: Object.freeze([
      "artifacts",
      "sellable-agent",
      "portable-external",
      "stanley-ideate",
    ]),
  }),
]);
const SOUL_LOCK_ROOT = "soul-locks";
const LEGACY_USER_STATE_MIGRATION_SCHEMA =
  "sellable-hermes-legacy-user-state-migration/v1";
const RESTORE_STATE_SCHEMA = "sellable-hermes-restore-state/v1";
const RETRY_STATE_SCHEMA = "sellable-hermes-snapshot-retry/v1";
const snapshotReconciles = new Map();
const execFileAsync = promisify(execFile);
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
const failed = (code) => ({ ok: false, status: "PENDING_RETRY", code });

export function applyHermesHostedBackgroundReviewPolicy(config) {
  if (!config || typeof config !== "object" || Array.isArray(config)) {
    throw new Error("hermes_hosted_background_review_config_rejected");
  }
  const section = (name) => {
    const value = config[name];
    if (value == null) return {};
    if (typeof value !== "object" || Array.isArray(value)) {
      throw new Error("hermes_hosted_background_review_config_rejected");
    }
    return value;
  };
  return {
    ...config,
    // Hosted users still control memory explicitly through the memory tool.
    // Only the automatic post-turn fork is disabled: it can silently prune a
    // low-value entry while consuming the same model/CPU budget as live work.
    memory: {
      ...section("memory"),
      nudge_interval:
        HERMES_HOSTED_BACKGROUND_REVIEW_POLICY.memoryNudgeInterval,
    },
    // Explicit skill_manage calls and manual /refine remain available. This
    // prevents autonomous skill rewrites without disabling the skill library.
    skills: {
      ...section("skills"),
      creation_nudge_interval:
        HERMES_HOSTED_BACKGROUND_REVIEW_POLICY.skillCreationNudgeInterval,
    },
  };
}

/**
 * Apply the complete hosted Sellable policy that belongs to Hermes config.
 *
 * Sellable owns customer onboarding and the one-time Slack welcome. Hermes'
 * generic personal-profile questionnaire competes with that flow on the first
 * customer message, so hosted Sellable profiles disable it while retaining
 * every unrelated first-touch flag Hermes may have already recorded.
 */
export function applyHermesSellableHostedPolicy(config) {
  const hosted = applyHermesHostedBackgroundReviewPolicy(config);
  const onboarding = hosted.onboarding;
  if (
    onboarding != null &&
    (typeof onboarding !== "object" || Array.isArray(onboarding))
  ) {
    throw new Error("hermes_sellable_onboarding_config_rejected");
  }
  const seen = onboarding?.seen;
  if (seen != null && (typeof seen !== "object" || Array.isArray(seen))) {
    throw new Error("hermes_sellable_onboarding_config_rejected");
  }
  const cron = config?.cron;
  if (cron != null && (typeof cron !== "object" || Array.isArray(cron))) {
    throw new Error("hermes_sellable_cron_config_rejected");
  }
  return {
    ...hosted,
    // Scheduled messages are the agent's final text. Hermes' job-ID header and
    // stop-instructions footer must not be added to them.
    cron: { ...(cron ?? {}), wrap_response: false },
    onboarding: {
      ...(onboarding ?? {}),
      profile_build: "off",
      sellable_managed_welcome: true,
      seen: {
        ...(seen ?? {}),
        profile_build_offered: true,
      },
    },
  };
}

function fsyncDirectory(path) {
  const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY);
  try {
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
}

function atomicWrite(path, bytes, mode) {
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}`);
  writeFileSync(temporary, bytes, { flag: "wx", mode });
  chmodSync(temporary, mode);
  const fd = openSync(temporary, constants.O_RDONLY);
  try {
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
  renameSync(temporary, path);
  fsyncDirectory(dirname(path));
}

function regularFile(path) {
  if (!existsSync(path)) throw new Error("hermes_snapshot_file_missing");
  const stat = lstatSync(path);
  if (stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o022) !== 0) {
    throw new Error("hermes_snapshot_file_rejected");
  }
  return stat;
}

function validProfilePath(profileId, profileRoot, hermesRoot) {
  return (
    ID.test(profileId ?? "") &&
    isAbsolute(profileRoot) &&
    isAbsolute(hermesRoot) &&
    resolve(profileRoot) === resolve(hermesRoot, "profiles", profileId)
  );
}

function assertHermesMergeRoot(path, { allowMissing = false } = {}) {
  if (!existsSync(path)) {
    if (allowMissing) return false;
    throw new Error("hermes_snapshot_merge_root_rejected");
  }
  const stat = lstatSync(path);
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
    throw new Error("hermes_snapshot_merge_root_rejected");
  }
  return true;
}

function mergeHermesStateEntries(
  sourceRoot,
  targetRoot,
  { skippedTopLevelNames = new Set(), linkFiles = false } = {}
) {
  const merge = (source, target, relativeRoot = "") => {
    for (const name of readdirSync(source)) {
      if (!relativeRoot && skippedTopLevelNames.has(name)) continue;
      // UV deliberately materializes wheel-cache entries as symlinks. The
      // cache is disposable and must not become durable profile state; skip
      // only the top-level cache while preserving the closed symlink reject
      // for sessions, skills, memory, and every other Hermes-owned path. Old
      // Admin installers also put a symlink-bearing Lightfield virtualenv in
      // `.sellable-admin/.venvs`; current images materialize it under the
      // runtime root, so it is disposable legacy state rather than profile
      // content.
      const relativePath = relativeRoot ? `${relativeRoot}/${name}` : name;
      if (isHermesTransientProfilePath(relativePath)) {
        continue;
      }
      const from = join(source, name);
      const to = join(target, name);
      const stat = lstatSync(from);
      if (stat.isSymbolicLink()) {
        throw new Error("hermes_snapshot_merge_symlink_rejected");
      }
      if (existsSync(to)) {
        const current = lstatSync(to);
        if (current.isSymbolicLink()) {
          throw new Error("hermes_snapshot_merge_target_rejected");
        }
        if (stat.isDirectory() && current.isDirectory())
          merge(from, to, relativePath);
        continue;
      }
      if (stat.isDirectory()) {
        mkdirSync(to, { mode: stat.mode & 0o777 });
        merge(from, to, relativePath);
      } else if (stat.isFile()) {
        if (linkFiles) {
          // Profile replacement happens on one encrypted Fly volume while the
          // gateway is stopped and the profile lock is held. A hard link keeps
          // durable Hermes state byte-identical without copying a potentially
          // large state tree into the new managed profile. The old tree is
          // unlinked asynchronously after promotion, leaving one live inode.
          linkSync(from, to);
          const linked = lstatSync(to);
          if (
            !linked.isFile() ||
            linked.dev !== stat.dev ||
            linked.ino !== stat.ino
          ) {
            throw new Error("hermes_snapshot_merge_link_rejected");
          }
        } else {
          copyFileSync(from, to);
          chmodSync(to, stat.mode & 0o777);
        }
      } else if (stat.isFIFO() || stat.isSocket()) {
        // Runtime control FIFOs and Unix sockets are process-local endpoints,
        // not durable Hermes state. A normal Restart can leave one behind
        // briefly while the replacement profile is staged; never copy it and
        // never turn that harmless handoff into a failed lifecycle operation.
        continue;
      } else {
        throw new Error("hermes_snapshot_merge_entry_rejected");
      }
    }
  };
  merge(sourceRoot, targetRoot);
}

function hermesTopLevelNameSet(names, errorCode) {
  return new Set(
    names.map((name) => {
      if (
        typeof name !== "string" ||
        !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) ||
        name === "." ||
        name === ".."
      ) {
        throw new Error(errorCode);
      }
      return name;
    })
  );
}

export function mergeHermesState(
  sourceRoot,
  targetRoot,
  /** @type {{skipSourceTopLevelNames?:string[]}} */
  { skipSourceTopLevelNames = [] } = {}
) {
  if (!assertHermesMergeRoot(sourceRoot, { allowMissing: true })) return;
  assertHermesMergeRoot(targetRoot);
  mergeHermesStateEntries(sourceRoot, targetRoot, {
    skippedTopLevelNames: hermesTopLevelNameSet(
      skipSourceTopLevelNames,
      "hermes_snapshot_merge_policy_rejected"
    ),
  });
}

/**
 * Clone one stopped profile into a same-volume staging directory without
 * duplicating durable file bytes. Managed files already rendered in the
 * target win; every other durable file is linked and independently verified.
 */
export function linkHermesState(
  sourceRoot,
  targetRoot,
  /** @type {{skipSourceTopLevelNames?:string[]}} */
  { skipSourceTopLevelNames = [] } = {}
) {
  if (!assertHermesMergeRoot(sourceRoot, { allowMissing: true })) return;
  assertHermesMergeRoot(targetRoot);
  const source = lstatSync(sourceRoot);
  const target = lstatSync(targetRoot);
  if (source.dev !== target.dev) {
    throw new Error("hermes_snapshot_merge_cross_device_rejected");
  }
  mergeHermesStateEntries(sourceRoot, targetRoot, {
    skippedTopLevelNames: hermesTopLevelNameSet(
      skipSourceTopLevelNames,
      "hermes_snapshot_merge_policy_rejected"
    ),
    linkFiles: true,
  });
}

/**
 * Replace one mutable Hermes subtree while retaining explicitly sealed
 * top-level namespaces in the staged target.
 *
 */
export function replaceHermesState(
  sourceRoot,
  targetRoot,
  /** @type {{preserveTargetTopLevelNames?:string[],preserveTargetPaths?:string[]}} */
  { preserveTargetTopLevelNames = [], preserveTargetPaths = [] } = {}
) {
  assertHermesMergeRoot(targetRoot);
  const preservedTopLevel = hermesTopLevelNameSet(
    preserveTargetTopLevelNames,
    "hermes_snapshot_replace_policy_rejected"
  );
  const preservedPaths = new Set(
    preserveTargetPaths.map((path) => {
      if (
        typeof path !== "string" ||
        !/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(
          path
        )
      ) {
        throw new Error("hermes_snapshot_replace_policy_rejected");
      }
      return path;
    })
  );
  for (const name of preservedTopLevel) preservedPaths.add(name);
  const prune = (target, relativeRoot = "") => {
    for (const name of readdirSync(target)) {
      const relativePath = relativeRoot ? `${relativeRoot}/${name}` : name;
      const targetPath = join(target, name);
      const stat = lstatSync(targetPath);
      if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile())) {
        throw new Error("hermes_snapshot_merge_target_rejected");
      }
      if (preservedPaths.has(relativePath)) continue;
      if (
        [...preservedPaths].some((path) => path.startsWith(`${relativePath}/`))
      ) {
        if (!stat.isDirectory()) {
          throw new Error("hermes_snapshot_merge_target_rejected");
        }
        prune(targetPath, relativePath);
        continue;
      }
      rmSync(targetPath, { recursive: true, force: false });
    }
  };
  prune(targetRoot);
  if (!assertHermesMergeRoot(sourceRoot, { allowMissing: true })) return;
  // The merge is deliberately no-clobber. Exact sealed target paths survive,
  // while mutable source siblings (for example sellable/<custom-skill>) are
  // carried into the staged profile.
  mergeHermesStateEntries(sourceRoot, targetRoot);
}

const LEGACY_LIGHTFIELD_VENV = ".venv-amplify-audit";
const LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA =
  "sellable-agent-lightfield-venv-quarantine/v1";

function privateMigrationDirectory(path) {
  if (existsSync(path)) {
    const stat = lstatSync(path);
    if (stat.isSymbolicLink() || !stat.isDirectory()) {
      throw new Error("hermes_lightfield_quarantine_path_rejected");
    }
    return;
  }
  mkdirSync(path, { mode: 0o700 });
  fsyncDirectory(dirname(path));
}

/**
 * Quarantine the one persisted pre-Phase-189 Lightfield virtualenv layout.
 * Its ordinary Python symlinks must never weaken the general snapshot scan.
 */
export function quarantineLegacyLightfieldVenv({
  profileId,
  profileRoot,
  hermesRoot,
} = {}) {
  if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
    return {
      ...failed("hermes_lightfield_quarantine_identity_rejected"),
      quarantinePath: null,
    };
  }
  const legacyPath = join(profileRoot, LEGACY_LIGHTFIELD_VENV);
  const quarantineBase = join(
    hermesRoot,
    "backups",
    "quarantine",
    "legacy-lightfield-venv-v1"
  );
  const quarantinePath = join(quarantineBase, profileId);
  const marker = join(
    profileRoot,
    ".sellable-agent",
    "migrations",
    "lightfield-venv-quarantine-v1.json"
  );
  try {
    if (existsSync(marker)) {
      regularFile(marker);
      const receipt = JSON.parse(readFileSync(marker, "utf8"));
      if (
        receipt?.schemaVersion !== LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA ||
        receipt.profileId !== profileId ||
        receipt.source !== LEGACY_LIGHTFIELD_VENV ||
        receipt.quarantinePath !== quarantinePath ||
        !existsSync(quarantinePath) ||
        lstatSync(quarantinePath).isSymbolicLink() ||
        !lstatSync(quarantinePath).isDirectory()
      ) {
        throw new Error("hermes_lightfield_quarantine_receipt_rejected");
      }
      return { ok: true, status: "REUSED", profileId, quarantinePath };
    }
    const sourcePresent = existsSync(legacyPath);
    const targetPresent = existsSync(quarantinePath);
    if (!sourcePresent && !targetPresent) {
      return {
        ok: true,
        status: "NO_LEGACY_LAYOUT",
        profileId,
        quarantinePath: null,
      };
    }
    if (sourcePresent && targetPresent) {
      throw new Error("hermes_lightfield_quarantine_conflict");
    }
    let status = "RESUMED";
    if (sourcePresent) {
      const source = lstatSync(legacyPath);
      if (source.isSymbolicLink() || !source.isDirectory()) {
        throw new Error("hermes_lightfield_quarantine_source_rejected");
      }
      privateMigrationDirectory(join(hermesRoot, "backups"));
      privateMigrationDirectory(join(hermesRoot, "backups", "quarantine"));
      privateMigrationDirectory(quarantineBase);
      moveDurably(legacyPath, quarantinePath);
      status = "QUARANTINED";
    }
    const quarantined = lstatSync(quarantinePath);
    if (quarantined.isSymbolicLink() || !quarantined.isDirectory()) {
      throw new Error("hermes_lightfield_quarantine_readback_rejected");
    }
    atomicWrite(
      marker,
      `${JSON.stringify({
        schemaVersion: LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA,
        profileId,
        source: LEGACY_LIGHTFIELD_VENV,
        quarantinePath,
      })}\n`,
      0o600
    );
    return { ok: true, status, profileId, quarantinePath };
  } catch {
    return {
      ...failed("hermes_lightfield_quarantine_failed"),
      quarantinePath: null,
    };
  }
}

function cronJobs(profileRoot) {
  const path = join(profileRoot, "cron", "jobs.json");
  if (!existsSync(path)) return [];
  regularFile(path);
  const jobs = JSON.parse(readFileSync(path, "utf8")).jobs;
  if (!Array.isArray(jobs)) throw new Error("hermes_snapshot_cron_rejected");
  return jobs;
}

function assertStagedCronDependencies(profileRoot) {
  const skillNames = new Set();
  const skillsRoot = join(profileRoot, "skills");
  const walkSkills = (directory) => {
    for (const name of readdirSync(directory)) {
      const path = join(directory, name);
      const stat = lstatSync(path);
      if (stat.isSymbolicLink()) {
        throw new Error("hermes_snapshot_dependency_rejected");
      }
      if (stat.isDirectory()) walkSkills(path);
      else if (stat.isFile() && name === "SKILL.md") {
        skillNames.add(basename(directory));
      }
    }
  };
  if (existsSync(skillsRoot)) {
    const stat = lstatSync(skillsRoot);
    if (stat.isSymbolicLink() || !stat.isDirectory()) {
      throw new Error("hermes_snapshot_dependency_rejected");
    }
    walkSkills(skillsRoot);
  }
  const scriptsRoot = join(profileRoot, "scripts");
  for (const job of cronJobs(profileRoot)) {
    if (!job || typeof job !== "object" || Array.isArray(job)) {
      throw new Error("hermes_snapshot_dependency_rejected");
    }
    const declaredSkills = [];
    if (Object.hasOwn(job, "skill") && job.skill !== null) {
      if (typeof job.skill !== "string" || !ID.test(job.skill)) {
        throw new Error("hermes_snapshot_dependency_rejected");
      }
      declaredSkills.push(job.skill);
    }
    if (Object.hasOwn(job, "skills") && job.skills !== null) {
      if (
        !Array.isArray(job.skills) ||
        job.skills.some((skill) => typeof skill !== "string" || !ID.test(skill))
      ) {
        throw new Error("hermes_snapshot_dependency_rejected");
      }
      declaredSkills.push(...job.skills);
    }
    if (declaredSkills.some((skill) => !skillNames.has(skill))) {
      throw new Error("hermes_snapshot_dependency_rejected");
    }
    if (Object.hasOwn(job, "script") && job.script !== null) {
      if (
        typeof job.script !== "string" ||
        !/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(
          job.script
        )
      ) {
        throw new Error("hermes_snapshot_dependency_rejected");
      }
      const script = job.script.startsWith("scripts/")
        ? job.script.slice("scripts/".length)
        : job.script;
      const scriptPath = join(scriptsRoot, script);
      if (!existsSync(scriptPath)) {
        throw new Error("hermes_snapshot_dependency_rejected");
      }
      const stat = lstatSync(scriptPath);
      if (stat.isSymbolicLink() || !stat.isFile()) {
        throw new Error("hermes_snapshot_dependency_rejected");
      }
    }
  }
}

const sameJson = (left, right) =>
  JSON.stringify(left) === JSON.stringify(right);

export async function ensureHermesNamedCron({
  profileRoot,
  name,
  isExpected,
  isUpdatable = () => true,
  create,
  update,
  codePrefix = "hermes_named_cron",
}) {
  if (
    !isAbsolute(profileRoot) ||
    !ID.test(name ?? "") ||
    typeof isExpected !== "function" ||
    typeof create !== "function" ||
    typeof update !== "function"
  ) {
    return failed(`${codePrefix}_identity_rejected`);
  }
  try {
    const before = cronJobs(profileRoot);
    const named = before.filter((job) => job?.name === name);
    if (named.length > 1) {
      return failed(`${codePrefix}_inventory_rejected`);
    }
    if (named[0] && isExpected(named[0])) {
      return {
        ok: true,
        status: "REUSED",
        jobId: named[0].id,
        job: named[0],
      };
    }
    if (named[0] && !isUpdatable(named[0])) {
      return failed(`${codePrefix}_inventory_rejected`);
    }
    const siblings = before.filter((job) => job?.name !== name);
    const mutation = named[0] ? await update(named[0]) : await create();
    if (childFailed(mutation)) {
      return failed(`${codePrefix}_${named[0] ? "update" : "create"}_failed`);
    }
    const after = cronJobs(profileRoot);
    const readback = after.filter((job) => job?.name === name);
    const afterSiblings = after.filter((job) => job?.name !== name);
    if (!sameJson(afterSiblings, siblings)) {
      return failed(`${codePrefix}_sibling_readback_rejected`);
    }
    if (readback.length !== 1 || !isExpected(readback[0])) {
      return failed(
        `${codePrefix}_${named[0] ? "update_readback" : "readback"}_rejected`
      );
    }
    return {
      ok: true,
      status: named[0] ? "UPDATED" : "CREATED",
      jobId: readback[0].id,
      job: readback[0],
    };
  } catch {
    return failed(`${codePrefix}_install_failed`);
  }
}

function isCron(job) {
  return Boolean(
    job?.id &&
      job.name === HERMES_SNAPSHOT_CRON.name &&
      job.script === HERMES_SNAPSHOT_CRON.script &&
      job.no_agent === true &&
      job.enabled === true &&
      job.schedule?.kind === "cron" &&
      job.schedule?.expr === HERMES_SNAPSHOT_CRON.schedule
  );
}

function isLegacyCron(job) {
  return Boolean(
    job?.id &&
      job.name === HERMES_SNAPSHOT_CRON.name &&
      job.script === LEGACY_HERMES_SNAPSHOT_SCRIPT &&
      job.no_agent === true &&
      job.enabled === true &&
      job.schedule?.kind === "cron" &&
      job.schedule?.expr === HERMES_SNAPSHOT_CRON.schedule
  );
}

const invocation = (args, hermesRoot, hermesCli = HERMES) => ({
  command: hermesCli,
  args,
  env: {
    ...process.env,
    HOME: hermesRoot,
    HERMES_HOME: hermesRoot,
    NO_COLOR: "1",
  },
  shell: false,
  timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
  maxOutputBytes: 64 * 1024,
});

async function runBoundedHermesChild(call) {
  try {
    const { stdout, stderr } = await execFileAsync(call.command, call.args, {
      env: call.env,
      encoding: "utf8",
      timeout: call.timeoutMs,
      maxBuffer: call.maxOutputBytes,
      killSignal: "SIGKILL",
    });
    return {
      exitCode: 0,
      signal: null,
      stdout,
      stderr,
      timedOut: false,
      outputLimitExceeded: false,
    };
  } catch (error) {
    const outputLimitExceeded =
      error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
    return {
      exitCode: Number.isInteger(error?.code) ? error.code : null,
      signal: error?.signal ?? null,
      stdout: String(error?.stdout ?? ""),
      stderr: String(error?.stderr ?? ""),
      timedOut: Boolean(error?.killed) && !outputLimitExceeded,
      outputLimitExceeded,
    };
  }
}

const childRunner = (runChild) => runChild ?? runBoundedHermesChild;
const childFailed = (result) =>
  result?.exitCode !== 0 || result?.timedOut || result?.outputLimitExceeded;

function nativeSummaryAccepted(result, complete, incomplete) {
  const output = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`;
  return (
    !childFailed(result) &&
    output.includes(complete) &&
    !output.includes(incomplete) &&
    !output.includes("Warnings (")
  );
}

function snapshotSensitiveFile(pathValue) {
  const name = basename(pathValue).toLowerCase();
  return (
    name === ".env" ||
    name.startsWith(".env.") ||
    SNAPSHOT_SENSITIVE_FILE_NAMES.has(name)
  );
}

function stageUserStateEntry(source, target, budget = null) {
  const stat = lstatSync(source);
  if (stat.isSymbolicLink()) return;
  if (stat.isDirectory()) {
    if (SNAPSHOT_TRANSIENT_DIRECTORIES.has(basename(source))) return;
    mkdirSync(target, { recursive: true, mode: 0o700 });
    for (const name of readdirSync(source)) {
      stageUserStateEntry(join(source, name), join(target, name), budget);
    }
    return;
  }
  if (!stat.isFile()) return;
  if (snapshotSensitiveFile(source)) return;
  if (budget) {
    if (budget.bytes + stat.size > budget.limit) {
      throw new Error(budget.code);
    }
    budget.bytes += stat.size;
  }
  mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
  linkSync(source, target);
}

function portableConfigBytes(profileRoot) {
  const configPath = join(profileRoot, "config.yaml");
  if (!existsSync(configPath)) return Buffer.from("{}\n", "utf8");
  regularFile(configPath);
  const document = parseDocument(readFileSync(configPath, "utf8"));
  const value = document.toJS();
  if (
    document.errors.length > 0 ||
    !value ||
    typeof value !== "object" ||
    Array.isArray(value)
  ) {
    throw new Error("hermes_snapshot_portable_config_rejected");
  }
  const projected = {};
  const projectSection = (name, keys) => {
    const section = value[name];
    if (section === undefined) return;
    if (!section || typeof section !== "object" || Array.isArray(section)) {
      throw new Error("hermes_snapshot_portable_config_rejected");
    }
    const selected = Object.fromEntries(
      keys
        .filter((key) => Object.prototype.hasOwnProperty.call(section, key))
        .map((key) => [key, structuredClone(section[key])])
    );
    if (Object.keys(selected).length > 0) projected[name] = selected;
  };
  projectSection("skills", SNAPSHOT_LIVE_SKILLS_CONFIG_KEYS);
  projectSection("plugins", SNAPSHOT_LIVE_PLUGIN_CONFIG_KEYS);
  const platformToolsets = value.platform_toolsets;
  if (platformToolsets !== undefined) {
    if (
      !platformToolsets ||
      typeof platformToolsets !== "object" ||
      Array.isArray(platformToolsets)
    ) {
      throw new Error("hermes_snapshot_portable_config_rejected");
    }
    if (Object.prototype.hasOwnProperty.call(platformToolsets, "slack")) {
      projected.platform_toolsets = {
        slack: structuredClone(platformToolsets.slack),
      };
    }
  }
  const bytes = Buffer.from(`${JSON.stringify(projected, null, 2)}\n`, "utf8");
  if (bytes.byteLength > MAX_PORTABLE_CONFIG_BYTES) {
    throw new Error("hermes_snapshot_portable_config_bound_rejected");
  }
  return bytes;
}

function stageHermesUserState({ profileId, profileRoot, hermesRoot }) {
  const backupsRoot = join(hermesRoot, "backups");
  mkdirSync(backupsRoot, { recursive: true, mode: 0o700 });
  const stagingRoot = mkdtempSync(join(backupsRoot, ".user-state-"));
  chmodSync(stagingRoot, 0o700);
  const stagedProfileRoot = join(stagingRoot, "profiles", profileId);
  mkdirSync(stagedProfileRoot, { recursive: true, mode: 0o700 });
  // Hermes import requires one recognizable profile marker. Preserve only
  // user-owned mutable configuration; control-plane bindings, providers,
  // environment, auth, and arbitrary plugin config never enter the archive.
  atomicWrite(
    join(stagedProfileRoot, "config.yaml"),
    portableConfigBytes(profileRoot),
    0o600
  );
  for (const name of HERMES_SNAPSHOT_USER_STATE_ENTRIES) {
    const source = join(profileRoot, name);
    if (existsSync(source)) {
      if (name === "SOUL.md") {
        const sourceStat = lstatSync(source);
        if (
          sourceStat.isSymbolicLink() ||
          !sourceStat.isFile() ||
          sourceStat.nlink !== 1
        ) {
          throw new Error("hermes_snapshot_soul_metadata_rejected");
        }
        const target = join(stagedProfileRoot, name);
        // Hermes rejects the live SOUL when its link count is not exactly one.
        // A capture can run for a while, so a staged hard link would make
        // otherwise healthy Agent turns fail for the whole capture window.
        copyFileSync(source, target, constants.COPYFILE_EXCL);
        chmodSync(target, 0o600);
        if (regularFile(target).nlink !== 1) {
          throw new Error("hermes_snapshot_soul_metadata_rejected");
        }
        continue;
      }
      stageUserStateEntry(
        source,
        join(stagedProfileRoot, name),
        name === "plugin-data"
          ? {
              bytes: 0,
              limit: MAX_PLUGIN_DATA_BYTES,
              code: "hermes_snapshot_plugin_data_bound_rejected",
            }
          : null
      );
    }
  }
  for (const external of HERMES_SNAPSHOT_EXTERNAL_USER_STATE) {
    const source = join(hermesRoot, external.sourceName);
    if (!existsSync(source)) continue;
    const sourceStat = lstatSync(source);
    if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) {
      throw new Error("hermes_snapshot_external_state_rejected");
    }
    const target = join(stagedProfileRoot, ...external.archivePath);
    rmSync(target, { recursive: true, force: true });
    stageUserStateEntry(source, target, {
      bytes: 0,
      limit: MAX_PORTABLE_EXTERNAL_BYTES,
      code: "hermes_snapshot_external_state_bound_rejected",
    });
  }
  return stagingRoot;
}

function restorePortableExternalUserState({ profileRoot, hermesRoot }) {
  for (const external of HERMES_SNAPSHOT_EXTERNAL_USER_STATE) {
    const source = join(profileRoot, ...external.archivePath);
    if (!existsSync(source)) continue;
    const sourceStat = lstatSync(source);
    if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) {
      throw new Error("hermes_snapshot_external_state_rejected");
    }
    const target = join(hermesRoot, external.sourceName);
    if (existsSync(target)) {
      const targetStat = lstatSync(target);
      if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
        throw new Error("hermes_snapshot_external_state_rejected");
      }
    } else {
      mkdirSync(target, { recursive: true, mode: 0o700 });
    }
    mergeHermesState(source, target);
  }
}

function legacyUserStateMigrationMarker(profileRoot) {
  return join(
    profileRoot,
    ".sellable-agent",
    "migrations",
    "legacy-root-user-state-v1.json"
  );
}

function readLegacyUserStateMigration(path, profileId) {
  regularFile(path);
  const value = JSON.parse(readFileSync(path, "utf8"));
  if (
    value?.schemaVersion !== LEGACY_USER_STATE_MIGRATION_SCHEMA ||
    value.profileId !== profileId ||
    !Array.isArray(value.entries) ||
    value.entries.some(
      (name) =>
        typeof name !== "string" ||
        !HERMES_LEGACY_USER_STATE_ENTRIES.includes(name)
    ) ||
    new Set(value.entries).size !== value.entries.length
  ) {
    throw new Error("hermes_legacy_user_state_migration_rejected");
  }
  return value;
}

/**
 * Move the pre-profile Hermes layout into the current profile exactly once.
 *
 * Older Admin Machines stored durable user state directly under HERMES_HOME
 * (for example /opt/data/skills and /opt/data/cron). Profile isolation moved
 * those paths under profiles/<id>. Snapshot restore cannot discover that
 * unscoped state because it intentionally restores profile archives only.
 * Stage only the bounded legacy user-state allowlist, merge without
 * overwriting newer profile bytes, then mark the resulting profile dirty so the
 * next portable snapshot becomes self-contained.
 */
export function migrateLegacyHermesUserState({
  profileId,
  profileRoot,
  hermesRoot,
}) {
  if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
    return failed("hermes_legacy_user_state_identity_rejected");
  }
  const marker = legacyUserStateMigrationMarker(profileRoot);
  try {
    if (existsSync(marker)) {
      const receipt = readLegacyUserStateMigration(marker, profileId);
      return {
        ok: true,
        status: "REUSED",
        profileId,
        entries: receipt.entries,
      };
    }
    const entries = HERMES_LEGACY_USER_STATE_ENTRIES.filter((name) =>
      existsSync(join(hermesRoot, name))
    );
    if (entries.length === 0) {
      return { ok: true, status: "NO_LEGACY_STATE", profileId, entries: [] };
    }
    const backupsRoot = join(hermesRoot, "backups");
    mkdirSync(backupsRoot, { recursive: true, mode: 0o700 });
    const stagingRoot = mkdtempSync(join(backupsRoot, ".legacy-user-state-"));
    chmodSync(stagingRoot, 0o700);
    try {
      for (const name of entries) {
        const source = join(hermesRoot, name);
        const stat = lstatSync(source);
        if (stat.isSymbolicLink()) {
          throw new Error("hermes_legacy_user_state_symlink_rejected");
        }
        stageUserStateEntry(source, join(stagingRoot, name));
        if (name.endsWith(".db")) {
          for (const suffix of ["-wal", "-shm", "-journal"]) {
            const sidecar = `${source}${suffix}`;
            if (existsSync(sidecar)) {
              stageUserStateEntry(
                sidecar,
                join(stagingRoot, `${name}${suffix}`)
              );
            }
          }
        }
      }
      mkdirSync(profileRoot, { recursive: true, mode: 0o700 });
      mergeHermesState(stagingRoot, profileRoot);
      if (entries.some((name) => !existsSync(join(profileRoot, name)))) {
        throw new Error("hermes_legacy_user_state_readback_rejected");
      }
      atomicWrite(
        marker,
        `${JSON.stringify({
          schemaVersion: LEGACY_USER_STATE_MIGRATION_SCHEMA,
          profileId,
          entries,
        })}\n`,
        0o600
      );
      markHermesSnapshotDirty(profileRoot);
      return { ok: true, status: "MIGRATED", profileId, entries };
    } finally {
      rmSync(stagingRoot, { recursive: true, force: true });
    }
  } catch {
    return failed("hermes_legacy_user_state_migration_failed");
  }
}

export function markHermesSnapshotDirty(profileRoot) {
  if (
    !isAbsolute(profileRoot) ||
    !existsSync(profileRoot) ||
    lstatSync(profileRoot).isSymbolicLink() ||
    !lstatSync(profileRoot).isDirectory()
  ) {
    throw new Error("hermes_snapshot_profile_rejected");
  }
  atomicWrite(
    join(profileRoot, ".sellable-agent", "memory", "dirty.json"),
    '{"schemaVersion":"sellable-hermes-snapshot-dirty/v1","dirty":true}\n',
    0o600
  );
}

export async function ensureHermesSnapshotCron({
  profileId,
  profileRoot,
  hermesRoot,
  hermesCli = HERMES,
  reconcileTemplatePath = "/opt/sellable/share/hermes-memory-reconcile.sh",
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot)
  ) {
    return failed("hermes_snapshot_cron_identity_rejected");
  }
  try {
    regularFile(reconcileTemplatePath);
    atomicWrite(
      join(profileRoot, HERMES_SNAPSHOT_CRON.installedScript),
      readFileSync(reconcileTemplatePath),
      0o700
    );
    const result = await ensureHermesNamedCron({
      profileRoot,
      name: HERMES_SNAPSHOT_CRON.name,
      isExpected: isCron,
      isUpdatable: (job) => isCron(job) || isLegacyCron(job),
      codePrefix: "hermes_snapshot_cron",
      create: () =>
        childRunner(runChild)(
          invocation(
            [
              "--profile",
              profileId,
              "cron",
              "create",
              HERMES_SNAPSHOT_CRON.schedule,
              "--name",
              HERMES_SNAPSHOT_CRON.name,
              "--script",
              HERMES_SNAPSHOT_CRON.script,
              "--no-agent",
            ],
            hermesRoot,
            hermesCli
          )
        ),
      update: (job) =>
        childRunner(runChild)(
          invocation(
            [
              "--profile",
              profileId,
              "cron",
              "edit",
              job.id,
              "--script",
              HERMES_SNAPSHOT_CRON.script,
              "--no-agent",
            ],
            hermesRoot,
            hermesCli
          )
        ),
    });
    if (!result.ok) {
      if (result.code === "hermes_snapshot_cron_update_failed") {
        return failed("hermes_snapshot_cron_migrate_failed");
      }
      if (result.code === "hermes_snapshot_cron_update_readback_rejected") {
        return failed("hermes_snapshot_cron_migrate_readback_rejected");
      }
      return result;
    }
    return {
      ok: true,
      status: result.status === "UPDATED" ? "MIGRATED" : result.status,
      jobId: result.jobId,
      profileId,
    };
  } catch {
    return failed("hermes_snapshot_cron_install_failed");
  }
}

const OPPORTUNITY_SEQUENCE_ASSETS = Object.freeze([
  "SKILL.md",
  "references/outbound-multichannel-v2.md",
  "references/positive-reply-to-agreed-meeting-v1.md",
]);
const SLACK_USER_ID = /^U[A-Z0-9]{8,20}$/;
const OPPORTUNITY_SEQUENCE_CRON_PYTHON = [
  "import json,sys",
  "from cron.jobs import create_job, update_job",
  "value=json.loads(sys.argv[1])",
  "job_id=value.pop('jobId', None)",
  "enabled=value.pop('enabled')",
  "result=update_job(job_id, {**value, 'enabled': enabled}) if job_id else create_job(**value)",
  "print(json.dumps({'id': result.get('id') if result else None}))",
].join("\n");
const OPPORTUNITY_SEQUENCE_CRON_REMOVE_PYTHON = [
  "import json,sys",
  "from cron.jobs import remove_job",
  "value=json.loads(sys.argv[1])",
  "print(json.dumps({'removed': bool(remove_job(value['removeJobId']))}))",
].join("\n");

const ONBOARDING_WORKSPACE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
const ONBOARDING_CHANNEL_ID = /^[CG][A-Z0-9]{8,20}$/;
const ONBOARDING_CRON_SCHEDULE =
  /^[0-9*/,-]+ [0-9*/,-]+ [0-9*/,-]+ [0-9*/,-]+ [0-9*/,-]+$/;

function firstCampaignOnboardingIdentity(profileRoot) {
  const path = join(
    profileRoot,
    "skills",
    HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL_GROUP,
    HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL,
    "SKILL.md"
  );
  regularFile(path);
  return { path, sha256: sha256(readFileSync(path)) };
}

function firstCampaignOnboardingPrompt({ workspaceId, skillSha256 }) {
  return [
    "Run the installed first-campaign-onboarding skill.",
    "mode: scheduled",
    `workspaceId: ${workspaceId}`,
    `maxReminderDays: ${HERMES_FIRST_CAMPAIGN_ONBOARDING_CRON.maxReminderDays}`,
    `Require the installed skill SHA-256 ${skillSha256}.`,
    "Reply [SILENT] exactly when the skill says no reminder is due.",
    "Never send prospect outreach and never use a direct Slack transport; the Hermes gateway delivers the reply.",
  ].join("\n");
}

/**
 * Install or repair the one customer-profile onboarding reminder. The public
 * Sellable MCP skill owns behavior; this helper owns only the exact native
 * Hermes schedule, customer binding, and gateway delivery target.
 */
export async function ensureHermesFirstCampaignOnboardingCron({
  profileId,
  profileRoot,
  hermesRoot,
  workspaceId,
  channelId,
  hermesPython = HERMES_PYTHON,
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot) ||
    !ONBOARDING_WORKSPACE_ID.test(workspaceId ?? "") ||
    /[\r\n\0]/.test(workspaceId ?? "") ||
    !ONBOARDING_CHANNEL_ID.test(channelId ?? "") ||
    /[\r\n\0]/.test(channelId ?? "")
  ) {
    return failed("hermes_first_campaign_onboarding_cron_identity_rejected");
  }
  try {
    const before = cronJobs(profileRoot);
    const matches = before.filter(
      (job) =>
        job?.name === HERMES_FIRST_CAMPAIGN_ONBOARDING_CRON.name ||
        job?.skill === HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL ||
        job?.skills?.includes?.(HERMES_FIRST_CAMPAIGN_ONBOARDING_SKILL)
    );
    if (matches.some((job) => !ID.test(job?.id ?? ""))) {
      return failed(
        "hermes_first_campaign_onboarding_cron_tombstone_inventory_rejected"
      );
    }
    const siblings = before.filter(
      (job) => !matches.some((match) => match.id === job?.id)
    );
    for (const job of matches) {
      const mutation = await childRunner(runChild)({
        command: hermesPython,
        args: [
          "-c",
          OPPORTUNITY_SEQUENCE_CRON_REMOVE_PYTHON,
          JSON.stringify({ removeJobId: job.id }),
        ],
        env: {
          ...process.env,
          HOME: hermesRoot,
          HERMES_HOME: profileRoot,
          HERMES_PROFILE: profileId,
          NO_COLOR: "1",
        },
        shell: false,
        timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
        maxOutputBytes: 64 * 1024,
      });
      if (childFailed(mutation)) {
        return failed(
          "hermes_first_campaign_onboarding_cron_tombstone_remove_failed"
        );
      }
    }
    if (!sameJson(cronJobs(profileRoot), siblings)) {
      return failed(
        "hermes_first_campaign_onboarding_cron_tombstone_readback_rejected"
      );
    }
    return {
      ok: true,
      status: matches.length ? "REMOVED" : "REUSED",
      profileId,
      workspaceId,
      jobId: null,
      removedJobIds: matches.map((job) => job.id),
      name: HERMES_FIRST_CAMPAIGN_ONBOARDING_CRON.name,
      tombstoned: true,
    };
  } catch {
    return failed("hermes_first_campaign_onboarding_cron_retire_failed");
  }
}

export async function ensureHermesCustomerProgramCrons({
  profileId,
  profileRoot,
  hermesRoot,
  workspaceId,
  channelId,
  timezone = "UTC",
  apiUrl,
  authorityPath,
  dueScriptTemplatePath = HERMES_CUSTOMER_PROGRAM_DUE_TEMPLATE_PATH,
  hermesPython = HERMES_PYTHON,
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot) ||
    !ONBOARDING_WORKSPACE_ID.test(workspaceId ?? "") ||
    !ONBOARDING_CHANNEL_ID.test(channelId ?? "") ||
    typeof timezone !== "string" ||
    !timezone.trim() ||
    /[\r\n\0]/.test(timezone) ||
    typeof apiUrl !== "string" ||
    !/^https?:\/\/[^\s/]+\/?$/.test(apiUrl) ||
    typeof authorityPath !== "string" ||
    !authorityPath.startsWith("/") ||
    /[\r\n\0]/.test(authorityPath)
  ) {
    return failed("hermes_customer_program_crons_identity_rejected");
  }
  try {
    new Intl.DateTimeFormat("en", { timeZone: timezone }).format(new Date(0));
    const template = regularFile(dueScriptTemplatePath);
    if ((template.mode & 0o111) === 0) {
      return failed("hermes_customer_program_crons_script_rejected");
    }
    const templateBytes = readFileSync(dueScriptTemplatePath);
    for (const definition of HERMES_CUSTOMER_PROGRAM_CRONS) {
      atomicWrite(
        join(profileRoot, "scripts", definition.script),
        templateBytes,
        0o700
      );
    }
    const stateRoot = join(profileRoot, ".sellable-agent", "customer-program");
    mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
    chmodSync(stateRoot, 0o700);
    atomicWrite(
      join(stateRoot, "review-schedule.json"),
      `${JSON.stringify({
        schemaVersion: "sellable-customer-program-schedule/v2",
        workspaceId,
        apiUrl: apiUrl.replace(/\/$/, ""),
        authorityPath,
        jobKeys: HERMES_CUSTOMER_PROGRAM_CRONS.map(
          (definition) => definition.jobKey
        ),
      })}\n`,
      0o600
    );
    const deliver = `slack:${channelId}`;
    const jobs = [];
    for (const definition of HERMES_CUSTOMER_PROGRAM_CRONS) {
      const prompt = [...definition.prompt, `workspaceId: ${workspaceId}`].join(
        "\n"
      );
      const expected = (job) =>
        Boolean(
          job?.id &&
            job.name === definition.name &&
            job.prompt === prompt &&
            job.script === definition.script &&
            job.no_agent === false &&
            job.enabled === true &&
            job.deliver === deliver &&
            sameJson(job.skills, definition.skills) &&
            sameJson(job.enabled_toolsets, ["safe", "sellable", "mcp"]) &&
            job.schedule?.kind === "cron" &&
            job.schedule?.expr === definition.schedule
        );
      const mutate = (jobId = null) => {
        const spec = {
          prompt,
          schedule: definition.schedule,
          name: definition.name,
          deliver,
          script: definition.script,
          skills: [...definition.skills],
          enabled_toolsets: ["safe", "sellable", "mcp"],
          no_agent: false,
          enabled: true,
          ...(jobId ? { jobId } : {}),
        };
        return childRunner(runChild)({
          command: hermesPython,
          args: ["-c", OPPORTUNITY_SEQUENCE_CRON_PYTHON, JSON.stringify(spec)],
          env: {
            ...process.env,
            HOME: hermesRoot,
            HERMES_HOME: profileRoot,
            HERMES_PROFILE: profileId,
            NO_COLOR: "1",
          },
          shell: false,
          timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
          maxOutputBytes: 64 * 1024,
        });
      };
      const result = await ensureHermesNamedCron({
        profileRoot,
        name: definition.name,
        isExpected: expected,
        create: () => mutate(),
        update: (job) => mutate(job.id),
        codePrefix: "hermes_customer_program_cron",
      });
      if (!result.ok) return result;
      jobs.push({
        jobKey: definition.jobKey,
        status: result.status,
        jobId: result.jobId,
        name: result.job.name,
        schedule: result.job.schedule,
        deliver: result.job.deliver,
        script: result.job.script,
        skills: result.job.skills,
      });
    }
    return {
      ok: true,
      status: jobs.every((job) => job.status === "REUSED")
        ? "REUSED"
        : "RECONCILED",
      jobs,
      profileId,
      workspaceId,
      timezone,
    };
  } catch {
    return failed("hermes_customer_program_crons_install_failed");
  }
}

export async function ensureHermesCampaignDailyReviewCron(input) {
  const result = await ensureHermesCustomerProgramCrons(input);
  if (!result.ok) return result;
  const daily = result.jobs.find(
    (job) => job.jobKey === "campaign_daily_review"
  );
  return { ...result, ...daily };
}

function managedCronTombstones(profileRoot) {
  const path = join(profileRoot, HERMES_MANAGED_CRON_TOMBSTONES.relativePath);
  const builtIn = new Set(HERMES_MANAGED_CRON_TOMBSTONES.names);
  if (!existsSync(path)) return { path, names: builtIn };
  const stat = regularFile(path);
  if ((stat.mode & 0o077) !== 0) {
    throw new Error("hermes_managed_cron_tombstone_mode_rejected");
  }
  const value = JSON.parse(readFileSync(path, "utf8"));
  if (
    !value ||
    typeof value !== "object" ||
    Array.isArray(value) ||
    Object.keys(value).sort().join(",") !== "names,schemaVersion" ||
    value.schemaVersion !== HERMES_MANAGED_CRON_TOMBSTONES.schemaVersion ||
    !Array.isArray(value.names) ||
    value.names.some((name) => typeof name !== "string" || !ID.test(name)) ||
    new Set(value.names).size !== value.names.length
  ) {
    throw new Error("hermes_managed_cron_tombstone_rejected");
  }
  return { path, names: new Set([...builtIn, ...value.names]) };
}

function opportunitySequenceIdentity(profileRoot) {
  const root = join(
    profileRoot,
    "skills",
    HERMES_OPPORTUNITY_SEQUENCES_SKILL_GROUP,
    HERMES_OPPORTUNITY_SEQUENCES_SKILL
  );
  return Object.fromEntries(
    OPPORTUNITY_SEQUENCE_ASSETS.map((relative) => {
      const path = join(root, relative);
      regularFile(path);
      return [relative, sha256(readFileSync(path))];
    })
  );
}

// Weekdays between 14:00 and 14:49 UTC, one stable minute per agent: every agent
// starting refill at 14:00:00 crowded the product's database pool (Sep 30).
export function refillSendsSchedule(profileId) {
  const minute = parseInt(sha256(profileId).slice(0, 8), 16) % 50;
  return `${minute} 14 * * 1-5`;
}

function refillSendsPrompt(workspaceId) {
  return [
    "Run the installed refill-sends skill with --yolo for this workspace.",
    "Call refill_v3_advance with yolo true and scope {kind: sender_local_horizon, days: 2}, then continue exactly as the skill says until the run is complete, awaiting an external change, or blocked.",
    "Post nothing: reply [SILENT] whatever the outcome. The daily review names anything only the customer can fix, and the next results update reports what was added.",
    `workspaceId: ${workspaceId}`,
  ].join("\n");
}

export async function ensureHermesRefillSendsCron({
  profileId,
  profileRoot,
  hermesRoot,
  workspaceId,
  hermesPython = HERMES_PYTHON,
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot) ||
    !ONBOARDING_WORKSPACE_ID.test(workspaceId ?? "")
  ) {
    return failed("hermes_refill_sends_cron_identity_rejected");
  }
  try {
    const cron = HERMES_REFILL_SENDS_CRON;
    const prompt = refillSendsPrompt(workspaceId);
    const schedule = refillSendsSchedule(profileId);
    const expected = (job) =>
      Boolean(
        job?.id &&
          job.name === cron.name &&
          job.prompt === prompt &&
          job.no_agent === false &&
          job.enabled === true &&
          job.deliver === cron.deliver &&
          sameJson(job.skills, cron.skills) &&
          sameJson(job.enabled_toolsets, cron.enabledToolsets) &&
          job.schedule?.kind === "cron" &&
          job.schedule?.expr === schedule
      );
    const mutate = (jobId = null) =>
      childRunner(runChild)({
        command: hermesPython,
        args: [
          "-c",
          OPPORTUNITY_SEQUENCE_CRON_PYTHON,
          JSON.stringify({
            prompt,
            schedule,
            name: cron.name,
            deliver: cron.deliver,
            skills: [...cron.skills],
            enabled_toolsets: [...cron.enabledToolsets],
            no_agent: false,
            enabled: true,
            ...(jobId ? { jobId } : {}),
          }),
        ],
        env: {
          ...process.env,
          HOME: hermesRoot,
          HERMES_HOME: profileRoot,
          HERMES_PROFILE: profileId,
          NO_COLOR: "1",
        },
        shell: false,
        timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
        maxOutputBytes: 64 * 1024,
      });
    const result = await ensureHermesNamedCron({
      profileRoot,
      name: cron.name,
      isExpected: expected,
      create: () => mutate(),
      update: (job) => mutate(job.id),
      codePrefix: "hermes_refill_sends_cron",
    });
    if (!result.ok) return result;
    return {
      ok: true,
      status: result.status,
      jobId: result.jobId,
      name: result.job.name,
      schedule: result.job.schedule,
      deliver: result.job.deliver,
      skills: result.job.skills,
      profileId,
      workspaceId,
    };
  } catch {
    return failed("hermes_refill_sends_cron_install_failed");
  }
}

function opportunitySequencePrompt({
  notificationSlackUserId,
  journalRoot,
  hashes,
}) {
  return [
    "Run the installed opportunity-sequences skill for one bounded page.",
    `Use the private journal at ${journalRoot}.`,
    `Require these installed asset hashes: ${JSON.stringify(hashes)}.`,
    `End with a compact native Slack summary addressed to <@${notificationSlackUserId}>.`,
    "Never send prospect outreach; never claim exactly-once delivery; never use a direct Slack or Lightfield transport outside the installed skill and Product MCP.",
  ].join(" ");
}

export async function ensureHermesOpportunitySequencesCron({
  profileId,
  profileRoot,
  hermesRoot,
  statePath,
  journalRoot = HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT,
  hermesPython = HERMES_PYTHON,
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot) ||
    !isAbsolute(statePath) ||
    !isAbsolute(journalRoot)
  ) {
    return failed("hermes_opportunity_sequences_cron_identity_rejected");
  }
  try {
    const tombstones = managedCronTombstones(profileRoot);
    if (tombstones.names.has(HERMES_OPPORTUNITY_SEQUENCES_CRON.name)) {
      const before = cronJobs(profileRoot);
      const matches = before.filter(
        (job) =>
          job?.name === HERMES_OPPORTUNITY_SEQUENCES_CRON.name ||
          job?.skill === HERMES_OPPORTUNITY_SEQUENCES_SKILL ||
          job?.skills?.includes?.(HERMES_OPPORTUNITY_SEQUENCES_SKILL)
      );
      if (matches.some((job) => !ID.test(job?.id ?? ""))) {
        return failed(
          "hermes_opportunity_sequences_cron_tombstone_inventory_rejected"
        );
      }
      const siblings = before.filter(
        (job) => !matches.some((match) => match.id === job?.id)
      );
      for (const job of matches) {
        const mutation = await childRunner(runChild)({
          command: hermesPython,
          args: [
            "-c",
            OPPORTUNITY_SEQUENCE_CRON_REMOVE_PYTHON,
            JSON.stringify({ removeJobId: job.id }),
          ],
          env: {
            ...process.env,
            HOME: hermesRoot,
            HERMES_HOME: profileRoot,
            HERMES_PROFILE: profileId,
            NO_COLOR: "1",
          },
          shell: false,
          timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
          maxOutputBytes: 64 * 1024,
        });
        if (childFailed(mutation)) {
          return failed(
            "hermes_opportunity_sequences_cron_tombstone_remove_failed"
          );
        }
      }
      if (!sameJson(cronJobs(profileRoot), siblings)) {
        return failed(
          "hermes_opportunity_sequences_cron_tombstone_readback_rejected"
        );
      }
      return {
        ok: true,
        status: matches.length ? "REMOVED" : "REUSED",
        jobId: null,
        removedJobIds: matches.map((job) => job.id),
        name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
        profileId,
        tombstonePath: tombstones.path,
      };
    }
    const stateStat = regularFile(statePath);
    if ((stateStat.mode & 0o077) !== 0) {
      return failed("hermes_opportunity_sequences_cron_state_mode_rejected");
    }
    const state = JSON.parse(readFileSync(statePath, "utf8"));
    const notificationSlackUserId =
      state?.opportunitySequences?.notificationSlackUserId;
    if (!SLACK_USER_ID.test(notificationSlackUserId ?? "")) {
      return failed("hermes_opportunity_sequences_cron_notification_rejected");
    }
    mkdirSync(journalRoot, { recursive: true, mode: 0o700 });
    chmodSync(journalRoot, 0o700);
    const journalStat = lstatSync(journalRoot);
    if (
      journalStat.isSymbolicLink() ||
      !journalStat.isDirectory() ||
      (journalStat.mode & 0o077) !== 0
    ) {
      return failed("hermes_opportunity_sequences_cron_journal_rejected");
    }
    const hashes = opportunitySequenceIdentity(profileRoot);
    const prompt = opportunitySequencePrompt({
      notificationSlackUserId,
      journalRoot,
      hashes,
    });
    const expected = (job) =>
      Boolean(
        job?.id &&
          job.name === HERMES_OPPORTUNITY_SEQUENCES_CRON.name &&
          job.prompt === prompt &&
          job.no_agent === false &&
          job.enabled === true &&
          job.deliver === HERMES_OPPORTUNITY_SEQUENCES_CRON.deliver &&
          sameJson(job.skills, HERMES_OPPORTUNITY_SEQUENCES_CRON.skills) &&
          sameJson(
            job.enabled_toolsets,
            HERMES_OPPORTUNITY_SEQUENCES_CRON.enabledToolsets
          ) &&
          job.schedule?.kind === "interval" &&
          job.schedule?.minutes === 30
      );
    const mutate = (jobId = null) => {
      const spec = {
        prompt,
        schedule: HERMES_OPPORTUNITY_SEQUENCES_CRON.schedule,
        name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
        deliver: HERMES_OPPORTUNITY_SEQUENCES_CRON.deliver,
        skills: [...HERMES_OPPORTUNITY_SEQUENCES_CRON.skills],
        enabled_toolsets: [
          ...HERMES_OPPORTUNITY_SEQUENCES_CRON.enabledToolsets,
        ],
        no_agent: false,
        enabled: true,
        ...(jobId ? { jobId } : {}),
      };
      return childRunner(runChild)({
        command: hermesPython,
        args: ["-c", OPPORTUNITY_SEQUENCE_CRON_PYTHON, JSON.stringify(spec)],
        env: {
          ...process.env,
          HOME: hermesRoot,
          HERMES_HOME: profileRoot,
          HERMES_PROFILE: profileId,
          NO_COLOR: "1",
        },
        shell: false,
        timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
        maxOutputBytes: 64 * 1024,
      });
    };
    const result = await ensureHermesNamedCron({
      profileRoot,
      name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
      isExpected: expected,
      create: () => mutate(),
      update: (job) => mutate(job.id),
      codePrefix: "hermes_opportunity_sequences_cron",
    });
    if (!result.ok) return result;
    const jobsMode =
      lstatSync(join(profileRoot, "cron", "jobs.json")).mode & 0o777;
    if ((jobsMode & 0o077) !== 0) {
      return failed("hermes_opportunity_sequences_cron_jobs_mode_rejected");
    }
    return {
      ok: true,
      status: result.status,
      jobId: result.jobId,
      name: result.job.name,
      prompt: result.job.prompt,
      schedule: result.job.schedule,
      profileId,
      enabledToolsets: result.job.enabled_toolsets,
      skills: result.job.skills,
      assetHashes: hashes,
      notificationSlackUserIdSource:
        "config-state.json:opportunitySequences.notificationSlackUserId",
      journalRoot,
      modes: {
        state: stateStat.mode & 0o777,
        journal: journalStat.mode & 0o777,
        jobs: jobsMode,
      },
    };
  } catch {
    return failed("hermes_opportunity_sequences_cron_install_failed");
  }
}

/**
 * The mirror's client slug and its SHARED Slack channel id. A client that fails
 * either check is skipped and reported rather than failing the whole Admin
 * runtime: one bad channel id must not cost every other client its digest.
 */
const CLIENT_DIGEST_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
const CLIENT_SHARED_CHANNEL = /^[CG][A-Z0-9]{8,}$/;

// `cron.jobs.remove_job(job_id)` is a real delete returning bool, so a stale
// digest job is removed rather than disabled.
const CLIENT_RELATIONSHIPS_PRUNE_PYTHON = [
  "import json,sys",
  "from cron.jobs import remove_job",
  "value=json.loads(sys.argv[1])",
  "print(json.dumps({'removed': bool(remove_job(value['removeJobId']))}))",
].join("\n");

function clientRelationshipsIdentity(profileRoot) {
  const root = join(
    profileRoot,
    "skills",
    HERMES_OPPORTUNITY_SEQUENCES_SKILL_GROUP,
    HERMES_CLIENT_RELATIONSHIPS_SKILL
  );
  return Object.fromEntries(
    CLIENT_RELATIONSHIPS_ASSETS.map((relative) => {
      const path = join(root, relative);
      regularFile(path);
      return [relative, sha256(readFileSync(path))];
    })
  );
}

function clientDigestPrompt({ slug, workspaceId, hashes }) {
  return [
    `Run the installed client-relationships skill in digest mode for client ${slug} (workspace ${workspaceId ?? "unbound"}) for today's business date (America/New_York).`,
    "Reply [SILENT] when the skill says nothing should be posted.",
    `Require these installed asset hashes: ${JSON.stringify(hashes)}.`,
    "Never send prospect outreach; never use any Slack transport - the gateway delivers your reply.",
  ].join(" ");
}

function committedDealChasePrompt({ shermeenSlackUserId, hashes }) {
  return [
    "Run the installed client-relationships skill in chase mode over the committed-deal book and today's digest job outcomes.",
    "Reply [SILENT] when nothing is due and nothing failed.",
    `Require these installed asset hashes: ${JSON.stringify(hashes)}.`,
    `End with a compact native Slack summary addressed to <@${shermeenSlackUserId}>.`,
    "Never send prospect outreach; never record a cron run as a touch; never use any Slack transport - the gateway delivers your reply.",
  ].join(" ");
}

/**
 * One named agent cron per active client plus one internal chase cron, each
 * materialized through the sibling-preserving `ensureHermesNamedCron`.
 *
 * CONTAINMENT RULING - do not reintroduce a CLI send. `hermes send` MUST NOT be
 * used from the agent's terminal. In the Sellable runtime the installer keeps
 * `SLACK_BOT_TOKEN` out of the process environment (hermes-bridge.mjs RAISES
 * when it is present under `SELLABLE_AGENT_RUNTIME=1`), delivers it to the
 * gateway over file descriptor 9 which no CLI child inherits, gates the legacy
 * `slack_tokens.json` read behind `SELLABLE_AGENT_RUNTIME != "1"`, and writes
 * the Admin token to `profile-slack.env` rather than `$HERMES_HOME/.env`. That
 * is a security boundary, not an oversight. Gateway-owned cron delivery needs
 * no token in the agent's reach, which is exactly why each job carries its own
 * `deliver:` target instead.
 */
export async function ensureHermesClientRelationshipsCrons({
  profileId,
  profileRoot,
  hermesRoot,
  statePath,
  hermesPython = HERMES_PYTHON,
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot) ||
    !isAbsolute(statePath)
  ) {
    return failed("hermes_client_relationships_cron_identity_rejected");
  }
  try {
    const stateStat = regularFile(statePath);
    if ((stateStat.mode & 0o077) !== 0) {
      return failed("hermes_client_relationships_cron_state_mode_rejected");
    }
    const state = JSON.parse(readFileSync(statePath, "utf8"));
    // Fail closed before any mutation: the chase summary addresses a private
    // configured id that is never hard-coded and never resolved from a name.
    const shermeenSlackUserId = state?.clientRelationships?.shermeenSlackUserId;
    if (!SLACK_USER_ID.test(shermeenSlackUserId ?? "")) {
      return failed("hermes_client_relationships_cron_notification_rejected");
    }
    const hashes = clientRelationshipsIdentity(profileRoot);
    const clients =
      state?.clients && typeof state.clients === "object" ? state.clients : {};
    const desired = [];
    const skipped = [];
    for (const slug of Object.keys(clients).sort()) {
      const entry = clients[slug];
      if (String(entry?.status ?? "").toLowerCase() !== "active") continue;
      if (!CLIENT_DIGEST_SLUG.test(slug)) {
        skipped.push({ slug, reason: "slug_rejected" });
        continue;
      }
      const shared = entry?.channels?.shared;
      if (!CLIENT_SHARED_CHANNEL.test(shared ?? "")) {
        skipped.push({ slug, reason: "shared_channel_rejected" });
        continue;
      }
      desired.push({
        name: `${HERMES_CLIENT_DIGEST_CRON_PREFIX}${slug}`,
        deliver: `slack:${shared}`,
        schedule: HERMES_CLIENT_RELATIONSHIPS_CRONS.digestSchedule,
        prompt: clientDigestPrompt({
          slug,
          workspaceId: entry?.workspaceId,
          hashes,
        }),
      });
    }
    desired.push({
      name: HERMES_CLIENT_RELATIONSHIPS_CRONS.chaseName,
      deliver: HERMES_CLIENT_RELATIONSHIPS_CRONS.deliver,
      schedule: HERMES_CLIENT_RELATIONSHIPS_CRONS.chaseSchedule,
      prompt: committedDealChasePrompt({ shermeenSlackUserId, hashes }),
    });

    const childEnv = {
      ...process.env,
      HOME: hermesRoot,
      HERMES_HOME: profileRoot,
      HERMES_PROFILE: profileId,
      NO_COLOR: "1",
    };
    const runNative = (program, payload) =>
      childRunner(runChild)({
        command: hermesPython,
        args: ["-c", program, JSON.stringify(payload)],
        env: childEnv,
        shell: false,
        timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
        maxOutputBytes: 64 * 1024,
      });

    // PRUNE FIRST. `ensureHermesNamedCron` proves its work by diffing the
    // sibling set around its own mutation, so a prune interleaved with the
    // creates would make one call's siblings include a job another call is
    // about to delete and report this function's own pruning as drift.
    const desiredNames = new Set(desired.map((job) => job.name));
    const removed = [];
    for (const job of cronJobs(profileRoot)) {
      if (
        typeof job?.name !== "string" ||
        !job.name.startsWith(HERMES_CLIENT_DIGEST_CRON_PREFIX) ||
        desiredNames.has(job.name)
      ) {
        continue;
      }
      const outcome = await runNative(CLIENT_RELATIONSHIPS_PRUNE_PYTHON, {
        removeJobId: job.id,
      });
      if (childFailed(outcome)) {
        return failed("hermes_client_relationships_cron_prune_failed");
      }
      if (cronJobs(profileRoot).some((after) => after?.id === job.id)) {
        return failed(
          "hermes_client_relationships_cron_prune_readback_rejected"
        );
      }
      removed.push({ name: job.name, jobId: job.id, status: "REMOVED" });
    }

    const jobs = [];
    for (const spec of desired) {
      const isExpected = (job) =>
        Boolean(
          job?.id &&
            job.name === spec.name &&
            job.prompt === spec.prompt &&
            job.no_agent === false &&
            job.enabled === true &&
            job.deliver === spec.deliver &&
            sameJson(job.skills, HERMES_CLIENT_RELATIONSHIPS_CRONS.skills) &&
            sameJson(
              job.enabled_toolsets,
              HERMES_CLIENT_RELATIONSHIPS_CRONS.enabledToolsets
            ) &&
            job.schedule?.kind === "cron" &&
            job.schedule?.expr === spec.schedule
        );
      const mutate = (jobId = null) =>
        runNative(OPPORTUNITY_SEQUENCE_CRON_PYTHON, {
          prompt: spec.prompt,
          schedule: spec.schedule,
          name: spec.name,
          deliver: spec.deliver,
          skills: [...HERMES_CLIENT_RELATIONSHIPS_CRONS.skills],
          enabled_toolsets: [
            ...HERMES_CLIENT_RELATIONSHIPS_CRONS.enabledToolsets,
          ],
          no_agent: false,
          enabled: true,
          ...(jobId ? { jobId } : {}),
        });
      const result = await ensureHermesNamedCron({
        profileRoot,
        name: spec.name,
        isExpected,
        create: () => mutate(),
        update: (job) => mutate(job.id),
        codePrefix: "hermes_client_relationships_cron",
      });
      if (!result.ok) return result;
      jobs.push({
        name: spec.name,
        status: result.status,
        jobId: result.jobId,
        deliver: result.job.deliver,
        prompt: result.job.prompt,
        schedule: result.job.schedule,
        skills: result.job.skills,
        enabledToolsets: result.job.enabled_toolsets,
      });
    }

    const jobsMode =
      lstatSync(join(profileRoot, "cron", "jobs.json")).mode & 0o777;
    if ((jobsMode & 0o077) !== 0) {
      return failed("hermes_client_relationships_cron_jobs_mode_rejected");
    }
    return {
      ok: true,
      status:
        removed.length === 0 && jobs.every((job) => job.status === "REUSED")
          ? "REUSED"
          : "CHANGED",
      profileId,
      jobs,
      removed,
      skipped,
      assetHashes: hashes,
      shermeenSlackUserIdSource:
        "config-state.json:clientRelationships.shermeenSlackUserId",
      modes: { state: stateStat.mode & 0o777, jobs: jobsMode },
    };
  } catch {
    return failed("hermes_client_relationships_cron_install_failed");
  }
}

export async function captureHermesBackup({
  profileId,
  profileRoot,
  hermesRoot,
  hermesVersion,
  archiveDirectory = "/tmp",
  hermesCli = HERMES,
  maxArchiveBytes = HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES,
  runChild,
  operationIdentity = /** @type {any} */ (undefined),
}) {
  const operationIdentityAccepted =
    operationIdentity === undefined ||
    (operationIdentity &&
      typeof operationIdentity === "object" &&
      !Array.isArray(operationIdentity) &&
      Object.keys(operationIdentity).sort().join(",") ===
        "authorityGeneration,operationId,profileSchemaVersion,runtimeArtifactId,sourceBootSessionId,sourceMachineId,sourceMachineVersion" &&
      BOUNDED_ID.test(operationIdentity.operationId ?? "") &&
      Number.isSafeInteger(operationIdentity.authorityGeneration) &&
      operationIdentity.authorityGeneration > 0 &&
      BOUNDED_ID.test(operationIdentity.runtimeArtifactId ?? "") &&
      BOUNDED_ID.test(operationIdentity.profileSchemaVersion ?? "") &&
      [
        operationIdentity.sourceMachineId,
        operationIdentity.sourceMachineVersion,
        operationIdentity.sourceBootSessionId,
      ].every((value) => value === null || BOUNDED_ID.test(value ?? "")));
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    !existsSync(profileRoot) ||
    hermesVersion !== VERSION ||
    !isAbsolute(archiveDirectory) ||
    !operationIdentityAccepted
  ) {
    return failed("hermes_snapshot_capture_identity_rejected");
  }
  mkdirSync(archiveDirectory, { recursive: true, mode: 0o700 });
  const archivePath = join(
    archiveDirectory,
    `.hermes-backup-${randomUUID()}.zip`
  );
  let stagingRoot;
  try {
    stagingRoot = stageHermesUserState({ profileId, profileRoot, hermesRoot });
    const stagedProfileRoot = join(stagingRoot, "profiles", profileId);
    assertStagedCronDependencies(stagedProfileRoot);
    const result = await childRunner(runChild)(
      invocation(["backup", "-o", archivePath], stagingRoot, hermesCli)
    );
    if (
      !nativeSummaryAccepted(
        result,
        `Backup complete: ${archivePath}`,
        "Backup incomplete:"
      ) ||
      !existsSync(archivePath)
    ) {
      return failed("hermes_snapshot_backup_failed");
    }
    const stat = regularFile(archivePath);
    if (stat.size < 1 || stat.size > maxArchiveBytes) {
      return failed("hermes_snapshot_archive_bound_rejected");
    }
    const bytes = readFileSync(archivePath);
    if (bytes.byteLength !== stat.size) {
      return failed("hermes_snapshot_archive_readback_rejected");
    }
    return {
      ok: true,
      status: "CAPTURED",
      archiveDigest: sha256(bytes),
      archiveBytes: bytes.byteLength,
      hermesVersion,
      sourceProfileId: profileId,
      ...(operationIdentity ?? {}),
      bytes,
    };
  } catch (error) {
    const code = error instanceof Error ? error.message : "";
    return failed(
      /^hermes_snapshot_(?:external_state|plugin_data|portable_config|dependency|soul_metadata)/.test(
        code
      )
        ? code
        : "hermes_snapshot_capture_failed"
    );
  } finally {
    rmSync(archivePath, { force: true });
    if (stagingRoot) rmSync(stagingRoot, { recursive: true, force: true });
  }
}

function requeueClaim(marker, claim) {
  if (!existsSync(claim)) return;
  if (!existsSync(marker)) renameSync(claim, marker);
  else rmSync(claim, { force: true });
}

function snapshotStatePaths(profileRoot, hermesRoot, profileId) {
  const memoryRoot = join(profileRoot, ".sellable-agent", "memory");
  return {
    claimPrefix: `.snapshot-${profileId}-`,
    claimRoot: join(hermesRoot, "backups"),
    marker: join(memoryRoot, "dirty.json"),
    memoryRoot,
    retry: join(memoryRoot, "retry.json"),
  };
}

function readRetryState(path) {
  if (!existsSync(path)) return null;
  regularFile(path);
  const value = JSON.parse(readFileSync(path, "utf8"));
  if (
    value?.schemaVersion !== RETRY_STATE_SCHEMA ||
    !Number.isSafeInteger(value.attempt) ||
    value.attempt < 1 ||
    typeof value.code !== "string" ||
    !value.code.startsWith("hermes_snapshot_") ||
    !Number.isSafeInteger(value.retryAt) ||
    value.retryAt < 0
  ) {
    throw new Error("hermes_snapshot_retry_state_rejected");
  }
  return value;
}

function recordRetryState(paths, now, code) {
  const previous = readRetryState(paths.retry);
  const attempt = Math.min(
    (previous?.attempt ?? 0) + 1,
    HERMES_SNAPSHOT_RETRY_BACKOFF_MS.length
  );
  const retryAt =
    now + HERMES_SNAPSHOT_RETRY_BACKOFF_MS[Math.max(0, attempt - 1)];
  atomicWrite(
    paths.retry,
    `${JSON.stringify({
      schemaVersion: RETRY_STATE_SCHEMA,
      attempt,
      code,
      retryAt,
    })}\n`,
    0o600
  );
  return retryAt;
}

function clearRetryState(paths) {
  if (!existsSync(paths.retry)) return;
  rmSync(paths.retry, { force: true });
  fsyncDirectory(paths.memoryRoot);
}

function recoverSnapshotClaims(paths) {
  if (!existsSync(paths.claimRoot)) return 0;
  const claims = readdirSync(paths.claimRoot)
    .filter((name) => name.startsWith(paths.claimPrefix))
    .sort();
  if (claims.length === 0) return 0;
  mkdirSync(paths.memoryRoot, { recursive: true, mode: 0o700 });
  let retained = existsSync(paths.marker);
  for (const name of claims) {
    const claim = join(paths.claimRoot, name);
    regularFile(claim);
    if (!retained) {
      renameSync(claim, paths.marker);
      retained = true;
    } else {
      rmSync(claim, { force: true });
    }
  }
  fsyncDirectory(paths.claimRoot);
  fsyncDirectory(paths.memoryRoot);
  return claims.length;
}

async function reconcileHermesSnapshotOnce({
  profileId,
  profileRoot,
  hermesRoot,
  hermesVersion,
  upload,
  now = Date.now(),
  debounceMs = 2_000,
  hermesCli = HERMES,
  runChild,
  operationIdentity = /** @type {any} */ (undefined),
}) {
  if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
    return failed("hermes_snapshot_reconcile_identity_rejected");
  }
  const paths = snapshotStatePaths(profileRoot, hermesRoot, profileId);
  mkdirSync(paths.claimRoot, { recursive: true, mode: 0o700 });
  recoverSnapshotClaims(paths);
  if (!existsSync(paths.marker)) return { ok: true, status: "CLEAN" };
  const retry = readRetryState(paths.retry);
  if (!operationIdentity && retry && now < retry.retryAt) {
    return { ok: true, status: "BACKOFF", retryAt: retry.retryAt };
  }
  const marked = regularFile(paths.marker);
  if (debounceMs > 0 && now - marked.mtimeMs < debounceMs) {
    return { ok: true, status: "DEBOUNCED" };
  }
  const claim = join(paths.claimRoot, `${paths.claimPrefix}${randomUUID()}`);
  renameSync(paths.marker, claim);
  try {
    const captured = await captureHermesBackup({
      profileId,
      profileRoot,
      hermesRoot,
      hermesVersion,
      hermesCli,
      runChild,
      operationIdentity,
    });
    if (!captured.ok) {
      requeueClaim(paths.marker, claim);
      const retryAt = recordRetryState(paths, now, captured.code);
      return { ...captured, retryAt };
    }
    await upload(captured);
    rmSync(claim, { force: true });
    clearRetryState(paths);
    return { ...captured, bytes: undefined, status: "UPLOADED" };
  } catch (error) {
    requeueClaim(paths.marker, claim);
    const code =
      /^hermes_snapshot_upload_failed_http_[1-5][0-9]{2}_(auth|bucket|mime|other|policy|size|token|upsert)$/.test(
        error?.message ?? ""
      )
        ? error.message
        : "hermes_snapshot_upload_failed";
    const retryAt = recordRetryState(paths, now, code);
    return { ...failed(code), retryAt };
  }
}

export async function reconcileHermesSnapshot({
  profileId,
  profileRoot,
  hermesRoot,
  hermesVersion,
  upload,
  now = Date.now(),
  debounceMs = 2_000,
  hermesCli = HERMES,
  runChild,
  operationIdentity = /** @type {any} */ (undefined),
}) {
  const key = `${resolve(hermesRoot)}\0${profileId}\0${
    operationIdentity?.operationId ?? "background"
  }`;
  const active = snapshotReconciles.get(key);
  if (active) return active;
  const operation = reconcileHermesSnapshotOnce({
    profileId,
    profileRoot,
    hermesRoot,
    hermesVersion,
    upload,
    now,
    debounceMs,
    hermesCli,
    runChild,
    operationIdentity,
  }).finally(() => snapshotReconciles.delete(key));
  snapshotReconciles.set(key, operation);
  return operation;
}

function validLatest(latest, hermesVersion) {
  return (
    Buffer.isBuffer(latest?.bytes) &&
    latest.bytes.byteLength > 0 &&
    latest.bytes.byteLength <= HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES &&
    latest.archiveBytes === latest.bytes.byteLength &&
    SHA256.test(latest.archiveDigest ?? "") &&
    sha256(latest.bytes) === latest.archiveDigest &&
    latest.hermesVersion === hermesVersion &&
    ID.test(latest.sourceProfileId ?? "")
  );
}

function restoreStatePaths({ profileId, profileRoot, hermesRoot }) {
  const backupsRoot = join(hermesRoot, "backups");
  const preserved = join(backupsRoot, `.restore-state-${profileId}`);
  return {
    active: join(hermesRoot, ".sellable-agent"),
    backupsRoot,
    manifest: join(preserved, "state.json"),
    preserved,
    preservedServerState: join(preserved, "server-state"),
    preservedSoul: join(preserved, "SOUL.md"),
    soulPath: join(profileRoot, "SOUL.md"),
  };
}

function validRestoreStateManifest(value, profileId) {
  return Boolean(
    value &&
      value.schemaVersion === RESTORE_STATE_SCHEMA &&
      value.profileId === profileId &&
      ["PREPARING", "READY"].includes(value.phase) &&
      typeof value.originalSoul === "boolean" &&
      Array.isArray(value.serverStateNames) &&
      value.serverStateNames.every(
        (name) =>
          typeof name === "string" &&
          name.length > 0 &&
          name !== SOUL_LOCK_ROOT &&
          basename(name) === name &&
          ![".", ".."].includes(name)
      ) &&
      new Set(value.serverStateNames).size === value.serverStateNames.length
  );
}

function readRestoreStateManifest(paths, profileId) {
  const stat = lstatSync(paths.manifest);
  if (stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o022) !== 0) {
    throw new Error("hermes_snapshot_restore_state_rejected");
  }
  const value = JSON.parse(readFileSync(paths.manifest, "utf8"));
  if (!validRestoreStateManifest(value, profileId)) {
    throw new Error("hermes_snapshot_restore_state_rejected");
  }
  return value;
}

function moveDurably(source, target) {
  renameSync(source, target);
  fsyncDirectory(dirname(source));
  if (dirname(target) !== dirname(source)) fsyncDirectory(dirname(target));
}

function preserveServerState({ profileId, profileRoot, hermesRoot }) {
  const paths = restoreStatePaths({ profileId, profileRoot, hermesRoot });
  if (existsSync(paths.preserved)) {
    throw new Error("hermes_snapshot_restore_state_pending");
  }
  mkdirSync(paths.backupsRoot, { recursive: true, mode: 0o700 });
  mkdirSync(paths.preserved, { mode: 0o700 });
  mkdirSync(paths.preservedServerState, { mode: 0o700 });
  fsyncDirectory(paths.backupsRoot);
  const originalSoul = existsSync(paths.soulPath);
  if (originalSoul) regularFile(paths.soulPath);
  if (existsSync(paths.active)) {
    const active = lstatSync(paths.active);
    if (active.isSymbolicLink() || !active.isDirectory()) {
      throw new Error("hermes_snapshot_server_state_rejected");
    }
  }
  const serverStateNames = (
    existsSync(paths.active)
      ? readdirSync(paths.active).filter((name) => name !== SOUL_LOCK_ROOT)
      : []
  ).sort();
  const manifest = {
    schemaVersion: RESTORE_STATE_SCHEMA,
    profileId,
    phase: "PREPARING",
    originalSoul,
    serverStateNames,
  };
  atomicWrite(paths.manifest, `${JSON.stringify(manifest)}\n`, 0o600);
  if (originalSoul) {
    moveDurably(paths.soulPath, paths.preservedSoul);
  }
  if (existsSync(paths.active)) {
    for (const name of serverStateNames) {
      moveDurably(
        join(paths.active, name),
        join(paths.preservedServerState, name)
      );
    }
  }
  atomicWrite(
    paths.manifest,
    `${JSON.stringify({ ...manifest, phase: "READY" })}\n`,
    0o600
  );
  return paths;
}

function restoreServerState({ profileId, profileRoot, hermesRoot }) {
  const paths = restoreStatePaths({ profileId, profileRoot, hermesRoot });
  if (!existsSync(paths.preserved)) return false;
  const manifest = readRestoreStateManifest(paths, profileId);
  const ready = manifest.phase === "READY";
  mkdirSync(paths.active, { recursive: true, mode: 0o700 });
  const preservedNames = readdirSync(paths.preservedServerState);
  for (const name of preservedNames) {
    if (name === SOUL_LOCK_ROOT || !manifest.serverStateNames.includes(name)) {
      throw new Error("hermes_snapshot_restore_state_rejected");
    }
  }
  if (ready) {
    for (const name of readdirSync(paths.active)) {
      if (
        name !== SOUL_LOCK_ROOT &&
        !manifest.serverStateNames.includes(name)
      ) {
        rmSync(join(paths.active, name), { recursive: true, force: true });
      }
    }
    fsyncDirectory(paths.active);
  }
  for (const name of manifest.serverStateNames) {
    const preserved = join(paths.preservedServerState, name);
    const target = join(paths.active, name);
    if (existsSync(preserved)) {
      if (existsSync(target)) {
        if (!ready) throw new Error("hermes_snapshot_restore_state_conflict");
        rmSync(target, { recursive: true, force: true });
      }
      moveDurably(preserved, target);
    } else if (!existsSync(target)) {
      throw new Error("hermes_snapshot_restore_state_rejected");
    }
  }

  if (manifest.originalSoul) {
    mkdirSync(profileRoot, { recursive: true, mode: 0o700 });
    if (existsSync(paths.preservedSoul)) {
      if (existsSync(paths.soulPath)) {
        if (!ready) throw new Error("hermes_snapshot_restore_state_conflict");
        rmSync(paths.soulPath, { recursive: true, force: true });
      }
      moveDurably(paths.preservedSoul, paths.soulPath);
    } else if (!existsSync(paths.soulPath)) {
      throw new Error("hermes_snapshot_restore_state_rejected");
    }
    regularFile(paths.soulPath);
  } else if (existsSync(paths.preservedSoul)) {
    throw new Error("hermes_snapshot_restore_state_rejected");
  }

  rmSync(paths.preserved, { recursive: true, force: true });
  fsyncDirectory(paths.backupsRoot);
  return true;
}

function recoverInterruptedRestoreState({
  profileId,
  profileRoot,
  hermesRoot,
  retryMarker,
}) {
  const paths = restoreStatePaths({ profileId, profileRoot, hermesRoot });
  if (!existsSync(paths.preserved)) return;
  if (!existsSync(retryMarker)) {
    throw new Error("hermes_snapshot_restore_state_orphaned");
  }
  restoreServerState({ profileId, profileRoot, hermesRoot });
}

async function restoreLatestHermesSnapshotLocked({
  profileId,
  profileRoot,
  hermesRoot,
  hermesVersion,
  fetchLatest,
  hermesCli = HERMES,
  runChild,
}) {
  if (
    !validProfilePath(profileId, profileRoot, hermesRoot) ||
    hermesVersion !== VERSION
  ) {
    return failed("hermes_snapshot_restore_identity_rejected");
  }
  const retryMarker = join(
    hermesRoot,
    "backups",
    `.restore-pending-${profileId}`
  );
  if (existsSync(profileRoot) && !existsSync(retryMarker)) {
    return { ok: true, status: "NONEMPTY_SKIP" };
  }
  const latest = await fetchLatest();
  if (!latest) {
    if (existsSync(retryMarker)) {
      rmSync(retryMarker, { force: true });
      fsyncDirectory(dirname(retryMarker));
    }
    return { ok: true, status: "NO_SNAPSHOT" };
  }
  if (!validLatest(latest, hermesVersion)) {
    return failed("hermes_snapshot_download_rejected");
  }
  mkdirSync(dirname(retryMarker), { recursive: true, mode: 0o700 });
  atomicWrite(
    retryMarker,
    '{"schemaVersion":"sellable-hermes-restore-pending/v1"}\n',
    0o600
  );
  const archivePath = join(
    dirname(retryMarker),
    `.restore-${randomUUID()}.zip`
  );
  const restoreState = { profileId, profileRoot, hermesRoot };
  let serverStatePending = false;
  try {
    atomicWrite(archivePath, latest.bytes, 0o600);
    preserveServerState(restoreState);
    serverStatePending = true;
    const result = await childRunner(runChild)(
      invocation(["import", archivePath, "--force"], hermesRoot, hermesCli)
    );
    restoreServerState(restoreState);
    serverStatePending = false;
    if (
      !nativeSummaryAccepted(result, "Import complete:", "Import incomplete:")
    ) {
      return failed("hermes_snapshot_import_failed");
    }
    if (latest.sourceProfileId !== profileId) {
      const sourceRoot = join(hermesRoot, "profiles", latest.sourceProfileId);
      if (!existsSync(sourceRoot) || existsSync(profileRoot)) {
        return failed("hermes_snapshot_profile_relocation_rejected");
      }
      const renamed = await childRunner(runChild)(
        invocation(
          ["profile", "rename", latest.sourceProfileId, profileId],
          hermesRoot,
          hermesCli
        )
      );
      if (
        childFailed(renamed) ||
        existsSync(sourceRoot) ||
        !existsSync(profileRoot)
      ) {
        return failed("hermes_snapshot_profile_relocation_failed");
      }
    } else if (!existsSync(profileRoot)) {
      return failed("hermes_snapshot_profile_missing");
    }
    restorePortableExternalUserState({ profileRoot, hermesRoot });
    rmSync(retryMarker, { force: true });
    fsyncDirectory(dirname(retryMarker));
    return {
      ok: true,
      status: "RESTORED",
      archiveDigest: latest.archiveDigest,
    };
  } catch {
    return failed("hermes_snapshot_restore_failed");
  } finally {
    if (serverStatePending) restoreServerState(restoreState);
    rmSync(archivePath, { force: true });
  }
}

export async function restoreLatestHermesSnapshot(input) {
  const { profileId, profileRoot, hermesRoot } = input ?? {};
  if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
    return failed("hermes_snapshot_restore_identity_rejected");
  }
  try {
    return await withProfileSoulLock(
      profileId,
      async () => {
        const retryMarker = join(
          hermesRoot,
          "backups",
          `.restore-pending-${profileId}`
        );
        recoverInterruptedRestoreState({
          profileId,
          profileRoot,
          hermesRoot,
          retryMarker,
        });
        return await restoreLatestHermesSnapshotLocked(input);
      },
      { dataRoot: hermesRoot }
    );
  } catch {
    return failed("hermes_snapshot_restore_failed");
  }
}