Drop the Scene entity/grouping in favor of chapters carrying prose directly
and beats belonging to many characters. Add markdown editor + character
multi-select components to the web client. Remove all XML doc and inline
comments across the touched C#/TS/CSS files in favor of self-documenting
names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP
server config, no secrets) and ignore .idea/.
The layered split into Domain/Application/Infrastructure/Api was forcing
organisation by layer: adding one capability meant touching four projects and
four folders that each held a slice of it. Those four projects are now one
feature-organised Novelly.Api, where each folder — Projects, Characters,
Chapters, Beats, Scenes, Tags, Agent — holds its entity, DTOs, service and
endpoints together. Common/ holds what genuinely crosses features (the patch
semantics, the two exception types, DraftStatus) and Data/ holds the DbContext
and migrations.
Six .NET projects become five: the three layer projects are gone, and
Novelly.AppHost and Novelly.ServiceDefaults are new.
- Namespaces move from NovelSoftware.* to Novelly.*, including the entity type
names recorded in the EF model snapshots. The migration ids are untouched, so
an existing novel.db still migrates cleanly — verified against a fresh file.
- Aspire orchestration mirrors the mic-check setup: the AppHost starts the API
on :5080 and the Vite dev server on :5173, and the API picks up OpenTelemetry,
health checks and service discovery from ServiceDefaults. /health and /alive
now answer in development.
- A Husky pre-push hook runs scripts/ci/prepush.sh: build, test, then a web
build. The scripts are plain bash so CI can run the same steps.
- The MCP server's env var is now NOVELLY_API_URL.
Verified beyond the build: 44 tests pass, the web client builds, the API was
exercised over curl (project/chapter/beat/tag round trip, tag cross-reference,
503 on the agent without a key while conversation listing still returns 200),
the MCP server was driven over stdio JSON-RPC (26 tools, errors still surface
the API's own message rather than being flattened), and the AppHost was run to
confirm both resources come up and Vite proxies /api through to the API.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
Builds out the vertical slice for planning and writing a novel. Three front
ends — the React UI, an embedded Claude agent, and an MCP stdio server — all go
through one REST API, so an edit made from Claude Code and one made in the
browser are the same edit.
Layout:
Domain entities and enums, no dependencies
Application services, DTOs, the agent tool-use loop and its 15 tools
Infrastructure EF Core 10 + SQLite, Anthropic SDK client
Api ASP.NET Core 10 minimal APIs, OpenAPI, ProblemDetails
Mcp MCP stdio server, 21 tools over the same REST API
Web React 19 + Vite + TanStack Query + Tailwind v4
Data model is Project > Characters / OutlineNodes / Chapters > Scenes, plus
agent conversations. The outline is a self-nesting tree so acts, sequences and
beats can be arranged however the book wants; scenes carry goal/conflict/outcome
because that is what the agent drafts prose from.
Notes on a few choices:
- Conversation history replays to the model as text only. The agent re-reads
current state through its tools rather than trusting a record of edits that
may since have changed in the UI.
- The user's turn is persisted before the tool loop runs, so a question is
recorded even when the model call fails. Turn order uses an explicit sequence
column; timestamps tie when a turn completes inside one tick.
- Tool failures return is_error results rather than throwing, so the model can
read the message and correct itself. MCP tools do the same via CallToolResult,
which keeps the API's own message instead of a generic SDK error.
- The Anthropic client is constructed lazily. It is injected into the agent
service, which also serves read-only endpoints, and those should keep working
on an install with no key. Sending without one returns 503, not 400.
- DateTimeOffset is stored as UTC ticks. SQLite refuses to ORDER BY the default
text form, which every "recently updated first" listing depends on.
Tests run against real in-memory SQLite rather than the EF in-memory provider so
they exercise the cascade deletes and query translation that actually ship.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw