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>
18 KiB
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:
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:
# 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:
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:
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:
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.
// 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):
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):
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(withceo: "CEO") — the authoritative map, untouchedAGENT_ROLESarray containing"ceo"— these are stored values, not display stringsAPPROVAL_TYPES,INVITE_TYPES— stored DB enum values, untouchedAPI.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):
/* 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:
// 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:
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 schemapackages/db/src/migrations/— migration SQLpackages/shared/src/constants.ts— stored enum valuespackages/shared/src/api.ts— route constantsserver/src/routes/— API route handlers- Any
package.json"name"field other than the new branding package pnpm-workspace.yaml(except to addpackages/branding)- Any TypeScript identifier (function name, variable name, class name)
Sources
- History-preserving fork maintenance with git
- GitHub: Strategies for friendly fork management
- VSCodium Build System — DeepWiki
- Git range-diff documentation
- Git rerere — Pro Git book
- Mastering Git Rerere — This Dot Labs
- Vite define option
- Tailwind CSS v4 + Vite — CSS custom properties theming
- A Scalable Text Management Pattern — React Context + TypeScript
- TypeScript Record pattern for display labels
- How to Synchronize Your Fork with Upstream Changes
Stack research: 2026-03-30