Thanks for your interest! This guide covers local setup, tests, and conventions.
WRITING AGENT runs on Python 3.10+ and is tested on Linux, macOS, and Windows.
git clone https://github.com/vikast908/WritingAgent.git
cd WritingAgent
python -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows (PowerShell)
pip install -e ".[dev]" # editable install + pytest + ruffOptional - the app runs fine without them (imported lazily, degrade gracefully).
- Deep-research fetch backend:
pip install -e ".[deep]"(thescrapo-aiengine + Playwright, Python 3.11+), thenpython -m playwright install chromium. Absent it, a stdlib fetch is used. - Firecrawl search: set
FIRECRAWL_API_KEYin.envandsearch_provider: firecrawlto swap the default DuckDuckGo backend (a missing key falls back to DuckDuckGo).
The old
[headroom]context-compression extra has been removed - it saved ~nothing on single-turn payloads and perturbed the DeepSeek prompt cache. Cost is handled by prompt-cache pinning (openrouter_providers) andcost_mode: budget.
pyproject.toml is the canonical dependency declaration; there is no checked-in lock
file. To pin an exact, reproducible set for a deploy, generate one in a clean venv
installed from public PyPI:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # add deep,web to lock those extras too
python scripts/gen_lock.py > requirements.lock.txtscripts/gen_lock.py resolves the closure of the project's declared dependencies against
the installed environment (so an unrelated package in the venv can't pollute the lock).
Don't commit a lock generated in a non-clean environment - the pins are only as resolvable
as the environment they're read from.
cp .env.example .env # Windows: copy .env.example .env
# add your OPENROUTER_API_KEYYou do not need a key to develop or run the tests - see fake mode below.
pytestThe suite runs fully offline. Tests that exercise the pipeline use fake mode
(WRITINGAGENT_FAKE=1), where every LLM node returns deterministic placeholder
output - no network, no key. You can drive the whole app this way too:
WRITINGAGENT_FAKE=1 python -m writingagent new --abstract "test" --pick 1
WRITINGAGENT_FAKE=1 python -m writingagent runWe use ruff:
ruff check . # lint
ruff format . # formatA .pre-commit-config.yaml is provided - run pre-commit install to lint on commit.
docs/plan.mdis the architecture/spec source of truth;docs/dev/resume.mdis the running dev journal (newest entry on top). Durable decisions go indocs/plan.md, notdocs/dev/resume.md.- Keep nodes as deterministic, single-purpose LLM calls - see
docs/plan.md§4. (Self-directing behavior is the opt-in agentic controller inagentic/(docs/plan.md§21), a separate layer - don't bake it into a node.) - Network/IO is best-effort: degrade gracefully, never crash the pipeline on a fetch error.
- All numeric thresholds are tunable config (
config/settings.yamlin the agent home -$WRITINGAGENT_HOME, default: the OS user-data dir; seesrc/writingagent/paths.py), not hard-coded. - Editorial design system: the web dashboard and the TUI both follow
docs/design.md- ink on warm paper, one accent (manuscript red#a3341f), the Fraunces display serif, WCAG-AA verified. Every value is a token: port to CSS vars for the web app or to aui.THEMESpalette for the TUI (defaulteditorial, "ink & brass"). Named themes recolor, never restructure - matchdocs/design.mdrather than hand-picking colors. - Cross-platform: use
pathlib, avoid shelling out, and don't assume a POSIX or Windows path layout. CI runs the suite on all three OSes - keep it green.
- Branch off
master. - Make the change + add/adjust tests.
ruff check . && pytestlocally.- Open a PR describing the change and linking any issue. CI must pass.
A 60-second map (full detail in docs/plan.md and the README's Architecture section):
src/writingagent/orchestrator/- durable on-disk state machine (the brain is the checkpoint); a package (common/book/article/review/export/manage).agentic/is the opt-in self-directing controller layered over it (docs/plan.md§21).nodes.py/prompts.py/schemas.py- the LLM nodes, their prompts (incl. thewrap_untrustedinjection fence), and structured outputs.llm.py- model-host client (OpenAI-compatible, works with any of the 23 hosts): retry/backoff, timeout, repair, run token budget, usage/cost tallies;cost_mode: budgetroutes the judgment nodes to the flash tier.telemetry.py- per-call JSONL records + the/dashboardaggregation (per-node / per-unit cost attribution behind the web Telemetry / Cost views).brain.py/store.py- markdown filesystem layout + SQLite/FTS canon & graph.search.py/deep_research.py/images.py/cache.py- the optional research stack (search_provider: DuckDuckGo default, Firecrawl opt-in viaFIRECRAWL_API_KEY).seo.py/promote.py- the local distribution layer: a deterministic on-page SEO audit + keyword pack, and platform variants / headline variants / restyle. Local artifacts only - neither posts or schedules anything.webui/- the local web dashboard (server.py: stdlibThreadingHTTPServer+ SSE,static/: the single-page app).writing-agent webserves it on127.0.0.1only, no auth, one job at a time; no build step and no bundler. It calls the same engine facade as the TUI/CLI.registers.py/craft.py/exemplars.py/surgery.py/fields.py- the craft engine: genre/register profiles, deterministic craft metrics, few-shot exemplars, surgical show-don't-tell / passive passes, and structural templates (docs/plan.md§22).compositor.py/personas.py(+resources/personas/*.md) /emotions.py- the layer cascade that selects one voice from register ⊃ field ⊃ persona ⊃ emotion ⊃ skills: selectable personas (manner) and anti-cliché emotion deny-lists (docs/plan.md§23).resources/gold/*.md- the per-register genre style corpus (the default "match this" voice exemplar). The gold/persona corpora and the register profiles are tunable data, not hard-code.shell//cli//ui.py- Rich TUI, one-shot CLI (both packages), and the theme registry (11 themes: palette + wordmark figlet per theme).export.py- pdf · epub · html · docx · txt · md renderers.