Constructdocs
section · guides

Project scopes — .construct, the state root, and user home

Where Construct writes state, what belongs in git, and how intake differs from the tool install.

5 min read·guides / concepts / project-scopes

Construct splits state across four roots. Mixing them is the usual source of “why is this in git?” or “why did my disk fill up?” confusion.

The three-way split (ADR-0074, ADR-0066)

A host project directory carries a single Construct-authored directory — .construct/ — plus one deliberate exception (.beads/). Everything Construct writes into the project lives under .construct/ and falls into three kinds:

  1. Config-layer (committed text)construct.config.json, .construct/context.md, and user-authored custom specialists/teams under .construct/org/, plus ADR-0027's marker blocks (AGENTS.md/CLAUDE.md fenced regions). Small, human-legible, meant to travel with the repo. .construct/org/ is carved back out of the otherwise-ignored .construct/ tree by a .gitignore negation so custom specialists stay version-controlled.
  2. Machine-scoped heavy state — traces, orchestration runs, worker logs, the LanceDB vector index, and the docling/whisper bootstrap venvs. None of this is project-relative any more: it lives at ~/.construct/projects/\<key>/, keyed by a derivation stable across every clone/worktree of the same git remote (ADR-0066), resolved through lib/state-root.mjs's resolveStateRoot/resolveStateDir/resolveStatePath. A fresh construct init never scaffolds these directories — they appear only once the corresponding subsystem first writes.
  3. Beads (.beads/) — the deliberate exception. Issue history is project content, not machine state (ADR-0026), so it stays project-local and travels with the repository regardless of size.

Everything else Construct writes under a project's .construct/ is a small, project-local runtime marker — intake state, the living graph, oracle verdicts, observations, knowledge, workflow/task-graph JSON — gitignored but not "heavy" in ADR-0066's sense (none of it was migrated; each has enough independent readers duplicating its path that moving the writer alone would split-brain reader and writer, so migration is deliberately deferred category by category). The project launcher — the dependency-free run.mjs shim that hooks invoke, plus its version pin and cached binary — lives under .construct/launcher/, staged by construct init and regenerated by construct sync (ADR-0074).

Before ADR-0074 the project footprint was split across two directories — .construct/ (launcher only) and .cx/ (config + runtime markers). They are now one .construct/, with the launcher demoted to the .construct/launcher/ subdirectory. Detection still accepts a legacy .cx/ marker during the transition window.

Scope table

LocationScopePurposeCommit to git?
templates/**Package (Construct repo)Shipped demos, scripts, PDF themes, doc templatesYes
docs/**Package / project docsADRs, cookbooks, durable research that informed decisionsYes
.construct/ (project root)Host project — config-layer + small runtime markerscontext.md/context.json, workflow.json, org/ (committed), intake, oracle, observations, knowledge, the living graph, project research briefs, recorded demosNo (gitignored, except .construct/org/)
.construct/launcher/ (project root)Host project — launcherrun.mjs shim hooks invoke, version pin, cached binary, bootstrap shimsNo (gitignored)
.beads/ (project root)Host project — issue historyDolt-backed issue database (ADR-0026)Yes (git-native sync)
~/.construct/projects/\<key>/User machine, per-projectTraces, orchestration runs, worker logs, the LanceDB vector index, docling/whisper bootstrap venvs (ADR-0066)No
~/.construct/User machineconfig.env, credentials, doctor state, auth tokensNo
~/.cx/User machineCross-project telemetry, role-pending queue, embed daemon logs when no project markerNo

Project marker: a directory containing .construct/ (or a legacy .cx/) at the project root (lib/project-root.mjs, lib/config-dir.mjs). Writers call resolveProjectScopedPath() to target \<project>/.construct/\<file>; without a marker they fall back to ~/.cx/. Writers that resolve through lib/state-root.mjs instead target ~/.construct/projects/\<key>/\<file>, keyed off the same project root rather than a marker. Path construction goes through the lib/config-dir.mjs resolver (configPath/launcherPath), never a hardcoded directory name.

What lives under .construct/ (host project, config-layer + small runtime markers)

SubtreeSubsystemHand-edit?
.construct/context.md / .construct/context.jsonSession handoff contextYes
.construct/org/User-authored custom specialists/teams (committed)Via construct specialist|team create --custom
.construct/workflow.jsonWorkflow/task state (lib/workflow-state.mjs)Via CLI/MCP workflow tools
.construct/intake/Intake triage queue (pending/, processed/, skipped/, quarantine/, dead-letter/)Via construct intake CLI
.construct/knowledge/Ingested / curated knowledgeYes (see knowledge layout)
.construct/research/Project research briefs (working)Yes
.construct/graph/The living dependency/workflow graph storeNo
.construct/task-graphs/Task graph JSON (soft-warn disk budget)No
.construct/observations/Machine observations + entity graphNo
.construct/oracle/Oracle verdicts, routing, pending actionsNo
.construct/demos/Recorded demo outputs (.mp4, project .tape overrides)Optional override tapes
.construct/publish/construct publish outputsNo
.construct/launcher/Project launcher (run.mjs, version, cached binary)No

Intake has one visible drop zone — the project-root inbox/ — feeding the gitignored .construct/intake/ triage queue. Construct the package does not keep project state in git; shipped demos live in templates/demos/.

What lives under ~/.construct/projects/\<key>/ (machine-scoped heavy state, ADR-0066)

SubtreeSubsystemResolver
traces/Session trace shardslib/worker/trace.mjs, lib/telemetry/client.mjs
runtime/orchestration/runs/Orchestration run records (Mode-A filesystem store)lib/orchestration/run-store.mjs
runtime/worker/Worker execution logslib/resources/budget.mjs
runtime/docling/Shared docling venvlib/runtime/uv-bootstrap.mjs
runtime/whisper/Whisper model cachelib/runtime/whisper-bootstrap.mjs
context-repos/\<targetId>/Cloned content cache for corpus source targets (sources sync); \<targetId>.meta.json sibling holds remote/ref/HEAD/last-fetchlib/sources/repo-cache.mjs
lancedb/LanceDB vector index (unless CONSTRUCT_LANCEDB_PATH overrides it)lib/storage/vector-client.mjs, lib/storage/admin.mjs

\<key> is deriveProjectKey(projectRoot): the normalized git origin remote when one exists (so every clone/worktree of the same repository shares state), otherwise a hash of the canonical absolute project path. A project that still carries a non-empty .construct/runtime/, .construct/traces/, or .construct/lancedb/ in-project is flagged by construct doctor as legacy heavy state — no auto-migration shim; the durable copy already lives at the state root, so the old directory is a manual rm -rf once confirmed unneeded.

Construct package repo vs host project

isConstructPackageRepo() (lib/host-disposition.mjs) detects the Construct tool repository. There:

  • .construct/** is gitignored, with .construct/org/ carved back out for committed custom specialists.
  • The package repo tracks its own .construct/launcher/version (force-added by the version npm script) even though .construct/ is otherwise ignored.
  • Shipped VHS tapes live in templates/demos/tapes/; ADR-cited research inputs live in docs/notes/research/decision-input/.

Consumer projects created with construct init get .construct/ gitignored via IGNORED_PATTERNS and accumulate config-layer + small runtime-marker state locally, while heavy state accumulates at ~/.construct/projects/\<key>/.

Related