nexus/.planning/research/ARCHITECTURE.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

16 KiB

Architecture Patterns: Display-Layer Fork Isolation

Domain: TypeScript monorepo fork (Paperclip → Nexus) Researched: 2026-03-30 Confidence: HIGH — based on direct codebase inspection + verified patterns


The core constraint is: every file Nexus touches is a potential rebase conflict site. The architecture goal is therefore to minimize the number of upstream files modified by concentrating all fork-specific content into new files that upstream will never create.

Isolation Strategy: Minimal-Touch with Fork Overlay

Upstream files          Fork overlay files
─────────────          ──────────────────
constants.ts      →    [keep AGENT_ROLE_LABELS.ceo = "CEO", change display via wrapper]
CompanyRail.tsx   →    (modify inline — unavoidable, low risk)
OnboardingWizard  →    nexus/OnboardingWizard.nexus.tsx  (new file, rewire import)
onboard.ts (CLI)  →    modify inline strings only (no logic change)
SOUL.md / AGENTS  →    replace file content (same path, different content)

Two categories of change, each with a different isolation strategy:

Category Strategy Conflict Risk
New files added by Nexus Add-only (upstream never touches these) Zero
Upstream files with string changes Inline edit, minimal diff Low — strings rarely conflict
Upstream files requiring logic changes Wrapper/replacement file, rewire import Medium — requires Vite alias or import swap

Component Boundaries

Component Responsibility Fork Change Type
ui/src/lib/nexus-labels.ts Central display-string registry (NEW file) New file — zero conflict risk
ui/src/components/OnboardingWizard.tsx Multi-step first-run UX Inline rewrite — file is owned by Nexus entirely
packages/shared/src/constants.ts AGENT_ROLE_LABELS map Inline string change only — change ceo: "CEO" to ceo: "Project Manager"
ui/src/pages/Companies.tsx "New Company" button, "Companies" breadcrumb Inline string change — 2-3 occurrences
cli/src/commands/onboard.ts Terminal output strings Inline string change — no logic change
server/src/onboarding-assets/ceo/ PM agent template content File content replacement — same paths
server/src/home-paths.ts .paperclip.nexus home dir Inline constant change — single string
ui/src/components/CompanyRail.tsx Sidebar rail icon (Paperclip lucide icon) Single import swap

Isolation Pattern 1: Central Label Registry (New File)

Create ui/src/lib/nexus-labels.ts as a new file. This file is pure Nexus — upstream will never create it, so it can never conflict.

// ui/src/lib/nexus-labels.ts  [NEXUS-OWNED FILE]
// Central display vocabulary. Never referenced by upstream.
// All UI components import from here instead of hardcoding strings.

export const NEXUS_LABELS = {
  // Entity names
  workspace: "Workspace",
  workspaces: "Workspaces",
  projectManager: "Project Manager",
  owner: "Owner",

  // Actions
  addAgent: "Add Agent",
  removeAgent: "Remove Agent",

  // Onboarding
  onboardingRootPrompt: "Choose your root directory",
  onboardingTitle: "Welcome to Nexus",

  // App identity
  appName: "Nexus",
  cliCommand: "nexus",
} as const;

Usage pattern in existing components: Import NEXUS_LABELS and replace the hardcoded string. The diff in the upstream file is minimal — a one-line import addition and a string substitution.

Conflict profile: The import addition is a single new line at the top of the file. String substitutions are isolated to specific JSX attributes. These lines are unlikely to be touched by upstream changes because upstream will not add an import from nexus-labels.


Isolation Pattern 2: Inline String Replacement (Low-Conflict Edits)

For files with a small number of hardcoded display strings, edit inline with targeted changes. Prefix all changed lines with a // [nexus] comment on the preceding line so they are trivially identified during rebase conflict resolution.

Example — packages/shared/src/constants.ts line 53:

// [nexus] display label override
ceo: "Project Manager",

Example — ui/src/pages/Companies.tsx line 72:

// [nexus] breadcrumb rename
setBreadcrumbs([{ label: "Workspaces" }]);

Example — ui/src/pages/Companies.tsx line 96:

// [nexus] button rename
New Workspace

The // [nexus] marker serves three purposes:

  1. Identifies fork-owned lines during git diff triage
  2. Signals to the developer during a rebase conflict which side is Nexus vs upstream
  3. Enables grep -r '\[nexus\]' to produce a complete inventory of modified lines at any time

Isolation Pattern 3: File Content Replacement (Onboarding Assets)

The server/src/onboarding-assets/ceo/ files (SOUL.md, AGENTS.md, HEARTBEAT.md, TOOLS.md) are plain prose. They have no code entanglement. Replace their content entirely.

