Skip to content

ADR-0005: Three Orthogonal File-Trees

  • Status: Accepted
  • Date: 2026-04-14
  • Supersedes: None

Context and Problem Statement

Three distinct concerns have historically been conflated in similar tools:

  1. What we (nubos-pilot contributors) author and commit: the source repository.
  2. What we ship to end users via npx nubos-pilot init: the install payload landing at the user's .claude/nubos-pilot/.
  3. What an end user's project accumulates as they plan and execute with nubos-pilot: STATE.md, ROADMAP.md, phase directories, todos, checkpoints, metrics.

Mixing these three concerns produces two well-known failure modes:

  • Install bit-rot: the "copy-paste N blocks to remove stale X" problem. When installer files and state files live in the same tree, "clean install" and "clean state" cannot be distinguished.
  • Contributor confusion: authors cannot tell, for a given file, whether they are editing something that ships to users, something that only contributors see, or something users will mutate at runtime.

Decision Drivers

  • Install correctness: the installer's manifest needs a distinct payload subtree to manifest against.
  • Contributor clarity: every file answers unambiguously: "do I edit this, does it ship, or does a user's project produce it?"
  • Removability: end-user uninstall must operate on one well-defined tree without sweeping scattered files.
  • Avoiding bit-rot: stale-install cleanup only works deterministically if payload boundaries are precisely defined.

Considered Options

  • Three orthogonal file-trees: Source / Install-Payload / Project-State, defined below. (CHOSEN)
  • Single tree with everything intermixed.
  • Two trees: either merge Install-Payload into Source, or merge Project-State into Install-Payload.

Decision Outcome

Chosen: "Three orthogonal file-trees". The three trees are enumerated below; each has a distinct owner, lifecycle, and runtime location.

The three trees

  1. Source tree — what lives inside the nubos-pilot git repository (this monorepo subdirectory at tools/nubos-pilot/). Owned by contributors. Lifecycle: normal git. Contains bin/ (installer + CLI helpers), lib/ (core modules), agents/, workflows/, templates/, docs/, package.json, and related author-facing artifacts. Committed.

  2. Install-Payload tree — the subset of the Source tree that is copied onto an end user's machine when they run npx nubos-pilot install. Owned by the end user after install, but managed by the installer via a manifest. Lifecycle: overwritten on npx nubos-pilot reinstall; cleaned up via npx nubos-pilot doctor --fix against the manifest. Runtime location on the end user's machine: .claude/nubos-pilot/ for Claude Code (primary), .opencode/nubos-pilot/ for OpenCode, adapter-specific paths for other runtimes.

  3. Project-State tree — state that end users' projects accumulate as they plan and execute: PROJECT.md, REQUIREMENTS.md, roadmap.yaml, STATE.md, milestones/M<NNN>/ (with its slices/S<NNN>/ and nested tasks/T<NNNN>/ directories), todos/pending/, notes/, threads/, checkpoints/, metrics/, codebase/. Owned by the end user's project. Lifecycle: mutated only through nubos-pilot workflows under a single-writer lock. Runtime location: .nubos-pilot/.

Orthogonality rule

No file lives in two trees simultaneously at runtime. A file is either source (in the contributor's repo), or payload (at the end user's install location), or state (in the end user's project), never two at once. Workflow commands operate on exactly one tree per invocation; a single commit touches files in one tree at a time (see ADR-0004).

Source-vs-Install-Payload overlap nuance

The install-payload content ORIGINATES in the Source tree; it is authored by contributors in a staging subdirectory of the repo. At install time, the installer copies those files into a distinct tree at the end user's .claude/nubos-pilot/, a different filesystem location entirely. "Orthogonality" applies to runtime locations, not to pre-install staging. On the contributor's machine, only the Source tree is populated; on the end user's machine, only the Install-Payload and Project-State trees are populated.

Consequences

  • Good: the installer has a well-defined source-of-truth for manifest generation.
  • Good: uninstall and stale-cleanup are mechanical: operate on the Install-Payload tree only; Project-State is never touched.
  • Good: git log on the nubos-pilot repo contains contributor work only, never end-user state churn.
  • Good: users can delete .nubos-pilot/ to reset their state without disturbing the installed tool, or delete .claude/nubos-pilot/ to uninstall without losing their plans.
  • Bad: contributors must remember the staging-subtree-vs-installed-payload distinction.
  • Neutral: project-state schema evolves over time; ADR-0005 is agnostic to the schema, only asserts the tree boundary.

Pros and Cons of the Options

Three orthogonal file-trees — chosen

  • Good: maps cleanly to the three distinct owners (contributor / installed-tool / user-project) without overlap.
  • Good: each tree has a single, well-defined lifecycle operation (git-commit / npx-reinstall / workflow-mutate).
  • Good: makes manifest-based install implementable without heuristic boundaries.
  • Bad: contributors must mentally track the staging-vs-installed distinction.

Single tree with everything intermixed — rejected

  • Good: "simpler" in the sense of having fewer rules.
  • Bad: reproduces the "copy-paste N blocks to remove stale X" bit-rot pattern.
  • Bad: a user who wants to reset state deletes things that look like state but may be installed-tooling.
  • Bad: manifest-based install becomes undecidable.

Two trees — rejected

  • Good: reduces the tree count, superficially simpler.
  • Bad: merge source+payload ships contributor-only artifacts (planning docs, ADRs, internal notes) to end users.
  • Bad: merge payload+state reintroduces the bit-rot failure mode exactly.

More Information

  • Related ADR: ADR-0002. The Install-Payload tree contains only .cjs + Markdown.
  • Related ADR: ADR-0004. Commits touch files in a single tree at a time.