# AI OS Foundation — design

audience: AI coding agents first. Sub-project 1 of the AI-OS program: overdeck becomes the single monorepo ("AI OS") hosting all unpublished personal dev tooling, portable across workstations.

## Goals (this sub-project)

1. `modules/` namespace in overdeck + a module contract every current/future module satisfies.
2. History-preserving import of: mega-plan-harness → `modules/harness`, systray-ai → `modules/systray`, system-monitor → `modules/monitor`, security-gate → `modules/security`.
3. `modules/workstation` — curated, secret-free source of truth for agent-CLI config (claude, codex, cursor-agent, opencode, gemini, kiro, pi, antigravity — user's "agy", `~/.antigravity` — + any found later) and machine prefs.
4. `bin/deckctl` — idempotent bootstrap/sync/status CLI. New workstation = clone + `deckctl bootstrap`.
5. Zero regression: harnessd, systray-ai, monitoring, security-gate hooks, and every reference captured by the task-0 machine-wide inventory keep resolving through and after cutover.


## Module naming (generic, user-confirmed direction)

| Source | Module | install.mode | Note |
|---|---|---|---|
| mega-plan-harness | `harness` | `artifact` | git-operates on overdeck (lands plans into it) |
| systray-ai | `systray` | `in-place` | no git ops on overdeck |
| system-monitor | `monitor` | `in-place` | install scripts only |
| security-gate | `security` | `in-place` | `check.sh` has no `rev-parse`/`show-toplevel` (verified) | bare "gate" overloaded (gate0, local-gate, slopgate) → `security` |
| Botmaster (deferred) | `bots` | — | |
| — (new) | `workstation` | `in-place` | agent-CLI config + machine prefs |

## Non-goals (deferred sub-projects, in order)

- **Botmaster**: stays deployed on Cloudflare, repo migrates later; near-term future role = communication adapter (bots announce GOLIVE progress/regressions). User: "this is lower priority".
- Runner-offload expansion to debian1/2 beyond what controller/ already does.
- Deeper cross-module integrations (GOLIVE announcements, dashboard panels for new modules).
- Absorbing further one-off tools (autoremaster, promptmaestro, …) — mechanical repeats of the import procedure once the contract exists.

## Current state (verified 2026-07-28)

| Source | Git state | Runtime | Junk to filter on import |
|---|---|---|---|
| mega-plan-harness | 53 dirty, detached HEAD, remote alexcodeplace | `harnessd.service` ACTIVE | tmp/ 5.1G, tui/target 2.2G, runstate/ 238M (LIVE state — see Cutover), nested `mega-plan-harness/` 299M, `.worktrees/` (8 registered) + 5 stale `.wt-*` dirs, node_modules, scratchpad, web/ (superseded UI) |
| systray-ai | on `wip/dashboard-hybrid-redesign-continuation`, 5 dirty | `systray-ai.service` ACTIVE | __pycache__ |
| system-monitor | **not a git repo** | netdata/grafana installed | tmp/ |
| security-gate | 21 dirty, `master`, no remote | hook-invoked (prevent-band) | __pycache__, scratchpad, tmp |
| overdeck | clean `main` | overdeck-{web,collector,controller,tunnel}.service ACTIVE | — |

Hard-coded external refs that MUST keep resolving (enumerated by grep, re-verify at implementation):
- `~/.claude/skills/run-plan/SKILL.md`, `~/.claude/bin/wt-reaper.sh`, `~/.claude/workflows/.run/mega-plan-harness.js` → `~/Projects/mega-plan-harness/...`
- `~/.claude/skills/{workflow-router,security-gate,ask-codex}/…` → systray-ai / security-gate / system-monitor paths.
- This grep was ~/.claude-only and NOT exhaustive — see Reference inventory (task 0) for the authoritative machine-wide sweep.
- `HARNESS_HOME=~/.harness` exists OUTSIDE the repo — its contents must be inventoried before cutover (state vs config vs junk).

## Target layout

```
overdeck/
  apps/web  packages/deck-ui  collector  controller  spine  tui   # existing — UNTOUCHED
  bin/deckctl                                                     # new CLI (bash)
  modules/
    harness/          # mega-plan-harness (full git history)
    systray/          # systray-ai
    monitor/          # system-monitor (git-init'd at import)
    security/         # security-gate
    workstation/      # agent-CLI config + machine prefs (new content, no import)
  spec/deck-module.schema.json                                    # module contract schema
  docs/specs|plans    # existing convention
```

No `tui` collision: overdeck's top-level `tui/` (deck-tui) is distinct from `modules/harness/tui/` (runplan-tui); they stay separate binaries, sharing only the `tui-kit` crate (see Superseded UI surfaces).

Existing workspace members stay top-level (no churn); ONLY imported/new modules live under `modules/`.

**Root-gate scoping (deliberate):** `modules/**` stays OUT of `pnpm-workspace.yaml`, root `typecheck`/`test`, slopgate, and jscpd in this sub-project — joining them would flip the PR gate red immediately (frozen lockfile mismatch, harness dep tree, four codebases of new lint surface) and violate the no-ignored-signals doctrine at scale. Each module installs/tests via its own `bootstrap.sh`/`health.sh`, invoked locally by deckctl. **`modules/**` is UNGATED in CI this sub-project** (honest: these codebases are ungated today; the runner has no bats/uv toolchain provisioned) — wiring per-module CI steps and folding into root gates are named per-module roadmap items, one module at a time to green. No forced language unification.

## Module contract

Every `modules/<name>/` ships `deck.module.json` (schema at `spec/deck-module.schema.json`, validated by `deckctl doctor`):

```json
{
  "schema_version": 1,
  "name": "harness",
  "kind": "service | cli | config | library",
  "services": ["systemd/harnessd.service"],
  "bootstrap": "./bootstrap.sh",
  "health": "./health.sh",
  "install": { "mode": "in-place | artifact", "needs_git": true },
  "state_dirs": ["runstate", "tmp"]
}
```

- `services`: unit files owned by the module, installed/linked by deckctl into `~/.config/systemd/user/`. `ExecStart` path is mode-dependent: `in-place` modules → the module dir (`%h/Projects/overdeck/modules/<name>`); `artifact` modules → `%h/.local/opt/overdeck/<name>/current` — NEVER the live checkout. No hardcoded old paths.
- `bootstrap`: idempotent, fail-closed; installs deps (pnpm/uv/apt-hints), never touches another module.
- `health`: exit 0 = healthy; deckctl status aggregates.
- `install.mode`: `in-place` (module runs from the checkout — default for pure-config/library modules) or `artifact` (module is SOURCE ONLY in the repo; `deckctl install <name>` materializes a pinned runnable tree outside the checkout — MANDATORY for any module that git-operates on overdeck; see Runtime isolation). `needs_git`: artifact must be a real git clone (harness self-ops create worktrees), not an export.
- `state_dirs`: runtime state dirs; for `artifact` modules they live at `~/.overdeck/<name>/` (a per-rev artifact cannot own state; NEVER /tmp — tmpfs overlay breaks SQLite), with the artifact's expected in-tree paths symlinked there. `in-place` modules keep gitignored in-module state.

Depth: the manifest hides "how do I install/run/check this tool" behind one stable seam — deckctl and (later) the overdeck dashboard consume it without knowing module internals.

## Runtime isolation — the engine lives inside its own target repo (B1)

overdeck is itself a TARGET of the harness (`harness: land feat/…` commits on overdeck main). Engine root and target root are already decoupled in the engine (`bin/runplan`: `HARNESS_ROOT` is script-relative, `TARGET_ROOT` is CWD-derived) — that is NOT the hazard, and no `show-toplevel` call sites need patching. The hazard: a run landing a plan into overdeck performs `git checkout`/`merge` in the live checkout, which rewrites `modules/harness/**` — the running engine's own `HARNESS_ROOT` — mid-run.

Therefore `modules/harness` is **source only, never executed in place**:

- `deckctl install harness` → pinned, self-contained, **harness-rooted** runnable tree at `~/.local/opt/overdeck/harness/<rev>/`: split `modules/harness` out of overdeck at `<rev>` (`git subtree split -P modules/harness` or filter-repo `--subdirectory-filter`, cached per rev) and clone the split — a real harness-shaped git repo (`needs_git`), so `<artifact>/bin/runplan` is literally the path and each rev costs tens of MB, not a full overdeck clone. Plus a `current` symlink. Install clones from the LOCAL overdeck checkout at any commit-ish (feature branches includable pre-push), never from origin.
- `harnessd.service`, the `~/Projects/mega-plan-harness` compat symlink, and every external ref point at `…/harness/current` — NEVER at the live checkout.
- Upgrade = install new rev → health-check → flip `current` → restart units. Rollback = flip `current` back. `deckctl install --prune` (keep 2) removes artifact directories ONLY — it never dereferences/follows the `state_dirs` symlinks.
- Same rule binds any future module that git-operates on overdeck.

## Import procedure (one contract, applied 4×)

Import order (pilot smallest/lowest-blast-radius first, harness last): **monitor → security → systray → harness**. All four imports land on ONE overdeck feature branch, through the normal PR gate, as a single landing unit.

Per source repo — NEVER operate on the live checkout:

1. **Park WIP** in the source repo: commit dirty files to `wip/pre-ai-os-migration` (harness is on a DETACHED HEAD — `git switch -c wip/pre-ai-os-migration` from that exact commit so nothing is GC-able; systray: fold its 5 dirty into its existing wip branch), push where a remote exists. system-monitor: `git init` + initial commit first. Then `git bundle create` of the FULL source repo (all refs) to `~/.overdeck/bundles/<name>-pre-ai-os.bundle` — the durable undo for everything later dropped or archived (security-gate has no remote; the archive dir is user-deleted after 30 days).
2. **Fresh clone** to scratch; `git filter-repo` — strip junk paths (table above) + rewrite to `modules/<name>/` prefix.
3. **Secret scan the filtered history** (gitleaks or trufflehog over the whole filtered clone history) — a hit BLOCKS the merge until scrubbed (filter-repo strips paths, not secrets in old commits; security-gate and systray-ai were never reviewed for publication).
4. On the shared overdeck feature branch: `git merge --allow-unrelated-histories` the filtered clone. One import = one merge commit.
5. **Shim**: replace nothing yet — old checkout stays live until cutover (below).
6. Acceptance per module: harness — `deckctl install harness` then `<artifact>/bin/runplan --help` + engine smoke test from the artifact; systray — `python -m compileall` + service start from new path; security — `check.sh` green; monitor — `bash -n` on every script + `install.sh --dry-run` exits 0 (adding the `--dry-run` flag is an explicit task: prints planned actions, mutates NOTHING — netdata/grafana untouched).

Archived old repos (renamed during cutover phase 5): read-only, keep 30 days, then user deletes — the pre-migration git bundles remain the durable undo.

## Superseded UI surfaces (dedup policy)

Per-project UIs superseded by overdeck do NOT migrate; native/external UIs do. Excluded paths join the filter-repo strip list; full history remains in the archived old repo (reversible).

| Surface | Decision |
|---|---|
| harness `web/` (Astro backlog UI, unreferenced by systemd/Makefile/AGENTS.md) | DROP at import — overdeck apps/web supersedes (user-confirmed) |
| harness `tui/crates/tui-kit` | KEEP (imported as-is). Un-vendoring deck-tui's `vendor/tui-kit` snapshot is DEFERRED to the "fold harness into root gates" roadmap item — a path dep from root-gated deck-tui into ungated `modules/**` would breach the root-gate scoping, and tui-kit is a member of harness's own cargo workspace (cross-workspace path dep, unvalidated). The vendored snapshot @ b94483a7 stays this sub-project |
| harness `runplan-tui` binary | KEEP; audit-later candidate (deck-tui may supersede) — not this sub-project |
| security-gate `apps/web` (unreferenced by check.sh/CLAUDE.md) | DROP at import |
| systray `ui/` (native GTK/Cinnamon tray) | KEEP — user-confirmed not a dedup target |
| monitor `grafana/` (provisioning for external Grafana) | KEEP |

## Cutover (the engine cannot relocate itself mid-run)

If this plan executes via run-plan, `bin/runplan` runs FROM `~/Projects/mega-plan-harness` — a task that moves/kills that checkout kills its own executor. And the old checkout holds LIVE state: `runstate/` 238M (SQLite DBs, delivery receipts, `.live.json`/`.lock` files), 8 registered worktrees under `.worktrees/` with absolute gitdir pointers, plus whatever lives under `HARNESS_HOME=~/.harness`. Therefore:

- All import waves only ADD to overdeck; old checkouts stay untouched, services keep running from old paths.
- **Reference inventory is a cutover precondition** (task 0): `tools/ref-sweep.sh` — deterministic machine-wide sweep for old-path refs (`~/.claude`, `~/.codex`, `~/.cursor`, `~/.config/opencode`, `~/.gemini`, `~/.kiro`, `~/.pi`, shell rc files, systemd user units, cron, `~/Projects/*/.claude`) emitting a COMMITTED inventory file. Cutover asserts every inventoried ref resolves afterwards — "zero regression" is a test, not a claim.
- Final wave produces `modules/<name>/cutover.sh` per module (tested, fail-closed, idempotent), phases in order:
  1. Preflight: ref inventory current; `deckctl install` done + artifact healthy (harness); full state backup to `~/.overdeck/backups/<ts>/`.
  2. Stop the module's user units; assert no surviving processes and no live `.lock`/open-SQLite handles (`fuser`), else abort.
  3. **State migration**: inventory then `rsync -a` in-repo `state_dirs` (harness: `runstate/`, `tmp/`) → `~/.overdeck/<name>/`; verify (size+count parity). `~/.harness` (`HARNESS_HOME`) is ALREADY outside the repo — inventory it, touch nothing, leave `HARNESS_HOME` unchanged.
  4. **Worktree teardown** (harness): reap via existing `~/.claude/bin/wt-reaper.sh`, then `git worktree prune`; assert zero registered worktrees remain.
  5. **Rename old checkout** → `~/Projects/.archive/<name>-pre-ai-os` FIRST, then symlink the vacated project path → artifact `current` (harness) or module dir (in-place modules) — symlinking onto an existing dir fails or nests (`ln -sfn` footgun); re-link unit files; `systemctl --user daemon-reload`; start; health-check.
  6. Post: re-run ref-sweep assertion.
  Any failure at any phase → rollback: restore symlinks AND state from the phase-1 backup, restart units from old paths, report — never half-cut.
- **cutover.sh is executed by the MAIN session after the engine exits**, never as an engine task — and it runs FROM the live checkout (the pinned artifact may predate the script).
- After 1 green week: update inventoried refs to canonical new paths (workstation module owns those files by then), keep symlinks as belt-and-braces.

## modules/workstation

Curated config sync — NOT a dump of `~/.claude` (16G) / `~/.codex` (33G); those are mostly caches/history/auth.

- `manifest/<agent>.json` per agent (claude, codex, cursor-agent — the agent CLI's config subset of `~/.cursor`, NOT the Cursor IDE's — opencode + ~/.config/opencode, gemini, kiro, pi, antigravity at `~/.antigravity`): list of `{path, strategy}` entries, `strategy: "symlink" | "copy"`.
  - `symlink` (default): repo is source of truth — skills/, hooks/, agents/, commands/, output-styles/, lib/, bin/, workflows/, CLAUDE.md, RTK.md, keybindings.json, mcp.json.
  - `copy`: files the tool itself rewrites at runtime (e.g. settings.json where the CLI mutates it) — `deckctl sync pull` refreshes repo copy, `apply` writes it out; `sync diff` shows drift.
- **Secrets fail-closed**: deny-list (credentials, tokens, `*.key`, auth/, `settings.local.json`, history, sessions) + a secret-pattern scan on every `sync pull`; a hit ABORTS the pull with the path named. Nothing under `manifest/` may match the deny-list (doctor enforces).
- `deckctl sync pull` = home → repo (adopt current machine's config); `deckctl sync apply` = repo → home (new workstation / restore), backing up any non-symlink target to `~/.overdeck/backups/<ts>/` before replacing.
- Machine-specific values (hostnames, runner addresses) live in `machines/<hostname>.json`, merged over defaults at apply time.

## bin/deckctl

Bash, fail-closed (`set -euo pipefail`), subcommands — each idempotent, each with bats tests covering success + failure branch:

```
deckctl bootstrap            # full new-workstation setup: deps → sync apply → units → health
deckctl doctor               # validate every deck.module.json + manifest deny-list + dangling symlinks
deckctl status               # aggregate module health + unit states
deckctl sync pull|apply|diff [agent]
deckctl units apply|diff        # link module systemd user units into ~/.config/systemd/user
```

deckctl never guesses: unexpected state (foreign file where a symlink should go, unit active from an unexpected path) → error with detail + suggested command, never force.

## Error handling

- filter-repo only ever on scratch clones; live checkouts are read-only inputs until cutover.
- Every service mutation: stop → change → start → health, with rollback on failed health.
- deckctl and bootstrap.sh scripts are the ONLY entry points agents use post-merge (wrapper doctrine — naive `git filter-repo` / raw systemctl sequences are the footgun class).

## Test strategy

- deckctl: bats suite (both branches per subcommand), run in overdeck CI.
- Module acceptance commands (per-module, listed in Import procedure §5) wired as `health.sh`/smoke scripts so they're re-runnable forever, not one-off.
- Workstation sync: round-trip test in a sandbox HOME (`HOME=$(mktemp -d)` — pull → apply → diff empty; secret-file planted → pull aborts).
- Existing overdeck gates stay green: `pnpm -r typecheck|test`, slopgate, jscpd.
- Task 0 also VERIFIES gate0's surface derivation (root package scripts vs repo-tree walk) before the first import merge — if it walks the tree, the run halts on wave 1; scope it to root packages first.

## Roadmap after this sub-project

1. `ai-os-workstation-hardening` — refs canonicalized, symlink shims retired, second-workstation dry run on debian1.
2. `ai-os-runner-offload` — extend controller/ offload to more job classes on debian1/2.
3. `ai-os-botmaster-adapter` — Botmaster as communication adapter (GOLIVE announcements to channels); repo migration into `modules/bots` while deploys stay on Cloudflare.
4. Per-module root-gate folding — one roadmap item per module (`fold harness into root gates`, …), each driven to green individually.

## Post-cutover retention

Cutover completed 2026-07-28 for monitor, security, systray, harness. Four artifact classes survive it. Keep ALL of them 30 days (until 2026-08-27), then delete only the pre-cutover checkouts.

| Artifact | Path | Retention |
| --- | --- | --- |
| Pre-cutover checkout | `~/Projects/.archive/<module>-pre-ai-os` | 30 days — delete after |
| Git bundle | `~/.overdeck/bundles/<module>-pre-ai-os.bundle` | KEEP — sole full-history copy once the checkout is gone |
| State + unit backup | `~/.overdeck/backups/<UTC-timestamp>-<module>.<suffix>` | 30 days |
| Cutover receipt | `~/.overdeck/cutover/<module>.json` | KEEP — `--rollback` reads it |

Checkouts dominate the footprint (harness 8.3G, systray 612M, security 15M, monitor 2.3M); bundles total 5.7M. Delete checkouts on expiry, NEVER the bundles.

Rules:

1. Rollback a module: `modules/<name>/cutover.sh --rollback`. Requires its receipt AND backup dir — deleting either forfeits rollback.
2. Delete an archived checkout ONLY when its receipt reads `status: complete` and the module is green in `deckctl status`.
3. NEVER delete a bundle. It is the only pre-cutover history once the checkout is gone.
4. Uncommitted work found during worktree teardown was committed to `wip/cutover-rescue-*` branches in `~/Projects/.archive/harness-pre-ai-os` — audit before pruning them. Rescue branches exist for harness ONLY — the other three teardowns had nothing uncommitted, so their bundles are untouched pre-cutover snapshots and correctly contain no `wip/*` refs. The harness bundle was cut BEFORE the rescue and re-cut afterwards; verify with `git bundle list-heads ~/.overdeck/bundles/harness-pre-ai-os.bundle | grep cutover-rescue` (expect 4) before deleting `~/Projects/.archive/harness-pre-ai-os`.

### Archived-repo preservation (2026-07-29)

Every git repo moved under `~/Projects/.archive` was audited for work that existed nowhere else. 13 held unpushed commits, uncommitted changes, or stashes. All of it is now preserved:

- Uncommitted changes were committed on the archived repo itself as `archive-rescue: uncommitted work preserved at archival`.
- Stashes were pinned as `refs/archive-stash/<n>` so `--all` captures them (a bare bundle does NOT include `refs/stash`).
- Each repo was bundled to `~/.overdeck/bundles/archive/<slug>.bundle` and every local ref tip was verified present inside the bundle.

Largest holdings: `AaAAA` (228 unpushed), `security-pre-ai-os` (203), `harness-pre-ai-os` (77 + 10 stashes), `old/slack-connector` (5 + 8 stashes). Total 41M.

NEVER delete `~/.overdeck/bundles/archive/**` — for these repos it is the only copy of that work. Re-verify with `git bundle verify <file>` before removing any archived checkout.

## systemd ownership (2026-07-29)

Unit bodies live in the repo; the OS location holds a pointer or a copy. Two layers, two rules:

- **User units** — source `modules/<module>/systemd/user/**`, linked into `~/.config/systemd/user` by `deckctl units apply`. Symlinks, so a hand-edit to a live unit edits the repo and cannot drift. `deckctl units diff` exits non-zero on any unit not linked to the repo.
- **System units + `/usr/local` helpers** — source `modules/<module>/systemd/system/**` and `modules/<module>/system-{bin,sbin}/**`, installed by `lib/deckctl/system-units.sh` (`--dry-run` supported). **COPIED, never symlinked**: a root unit pointing into a user-writable repo lets anything that can write the repo run as root.

Enablement is not repo state — `deckctl units apply` links and reloads; enabling stays a per-machine `systemctl enable` decision. Masking is per-machine for the same reason: `units apply` never creates or clears a mask, so a unit masked here (`ci-fallback`, `platform-fallback`, `multideal-runner`) arrives unmasked on a fresh workstation.

Module installers MUST link user units, never copy them (`link_user_unit` in `modules/monitor/install.sh` and `grafana/install-dashboards.sh`) — a copy replaces the repo symlink and reintroduces drift. `deckctl bootstrap` runs `deckctl units apply` before per-module provisioning.

Session environment written by overdeck lives in `modules/workstation/shell/environment.d/`, synced by the `shell` manifest into `~/.config/environment.d/`.

Machine-local by design, excluded from the repo: `~/.cli-proxy-api/config.yaml` (provider API keys, no env-reference syntax), `~/.systray-ai/` (holds the account credentials `~/.codex/auth.json` points at).

## Architecture Decisions

- `modules/` namespace, existing overdeck members untouched — churn-free, monorepo still converges (existing members can move in a later cleanup if ever worth it).
- No language unification — polyglot modules, per-module toolchain, contract is the only shared seam.
- Artifact-install model for repo-operating modules (`install.mode: artifact`): the engine must never run from inside the checkout it lands plans into — pinned runnable clones under `~/.local/opt/overdeck/`, `current` symlink flip for upgrade/rollback.
- State for artifact modules relocates to `~/.overdeck/<name>/` (a replaceable per-rev artifact cannot own state); in-place modules keep gitignored in-module state. Supersedes the earlier keep-state-in-module lean.
- `modules/**` excluded from root gates (pnpm workspace, typecheck/test, slopgate, jscpd) this sub-project; per-module gates via bootstrap/health; folding is per-module roadmap work.
- Symlink-first config with per-path copy escape hatch — single source of truth where safe, drift-managed copies where tools rewrite files.
- cutover.sh executed by main session, not the engine — self-migration hazard.
- `~/.harness` stays put and `HARNESS_HOME` unchanged — already outside the repo, exactly where artifact-model state belongs; moving it would touch every invocation path during the riskiest phase for zero goal served.
- tui-kit un-vendor deferred to the fold-harness-into-root-gates roadmap item (gate-scope coherence + unvalidated cross-workspace path dep).
- Rejected: subtree-without-contract (goals unmet), Nix re-platform (cost ≫ benefit for 1-user fleet).
