@wifo/factory-core
v0.0.14
Published
Universal spec format, schema, parser, and lint CLI for software factories
Downloads
1,450
Maintainers
Readme
@wifo/factory-core
The format and the front door. Spec parser, lint, scaffold, slash commands, and the unified
factoryCLI dispatch.
@wifo/factory-core is the package every agent and every other package depends on. It defines the canonical spec format (Zod schemas + parser), provides the format-floor lint, scaffolds new projects via factory init, ships the /scope-project slash command bundled in the npm tarball, and dispatches the factory spec review and factory spec watch subcommands. If you've used factory <anything>, you've used this package.
For AI agents: start at
AGENTS.md(top-level). This README is detailed reference for once you have the mental model.
Install
pnpm add -D @wifo/factory-corebun is required for pnpm test only (v0.0.13+)
bun is required for pnpm test only — every workspace package's scripts.test is bun test src (the chosen test runner). pnpm build and pnpm typecheck are Node-native (Node 22+); the JSON-schema emitter runs via tsx scripts/emit-json-schema.ts with no bun on PATH at build time. pnpm install for consumers of the published packages does NOT require bun.
Or use without installing via npx:
npx -y @wifo/factory-core init --name my-projectfactory init is the recommended bootstrap path — see the canonical workflow below.
Peer dependency note (v0.0.13+)
@wifo/factory-spec-review is a non-optional peer dependency of
@wifo/factory-core — it powers factory spec review and the documented
happy path requires it.
pnpm 8+ / npm 7+ auto-install peer dependencies, so
pnpm add -D @wifo/factory-core(ornpm i -D @wifo/factory-core) brings in@wifo/factory-spec-reviewfor free.Legacy npm (< 7) does NOT auto-install peers. Install both packages explicitly:
npm i @wifo/factory-core @wifo/factory-spec-review
The shift from dependencies to peerDependencies in v0.0.13 breaks the
core ↔ spec-review workspace cycle that bit during the v0.0.12 publish.
v0.0.14: subprocess transition. factory spec review now invokes the
factory-spec-review bin via child_process.spawn (auto-linked onto PATH
by npm/pnpm install). Core no longer imports from spec-review at all —
the process boundary eliminates the v0.0.12/v0.0.13 createRequire(import.meta.url)
CJS/ESM resolution mismatch against spec-review's ESM-only exports map (which
silently exit-0'd on the published artifact). If the bin isn't on PATH, the
dispatcher emits factory: factory-spec-review not found on PATH and exits 2.
When to reach for it
- Bootstrap a new factory project.
factory initdrops a complete scaffold (package.jsonwith deps + scripts pinned,tsconfig.json,tsconfig.build.json,.gitignore,biome.json,factory.config.json,README.md,.claude/commands/scope-project.md, and thedocs/{specs,technical-plans}/done/skeleton). Idempotent + safe — exits 2 with a list of conflicts if any target file exists; never overwrites without consent. - Lint a spec.
factory spec lint <path>runs the format-floor check (frontmatter shape, scenario structure, satisfaction-line syntax,depends-onvalidation, wide-blast-radius warning). Fast, free, deterministic. - Review spec quality.
factory spec review <path>dispatches into@wifo/factory-spec-reviewto run 8 LLM judges scoring spec quality beyond format. Cache-backed; subscription-paid viaclaude -p. - Watch a directory tree continuously (v0.0.10+).
factory spec watch <path>re-runs lint (+ optionally review) on every*.mdchange. Companion to the PostToolUse hook recipe. - Programmatically parse / lint / watch specs. Import
parseSpec,lintSpec,lintSpecFile,parseDodBullets,watchSpecsdirectly.
What's inside
CLI commands
The factory binary dispatches into subcommands:
factory init [--name <pkg>] [--adopt] # Scaffold a new factory project (--adopt: additive, for existing repos)
factory spec lint <path> # Format-floor lint (recurses on dirs)
factory spec review <path> [flags] # Quality review (8 LLM judges)
factory spec watch <path> [--review] [...] # Continuous lint+review on save (v0.0.10+)
factory spec schema # Emit JSON Schema for editor intellisense
factory finish-task <id> [--dir <p>] [--context-dir <p>] # Move converged spec to done/ + emit factory-spec-shipped (v0.0.12+)
factory finish-task --all-converged [--since <factorySequenceId>] [--dir <p>] [--context-dir <p>]
# Batch-ship every converged spec under a factory-sequence (v0.0.13+)
# v0.0.14+: --context-dir defaults to ./.factory (was ./context); auto-detects
# factory.config.json runtime.contextDir. Precedence: CLI flag > config > default.factory init --adopt (v0.0.12+) is the brownfield-adopter onramp: walks the same template plan as factory init but skips files in IGNORE_IF_PRESENT (package.json, tsconfig.json, biome.json, bunfig.toml, README.md) when they already exist, appends factory entries (.factory, .factory-spec-review-cache) to a pre-existing .gitignore, and creates only the factory-specific bits (docs/specs/done/, docs/technical-plans/done/, factory.config.json, .claude/commands/scope-project.md). Idempotent — running twice never duplicates .gitignore entries. Does NOT mutate your package.json (a future --write-deps will opt-in to that).
factory finish-task <id> (v0.0.12+) ships a converged spec: moves <dir>/<id>.md to <dir>/done/<id>.md (creating done/ if missing) and emits a factory-spec-shipped context record parented on the converged factory-run so the lifecycle is reconstructible from the store alone. Refuses to run if no converged factory-run exists for the given spec id. The runtime emits a factory-runtime: <id> converged → ship via 'factory finish-task <id>' hint on stdout when a spec converges.
factory finish-task --all-converged (v0.0.13+) batch-ships every converged spec under a single factory-sequence — the natural counterpart to factory-runtime run-sequence, which already ships clusters of 4-6 specs in one invocation. With no flag, it walks the most recent factory-sequence (largest recordedAt; tie-break on lex-larger id). --since <factorySequenceId> overrides the default to target a specific sequence (full id only — no prefix matching). Mutually exclusive with the positional <spec-id> form (passing both exits 2). Errored specs in the same sequence stay at <dir>/<id>.md for the maintainer to retry; per-spec move failures abort the batch.
Status-aggregator semantic (v0.0.14+). --all-converged's "converged" predicate now matches run-sequence's no-converge verdict: it walks each candidate factory-run's factory-phase records grouped by iteration and keys off the FINAL iteration's terminal phase (status: 'pass' → ship; anything else → skip). Earlier iterations' failures are part of the implement-validate-iterate loop, not a verdict. The v0.0.13 BASELINE caught the inverse drift — finish-task shipped specs that run-sequence considered no-converge. Per-skip log: factory: skipped <id> (run <runId-short> did not converge — last phase: <status>). Summary line always reports both counts: factory: shipped <N> specs from sequence <seqId-short> (<M> skipped). An all-no-converge sequence is a safe no-op (exit 0, shipped=0).
# Ship every converged spec from the most recent run-sequence:
factory finish-task --all-converged
# → factory: shipped core-store-and-slug → done/ (run aa000000)
# factory: shipped shorten-endpoint → done/ (run bb000000)
# factory: skipped redirect (run cc000000 did not converge — last phase: fail)
# factory: shipped 2 specs from sequence 00112233 (1 skipped)
# Retroactively ship from an earlier sequence:
factory finish-task --all-converged --since 00112233aabbccddKey flags (spec review):
| Flag | Default | Notes |
|---|---|---|
| --cache-dir <path> | .factory-spec-review-cache | Per-spec-bytes cache so unchanged specs are free to re-run. |
| --no-cache | off | Disable cache layer. |
| --judges <a,b,c> | all 8 | Comma-separated subset. |
| --claude-bin <path> | claude on PATH | Override (test injection). |
| --technical-plan <path> | auto-resolved | Override path to paired technical-plan. |
| --timeout-ms <n> | 60000 | Per-judge timeout. |
Key flags (spec watch, v0.0.10+):
| Flag | Default | Notes |
|---|---|---|
| --review | off | Also run factory spec review --no-cache per change. |
| --debounce-ms <n> | 200 | Per-file debounce window. |
| --claude-bin <path> | claude | Forwarded to review subprocess. |
Public API (34 exports as of v0.0.12)
// Schema (Zod)
import {
KEBAB_ID_REGEX,
SpecFrontmatterSchema, SpecExemplarSchema, SpecClassificationSchema,
SpecScenarioKindSchema, SpecScenarioSatisfactionKindSchema, SpecScenarioSatisfactionSchema,
SpecStatusSchema, SpecTypeSchema,
} from '@wifo/factory-core';
import type {
Spec, SpecFrontmatter, SpecExemplar,
Scenario, ScenarioSatisfaction,
SpecClassification, SpecScenarioKind, SpecScenarioSatisfactionKind,
SpecStatus, SpecType,
} from '@wifo/factory-core';
// Parser
import { parseSpec, parseDodBullets, SpecParseError, splitFrontmatter } from '@wifo/factory-core';
import type { ParseSpecOptions, ParseIssue, DodBullet, FrontmatterSplit } from '@wifo/factory-core';
// Section slicer + scenario walker
import { findSection, parseScenarios } from '@wifo/factory-core';
import type { SectionExtract } from '@wifo/factory-core';
// Lint
import { lintSpec, lintSpecFile } from '@wifo/factory-core';
import type { LintError, LintOptions, LintSeverity } from '@wifo/factory-core';
// JSON Schema (editor intellisense)
import { getFrontmatterJsonSchema, SPEC_FRONTMATTER_SCHEMA_ID } from '@wifo/factory-core';
// Watch helper (v0.0.10+)
import { watchSpecs } from '@wifo/factory-core';
import type { WatchSpecsOptions } from '@wifo/factory-core';
// Spec lifecycle (v0.0.12+) — programmatic counterpart to `factory finish-task <id>`
import { finishTask } from '@wifo/factory-core';
import type { FinishTaskOptions, FinishTaskResult } from '@wifo/factory-core';
// Errors
import { FrontmatterError } from '@wifo/factory-core';Concepts
Spec format. YAML frontmatter (id, classification, type, status, exemplars, depends-on, agent-timeout-ms) + Markdown body sections (## Intent, ## Scenarios, ## Constraints / Decisions, ## Subtasks, ## Definition of Done, optionally ## Holdout Scenarios). Strict — unknown frontmatter fields surface as warnings. See docs/SPEC_TEMPLATE.md for the canonical skeleton.
parseDodBullets (v0.0.10+). Walks a ## Definition of Done section and classifies each bullet as kind: 'shell' (executable Bash from a locked allowlist) or kind: 'judge' (LLM-evaluated criterion). Powers the runtime's dodPhase. The shell allowlist is closed: pnpm, bun, npm, node, tsc, git, npx, bash, sh, make, pwd, ls, plus ./ and ../ paths.
watchSpecs (v0.0.10+). Long-running watcher returning { stop }. Re-runs lint (+ optionally review) per-file with a 200ms debounce. SIGINT-aware in the CLI wrapper.
Lint codes + NOQA. Format-level errors block; quality-level warnings inform. Codes include frontmatter/*, scenario/*, scenarios/*, spec/invalid-depends-on, spec/depends-on-missing, spec/wide-blast-radius, spec/test-name-quote-chars (v0.0.12+ — test: patterns using curly ‘ ’ “ ” get rewritten ASCII-clean before run-time), spec/dod-needs-explicit-command (v0.0.12+ — DoD bullets that look like runtime gates but don't embed a backtick-wrapped shell command; pairs with the runtime's literal-command DoD contract), and spec/yaml-colon-needs-quoting (v0.0.14+ — top-level frontmatter values that contain an unquoted colon-space, which YAML parses as a nested mapping and trips BLOCK_AS_IMPLICIT_KEY; wrap the value in single quotes — e.g., why: 'clicks: Map<string, Click[]>'). Suppress warnings via <!-- NOQA: spec/<code> --> HTML comment anywhere in the spec body (v0.0.10+; warnings only — errors cannot be suppressed). See AGENTS.md § 6 for the full code table.
Slash commands
/scope-project (v0.0.7+; auto-installed by factory init since v0.0.8)
Decompose a natural-language product description into 4-6 ordered LIGHT specs. Canonical source: packages/core/commands/scope-project.md. factory init writes the bundled file to <cwd>/.claude/commands/scope-project.md automatically.
For Claude Code sessions across all projects (user-level install):
cp node_modules/@wifo/factory-core/commands/scope-project.md ~/.claude/commands/scope-project.mdInvoke from any Claude Code session in a factory-bootstrapped project:
/scope-project A URL shortener with click tracking and JSON stats.
JSON-over-HTTP, in-memory storage, no auth.Output: 4-6 spec files under docs/specs/, first status: ready, rest status: drafting, every spec populates depends-on. Worked-example output: docs/baselines/scope-project-fixtures/url-shortener/.
Smoke-boot extension (v0.0.12+). When a generated spec mentions an HTTP entrypoint pattern (createServer, listen(<port>), app.listen, http.createServer, Bun.serve, serve(), /scope-project appends a smoke-boot scenario that spawns bun src/main.ts, probes a route, and kills the process. The smoke-boot test forces the production entrypoint into existence — closing the v0.0.11 BASELINE gap where library code shipped but bun src/main.ts 404'd because no test: line ever forced the entrypoint to be written.
Init-scaffold polishes (v0.0.13+). Three first-contact frictions surfaced in the v0.0.12 BASELINE close in this release:
biome.jsonschema migrates to Biome 2.x. The scaffold pins@biomejs/biome ^2.4.4(a 2.x release) but until v0.0.13 emitted the Biome 1.xfiles.includekey, sopnpm checkerrored on first run. v0.0.13 emitsfiles.includes(Biome 2.x) and addsformatter.indentStyle: 'space'so the JSON-stringified config is self-consistent. The schema major must stay locked to the pinned biome major..factory/is pre-created via.gitkeep. Pre-v0.0.13,.factory/was created lazily by the runtime, so any pre-runtime tooling (e.g.,tee .factory/run-sequence.log) failed until first run. v0.0.13 ships.factory/.gitkeepin the scaffold so the dir exists fromgit clonetime..gitignorenow lists.factory/worktrees/and.factory/twin-recordings/(the per-record subdirs the runtime writes);.factory/itself is tracked so users see the dir without ambiguity.factory.config.jsongainsdod.template. Astring[]of literal-command DoD bullet bodies derived from the scaffold'spackage.jsonscripts (typecheck, test, check)./scope-projectreads this at spec-author time and emits the same block into every generated spec's## Definition of Done, so the v0.0.12spec/dod-needs-explicit-commandlint stays green from the very first author.buildis intentionally excluded — build is a publish prereq, not a per-spec DoD gate. Override per-project by editingfactory.config.json.dod.template.
Day-zero scaffold polish (v0.0.14+). Two follow-on frictions surfaced in the v0.0.13 BASELINE close in this release — the scaffold can now pass its own DoD gates on day zero (pnpm typecheck && pnpm test && pnpm check all exit 0):
biome.jsonshort arrays are single-line. The v0.0.13 template wrotefiles.includesas a multi-line JSON array (a side-effect ofJSON.stringify(..., null, 2)); biome's lineWidth=100 rule self-flags multi-line arrays of 1-2 elements, so the scaffold's ownpnpm checkerrored on biome.json's own format. v0.0.14 ships biome.json (and tsconfig.json) as pre-formatted strings with single-line short arrays, and widensfiles.includesto["**"]sopnpm checkvalidates the whole scaffold (not just an empty src/).- Stub
src/index.ts+src/index.test.ts. The v0.0.13 scaffold'ssrc/was empty (only a.gitkeep), sopnpm typecheckandpnpm testhad nothing to operate on —bun test srcerrored "no tests found" and tsc compiled zero files. v0.0.14 ships a one-line stubexport const VERSION = "0.0.0";plus a four-line bun:test that imports it, replacing the v0.0.13src/.gitkeep. The stubs are intentionally tiny so/scope-projectand the agent overwrite them on first feature scope without ceremony.
/scope-task
Single-task analog. Lives at ~/.claude/commands/scope-task.md (predates the "ship in repo" convention).
factory.config.json
Written by factory init at the scaffold root. Defaults the canonical run flags so factory-runtime run collapses to a flagless invocation:
{
"runtime": {
"maxIterations": 5,
"maxTotalTokens": 1000000,
"maxPromptTokens": 100000,
"noJudge": false,
"maxSequenceTokens": 5000000,
"continueOnFail": false,
"includeDrafting": false,
"skipDodPhase": false
}
}Precedence: CLI flag > config file > built-in default. Unknown keys are tolerated for forward-compatibility. Edit to taste; absent or malformed files are ignored silently.
Harness-enforced spec linting + review (Claude Code hook recipe)
factory spec lint and factory spec review are most valuable when they run on every save — but agents forget. The fix is harness-enforced: a PostToolUse hook runs both whenever the agent writes a spec file.
Add to ~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "if [ \"${CLAUDE_PROJECT_DIR}/${CLAUDE_FILE_PATH}\" = *docs/specs/*.md ]; then pnpm exec factory spec lint \"$CLAUDE_FILE_PATH\" && pnpm exec factory spec review \"$CLAUDE_FILE_PATH\" --no-cache; fi"
}
]
}
}The shell guard fires only for docs/specs/*.md writes. Bash/zsh-portable. This recipe is intentionally opt-in — factory init does not touch ~/.claude/, and there is no factory hook install command. The Claude-Code-independent companion is factory spec watch (v0.0.10+) — runs in a terminal, no hook needed.
Worked example
mkdir my-app && cd my-app && git init -q
npx -y @wifo/factory-core init --name my-app
pnpm install
# Author a single spec (one-off feature)
echo "..." > docs/specs/my-feature.md # or use /scope-task
pnpm exec factory spec lint docs/specs/
# → docs/specs/my-feature.md: OK
pnpm exec factory spec review docs/specs/my-feature.md
# Continuous watch in another terminal
pnpm exec factory spec watch docs/specs/ --reviewSee also
AGENTS.md— single doc for AI agents using the toolchain.README.md(top-level) — project overview + worked examples.docs/SPEC_TEMPLATE.md— canonical spec skeleton.packages/runtime/README.md— the runtime that ships specs into code.packages/spec-review/README.md— the LLM judgesfactory spec reviewdispatches into.CHANGELOG.md— every release's deltas.
Status
Pre-alpha. APIs may break in point releases until v0.1.0.
