Knowledge layout
How .construct/, beads, the vector index, and SQL fit together to make context durable across sessions.
.construct/ is Construct's knowledge store. It's a directory in your project root that persists everything Construct learns: ingested documents, architectural decisions, session history, and observations from past work.
Overview
.construct/ has three layers:
| Layer | Dirs | Purpose |
|---|---|---|
| Knowledge | .construct/knowledge/ | Typed, persistent, human-curated or inbox-ingested documents |
| R&D loop | inbox/ (drop zone), .construct/intake/{pending,processed,skipped}/, .construct/task-graphs/, .construct/traces/ | Per-signal drop zone + triage queue, per-signal execution plans, append-only trace event log |
| Runtime | .construct/observations/, .construct/sessions/, .construct/runtime/ | Machine-written, high-churn, agent working memory |
Runtime dirs are never hand-edited. Knowledge dirs are hand-editable — but like the rest of .construct/ they are local-only: construct init gitignores .construct/ in full, so knowledge persists on the machine across sessions and is never committed to the repo (see the README section ".cx/ is local-only runtime state"). R&D-loop dirs are written by the daemon and the CLI; agents update them via construct intake / construct graph, not by editing files.
Knowledge Subdirectories
.construct/knowledge/
internal/ ← team notes, meeting minutes, internal specs, ADRs, PRDs, incident records
external/ ← customer feedback, support tickets, field notes, external research
decisions/ ← architecture decision records (ADRs), design decisions, RFCs accepted
strategy/ ← product strategy per scope: product.md, technical.md, gtm.md, platform.md
how-tos/ ← runbooks, setup guides, operational playbooks, troubleshooting procedures
reference/ ← specs, RFCs (pre-decision), schemas, API references, architecture docs
Strategy is a protected subdirectory. Files under decisions/strategy/ are NEVER written automatically by the embed daemon or intake pipeline: they require an explicit writeStrategy() call or a user-initiated construct strategy update invocation. The strategy store exposes readStrategy(scope), readAllStrategies(), and getStrategyDigestSync(). Agents inject a compact strategy digest (≤ 500 tokens) at prompt-assembly time so declared Bets and Non-bets are visible without a manual get_skill() call.
Routing rules
Files dropped in inbox/ are automatically routed by filename convention:
| Filename pattern | Routed to |
|---|---|
adr-NNN-*, architecture-decision-* | decisions/ |
*-spec*, *rfc*, *schema*, *api-ref* | reference/ |
*runbook*, *playbook*, *how-to*, *setup*, *guide* | how-tos/ |
*customer*, *feedback*, *support*, *external* | external/ |
*postmortem*, *incident*, *rca* | internal/ |
| (everything else) | internal/ |
Routing is additive: files already in the right subdirectory are not moved.
Neurodiversity-friendly documentation guidelines
To support neurodivergent readers (including those with ADHD, dyslexia, autism, etc.), all knowledge documents should follow these guidelines where practical:
- Clear hierarchy: Use descriptive headings (H1, H2, H3) to create a scannable outline. Avoid skipping heading levels.
- Consistent structure: Similar document types (e.g., all runbooks) should follow a predictable template (e.g., Purpose, Prerequisites, Steps, Troubleshooting).
- Chunking: Break text into short paragraphs (max 3-4 sentences). Use bullet points or numbered lists for steps or items.
- Plain language: Avoid jargon when possible; define necessary terms inline or in a glossary.
- Visual contrast: Ensure sufficient text-to-background contrast; avoid relying solely on color to convey information.
- Predictable navigation: Use consistent naming conventions and logical grouping within directories.
- Reduce cognitive load: Highlight important information with callouts or bold text sparingly; avoid dense walls of text.
- Multiple modalities: Where possible, supplement text with diagrams, flowcharts, or video walkthroughs (linked or embedded).
- Linear flow: For procedural documents, ensure steps are numbered and sequential; avoid branching instructions within the main flow (use appendices for variations).
These guidelines are aspirational; existing documents need not be refactored immediately, but new documents should aim to comply.
Using the Inbox
Drop any supported file into inbox/ and the embed daemon will:
- Detect it on the next inbox-watcher cycle (reactive within a second or two; scheduler fallback every two minutes)
- Classify it using the filename rules above
- Extract text (PDF, DOCX, XLSX, PPTX, Markdown, plain text, code…)
- Write a normalised Markdown artifact to
.construct/knowledge/\<subdir>/\<filename>.md - Record a typed observation in
.construct/observations/with tagknowledge:\<subdir> - Run
classifyRdIntakeand write an R&D triage packet to.construct/intake/pending/\<id>.json: intake type, R&D stage, primary owner persona, recommended chain, recommended action, risk, confidence, rationale. Drive the queue withconstruct intake list / show / done / skip / reopen. See intake and triage.
Supported formats:
- Plain text / Code:
.md,.txt,.rst,.adoc,.json,.yaml,.yml,.toml,.js,.mjs,.ts,.tsx,.jsx,.py,.go,.rs,.sh,.bash,.html,.css,.csv,.tsv,.xml,.env,.conf,.ini,.sql,.log - Transcripts:
.vtt(WebVTT),.srt(SubRip),.lrc(lyrics),.transcript: Zoom, Teams, meeting recordings - Office documents (via docling Python sidecar):
.docx,.xlsx,.pptx,.odt,.ods - Rich text (via docling):
.doc,.rtf - Legacy iWork/Office (via docling):
.xls,.ppt,.pages,.numbers,.key - PDF (via docling, layout-aware with built-in OCR):
.pdf - Audio / Video (via whisper.cpp, Metal-accelerated on macOS):
.mp3,.wav,.m4a,.mp4,.mov,.avi,.mkv,.flac,.ogg,.webm,.m4v
High-fidelity extraction runs through the docling Python sidecar (provisioned automatically via uv on first use into a machine-shared ~/.construct/runtime/docling/.venv — one venv per machine, not one per project). Pass --legacy-extractor to construct ingest to fall back to the pre-docling regex path. Audio/video transcription requires whisper-cli (brew install whisper-cpp on macOS); see Audio and Video Intake.
Ingested documents are also stored content-addressed at .construct/ingest/\<sha256>/{source,markdown,meta}.json for idempotent re-ingest, and indexed into knowledge_search so dropped documents become retrievable from any agent session.
Full list in lib/document-extract.mjs.
50 MB hard cap: files above this are skipped silently.
Extra inbox dirs
Set CX_INBOX_DIRS to a colon-separated list of additional directories to watch:
CX_INBOX_DIRS=/Users/me/Downloads/docs:/Volumes/shared/specs
Manual Ingest
Use construct ingest to place files directly without going through the inbox:
# Route to a specific knowledge subdir
construct ingest ./my-runbook.md --target=knowledge/how-tos
# Route to decisions
construct ingest ./adr-001-auth.md --target=knowledge/decisions
# Default: route to internal knowledge
construct ingest ./customer-research.pdf
# Sibling: write .md next to source
construct ingest ./spec.docx --target=sibling
Valid --target values: sibling, knowledge/internal, knowledge/external, knowledge/decisions, knowledge/how-tos, knowledge/reference.
Observation Tags
Every inbox-ingested observation carries:
| Tag | Example | Meaning |
|---|---|---|
inbox | inbox | Created by inbox-watcher |
ingested-doc | ingested-doc | Produced by document extraction |
\<category> | decision, pattern, anti-pattern, insight | Observation category |
knowledge:\<subdir> | knowledge:decisions | Which knowledge subdir the artifact landed in |
Use these tags in searchObservations calls or the dashboard to filter by type.
Runtime Directories (do not hand-edit)
.construct/
observations/ ← machine-written observations (addObservation)
entities.json ← entity store: name, type, summary, relatedEntities[] (graph edges)
entity-vectors.json ← entity embeddings for hybrid search
sessions/ ← distilled session records
runtime/
inbox-state.json ← mtime-keyed state so files aren't re-ingested
daemon.json ← daemon PID + uptime state
sync.lock ← sync-agents write lock
decisions/ ← session-scoped ADRs (short, per-session decisions)
roadmap.md ← generated hourly by roadmap.mjs
context.md ← human-readable project context (hand-maintained)
context.json ← machine-readable context (kept in sync with context.md)
intake/ ← per-signal queue state: pending/processed/skipped/quarantine/dead-letter
inbox/ ← project-root drop zone (visible; files ingested then moved to knowledge/)
Corpus source targets keep their cloned content out of the project tree entirely. A github target opted into content: {mode:"corpus"} (and any future git-hosted provider whose manifest declares a content block) clones under the machine state root at ~/.construct/projects/\<key>/context-repos/\<targetId>/, refreshed incrementally by construct sources sync (lib/sources/repo-cache.mjs); a directory target reads its content — docs and code — in place and clones nothing. See Project scopes for the full state-root table.
Multi-root corpus & chunk provenance
The knowledge corpus is multi-root, and cross-repo federation covers code as well as docs. lib/sources/content-roots.mjs resolves the content-capable subset of sources.targets[] — directory targets to their path, corpus targets to their synced cache — and both buildCorpus (lib/knowledge/rag.mjs, backing construct ask) and buildSourceList (lib/knowledge/search.mjs, backing construct knowledge search and MCP knowledge_search) fold those roots into a single searchable index: every markdown file, plus every code/text file whose extension is in UTF8_TEXT_EXTS (lib/document-extract.mjs — .js/.mjs/.ts/.tsx/.jsx/.py/.go/.rs/.sh plus config, data, and markup text formats), skipping vendored/build directories (.git, node_modules, dist, build, vendor, .venv, __pycache__). Binary and non-text files never join the corpus. Every chunk carries a structured origin — {targetId, provider, projectKey, relPath, ref, kind}, with code chunks tagged kind: 'code' — so a hit is always attributable to its source project; the host project is the reserved origin (targetId: null, projectKey: "self"). Retrieval narrows by project with --projects=\<id,...> (or all / self); an unknown id is a hard error, never a silent empty result. construct ingest \<dir> --as=\<targetId> stamps origin_target_id/origin_provider into the ingested file's frontmatter so imported knowledge stays re-verifiable back to its registered source.
Alongside the searchable corpus, construct graph build-targets builds a per-repo import/symbol code map for the same registered targets, persisted at .cx/graph/targets/\<targetId>/ and queried with construct graph query \<node-id> --projects=\<id,...>|all|self (same id semantics and unknown-id hard error as knowledge search). The code map covers JavaScript-family sources only (.js/.mjs/.cjs, resolved from relative import/export/require specifiers — lib/graph/build-import-graph.mjs); files in other languages are searchable in the knowledge corpus but do not appear in the import graph.
Entity graph (GraphRAG)
entities.json is the JSONL-backed graph. Each entity carries a
relatedEntities[] array which forms the undirected edge set.
lib/knowledge/graph.mjs reads this file and produces communities via label
propagation. Two queries are exposed:
askGlobal({ query, rootDir })returns the top communities ranked by BM25 against their member summaries. Useful for "how does X relate across the project?" questions that pure semantic retrieval misses.- MCP wrapper
knowledge_graph_askexposes the same primitive to subagents.
The Postgres projection lives in construct_entities.community_id and
construct_entity_communities (see db/schema/006_graph.sql). The JSONL
file remains the source of truth in solo mode.
Slack Channel Intent → Knowledge Category
When SLACK messages are ingested via the embed daemon, the channel intent tag determines observation category and implicitly the knowledge subdir:
| SLACK_CHANNELS entry | Intent | Observation category | Knowledge subdir |
|---|---|---|---|
#eng-general (no tag) | internal | insight | internal/ |
#incidents:risk | risk | anti-pattern | internal/ |
#decisions:decision | decision | decision | decisions/ |
#tips:how-to | how-to | pattern | how-tos/ |
#customer:external | external | insight | external/ |
Configure channels in ~/.config/construct/config.env:
SLACK_CHANNELS=#eng-general,#incidents:risk,#decisions:decision,#customer-feedback:external
Migration from product-intel/
product-intel is retired. New ingests and cleanup tools use .construct/knowledge/ only.
If an older project still has .cx/product-intel/sources/ingested/, move those markdown files into the closest matching .cx/knowledge/\<subdir>/ directory.
Structured Product Intelligence stores now live under .construct/knowledge/internal/ as well:
customer-profiles/workspaces/
Compatibility shims still read or migrate older product-intel locations when present, but new writes should never target that namespace.