# Technology Stack: Fork Maintenance Approach

**Project:** Nexus (fork of Paperclip)
**Researched:** 2026-03-30
**Scope:** Safely maintaining a display-layer fork of a TypeScript monorepo while staying rebassable on upstream

---

## Summary Recommendation

Use **git rebase with a [nexus] commit prefix convention** for fork maintenance. Extract all display strings into **a single `packages/branding/` package** that acts as the exclusive mutation surface. Keep every code identifier, route, schema, and package name unchanged. This combination minimises conflict surface to two file types: branding constants and onboarding assets.

---

## 1. Fork Maintenance Strategy

### Recommended: Rebase-Over-Upstream with Prefix Convention

**Confidence: HIGH** — Used by git-for-windows, microsoft/git, and VSCodium. Standard practice for long-lived forks.

**How it works:**

Every Nexus-specific commit carries a `[nexus]` prefix in the commit message. On each upstream release:

```bash
git fetch upstream
git rebase upstream/master
```

During rebase, conflicts only appear on commits that touch the same lines as upstream changes. With display-only mutations (string constants, Markdown prose, one config file), the conflict surface is tiny. Non-conflicting commits replay cleanly.

**Commit message convention:**
```
[nexus] Rename CEO→Project Manager in OnboardingWizard
[nexus] Replace AGENT_ROLE_LABELS display value for ceo role
[nexus] Rewrite onboarding-assets/ceo/ SOUL.md and AGENTS.md
```

The prefix does two things: it makes `[nexus]` commits immediately identifiable in `git log`, and it allows `git range-diff` to verify that a rebase correctly replayed all downstream patches.

**Verification after every upstream sync:**

```bash
# Compare the old and new version of the downstream patch series
git range-diff upstream/master ORIG_HEAD HEAD
```

`git range-diff` shows which `[nexus]` commits changed during rebase (conflict resolutions), which replayed identically, and which were dropped. This is the standard tool used by the Git project itself for patch-series validation. **Confidence: HIGH** (official Git tooling, not a third-party tool).

**Enable rerere to auto-replay recurring resolutions:**

```bash
git config rerere.enabled true
```

`git rerere` records how each conflict was resolved. On the next upstream sync, if the same conflict hunk appears again (common when upstream frequently touches the same area), Git auto-resolves it identically. This eliminates repetitive manual conflict resolution. **Confidence: HIGH** (official Git feature, described in Pro Git book).

**Atomic commits — most important discipline:**

Each `[nexus]` commit must touch exactly one logical unit. Never mix a display-string change with a behaviour change in the same commit. Rationale: if upstream changes the same file for a different reason, a mixed commit creates conflicts in code paths you didn't mean to touch. Atomic commits mean a conflict only appears on the exact line you changed. **Confidence: HIGH** (documented in git-for-windows strategy and GitHub's friendly fork guide).

---

### Alternative Considered: git-format-patch / Quilt-style Patch Queue

**What it is:** Maintain Nexus changes as a series of `.patch` files outside the tree, applied on top of a clean upstream checkout. Used by VSCodium for build-time patch application with placeholder substitution.

**Why not for Nexus:** VSCodium's patch approach works because they rebuild from source on every release. Nexus is a live development fork where engineers commit code daily. Applying patches at build time would break the normal `git commit` / `git push` workflow. Rebase-over-upstream is the right model when the fork is being actively developed, not just rebranded at release time.

**Confidence: MEDIUM** — VSCodium's approach is well-documented but architecturally different from a dev fork.

---

### Alternative Considered: Merge (not rebase)

Merge upstream with `git merge upstream/master` produces a merge commit that interleaves upstream and Nexus history. GitHub's friendly fork guide recommends merge for multi-contributor forks. For a solo-developer fork with a small, clearly bounded patch set, rebase produces a cleaner history and makes it obvious exactly which commits are Nexus-specific. Use merge only if the team grows beyond one or two contributors.

---

## 2. String Extraction Pattern

### Recommended: Centralised Branding Package with Typed Constants

**Confidence: HIGH** — Standard TypeScript monorepo pattern, no third-party risk.

#### Why NOT i18n (react-i18next, LinguiJS, etc.)

i18n libraries are designed for multi-locale text management. They add runtime overhead, require JSON translation files, and introduce a dependency that Paperclip upstream does not have. Importing one into a display-layer fork creates a new package.json entry that will conflict if upstream ever adds i18n itself. The simpler approach is a plain TypeScript constants module.

#### The Pattern: `packages/branding/`

