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.
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:
- 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.mdfenced regions). Small, human-legible, meant to travel with the repo..construct/org/is carved back out of the otherwise-ignored.construct/tree by a.gitignorenegation so custom specialists stay version-controlled. - 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 throughlib/state-root.mjs'sresolveStateRoot/resolveStateDir/resolveStatePath. A freshconstruct initnever scaffolds these directories — they appear only once the corresponding subsystem first writes. - 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
| Location | Scope | Purpose | Commit to git? |
|---|---|---|---|
templates/** | Package (Construct repo) | Shipped demos, scripts, PDF themes, doc templates | Yes |
docs/** | Package / project docs | ADRs, cookbooks, durable research that informed decisions | Yes |
.construct/ (project root) | Host project — config-layer + small runtime markers | context.md/context.json, workflow.json, org/ (committed), intake, oracle, observations, knowledge, the living graph, project research briefs, recorded demos | No (gitignored, except .construct/org/) |
.construct/launcher/ (project root) | Host project — launcher | run.mjs shim hooks invoke, version pin, cached binary, bootstrap shims | No (gitignored) |
.beads/ (project root) | Host project — issue history | Dolt-backed issue database (ADR-0026) | Yes (git-native sync) |
~/.construct/projects/\<key>/ | User machine, per-project | Traces, orchestration runs, worker logs, the LanceDB vector index, docling/whisper bootstrap venvs (ADR-0066) | No |
~/.construct/ | User machine | config.env, credentials, doctor state, auth tokens | No |
~/.cx/ | User machine | Cross-project telemetry, role-pending queue, embed daemon logs when no project marker | No |
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)
| Subtree | Subsystem | Hand-edit? |
|---|---|---|
.construct/context.md / .construct/context.json | Session handoff context | Yes |
.construct/org/ | User-authored custom specialists/teams (committed) | Via construct specialist|team create --custom |
.construct/workflow.json | Workflow/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 knowledge | Yes (see knowledge layout) |
.construct/research/ | Project research briefs (working) | Yes |
.construct/graph/ | The living dependency/workflow graph store | No |
.construct/task-graphs/ | Task graph JSON (soft-warn disk budget) | No |
.construct/observations/ | Machine observations + entity graph | No |
.construct/oracle/ | Oracle verdicts, routing, pending actions | No |
.construct/demos/ | Recorded demo outputs (.mp4, project .tape overrides) | Optional override tapes |
.construct/publish/ | construct publish outputs | No |
.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)
| Subtree | Subsystem | Resolver |
|---|---|---|
traces/ | Session trace shards | lib/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 logs | lib/resources/budget.mjs |
runtime/docling/ | Shared docling venv | lib/runtime/uv-bootstrap.mjs |
runtime/whisper/ | Whisper model cache | lib/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-fetch | lib/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 theversionnpm script) even though.construct/is otherwise ignored. - Shipped VHS tapes live in
templates/demos/tapes/; ADR-cited research inputs live indocs/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
- Knowledge layout —
.construct/knowledge/subdirs - Intake and triage — inbox → intake packets
- Host disposition — ADR-0027 ignored patterns
- ADR-0074 — the single-directory consolidation this page documents
- ADR-0066 — the machine-scoped heavy-state split