# AI OS Foundation Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /run-plan (harness engine) for Waves 1–4. Wave 0 and the Post-run procedures are MAIN-SESSION ONLY — never dispatch them (see "Execution split"). Steps use checkbox (`- [ ]`) syntax.

**Goal:** overdeck becomes the AI OS: `modules/` namespace + contract, history-preserving imports of monitor/security/systray/harness, workstation config module, `deckctl`, harness artifact-install, main-session cutover.

**Architecture:** Spec = `docs/specs/2026-07-28-ai-os-foundation-design.md` (advisor-approved after 2 rounds — read it before any task). Engine tasks author ONLY new files (or modify already-imported files); all git-topology surgery (import merges) and all service cutover run in the MAIN session, scripted and fail-closed.

**Tech Stack:** bash (fail-closed `set -euo pipefail`), jq, git-filter-repo, gitleaks, plain-bash test scripts under `tests/os/` (bats is NOT installed — do not use it), systemd user units.

---

## Execution split (read first)

- **Wave 0 (main session, BEFORE engine launch):** park WIP + bundles, author+test `bin/import-module.sh`, run the four import merges onto `feat/ai-os-foundation`, verify gate0 surface. The engine's delivery path may squash/flatten commits — an import merge landed through it would DESTROY imported history; imports therefore never run as engine tasks.
- **Waves 1–4 (engine, base_branch = `feat/ai-os-foundation`):** all file-authoring tasks below.
- **Post-run (main session):** workstation `sync pull` population, `deckctl install harness`, landing via delivery controller, per-module `cutover.sh`, archive. cutover.sh is NEVER an engine task (spec: Cutover).

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 0 | main-session procedure P0 (below) | source repos, feat branch, bin/import-module.sh | main session, sequential |
| 1 | T1, T2, T3 | spec/deck-module.schema.json · tools/ref-sweep.sh+inventory · modules/monitor/install.sh | ✅ no overlap |
| 2 | T4, T5, T6, T7, T8 | bin/deckctl+lib/deckctl/{doctor,status}.sh · modules/harness/* · modules/systray/* · modules/monitor/{deck.module.json,health.sh,bootstrap.sh} · modules/security/* | ✅ no overlap (T7 disjoint from T3's install.sh) |
| 3 | T9, T10, T11 | lib/deckctl/install.sh · lib/deckctl/sync.sh+modules/workstation/** · lib/deckctl/cutover-lib.sh+modules/*/cutover.sh | ✅ no overlap |
| 4 | T12 | lib/deckctl/bootstrap.sh | single task |

Dependencies: T4–T8 ← T1. T9,T10 ← T4. T11 ← T2,T5,T6,T7,T8. T12 ← T9,T10.

## Decision enumeration

No `gated` records: the engine waves contain zero irreversible/user-preference operations (all additive file authoring on a feature branch). Irreversible-adjacent steps (service stops, archive renames) live in main-session cutover with built-in rollback + pre-migration bundles, per spec. Publication is delivery-controller-internal (GIT_FATIGUE §12) — no user gate.

---

## Wave 0 — Main-session procedure P0 (pre-engine)

- [ ] P0.1 Verify gate0 surface derivation (root package scripts vs tree walk) — if tree-walk, scope to root packages before any merge; record finding in session file as `session_memory`.
- [ ] P0.2 Author `bin/import-module.sh` + `tests/os/import-module.test.sh` (contract below), test green, commit to `feat/ai-os-foundation` (cut from `origin/main`).
- [ ] P0.3 Per source repo (order: monitor → security → systray → harness): park WIP (harness: `git switch -c wip/pre-ai-os-migration` from the detached HEAD; monitor: `git init`+commit first; systray: fold into existing wip branch), push where remote exists, `git bundle create ~/.local/state/overdeck/bundles/<name>-pre-ai-os.bundle --all`.
- [ ] P0.4 Run `bin/import-module.sh <src> <name>` per module → verify each merge commit + `git log modules/<name>` shows imported history; gitleaks clean (a hit BLOCKS until scrubbed).
- [ ] P0.5 Push `feat/ai-os-foundation`; update session-file `meta.base_branch` to `feat/ai-os-foundation`; launch `/run-plan ai-os-foundation`.

