Agent skill

Harness Migration

by FairladyZ625 in FairladyZ625/harness-anything

Diagnose and migrate a Harness Anything ledger from a machine that does not have the current Harness installed.

AGPL-3.0Auto-check passed

Install Harness Migration

skills CLI
$ npx skills add FairladyZ625/harness-anything --skill harness-migration -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install FairladyZ625/harness-anything harness-migration --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/FairladyZ625/harness-anything.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/harness-migration .claude/skills/harness-migration && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
harness-migration
GitHub stars
226
Token cost
~14k tokens
SKILL.md length
6,940 words
Files
2
Skills in repo
12
Repo updated
First seen
Licence
AGPL-3.0

At a glance

Diagnose and migrate a Harness Anything ledger from a machine that does not have the current Harness installed.

  • Works in 9 steps: Fetch the current source → Freeze and back up the source ledger → Initialize the destination → …
  • A projects harness/ ledger predates the current generation
  • SKILL.md covers Before anything: confirm this…, The one rule that makes this…, 1. Fetch the current source and 1a. Confirm the symptom is a…, plus 10 more sections
  • Calls git, npm and node

What it does

Harness Migration is an agent skill from FairladyZ625/harness-anything. 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.

Its SKILL.md is about 14k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `agents/openai.yaml`).

The repository describes itself as: Clean-room Harness rewrite monorepo. The licence is AGPL-3.0.

When your agent uses it

  • A projects harness/ ledger predates the current generation
  • Daemon attach fails because a current ledger has pre-S4 doc cuts
  • A user asks to upgrade
  • Repair an old Harness ledger

Example prompts

  • “/harness-migration”

Requirements

  • Node.js

Workflow steps

9 steps, taken from the step headings in SKILL.md.

  1. Fetch the current source
  2. Freeze and back up the source ledger
  3. Initialize the destination
  4. Dry-run and read the output
  5. Resolve destination conflicts — one batch, one decision
  6. Rebuild legacy presets as v3 packages
  7. Handle historical-format observations
  8. Recreate the destination, then apply once
  9. Land the ledger

What it can do on your machine

Read from SKILL.md and the folder at commit c805ddf. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • git
    • npm
    • node
    • sh

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use git and npm, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Harness Migration loads about 14k tokens when it runs. Until then it costs about 134 tokens; SKILL.md has 6,940 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~134
When it runs · the whole SKILL.md, loaded when a task matches
~14k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from FairladyZ625/harness-anything at commit c805ddf, republished under its AGPL-3.0 licence (© FairladyZ625). 6,940 words, ~14,341 tokens.

Download SKILL.mdSave it as .claude/skills/harness-migration/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
harness-migration
description
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.

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.

    Know what replay costs you here: it remaps entity IDs and writes a new ledger, which is more than this ledger strictly needs — only its cut identity is stale. Budget for the ID remapping (see the ID mapping steps below) rather than looking for a narrower tool. If remapped IDs are genuinely unacceptable for your ledger, stop and report that; do not improvise a hand-edit of event bytes.

  • If all three failure counts are zero, the stream already parses under the current code. This is not a generation mismatch, so migration will not fix it; stop and investigate the reported symptom separately.

    One in-place restatement does exist for such a ledger, and it is narrow: tasks that still carry the retired milestone or epic task class. Grouping is now a work — one root task plus its subtree — so run ha task contract migrate --dry-run, review the planned rows, then ha task contract migrate --apply against the daemon that serves the ledger. It restates milestone to work and epic to standard and keeps each task's historical presetId as provenance. Replay applies the same restatement on import, so a replayed ledger does not need it.

If the scan itself cannot read the events directory or run the current parser, stop and report that failure. Do not infer the generation from harness.yaml: both generations can carry schema: harness-anything/v1.

2. Freeze and back up the source ledger

Back up and digest harness/ only — never the repository root.

bash
export WORK_SOURCE="$HARNESS_MIGRATION_WORK/legacy-copy"
mkdir -p "$HARNESS_MIGRATION_WORK/backups" "$WORK_SOURCE"
export LEDGER_ARCHIVE="$HARNESS_MIGRATION_WORK/backups/legacy-harness.tar"
COPYFILE_DISABLE=1 tar -cf "$LEDGER_ARCHIVE" -C "$ARCHIVE_SOURCE" harness
export SOURCE_SHA_BEFORE="$(COPYFILE_DISABLE=1 tar -cf - --exclude='harness/.git' -C "$ARCHIVE_SOURCE" harness | shasum -a 256 | awk '{print $1}')"
printf 'source-before %s\n' "$SOURCE_SHA_BEFORE"
tar -xf "$LEDGER_ARCHIVE" -C "$WORK_SOURCE"

$WORK_SOURCE now contains harness/ and nothing else, which is all the importer reads — --source takes the repository root and descends into harness/ itself.

Three things this deliberately does not do, each for a reason worth knowing:

  • No git bundle. harness/ is its own git repository and the outer repo ignores it (/harness/ in .gitignore, git ls-files harness/ returns nothing). A bundle of the outer repo therefore contains zero ledger content — it looks like a backup and protects nothing.
  • No repository-root digest. The root contains .harness/, which is ignored runtime state — locks, write-journal, cache, script-runs, task-holders — that any running daemon rewrites continuously. A root digest changes on its own between the before and after reads, so the "source untouched" check would report a false failure every time. A check that must be ignored to proceed is worse than no check.
  • No cp -a of the root. It copies node_modules and the whole .git directory, which the importer never reads.

The digest excludes harness/.git for the same reason the root digest is excluded entirely: it is live metadata, not content. A git fetch from any mirror or a background maintenance run rewrites packed-refs, FETCH_HEAD and the reflog without touching a single ledger file, and the closing comparison then fails for a reason unrelated to the importer. This bit a real migration — an unrelated mirror fetch landed mid-run and the source looked modified when it was not. The archive is not filtered: a backup must carry the ledger's own git history, and only the digest needs the exclusion.