Create a dedicated workspace package at `packages/branding/` that is the single place all display-layer strings live. Nothing else in the monorepo hardcodes Nexus-facing strings.

**Package structure:**

```
packages/branding/
  src/
    index.ts          -- re-exports everything
    vocabulary.ts     -- entity names (Workspace, Project Manager, Owner)
    ui-labels.ts      -- button text, page titles, sidebar labels
    cli-strings.ts    -- CLI output messages, prompts, banner
    agent-roles.ts    -- display labels for role constants
  package.json        -- name: "@paperclipai/branding" (keeps @paperclipai namespace)
  tsconfig.json
```

**`vocabulary.ts` example:**

```typescript
export const VOCAB = {
  // The Company entity displayed as:
  company: {
    singular: "Workspace",
    plural: "Workspaces",
    possessive: "Workspace's",
  },
  // The CEO role displayed as:
  ceo: {
    singular: "Project Manager",
    short: "PM",
  },
  // The Board role displayed as:
  board: {
    singular: "Owner",
  },
  // Product name
  product: {
    name: "Nexus",
    cli: "nexus",
    tagline: "Your agent workspace",
  },
} as const;
```

**`agent-roles.ts` example — overrides `AGENT_ROLE_LABELS` from shared:**

```typescript
import { AGENT_ROLE_LABELS } from "@paperclipai/shared";

// Override display labels only. Underlying keys (ceo, engineer, etc.) are unchanged.
export const DISPLAY_ROLE_LABELS: typeof AGENT_ROLE_LABELS = {
  ...AGENT_ROLE_LABELS,
  ceo: "Project Manager",
};
```

**Why keep the package name `@paperclipai/branding`:** The `@paperclipai/*` namespace is used by thousands of import statements. Adding a new package under the same namespace costs nothing and avoids the namespace change that would ripple through every file. The branding package is net-new; it does not rename any existing package.

**Usage in UI:**

Components import from `@paperclipai/branding` instead of hardcoding strings. The existing `AGENT_ROLE_LABELS` from `@paperclipai/shared` stays unchanged; components use `DISPLAY_ROLE_LABELS` from branding instead.

```tsx
// Before (upstream hardcoded):
<span>Company</span>
<span>{AGENT_ROLE_LABELS[agent.role]}</span>

// After (Nexus):
import { VOCAB, DISPLAY_ROLE_LABELS } from "@paperclipai/branding";
<span>{VOCAB.company.singular}</span>
<span>{DISPLAY_ROLE_LABELS[agent.role]}</span>
```

**Usage in CLI (`cli/src/commands/onboard.ts`):**

```typescript
import { VOCAB } from "@paperclipai/branding";

p.intro(`${VOCAB.product.name} setup`);
// Replaces: p.intro("Paperclip setup");
```

**Usage in server banner (`server/src/startup-banner.ts`):**

```typescript
import { VOCAB } from "@paperclipai/branding";

// Replace ASCII art "PAPERCLIP" with "NEXUS"
// Replace embedded CLI command text with VOCAB.product.cli references
```

#### What Stays in `@paperclipai/shared` — Unchanged

The following stay exactly as upstream to preserve upstream rebasability:

- `AGENT_ROLE_LABELS` (with `ceo: "CEO"`) — the authoritative map, untouched
- `AGENT_ROLES` array containing `"ceo"` — these are stored values, not display strings
- `APPROVAL_TYPES`, `INVITE_TYPES` — stored DB enum values, untouched
- `API.companies = "/api/companies"` — route constants, untouched

The branding package only **overrides at the callsite**, never modifying shared constants.

---

## 3. UI Branding / Theming Layer

### Recommended: CSS Custom Properties in Tailwind v4 + a Single `branding.css` File

**Confidence: HIGH** — Tailwind v4's CSS-first config model is designed for this. Official Vite + Tailwind v4 docs confirm CSS custom properties as the standard.

Paperclip already uses Tailwind CSS 4.0.7. In Tailwind v4, theme tokens are defined as CSS custom properties in the CSS file, not in a JavaScript config. This makes branding overrides a single CSS file change.

**`ui/src/branding.css` (new [nexus] file):**

```css
/* Nexus brand overrides — Tailwind v4 custom properties */
:root {
  --color-brand-primary: oklch(65% 0.2 270); /* Nexus blue-purple */
  --color-brand-secondary: oklch(75% 0.15 200);
}
```

Import this file once in `ui/src/main.tsx` after the main Tailwind CSS import. Zero upstream conflict risk: it is a net-new file.

