# Domain Pitfalls — Nexus Fork of Paperclip

**Domain:** Forked open-source project with display-layer renames, no i18n layer
**Researched:** 2026-03-30
**Confidence:** HIGH — based primarily on direct codebase analysis of `/Volumes/UsbNvme/repos/nexus` via CONCERNS.md, supplemented by fork maintenance community research

---

## Critical Pitfalls

Mistakes that cause data loss, broken upstream rebase, or irreversible divergence.

---

### Pitfall 1: Renaming a Code Identifier That Is Also a Stored DB Value

**What goes wrong:** You rename a TypeScript constant, CLI command, or function to use the new Nexus vocabulary, not realising the same string is also stored as a literal value in database rows. The app breaks for any existing installation because the server checks `approval.type === "hire_agent"` but the DB still has `"hire_agent"` rows. Or worse: you change the constant on one side (server) but not the other (CLI) and the two sides silently disagree.

**Why it happens:** In Paperclip the same string serves double duty: it is both a TypeScript constant/enum and a persisted DB value. The CONCERNS.md audit identifies these dual-purpose strings explicitly:
- `"ceo"` — stored in `agents.role` column AND used in TypeScript `AGENT_ROLES` array
- `"hire_agent"` — stored in `approvals.type` column AND checked in route handlers
- `"approve_ceo_strategy"` — stored in `approvals.type` column AND displayed in `ApprovalPayload.tsx`
- `"bootstrap_ceo"` — stored in `invites.invite_type` column AND checked in `InviteLanding.tsx`
- `"company"` — stored as a value in `goals.level` column AND used as a string literal in constants
- `"board"` — stored in `cli_auth_challenges.requested_access` column AND used in auth middleware

**Consequences:** Silent data incompatibility on existing installations. New rows written with the renamed value, old rows still have the old value. Code that does `WHERE type = $new_value` misses all old rows. A fresh install works; an existing install silently loses data or shows empty lists.

**Prevention:**
1. Treat every string in the Summary Risk Table (CONCERNS.md) marked "Critical" as immutable. Do not rename them, even in display contexts, without a data migration.
2. For display renaming only: change the label map (`AGENT_ROLE_LABELS`, `ApprovalPayload` display maps) without touching the underlying constant value. Rename `ceo: "CEO"` to `ceo: "Project Manager"` — the key `ceo` stays, the display label changes.
3. Before touching any string, grep for it in the schema directory (`packages/db/src/schema/`) and migration files. If it appears there, it is a stored value, not just a display string.

**Detection (warning signs):**
- Any string that also appears in `packages/db/src/schema/` or `packages/db/src/migrations/` is a stored value
- Approval, invite, and goal lists that show empty on an existing install but work on a fresh install
- TypeScript constants in `APPROVAL_TYPES`, `INVITE_TYPES`, `GOAL_LEVELS`, `AGENT_ROLES` — these feed directly into DB queries

**Phase:** Phase 1 (Display Rename). Must be resolved before any rename touches these identifiers.

---

### Pitfall 2: Treating "Display-Only Rename" as a Simple Find-Replace

**What goes wrong:** You run a bulk `sed` or IDE find-replace on "company" → "workspace" across the entire codebase to get the strings right fast. The rename touches service files, route files, schema files, and test files indiscriminately. The next `git rebase upstream/master` has conflicts on hundreds of files, most of which were upstream-compatible before.

**Why it happens:** "Display-only" is a *policy* decision, not a property the codebase enforces. Nothing in the TypeScript source distinguishes a user-facing label string from an internal identifier. Both are just string literals. A naive find-replace cannot tell `<h1>Company Settings</h1>` (display — safe to rename) from `companyService()` (code identifier — must not be renamed) from `"company"` in `GOAL_LEVELS` (stored DB value — renaming breaks data).

**Consequences:** Blown upstream sync. Every file that had `company` as a code identifier now has a conflict on rebase. The entire maintenance advantage of display-only renaming is lost. Recovering requires reverting the bulk rename and redoing it file-by-file.