The digest and the tar print nothing while they run. On a small ledger they are effectively instant — about a second each for a 963-event, 257 MB harness/ — so if you are staring at a blank prompt for more than a few seconds, look at the size of what you are digesting rather than waiting. Only a genuinely large ledger takes tens of seconds. Either way, if it runs for many minutes you are digesting more than harness/; check the -C argument. Nothing may write to $ARCHIVE_SOURCE/harness/ while the migration runs, or the closing digest will differ for a reason that has nothing to do with the importer.

Show the user $LEDGER_ARCHIVE and stop until they confirm one independent copy exists off this machine. Migration is one-shot; this is the only point where that confirmation is cheap.

If you are a dispatched agent with no way to reach the user, you cannot satisfy that stop — say so rather than pretending you did. Record the archive path and its size, state plainly in your report that the off-machine copy was never confirmed, and continue. The rest of the migration writes only to $HARNESS_MIGRATION_WORK, so nothing before step 9 can lose the source; step 9 is where the missing confirmation actually matters. Only the confirmation is waived, not step 9. Unless your dispatch says otherwise, landing the ledger and running the six landing checks are yours to do; carry the unconfirmed backup forward as a stated caveat in your report instead of treating it as permission to stop at step 8.

Every repair below targets $WORK_SOURCE. $ARCHIVE_SOURCE is read-only and its harness/ digest is re-checked at the end.

3. Initialize the destination

Ask the user for the new repository id, owner person id, and display name.

bash
export TARGET_REPO="$HARNESS_MIGRATION_WORK/new-repository"
mkdir -p "$TARGET_REPO" && cd "$TARGET_REPO"
git init -q . && git commit -q --allow-empty -m "base"
ha init --repo-id <new-repo-id> --person-id <owner-person-id> --display-name '<display-name>'

Expect a receipt listing created paths and a commit sha. The first command in a fresh user root starts an isolated daemon on its own; that is expected and it belongs to this migration, not to the user's installation.

If init fails, move this directory aside and start a new empty one. Do not repair a half-initialized target in place.

4. Dry-run and read the output

bash
export DRY_RUN="$HARNESS_MIGRATION_WORK/dry-run.txt"
ha migrate import --source "$WORK_SOURCE" --dry-run > "$DRY_RUN" 2>&1; echo "exit=$?"
cat "$DRY_RUN"

Use the importer as the classifier. Do not inventory the old repository or invent categories of your own. Act only on what the report prints:

  • Oracle: same-cut-projection or Oracle: rebuilt-source
  • five reconciliation rows (task / decision / fact / relation / execution)
  • Format observations and each ACCEPT or SKIP line
  • Attribution
  • the authored coverage table and each required row
  • Authored reconciliation

Do not manufacture .harness/cache/task.sqlite in the working source. If it is absent, the importer rebuilds a disposable read-only oracle from committed flat or sharded events and older authored packages. Oracle: rebuilt-source is the expected receipt for that path; the source tree and Git refs must remain byte-for-byte unchanged.

Branch on those rows:

What the report showsGo to
a required row saying destination content differssection 5
required on presets/**section 6
ACCEPT schedule_definition_facet_mismatchreview it in section 7; no source edit
unsupported_legacy_event or migration_projection_rebuild_failedsection 7
any - SKIP linereview it in section 7; it is diagnostic, not a count subtraction
any other required rowshow the exact row to the user and stop — this workflow does not cover it
no required and all five reconciliation rows passsection 8

5. Resolve destination conflicts — one batch, one decision

ha init seeds README, standards, walls and people.yaml files. A source ledger usually has its own versions of those paths. Each conflict row prints both sides and the exact flag to use:

| people.yaml | required | 1 | FAIL | destination content differs:
  source kind=file, source sha256=240b9a55…, source bytes=561;
  destination kind=file, destination sha256=39e0af13…, destination bytes=512;
  resolve with --resolve harness/people.yaml=destination|source |

Handle every conflict in one pass. Never ask about them one at a time — a migration has a handful of these and each round trip costs the user an interruption for a decision that is usually the same one.

Read both sides of all of them first:

bash
CONFLICTS=($(grep -o -- '--resolve [^=]*=' "$DRY_RUN" | sed 's/--resolve //;s/=$//' | sort -u))
printf 'conflicts: %s\n' "${#CONFLICTS[@]}"
for c in "${CONFLICTS[@]}"; do
  printf '\n===== %s :: DESTINATION =====\n' "$c"; cat "$TARGET_REPO/$c" 2>/dev/null
  printf '\n===== %s :: SOURCE =====\n' "$c"; cat "$WORK_SOURCE/$c" 2>/dev/null
done

The default is destination for every row. The destination file is the current format's skeleton and the migration exists to adopt it — a source file that merely says the same thing in the old shape has nothing to preserve.

What the source file may have is project-specific substance the skeleton does not carry: local conventions, routing rules, directory contracts, a project's own standards. Carry that content forward into the destination file. Merging is an edit you make, not something the importer does; the flag is still =destination.

So present one table and ask for one confirmation:

pathresolutionwhat carries over from the old file
harness/adr/README.mddestinationwhen a lightweight ADR fits, ha decision propose for load-bearing choices, back-link rule
harness/context/architecture/README.mddestinationmanifest read order, model update boundary
harness/people.yamlchoosenothing — see below

Say plainly: this is the default, and they can override any row to source if they want the old file kept verbatim. One answer covers the whole table.

people.yaml is normally automatic. Compatible rosters are unioned through the same People Action transition and published as people-event/v1; the destination owner binding remains and source-only people, roles, credentials, and non-conflicting scalar details carry forward. No question or hand edit is needed.

Only a genuine scalar or role-authority contradiction produces a required row. For that row, ask which side to retain with the explicit flag and record the losing declaration in the hand-over. After migration, reconcile individual entries through the current identity and access management surface. ha people now exposes delegation and revocation, not roster add/remove. Never commit a manual edit to the ledger.

Do the merge edits after the final apply in step 8, not now. Step 8 recreates the destination from scratch, which would discard anything edited earlier. Record the third column now; apply it once at the end.

Collect the answers into repeated flags and re-run:

bash
export RESOLVE_ARGS=(
  --resolve 'harness/context/README.md=destination'
  --resolve 'harness/people.yaml=source'
)
ha migrate import --source "$WORK_SOURCE" "${RESOLVE_ARGS[@]}" --dry-run > "$DRY_RUN" 2>&1; echo "exit=$?"

Each answer comes back as a row beginning resolved: destination or resolved: source, carrying both digests so the discarded side stays visible. A conflict left out of the flags stays required.

A directory target accepts only =destination. If =source reports that the target is a directory, show the error and have the user handle that path.

6. Rebuild legacy presets as v3 packages

Legacy preset packages are not carried over — they are rebuilt against the current format.

bash
find "$WORK_SOURCE/harness/presets" -mindepth 1 -maxdepth 1 -type d | sort
ha preset inspect standard-task --profile baseline --vertical software/coding --locale en-US --json

The legacy-migration preset bundled with the current release documents the exact v2 → v3 field mapping. Read it:

bash
cat "$HARNESS_MIGRATION_WORK/ha-src/packages/preset/assets/software-coding/presets/legacy-migration/PRESET.md"

Build each replacement under $HARNESS_MIGRATION_WORK/rebuilt-presets/<id>/, then validate and install from the destination:

bash
cd "$TARGET_REPO"
for NEW_PRESET in "$HARNESS_MIGRATION_WORK/rebuilt-presets"/*; do
  ha preset validate --source "$NEW_PRESET" --json
  ha preset install --source "$NEW_PRESET" --json
done
ha preset audit --vertical software/coding --json

Continue only when every validation reports "valid": true and the audit shows no blocked package.

preset audit --json answers about the vertical as a whole — a count of packages and a count of issues — not one row per package. So it tells you whether something is blocked, not which package it is. The per-package verdict is the validate output in the loop above; if the audit reports issues, read back the validations rather than looking for a package list in the audit receipt that is not there.

Then take the old packages out of the copy being imported — the archived original still has them:

bash
mv "$WORK_SOURCE/harness/presets" "$HARNESS_MIGRATION_WORK/legacy-presets-rebuilt"

Re-run the dry-run from section 4 with "${RESOLVE_ARGS[@]}". The presets/** row must be gone.

7. Handle historical-format observations

bash
grep -E '^- (ACCEPT|SKIP) ' "$DRY_RUN"

Show every observation to the user. SKIP rows are legacy-parser diagnostics; they are not subtracted from the projection oracle and do not independently block a reconciled import.

ACCEPT schedule_definition_facet_mismatch with treatment=accepted_truth_gap is the one known schedule declaration-claim variant. The disposable oracle uses the definition facet from the event while the original claim blob stays untouched in the forensic archive. Confirm the warning has that exact code and treatment, then continue; do not rewrite it.

For unsupported_legacy_event, preserve the source and collect the exact event path, schema, and nested parser error. For migration_projection_rebuild_failed, collect the nested cause and identify any missing or corrupt committed blob. Stop and report either case as a missing compatibility fixture. Do not hand-edit canonical event bytes to get past it. A migration_projection_oracle_cut_mismatch means source writers were not frozen or a local projection is stale: stop the writers, remove or regenerate that local cache, and dry-run again.

If a dry-run reports reconciliation FAIL or invalid_write_plan, do not apply. Keep the full receipt and report the named kind/event. Apply is allowed only after dry-run exits zero and proves every planned event against the current write contract.

8. Recreate the destination, then apply once

Source repairs must be replayed into a target that never saw the broken data.

bash
cd "$HARNESS_MIGRATION_WORK"
mv "$TARGET_REPO" "$HARNESS_MIGRATION_WORK/preview-repository"
mkdir -p "$TARGET_REPO" && cd "$TARGET_REPO"
git init -q . && git commit -q --allow-empty -m "base"
ha init --repo-id <new-repo-id> --person-id <owner-person-id> --display-name '<display-name>'
for NEW_PRESET in "$HARNESS_MIGRATION_WORK/rebuilt-presets"/*; do ha preset install --source "$NEW_PRESET" --json; done
ha migrate import --source "$WORK_SOURCE" "${RESOLVE_ARGS[@]}" --dry-run; echo "exit=$?"

Apply only when that exits zero, all five reconciliation rows pass, authored coverage has no required, and every accepted historical observation has been reviewed. A source rebuilt without task.sqlite must say Oracle: rebuilt-source.

Run apply detached, and poll it. It prints nothing at all until it finishes, so a foreground run is indistinguishable from a hang and any caller with a command timeout will kill it partway. That has already happened once: an agent harness terminated a real apply at around the 60-minute mark, and a half-imported target is not recoverable — step 8 has to start over from ha init.

Budget from the event count, not from the worst case. Apply costs roughly 200 ms per event and scales with the count, not with the byte size: a 963-event / 257 MB ledger finished in 211 seconds, while a 21,000-event ledger took 1h22m. Multiply before you plan around it; treating every migration as an overnight job over-provisions a small one by more than an order of magnitude.

Have the detached run record its own exit code. A background process that has already been reaped cannot be asked for its status from another shell — and "apply exited zero" is a line in the "Done when" list, so an unrecoverable exit code means the migration cannot be signed off. Write it to a file as the process ends:

bash
cat > "$HARNESS_MIGRATION_WORK/apply.sh" <<'EOF'
#!/bin/sh
node "$HA_ENTRY" migrate import --source "$WORK_SOURCE" "$@"
echo "exit=$?" > "$HARNESS_MIGRATION_WORK/apply-exit.txt"
EOF
chmod +x "$HARNESS_MIGRATION_WORK/apply.sh"

cd "$TARGET_REPO"
nohup "$HARNESS_MIGRATION_WORK/apply.sh" "${RESOLVE_ARGS[@]}" \
  > "$HARNESS_MIGRATION_WORK/apply.log" 2>&1 &
echo "apply pid=$!"

The wrapper is a file rather than an inline sh -c because RESOLVE_ARGS is a shell array and does not survive being flattened into a quoted string. It reads HA_ENTRY, WORK_SOURCE, HARNESS_MIGRATION_WORK and HARNESS_DAEMON_USER_ROOT from the environment, so they must be exported — which they are, if you followed the export blocks above. Note also node "$HA_ENTRY" rather than ha: the shell function from step 1 does not exist inside nohup.

Poll for the exit file, not for the pid — the file is what survives:

bash
if [ -f "$HARNESS_MIGRATION_WORK/apply-exit.txt" ]; then
  cat "$HARNESS_MIGRATION_WORK/apply-exit.txt"; tail -20 "$HARNESS_MIGRATION_WORK/apply.log"
else
  echo "still running"
fi

While it runs, du -sh "$TARGET_REPO/harness" and the commit count in $TARGET_REPO/harness both climb — that is the only live progress signal there is. A stalled apply shows neither growing for many minutes.

Once it has finished, confirm the source was never written to:

bash
export SOURCE_SHA_AFTER="$(COPYFILE_DISABLE=1 tar -cf - --exclude='harness/.git' -C "$ARCHIVE_SOURCE" harness | shasum -a 256 | awk '{print $1}')"
test "$SOURCE_SHA_BEFORE" = "$SOURCE_SHA_AFTER" && echo "source ledger untouched"

The --exclude must match step 2's exactly. Digesting different sets on the two sides guarantees a mismatch and tells you nothing.

If apply exits nonzero, keep its output, move the target aside, and restart from a fresh ha init. Never re-run apply against a partially imported target.

Then pack the new ledger's git repository. The importer commits once per event and never packs, so a freshly imported ledger is all loose objects — the 21,000-event migration produced 218,213 loose objects, zero packs, and an 18 GB .git for about 850 MB of actual content.

bash
git -C "$TARGET_REPO/harness" gc --aggressive --prune=now
du -sh "$TARGET_REPO/harness/.git" "$TARGET_REPO/harness"
git -C "$TARGET_REPO/harness" rev-list --count HEAD
git -C "$TARGET_REPO/harness" fsck --no-progress

That run took the same ledger from 18 GB to a 181 MB .git in a single pack, with the commit count unchanged and fsck clean. The effect holds at small scale with the same shape and a far smaller bill: a 963-event ledger went from 104 MB / 10,332 loose objects / 0 packs to 19 MB / 0 loose / 1 pack in about 7 seconds, again with the commit count unchanged and fsck clean. Expect seconds on a small ledger, not the long wait the 18 GB figure suggests.

Do this before step 9 either way — it is the difference between handing the user a 19 GB directory and a 862 MB one, and after landing the daemon holds the repository.

Now execute the merge column recorded in step 5. $HARNESS_MIGRATION_WORK/preview-repository still holds the previous destination, and $WORK_SOURCE still holds the old files, so both sides remain readable:

bash
diff -u "$TARGET_REPO/harness/adr/README.md" "$WORK_SOURCE/harness/adr/README.md" | head -40

For each row, edit the destination file to carry the project-specific content forward. Keep the current file's structure and add to it — do not paste the old file over it, which would undo the resolution that was just applied. Show the user the resulting diff. This is the last write of the migration.

9. Land the ledger

$TARGET_REPO/harness is a standalone git repository — its own .git, its own history, its own remote. That is true in both placements below, and it is the thing that must not be lost: a ledger is never a subdirectory of the project's repository. One ledger, mirrored by the checkouts that use it.

Local — the default. The ledger lives inside the project directory as <project>/harness, a repository of its own, and the project ignores it. This is the shape ha init produces for a new project and the shape previous generations used. Choose it unless the user says otherwise.

Central. The ledger lives outside any project, in a directory of its own, and is registered by absolute path. Choose it when the user tells you the ledger is shared — several machines mirroring one authoritative copy.

Show full SKILL.md (3,018 more words)Show less
First: the machine needs a current-generation CLI that outlives this work directory

Read this before touching the destination. Everything up to here ran from $HARNESS_MIGRATION_WORK, which is disposable. The landed ledger is not: it needs a daemon serving it from here on, and that daemon must be the current generation — the whole reason for this migration is that the machine's installed ha belongs to the previous one, and a previous-generation daemon cannot serve a current-format ledger.

Check what the machine actually has — do not assume it has nothing. A current-generation ha is often already installed, built from this repository rather than from a registry:

bash
env -u HARNESS_DAEMON_USER_ROOT command ha --version; echo "exit=$?"

A current-generation CLI prints a version and exits 0. A previous-generation one rejects the flag outright and exits nonzero:

{"ok":false,"command":"parse","error":{"code":"unknown_option",
 "hint":"Unknown option '--version' for 'ha'. Did you mean '--json'?"}}

Route on whether the flag is accepted, never on the number it prints. The version string is 0.0.1 and carries no generation marker, so it is the same on a current-generation global install and on the source checkout you have been running all along — seeing 0.0.1 twice tells you nothing about which build is which. Acceptance of --version is the whole signal; the number is noise.

If the flag is accepted, use that installation and skip to the ha_serving block below. Running the migration checkout would also work, but handing the user a command they already have beats handing them a checkout to maintain.

If it rejects, the installed CLI cannot serve what you are about to land, and there is nothing to npm install: @harness-anything/cli is not published (npm view returns 404). Two workable answers, in order of preference:

bash
# preferred — build the checkout and install it on PATH, so `ha` just works
cd "$HARNESS_MIGRATION_WORK/ha-src/packages/cli" && npm run build && npm install -g .
command ha --version

# fallback — keep a durable checkout and invoke it by path
export HA_HOME="$HOME/.harness-cli-src"          # anywhere durable; not $HARNESS_MIGRATION_WORK
cp -R "$HARNESS_MIGRATION_WORK/ha-src" "$HA_HOME"
export HA_ENTRY="$HA_HOME/packages/cli/src/index.ts"
node "$HA_ENTRY" --version

The package's bin points into dist/, which a fresh clone does not contain — that is why the global install needs an explicit npm run build first, and why the skill has been running the TypeScript entry point directly up to now. A build is all that is missing; it is not an unavailable artifact.

Whichever branch you took, name the result once and use it for the rest of step 9, so the serving CLI and the throwaway migration entry point never get confused for each other:

bash
ha_serving() { command ha "$@"; }           # the machine's ha is current-generation
# ha_serving() { node "$HA_ENTRY" "$@"; }   # ...or the durable checkout instead
ha_serving --version

Hand the user that same invocation — the exact command that now drives this ledger — and say plainly whether it replaces their existing ha. Do not leave them to discover it the first time ha task list fails.

Note also that env -u HARNESS_DAEMON_USER_ROOT ha … below resolves ha from PATH, which skips any shell function or alias the user has defined around it. If theirs injects flags — --actor, a default --root — those are silently dropped. This is not hypothetical: a real machine defines ha as a function wrapping command ha --actor human:<person>, so commands run through env are attributed to a different actor than the same commands typed by hand. Run type ha before relying on either form, and write command ha when you mean the binary.

The procedure below is the same for both placements. Only LEDGER_HOME differs.

bash
# local (default) — the project directory the user is migrating
export LEDGER_HOME="/absolute/path/to/the/project"
# central — a directory of its own, outside every project
# export LEDGER_HOME="/absolute/path/the/user/chooses"

cd "$LEDGER_HOME"
# If the machine's own daemon already serves a ledger here, release it FIRST -- see below.
mv harness "harness.pre-migration-$(date +%Y%m%d-%H%M%S)"    # superseded, not disposable
cp -R "$TARGET_REPO/harness" ./harness

If the destination is already a live Harness workspace, release it before the mv, not after. Everything up to here ran against the throwaway migration daemon; the ledger you are replacing belongs to the machine's own daemon, which is a different registry and is holding an open cell and a writer lock on the directory you are about to move. Release it first, with HARNESS_DAEMON_USER_ROOT unset so the commands reach that daemon rather than the migration one:

bash
env -u HARNESS_DAEMON_USER_ROOT command ha repo unbind <existing-repo-id>

The daemon keeps running and keeps serving its other repositories; only this ledger is released, which you can confirm by the writer lock next to it disappearing. Then do the mv and cp above.

A destination with no Harness yet has nothing to release, and this step is skipped.

Move the old ledger aside; do not delete it. The step 2 archive holds its content, but the directory is also a git repository with its own history, and this is the one moment in the migration where a mistake is discovered late. The same reasoning already governs the old runtime directory below: report it, hand over the path, and let the user delete it when they are satisfied. A rename is reversible in one command; rm -rf against a directory the skill does not own is not.

In a project, isolate the ledger from the project's own repository before committing anything. ha init does this on a fresh project, but registering an existing ledger does not, so do it here:

bash
for rule in '/harness/' '/.harness/' '/harness.pre-migration-*/'; do
  grep -qxF "$rule" .gitignore 2>/dev/null || printf '%s\n' "$rule" >> .gitignore