**Vite `define` for build-time constants:**

For values injected at build time (version strings, product name in `<title>` tag), use Vite's `define` option in `vite.config.ts`:

```typescript
// vite.config.ts — [nexus] section
define: {
  __NEXUS_PRODUCT_NAME__: JSON.stringify("Nexus"),
  __NEXUS_VERSION__: JSON.stringify(process.env.npm_package_version),
},
```

Declare the type in `ui/src/vite-env.d.ts`:

```typescript
declare const __NEXUS_PRODUCT_NAME__: string;
```

Use this only for values that must appear in static HTML before React hydrates (e.g. `<title>` tag, meta tags). Component-level strings should use the branding package, not `define`.

**Why not a full Catppuccin Mocha theme in v1:** Full theme overhaul is listed as out-of-scope in PROJECT.md. CSS custom properties allow it to be added later as a single-file change.

---

## 4. Onboarding Assets — Separate Files, Zero Code Conflict

### Recommended: Direct File Replacement, No Pattern Needed

**Confidence: HIGH** — This is already how the codebase works.

The files in `server/src/onboarding-assets/ceo/` (SOUL.md, AGENTS.md, HEARTBEAT.md, TOOLS.md) are plain Markdown loaded at runtime via `fs.readFile`. They contain the hardcoded "You are the CEO" prose that must change for Nexus.

**Strategy:** Replace these files entirely as a `[nexus]` commit. The directory name `ceo/` stays unchanged (directory rename would cause upstream conflicts on every change upstream makes to these files). The file content changes. These files are prose with no TypeScript identifiers — conflict risk is purely editorial (if upstream rewrites the CEO instructions, the rebase will conflict on the content, which is a genuine conflict to resolve manually).

**For new Nexus-specific agent templates** (PM and Engineer predefined templates), add new directories:

```
server/src/onboarding-assets/
  ceo/          -- upstream directory, content replaced by [nexus]
  pm/           -- [nexus] new directory, PM template
  engineer/     -- [nexus] new directory, Engineer template
```

New directories are never touched by upstream; they replay through rebase with zero conflicts.

---

## 5. What NOT to Do — Anti-Patterns

### Anti-Pattern 1: Rename any `@paperclipai/*` package

**What happens:** Every TypeScript file in the monorepo imports from `@paperclipai/shared`, `@paperclipai/db`, etc. Renaming any of these produces thousands of lines of import-statement diffs across every file. On the next upstream rebase, every one of those files conflicts because upstream and Nexus both modified the imports (upstream: added a new function, Nexus: changed the import path). This turns a clean rebase into a multi-hour conflict session on every upstream release.

**Instead:** Keep all `@paperclipai/*` names. The new branding package is `@paperclipai/branding` — same namespace, no existing files modified.

### Anti-Pattern 2: Rename TypeScript identifiers (`companyService`, `CompanyContext`, etc.)

**What happens:** If `companyService` is renamed to `workspaceService` in Nexus, any upstream commit that touches `companies.ts` will produce a conflict at that identifier. The function is the same; only the name differs. This is a pure noise conflict with zero semantic value.

**Instead:** Leave all identifiers unchanged. `CompanyContext` stays `CompanyContext` internally; only the string it renders in JSX changes.

### Anti-Pattern 3: Scatter display strings across individual component files

**What happens:** If each component file hardcodes its own Nexus strings (`<span>Workspace</span>` scattered across 30 files), every upstream change to a component file produces a conflict on the string line. Finding and resolving these becomes the dominant cost of each sync.

**Instead:** All display strings live in `packages/branding/`. Each component imports one constant. Upstream touches component logic; Nexus touches the branding package. File overlap is minimised.

### Anti-Pattern 4: Change DB column names, stored enum values, or API routes

**What happens:** These are breaking changes with migration requirements. They also conflict with upstream on every schema or route change.

**Instead:** These are already out-of-scope per PROJECT.md. The ORM layer stays `companies`, `company_id`, `"ceo"` role. The branding package translates at display time.

### Anti-Pattern 5: Mix Nexus and upstream changes in one commit

**What happens:** If a `[nexus]` commit also contains an upstream bug fix, the bug fix becomes entangled with the display change. On rebase, if upstream fixes the same bug, there is a conflict in a commit that was supposed to be a display-only patch.

**Instead:** If a bug fix is needed, create a separate commit without the `[nexus]` prefix. Consider submitting it upstream. Keep `[nexus]` commits purely display-layer.

### Anti-Pattern 6: Rename `~/.paperclip` to `~/.nexus` (data directory)