**Contract — `bin/import-module.sh <source-path> <module-name>`:**
- Fail-closed; asserts: source repo clean (WIP parked), bundle exists, scratch clone (never the live checkout), overdeck CWD on `feat/ai-os-foundation`.
- filter-repo on the scratch clone: strip per-module junk paths (spec "Current state" table + Superseded UI DROPs: harness `web/`, security `apps/web`) then `--to-subdirectory-filter modules/<name>`.
- gitleaks over the FULL filtered history; any finding → abort, print findings path.
- `git fetch <scratch> && git merge --allow-unrelated-histories --no-ff -m "import: <name> (<src> @ <sha>)"`.
- Test: fixture repo built in mktemp (2 commits + a junk path + a planted fake secret) → junk stripped, prefix correct, secret aborts, merge preserves both commits.

---

## Wave 1

### Task 1: Module contract schema

**Wave:** 1 · **Blocks:** T4–T8 · **Blocked by:** —
**Files:** Create `spec/deck-module.schema.json`; Test `tests/os/schema.test.sh`
**Contract:** JSON Schema (draft-07) for `deck.module.json` exactly as the spec "Module contract" section pins: required `schema_version` (const 1), `name` (kebab), `kind` enum `service|cli|config|library`, optional `services[]`, `bootstrap`, `health`, `install{mode: in-place|artifact, needs_git: bool}`, `state_dirs[]`. `additionalProperties: false`.
**Behavior:** validation via `jq`-based check script embedded in test (no new deps); valid example passes, each of: missing schema_version / bad kind / extra key → fails.
**Acceptance:** `tests/os/schema.test.sh` → PASS
- [ ] Write failing test with valid+invalid fixtures · [ ] write schema · [ ] test green · [ ] commit `spec/deck-module.schema.json tests/os/schema.test.sh`

### Task 2: ref-sweep inventory

**Wave:** 1 · **Blocks:** T11 · **Blocked by:** —
**Files:** Create `tools/ref-sweep.sh`, `tools/ref-inventory.json`; Test `tests/os/ref-sweep.test.sh`
**Contract:** `tools/ref-sweep.sh [--assert]` — machine-wide deterministic sweep for old-path references (`~/Projects/mega-plan-harness`, `~/Projects/systray-ai`, `~/Projects/security-gate`, `~/Projects/system-monitor`) across: `~/.claude` (skills/hooks/lib/bin/workflows/commands/settings*), `~/.codex`, `~/.cursor`, `~/.config/opencode`, `~/.gemini`, `~/.kiro`, `~/.pi`, `~/.antigravity`, shell rc files, `~/.config/systemd/user`, crontab, `~/Projects/*/.claude`. Excludes caches/history/logs/node_modules. Emits sorted JSON `[{file, line, old_path}]` to stdout; default mode rewrites `tools/ref-inventory.json`; `--assert` mode: every inventoried file:line's `old_path` must resolve (symlink or dir exists) — nonzero on any dangler, prints them.
**Behavior:** deterministic ordering (sort); unreadable dir → error not skip; empty result valid.
**Acceptance:** `tests/os/ref-sweep.test.sh` (sandbox HOME fixture: planted ref found; cache path excluded; --assert fails on dangling, passes after symlink) → PASS
- [ ] Failing test · [ ] implement · [ ] green · [ ] run real sweep, commit inventory + script + test

### Task 3: monitor install.sh --dry-run

**Wave:** 1 · **Blocks:** — · **Blocked by:** — (imports landed in Wave 0)
**Files:** Modify `modules/monitor/install.sh`; Test `tests/os/monitor-dryrun.test.sh`
**Contract:** `install.sh --dry-run` prints each planned action (`DRY-RUN: <verb> <target>` lines) and exits 0 having mutated NOTHING (no file/package/systemd/netdata/grafana changes). Unknown flags still error.
**Behavior:** every mutating branch in the script routes through a `run()` wrapper honoring `DRY_RUN`; `bash -n` stays clean.
**Acceptance:** `tests/os/monitor-dryrun.test.sh` (runs --dry-run under sandbox HOME, asserts exit 0 + ≥1 `DRY-RUN:` line + no writes outside mktemp) → PASS
- [ ] Failing test · [ ] implement · [ ] green · [ ] commit `modules/monitor/install.sh tests/os/monitor-dryrun.test.sh`

## Wave 2

### Task 4: deckctl core (dispatcher + doctor + status)