done
git rm -r -q --cached --ignore-unmatch -- harness .harness

The third rule is load-bearing, and so is checking the rules one at a time. The mv above leaves a harness.pre-migration-<timestamp>/ directory sitting in the project root, and /harness/ does not match it — it matches only a directory named exactly harness. Without its own rule that directory is untracked and visible, so the git add -A below sweeps it into the project's index: at best a 39 MB ledger copy committed into the project, at worst — since it carries its own .git — a broken gitlink to an embedded repository. It also makes the project tree permanently non-empty, which means the git status --porcelain landing check further down can never pass. Checking each rule individually matters for the same reason: a single git check-ignore -q harness guard short-circuits on projects that already ignored harness/, and those are precisely the projects that have an old ledger to move aside.

The git rm --cached is not redundant either. .gitignore has no effect on paths the index already tracks, so a project that had committed any part of the old harness/ keeps tracking it — and the ledger is private while the project may well be public.

Then attach the landed ledger to the daemon that will actually serve it, and commit the project side. Start that daemon first — register does not autostart one, and against a stopped daemon it fails with:

error code=daemon_unavailable hint=connect ENOENT /var/folders/…/daemon-501-u-….sock
bash
export HARNESS_DAEMON_USER_ROOT="$HOME/.harness"    # the serving root, not the migration one
ha_serving daemon start --service
ha_serving daemon repo register --repo-id <repo-id> --root "$LEDGER_HOME"
git add -A && git commit -m "adopt migrated ledger"    # local placement only

