Skip to content

Repository files navigation

construct

Invocation

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.

What it does

  1. Interview the user about the product (one question at a time) → brief.json.
  2. 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).
  3. Analyze the dossier: name every feature/competitor/tech/seed that would render ungrounded, with the drill command that fixes each gap.
  4. 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-readable BUILD-PLAN.json task DAG. At --level complex it also renders a design system (design/: principles, design tokens, components, screens/flows, an accessibility contract); pass --no-design to skip it.
  5. Check it: a hard structural-completeness gate, an advisory grounding-coverage report (opt-in --min-grounding threshold), and — via review + check --semantic — an opt-in claim-support gate that fails on any cited evidence that doesn't actually back its claim.
  6. Verify the build (optional): the agent implements the app task-by-task from BUILD-PLAN.json (status --json lists the buildable task frontier so independent same-milestone tasks can be built in parallel); construct verify referees 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.

Install

npx skills add maxgfr/construct

Use (standalone CLI)

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.

Output

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`)

Grounding is advisory; completeness is enforced

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.

Use it as an MCP server

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/mcp
// Claude Desktop takes stdio servers only — a remote URL here will not work.
{ "mcpServers": { "construct": { "command": "node", "args": ["/abs/path/to/scripts/construct.mjs", "mcp"] } } }
// Cursor, HTTP:
{ "mcpServers": { "construct": { "url": "http://127.0.0.1:7342/mcp" } } }

It 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.

Tools

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.

Prompts — the workflow, not just the tools

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.

Resources — the skill's own documentation

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_research is 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.1 and 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.

Optional local stacks

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.

Acceptance execution evidence

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 --json

Every 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.

License

MIT © maxgfr

PDF sources

A .pdf URL or an application/pdf response goes through an extractor ladder (src/research/pdf/): npx @firecrawl/pdf-inspector@1npx @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, 0 disables). Both binaries are checked before the tool is spawned: asked for a missing tesseract, copyable-pdf offers to run brew install / sudo apt-get install -y and 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.

Shared container stack

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.

Manual or automatic

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.

Releases

Packages

Used by

Contributors

Languages