Constructdocs
section · guides

Intake and triage

How signals dropped into `inbox/` get classified by the active profile's taxonomy and routed to the right specialist.

6 min read·guides / concepts / intake-and-triage

Construct treats every file that lands in inbox/ as a candidate signal: a bug report, a customer comment, an experiment hypothesis, an incident note, a competitor PDF, a campaign brief, a research question. The active org profile decides how that signal is classified and where it routes; the pipeline that moves it through triage is the same across every profile.

This page explains the pipeline once, then walks the taxonomy under each curated profile.

inbox/any fileinbox watcherreactive, ~1singesternormalize to markdown + index for retrievalprepareIntakelane · related excerpt · triageclassifyRdIntakedeterministic no LLM.construct/intake/pending/triage blocksession startsurfaces pending packetsagentreads packet does analysis

The daemon path (watcher through classification) never calls an LLM. The agent in your editor does the analysis. The triage block is a routing hint, not a verdict.

The pipeline (profile-agnostic)

  1. The embed daemon's reactive watcher (lib/embed/inbox-live-watcher.mjs) picks the file up within a second or two.

  2. The inbox ingester normalizes it into markdown under ~/.cx/knowledge/ and indexes it for retrieval.

  3. prepareIntakeForIngestedFile runs four deterministic preparation steps:

    • lane suggestion via lib/docs-routing.mjs
    • related-doc retrieval via the hybrid corpus query
    • excerpt extraction for the agent to see without reopening the file
    • triage via classifyRdIntake (keyword/heuristic, no LLM) against the active profile's table
  4. The result is written to .construct/intake/pending/<id>.json.

  5. At the next session start, the hook surfaces a one-line summary per pending packet:

    ## Pending intake (3)
    - login-feedback.md → bug / implementation · owner: debugger · next: diagnose
    - competitor-pricing.md → research / research · owner: business-strategist · next: research
    - hallucination-trace.md → eval-finding / evaluation · owner: evaluator · next: evaluate
    

The daemon never calls an LLM. The model spend stays with the agent in the user's editor.

How the active profile shapes triage

Each profile ships a classification table at lib/intake/tables/<profile>.mjs. The table defines:

  • the intake types the profile recognizes (e.g. bug, brief, request)
  • the stages in this profile's work loop (e.g. implementation vs draft vs triage)
  • the owner role per intake type
  • the recommended chain of specialists for the handoff sequence

construct scope show tells you which table is active. construct scope set <id> switches.

Default: rnd (software R&D)

Loop: signal → framing → hypothesis → research → artifact → design → implementation → evaluation → release → operations.

Intake typeExamplesPrimary owner
user-signalcustomer feedback, NPS, support tickets, churnproduct-manager
bugstack traces, errors, regressions, crashesdebugger
requirementfeature requests, PRD drafts, acceptance criteriaproduct-manager
researchcompetitor scans, market research, pricing teardownsbusiness-strategist
experimenthypotheses, spikes, prototypes, falsifiable plansrd-lead
eval-findinghallucinations, score regressions, judge findingsevaluator
architectureADR drafts, RFC drafts, system-design tradeoffsarchitect
incidentoutages, SLO breaches, latency spikes, pagessre
launch-assetrelease notes, version bumps, ship candidatesrelease-manager
opsrunbooks, cron jobs, capacity plans, dependency upgradesoperations
securityCVEs, vulnerabilities, secret leaks, exploit reportssecurity
legal-complianceGDPR, license audits, DPA reviewslegal-compliance
unknownnothing matched; agent decidesorchestrator

operations

Loop: request → triage → resolve → document → improve.

Intake types: request, bug, incident, ops, security, docs, unknown. Owners map to operator / engineer / sre roles.

creative

Loop: brief → research → draft → review → publish → measure.

Intake types: brief, content-request, asset, experiment, report, legal-compliance, ops, unknown. Owners map to product-lead / content-writer / designer / data-analyst.

research

Loop: question → gather → analyze → synthesize → recommend.

Intake types: question, study, synthesis, report, experiment, ops, unknown. Owners map to researcher / ux-researcher / evaluator / data-analyst.

Custom

Drop a .construct/scope.json with custom: true and an intake.classificationTable path that lives under .construct/. The validator at lib/scopes/validate-custom.mjs enforces the cap structure. See Profile lifecycle.

Triage block schema (same across profiles)

classifyRdIntake returns the base triage object; prepareIntake adds the resolved researchProfile when the active scope maps the classified intake/artifact lane to one. The values vary by profile; the shape does not.

FieldPurpose
intakeTypeOne of the profile's declared types
rdStageOne of the profile's declared stages (the field name is historical; it's the active profile's stage)
primaryOwnerPersona name from specialists/org
recommendedChainOrdered handoff sequence
recommendedActionVerb-led next step (diagnose, draft-prd, create-runbook, etc.)
risklow, medium, or high
requiresApprovaltrue for high-stakes actions
confidence[0, 1], keyword-match density
rationaleOne-line explanation
researchProfileOptional scope-level research profile for downstream routing (codebase, market, etc.)

Classification heuristics

The classifier is deterministic: same input → same output, no LLM. It builds a signal corpus from the filename, the extracted excerpt, and the titles of related docs, then scores it against the active profile's keyword sets. Ties break in favor of higher-stakes classes. security beats research when both match; incident beats architecture.

This is a fast-tier classifier, not a final answer. The agent in the user's editor reads the packet and does the actual analysis. The triage block is a routing hint, not a verdict.

On-disk layout

<project>/.construct/intake/
  pending/
    2026-05-14T15-22-08-login-feedback.json     newly arrived signal
  processed/
    2026-05-13T11-04-19-payment-postmortem.json agent finished
  skipped/
    2026-05-12T09-47-30-noise-pdf.json          agent intentionally skipped

Each <id>.json carries: id, createdAt, status, intake (sourcePath, outputPath, characters, knowledgeSubdir), triage (the fields above), suggestion (lane), related (top-K artifacts), excerpt, query. Status transitions add processedAt + processedBy + notes or skippedAt + skippedBy + reason.

CLI

construct intake list                          # ID, type, stage, owner, action
construct intake show <id>                     # full packet: triage, excerpt, related artifacts
construct intake done <id>                     # move pending → processed
construct intake done <id> --output=<path>     # stamp intake_id + confidence into artifact frontmatter
construct intake done <id> --no-output=rejected|deferred|merged  # close without an artifact
construct intake skip <id> [--reason=…]        # move pending → skipped, audit trail preserved
construct intake reopen <id>                   # processed or skipped → pending
construct scope show                         # which taxonomy is currently active

--output=<path> stamps the artifact at <path> with intake_id, intake_confidence, and intake_rationale in its YAML frontmatter. Every intake-derived artifact carries verifiable provenance back to the source packet. The stamp is refused if the artifact already has a different intake_id. Use intake: none frontmatter in an artifact that intentionally has no intake source.

In solo mode the queue is the filesystem (.construct/intake/). In team and enterprise modes the same CLI talks to a Postgres-backed queue with row-locked worker claims; the contract is identical.

See also