ha_serving is the function defined at the top of this step; the point is that the register must not go through the throwaway migration root — its registry dies with the work directory, and the command would succeed while leaving the user with a ledger nothing serves. Overriding HARNESS_DAEMON_USER_ROOT to the serving root, as the first line does, is what prevents that.

The first attach is slow. The daemon builds its projection over every event in the ledger — for a 21,000-event ledger that took several minutes with no output. Do not conclude it is stuck, and do not run it under a short timeout.

Reuse <existing-repo-id> if you released one; otherwise pick the id the user wants. A previously released id is free to reuse at the new path.

--root takes $LEDGER_HOME, not $LEDGER_HOME/harness. Both are accepted and resolve to the same canonical root, but the runtime's .harness/ directory and its writer lock are placed relative to the root you pass, and only the former puts them where the ignore rules above expect them.

Check the landing before handing over. All six must hold:

bash
test -d harness/.git                                   && echo "ledger has its own git"
git check-ignore -q harness                            && echo "project ignores the ledger"
test -z "$(git ls-files harness/ .harness/)"           && echo "project tracks no ledger file"
git --no-optional-locks status --porcelain             # project tree — expected: empty
git --no-optional-locks -C harness status --porcelain  # LEDGER tree — see below
ha_serving task create --title "landing check" --kind chore

