fairladyz625/harness-anything/harness-migration

Claude Code and agent skill by fairladyz625/harness-anything

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Diagnose and migrate a Harness Anything ledger from a machine that does not have the current Harness installed. Use when a project's harness/ ledger predates the current generation, when daemon attach fails because a current ledger has pre-S4 doc cuts, or when a user asks to upgrade, migrate, replay, or repair an old Harness ledger. The skill first confirms the symptom really is a ledger-generation mismatch, then fetches the current source into a temporary location without disturbing an existing Harness install.

Install

npx vskill@latest install fairladyz625/harness-anything/harness-migration

SKILL.md

Harness Migration

First confirm a broken harness/ ledger actually has a generation mismatch rather than some other fault. If it does, replay it into a freshly initialized current-format repository; the source is never written to. New destination initialization has the identity prerequisites documented in harness-install; an isolated daemon root does not provide a signed-in identity. Replay is the only supported migration path — there is no in-place repair tool.

This skill assumes nothing is installed. It fetches the current source into a throwaway directory and runs everything from there. A Harness installation already on the machine — including a running daemon — is left untouched.

Before anything: confirm this is the right document

This skill is versioned in the harness-anything repository as skills/harness-migration/SKILL.md on `main`, and that branch is the authority. Read it from a git ref, never from whatever working tree happens to be at hand — a checkout parked on another branch may not carry this file at all, or may carry an older revision, and neither difference is visible once the text is in front of you.

bash
git -C <any-checkout-of-harness-anything> show origin/main:skills/harness-migration/SKILL.md

A machine may also carry a separately maintained skill with a similar name — harness-ledger-migration is the one that exists today, and it is an older, different document rather than an alias. Following it instead is a silent wrong turn. The front matter settles it: this skill's name: is exactly harness-migration.

The one rule that makes this work

Every command runs against an isolated daemon user root. Export it once and keep it exported for the whole session:

bash
export HARNESS_MIGRATION_WORK="$(cd "$(mktemp -d "${TMPDIR:-/tmp}/ha-migration.XXXXXX")" && pwd -P)"
export HARNESS_DAEMON_USER_ROOT="$HARNESS_MIGRATION_WORK/daemon-user-root"
mkdir -p "$HARNESS_DAEMON_USER_ROOT"

Without it, the CLI connects to whatever Harness daemon is already running on the machine and that daemon serves a different generation of the code. The failure does not announce itself as a conflict — it arrives as:

error code=missing_vertical

with "origin":"daemon" in the JSON receipt. Match on those two fields, not on the hint text: the hint describes an unavailable vertical, which is true but misleading, and it reads differently depending on which generation the running daemon serves. code=missing_vertical together with "origin":"daemon" means HARNESS_DAEMON_USER_ROOT is not set.

Do not stop or uninstall the user's existing Harness to work around it: isolation is sufficient, and stopping their daemon interrupts work you are not responsible for restoring.

The cd … && pwd -P wrapper around mktemp is what keeps the path readable. On macOS $TMPDIR already ends in a slash, so the template produces a doubled slash, and /tmp is itself a symlink into /private/tmp. Either way the raw path turns up in every receipt from here on and does not match what you see on the filesystem. pwd -P resolves both, once, at the start. Quoting is not involved: "${TMPDIR:-/tmp}"/ha-migration.XXXXXX and "${TMPDIR:-/tmp}/ha-migration.XXXXXX" are the same string to the shell, and neither one avoids the doubled slash.

If your shell does not persist between commands

The steps below accumulate exported variables and, in step 1, a shell function. An agent that gets a fresh shell per tool call keeps none of them — the second command runs with HARNESS_DAEMON_USER_ROOT unset and connects to the machine's own daemon, which is exactly the failure this section exists to prevent. This is the root cause of the downstream traps the later steps describe individually (zsh word-splitting in step 1, nohup not seeing the function in step 8, env resolving ha from PATH in step 9).

Write the session state to a file and source it at the start of every later command:

bash
export HARNESS_MIGRATION_ENV="$HARNESS_MIGRATION_WORK/env.sh"
cat > "$HARNESS_MIGRATION_ENV" <<EOF
export HARNESS_MIGRATION_WORK='$HARNESS_MIGRATION_WORK'
export HARNESS_MIGRATION_ENV='$HARNESS_MIGRATION_ENV'
export HARNESS_DAEMON_USER_ROOT='$HARNESS_DAEMON_USER_ROOT'
EOF
# every later command: . "$HARNESS_MIGRATION_ENV" && <command>

Append each new export to that file as the steps below introduce it — HA_ENTRY, ARCHIVE_SOURCE, WORK_SOURCE, TARGET_REPO, DRY_RUN, LEDGER_ARCHIVE, SOURCE_SHA_BEFORE. Record $HARNESS_MIGRATION_ENV somewhere you will still have it later; it is the one path you must not lose.

Keep this file inside `$HARNESS_MIGRATION_WORK`, and if you write anything anywhere else, put the repository name in its filename. mktemp already makes the work directory unique, but agents habitually drop scratch files into a shared session scratchpad instead — and env.sh, dry-run.txt and apply.log are the same name in every migration. When two migrations run in parallel, a sibling overwriting your env file is not hypothetical: it has happened, and it presents as your own variables quietly turning into someone else's, several steps after the damage. Either keep everything under $HARNESS_MIGRATION_WORK, or name it env-<repo>.sh, dry-run-<repo>.txt, apply-<repo>.log.

1. Fetch the current source