Strategy: Keep the same file paths. Write Nexus-specific content. Upstream changes to these files will produce conflicts, but:

  • Upstream changes to ceo/SOUL.md are relatively rare (onboarding prose is stable)
  • When conflicts occur, resolution is manual prose review — not code logic
  • The directory itself is not renamed (ceo/ stays ceo/) to avoid path-level conflicts

PM and Engineer templates: Add new template subdirectories under server/src/onboarding-assets/:

  • server/src/onboarding-assets/pm/ — new directory, zero conflict risk
  • server/src/onboarding-assets/engineer/ — new directory, zero conflict risk

Isolation Pattern 4: Build-Time File Swap via Vite Alias (High-Complexity Components)

For components that require substantial structural changes (primarily OnboardingWizard.tsx), use Vite's resolve.alias to swap the import at build time. This keeps the upstream file untouched.

Existing Vite config (ui/vite.config.ts) already uses resolve.alias:

resolve: {
  alias: {
    "@": path.resolve(__dirname, "./src"),
    lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
  },
},

Add a Nexus override alias:

// ui/vite.config.ts  [nexus]
resolve: {
  alias: {
    "@": path.resolve(__dirname, "./src"),
    lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
    // [nexus] component overrides
    "@/components/OnboardingWizard": path.resolve(
      __dirname, "./src/nexus/OnboardingWizard.tsx"
    ),
  },
},

New file: ui/src/nexus/OnboardingWizard.tsx — entirely Nexus-owned, never conflicts.

Upstream file: ui/src/components/OnboardingWizard.tsx — left unmodified. Any upstream updates to it are absorbed without conflict because the alias bypasses it.

Tradeoff: This pattern is only worth the complexity for large rewrites (100+ lines changed). For small string changes, inline edits are lower overhead. Apply to OnboardingWizard.tsx only.

Confidence: HIGH — Vite alias file swapping is a documented pattern used in white-label React apps. The existing config already demonstrates the alias syntax.


Isolation Pattern 5: Home Directory Pointer Mechanism

The ~/.nexus pointer file is Nexus-specific infrastructure. The approach:

  1. Modify server/src/home-paths.ts — change the single default string ".paperclip" to ".nexus". This is a one-line change; conflict risk is minimal because upstream rarely changes default paths.

  2. Create ~/.nexus as a single-line text file containing the root path. This is runtime data, not code.

  3. The PAPERCLIP_HOME env var override still works — Nexus does not rename it (display-only constraint).

Inline change in server/src/home-paths.ts:

// [nexus] home dir rename
const DEFAULT_HOME = ".nexus";

Data Flow: How Changes Propagate

nexus-labels.ts (NEW)
  └── imported by: Companies.tsx, CompanyRail.tsx, InstanceSidebar.tsx, etc.
      └── display strings centralized — upstream files only gain one import line

constants.ts (MODIFIED, minimal)
  └── AGENT_ROLE_LABELS.ceo = "Project Manager"
      └── used by: AgentConfigForm.tsx, NewAgent.tsx, ApprovalPayload.tsx
          └── no other changes needed in those files

OnboardingWizard.nexus.tsx (NEW)
  └── aliased via vite.config.ts (one-line alias addition)
      └── upstream OnboardingWizard.tsx untouched

