novelforge-agent 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +17 -13
  2. package/dist/src/cli/index.js +10 -2
  3. package/dist/src/core/contextBuilder.js +32 -0
  4. package/dist/src/core/fileNames.js +3 -3
  5. package/dist/src/core/projectDiscovery.js +1 -0
  6. package/dist/src/core/projectOps.js +7 -1
  7. package/dist/src/core/projectStore.js +4 -1
  8. package/dist/src/core/prompts/en-US.js +130 -3
  9. package/dist/src/core/prompts/zh-CN.js +130 -3
  10. package/dist/src/core/schemas.js +23 -0
  11. package/dist/src/core/steps/architectureExtension.js +72 -0
  12. package/dist/src/core/steps/chapterReview.js +2 -1
  13. package/dist/src/core/steps/index.js +4 -0
  14. package/dist/src/core/steps/memoryCard.js +22 -1
  15. package/dist/src/core/steps/novelMetadata.js +47 -2
  16. package/dist/src/core/steps/storyBible.js +1 -1
  17. package/dist/src/core/steps/styleGuide.js +12 -0
  18. package/dist/src/core/workflow.js +2 -0
  19. package/dist/src/mcp/tools.js +36 -8
  20. package/package.json +1 -1
  21. package/src/cli/index.ts +11 -3
  22. package/src/core/contextBuilder.ts +30 -0
  23. package/src/core/fileNames.ts +3 -3
  24. package/src/core/projectDiscovery.ts +2 -0
  25. package/src/core/projectOps.ts +9 -1
  26. package/src/core/projectStore.ts +8 -1
  27. package/src/core/prompts/en-US.ts +132 -3
  28. package/src/core/prompts/types.ts +2 -0
  29. package/src/core/prompts/zh-CN.ts +132 -3
  30. package/src/core/schemas.ts +25 -0
  31. package/src/core/steps/architectureExtension.ts +88 -0
  32. package/src/core/steps/chapterReview.ts +2 -1
  33. package/src/core/steps/index.ts +4 -0
  34. package/src/core/steps/memoryCard.ts +23 -1
  35. package/src/core/steps/novelMetadata.ts +48 -2
  36. package/src/core/steps/storyBible.ts +1 -1
  37. package/src/core/steps/styleGuide.ts +13 -0
  38. package/src/core/types.ts +32 -0
  39. package/src/core/workflow.ts +2 -0
  40. package/src/mcp/tools.ts +35 -8
package/README.md CHANGED
@@ -57,6 +57,7 @@ The installer is **idempotent and safe**: it never overwrites an existing entry
57
57
  |-------|------|---------------------|----------------------|
58
58
  | Setup | `novel_metadata` | Output JSON: title, genre, premise, cast | `novel.json` |
59
59
  | | `story_bible` | Output Markdown: characters, world rules, plot threads | `story-bible.md` |
60
+ | | `style_guide` | Output JSON: narrative voice, pacing, diction, dialogue rules, prohibited patterns, prose rhythm, sample prose | `style-guide.json` |
60
61
  | | `architecture` | Output JSON: full / volume / pacing / chapter outlines | `architecture/{full.md, volumes.json, volume-pacing.json, chapters.json}` |
61
62
  | Loop | `chapter` | Write chapter N Markdown | `chapters/NNN.md` |
62
63
  | | `chapter_review` | Enforce the chapter acceptance gate: required beats, plot/character/thread progress, story-bible consistency, ending hook, repetition check | `reviews/chapter/chapter-NNN.json` |
@@ -67,7 +68,7 @@ The installer is **idempotent and safe**: it never overwrites an existing entry
67
68
  | | `chapter_revision` | Rewrite a chapter; previous version auto-archived | `chapters/.versions/NNN.<ts>.md` |
68
69
  | | `cross_chapter_review` | Cross-chapter continuity audit | `reviews/cross/cross-S-E.json` |
69
70
 
70
- Each chapter / bible / memory write also feeds a per-project BM25 index (`.index/`) so the agent can hand the host semantically relevant snippets when later chapters are generated, or answer ad-hoc `retrieve` queries from the host. Chapter generation context also includes the independent character state table (`characters.json`) and current volume pacing board (`architecture/volume-pacing.json`) when available.
71
+ Each chapter / bible / memory write also feeds a per-project BM25 index (`.index/`) so the agent can hand the host semantically relevant snippets when later chapters are generated, or answer ad-hoc `retrieve` queries from the host. Chapter generation context also includes the style guide (`style-guide.json`), independent character state table (`characters.json`), and current volume pacing board (`architecture/volume-pacing.json`) when available. The style guide includes `proseRhythm`, which checks rhythm anti-patterns such as excessive short-sentence density, consecutive one-sentence paragraphs, fake rhythm through line breaks, overly direct interior explanation, and repeated sentence patterns.
71
72
 
72
73
  ## Install
73
74
 
@@ -170,7 +171,7 @@ NOVELFORGE_WORKSPACE = "/absolute/path/where/projects/should/live"
170
171
  ## Tool reference
171
172
 
172
173
  ### Project lifecycle