Node 24 or newer is required. Check before cloning:

bash
node --version

The CLI is run from its TypeScript entry point, which relies on Node's native type stripping. On an older Node every command fails with

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

which reads like a missing build step and is not one — no amount of npm run build fixes it. Stop and tell the user to upgrade Node; nothing below works until they do.

bash
cd "$HARNESS_MIGRATION_WORK"
git clone --depth 1 https://github.com/FairladyZ625/harness-anything.git ha-src
cd ha-src
npm install --no-audit --no-fund
export HA_ENTRY="$HARNESS_MIGRATION_WORK/ha-src/packages/cli/src/index.ts"
ha() { node "$HA_ENTRY" "$@"; }
ha --version

Expect a version line. The repository is about 10 MB and install takes seconds.

`ha` here is a shell function, not an exported variable. The obvious export HA="node …/index.ts" followed by $HA --version works in bash and silently fails in zsh, which does not word-split unquoted parameters: zsh passes node …/index.ts as a single argument and the receipt comes back unsupported_command. A function behaves identically in both shells.

If your shell does not persist between commands, append both the variable and the function to the env file from the top of this skill, and source it every time:

bash
{ echo "export HA_ENTRY='$HA_ENTRY'"
echo 'ha() { node "$HA_ENTRY" "$@"; }'; } >> "$HARNESS_MIGRATION_ENV"

Two consequences worth knowing now rather than at step 8:

  • The function lives in the shell that defined it. Anything that runs in a

detached process — see step 8 — must spell out node "$HA_ENTRY" … instead, and sourcing the env file does not change that.

  • env -u HARNESS_DAEMON_USER_ROOT ha …, which step 9 uses on purpose, does

not see the function. env execs a program, so it resolves ha from PATH — the machine's own installation. That is exactly what step 9 wants, and step 9 says so again where it matters.

Do not look for `node_modules/.bin/ha`. The published bin points at dist/, which a source checkout does not contain, so the linked binary is absent. Running the TypeScript entry directly is the supported path here and needs no build step.

1a. Confirm the symptom is a ledger-generation mismatch before importing

Do this after step 1 fetches the current source, before creating a destination or running `migrate import`. The symptoms below are an entry point, not a decision: both kinds of ledger can produce them.

What you seeWhat it meansNext action
doc event envelope or payload is invalidCould be either an older ledger or a current ledger with pre-S4 doc cuts.Run the read-only event scan below.
daemon receipt repo_attach_failed or repo_unavailable while attaching the repositoryThe daemon could not build its projection; it does not identify the ledger generation.Run the read-only event scan below; do not retry attach as a probe.

Set the source path once. The scan only reads its event files; it does not need the daemon and does not alter the source.

bash
export ARCHIVE_SOURCE="$(cd /absolute/path/to/repository-with-harness && pwd -P)"
export CANONICAL_EVENT_CONTRACT="file://$HARNESS_MIGRATION_WORK/ha-src/packages/kernel/src/domain/doc-sync.contract.ts"
node --input-type=module - "$ARCHIVE_SOURCE/harness/events" "$CANONICAL_EVENT_CONTRACT" <<'NODE'
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
const [eventsRoot, contractUrl] = process.argv.slice(2);
const { parseCanonicalEvent } = await import(contractUrl);
const counts = { files: 0, parsed: 0, unknown_schema: 0, legacy_cut_shape: 0, other: 0 };
async function* eventFiles(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const file = join(dir, entry.name);
if (entry.isDirectory()) yield* eventFiles(file);
else if (entry.isFile() && entry.name.endsWith('.json') && entry.name !== 'head.json') yield file;
}
}
function hasLegacyCut(value) {
if (!value || typeof value !== 'object') return false;
if (Array.isArray(value)) return value.some(hasLegacyCut);
const cut = value.baseLedgerSha;
if (cut && typeof cut === 'object' && !Array.isArray(cut)) {
const keys = Object.keys(cut).sort();
if (keys.length === 2 && keys[0] === 'repoId' && keys[1] === 'sha') return true;
}
return Object.values(value).some(hasLegacyCut);
}
for await (const file of eventFiles(eventsRoot)) {
counts.files++;
let text, event;
try {
text = await readFile(file, 'utf8');
event = JSON.parse(text);
parseCanonicalEvent(text);
counts.parsed++;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message === 'canonical event schema is unknown') counts.unknown_schema++;
else if (message === 'doc event envelope or payload is invalid' && event?.schema === 'doc-event/v1' && hasLegacyCut(event)) counts.legacy_cut_shape++;
else counts.other++;
}
}
console.log(JSON.stringify(counts, null, 2));
NODE

Interpret the counts conservatively:

  • If unknown_schema > 0 or other > 0, this is a previous-generation

ledger rather than the narrow S4-only cut mismatch. Continue with the replay steps in this skill. The replay importer constructs current migration events rather than copying source event bytes.

  • If legacy_cut_shape > 0, unknown_schema = 0, and other = 0, the ledger

is a current-generation ledger whose doc cuts predate S4. Replay handles it, and replay is the only supported path: there is no in-place restamp migration, and none is planned. An in-place restamp was built and evaluated; it was deliberately not shipped, because a tool that rewrites cut identity in place has to be trusted on a ledger nobody can re-derive, while replay reconstructs the destination from source events and leaves the source untouched.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/fairladyz625/harness-anything/harness-migration/badge)](https://verified-skill.com/skills/fairladyz625/harness-anything/harness-migration)

Something wrong with this skill or its page? Tell us.