**Wave:** 2 · **Blocks:** T9, T10 · **Blocked by:** T1
**Files:** Create `bin/deckctl`, `lib/deckctl/doctor.sh`, `lib/deckctl/status.sh`; Test `tests/os/deckctl-core.test.sh`
**Contract:** `bin/deckctl <bootstrap|doctor|status|sync|install> [...]` — dispatcher sources `lib/deckctl/<cmd>.sh` and calls `cmd_<name> "$@"`; unknown cmd → usage, exit 2. `doctor`: validates every `modules/*/deck.module.json` against `spec/deck-module.schema.json` (jq check from T1), asserts referenced bootstrap/health/services files exist+executable, workstation manifests contain no deny-listed path, reports dangling symlinks in `~/.local/opt/overdeck` and `~/.local/state/overdeck`; any finding → exit 1 with per-finding line. `status`: per module — manifest kind, unit ActiveState (`systemctl --user`), health.sh exit; table to stdout; exit 0 always (status reports, doctor judges).
**Behavior:** fail-closed; missing schema/module dir → error, never guess; no color codes (parseable).
**Acceptance:** `tests/os/deckctl-core.test.sh` (fixture module valid→doctor 0; broken manifest→doctor 1 naming it; unknown subcommand→2) → PASS
- [ ] Failing test · [ ] implement · [ ] green · [ ] commit `bin/deckctl lib/deckctl/doctor.sh lib/deckctl/status.sh tests/os/deckctl-core.test.sh`

### Task 5: harness module manifest + units

**Wave:** 2 · **Blocks:** T11 · **Blocked by:** T1
**Files:** Create `modules/harness/deck.module.json`, `modules/harness/bootstrap.sh`, `modules/harness/health.sh`; Modify `modules/harness/systemd/harnessd.service`, `harnessd-daily.service`, `harnessd-daily.timer`
**Contract:** manifest: `kind:"service"`, `install:{mode:"artifact",needs_git:true}`, `services:["systemd/harnessd.service","systemd/harnessd-daily.service","systemd/harnessd-daily.timer"]`, `state_dirs:["runstate","tmp"]`. Units: every `ExecStart`/`WorkingDirectory`/path env → `%h/.local/opt/overdeck/harness/current/...` (NEVER the checkout, NEVER `%h/Projects/mega-plan-harness`); preserve all other directives verbatim. `HARNESS_HOME` untouched (spec). health.sh: artifact present + `current/bin/runplan --help` exits 0, else exit 1 with reason. bootstrap.sh: idempotent dep install for the harness (node deps per its own docs), no other module.
**Behavior:** health must not require a running daemon (checks artifact runnability, unit file linked).
**Acceptance:** `bash -n` all three scripts + `jq -e` manifest validates against schema + `grep -c 'Projects/mega-plan-harness' modules/harness/systemd/*` == 0
- [ ] Implement · [ ] acceptance green · [ ] commit listed paths

### Task 6: systray module manifest + unit

**Wave:** 2 · **Blocks:** T11 · **Blocked by:** T1
**Files:** Create `modules/systray/deck.module.json`, `modules/systray/bootstrap.sh`, `modules/systray/health.sh`, `modules/systray/systemd/systray-ai.service` (relocate/derive from the unit currently installed or in `modules/systray/packaging/` — read the repo)
**Contract:** manifest `kind:"service"`, `install:{mode:"in-place",needs_git:false}`, `services:["systemd/systray-ai.service"]`, `state_dirs:["runstate","tmp"]`. Unit ExecStart → `%h/Projects/overdeck/modules/systray/...`. health.sh: `python3 -m compileall -q modules/systray` exit 0 + unit file linked. bootstrap.sh: python deps per its repo docs (uv/pip — follow what the repo already uses).
**Acceptance:** `bash -n` scripts + manifest validates + `systemd-analyze verify --user modules/systray/systemd/systray-ai.service` (or `grep` ExecStart path if verify unavailable) clean
- [ ] Implement · [ ] acceptance green · [ ] commit listed paths

### Task 7: monitor module manifest

**Wave:** 2 · **Blocks:** T11 · **Blocked by:** T1
**Files:** Create `modules/monitor/deck.module.json`, `modules/monitor/health.sh`, `modules/monitor/bootstrap.sh`
**Contract:** manifest `kind:"config"`, `install:{mode:"in-place",needs_git:false}`, no `services` (netdata/grafana are system-level, owned by install.sh). health.sh: `bash -n` every `*.sh` in the module + `install.sh --dry-run` exits 0. bootstrap.sh: no-op with explanatory echo (install.sh is the installer; bootstrap MUST NOT auto-run it).
**Acceptance:** manifest validates + `modules/monitor/health.sh` exits 0
- [ ] Implement · [ ] green · [ ] commit listed paths