**What happens:** Requires changing `PAPERCLIP_HOME` environment variable references across server, CLI, Docker files, and documentation. Breaks all existing deployments. Creates conflicts on every upstream change touching home-path logic.

**Instead:** Use `~/.nexus` as a pointer file only (containing the root directory path), as described in PROJECT.md. The actual data directory stays `~/.paperclip`. The `~/.nexus` pointer file is a net-new file; upstream never touches it.

---

## 6. Tooling Summary

| Tool | Purpose | Confidence |
|------|---------|------------|
| `git rebase upstream/master` | Sync with upstream releases | HIGH |
| `[nexus]` commit prefix | Identify all downstream-only commits | HIGH |
| `git range-diff` | Verify rebase replayed all patches correctly | HIGH |
| `git rerere` | Auto-resolve recurring conflict patterns | HIGH |
| `packages/branding/` package | Single mutation surface for display strings | HIGH |
| `ui/src/branding.css` | CSS custom property overrides for Tailwind v4 | HIGH |
| `vite.config.ts define` | Build-time product name injection for static HTML | HIGH |

---

## 7. File Mutation Surface (Complete List)

Files that `[nexus]` commits are permitted to touch, and the rationale:

| File / Directory | Change Type | Upstream Conflict Risk |
|------------------|------------|----------------------|
| `packages/branding/` (new) | Create entire package | None — net new |
| `ui/src/branding.css` (new) | Create branding CSS | None — net new |
| `server/src/onboarding-assets/ceo/*.md` | Replace prose content | Low — prose-level conflict only if upstream rewrites instructions |
| `server/src/onboarding-assets/pm/` (new) | Create PM template | None — net new |
| `server/src/onboarding-assets/engineer/` (new) | Create Engineer template | None — net new |
| `ui/src/components/OnboardingWizard.tsx` | Replace JSX strings with branding imports | Medium — upstream actively modifies onboarding |
| `ui/src/pages/App.tsx` | Replace CLI command strings | Low — static text, rarely changed |
| `server/src/startup-banner.ts` | Replace ASCII art and startup text | Low — rarely changed |
| `cli/src/commands/onboard.ts` | Replace terminal output strings | Medium — onboarding logic changes |
| `vite.config.ts` | Add `define` block | Low — config changes rarely conflict |
| `ui/index.html` | Update `<title>` tag | Low — rarely touched |

Files that `[nexus]` commits must NEVER touch:

- `packages/db/src/schema/` — DB schema
- `packages/db/src/migrations/` — migration SQL
- `packages/shared/src/constants.ts` — stored enum values
- `packages/shared/src/api.ts` — route constants
- `server/src/routes/` — API route handlers
- Any `package.json` `"name"` field other than the new branding package
- `pnpm-workspace.yaml` (except to add `packages/branding`)
- Any TypeScript identifier (function name, variable name, class name)

---

## Sources

- [History-preserving fork maintenance with git](https://amboar.github.io/notes/2021/09/16/history-preserving-fork-maintenance-with-git.html)
- [GitHub: Strategies for friendly fork management](https://github.blog/developer-skills/github/friend-zone-strategies-friendly-fork-management/)
- [VSCodium Build System — DeepWiki](https://deepwiki.com/VSCodium/vscodium/2-build-system)
- [Git range-diff documentation](https://git-scm.com/docs/git-range-diff)
- [Git rerere — Pro Git book](https://git-scm.com/book/en/v2/Git-Tools-Rerere)
- [Mastering Git Rerere — This Dot Labs](https://www.thisdot.co/blog/mastering-git-rerere-solving-repetitive-merge-conflicts-with-ease)
- [Vite define option](https://vite.dev/config/shared-options#define)
- [Tailwind CSS v4 + Vite — CSS custom properties theming](https://medium.com/render-beyond/build-a-flawless-multi-theme-ui-using-new-tailwind-css-v4-react-dca2b3c95510)
- [A Scalable Text Management Pattern — React Context + TypeScript](https://nicholasgalante1997.medium.com/a-scalable-text-management-pattern-for-web-developers-with-react-context-and-typescript-5b26aacceceb)
- [TypeScript Record pattern for display labels](https://dev.to/naserrasouli/mastering-record-in-typescript-the-clean-way-to-map-enums-to-labels-and-colors-46bh)
- [How to Synchronize Your Fork with Upstream Changes](https://nhutduong.com/blog/how-to-synchronize-your-fork-repository-with-upstream-changes/)

---

*Stack research: 2026-03-30*