**Prevention:**
1. Establish a strict three-zone taxonomy before touching any string:
   - **Zone A — Display strings**: JSX text nodes, `p.log()` CLI output, Markdown prose in onboarding assets, comment text. These are in scope.
   - **Zone B — Code identifiers**: TypeScript variable names, function names, class names, file names, import paths, package names. These are OUT of scope.
   - **Zone C — Dual-purpose stored values**: strings that are both code constants and stored in the DB (see Pitfall 1). OUT of scope for value; label-map only for display.
2. Never run a global find-replace. Work file-by-file with the zone taxonomy applied per file.
3. When unsure, ask: "Would upstream Paperclip have to change this file to fix a bug?" If yes, minimise changes to it.

**Detection (warning signs):**
- A PR diff that touches `server/src/services/`, `server/src/routes/`, or `packages/db/` with rename changes is a red flag
- A diff that shows changes to TypeScript identifier names (not string literals in JSX) is a Zone B violation
- Rebase producing conflicts in files not intentionally modified by Nexus

**Phase:** Phase 1 (Display Rename). The zone taxonomy must be documented and applied from the first commit.

---

### Pitfall 3: Diverging the Onboarding Assets Directory Name From Upstream

**What goes wrong:** You rename the `server/src/onboarding-assets/ceo/` directory to `server/src/onboarding-assets/pm/` (or similar) to match the new PM vocabulary. Upstream changes a file inside `ceo/` in a future commit. `git rebase` cannot reconcile a file renamed on one side with a content edit on the other — it presents as a delete/modify conflict and the upstream change is silently dropped.

**Why it happens:** Git rename detection is heuristic. When you rename a directory AND upstream edits a file within that directory, git frequently misidentifies this as "deleted old file + created new file" rather than "renamed file + edited renamed file." The merge resolves by keeping your renamed version and discarding upstream's content edit.

**Consequences:** You silently miss upstream improvements to agent instructions. If upstream fixes a security or correctness issue in the default agent template, your fork never gets it.

**Prevention:**
1. Do not rename the `ceo/` directory. Keep the directory path as `onboarding-assets/ceo/` in the filesystem. Only change the file *content* (the Markdown prose that says "You are the CEO").
2. The directory name `ceo/` is an internal asset path loaded by `default-agent-instructions.ts` — it is Zone B. The prose inside `SOUL.md`, `AGENTS.md`, `HEARTBEAT.md` is Zone A.
3. If a directory rename is truly necessary, document it explicitly and set up a post-rebase hook that verifies the content was not silently dropped.

**Detection (warning signs):**
- Rebase conflict shows a file as "deleted" that you expected to be "modified"
- Upstream changelog mentions onboarding asset changes but your fork's onboarding assets are unchanged after rebase

**Phase:** Phase 1 (Onboarding Redesign). Address before modifying any asset file.

---

### Pitfall 4: Changing the `localStorage` Key or `~/.paperclip` Config Path Without a Migration

**What goes wrong:** The UI stores the selected company/workspace ID in `localStorage` under the key `"paperclip.selectedCompanyId"` (identified in `CompanyContext.tsx`). If you rename this key to `"nexus.selectedWorkspaceId"`, every existing browser session loses its selected workspace on next load. Similarly, if `~/.paperclip` config path is changed to `~/.nexus` without migrating existing data, the server starts as if it were a fresh install, losing all existing agents, API keys, and worktrees.

**Why it happens:** These are persisted-state keys — they survive across deploys. Unlike code, they cannot be "renamed" by changing source; existing data already written under the old key must be read and migrated or the old key must continue to be read as a fallback.

**Consequences:** On `~/.paperclip` rename: complete data loss for the running installation. All agents, projects, API keys, and worktrees appear to vanish. On `localStorage` key rename: users are logged out of the UI on next load (minor but disorienting).

**Prevention:**
1. For `~/.paperclip`: Keep the default path OR implement a read-both-paths fallback (check `~/.nexus` first, fall back to `~/.paperclip`, emit a deprecation log). The `~/.nexus` pointer-file mechanism described in PROJECT.md should write to `~/.nexus` but read from `~/.paperclip` if `~/.nexus` does not exist.
2. For `localStorage`: Either keep the key name `"paperclip.selectedCompanyId"` (it is internal, users never see it), or write a migration on app boot that reads the old key and writes the new key before deleting the old one.
3. Treat `PAPERCLIP_*` environment variable names as immutable for the same reason — existing Docker configs and systemd units use them.