The last one is the only proof that the ledger is writable where it now sits, and it has to run against the serving daemon to prove anything — through the migration daemon it would pass while telling you nothing about what the user will actually experience. The others only prove the layout is right.

The ledger tree check is not a formality. The importer commits event data, but the project-specific authored content merged at the end of step 8 is written to the working tree and is not part of any commit. On a real migration this left governance/walls/walls.json uncommitted — with the committed version holding an empty walls: [] array and seventeen governance walls existing only in the unstaged file. One git checkout would have erased them silently.

If that tree is dirty, do not reach for git commit — read the next paragraph first. Carry the content forward like this instead:

bash
git --no-optional-locks -C harness status --porcelain      # the real list
ha_serving doc sync --submit --path <one-dirty-path>       # per prose file

Take the list from git, not from doc status. A bare ha doc status or ha doc sync --dry-run does not enumerate every dirty authored file: with no --path, the scanner keeps a dirty path only when it is prose (.md / .txt) or its route is blocked, so a dirty file that is neither — any .json or .yaml outside the route registry, governance/walls/walls.json being the case that actually bit someone — is filtered out and never appears. The operator reads an empty report, concludes there is nothing to carry over, and hands over a dirty tree. The filter is one expression in packages/daemon/src/doc-sync-candidate-scanner.ts:13.

Then sort the list by what each file can actually do:

  • Prose (.md / .txt), route allowed — doc sync --submit --path accepts it. This is the road; naming the path explicitly is fine and expected.
  • Non-prose, route allowed (walls.json and friends) — there is no road. Naming it does not help: the scanner blocks it a few lines later with path is not canonical prose. --path converts silence into a stated reason, which is worth doing so you know what you are looking at, but it does not make the file submittable. Leave it in the working tree, name it and its contents in the hand-over, and do not commit it — see the next paragraph for why that cure is worse than the disease.
  • Route blocked (people.yaml, harness.yaml, anything under events/ or objects/, task-package files) — the block reason names the owning command. For people.yaml, use identity and access management; step 5 covers migration conflicts.

So keep the step 5 merge column to prose files wherever you have the choice. A non-prose file in that column is content the migration cannot land.

Keep accepting data separate from Git publication. Generation-1 acceptance lives in .harness/store/generations/1/ledger.sqlite with its required objects/ closure. The event-derived Git publisher certifies its document/manifest cut independently. Concurrent authored edits may leave worktree visibility pending even when Git verifies.

Use ha receipt show <op-id> --wait git_verified to inspect an accepted command's publication. Use ha ledger reconcile --generation 1 for independent import-prefix, ledger, object, and Git checks. Do not reset Git or discard the canonical database as a response to a publication failure: neither operation undoes accepted commands. Preserve concurrent edits and repair the reported follower discrepancy before retrying settlement. A wait timeout is not a failed acceptance.

Then tell them:

  • their existing Harness installation was not modified;
  • which CLI now serves this ledger, spelled out as a command they can run — whatever the probe at the top of this step settled on: their existing ha if it accepted --version, otherwise the newly installed one or the durable checkout, in which case say that their old ha will not work against this ledger;
  • any dirty file left in the ledger tree that has no write road — name each one and what it contains, because a later git checkout there would erase it silently;
  • never git commit inside the ledger directory, and what to do if they already have (preserve the discrepancy and use the receipt/reconcile flow above);
  • the ledger is a git repository of its own, and the project must never track it;
  • the migration daemon lives under the migration HARNESS_DAEMON_USER_ROOT and can be removed with the work directory;
  • the previous ledger's git history is not carried forward — this migration rebuilds the ledger from its events, and the old history remains in the step 2 archive and in the harness.pre-migration-* directory beside the new one;
  • that directory is theirs to delete once they are satisfied, and nothing in the migration depends on it any more — give them the path.
Report the old runtime directory — do not delete it

The source repository has a .harness/ directory beside the harness/ ledger. It is git-ignored runtime state and the importer never read it.

It is no longer purely old state once you have landed a local placement. daemon repo register --root "$LEDGER_HOME" puts the new runtime directory at $LEDGER_HOME/.harness — and in the default local placement $LEDGER_HOME is $ARCHIVE_SOURCE, so from the moment you registered, the current generation's live cache/ sits in the same directory as the previous generation's dead write-journal/, task-holders/ and script-runs/. Never describe this directory as safe to remove wholesale, and never hand the user an rm -rf of it; that command would delete the working state of the ledger you just landed. The narrow staging-only reclaim below stays safe under both placements because it names paths the current generation does not use — that is why it names them exactly. Under a central placement the two generations do live apart, but the distinction is not worth relying on: the narrow command is correct either way.

On a long-lived repository the directory can be very large: previous generations wrote a full copy of the ledger into a staging/ directory on every script or preset run and never removed them, so .harness/ can reach hundreds of gigabytes while the ledger itself is a fraction of that.

Measure before quoting any of that. The staging directories are the usual culprit but not a given — on a repository where someone has already reclaimed them, the cleanup below frees nothing and cache/ and write-journal/ are what is left. Report the actual numbers:

bash
du -sh "$ARCHIVE_SOURCE/.harness" "$ARCHIVE_SOURCE/harness"
du -sh "$ARCHIVE_SOURCE"/.harness/* 2>/dev/null | sort -rh | head

Do not delete it, and do not offer to. Report it and hand them the command:

bash
# after you are satisfied with the new repository — reclaims the ledger copies,
# keeps each run's context/result/stderr metadata
chmod -R u+w "$ARCHIVE_SOURCE"/.harness/script-runs/*/staging \
             "$ARCHIVE_SOURCE"/.harness/evidence/presets/*/*/staging 2>/dev/null
rm -rf "$ARCHIVE_SOURCE"/.harness/script-runs/*/staging \
       "$ARCHIVE_SOURCE"/.harness/evidence/presets/*/*/staging

Name those two paths exactly. A shorter .harness/*/staging also matches .harness/preset-runs/staging, which the current generation uses while a run is in flight.

The chmod is not optional. Some staged copies contain read-only archives — benchmark fixtures are stored dr-xr-xr-x/-r--r--r-- on purpose — and rm stops on them with Permission denied, leaving the reclaim silently partial. Only the copies are made writable; the originals under harness/ keep their protection.

Deleting is theirs to decide, for two reasons worth saying out loud: the moment right after a one-shot migration is exactly when the old installation is most likely to be needed again, and this skill has no way to restore what it removes. The same reasoning already governs why it does not stop their daemon.

Tell them the current generation does not accumulate this way — run staging now holds a preset package rather than the ledger, and is removed on both the success and failure paths.

Done when

  • Apply exited zero and the final dry-run had Reconciliation: PASS.
  • All five entity reconciliation rows pass and satisfy the reported source/target explained-difference equality.
  • Oracle basis is recorded; every ACCEPT observation has been reviewed.
  • Authored coverage has no required row.
  • Every conflict appears as an explicit resolved: row.
  • Every merge recorded in the step 5 table has been applied to the destination.
  • Every legacy preset has a validated v3 replacement installed.
  • source-before equals source-after.
  • The ledger has its own .git at its landed location, the project tracks no ledger file, and task create succeeded there through the serving daemon.
  • The ledger's own working tree holds nothing that had a write road and was not taken through it. Every prose file has gone through doc sync --submit; any file left dirty is one the CLI cannot accept, and it is named in the hand-over.
  • The ledger's git repository has been packed (git gc) and fsck is clean.
  • The user has been handed the exact command that now drives this ledger.
  • The superseded harness.pre-migration-* directory still exists, and the user has been told where it is and that deleting it is theirs to decide.

Known rough edges

  • Connecting to a foreign daemon reports missing_vertical with "origin":"daemon". The hint is readable text about an unavailable vertical, but it still does not name the real cause — a missing HARNESS_DAEMON_USER_ROOT. Route on the code and origin, not the wording.
  • --daemon-mode direct does not support init — it rejects with unsupported_command. Isolation is done with the user root, not with direct mode.
  • daemon repo register does not isolate a ledger from the project repository around it; only ha init does. That is why step 9 writes the ignore rules and runs git rm --cached by hand rather than trusting registration to do it.
  • The writer lock is created as a sibling of the root you register — for a local placement that is <parent-of-project>/<project-name>.harness-anything-writer.lock. It is outside the project, so it does not dirty the working tree, but it is visible next to the project directory while the daemon holds the ledger.
  • @harness-anything/cli is not on a registry — npm view returns 404 — so there is nothing to npm install by name. It is still installable: a fresh clone has no dist/, but npm run build produces one and npm install -g . puts a working ha on PATH. Many machines already have exactly that. Step 9 probes for it before assuming otherwise.
  • The CLI version string does not identify a generation. Both the current source checkout and a current-generation global install print 0.0.1. What discriminates is whether --version is accepted at all: the previous generation rejects it with unknown_option and a nonzero exit. Never compare version numbers to decide which build you are talking to.
  • Roster changes belong to identity and access management. ha people exposes execution delegation and revocation; add/remove are retired. Do not use doc sync to replace roster authorization. Resolve migration roster conflicts through the importer’s explicit choices in step 5.
  • doc status and doc sync --dry-run under-report dirty files. With no --path they list only prose files and route-blocked paths, so a dirty non-prose authored file is silently absent. Take the list from git -C harness status --porcelain instead. Naming such a file with --path surfaces a path is not canonical prose block rather than submitting it — there is no road for non-prose authored content today.
  • migrate import prints nothing until it finishes — no progress, no heartbeat. Run it detached (step 8), and have the wrapper record its exit code to a file: once the process is reaped, the exit status cannot be recovered from another shell, and "apply exited zero" is a sign-off condition. A half-imported target cannot be repaired, only discarded.
  • Do not substitute Git edits for accepted ledger commands. Inspect an accepted operation’s publication with the receipt/reconcile flow in step 9; preserve concurrent edits and repair the reported follower discrepancy.
  • Read the actual projection status after import. A normal ready response is a result, not a reason to rebuild. Pending status and revision gaps must be reported from current output; historical batch sizes and per-event timings are not current performance guarantees.

© FairladyZ625, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file in skills/harness-migration of FairladyZ625/harness-anything.

  • SKILL.md
  • agents/openai.yaml

Open the folder on GitHubat commit c805ddf

Compare with similar skills

Harness Migration next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Harness Migration compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Harness Migration this skillFairladyZ625/harness-anything226—~14kAutomated safety check: PassAGPL-3.0
Reversible MigrationJuliusBrussee/caveman111k1 repos~196Automated safety check: PassApache-2.0
Database Migrationsaffaan-m/ECC276k4 repos~3kAutomated safety check: PassMIT
Database Migrationsaffaan-m/ECC276k1 repos~2.4kAutomated safety check: PassMIT
Safe Database Migration Patternsaffaan-m/ECC276k—~3.3kAutomated safety check: PassMIT
Migrate Createruvnet/ruflo74k—~583Automated safety check: NotesMIT

Similar skills

  • Reversible Migration

    JuliusBrussee/caveman

    Implement reversible compatibility-safe transitions. Use for schema, data, API, protocol, configuration, or dependency migrations requiring rollback and…

    111k GitHub starsUsed in 1 repo~196 tokens
    DevelopmentAuto-check passed
  • Safe, reversible database migration patterns: forward-only production changes, expand-contract zero-downtime renames, concurrent indexes, batched backfills, and per-tool workflows for PostgreSQL…

    276k GitHub starsUsed in 4 repos~3k tokens
    DatabasesAuto-check passed
  • Şema değişiklikleri, veri migration'ları, rollback'ler ve PostgreSQL, MySQL ve yaygın ORM'ler (Prisma, Drizzle, Django, TypeORM, golang-migrate) arasında sıfır kesinti deployment'ları için…

    276k GitHub starsUsed in 1 repo~2.4k tokens
    DatabasesAuto-check passed
  • Rules and examples for safe, reversible schema changes in production: zero-downtime column and index changes, large data backfills and ORM migration workflows.

    276k GitHub stars~3.3k tokensUpdated today
    DatabasesAuto-check passed
  • Migrate Create

    ruvnet/ruflo

    Create a new sequentially numbered database migration with up/down SQL files

    74k GitHub stars~583 tokensUpdated today
    DatabasesAuto-check: notes
  • Migration

    kortix-ai/suna

    How to change the database schema in this repo. An agent skill from kortix-ai/suna.

    20k GitHub stars~1.2k tokensUpdated today
    DatabasesAuto-check passed

More from FairladyZ625/harness-anything

All 12 skills in this repo
  • Harness Contributing

    FairladyZ625/harness-anything

    Contribute a public change to Harness Anything from a GitHub issue through an isolated worktree, scoped tests, manifest-selected gates, a complete bilingual PR body, review triage, and maintainer…

    226 GitHub stars~4.4k tokensUpdated today
    Auto-check passed
  • Gate Customization

    FairladyZ625/harness-anything

    在目标仓库 harness.yaml 的 settings.gates 里声明与定制任务完成门:local-command 本地命令见证、manual-attest 人工签字、github-actions CI 见证、none 显式免除,治理字段 mandatorySignoff(双控签注)与 allowOverride(owner 特批),以及…

    226 GitHub stars~3.5k tokensUpdated today
    Auto-check passed
  • Preset Creator

    FairladyZ625/harness-anything

    Create, review, or update Harness Anything preset-manifest/v3 packages, including profiles, completion gates, template selections, output shapes, script entrypoints, capability declarations, package…

    226 GitHub stars~1.9k tokensUpdated today
    Auto-check passed
  • Preset Trigger

    FairladyZ625/harness-anything

    Start Harness Anything task creation by choosing a software/coding preset first.

    226 GitHub stars~928 tokensUpdated today
    Auto-check passed
  • Task Explainer HTML

    FairladyZ625/harness-anything

    产出与增量维护任务包中的活解释页 artifacts/explainer.html;按物化模板中的唯一权威注释编写,并消费派工注入的 task-context 因果上下文。Use when a task-bound dispatch requires creating, updating, or freezing the task's living explainer page, or…

    226 GitHub stars~689 tokensUpdated today
    Auto-check passed
  • Vertical Creator

    FairladyZ625/harness-anything

    Create, review, or update Harness Anything verticals. An agent skill from FairladyZ625/harness-anything.

    226 GitHub stars~1.7k tokensUpdated today
    Auto-check passed

Questions about Harness Migration

What does Harness Migration do?

Diagnose and migrate a Harness Anything ledger from a machine that does not have the current Harness installed. Harness Migration is an agent skill from FairladyZ625/harness-anything. Diagnose and migrate a Harness Anything ledger from a machine that does not have the current Harness installed.

When should I use Harness Migration?

Harness Migration fits situations like: A projects harness/ ledger predates the current generation; daemon attach fails because a current ledger has pre-S4 doc cuts; A user asks to upgrade; repair an old Harness ledger.

How do I install Harness Migration in Claude Code?

Run `npx skills add FairladyZ625/harness-anything --skill harness-migration -a claude-code`. Or copy the skill folder (skills/harness-migration in FairladyZ625/harness-anything) into .claude/skills/harness-migration in your project. Claude Code loads it when a task matches its description.

How do I install Harness Migration in Codex?

Run `npx skills add FairladyZ625/harness-anything --skill harness-migration -a codex`. Or copy the skill folder (skills/harness-migration in FairladyZ625/harness-anything) into .agents/skills/harness-migration in your project. Codex loads it when a task matches its description.

Can I use Harness Migration in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add FairladyZ625/harness-anything --skill harness-migration -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/harness-migration, .gemini/skills/harness-migration, .github/skills/harness-migration and .opencode/skills/harness-migration in your project.

What does Harness Migration need to run?

Going by SKILL.md and its folder, Harness Migration needs the command-line tools its instructions call (git, npm, node and sh). Our summary lists: Node.js.

Does Harness Migration access the network?

SKILL.md contains no URLs. Its commands use git and npm, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Harness Migration safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Harness Migration use?

Harness Migration is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Harness Migration use?

About 14k tokens (SKILL.md is roughly 57k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Harness Migration?

Skills that share tags, products or a category with Harness Migration: Reversible Migration (JuliusBrussee/caveman, 111k stars), Database Migrations (affaan-m/ECC, 276k stars), Database Migrations (affaan-m/ECC, 276k stars) and Safe Database Migration Patterns (affaan-m/ECC, 276k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Harness Migration?

FairladyZ625 (a GitHub user) maintains it in FairladyZ625/harness-anything, which has 226 GitHub stars. The repository holds 12 skills in this directory. The repository was last updated on October 10, 2026.

Source: FairladyZ625/harness-anything on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.