173
- - **`start_novel_project`** `(prompt, language?, outputDir?, targetChapters?)` — create a new project under `<workspaceRoot>/<outputDir>/<slug>-<rand6>/` and return the first step's instruction.
174
+ - **`start_novel_project`** `(prompt, language?, outputDir?, targetChapters?, plannedTotalChapters?)` — create a new project under `<workspaceRoot>/<outputDir>/<prompt-slug>-<rand6>/` and return the first step's instruction. After `novel_metadata` is accepted, the directory is renamed to `<title-slug>-<same-rand6>/`; callers must continue with the returned `state.projectPath`. `targetChapters` is the per-batch planning size; MCP defaults to 5. `plannedTotalChapters` is the whole-book target; MCP defaults to 12.
174
175
  - **`list_projects`** `(outputDir?)` — list all projects in the workspace, newest first.
175
176
  - **`get_project_status`** `(projectPath)` — compact summary: current step, chapters written, open threads, latest review verdict.
176
177
  - **`get_next_step`** `(projectPath)` — return the prompt + packed context for whatever the workflow expects next.
@@ -179,13 +180,15 @@ NOVELFORGE_WORKSPACE = "/absolute/path/where/projects/should/live"
179
180
  - **`submit_step_result`** `(projectPath, step, content)` — validate `content` against the step's zod schema, persist it, advance the state machine. On failure the bad submission is written to `.agent-recovery/failed-*.txt` and the state does not advance.
180
181
  - **`get_context`** `(projectPath, purpose, chapterNumber?, start?, end?)` — build purpose-specific context without changing state. Useful when the host wants to read what the agent *would* have packed.
181
182
 
183
+ Dynamic planning is built into the state machine: after each accepted chapter and memory card, the agent checks `plannedTotalChapters` and the highest chapter covered by `architecture/chapters.json`. If the next chapter is still inside the whole-book target but not yet planned, the next step becomes `architecture_extension`; after the host submits that JSON, generation resumes at `chapter`.
184
+
182
185
  ### Semantic actions (verb-style; safe to call any time)
183
186
  - **`generate_chapter`** `(projectPath, chapterNumber)` — return generation context for a specific chapter.
184
187
  - **`extract_memory_card`** `(projectPath, chapterNumber)` — return memory-extraction context for a specific chapter.
185
188
  - **`review_chapter`** `(projectPath, chapterNumber)` — switch into a single-chapter editorial review side-track and return its prompt. After `submit_step_result(step="chapter_review")`, the workflow resumes its prior step automatically.
186
189
  - **`revise_chapter`** `(projectPath, chapterNumber, feedback?)` — switch into a chapter-revision side-track. Submitting `chapter_revision` content auto-archives the previous version under `chapters/.versions/`.
187
190
  - **`cross_chapter_review`** `(projectPath, start?, end?)` — switch into a cross-chapter audit side-track over the given range (defaults to all generated chapters).
188
- - **`save_chapter`** `(projectPath, chapterNumber, title, content)` — write a chapter Markdown file directly, without going through the state machine.
191
+ - **`save_chapter`** `(projectPath, chapterNumber, title, content)` — submit the current chapter through the state machine; it requires `currentStep="chapter"` and then advances to mandatory `chapter_review`.
189
192
 
190
193
  ### Project operations
191
194
  - **`amend_story_bible`** `(projectPath, content, reason?)` — replace `story-bible.md`, archive the previous version, and rebuild the bible index.
@@ -204,11 +207,12 @@ NOVELFORGE_WORKSPACE = "/absolute/path/where/projects/should/live"
204
207
  A project on disk:
205
208
 
206
209
  ```
207
- novels/<slug>-<rand6>/
210
+ novels/<title-slug>-<rand6>/
208
211
  ├── agent-state.json # workflow state (currentStep, currentChapter, files map, …)
209
212
  ├── novel.json # metadata (NovelMetadataSchema)
210
213
  ├── characters.json # independent character state table
211
214
  ├── story-bible.md
215
+ ├── style-guide.json # enforceable prose style guide
212
216
  ├── architecture/
213
217
  │ ├── full.md
214
218
  │ ├── volumes.json
@@ -237,15 +241,15 @@ The whole directory is self-contained — copy it, share it, delete it.
237
241
  ## How the workflow advances
238
242
 
239
243
  ```
240
- novel_metadata → story_bible → architecture → chapter
241
-
242
- chapter_review
243
-
244
- ┌──────────┴──────────┐
245
- clean issues_found
246
- ↓ ↓
247
- memory_card chapter_revision
248
- ↓ ↓
244
+ novel_metadata → story_bible → style_guide → architecture → chapter
245
+
246
+ chapter_review
247
+
248
+ ┌──────────┴──────────┐
249
+ clean issues_found
250
+ ↓ ↓
251
+ memory_card chapter_revision
252
+ ↓ ↓
249
253
  ┌─────────────┴─────────────┐ │
250
254
  (more chapters) (all done) │
251
255
  ↓ ↓ │
@@ -38,9 +38,17 @@ export async function runCli(argv = process.argv.slice(2), cwd = process.cwd())
38
38
  if (!prompt.trim())
39
39
  throw new Error('Missing --prompt');
40
40
  const language = parseLanguage(valueAfter(argv, '--language') || 'zh-CN');
41
- const chapters = Number(valueAfter(argv, '--chapters') || 3);
41
+ const chapters = Number(valueAfter(argv, '--chapters') || 5);
42
+ const totalChapters = Number(valueAfter(argv, '--total-chapters') || 12);
42
43
  const outputDir = valueAfter(argv, '--output') || 'novels';