**Detection (warning signs):**
- After deploy, server logs show "no config found, starting fresh" on a machine with existing data
- UI shows empty workspace list on first load after deploy
- `docker-compose.untrusted-review.yml` still references `PAPERCLIP_HOME` after an env var rename

**Phase:** Phase 2 (Directory Restructure / `~/.nexus` Pointer). Must have migration or fallback before shipping.

---

### Pitfall 5: Upstream Rebase Cadence Slipping Below Weekly

**What goes wrong:** The fork is deployed and working. A busy week becomes two, then a month. Upstream ships 15 commits. Now the rebase involves resolving conflicts in files you modified for display renames AND new logic added upstream to the same files. What was a 10-minute weekly rebase becomes a 4-hour archaeology session. This compounds: the next month is even harder.

**Why it happens:** Fork drift is non-linear. Each upstream commit that touches a file you also modified adds another conflict to resolve. When upstream commits accumulate faster than you rebase, the conflict count grows faster than linearly because upstream changes begin to interact with each other in ways that are opaque without context.

**Consequences:** Either you stop rebasing (fork permanently diverges, missing security patches and new features) or you spend disproportionate time on merge archaeology. Community research confirms: "initial updates took minutes; later attempts required an hour or two."

**Prevention:**
1. Rebase against `upstream/master` at minimum weekly, ideally on a fixed schedule (e.g., every Sunday).
2. Keep a `[nexus]` commit prefix convention strictly — every Nexus-specific commit is prefixed. This makes it trivial to identify which commits are yours vs. rebased upstream commits during conflict resolution.
3. Run a CI check (even a local cron) that attempts `git rebase upstream/master` on a test branch and alerts on failure. Catch conflicts before they accumulate.
4. If an upstream commit touches a file you have also modified, resolve it immediately rather than deferring.

**Detection (warning signs):**
- Last rebase was more than 2 weeks ago
- `git log upstream/master..HEAD` shows more than 20 upstream commits unmerged
- Rebase produces conflicts in more than 5 files at once

**Phase:** Ongoing. Establish cadence in Phase 1; automate alert in Phase 2.

---

## Moderate Pitfalls

---

### Pitfall 6: Renaming the CLI Binary Name (`paperclipai` → `nexus`) Without a Shim

**What goes wrong:** The CLI binary is currently invoked as `pnpm paperclipai run`. The UI (`App.tsx`, `startup-banner.ts`) renders the literal string `pnpm paperclipai auth bootstrap-ceo` as instructional copy. If you rename the binary to `nexus` but forget to update every UI string that mentions `paperclipai`, users see a mix of `nexus` and `paperclipai` commands in the UI, causing confusion and failed copy-paste attempts.

**Why it happens:** The binary name appears in at least four distinct locations: `package.json` bin entry, `startup-banner.ts`, `App.tsx`, and `onboard.ts` terminal output. These are not linked by a constant. Changing the binary name in `package.json` alone does not update the rendered copy.

**Prevention:**
1. Inventory every occurrence of `paperclipai` as a user-facing command string (not package name) before renaming.
2. Consider keeping the binary named `paperclipai` and adding a `nexus` alias, so existing muscle memory and documented commands continue to work. The alias can be the primary name in Nexus docs while `paperclipai` continues to work.
3. If renaming, treat it as an atomic change: rename binary, update all instructional strings, update docs, and test the smoke tests in one commit.

**Detection (warning signs):**
- `startup-banner.ts` still says `paperclipai` after binary rename
- `ui/src/pages/App.tsx` shows mixed command names

**Phase:** Phase 1 (CLI String Updates).

---

### Pitfall 7: Partial Rename — Changing Some Occurrences But Not All

**What goes wrong:** You rename "CEO" → "Project Manager" in the `OnboardingWizard.tsx` default task description and the `AGENT_ROLE_LABELS` constant, but miss the `DEFAULT_TASK_DESCRIPTION` which starts "You are the CEO." You also miss `InviteLanding.tsx` which checks `invite.inviteType === "bootstrap_ceo"` and renders "Bootstrap your Paperclip instance." Users see a mix of "CEO" and "Project Manager" in different parts of the UI.

