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>
364 lines
18 KiB
Markdown
364 lines
18 KiB
Markdown
# 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*
|