Plan — RunIndex port

Mission: runindex-feature-runs-port · fixes #5390, #5389 · refs #2624

Architecture

Introduce src/runtime/next/run_index.py — the RunIndex port, the single canonical reader/writer of .kittify/runtime/feature-runs.json. It owns:

invoking repo_root, expanding ${VAR} via kernel.env_expand.expand_raw_template; refuse (raise RunDirOutsideRepoError) any resolved path escaping repo_root;

feature-runs.json.lock sidecar: acquire → re-read fresh → canonicalize/merge → re-check same mission → atomic replace.

  • the filename/path constants (FEATURE_RUNS_FILENAME, _feature_runs_path, lock-path);
  • token serialization on write: run_dir → repo-relative token (.as_posix());
  • read-time resolution + containment: token → absolute Path, resolved against the
  • locked read-modify-write via kernel.locks.machine_file_lock against a dedicated

runtime_bridge_io.py keeps its function names (load_feature_runs, save_feature_runs, get_or_start_run, _existing_run_ref, _resolve_run_dir_for_mission, _require_run_state) but their bodies delegate to the port so the compat/monkeypatch seams and the existing call graph are preserved. runtime_bridge.py delegates stay thin. state/contract.py imports the filename constant from the port (keeps the literal single-homed).

Layering: port imports kernel.locks, kernel.env_expand, kernel.atomic/specify_cli.core.atomic (already used by the seam), stdlib. No new runtime→specify_cli edges beyond what the seam already declares.

Token contract

(always .kittify/runtime/runs/<run_id> since run_store is deterministic).

containment-check against repo_root; if inside, use it (healable); if outside, refuse. If relative, resolve repo_root / token, then containment-check.

  • Canonical token for any run: Path(run_dir).relative_to(repo_root).as_posix()
  • Read resolution: expand_raw_template(token) → if absolute, that's a legacy/foreign value:
  • Heal rewrites any absolute entry to .kittify/runtime/runs/<basename> (canonical relative).

Work packages (single implementer/reviewer, sequential — direct on branch)

entry-key literals (run_id/run_dir/mission_type/mission_key/mission_id/ mission_slug) and the runtime/runs path segments to named constants in the seam; no functional change. Focused characterization test stays green.

through the real next entry point — copy, move, two-process concurrent start — RED on pre-fix code. Plus a port-level never-persists-absolute test (RED pre-fix).

≤15 complexity by extracting _register_new_run/_reuse_existing_entry helpers. GREEN.

describe_leaks) + cli/commands/_run_index_doctor.py (register(app)); tests.

(b) never-persist-absolute behavioural gate.

  • WP00 — Campsite (distinct preceding step, behaviour-preserving): hoist repeated
  • WP01 — Red-first regression tests (ATDD): issue-pinned @pytest.mark.regression tests
  • WP02 — RunIndex port + rewire: implement the port; delegate the seam functions; keep
  • WP03 — Heal migration + doctor: m_*_heal_run_index_paths (BaseMigration,
  • WP04 — Empty-allowlist gates: (a) single-reader literal gate + self-mutation test;

Testing (mission scope — NO heavy suites)

make test-fast + tests/runtime/, tests/next/ (touched files), the migration/doctor test files, and the SPECIFIC new gate files. Record commands + counts in the PR.

Risk / point-cuts for the adversarial squad

the run_store used; verify owned runs still resolve.

token separators.

  • Owned-checkout arm (fresh from #5445): the token must resolve against the same repo_root
  • Legacy absolute-inside-repo (in-place old project) must keep working (not refused).
  • Lock path must be a dedicated sidecar (never truncate the payload — kernel lock G1).
  • Containment must use resolved real paths (symlink/.. safe) and not break Windows posix