nexus/.planning/research/PITFALLS.md
Mikkel Georgsen 6c4272ce85 [nexus] chore: migrate .planning/ from agent repo to nexus repo
Planning artifacts (milestones v1.0-v1.2.1, v1.3 queue, PROJECT.md,
STATE.md, config) now live alongside the code they describe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:42 +00:00

22 KiB

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 (paperclipainexus) 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 paperclipainexus 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