Manual by default. construct runs when you ask for it: $construct in Codex,
/construct in Claude Code or OpenCode. The agent never starts it on its own, and
CLI commands are unchanged. One setting per host makes it automatic — see
Manual or automatic.
Turn a product idea into a grounded, buildable SRD suite — a Software Requirements Document whose requirements and decisions rest on real research (competitors, open-source prior art, technology docs, known pitfalls), not the model's memory. A skills.sh agent skill.
construct is the companion to reconstruct
(rebuild a repo into PRDs) and ultradoc
(answer questions grounded in a repo). Same engineering: a single committed,
zero-dependency Node bundle + a thick agent playbook, fully tested, released
by Conventional Commits.
- Interview the user about the product (one question at a time) →
brief.json. - Research the idea across keyless angles → an evidence dossier with
[E#]ids:- market — competitors & positioning (SearXNG → DuckDuckGo → your WebSearch; pages cleaned through a local Firecrawl when its stack is up),
- oss — comparable open-source projects + their issues/PRs (GitHub/GitLab),
- tech — candidate-technology docs + StackOverflow pitfalls,
- semantic (optional) — a local-embedding relevance pass (Qdrant + Ollama).
- Analyze the dossier: name every feature/competitor/tech/seed that would render ungrounded, with the drill command that fixes each gap.
- Render a complete SRD tree (vision, scope, numbered functional requirements
with Given/When/Then, non-functional requirements, system context, an
inferred data model and interfaces, ADRs, competitive landscape, build plan,
traceability) +
SRD.json+ a machine-readableBUILD-PLAN.jsontask DAG. At--level complexit also renders a design system (design/: principles, design tokens, components, screens/flows, an accessibility contract); pass--no-designto skip it. - Check it: a hard structural-completeness gate, an advisory
grounding-coverage report (opt-in
--min-groundingthreshold), and — viareview+check --semantic— an opt-in claim-support gate that fails on any cited evidence that doesn't actually back its claim. - Verify the build (optional): the agent implements the app task-by-task
from
BUILD-PLAN.json(status --jsonlists the buildable task frontier so independent same-milestone tasks can be built in parallel);construct verifyreferees it against the SRD — declared files exist, every requirement is referenced by a test, and (with--run-tests) the declared test commands pass.
No API keys. No npm install at skill-use time.
npx skills add maxgfr/construct
node scripts/construct.mjs init --idea "a self-hosted read-it-later app" --out ./readpile
# optional: diverge first, then fold kept ideas into the brief
node scripts/construct.mjs brainstorm --out ./readpile # scaffold BRAINSTORM.md
node scripts/construct.mjs brainstorm --out ./readpile --merge # kept → brief.json
# …fill ./readpile/brief.json via the interview…
node scripts/construct.mjs research --out ./readpile --angles market,oss,tech
node scripts/construct.mjs analyze --out ./readpile # what's thin? drill it
node scripts/construct.mjs render --out ./readpile --level complex
node scripts/construct.mjs check --out ./readpile # add --min-grounding 70 to enforce
node scripts/construct.mjs review --out ./readpile # adjudicate each cited [E#] → verdicts.json
node scripts/construct.mjs check --out ./readpile --semantic # gate refuted/unsupported claims
node scripts/construct.mjs verify --out ./readpile --app ./readpile-app --run-tests --strict
Add --merge to also emit a single-file SRD.md. Run --help for the full
surface, or see SKILL.md for the agent playbook and
DOCUMENTATION.md for internals.
00-overview/ VISION.md · SCOPE.md
requirements/ FUNCTIONAL.md · NON-FUNCTIONAL.md
architecture/ SYSTEM-CONTEXT.md · DATA-MODEL.md · INTERFACES.md · decisions/NNNN-*.md
design/ PRINCIPLES.md · DESIGN-TOKENS.md (+ design-tokens.json) · COMPONENTS.md · SCREENS.md · ACCESSIBILITY.md (complex; --no-design to skip)
competitive/ LANDSCAPE.md
BUILD-PLAN.md · BUILD-PLAN.json (task DAG for the build phase) · TRACEABILITY.md
evidence/ EVIDENCE.md · evidence.json · meta.json · brief.json · SRD.json
VERIFY.md · VERIFY.todo.json · VERIFY.json (claim-support review, from `review`)
construct check separates the two axes. The structural gate fails the build
on an incomplete SRD (unresolved 🧠 decisions, an FR with no acceptance
criteria, a dangling reference, a missing required NFR category, a malformed
ADR). The grounding coverage is a report — it tells you how well-cited the
SRD is so you can invest research where it matters, but it never fails the build
by default. When you do want it enforced, --min-grounding <0-100> opts into
a second gate that fails below the threshold.
Coverage counts citations; it does not check they hold. construct review
builds a claim↔evidence worklist (one pair per cited [E#]); an agent (or a
fan-out of skeptic subagents — see references/orchestration.md) adjudicates
each as supported | partial | refuted | unsupported, and check --semantic
turns that into a third opt-in gate that fails on a refuted or unsupported
claim.
The skill shells out to the CLI and parses its output. An MCP server skips both: your agent calls construct as typed tools, with JSON schemas in and structured results out. Same engine, same page cache, no wrapper.
# stdio — the default, and what Claude Code / Claude Desktop / Cursor expect
claude mcp add construct -- node /abs/path/to/scripts/construct.mjs mcp
# or over HTTP, on loopback
node scripts/construct.mjs mcp --transport http --port 7342
claude mcp add --transport http construct http://127.0.0.1:7342/mcpIt serves all three MCP primitives, because a skill is three things: the engine
(tools), the method (prompts), and the documentation the method refers
to (resources). Here that matters more than usual: check gates structure
and grounding is advisory, so a client given only the tools produces a
well-shaped SRD full of decisions nobody researched — and every gate goes green.
| Tool | What it does |
|---|---|
construct_status |
What exists in the run, and the exact next command |
construct_research |
The only command that grounds anything — market, OSS, tech → a dossier |
construct_research_angle |
Probe ONE angle (web/oss/tech/so), persist nothing |
construct_analyze |
What is thin, and the command that fills each gap |
construct_render |
The SRD suite: requirements, ACs, NFRs, ADRs, build plan, traceability |
construct_check |
The structural gate (grounding coverage is advisory) |
construct_review |
Claim↔evidence worklist — where advisory grounding becomes real |
construct_verify |
Referee a built app against its SRD |
construct_cache |
What the page cache holds |
construct_read |
A file, or a line range, from the run |
--allow-write additionally exposes construct_init, which scaffolds a run
folder on disk. Pass --out <run> at startup to dedicate the server to one SRD
— run then becomes optional on every tool.
| Prompt | Arguments | What it drives |
|---|---|---|
interview_idea |
idea |
The questions whose answers change what gets built — one at a time, following the surprising answer |
enrich_srd |
run |
evidence → testable requirements → check → review |
judge_adr |
run, decision? |
Ground a technology choice in its docs, its open issues, and what people hit in production |
Each states the thing the gates cannot: the rigor is yours. A green
construct_check means the SRD is well-formed, not well-researched.
SKILL.md and all 19 references/*.md are served under skill://, read off
disk at request time — so a documentation fix reaches every client without a
rebuild.
Two things worth knowing:
construct_researchis the slow one — a network fan-out across angles, minutes on a first run. Re-runs are nearly free thanks to the page cache.- The HTTP transport binds
127.0.0.1and refuses anything else unless you pass--allow-remote. This server fetches arbitrary URLs and reads local files; an exposed port is a fetch-anything primitive for whoever finds it.
node scripts/construct.mjs semantic up # Qdrant + Ollama + SearXNG, fully local, no key
node scripts/construct.mjs firecrawl up # Firecrawl, fully local, no key (~3 GB, own profile)
The first adds embedding-based re-ranking (--semantic) and keyless web
discovery. The second swaps the built-in regex HTML stripper for browser-based
main-content extraction on every page fetch — the only way a JS-rendered page
yields evidence at all. Both are optional and degrade quietly: when a stack is
down, research runs exactly as it did before and says so in the dossier notes.
See references/semantic-setup.md.
verify without --acceptance checks static consistency; FR tags are not proof
that a criterion ran. For the final build gate, inspect
verify --out <run> --acceptance --json (nonzero until executed), bind each
current {frId,index,fingerprint} to a dedicated test command under its done
task's verify.criteria, then run:
node scripts/construct.mjs verify --out <run> --strict --acceptance --run-tests --jsonEvery criterion reports passed, failed or not-tested with its full-FR
fingerprint and bounded command output. Missing, stale, duplicate or unexecuted
criteria fail. The command always executes afresh; it does not import old green
reports. Review the actual assertions before claiming implementation: exit 0
does not prove a test exercises its mapped requirement. Commands run locally
with your privileges, through the existing shell runner, not a sandbox. Schema,
limits and rebinding protocol: verify reference.
MIT © maxgfr
A .pdf URL or an application/pdf response goes through an extractor
ladder (src/research/pdf/): npx @firecrawl/pdf-inspector@1 → npx @firecrawl/anydoc@0.1 (the PDF on
stdin, in a child process) → the self-hosted Firecrawl → pdftotext → a
built-in dependency-free reader — stopping at the first rung whose output passes
a quality gate, and REFUSING rather than quoting a PDF none of them could read.
Office documents — .docx/.doc/.odt/.rtf, .pptx/.ppt/.odp,
.xlsx/.xls/.ods, .epub, .csv — go through their own two-rung ladder
(src/research/doc/): npx @firecrawl/anydoc@0.1 (the bytes on stdin, converted to
GitHub-Flavored Markdown) → the self-hosted Firecrawl. Same gate, same refusal.
The refusal is the point: these are ZIP and OLE containers, so the fall-through
this replaced did not degrade the evidence, it fabricated it — a .docx was
quoted into requirements as if it were prose, as kilobytes of replacement characters, silently. anydoc needs
Node 20+, so an unavailable converter is a normal outcome rather than a
misconfiguration; CONSTRUCT_DOC_ENGINE=none disables the ladder.
Scanned PDFs — no text layer at all, so every rung above fails — are
rescued by a final OCR rung: copyable-pdf
tesseract, when both are installed. It is last because it is the only expensive one (~2.7s per page at 300 DPI) and is budgeted per process (CONSTRUCT_OCR_MAX, default 3,0disables). Both binaries are checked before the tool is spawned: asked for a missing tesseract, copyable-pdf offers to runbrew install/sudo apt-get install -yand waits on stdin, and a research run must never install a system package as a side effect.
Without it a PDF body was returned verbatim: its bytes decoded as UTF-8, cached, and quoted into requirements as if it were prose. The gate rejects text laced with C0/C1 control bytes or U+FFFD at ANY length — the built-in reader can emit 16 MB of image-stream garbage for a 12 MB paper, which every length-limited check waves through.
CONSTRUCT_NO_NPX=1 drops the npx rung; CONSTRUCT_PDF_ENGINE=<rung> pins one.
The stack is shared with the sibling skills (ultrasearch, construct, ultradoc): one compose project, one set of containers, one set of volumes. They used to define three separate projects on the same host ports, so only one could be up at a time — starting a second failed on the port after leaving its sidecars running. Bringing it up from any of them now targets the same containers, so the second is a no-op and the RAM is paid once.
Compose files and bind-mounted settings use the shared directory
~/.cache/skills/compose. Set ULTRA_STACK_CACHE_DIR to the same directory
for all three tools to override its parent. Per-tool HTTP and clone cache
overrides still apply only to those caches. The first startup after upgrading
from per-tool Compose directories may recreate SearXNG once to move its mount;
subsequent startups from another tool reuse it.
Upgrading from a version with per-skill container names? Remove the old ones once — this file can no longer stop them, and they still hold the ports:
docker rm -f $(docker ps -aq --filter name='^(ultrasearch|construct|ultradoc)-')See shared engine maintenance for pins, source adoption checks and the daily repin workflow.
construct ships explicit-only, and skills add installs it that way: it runs
when you invoke it, never when the agent feels like it. Use $construct in Codex,
/construct in Claude Code or OpenCode, prefixing the plugin namespace when it is
installed as a Claude plugin.
Letting the agent choose it is one setting per host, applied to the installed copy of the skill:
| Host | Shipped, manual | Automatic |
|---|---|---|
| Claude Code | disable-model-invocation: true in SKILL.md |
delete that line, or set it to false |
| Codex | allow_implicit_invocation: false under policy: in agents/openai.yaml |
set it to true |
| OpenCode | metadata.opencode/autoinvoke: 'false' in SKILL.md |
delete that entry, or set it to 'true' |
Claude Code can do it without touching the file: put
"skillOverrides": { "construct": "on" } in settings.json, where
"user-invocable-only" forces manual mode back. Plugin installs ignore
skillOverrides, so edit the frontmatter there. Updating or reinstalling the
skill restores the shipped default, so reapply the change afterwards.
OpenCode V1 reads no autoinvoke metadata. Keep it manual with
permission.skill in ~/.config/opencode/opencode.json or the project
configuration, retaining unrelated permissions; dropping the entry, or setting
"allow", is what lets the agent reach it:
{
"permission": {
"skill": {
"construct": "deny"
}
}
}On OpenCode 1.18.30 that rule hides the skill from the agent and rejects
skill-tool loading, while the explicit /construct command still works.
Installation with skills add does not write this OpenCode V1 configuration.