### Task 8: security module manifest

**Wave:** 2 · **Blocks:** T11 · **Blocked by:** T1
**Files:** Create `modules/security/deck.module.json`, `modules/security/health.sh`, `modules/security/bootstrap.sh`
**Contract:** manifest `kind:"cli"`, `install:{mode:"in-place",needs_git:false}`. health.sh: runs `modules/security/check.sh`, propagates exit. bootstrap.sh: python deps per its repo (read it).
**Acceptance:** manifest validates + `modules/security/health.sh` exits 0
- [ ] Implement · [ ] green · [ ] commit listed paths

## Wave 3

### Task 9: deckctl install (artifact)

**Wave:** 3 · **Blocks:** T12 · **Blocked by:** T4
**Files:** Create `lib/deckctl/install.sh`; Test `tests/os/deckctl-install.test.sh`
**Contract:** `deckctl install <module> [<commit-ish>] [--prune]` — only for `install.mode: artifact` modules (else error). From the LOCAL overdeck checkout: `git subtree split -P modules/<name> <commit-ish>` (default HEAD) → cached split ref → clone split into `~/.local/opt/overdeck/<name>/<rev>/` (rev = split sha12) → create `state_dirs` symlinks into `~/.local/state/overdeck/<name>/` (mkdir -p) → run module health.sh against the new tree → atomically flip `current` symlink (`ln -sfn` on a TEMP name + `mv -T`). Failure at any step → `current` untouched, partial dir removed. `--prune`: delete artifact dirs except `current` target + 1 predecessor; NEVER follows/dereferences symlinks (rm on the dir path only).
**Behavior:** idempotent (re-install same rev = no-op with notice); never touches origin; never runs inside the artifact.
**Acceptance:** `tests/os/deckctl-install.test.sh` (fixture module repo: install → `<opt>/current/bin/…` executable + state symlink correct; failed-health rev → current unchanged; prune keeps 2) → PASS
- [ ] Failing test · [ ] implement · [ ] green · [ ] commit `lib/deckctl/install.sh tests/os/deckctl-install.test.sh`

### Task 10: workstation module + deckctl sync

**Wave:** 3 · **Blocks:** T12 · **Blocked by:** T4
**Files:** Create `modules/workstation/deck.module.json` (`kind:"config"`, in-place), `modules/workstation/manifest/<agent>.json` for: claude, codex, cursor-agent, opencode, gemini, kiro, pi, antigravity; `modules/workstation/deny.list`; `lib/deckctl/sync.sh`; Test `tests/os/deckctl-sync.test.sh`
**Contract:** per-agent manifest `{ "home": "~/.claude", "entries": [{"path":"skills","strategy":"symlink"}, ...] }`. Strategies: `symlink` (repo→home symlink; foreign non-symlink at target → backup to `~/.local/state/overdeck/backups/<ts>/` then replace), `copy` (pull: home→repo; apply: repo→home; diff: show drift). Claude manifest entries exactly: skills, hooks, agents, commands, output-styles, lib, bin, workflows, CLAUDE.md, RTK.md, keybindings.json, mcp.json (all symlink) + settings.json (copy). Other agents: START with config-file entries visible in their dirs (implementer inspects each `~/.<agent>` READ-ONLY, picks config-not-cache-not-auth; when uncertain → leave OUT + `TODO-review` note in manifest description field — under-sync is safe, over-sync leaks). `deny.list`: gitignore-style patterns — credentials, tokens, `*.key`, `*.pem`, auth*, `settings.local.json`, history*, sessions*, `*.sqlite*`, caches. `sync pull|apply|diff [agent]`: pull refuses (exit 1, names path) any entry matching deny.list or any pulled file whose content trips a secret regex set (gitleaks `detect --no-git` over the staged copy); apply is idempotent; diff exit 0 clean / 1 drift.
**Behavior:** all paths under sandbox-able `$HOME`; never dereference symlink loops; `sync` without agent = all agents.
**Acceptance:** `tests/os/deckctl-sync.test.sh` (sandbox HOME round-trip: pull→apply→diff clean; planted `credentials.json` → pull refuses; foreign file backed up on apply) → PASS
- [ ] Failing test · [ ] implement sync.sh · [ ] author manifests (read-only inspection) · [ ] green · [ ] commit listed paths