onboarding-assets/ceo/*.md (MODIFIED content, same paths)
  └── loaded by default-agent-instructions.ts (unchanged)

onboarding-assets/pm/ (NEW directory)
onboarding-assets/engineer/ (NEW directory)
  └── loaded by new template selector in OnboardingWizard.nexus.tsx

Anti-Patterns to Avoid

Anti-Pattern 1: Renaming Upstream Files or Directories

What: Renaming CompanyRail.tsxWorkspaceRail.tsx, or onboarding-assets/ceo/onboarding-assets/pm/ Why bad: Git tracks renames as delete + add. During git rebase upstream/master, if upstream makes changes to CompanyRail.tsx, the patch will not apply to WorkspaceRail.tsx. You get an unresolved conflict that requires manual merge of the upstream diff into the renamed file. Instead: Keep all upstream file paths. Use wrapper files or content replacement. Reserve new names for new files only.

Anti-Pattern 2: Renaming TypeScript Identifiers in Upstream Files

What: Renaming companyServiceworkspaceService, CompanyContextWorkspaceContext Why bad: Any upstream commit touching those files produces a merge conflict on every renamed symbol. The conflict surface grows proportionally to how many usages exist (currently hundreds of import sites). Instead: Leave all identifiers unchanged. The mapping from internal name to display name happens in nexus-labels.ts and the AGENT_ROLE_LABELS constant only.

Anti-Pattern 3: Squashing All Nexus Commits

What: Maintaining Nexus changes as a single squashed "fork" commit Why bad: During git rebase upstream/master, all conflicts appear in one commit resolution session, making them impossible to isolate. A single upstream change to constants.ts forces you to re-resolve every Nexus change in that file simultaneously. Instead: Keep one atomic [nexus] commit per change area (labels, onboarding, home dir, templates). Small commits rebase cleanly. Conflicts are isolated.

Anti-Pattern 4: Package Name Renames

What: @paperclipai/shared@nexusai/shared Why bad: Every upstream file that imports from @paperclipai/* will conflict because Nexus has rewritten the import path. This is effectively every file in the monorepo. Instead: Keep all @paperclipai/* package names. This is explicitly in scope as "out of scope" in PROJECT.md.

Anti-Pattern 5: Centralizing All Changes in One File

What: Putting all Nexus overrides in constants.ts or App.tsx Why bad: High-traffic upstream files accumulate the most conflicts. Concentrating fork changes there maximizes conflict exposure. Instead: Prefer adding new files (zero conflict risk) over modifying high-traffic upstream files.


Scalability Considerations

Concern Now (v1) Future upstream rebases
Label changes 1 constants.ts edit + nexus-labels.ts nexus-labels.ts never conflicts; constants.ts conflict is isolated to 1 line
Onboarding OnboardingWizard aliased via Vite Upstream OnboardingWizard changes ignored automatically
Template content ceo/ files replaced in-place Manual prose merge if upstream edits ceo/ — rare
New upstream entities Zero action needed New upstream files have no Nexus edits
New Nexus features Add to nexus/ directory Zero conflict risk — new files only

Implementation Order (Least to Most Conflict Risk)

This order ensures each phase can be validated and rebased independently before the next phase adds more change surface.

Phase 1 — Foundation (zero upstream file changes)

  1. Create ui/src/nexus/ directory
  2. Create ui/src/lib/nexus-labels.ts with full label registry
  3. Create server/src/onboarding-assets/pm/ and engineer/ template directories
  4. Add [nexus] commit: "add nexus overlay directory and label registry"

Why first: Establishes the containment structure with no upstream file touches. Safe to rebase at any point.

Phase 2 — Constants and Labels (1 upstream file, 1-line change)

  1. Edit packages/shared/src/constants.ts — change ceo: "CEO" to ceo: "Project Manager" in AGENT_ROLE_LABELS
  2. Add [nexus] commit: "rename CEO display label to Project Manager"

Why second: Single file, single line. Easiest conflict to resolve if upstream touches the same line.

Phase 3 — Home Directory (1 upstream file, 1-line change)

  1. Edit server/src/home-paths.ts — change default home dir string to .nexus
  2. Edit cli/src/config/home.ts — same change
  3. Add [nexus] commit: "change default home dir from .paperclip to .nexus"

Why third: Low-risk lines. Home dir defaults are very rarely changed by upstream.

Phase 4 — UI String Renames (several upstream files, inline strings only)

  1. Edit ui/src/pages/Companies.tsx — rename "Companies" breadcrumb and "New Company" button to "Workspaces" / "New Workspace"
  2. Edit ui/src/components/CompanyRail.tsx — swap Paperclip lucide icon for a different icon
  3. Edit ui/src/pages/CompanySettings.tsx, InstanceSidebar.tsx — display-string renames
  4. Edit cli/src/commands/onboard.ts — terminal output strings
  5. One [nexus] commit per file changed

Why fourth: More files touched, but changes are string-only. Each commit is independently rebaseable. // [nexus] markers make conflict resolution mechanical.

Phase 5 — Onboarding Redesign (Vite alias + new file)

  1. Add Vite alias in ui/vite.config.ts pointing OnboardingWizard to nexus/OnboardingWizard.tsx
  2. Write ui/src/nexus/OnboardingWizard.tsx as a full replacement (root dir picker, PM + Engineer auto-create)
  3. Replace server/src/onboarding-assets/ceo/ file content with PM-framed prose
  4. One [nexus] commit: "redesign onboarding for single-dev workspace flow"

Why last: Most complex change. The Vite alias approach means upstream OnboardingWizard.tsx can evolve freely without conflicting. Template content is the highest natural-language conflict risk but lowest structural risk.


Rebase Workflow

# Pull upstream changes
git fetch upstream
git rebase upstream/master

# For each [nexus] commit, git will pause on conflicts.
# Expected conflict files per phase:
#   Phase 2: packages/shared/src/constants.ts (1 line)
#   Phase 3: server/src/home-paths.ts, cli/src/config/home.ts (1 line each)
#   Phase 4: ui/src/pages/*.tsx, cli/src/commands/onboard.ts (string lines)
#   Phase 5: server/src/onboarding-assets/ceo/*.md (prose), ui/vite.config.ts (1 line)
#
# Resolution rule: keep [nexus] version for any line marked // [nexus]
#                  accept upstream for everything else

# After rebase, verify no Nexus labels reverted:
grep -r '\[nexus\]' /Volumes/UsbNvme/repos/nexus --include="*.ts" --include="*.tsx"

Sources