43
- const result = await createProject({ workspaceRoot: cwd, prompt, language, outputDir, targetChapters: chapters });
44
+ const result = await createProject({
45
+ workspaceRoot: cwd,
46
+ prompt,
47
+ language,
48
+ outputDir,
49
+ targetChapters: chapters,
50
+ plannedTotalChapters: totalChapters,
51
+ });
44
52
  const next = await getNextStep(result.state.projectPath);
45
53
  console.log(JSON.stringify({ state: result.state, next }, null, 2));
46
54
  return;
@@ -15,6 +15,7 @@ export async function buildContext(input) {
15
15
  const parts = [];
16
16
  const metadata = await readOptional(join(input.projectPath, 'novel.json'));
17
17
  const storyBible = await readOptional(join(input.projectPath, 'story-bible.md'));
18
+ const styleGuideJson = await readOptional(join(input.projectPath, 'style-guide.json'));
18
19
  const chaptersJson = await readOptional(join(input.projectPath, 'architecture/chapters.json'));
19
20
  const charactersJson = await readOptional(join(input.projectPath, 'characters.json'));
20
21
  const volumePacingJson = await readOptional(join(input.projectPath, 'architecture/volume-pacing.json'));
@@ -22,6 +23,8 @@ export async function buildContext(input) {
22
23
  parts.push(`## Novel Metadata\n${metadata}`);
23
24
  if (storyBible)
24
25
  parts.push(`## Story Bible\n${storyBible.slice(0, 4000)}`);
26
+ if (styleGuideJson)
27
+ parts.push(`## Style Guide\n${styleGuideJson}`);
25
28
  if (charactersJson)
26
29
  parts.push(`## Character State Table\n${charactersJson}`);
27
30
  function addVolumePacing(volumeId) {
@@ -104,6 +107,35 @@ export async function buildContext(input) {
104
107
  if (memoryParts.length)
105
108
  parts.push(`## Memory Cards\n${memoryParts.join('\n')}`);
106
109
  }
110
+ if (input.purpose === 'architecture_extension') {
111
+ if (chaptersJson)
112
+ parts.push(`## Existing Chapter Architecture List\n${chaptersJson}`);
113
+ if (volumePacingJson)
114
+ parts.push(`## Existing Volume Pacing Boards\n${volumePacingJson}`);
115
+ const start = Math.max(1, (input.chapterNumber ?? 1) - 5);
116
+ const end = Math.max(0, (input.chapterNumber ?? 1) - 1);
117
+ const memoryParts = [];
118
+ for (let i = start; i <= end; i += 1) {
119
+ const memory = await readOptional(join(input.projectPath, 'memory', memoryFileName(i)));
120
+ if (memory)
121
+ memoryParts.push(`### Chapter ${i} Memory\n${memory}`);
122
+ }
123
+ if (memoryParts.length)
124
+ parts.push(`## Recent Memory Cards\n${memoryParts.join('\n')}`);
125
+ const allThreads = await loadThreads(input.projectPath);
126
+ const active = activeThreads(allThreads);
127
+ if (active.length) {
128
+ const lines = active.map((t) => {
129
+ const flags = [`#${t.id}`, `status=${t.status}`, `planted=ch${t.plantedAt}`];
130
+ if (t.plannedPayoffAt)
131
+ flags.push(`payoff=ch${t.plannedPayoffAt}`);
132
+ if (t.lastTouchedAt !== t.plantedAt)
133
+ flags.push(`touched=ch${t.lastTouchedAt}`);
134
+ return `- ${t.description} (${flags.join(', ')})`;
135
+ });
136
+ parts.push(`## Active Foreshadow Threads\n${lines.join('\n')}`);
137
+ }
138
+ }
107
139
  if (input.purpose === 'chapter_review' && input.chapterNumber) {
108
140
  if (chaptersJson) {
109
141
  const chapters = JSON.parse(chaptersJson);
@@ -8,12 +8,12 @@ export function makeProjectSlug(title) {
8
8
  const replaced = title
9
9
  .trim()
10
10
  .split('')
11
- .map((char) => PINYIN_FALLBACK[char] || char)
12
- .join('-')
11
+ .map((char) => PINYIN_FALLBACK[char] ? `-${PINYIN_FALLBACK[char]}-` : char)
12
+ .join('')
13
13
  .normalize('NFKD')
14
14
  .replace(/[\u0300-\u036f]/g, '')
15
15
  .toLowerCase()
16
- .replace(/[^a-z0-9]+/g, '-')
16
+ .replace(/[^\p{Letter}\p{Number}]+/gu, '-')
17
17
  .replace(/^-+|-+$/g, '');
18
18
  return replaced || `novel-${Date.now()}`;
19
19
  }
@@ -36,6 +36,7 @@ async function summarizeOne(projectPath) {
36
36
  currentStep: state.currentStep,
37
37
  currentChapter: state.currentChapter,
38
38
  targetChapters: state.targetChapters,
39
+ plannedTotalChapters: state.plannedTotalChapters ?? state.targetChapters,
39
40
  completedSteps: state.completedSteps.length,
40
41
  chaptersWritten: countChapters(state),
41
42
  updatedAt: state.updatedAt,
@@ -107,12 +107,14 @@ export async function deleteChapter(input) {
107
107
  const STEP_FILE_KEYS = {
108
108
  novel_metadata: ['novel'],
109
109
  story_bible: ['storyBible'],
110
+ style_guide: ['styleGuide'],
110
111
  architecture: ['architecture'],
111
112
  continuity_review: ['continuityReview'],
112
113
  };
113
114
  const STEP_FILE_PATHS = {
114
115
  novel_metadata: ['novel.json'],
115
116
  story_bible: ['story-bible.md'],
117
+ style_guide: ['style-guide.json'],
116
118
  architecture: ['architecture/full.md', 'architecture/volumes.json', 'architecture/chapters.json'],
117
119
  };
118
120
  export async function redoStep(input) {
@@ -143,7 +145,11 @@ export async function redoStep(input) {
143
145
  state.currentStep = input.step;
144
146
  state.pendingAction = undefined;
145
147
  }
146
- else if (input.step === 'novel_metadata' || input.step === 'story_bible' || input.step === 'architecture' || input.step === 'continuity_review') {
148
+ else if (input.step === 'novel_metadata'
149
+ || input.step === 'story_bible'
150
+ || input.step === 'style_guide'
151
+ || input.step === 'architecture'
152
+ || input.step === 'continuity_review') {
147
153
  const paths = STEP_FILE_PATHS[input.step] ?? [];
148
154
  for (const p of paths) {
149
155
  if (await tryUnlink(join(state.projectPath, p)))
@@ -45,7 +45,9 @@ export async function archiveStoryBible(projectPath, versionRelative) {
45
45
  export async function createProject(input) {
46
46
  const workspaceRoot = resolve(input.workspaceRoot);
47
47
  const baseDir = input.outputDir || 'novels';
48
- const targetChapters = Math.max(1, Math.floor(Number(input.targetChapters || 3)));
48
+ const hasExplicitTargetChapters = input.targetChapters !== undefined;
49
+ const targetChapters = Math.max(1, Math.floor(Number(input.targetChapters || 5)));
50
+ const plannedTotalChapters = Math.max(targetChapters, Math.floor(Number(input.plannedTotalChapters ?? (hasExplicitTargetChapters ? targetChapters : 12))));
49
51
  const baseSlug = makeProjectSlug(input.prompt.slice(0, 48));
50
52
  const suffix = randomBytes(3).toString('hex');
51
53
  const slug = `${baseSlug}-${suffix}`;
@@ -59,6 +61,7 @@ export async function createProject(input) {
59
61
  initialPrompt: input.prompt,
60
62
  language: input.language || 'zh-CN',
61
63
  targetChapters,
64
+ plannedTotalChapters,
62
65
  currentStep: 'novel_metadata',
63
66
  currentChapter: 1,
64
67
  completedSteps: [],
@@ -74,6 +74,55 @@ Rules:
74
74
  - Do not output JSON.`,
75
75
  };
76
76
  }
77
+ function buildStyleGuidePrompt(input) {
78
+ return {
79
+ purpose: 'style_guide',
80
+ expectedFormat: 'JSON matching StyleGuideSchema',
81
+ prompt: `You are the style editor for a long-form novel. From the user prompt, metadata, and story bible, create a style guide that chapter writing and review can enforce over the whole project.
82
+
83
+ ## User Prompt
84
+ ${input.state.initialPrompt}
85
+
86
+ ${input.context ? `## Existing Context\n${input.context}\n` : ''}## Output Requirements
87
+ Output valid JSON only, in this shape:
88
+ {
89
+ "narrativeVoice": "Narration person, POV distance, narrator texture, emotional temperature",
90
+ "pacing": "Rules for openings, transitions, conflict movement, and chapter-end hooks",
91
+ "diction": "Word choice, sentence density, genre terminology boundaries",
92
+ "dialogueRules": [
93
+ "Rules for core character dialogue length, tone, subtext, and forms of address"
94
+ ],
95
+ "prohibitedPatterns": [
96
+ "Patterns to avoid: modern memes, explanatory narration, lore dumping, voice drift, etc."
97
+ ],
98
+ "proseRhythm": {
99
+ "sentenceRhythm": "How short, medium, and long sentences should be used; short sentences should serve turns, danger, or emotional landings, not default narration",
100
+ "paragraphing": "Paragraphs should form complete narrative units; avoid consecutive one-sentence paragraphs and line breaks used as fake rhythm",
101
+ "interiorityMode": "How interiority should be refracted through action, hesitation, and sensory response; avoid frequent direct explanation of thoughts",
102
+ "emphasisBudget": "Budget for repetition, dashes, isolated short sentences, and other emphasis tools",
103
+ "antiPatterns": [
104
+ "3 or more consecutive one-sentence short paragraphs",
105
+ "many short sentences used to simulate tension",
106
+ "explaining psychology immediately after every action",
107
+ "repeating the same sentence pattern to create fake rhythm"
108
+ ]
109
+ },
110
+ "sampleParagraph": "A 120-250 word target-style sample. Do not turn it into plot outline.",
111
+ "consistencyChecks": [
112
+ "Concrete checks future chapter reviews should use to detect style drift"
113
+ ]
114
+ }
115
+
116
+ Rules:
117
+ - Match genre, premise, character identities, and reader expectations.
118
+ - Do not rely on abstract adjectives only; every field must guide actual prose.
119
+ - proseRhythm must not be fixed word-count rules; describe reviewable rhythm principles and anti-patterns.
120
+ - sampleParagraph demonstrates prose texture only. Do not reveal future plot.
121
+ - prohibitedPatterns must contain at least 3 entries; consistencyChecks must contain at least 3 entries.
122
+ - proseRhythm.antiPatterns must contain at least 4 entries.
123
+ ${strictJsonOutputRules()}`,
124
+ };
125
+ }
77
126
  function buildArchitecturePrompt(input) {
78
127
  return {
79
128
  purpose: 'architecture',
@@ -84,7 +133,7 @@ function buildArchitecturePrompt(input) {
84
133
  ${input.state.initialPrompt}
85
134
 
86
135
  ## Goals
87
- - Generate at least ${input.state.targetChapters} chapter architectures for this first run.
136
+ - Whole-book target is about ${input.state.plannedTotalChapters ?? input.state.targetChapters} chapters; generate only the first ${input.state.targetChapters} chapter architectures in this first batch.
88
137
  - The full-book architecture should define the long-term main line and ending direction.
89
138
  - Volume architecture should define phase conflict, climax, and volume-end hooks.
90
139
  - Chapter architecture must cover only what should happen in that chapter and must not reveal later concrete events early.
@@ -126,6 +175,7 @@ Output valid JSON only, in this shape:
126
175
 
127
176
  Rules:
128
177
  - chapters.length must be at least ${input.state.targetChapters}.
178
+ - chapters do not need to cover the whole book; when writing reaches the boundary, the workflow will request architecture_extension.
129
179
  - chapterNumber must start at 1 and increase contiguously.
130
180
  - volumeId must reference an id from volumes.
131
181
  - volumePacing must provide one pacing board for every volume.
@@ -133,6 +183,71 @@ Rules:
133
183
  ${strictJsonOutputRules()}`,
134
184
  };
135
185
  }
186
+ function buildArchitectureExtensionPrompt(input) {
187
+ const start = input.state.currentChapter;
188
+ const total = input.state.plannedTotalChapters ?? input.state.targetChapters;
189
+ const end = Math.min(total, start + input.state.targetChapters - 1);
190
+ return {
191
+ purpose: 'architecture_extension',
192
+ expectedFormat: 'JSON matching ArchitectureExtensionPayloadSchema',
193
+ prompt: `You are the chief architect for a long-form novel. The manuscript has reached the edge of the existing chapter plan; extend the architecture from current continuity.
194
+
195
+ ## Extension Range
196
+ - Start at chapter ${start}.
197
+ - This batch should plan through chapter ${end} at most.
198
+ - The whole-book target ends at chapter ${total}.
199
+
200
+ ## Extension Principles
201
+ - Do not rewrite existing chapter architecture; append only new chapter architecture.
202
+ - New chapters must follow recent memory, the character state table, active foreshadow threads, and volume pacing boards.
203
+ - If the next chapters enter a new volume, add volumes and volumePacing. If they remain in an existing volume, you may provide an updated pacing board for that volume.
204
+ - If the full-book direction needs adjustment because of written material, include fullUpdate. fullUpdate must be a complete replacement for architecture/full.md, not a change note.
205
+ - Chapter architecture must cover only what should happen in that chapter and must not reveal later concrete events early.
206
+
207
+ ${input.context ? `## Existing Context\n${input.context}\n` : ''}## Output Requirements
208
+ Output valid JSON only, in this shape:
209
+ {
210
+ "fullUpdate": "optional complete updated full-book architecture",
211
+ "volumes": [
212
+ {
213
+ "id": "v2",
214
+ "title": "New or updated volume title",
215
+ "summary": "Volume goal, conflict, climax, and end hook",
216
+ "order": 2
217
+ }
218
+ ],
219
+ "volumePacing": [
220
+ {
221
+ "volumeId": "v2",
222
+ "start": "Volume starting state",
223
+ "promise": "Volume promise",
224
+ "keyTurns": ["Key turn 1", "Key turn 2"],
225
+ "midpoint": "Midpoint turn",
226
+ "climax": "Volume climax",
227
+ "payoffs": ["Planned payoffs"],
228
+ "lingeringMysteries": ["Lingering mysteries"]
229
+ }
230
+ ],
231
+ "chapters": [
232
+ {
233
+ "chapterNumber": ${start},
234
+ "title": "Chapter title",
235
+ "volumeId": "v1",
236
+ "summary": "Chapter plot summary",
237
+ "requiredBeats": ["Required beat 1"]
238
+ }
239
+ ]
240
+ }
241
+
242
+ Rules:
243
+ - chapters[0].chapterNumber must equal ${start}.
244
+ - chapterNumber must increase contiguously and must not exceed ${total}.
245
+ - chapters.length should be ${end - start + 1} unless the book has reached its ending.
246
+ - requiredBeats must include at least one concrete, actionable beat.
247
+ - volumeId must reference an existing volume id or a volume id supplied in this response.
248
+ ${strictJsonOutputRules()}`,
249
+ };
250
+ }
136
251
  function buildChapterPrompt(input) {
137
252
  const ch = input.state.currentChapter;
138
253
  const isFirstChapter = ch <= 1;
@@ -142,7 +257,7 @@ function buildChapterPrompt(input) {
142
257
  prompt: `You are a professional long-form fiction writer. Write chapter ${ch} directly.
143
258
 
144
259
  ## Priority Order
145
- 1. Strictly follow the current chapter architecture, user additions, story bible hard constraints, and previous-chapter continuity.
260
+ 1. Strictly follow the current chapter architecture, user additions, story bible hard constraints, style guide, and previous-chapter continuity.
146
261
  2. Use relevant memory, prior text evidence, and active foreshadow threads.
147
262
  3. Treat full-book and volume plans as distant planning context only. Do not write concrete future events early.
148
263
 
@@ -157,6 +272,8 @@ ${isFirstChapter
157
272
  - The chapter must end on a clear hook: cliffhanger, mystery, emotional resonance, reveal, or volume close — per the chapter architecture endHookFocus. Default: cliffhanger.
158
273
 
159
274
  ## Style
275
+ - Enforce the Style Guide from context. Treat sampleParagraph as prose texture only; do not copy its content.
276
+ - Enforce Style Guide.proseRhythm: short sentences, one-line paragraphs, repeated sentences, and dashes are emphasis tools, not default narration. Ordinary narration should form natural sentence groups.
160
277
  - Match the novel's genre, world, character identities, and emotional tone.
161
278
  - Natural, stable, readable language; prioritize narrative progress, character work, and emotional accumulation.
162
279
  - Dialogue fits each character's identity, relationship, and situation.
@@ -250,7 +367,7 @@ ${strictJsonOutputRules()}`,
250
367
  };
251
368
  }
252
369
  function buildContinuityReviewPrompt(input) {
253
- const end = Math.max(input.state.targetChapters, input.state.currentChapter - 1);
370
+ const end = Math.max(input.state.plannedTotalChapters ?? input.state.targetChapters, input.state.currentChapter - 1);
254
371
  return {
255
372
  purpose: 'continuity_review',
256
373
  expectedFormat: 'JSON matching ContinuityReviewSchema',
@@ -299,6 +416,8 @@ ${input.context ? `## Review Context\n${input.context}\n` : ''}## Review Focus
299
416
  - Whether every requiredBeat is fulfilled; missing beats must appear in acceptance.requiredBeats.missingBeats.
300
417
  - Whether this chapter advances the main line, character state, or active foreshadow threads. If it is static, at least one of narrativeProgress/characterProgress/foreshadowProgress must fail.
301
418
  - Whether it violates the story bible, character state table, volume pacing board, or prior memory.
419
+ - Whether it violates the Style Guide: narrative voice, sentence density, genre diction, dialogue rules, or prohibited patterns.
420
+ - Whether it violates Style Guide.proseRhythm: excessive short-sentence density, consecutive one-sentence paragraphs, fake rhythm through line breaks, overly direct interior explanation, or repeated sentence patterns.
302
421
  - Whether the ending has a clear hook that matches the chapter architecture endHookFocus.
303
422
  - Whether it repeats prior chapter beats, conflict patterns, reveals, or dialogue functions.
304
423
  - Character voice, motivation, and state vs the story bible and prior memory.
@@ -334,6 +453,10 @@ Output valid JSON only, in this shape:
334
453
  "status": "pass | fail",
335
454
  "evidence": "Whether it matches the story bible, character state table, and world rules"
336
455
  },
456
+ "proseRhythm": {
457
+ "status": "pass | fail",
458
+ "evidence": "Whether it follows Style Guide.proseRhythm; explain whether short sentences, one-line paragraphs, repetition, and interior explanation are controlled"
459
+ },
337
460
  "endingHook": {
338
461
  "status": "pass | fail",
339
462
  "evidence": "The ending hook passage and its function"
@@ -428,8 +551,12 @@ function buildPromptForStep(input) {
428
551
  return buildMetadataPrompt(input);
429
552
  case 'story_bible':
430
553
  return buildStoryBiblePrompt(input);
554
+ case 'style_guide':
555
+ return buildStyleGuidePrompt(input);
431
556
  case 'architecture':
432
557
  return buildArchitecturePrompt(input);
558
+ case 'architecture_extension':
559
+ return buildArchitectureExtensionPrompt(input);
433
560
  case 'chapter':
434
561
  return buildChapterPrompt(input);
435
562
  case 'memory_card':
@@ -74,6 +74,55 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出结构
74
74
  - 不要输出 JSON。`,
75
75
  };
76
76
  }
77
+ function buildStyleGuidePrompt(input) {
78
+ return {
79
+ purpose: 'style_guide',
80
+ expectedFormat: 'JSON matching StyleGuideSchema',
81
+ prompt: `你是一名长篇小说文风主编。请根据用户提示、metadata 和故事圣经,生成可被每章写作与审稿长期执行的风格圣经。
82
+
83
+ ## 用户提示词
84
+ ${input.state.initialPrompt}
85
+
86
+ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
87
+ 请只输出合法 JSON,格式如下:
88
+ {
89
+ "narrativeVoice": "叙事人称、视角距离、旁白气质、情绪温度",
90
+ "pacing": "开章、转场、冲突推进、章末钩子的节奏规则",
91
+ "diction": "词汇选择、句式密度、题材术语使用边界",
92
+ "dialogueRules": [
93
+ "核心人物对白的长度、语气、潜台词、称谓规则"
94
+ ],
95
+ "prohibitedPatterns": [
96
+ "禁止出现的现代梗、解释型旁白、设定堆砌、口吻漂移等"
97
+ ],
98
+ "proseRhythm": {
99
+ "sentenceRhythm": "短句、中句、长句的使用原则;短句应服务转折、危险、情绪落点,而不是默认叙述单位",
100
+ "paragraphing": "段落应形成完整叙事单元;避免连续单句成段、靠频繁换行制造伪节奏",
101
+ "interiorityMode": "心理活动如何通过动作、迟疑、感官反应折射;避免频繁直接解释人物想法",
102
+ "emphasisBudget": "重复句、破折号、孤立短句等强调资源的使用预算",
103
+ "antiPatterns": [
104
+ "连续 3 个以上单句短段",
105
+ "用大量短句模拟紧张感",
106
+ "每个动作后立刻解释心理",
107
+ "重复同一句式制造伪节奏"
108
+ ]
109
+ },
110
+ "sampleParagraph": "一段 120-250 字的目标风格示例,不要写成剧情大纲",
111
+ "consistencyChecks": [
112
+ "后续章节审稿时用于判断风格是否跑偏的具体检查项"
113
+ ]
114
+ }
115
+
116
+ 要求:
117
+ - 风格必须匹配 genre、premise、人物身份和目标读者预期。
118
+ - 不要只写抽象形容词;每个字段都要能指导实际行文。
119
+ - proseRhythm 不要写成固定字数规则;要描述可审稿的节奏原则和反模式。
120
+ - sampleParagraph 只展示语言质感,不要提前泄露后续剧情。
121
+ - prohibitedPatterns 至少 3 条,consistencyChecks 至少 3 条。
122
+ - proseRhythm.antiPatterns 至少 4 条。
123
+ ${strictJsonOutputRules()}`,
124
+ };
125
+ }
77
126
  function buildArchitecturePrompt(input) {
78
127
  return {
79
128
  purpose: 'architecture',
@@ -84,7 +133,7 @@ function buildArchitecturePrompt(input) {
84
133
  ${input.state.initialPrompt}
85
134
 
86
135
  ## 目标
87
- - 本次至少生成 ${input.state.targetChapters} 个章架构。
136
+ - 全本目标约 ${input.state.plannedTotalChapters ?? input.state.targetChapters} 章;本次只生成首批 ${input.state.targetChapters} 个章架构。
88
137
  - 全本架构负责长期主线和结局方向。
89
138
  - 卷架构负责阶段冲突、高潮和卷尾钩子。
90
139
  - 章架构必须只覆盖本章应发生的内容,不要提前泄露后续具体事件。
@@ -126,6 +175,7 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
126
175
 
127
176
  要求:
128
177
  - chapters.length 必须大于等于 ${input.state.targetChapters}。
178
+ - chapters 不需要一次覆盖全本;后续写到边界时会进入 architecture_extension 续规划。
129
179
  - chapterNumber 从 1 开始连续递增。
130
180
  - volumeId 必须引用 volumes 中存在的 id。
131
181
  - volumePacing 必须为每个 volume 提供节奏板。
@@ -133,6 +183,71 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
133
183
  ${strictJsonOutputRules()}`,
134
184
  };
135
185
  }
186
+ function buildArchitectureExtensionPrompt(input) {
187
+ const start = input.state.currentChapter;
188
+ const total = input.state.plannedTotalChapters ?? input.state.targetChapters;
189
+ const end = Math.min(total, start + input.state.targetChapters - 1);
190
+ return {
191
+ purpose: 'architecture_extension',
192
+ expectedFormat: 'JSON matching ArchitectureExtensionPayloadSchema',
193
+ prompt: `你是一名长篇小说总架构师。当前已写到既有章纲边界,请基于已有内容续写后续章架构。
194
+
195
+ ## 续规划范围
196
+ - 从第 ${start} 章开始。
197
+ - 本批最多规划到第 ${end} 章。
198
+ - 全本目标到第 ${total} 章结束。
199
+
200
+ ## 续规划原则
201
+ - 不要改写已经存在的章节架构;只追加新的 chapter architecture。
202
+ - 新增章节必须承接最近记忆、角色状态表、活跃伏笔和卷级节奏板。
203
+ - 如果后续章节进入新卷,可以新增 volumes 和 volumePacing;如果仍在旧卷,可以补充/更新该卷节奏板。
204
+ - 如果全本方向因已写内容需要微调,可以输出 fullUpdate;fullUpdate 必须是可覆盖 architecture/full.md 的完整更新版,而不是变更说明。
205
+ - 章架构必须只覆盖本章应发生的内容,不要提前泄露更后面的具体事件。
206
+
207
+ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
208
+ 请只输出合法 JSON,格式如下:
209
+ {
210
+ "fullUpdate": "可选:完整更新后的全本架构 Markdown/文本",
211
+ "volumes": [
212
+ {
213
+ "id": "v2",
214
+ "title": "新增或更新卷标题",
215
+ "summary": "本卷目标、冲突、高潮和卷尾钩子",
216
+ "order": 2
217
+ }
218
+ ],
219
+ "volumePacing": [
220
+ {
221
+ "volumeId": "v2",
222
+ "start": "本卷起点",
223
+ "promise": "本卷承诺",
224
+ "keyTurns": ["关键转折1", "关键转折2"],
225
+ "midpoint": "中点转折",
226
+ "climax": "本卷高潮",
227
+ "payoffs": ["计划回收点"],
228
+ "lingeringMysteries": ["遗留悬念"]
229
+ }
230
+ ],
231
+ "chapters": [
232
+ {
233
+ "chapterNumber": ${start},
234
+ "title": "章标题",
235
+ "volumeId": "v1",
236
+ "summary": "本章剧情摘要",
237
+ "requiredBeats": ["必须完成的情节点1"]
238
+ }
239
+ ]
240
+ }
241
+
242
+ 要求:
243
+ - chapters[0].chapterNumber 必须等于 ${start}。
244
+ - chapterNumber 必须连续递增,且不能超过 ${total}。
245
+ - chapters.length 建议为 ${end - start + 1},除非已经到全本结尾。
246
+ - requiredBeats 至少 1 条,且必须具体可执行。
247
+ - volumeId 必须引用已有或本次新增 volumes 中存在的 id。
248
+ ${strictJsonOutputRules()}`,
249
+ };
250
+ }
136
251
  function buildChapterPrompt(input) {
137
252
  const ch = input.state.currentChapter;
138
253
  const isFirstChapter = ch <= 1;
@@ -142,7 +257,7 @@ function buildChapterPrompt(input) {
142
257
  prompt: `你是一位擅长创作长篇网络小说的职业作者。请直接完成第 ${ch} 章正文。
143
258
 
144
259
  ## 执行优先级
145
- 1. 先严格遵守"本章架构、用户补充要求、故事圣经硬约束、上一章承接"。
260
+ 1. 先严格遵守"本章架构、用户补充要求、故事圣经硬约束、风格圣经、上一章承接"。
146
261
  2. 再参考"历史相关记忆、历史原文证据、活跃伏笔"保证一致性。
147
262
  3. 最后才参考"全本/本卷远场规划",且不得提前写出尚未发生的情节。
148
263
 
@@ -157,6 +272,8 @@ ${isFirstChapter
157
272
  - 章末必须有清晰的"钩子":可以是悬念、反转、剧情承诺、情绪余韵或卷末高潮——按本章架构 endHookFocus 字段决定。如果未指定,默认用悬念。
158
273
 
159
274
  ## 风格
275
+ - 严格执行上下文中的 Style Guide;sampleParagraph 只作为语言质感参考,不要复写其内容。
276
+ - 严格执行 Style Guide.proseRhythm:短句、单句段、重复句、破折号是强调资源,不是默认叙述单位;常规叙述要形成自然句群。
160
277
  - 文风必须与本书题材、世界观、人物身份、情感基调一致。
161
278
  - 语言自然、稳定、可读,优先服务叙事推进、人物塑造和情绪积累。
162
279
  - 对话符合人物身份、关系和处境;重要情绪通过动作、神态、节奏、潜台词体现。
@@ -249,7 +366,7 @@ ${strictJsonOutputRules()}`,
249
366
  };
250
367
  }
251
368
  function buildContinuityReviewPrompt(input) {
252
- const end = Math.max(input.state.targetChapters, input.state.currentChapter - 1);
369
+ const end = Math.max(input.state.plannedTotalChapters ?? input.state.targetChapters, input.state.currentChapter - 1);
253
370
  return {
254
371
  purpose: 'continuity_review',
255
372
  expectedFormat: 'JSON matching ContinuityReviewSchema',
@@ -298,6 +415,8 @@ ${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
298
415
  - requiredBeats 是否全部完成;缺失项必须写入 acceptance.requiredBeats.missingBeats。
299
416
  - 本章是否推进主线、人物状态或活跃伏笔;如果完全原地踏步,narrativeProgress/characterProgress/foreshadowProgress 至少一项必须 fail。
300
417
  - 是否违反故事圣经、角色状态表、卷级节奏板或历史记忆。
418
+ - 是否违反 Style Guide:叙事声音、句式密度、题材词汇、对白规则和禁用模式。
419
+ - 是否违反 Style Guide.proseRhythm:短句密度过高、连续单句短段、靠换行制造伪节奏、心理解释过直白、重复同一句式。
301
420
  - 章末是否有清晰钩子,且符合本章 endHookFocus。
302
421
  - 是否重复之前章节已经完成的桥段、冲突结构、信息揭示或对话功能。
303
422
  - 人物声音、动机、状态是否符合故事圣经与历史记忆。
@@ -333,6 +452,10 @@ ${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
333
452
  "status": "pass | fail",
334
453
  "evidence": "是否符合故事圣经、角色状态表和世界规则"
335
454
  },
455
+ "proseRhythm": {
456
+ "status": "pass | fail",
457
+ "evidence": "是否符合 Style Guide.proseRhythm;说明短句/单句段/重复句/心理解释是否被合理控制"
458
+ },
336
459
  "endingHook": {
337
460
  "status": "pass | fail",
338
461
  "evidence": "章末钩子的具体段落和作用"
@@ -427,8 +550,12 @@ function buildPromptForStep(input) {
427
550
  return buildMetadataPrompt(input);
428
551
  case 'story_bible':
429
552
  return buildStoryBiblePrompt(input);
553
+ case 'style_guide':
554
+ return buildStyleGuidePrompt(input);
430
555
  case 'architecture':
431
556
  return buildArchitecturePrompt(input);
557
+ case 'architecture_extension':
558
+ return buildArchitectureExtensionPrompt(input);
432
559
  case 'chapter':
433
560
  return buildChapterPrompt(input);
434
561
  case 'memory_card':