# Migrating to File-less Roadmap Mode

This guide walks operators through migrating a project's roadmap from the legacy file-backed format (`docs/roadmap.md` as the source of truth) to file-less mode, where the GitHub Issues tracker becomes the canonical source. The migration is one-shot, idempotent on re-run, and explicitly recoverable when partial failures interrupt the process. See [proposal.md](./proposal.md) for the design rationale and decisions table.

## Pre-flight checklist

Before running the migration, confirm each of the following:

- `roadmap.tracker.kind: "github"` is set in `harness.config.json` (only `github` is supported in this release).
- The `GITHUB_TOKEN` environment variable is present in the shell that will run the command, and the token has `repo` scope (writable) for the target repository.
- The repository configured at `roadmap.tracker.repo` is writable by that token (verify by, for example, manually creating and deleting a test issue).
- The working tree is clean. Strongly recommended: create a git checkpoint (`git commit` or `git stash`) before running so rollback is trivial.
- No concurrent dashboard or orchestrator activity touches the roadmap during the run. The migration assumes single-writer semantics; coordinate with your team so nobody else is calling claim/release/complete or editing `docs/roadmap.md` while the command runs.

## Dry run (always do this first)

Run a dry-run to inspect the migration plan without making any changes:

```sh
harness roadmap migrate --to=file-less --dry-run
```

Sample output:

```
DRY RUN Migration plan:
  Would create: 12
    - Alpha
    - Beta
    - ...
  Would update: 4
    - Charlie (github:o/r#42): priority,spec
    - ...
  Unchanged:    8
  Would append history: 3
  Ambiguous:    0
DRY RUN complete: 16 writes would be performed.
```

What to check:

- The `Would create` count matches the number of features in `docs/roadmap.md` that lack an `External-ID:` field.
- The `Would update` count is what you expect — see the **count semantics** note below for why this can be higher than intuition suggests.
- `Ambiguous: 0`. Any non-zero value blocks the real run; resolve each entry per the **Title-only collision** section before proceeding.

Note on count semantics: the `Would update` count includes idempotent canonical re-writes. The GitHub Issues adapter in this release does not expose raw issue bodies to the migration planner, so any feature with an existing External-ID falls into `toUpdate` by default. Re-running the migration would update those same issues again with a byte-identical body block; this is functionally a no-op but inflates the count. This is expected and harmless.

## Real run

Once dry-run output looks correct:

```sh
harness roadmap migrate --to=file-less
```

Expected stdout: the same plan summary as dry-run, followed by progress messages and a final success line of the form `Migration applied: N created, M updated, K unchanged, H history events appended.`

Expected duration: under 60 seconds for a typical project of up to 50 features, per the proposal's Performance budget (P4). Larger roadmaps scale roughly linearly with the number of `create`/`update` calls.

## Verification

After the real run, confirm:

- `harness validate` exits cleanly. The new `validateRoadmapMode` rules require `docs/roadmap.md` to be absent in file-less mode; the migration archives it for you.
- `docs/roadmap.md.archived` exists (the archived legacy file).
- `harness.config.json` contains `"mode": "file-less"` under `roadmap`.
- `harness.config.json.pre-migration` exists (byte-identical backup of the original config).
- The GitHub repository's Issues pane shows N issues with a `<!-- harness-meta` body block at the top — these are the canonical features.

## Rollback recipe

If you need to revert immediately after a successful migration:

```sh
mv docs/roadmap.md.archived docs/roadmap.md
mv harness.config.json.pre-migration harness.config.json
harness validate
```

Caveats:

- Rolling back leaves the GitHub issues in place. They can be left untouched (the file-backed sync engine will re-sync them on the next `harness sync`) or closed manually if you prefer a clean slate.
- History comments added to issues during the migration (`<!-- harness-history` blocks) remain on the issues and are not removed by rollback. They are inert in file-backed mode.

## Recovery from partial failure

If the real run aborts after some issues were already created (for example, a transient network error or a rate-limit hit halfway through), the command will exit non-zero and print a `Features created before abort` line listing each created feature with its new `externalId`. For each one, edit `docs/roadmap.md` and add an `External-ID:` field to the matching feature entry:

```md
### Alpha

- **External-ID:** github:o/r#42
- **Status:** backlog
- ...
```

Then re-run `harness roadmap migrate --to=file-less`. The migration is idempotent: the features that already exist on the tracker (identified by External-ID) are skipped at the create step, and only the remaining features are processed. No double-creates are possible as long as the External-IDs are recorded before re-running.

## Title-only collision

If `docs/roadmap.md` contains a feature with no `External-ID:` field whose title (case-insensitive) matches an existing tracker issue, the migration refuses to proceed and lists the offending feature under `Ambiguous`. This is the D-P5-E safety rule and prevents accidental double-creates when an issue already exists for a feature you forgot to record.

Remediation: locate the existing tracker issue, add its identifier as the `External-ID:` for that feature in `docs/roadmap.md`, and re-run the migration. If the existing issue truly is unrelated (rare; two features with the same name across different milestones), rename the local feature to disambiguate, then re-run.

## Concurrent invocation protection (REV-P5-S7)

The migrate command writes an advisory lockfile at `.harness/migrate.lock` containing `{pid, startedAt, hostname}` before fetching tracker state, and removes it on exit (success or failure). A second concurrent invocation refuses with `another migration is in progress (pid X on host, started ...)`.

The lockfile is _advisory_: a crash leaves a stale lock that the next run automatically recovers. Stale-lock recovery triggers when either of the following is true:

- The recorded `pid` is no longer alive.
- The recorded `startedAt` is older than 30 minutes.

If you see a refusal but no other migrate process is running, wait 30 minutes or remove `.harness/migrate.lock` manually.

## Exit codes (REV-P5-S3)

The migration command returns distinct exit codes per abort reason so CI consumers can branch on failure mode without parsing stderr text:

| Code | Name              | Meaning                                                                                                                          |
| ---- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 0    | SUCCESS           | Plan applied, dry-run completed, or already-migrated short-circuit.                                                              |
| 1    | GENERIC_FAILURE   | Any other failure: tracker write error, parse error, network error, unknown abort reason.                                        |
| 2    | AMBIGUOUS         | Title-only collision: a local feature without `External-ID` matches an existing tracker issue title. See "Title-only collision". |
| 3    | ARCHIVE_COLLISION | `docs/roadmap.md.archived` already exists. See "Archive collision".                                                              |
| 4    | CONFIG_ERROR      | Missing tracker config, missing `roadmap.tracker.repo`, missing `docs/roadmap.md`, or any pre-flight validation failure.         |
| 5    | PARTIAL_CREATE    | Some features were created before an abort; operator hand-recovery is needed. The report's `createdSoFar` lists the new IDs.     |

## JSON output (REV-P5-S2)

Pass `--format=json` to suppress the human-readable summary and emit a single JSON object on stdout. This is intended for CI consumers; the human format remains the default for interactive use.

```sh
harness roadmap migrate --to=file-less --dry-run --format=json
```

The output shape is stable: additive fields are backward-compatible; field removals are breaking. Example:

```json
{
  "ok": true,
  "mode": "dry-run",
  "exitCode": 0,
  "plan": {
    "toCreate": [{ "name": "Alpha" }],
    "toUpdate": [],
    "unchanged": [],
    "historyToAppend": [],
    "ambiguous": []
  },
  "report": {
    "mode": "dry-run",
    "created": 0,
    "updated": 0,
    "unchanged": 0,
    "historyAppended": 0,
    "archivedFrom": null,
    "archivedTo": null,
    "configBackup": null
  }
}
```

On failure the JSON includes an `error` field and `exitCode` reflects the precise classification from the exit codes table above.

## Archive collision

If `docs/roadmap.md.archived` already exists (for example, from a previous attempt at a different time), the migration refuses to overwrite it and aborts with reason `archive-collision`. This is the D-P5-D safety rule.

Remediation: move the existing archive aside and re-run:

```sh
mv docs/roadmap.md.archived docs/roadmap.md.archived.$(date +%s)
harness roadmap migrate --to=file-less
```

## What changes (semantics)

In file-less mode:

- The tracker is the source of truth for feature status, assignment, blockers, priority, and milestone. `docs/roadmap.md` no longer exists.
- Positional ordering of features (the order they appeared in the legacy file) is dropped. Features are sorted by `Priority` (descending) then by issue number (ascending) for display. This is decision **D4** in the proposal.
- Claim, release, and complete operations write directly to GitHub Issues via labels and assignee fields, with a deterministic `<!-- harness-history` comment block appended for each event.
- `harness validate` enforces that `docs/roadmap.md` is absent and that `roadmap.mode` is `"file-less"` in config.

For the full decisions table and rationale, see [proposal.md](./proposal.md).

## Post-migration QA (file-less mode)

After the migration completes successfully and `harness validate` is clean, run the following manual browser checks against the dashboard to verify the Phase 7 conflict UX surface works end-to-end against the live tracker. These checks are intentionally manual because they probe focus management, screen-reader announcements, and motion preferences that automated tests do not cover. See [`docs/knowledge/dashboard/claim-workflow.md`](../../knowledge/dashboard/claim-workflow.md) for the technical detail behind each behavior.

### 1. Two-tab claim race

Open the dashboard in two browser tabs (same user or two collaborators), navigate both to the Roadmap page, and click **Claim** on the same feature from both tabs within ~1s of each other. The losing tab should show:

- [ ] A non-disruptive toast appears (does not steal focus or block input)
- [ ] Screen reader (VoiceOver/NVDA) announces the toast within ~1s (`aria-live="polite"`)
- [ ] Toast text matches the canonical wording: `claimed by X — refresh`
- [ ] The dashboard auto-refetches roadmap state when the toast is shown (or its refresh action is invoked)
- [ ] Browser scrolls smoothly to the contested feature row
- [ ] The row gets a 2-second amber pulse highlight (`data-conflict-highlight="true"`)
- [ ] The dismiss button is keyboard-accessible (Tab to focus, Enter or Space to activate)
- [ ] Users with `prefers-reduced-motion: reduce` see a static amber outline (no animation)

### 2. Status-update conflict race

From two tabs, change the same feature's status (e.g., `backlog` → `in_progress`) within ~1s of each other. The losing tab should exhibit the same UX as the claim race above (toast, aria-live announcement, canonical wording, auto-refetch, smooth scroll, pulse highlight, keyboard-accessible dismiss, reduced-motion fallback).

### 3. Append-from-Analyze conflict

From the Analyze page, append the same feature to the roadmap twice in quick succession (or from two tabs concurrently). The duplicate-attempt tab should exhibit the same conflict UX as above.

If any of the above checks fail, capture the browser console output and file an issue. The canonical wire shape, ARIA contract, and pulse-ring timing are documented in [`docs/knowledge/dashboard/claim-workflow.md`](../../knowledge/dashboard/claim-workflow.md).
