From 2d559748872840cdb38bf74eff89ad002268c914 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Fri, 21 Aug 2026 11:01:28 -0500 Subject: [PATCH 1/4] home-repo re-root spec: user/ is the repo, machine scope travels keyed Co-Authored-By: Claude Fable 5 --- .../specs/2026-08-21-home-repo-reroot.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-21-home-repo-reroot.md diff --git a/docs/superpowers/specs/2026-08-21-home-repo-reroot.md b/docs/superpowers/specs/2026-08-21-home-repo-reroot.md new file mode 100644 index 00000000..46bc0383 --- /dev/null +++ b/docs/superpowers/specs/2026-08-21-home-repo-reroot.md @@ -0,0 +1,78 @@ +# Home-repo re-root — user/ IS the repo (spec amendment) + +Ruled by Matt 2026-08-21 (conversation; supersedes MAT-374 ruling 2's "~/.mattstack +IS the clone" and stratum 5's "machine-local never travels"). Motivation: the +settings consolidation routed all declarative intent into the stores under +`user/`, hollowing out root-level-clone's rationale while leaving employer +material (team clones, ci-attendants) under a personal repo tree guarded only by +.gitignore. Boundaries move from ignore-based to structural. + +## The ruled shape + +``` +~/.mattstack/ ← plain directory, NO repo + user/ ← THE personal repo (mattstack-home re-rooted) + settings.user.jsonc ← user scope (renamed from settings.jsonc) + local// + settings.local.jsonc ← machine scope, TRACKED, keyed per machine + secrets/*.json ← sops-encrypted (unchanged) + skills.jsonc ← moves in from root + snapshot-owners.jsonc ← moves in from root + .sops.yaml ← moves in from root (cwd-pin follows) + prefs/, skills/, ... ← existing content unchanged + teams//mattstack/ + settings.team.jsonc ← team scope (renamed from settings.jsonc); THEIR repo + rt/ deck/ shepherdr/ repos/ ci-attendants/ work/ ← state; no repo; never travels +``` + +## Rulings + +1. **Structural boundary**: no personal repo tree contains employer material or + runtime state by construction. .gitignore stops being load-bearing for + security; the interim guard hooks on the root repo retire when the root repo + does. +2. **Machine scope is tracked and keyed** ("machine-scoped travels keyed" + amends "machine-local never travels"): each machine's settings live at + `user/local//settings.local.jsonc`. Restore = clone + pick your + machine profile from the list (human-readable keys). Multi-machine never + clobbers — each writes only its own key. +3. **Machine key**: derived from hostname (slugified LocalHostName), overridable + via an untracked `~/.mattstack/machine-key` file; `rt restore` prompts to + pick an existing profile or name a new one. +4. **Naming**: scope in the filename, identity in the path — + settings.user.jsonc / settings.local.jsonc / settings.team.jsonc. +5. **State rule** (regeneration rule restated): everything outside user/ and + teams/ is re-derivable or acceptable-to-lose; anything neither is mis-filed + and must be promoted to the declarative layer. Age key stays keychain+ + password-manager (own channel). +6. **Paths/identity in the tracked machine sections are accepted** (private + personal repo), including board.claudeCommand's account string — Matt's + explicit call. +7. **Precedence unchanged**: default < team < user < machine (+ repo sections), + the VS Code-style most-specific-wins ladder. Only storage locations move. + +## Implementation sketch (its own lane, parallel to board) + +- Resolver (`packages/rt-client/src/settings/paths.ts` + stores/write): + `userSettingsPath` → `~/.mattstack/user/settings.user.jsonc`; + `machineSettingsPath` → `user/local//settings.local.jsonc` + (machineKey resolves hostname-slug with the override file); + `teamSettingsPath` → `teams//mattstack/settings.team.jsonc`. + setSetting machine-scope creates the machine dir on first write. +- Live migration (orchestrator): re-root mattstack-home history + (`git filter-repo --subdirectory-filter user` on a fresh clone + move the + three root files in + `git mv settings.jsonc settings.user.jsonc` + + seed `local//settings.local.jsonc` from the current + `settings.local.jsonc`), force-push to the SAME remote (private, solo — + history rewrite acceptable here as the repo is 1 day old; Matt's rotate-not- + rewrite doctrine concerns secrets, and the secrets remain encrypted in both + histories), re-clone into `~/.mattstack/user`, delete the root `.git` + + hooks, rename the team file, restart daemon + deck, full verify. +- Consumers to update: sops cwd-pin (`buildSecretsSpawnOptions` → + `user/`), skills.jsonc readers (manifest walk), snapshot-owners readers + (H2, unbuilt), `rt home init`/`key`/boundary + guard code (init targets + user/; HOME_BOUNDARY simplifies to the user-repo tree), hooks.json shims + (unaffected — rt/ zone), docs. +- H2 (snapshot daemon) spec: watches `~/.mattstack/user` only; commits by + allowlist (belt), which is now just "the whole repo minus *.tmp". +- e2e/test fixtures that seed store paths update to the new names. From 4845610f636b97207c0351775a27f9d5fc22bc85 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Fri, 21 Aug 2026 11:14:53 -0500 Subject: [PATCH 2/4] =?UTF-8?q?RT-30:=20re-root=20spec=20round=202=20?= =?UTF-8?q?=E2=80=94=20collisions=20ruled,=20sops=20triple,=20rt-paths=20a?= =?UTF-8?q?uthority,=20migration=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../specs/2026-08-21-home-repo-reroot.md | 147 ++++++++++++++---- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/docs/superpowers/specs/2026-08-21-home-repo-reroot.md b/docs/superpowers/specs/2026-08-21-home-repo-reroot.md index 46bc0383..263c8fa6 100644 --- a/docs/superpowers/specs/2026-08-21-home-repo-reroot.md +++ b/docs/superpowers/specs/2026-08-21-home-repo-reroot.md @@ -12,13 +12,14 @@ material (team clones, ci-attendants) under a personal repo tree guarded only by ``` ~/.mattstack/ ← plain directory, NO repo user/ ← THE personal repo (mattstack-home re-rooted) - settings.user.jsonc ← user scope (renamed from settings.jsonc) + settings.user.jsonc ← user scope (renamed from user/settings.jsonc) local// settings.local.jsonc ← machine scope, TRACKED, keyed per machine secrets/*.json ← sops-encrypted (unchanged) - skills.jsonc ← moves in from root + skills.jsonc ← moves in from root (compat symlink stays at root) snapshot-owners.jsonc ← moves in from root - .sops.yaml ← moves in from root (cwd-pin follows) + .sops.yaml ← moves in from root, REWRITTEN (see sops triple) + .gitignore ← rewritten (see below); `local/` line DELETED prefs/, skills/, ... ← existing content unchanged teams//mattstack/ settings.team.jsonc ← team scope (renamed from settings.jsonc); THEIR repo @@ -30,7 +31,9 @@ material (team clones, ci-attendants) under a personal repo tree guarded only by 1. **Structural boundary**: no personal repo tree contains employer material or runtime state by construction. .gitignore stops being load-bearing for security; the interim guard hooks on the root repo retire when the root repo - does. + does. (The hooks are hand-installed in `~/.mattstack/.git/hooks/` + {pre-commit,pre-push} with no source checked into any repo — retiring them + is a live-system deletion alongside the root `.git`, not a code change.) 2. **Machine scope is tracked and keyed** ("machine-scoped travels keyed" amends "machine-local never travels"): each machine's settings live at `user/local//settings.local.jsonc`. Restore = clone + pick your @@ -38,41 +41,127 @@ material (team clones, ci-attendants) under a personal repo tree guarded only by clobbers — each writes only its own key. 3. **Machine key**: derived from hostname (slugified LocalHostName), overridable via an untracked `~/.mattstack/machine-key` file; `rt restore` prompts to - pick an existing profile or name a new one. + pick an existing profile or name a new one, and PERSISTS the choice to + `~/.mattstack/machine-key` (that file is regenerable, so it may live outside + user/ without violating ruling 5). `rt restore` itself is forward work + (RT-31, unbuilt — like snapshot-owners/H2); until it exists, seeding the + machine-key file is a manual step documented in the migration. 4. **Naming**: scope in the filename, identity in the path — settings.user.jsonc / settings.local.jsonc / settings.team.jsonc. 5. **State rule** (regeneration rule restated): everything outside user/ and teams/ is re-derivable or acceptable-to-lose; anything neither is mis-filed - and must be promoted to the declarative layer. Age key stays keychain+ - password-manager (own channel). + and must be promoted to the declarative layer. Corollary: everything INSIDE + user/ is declarative and travels — scratch does not belong under user/ (see + collisions). Age key stays keychain+password-manager (own channel). 6. **Paths/identity in the tracked machine sections are accepted** (private personal repo), including board.claudeCommand's account string — Matt's explicit call. 7. **Precedence unchanged**: default < team < user < machine (+ repo sections), the VS Code-style most-specific-wins ladder. Only storage locations move. -## Implementation sketch (its own lane, parallel to board) +## user/local/ collisions (ruled destinations) -- Resolver (`packages/rt-client/src/settings/paths.ts` + stores/write): +Live today, `user/local/` holds untracked non-profile content that collides +with the machine-profile namespace. Rulings (orchestrator, 2026-08-21): + +- **Scratch** (handoff/reply markdown, attic tarballs — including + `handoff-2026-08-20-installer-needs-from-settings-lane.md`, the live + installer channel) moves to `~/.mattstack/work/scratch/` (state zone, no + repo). The cross-agent handoff convention moves with it: new drops go to + `work/scratch/`, and the attic tarballs are employer-adjacent so they must + not enter the personal repo. +- **`user/local/claimview/`** (dev-flags.json, ld-flags/ — employer-adjacent + runtime, forbidden in the personal repo by ruling 1) moves to + `~/.mattstack/work/claimview/`. The lane greps all mattstack repos + + `~/.claude` installed skills for readers of the old path and repoints any it + finds; zero known readers is the expected result, but the grep is mandatory. +- **Inner `.gitignore`**: `user/.gitignore` today is the single line `local/`. + After the re-root it is THE repo's gitignore, and that line would silently + untrack every machine profile. It is REWRITTEN during migration to exactly: + `.DS_Store`, `*.sock`, `*.tmp`, `secrets/*.tmp` (no `local/`). The stray + `user/.DS_Store` present today is deleted before the first commit. H2's + allowlist ("whole repo minus ignores") composes with this list. +- **Profile picker**: enumerates only `local//` containing a + `settings.local.jsonc` — a defensive gate so any future non-profile + directory is never offered as a machine. + +## Implementation sketch (its own lane) + +**Sequencing**: the keys wave is MERGED (origin/main includes PR #7 and the +deck whitelist PR #8) — this lane branches a fresh worktree off origin/main. +The only concurrent rt-side branch is `board-secrets-scope` (touches +`lib/daemon/handlers/secrets.ts` only — no overlap with paths/stores). Runs +parallel to the board lane; the settings API is unchanged. + +- **Resolver — `lib/rt-paths.ts` is the authority; change it FIRST, mirror in + `packages/rt-client/src/settings/paths.ts`** (the parity test + `lib/__tests__/settings-paths-parity.test.ts` fails the build on divergence). `userSettingsPath` → `~/.mattstack/user/settings.user.jsonc`; - `machineSettingsPath` → `user/local//settings.local.jsonc` - (machineKey resolves hostname-slug with the override file); + `machineSettingsPath` → `user/local//settings.local.jsonc` — + machineKey() (hostname-slug, override-file read) makes this impure and must + be duplicated IDENTICALLY on both sides or parity fails; `teamSettingsPath` → `teams//mattstack/settings.team.jsonc`. - setSetting machine-scope creates the machine dir on first write. -- Live migration (orchestrator): re-root mattstack-home history - (`git filter-repo --subdirectory-filter user` on a fresh clone + move the - three root files in + `git mv settings.jsonc settings.user.jsonc` + - seed `local//settings.local.jsonc` from the current - `settings.local.jsonc`), force-push to the SAME remote (private, solo — - history rewrite acceptable here as the repo is 1 day old; Matt's rotate-not- - rewrite doctrine concerns secrets, and the secrets remain encrypted in both - histories), re-clone into `~/.mattstack/user`, delete the root `.git` + - hooks, rename the team file, restart daemon + deck, full verify. -- Consumers to update: sops cwd-pin (`buildSecretsSpawnOptions` → - `user/`), skills.jsonc readers (manifest walk), snapshot-owners readers - (H2, unbuilt), `rt home init`/`key`/boundary + guard code (init targets - user/; HOME_BOUNDARY simplifies to the user-repo tree), hooks.json shims - (unaffected — rt/ zone), docs. -- H2 (snapshot daemon) spec: watches `~/.mattstack/user` only; commits by - allowlist (belt), which is now just "the whole repo minus *.tmp". -- e2e/test fixtures that seed store paths update to the new names. + Also: `listTeams()` (rt-client stores.ts) gates team discovery on the + settings filename — update its probe to `settings.team.jsonc`; the `--scope` + option hints in `lib/command-tree-def.ts` (~:602) print all three store + paths — update. setSetting machine-scope creates `local//` on first + write. +- **sops triple — three cwd-coupled values move in lockstep** (breaking one + silently matches NO creation rule, the wrong-recipient failure mode noted in + store.ts): (1) `buildSecretsSpawnOptions` cwd pin `mattstackHome()` → + `/user`; (2) `.sops.yaml` `path_regex: user/secrets/.*` → + `secrets/.*` — the live file is REWRITTEN via `renderSopsYaml` (updated in + lib/home/age-key.ts), not `git mv`'d; (3) the staging + `--filename-override user/secrets/.json` → `secrets/.json` + (staging stays in `rt/tmp/`, outside the repo). The verify includes an + encrypt/decrypt round-trip proving the rules still match post-move. +- **`rt home init` is a rewrite, not a retarget**: the init-plan vocabulary + built on "root is the repo / prefs folds in" retires — `createRepo`, + `gitInit`, `foldInPrefs`, `unlinkUserClone`, `adoptCommit`, + `HomeState.prefsRemoteUrl`, `reason: "prefs-remote-unreadable"` all go. + Init becomes: ensure `~/.mattstack/` skeleton (state dirs), clone the user + repo to `user/`, write the machine-key file, create `local//` if new. + `writeGitignore` writes the ruled inner-gitignore content; `writeOwners` + targets `user/snapshot-owners.jsonc`. HOME_BOUNDARY guard simplifies to the + user-repo tree. +- **skills.jsonc — 27 cross-repo readers** (mr-board, mattstack-skills, gitq + `scripts/resolve-args.sh` shims + installed `~/.claude` copies) hardcode + `~/.mattstack/skills.jsonc`. This lane does NOT patch those repos: the + migration leaves a compat symlink `~/.mattstack/skills.jsonc → + user/skills.jsonc`. The symlink retires in a follow-up skills-estate sweep + (tracked outside this lane). `snapshot-owners.jsonc` needs no symlink (zero + live readers; H2 unbuilt reads the new path). +- Consumers otherwise unchanged: hooks.json shims (rt/ zone), docs sweep, + e2e/test fixtures that seed store paths update to the new names. +- **H2 (snapshot daemon) spec**: watches `~/.mattstack/user` only; commits by + allowlist (belt) = the whole repo minus the ruled gitignore. + +## Live migration (orchestrator-only, destructive — ordered) + +1. **Backup first**: `tar -czf ~/mattstack-preroot-backup-.tar.gz` + of the full `~/.mattstack` tree (kept outside the tree being mutated; + delete after the verify passes and a quiet day). +2. Move scratch + claimview per the collision rulings (`work/scratch/`, + `work/claimview/`); delete `user/.DS_Store` and the stale `local/.gitkeep`. +3. On a FRESH clone of mattstack-home (scratchpad): + `git filter-repo --subdirectory-filter user`, move in `skills.jsonc`, + `snapshot-owners.jsonc`; write the rewritten `.sops.yaml` + `.gitignore`; + `git mv settings.jsonc settings.user.jsonc`; seed + `local//settings.local.jsonc` from the live root + `settings.local.jsonc`. (filter-repo drops the root files' one-day history + — they re-enter as fresh adds; accepted, the repo is a day old. Rotate-not- + rewrite doctrine concerns secrets, which remain encrypted in both + histories.) +4. Force-push to the SAME remote (private, solo). Clone to + `~/.mattstack/user.new`; verify contents; swap (`user` → aside, `user.new` + → `user`); only after the full verify passes, delete the aside copy, the + root `.git/` + guard hooks, and the root `settings.local.jsonc`. +5. Compat symlink `~/.mattstack/skills.jsonc → user/skills.jsonc`; rename the + team file (`teams/claimview/mattstack/`: `settings.jsonc` → + `settings.team.jsonc`, committed+pushed in THEIR repo); write + `~/.mattstack/machine-key`. +6. Restart daemon + deck. +7. **Full verify (defined)**: `rt settings list`/`explain` resolve across all + three scopes with the expected values; `rt secrets` decrypt round-trip on + an existing domain + an encrypt round-trip on a scratch key (then removed); + daemon + deck restart clean; board serves; `rt verify` 12/12. From 3006b5207bde65536fbcc0b8188c126a7619cb98 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Fri, 21 Aug 2026 11:16:48 -0500 Subject: [PATCH 3/4] =?UTF-8?q?RT-30:=20re-root=20spec=20approved=20?= =?UTF-8?q?=E2=80=94=20advisory=20polish=20(init=20symlink,=20verify=20phr?= =?UTF-8?q?asing)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/superpowers/specs/2026-08-21-home-repo-reroot.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-08-21-home-repo-reroot.md b/docs/superpowers/specs/2026-08-21-home-repo-reroot.md index 263c8fa6..2d314e86 100644 --- a/docs/superpowers/specs/2026-08-21-home-repo-reroot.md +++ b/docs/superpowers/specs/2026-08-21-home-repo-reroot.md @@ -78,7 +78,7 @@ with the machine-profile namespace. Rulings (orchestrator, 2026-08-21): - **Inner `.gitignore`**: `user/.gitignore` today is the single line `local/`. After the re-root it is THE repo's gitignore, and that line would silently untrack every machine profile. It is REWRITTEN during migration to exactly: - `.DS_Store`, `*.sock`, `*.tmp`, `secrets/*.tmp` (no `local/`). The stray + `.DS_Store`, `*.sock`, `*.tmp` (no `local/`). The stray `user/.DS_Store` present today is deleted before the first commit. H2's allowlist ("whole repo minus ignores") composes with this list. - **Profile picker**: enumerates only `local//` containing a @@ -120,7 +120,10 @@ parallel to the board lane; the settings API is unchanged. `gitInit`, `foldInPrefs`, `unlinkUserClone`, `adoptCommit`, `HomeState.prefsRemoteUrl`, `reason: "prefs-remote-unreadable"` all go. Init becomes: ensure `~/.mattstack/` skeleton (state dirs), clone the user - repo to `user/`, write the machine-key file, create `local//` if new. + repo to `user/`, write the machine-key file, create `local//` if new, + and create the `skills.jsonc` compat symlink (load-bearing provisioning — a + restored machine needs it for the 27 unpatched readers, not just this + migration). `writeGitignore` writes the ruled inner-gitignore content; `writeOwners` targets `user/snapshot-owners.jsonc`. HOME_BOUNDARY guard simplifies to the user-repo tree. @@ -164,4 +167,5 @@ parallel to the board lane; the settings API is unchanged. 7. **Full verify (defined)**: `rt settings list`/`explain` resolve across all three scopes with the expected values; `rt secrets` decrypt round-trip on an existing domain + an encrypt round-trip on a scratch key (then removed); - daemon + deck restart clean; board serves; `rt verify` 12/12. + daemon + deck restart clean; board serves; `rt verify` with all critical + checks passing. From 88d52798d4f09d5f3f67bcfad0a75dc6611eee43 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Fri, 21 Aug 2026 11:18:27 -0500 Subject: [PATCH 4/4] RT-30: re-root implementation plan Co-Authored-By: Claude Fable 5 --- .../plans/2026-08-21-home-repo-reroot.md | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-21-home-repo-reroot.md diff --git a/docs/superpowers/plans/2026-08-21-home-repo-reroot.md b/docs/superpowers/plans/2026-08-21-home-repo-reroot.md new file mode 100644 index 00000000..a4f61502 --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-home-repo-reroot.md @@ -0,0 +1,103 @@ +# Home-Repo Re-Root Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox syntax. + +**Goal:** `~/.mattstack/user` becomes THE personal repo (no repo at root); machine scope moves to tracked `user/local//settings.local.jsonc`; stores rename to scope-in-filename; the live tree migrates safely. + +**Architecture:** Path constructors change in `lib/rt-paths.ts` (authority) and mirror into `packages/rt-client/src/settings/paths.ts` (parity test enforces). A new `machineKey()` (hostname slug + untracked override file) makes `machineSettingsPath()` impure — duplicated IDENTICALLY both sides. The sops cwd/path_regex/filename-override triple moves in lockstep to `user/`-rooted. `rt home init` is rewritten from "adopt root + fold in prefs" to "clone user repo + provision machine". Live migration is orchestrator-only. + +**Tech Stack:** Bun, TypeScript, sops+age, git filter-repo. + +**Spec:** `docs/superpowers/specs/2026-08-21-home-repo-reroot.md` (this repo, same branch). Upstream: `docs/superpowers/specs/2026-08-20-suite-settings-migration.md`. + +## Global Constraints + +- Worktree `/Users/matt/Documents/GitHub/repo-tools-reroot-wt`, branch `reroot-user-repo` off origin/main. NEVER touch the real `~/.mattstack`, the keychain, or the live daemon from tests — all tests repoint `process.env.HOME` at temp dirs (call-time resolution makes this work; never cache paths at module load). +- `lib/rt-paths.ts` is the authority; `packages/rt-client/src/settings/paths.ts` mirrors it verbatim (including `machineKey()`), or `lib/__tests__/settings-paths-parity.test.ts` fails. +- The precedence ladder, registry, resolve/write semantics are UNCHANGED — only paths and filenames move. +- After editing anything under `packages/rt-client/src/`, run `bun run build` inside `packages/rt-client/` (the dist-freshness test fails otherwise). +- Gates per task: `bun test lib commands packages` + `bun x tsc --noEmit`. Foreground-only; no monitors; NEEDS_CONTEXT on anything outside the worktree. +- Comments constraint-only. Trailer: `Co-Authored-By: Claude Fable 5 ` + +--- + +### Task 1: machineKey() + store path constructors + consumers + +**Files:** +- Modify: `lib/rt-paths.ts` (settings-stores section, ~:70-107) +- Modify: `packages/rt-client/src/settings/paths.ts` (mirror) +- Modify: `packages/rt-client/src/settings/stores.ts` (`listTeams()` probe ~:85-129) +- Modify: `packages/rt-client/src/settings/write.ts` (machine-scope write creates `local//` — check whether its write path already mkdirs; add if not) +- Modify: `lib/command-tree-def.ts` (~:602 `--scope` hints print the three store paths) +- Test: `lib/__tests__/settings-paths-parity.test.ts` (update expectations), plus paths unit tests in both trees + +**Interfaces produced:** `machineKey(): string` (exported from both paths modules); new path returns: +`userSettingsPath()` → `/.mattstack/user/settings.user.jsonc`; +`machineSettingsPath()` → `/.mattstack/user/local//settings.local.jsonc`; +`teamSettingsPath(t)` → `/.mattstack/teams//mattstack/settings.team.jsonc`. + +`machineKey()` exact behavior (identical in both modules): +1. If `/.mattstack/machine-key` exists, return its trimmed content (must be non-empty after trim; empty falls through). +2. Else slugify the hostname: `require("os").hostname()` lowercased, strip a trailing `.local`, replace every run of chars outside `[a-z0-9-]` with `-`, trim leading/trailing `-`. Empty result → `"default"`. + +```ts +export function machineKey(): string { + const override = join(home(), ".mattstack", "machine-key"); + try { + const v = readFileSync(override, "utf8").trim(); + if (v) return v; + } catch {} + const slug = hostname().toLowerCase().replace(/\.local$/, "") + .replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, ""); + return slug || "default"; +} +``` +(`readFileSync` from `fs`, `hostname` from `os` — add imports both sides.) + +`listTeams()` probe: `mattstack/settings.jsonc` → `mattstack/settings.team.jsonc`. + +- [ ] RED tests: machineKey override-file wins; hostname slugging (mixed case, `.local`, illegal chars, empty→default); the three path shapes; parity test updated; listTeams probe on new filename (old-name dir NOT listed) +- [ ] Implement in `lib/rt-paths.ts`, mirror verbatim in rt-client paths.ts; update stores.ts, write.ts mkdir, command-tree-def hints +- [ ] `cd packages/rt-client && bun run build`; full gates green +- [ ] Commit + +### Task 2: sops triple moves to user/-rooted + +**Files:** +- Modify: `lib/home/age-key.ts` (`renderSopsYaml` :122-124 — `path_regex: user/secrets/.*` → `secrets/.*`) +- Modify: `lib/secrets/store.ts` (`buildSecretsSpawnOptions` :340-348 — `cwd: mattstackHome()` → `join(mattstackHome(), "user")`; the `--filename-override user/secrets/.json` construction (~:222-230 + the header comment :10) → `secrets/.json`) +- Test: existing suites for both modules — update expectations; the store suite's cwd-pin unit test asserts the new cwd + +The three values are cwd-coupled: sops resolves `.sops.yaml` and its `path_regex` relative to cwd, and the filename-override must match the regex or sops matches NO creation rule (silent wrong-recipient). All three change in this one task, never separately. + +- [ ] RED: renderSopsYaml output equals the new literal; spawn options cwd = `/user`; filenameOverride = `secrets/.json` +- [ ] Implement; full gates green +- [ ] Commit + +### Task 3: rt home init rewrite + boundary + +**Files:** +- Modify: `lib/home/init-plan.ts` — retire `createRepo`/`gitInit`/`foldInPrefs`/`unlinkUserClone`/`adoptCommit` step kinds, `HomeState.prefsRemoteUrl`/`hasUserClone`, `reason: "prefs-remote-unreadable"`, `ADOPT_COMMIT_MESSAGE`. New `HomeState` probes: `{ userRepoPresent, machineKeyFilePresent, profileDirPresent, skillsSymlinkPresent, stateDirsMissing: string[] }`. New plan (ordered): `ensureStateDirs` (rt/deck/shepherdr/repos/work/teams as missing) → `cloneUserRepo { url }` (skip if present) → `writeMachineKey { key }` (skip if file present) → `ensureProfileDir { key }` (mkdir `user/local//` if missing) → `writeSkillsSymlink` (root `skills.jsonc` → `user/skills.jsonc`; skip if a correct symlink exists; a REAL file at the root path is a `blocked` reason, never overwritten). +- Modify: `lib/home/init-exec.ts` — executors for the new step kinds; delete retired executors. +- Modify: `lib/home/boundary.ts` — `HOME_BOUNDARY` becomes the USER-repo gitignore authority: ignored = `[".DS_Store", "*.sock", "*.tmp"]` (NO `local/`); update the header comment (the gitignore is hygiene now, not the security boundary — structure is). +- Modify: `commands/home.ts` — wording follows ("write the boundary .gitignore" → user repo); `rt home init` clones `mattstack-home` (same remote URL discovery it uses today — check how it currently learns the remote; if it derived it from the existing root repo, it now takes `--url ` with the current remote as the recorded default). +- Test: `lib/home/__tests__/` — init-plan table tests rewritten for the new state machine; boundary test expectations. + +- [ ] RED: plan-builder table tests (fresh HOME → all steps; repo present → provisioning-only; real file at symlink path → blocked) +- [ ] Implement plan + exec + boundary + command wording; full gates green +- [ ] Commit + +### Task 4: fixture/docs sweep + +**Files:** +- Modify: `e2e/tests/settings.test.ts`, `e2e/tests/endpoint.test.ts` (seed stores at the new paths/names; machine store fixtures must write `user/local//settings.local.jsonc` — use the override file to pin a known key in fixtures) +- Modify: any remaining unit-test fixtures found by `grep -rln 'settings\.jsonc\|settings\.local\.jsonc' lib commands packages e2e --include='*.ts'` (excluding rt-client dist) +- Modify: docs that state the old layout: `grep -rln 'settings\.local\.jsonc\|user/settings\.jsonc' docs README.md` + +- [ ] Sweep both greps to zero stale references (report any intentionally kept, e.g. historical spec text) +- [ ] Full gates + `bun run test:all` if present; e2e suite green +- [ ] Commit + +### Task 5 (ORCHESTRATOR-ONLY, live): migration + +Execute the spec's "Live migration" section verbatim — ordered, backup first, swap-before-delete, full verify as defined there. Includes the team-repo file rename (their repo, commit+push), the machine-key file, the compat symlink, deleting the root `.git/` + interim guard hooks, and updating the memory + handoff conventions to `work/scratch/`.