The prior commit's git add silently dropped these five files because
one path in the same invocation didn't exist. Docs, solution file, and
example config still needed the paired update: Novelly.slnx drops the
Novelly.Mcp project entry, README and CLAUDE.md describe MCP as an
in-API HTTP endpoint instead of a stdio binary, .mcp.json.example uses
the type: http shape, and outline-importer.md's tool references are
fixed to real tool names.
Dual-engine workflow (.github/workflows/ci.yml, read by both GitHub Actions
and Gitea Actions): build, test, coverage badge on every push; on Gitea main
pushes only, build+push API/web images to the Gitea registry and redeploy
the persistent LAN stack via the shared [self-hosted, qa] runner. Replaces
the ad hoc docker-compose.deploy.yml manual workflow with
deploy/qa/docker-compose.qa.yml, pulled by CI — data volume preserved
across deploys (no -v on down), unlike mic-check's throwaway QA stack.
Docs had drifted from the code across several prior renames; refreshes the
data model, API surface table, and tool counts to match, and documents the
new Locations feature and chapter character summary alongside the actual
current commit.
.mcp.json needs NOVELLY_API_KEY to match the API's Auth:ServiceApiKey
user secret, so it can't be a checked-in file — gitignore it and keep
.mcp.json.example (with the key blanked out) as the template. README
walks through copying the example and setting the matching user secret.
Aspire doesn't run or manage src/Novelly.Mcp — it's a separate stdio
process the MCP client spawns from a published binary that nothing
rebuilds automatically. It had drifted 12 days out of date and was
silently missing the service-API-key auth header, causing confusing
401s. Script wraps the existing dotnet publish command via the shared
ensure_dotnet helper; README points at it instead of the raw command.
Also registers a UserSecretsId on Novelly.Api so Auth:ServiceApiKey
can be set locally without landing in appsettings.
Services now return entities; endpoints (and the agent toolsets) map to
*Response records instead of services building wire DTOs themselves.
Also brings in the outline-import agent, MCP tool, ledger and web dialog
that were already in progress on disk.
The character page gains three sections under the dossier: the arc as an
editable ordered table with each stage pinnable to a chapter, every beat the
character appears in across the book (each row linking into that chapter's
outline), and the character's open questions. The sidebar groups main
characters above supporting ones, and both the sheet and the add dialog let you
set importance.
The arc section shows for main characters, and also for supporting ones that
already have stages — demoting someone should not hide work they thought they
had lost.
The outline page gains a notes section and an open-questions section at the
bottom. Beat rows are now anchored so the character page can link straight to a
row. Raising a question from either page attaches it to what that page is
about, and the section hides the association it is already scoped to rather
than repeating "Landfall" on every row.
Also fixes an ordering wart the browser run exposed: both CharacterRole and
CharacterImportance are stored as text, so ordering them in SQL ordered the
spelling — "Deuteragonist" beat "Protagonist" and the sidebar put the second
lead above the character the book is about. Listing now sorts after
materialising, which uses the enums' declaration order. The test for it was
checked both ways: it fails on the SQL ordering and passes on the fix.
73 tests pass, the web client builds and lints clean. Driven in a browser
end to end: resolving a question with "also add to notes" drops it off the open
list and appends the decision under the chapter's existing note, "show
resolved" brings it back with a Reopen button, and a beat link on the character
page lands on the right chapter outline at that beat's anchor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
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
The self-nesting outline tree was more structure than chapter outlining needs.
A chapter outline is now a paragraph plus a flat, ordered table of beats, and
tags do the cross-referencing that nesting was doing badly.
A beat is one row: a three-to-five word title, an optional character, what
happened, and what's next. Ordering is a SortOrder column within the chapter —
no parent pointers, no cycle guards, no recursive tree building. Reordering is
one call taking beat ids in the order wanted; ids left out keep their relative
position at the end rather than jumping to the front.
Beats plan, scenes carry prose. The two layers stay separate and a beat's
SceneId is the optional link between them, nullable in both directions —
deleting a scene ungroups its beats rather than deleting the plan, since that
is a decision about prose and not about the outline.
Tags are project-scoped, unique by name case-insensitively, and attach to
characters, chapters and beats through three join tables so cascade deletes are
the database's job rather than ours. Applying an unknown tag by name creates it,
which keeps tagging a single action; GET /api/tags/{id}/references returns
everything carrying a tag across all three kinds at once.
Removed: OutlineNode, OutlineService, its endpoints, agent and MCP tools, and
the Outline tab. Added: Beat and Tag with their services, endpoints, 5 agent
tools and 10 MCP tools, a beat table on the chapter page, a tag editor used in
three places, and a Tags tab for cross-referencing.
Migration drops OutlineNodes — the scaffolder's data-loss warning is the
intended removal, not an accident.
44 tests, up from 31.
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