**Why it happens:** With no i18n layer, there is no single source of truth for any display string. "CEO" appears in at least 12 distinct files. A partial search (only checking one or two obvious files) will miss the rest. There is no compile-time check that a string has been fully replaced.

**Consequences:** Inconsistent vocabulary in the product. Users see "Project Manager" on the dashboard and "CEO" in the invite flow and onboarding wizard. This degrades trust in the product.

**Prevention:**
1. Before declaring a rename complete, run a case-insensitive `grep -r "CEO" ui/src cli/src server/src` and verify that every remaining occurrence is either: (a) intentionally kept (Zone B/C), or (b) not user-visible (e.g., an internal comment).
2. Maintain a rename checklist in `.planning/` that tracks each term and its known locations. Check off each location as it is addressed.
3. After each phase, do a full-corpus string audit for any target terms that should have been renamed.

**Detection (warning signs):**
- grep of the target term still returns JSX text nodes after the rename commit
- Onboarding flow or invite page still shows old vocabulary

**Phase:** Phase 1 (Display Rename). Checklist needed before Phase 1 is marked complete.

---

### Pitfall 8: The `[nexus]` Commit Prefix Not Applied Consistently From the Start

**What goes wrong:** Early commits are made without the `[nexus]` prefix convention. Later, when rebasing, you cannot easily distinguish "these are our changes, apply them on top of new upstream" from "this is an upstream commit we already rebased." You end up with duplicate commits or missing commits.

**Why it happens:** The prefix convention feels optional at the start when there are only a few commits. Once there are 30+ commits, inconsistent prefixing means manual archaeology to reconstruct which commits are yours.

**Prevention:**
1. Apply `[nexus]` prefix from the very first commit in the fork.
2. Add a pre-commit hook that rejects commits whose message does not start with `[nexus]` or `[upstream]` (or an equivalent marker).
3. Periodically run `git log --oneline HEAD` and verify every Nexus commit has the prefix.

**Detection (warning signs):**
- Any commit without `[nexus]` prefix in the fork's log
- Difficulty answering "which commits are mine?" during a rebase

**Phase:** Phase 1. The hook should be in place before the first Nexus commit.

---

### Pitfall 9: Onboarding Redesign Coupled to the Corporate Metaphor in Data Layer

**What goes wrong:** The new onboarding flow (root directory picker, auto-create PM + Engineer) is implemented by calling the existing `companiesApi.create()` endpoint. But the wizard's UI variables are all named `companyName`, `companyGoal`, and the new onboarding flow does not pass a "company name" at all (the user picks a directory, not a name). If you rename the variables in the wizard without considering what the API expects, the API call sends an empty or undefined `name` field, and the company is created with no name.

**Why it happens:** The onboarding redesign changes the *UX flow* (fewer steps, different inputs) but the *API shape* has not changed. The mismatch between "user provides a directory path" and "API requires a company name" must be explicitly resolved — probably by deriving the workspace name from the directory basename.

**Prevention:**
1. Document the API contract (`POST /api/companies` body shape) before redesigning the wizard. Identify every required field.
2. For fields no longer collected from the user (company name), define a derivation rule (e.g., `basename(rootDir)`) and implement it explicitly rather than relying on defaults.
3. Test the onboarding flow with a fresh database to verify no required field is silently undefined.

**Detection (warning signs):**
- Workspace created with an empty name after the new onboarding flow
- API 422 errors in the network tab after submitting the redesigned onboarding form

**Phase:** Phase 2 (Onboarding Redesign).

---

## Minor Pitfalls

---

### Pitfall 10: Forgetting to Update Tests That Assert on Display Strings

**What goes wrong:** `server/src/__tests__/invite-onboarding-text.test.ts` likely asserts that invite text contains "CEO." After renaming "CEO" to "Project Manager" in the display layer, the test fails. This is the correct outcome — the test needs to be updated — but if you do not notice it, you either ship a failing test suite or (worse) you revert the display rename to make tests pass.

**Why it happens:** Tests that assert on display strings are fragile to any vocabulary change. There is no way to know from the source that `invite-onboarding-text.test.ts` contains "CEO" assertions without reading it.

**Prevention:**
1. Before any rename commit, run `grep -r "CEO\|company\|board\|hire\|fire\|paperclip" --include="*.test.ts" cli/src server/src ui/src` to find all test files that will need updating.
2. Update the relevant tests in the same commit as the display string change — not in a follow-up commit.

**Detection (warning signs):**
- CI fails on a test whose name contains "invite", "onboarding", or "branding" after a string rename

**Phase:** Phase 1 (Display Rename). Pre-rename test audit is a prerequisite step.

---

### Pitfall 11: Exporting a `.nexus.yaml` File While Upstream Exports `.paperclip.yaml`

**What goes wrong:** If the export file format is renamed to `.nexus.yaml`, any workspace exported from a Nexus instance cannot be imported into an upstream Paperclip instance and vice versa. This breaks the stated goal of "import upstream company bundles" and creates a permanent portability split.

**Why it happens:** The export format is an identifiable artifact with a schema header (`schema: paperclip/v1`). Renaming only the file extension while keeping the schema header creates a confusing half-rename. Renaming both breaks import compatibility.

**Prevention:**
1. Keep emitting `.paperclip.yaml` and reading `.paperclip.yaml`. The filename and schema header are Zone B/C — they are part of the interchange contract with upstream.
2. If a Nexus-native export format is ever needed, emit `.nexus.yaml` as an *additional* file alongside `.paperclip.yaml`, not as a replacement.

**Detection (warning signs):**
- Attempting to import a workspace from upstream Paperclip into Nexus returns "unrecognised format" error

**Phase:** Phase 1 (Display Rename). Decide explicitly: keep `.paperclip.yaml` unchanged.

---

## Phase-Specific Warnings

| Phase Topic | Likely Pitfall | Mitigation |
|-------------|---------------|------------|
| Display rename — CEO/Board/Company strings | Pitfall 1 (dual-purpose stored values) | Rename label maps only; leave constant values (`"ceo"`, `"hire_agent"`) unchanged |
| Display rename — bulk approach | Pitfall 2 (Zone B contamination) | File-by-file using zone taxonomy; never global find-replace |
| Onboarding asset content rewrite | Pitfall 3 (directory rename breaks git rebase) | Change file content only; leave `ceo/` directory name unchanged |
| CLI binary rename `paperclipai` → `nexus` | Pitfall 6 (partial instructional string update) | Atomic commit covering all instructional copy |
| Onboarding redesign (root dir picker) | Pitfall 9 (API shape mismatch) | Document API contract first; derive workspace name from directory basename |
| `~/.nexus` pointer file mechanism | Pitfall 4 (data path migration) | Read-both-paths fallback; never rename path without migration |
| `[nexus]` commit convention | Pitfall 8 (inconsistent prefix) | Pre-commit hook from first commit |
| Upstream rebase cadence | Pitfall 5 (drift) | Weekly schedule; CI rebase check |
| Test suite after string renames | Pitfall 10 (test assertions on display strings) | Pre-rename test audit; update tests in same commit |
| Export file format | Pitfall 11 (`.paperclip.yaml` vs `.nexus.yaml`) | Keep upstream format; no rename |

---

## Sources

- Codebase analysis: `/Volumes/UsbNvme/agent/.planning/codebase/CONCERNS.md` — direct audit of Paperclip source (HIGH confidence)
- [Stop Forking Around — Fork Drift in Open Source](https://preset.io/blog/stop-forking-around-the-hidden-dangers-of-fork-drift-in-open-source-adoption/) — fork drift patterns (MEDIUM confidence)
- [Lessons Learned from Maintaining a Fork](https://dev.to/bengreenberg/lessons-learned-from-maintaining-a-fork-48i8) — exponential maintenance cost (MEDIUM confidence)
- [Friendly Fork Management — GitHub Blog](https://github.blog/2022-05-02-friend-zone-strategies-friendly-fork-management/) — sync strategies, conflict accumulation (MEDIUM confidence)
- [The Dynamic Relationship of Forks with Upstream](https://ropensci.org/blog/2025/02/20/forks-upstream-relationship/) — upstream isolation patterns (MEDIUM confidence)