### Task 11: cutover scripts

**Wave:** 3 · **Blocks:** — · **Blocked by:** T2, T5, T6, T7, T8
**Files:** Create `lib/deckctl/cutover-lib.sh`, `modules/{harness,systray,monitor,security}/cutover.sh`; Test `tests/os/cutover.test.sh`
**Contract:** each `cutover.sh` = thin wrapper: `source lib/deckctl/cutover-lib.sh; cutover_run <name> <old-path> <target>` where target = `~/.local/opt/overdeck/harness/current` (harness) | module dir (others). `cutover_run` phases EXACTLY per spec Cutover section: (1) preflight: ref-inventory current (`tools/ref-sweep.sh` diff empty), artifact healthy when mode=artifact, full state backup to `~/.local/state/overdeck/backups/<ts>/`; (2) stop module units, assert no surviving processes + no live `.lock`/open SQLite handles (`fuser`), else abort; (3) rsync in-repo `state_dirs` → `~/.local/state/overdeck/<name>/`, verify size+count parity; `~/.harness` inventoried, untouched; (4) harness only: reap worktrees via `~/.claude/bin/wt-reaper.sh` + `git worktree prune`, assert zero remain; (5) rename old checkout → `~/Projects/archive/<name>-pre-ai-os` THEN `ln -s` target at vacated path, re-link units, `systemctl --user daemon-reload`, start, health; (6) `tools/ref-sweep.sh --assert`. ANY failure → rollback: restore symlink/rename, restore state from phase-1 backup, restart from old path, exit nonzero with phase named. Runs from the live checkout; refuses to run under the engine (`[ -n "${HARNESS_RUN:-}" ] && abort` — check the engine's actual env marker in `modules/harness/bin/runplan`, use what exists).
**Behavior:** idempotent (already-cut module → notice, exit 0); `--rollback` subcommand re-runs the restore path standalone.
**Acceptance:** `tests/os/cutover.test.sh` (sandbox HOME + fixture "old repo" with fake unit/state: full run relocates state, symlinks, relinks; induced phase-3 failure → rollback restores byte-identical state and old path) → PASS
- [ ] Failing test · [ ] implement lib · [ ] four wrappers · [ ] green · [ ] commit listed paths

## Wave 4

### Task 12: deckctl bootstrap

**Wave:** 4 · **Blocks:** — · **Blocked by:** T9, T10
**Files:** Create `lib/deckctl/bootstrap.sh`; Test `tests/os/deckctl-bootstrap.test.sh`
**Contract:** `deckctl bootstrap [--module <name>]` — full order: doctor (must pass) → per module: bootstrap.sh → `sync apply` (workstation) → `install` (artifact modules) → link+enable units → health; summary table; any failure → stop at that module, report, exit 1 (already-completed modules stay done — idempotent re-run continues).
**Behavior:** never re-runs a green step blindly (checks state first); `--module` scopes to one.
**Acceptance:** `tests/os/deckctl-bootstrap.test.sh` (sandbox HOME + 2 fixture modules: cold bootstrap → all green; induced failure in module 2 → module 1 intact, exit 1; re-run after fix → completes) → PASS
- [ ] Failing test · [ ] implement · [ ] green · [ ] commit `lib/deckctl/bootstrap.sh tests/os/deckctl-bootstrap.test.sh`

---

## Post-run — Main-session procedures (after engine exits)

- [x] P1 `deckctl sync pull` (populate modules/workstation from this machine), review pulled set against deny.list, commit on the feature branch. Five agents synced; claude/codex/opencode BLOCKED on live plaintext credentials on disk — sync stays fail-closed until they are rotated to env references.
- [x] P2 `deckctl install harness` from the feature branch; acceptance: artifact entrypoint and `src/runner.js` parse (`runplan` has no `--help` — it reads argv[1] as a plan slug, so the original criterion was unrunnable) + `runplan preflight` smoke from the artifact.
- [x] P3 Land the feature branch via the project's delivery mechanism (delivery controller / ship.sh — internal, no user gate).
- [x] P4 Re-install harness artifact from landed main; then per module (monitor → security → systray → harness): run `modules/<name>/cutover.sh`; after all four: `tools/ref-sweep.sh --assert` green.
- [x] P5 Verify services green (`deckctl status`), overdeck deck loads, systray tray up; record retention note (archive 30 days; bundles at `~/.local/state/overdeck/bundles/`).
