novelforge-agent 0.1.1 → 0.3.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.
- package/README.md +45 -18
- package/dist/src/cli/index.js +81 -4
- package/dist/src/core/bibleStore.js +36 -0
- package/dist/src/core/characterStore.js +74 -0
- package/dist/src/core/contextBuilder.js +76 -1
- package/dist/src/core/fileNames.js +4 -0
- package/dist/src/core/index.js +4 -0
- package/dist/src/core/projectDiscovery.js +1 -0
- package/dist/src/core/projectOps.js +193 -0
- package/dist/src/core/projectStore.js +15 -1
- package/dist/src/core/prompts/en-US.js +247 -16
- package/dist/src/core/prompts/zh-CN.js +246 -15
- package/dist/src/core/retrieval/index.js +8 -0
- package/dist/src/core/schemas.js +121 -1
- package/dist/src/core/steps/architecture.js +7 -1
- package/dist/src/core/steps/architectureExtension.js +72 -0
- package/dist/src/core/steps/chapter.js +11 -1
- package/dist/src/core/steps/chapterReview.js +26 -1
- package/dist/src/core/steps/chapterRevision.js +17 -0
- package/dist/src/core/steps/index.js +4 -0
- package/dist/src/core/steps/memoryCard.js +26 -1
- package/dist/src/core/steps/novelMetadata.js +4 -2
- package/dist/src/core/steps/storyBible.js +1 -1
- package/dist/src/core/steps/styleGuide.js +12 -0
- package/dist/src/core/threadStore.js +150 -0
- package/dist/src/core/workflow.js +5 -3
- package/dist/src/mcp/tools.js +228 -20
- package/package.json +5 -1
- package/src/cli/index.ts +84 -3
- package/src/core/bibleStore.ts +57 -0
- package/src/core/characterStore.ts +93 -0
- package/src/core/contextBuilder.ts +74 -4
- package/src/core/fileNames.ts +5 -0
- package/src/core/index.ts +4 -0
- package/src/core/projectDiscovery.ts +2 -0
- package/src/core/projectOps.ts +251 -0
- package/src/core/projectStore.ts +19 -1
- package/src/core/prompts/en-US.ts +258 -25
- package/src/core/prompts/types.ts +4 -1
- package/src/core/prompts/zh-CN.ts +250 -17
- package/src/core/retrieval/index.ts +10 -0
- package/src/core/schemas.ts +133 -1
- package/src/core/steps/architecture.ts +7 -1
- package/src/core/steps/architectureExtension.ts +88 -0
- package/src/core/steps/chapter.ts +11 -1
- package/src/core/steps/chapterReview.ts +28 -1
- package/src/core/steps/chapterRevision.ts +18 -0
- package/src/core/steps/index.ts +4 -0
- package/src/core/steps/memoryCard.ts +27 -1
- package/src/core/steps/novelMetadata.ts +4 -2
- package/src/core/steps/storyBible.ts +1 -1
- package/src/core/steps/styleGuide.ts +13 -0
- package/src/core/threadStore.ts +173 -0
- package/src/core/types.ts +134 -1
- package/src/core/workflow.ts +5 -3
- package/src/mcp/tools.ts +351 -21
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { cp, readdir, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { loadState, saveState } from './projectStore.js';
|
|
5
|
+
import { removeChapterFromIndex, removeMemoryCardFromIndex } from './retrieval/index.js';
|
|
6
|
+
import { chapterFileName, memoryFileName } from './fileNames.js';
|
|
7
|
+
export async function forkProject(input) {
|
|
8
|
+
const source = resolve(input.sourceProjectPath);
|
|
9
|
+
const state = await loadState(source);
|
|
10
|
+
const suffix = randomBytes(3).toString('hex');
|
|
11
|
+
const label = (input.label ?? 'fork').replace(/[^a-z0-9-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'fork';
|
|
12
|
+
const targetName = `${basename(source)}-${label}-${suffix}`;
|
|
13
|
+
const target = join(dirname(source), targetName);
|
|
14
|
+
await cp(source, target, { recursive: true });
|
|
15
|
+
const forkedState = {
|
|
16
|
+
...state,
|
|
17
|
+
projectId: randomUUID(),
|
|
18
|
+
projectPath: target,
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
updatedAt: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
await saveState(forkedState);
|
|
23
|
+
return { newProjectPath: target, newProjectId: forkedState.projectId };
|
|
24
|
+
}
|
|
25
|
+
async function tryUnlink(path) {
|
|
26
|
+
try {
|
|
27
|
+
await unlink(path);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function tryRmDirEntry(dirPath, prefix) {
|
|
35
|
+
const removed = [];
|
|
36
|
+
try {
|
|
37
|
+
const items = await readdir(dirPath);
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
if (item.startsWith(prefix)) {
|
|
40
|
+
const full = join(dirPath, item);
|
|
41
|
+
try {
|
|
42
|
+
await unlink(full);
|
|
43
|
+
removed.push(full);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// dir absent
|
|
53
|
+
}
|
|
54
|
+
return removed;
|
|
55
|
+
}
|
|
56
|
+
export async function deleteChapter(input) {
|
|
57
|
+
const state = await loadState(input.projectPath);
|
|
58
|
+
const n = input.chapterNumber;
|
|
59
|
+
if (n < 1)
|
|
60
|
+
throw new Error('chapterNumber must be >= 1');
|
|
61
|
+
const removed = [];
|
|
62
|
+
const chapterRel = join('chapters', chapterFileName(n));
|
|
63
|
+
if (await tryUnlink(join(state.projectPath, chapterRel)))
|
|
64
|
+
removed.push(chapterRel);
|
|
65
|
+
const memoryRel = join('memory', memoryFileName(n));
|
|
66
|
+
if (await tryUnlink(join(state.projectPath, memoryRel)))
|
|
67
|
+
removed.push(memoryRel);
|
|
68
|
+
// Versions of this chapter
|
|
69
|
+
const versionsRemoved = await tryRmDirEntry(join(state.projectPath, 'chapters/.versions'), `${chapterFileName(n).replace(/\.md$/, '')}.`);
|
|
70
|
+
removed.push(...versionsRemoved);
|
|
71
|
+
// Per-chapter review
|
|
72
|
+
const reviewName = `chapter-${String(n).padStart(3, '0')}.json`;
|
|
73
|
+
if (await tryUnlink(join(state.projectPath, 'reviews/chapter', reviewName))) {
|
|
74
|
+
removed.push(`reviews/chapter/${reviewName}`);
|
|
75
|
+
}
|
|
76
|
+
// Update state.files
|
|
77
|
+
const nextFiles = { ...state.files };
|
|
78
|
+
delete nextFiles[`chapter-${n}`];
|
|
79
|
+
delete nextFiles[`memory-${n}`];
|
|
80
|
+
delete nextFiles[`review-chapter-${n}`];
|
|
81
|
+
// Remove this chapter and its memory card from the lexical index
|
|
82
|
+
await removeChapterFromIndex(state.projectPath, n);
|
|
83
|
+
await removeMemoryCardFromIndex(state.projectPath, n);
|
|
84
|
+
// Adjust state.currentChapter & currentStep if needed
|
|
85
|
+
let newCurrentChapter = state.currentChapter;
|
|
86
|
+
let newCurrentStep = state.currentStep;
|
|
87
|
+
if (state.currentChapter > n) {
|
|
88
|
+
// user deleted an earlier chapter; current pointer becomes the deleted one to be regenerated
|
|
89
|
+
newCurrentChapter = n;
|
|
90
|
+
newCurrentStep = 'chapter';
|
|
91
|
+
}
|
|
92
|
+
else if (state.currentChapter === n + 1 && (state.currentStep === 'chapter' || state.currentStep === 'memory_card')) {
|
|
93
|
+
// we just finished chapter n and were about to do n+1; step back
|
|
94
|
+
newCurrentChapter = n;
|
|
95
|
+
newCurrentStep = 'chapter';
|
|
96
|
+
}
|
|
97
|
+
const nextState = {
|
|
98
|
+
...state,
|
|
99
|
+
files: nextFiles,
|
|
100
|
+
currentChapter: newCurrentChapter,
|
|
101
|
+
currentStep: newCurrentStep,
|
|
102
|
+
pendingAction: undefined,
|
|
103
|
+
};
|
|
104
|
+
await saveState(nextState);
|
|
105
|
+
return { removed, newCurrentChapter, newCurrentStep };
|
|
106
|
+
}
|
|
107
|
+
const STEP_FILE_KEYS = {
|
|
108
|
+
novel_metadata: ['novel'],
|
|
109
|
+
story_bible: ['storyBible'],
|
|
110
|
+
style_guide: ['styleGuide'],
|
|
111
|
+
architecture: ['architecture'],
|
|
112
|
+
continuity_review: ['continuityReview'],
|
|
113
|
+
};
|
|
114
|
+
const STEP_FILE_PATHS = {
|
|
115
|
+
novel_metadata: ['novel.json'],
|
|
116
|
+
story_bible: ['story-bible.md'],
|
|
117
|
+
style_guide: ['style-guide.json'],
|
|
118
|
+
architecture: ['architecture/full.md', 'architecture/volumes.json', 'architecture/chapters.json'],
|
|
119
|
+
};
|
|
120
|
+
export async function redoStep(input) {
|
|
121
|
+
const state = await loadState(input.projectPath);
|
|
122
|
+
const removed = [];
|
|
123
|
+
if (input.step === 'chapter' || input.step === 'memory_card') {
|
|
124
|
+
const chapter = input.chapterNumber ?? state.currentChapter;
|
|
125
|
+
if (input.step === 'memory_card') {
|
|
126
|
+
const rel = join('memory', memoryFileName(chapter));
|
|
127
|
+
if (await tryUnlink(join(state.projectPath, rel)))
|
|
128
|
+
removed.push(rel);
|
|
129
|
+
delete state.files[`memory-${chapter}`];
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// chapter: also remove its memory + per-chapter review since they depend on it
|
|
133
|
+
const cRel = join('chapters', chapterFileName(chapter));
|
|
134
|
+
if (await tryUnlink(join(state.projectPath, cRel)))
|
|
135
|
+
removed.push(cRel);
|
|
136
|
+
const mRel = join('memory', memoryFileName(chapter));
|
|
137
|
+
if (await tryUnlink(join(state.projectPath, mRel)))
|
|
138
|
+
removed.push(mRel);
|
|
139
|
+
delete state.files[`chapter-${chapter}`];
|
|
140
|
+
delete state.files[`memory-${chapter}`];
|
|
141
|
+
await removeChapterFromIndex(state.projectPath, chapter);
|
|
142
|
+
await removeMemoryCardFromIndex(state.projectPath, chapter);
|
|
143
|
+
}
|
|
144
|
+
state.currentChapter = chapter;
|
|
145
|
+
state.currentStep = input.step;
|
|
146
|
+
state.pendingAction = undefined;
|
|
147
|
+
}
|
|
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') {
|
|
153
|
+
const paths = STEP_FILE_PATHS[input.step] ?? [];
|
|
154
|
+
for (const p of paths) {
|
|
155
|
+
if (await tryUnlink(join(state.projectPath, p)))
|
|
156
|
+
removed.push(p);
|
|
157
|
+
}
|
|
158
|
+
const keys = STEP_FILE_KEYS[input.step] ?? [];
|
|
159
|
+
for (const k of keys) {
|
|
160
|
+
delete state.files[k];
|
|
161
|
+
}
|
|
162
|
+
state.currentStep = input.step;
|
|
163
|
+
state.pendingAction = undefined;
|
|
164
|
+
if (input.step === 'novel_metadata')
|
|
165
|
+
state.currentChapter = 1;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
throw new Error(`redo_step does not support step: ${input.step}`);
|
|
169
|
+
}
|
|
170
|
+
// Trim completedSteps after the redo target
|
|
171
|
+
const idx = state.completedSteps.lastIndexOf(input.step);
|
|
172
|
+
if (idx >= 0)
|
|
173
|
+
state.completedSteps = state.completedSteps.slice(0, idx);
|
|
174
|
+
await saveState(state);
|
|
175
|
+
return {
|
|
176
|
+
removed,
|
|
177
|
+
currentStep: state.currentStep,
|
|
178
|
+
currentChapter: state.currentChapter,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// guards
|
|
183
|
+
// =============================================================================
|
|
184
|
+
export function assertProjectPath(workspaceRoot, projectPath) {
|
|
185
|
+
const root = resolve(workspaceRoot);
|
|
186
|
+
const target = resolve(projectPath);
|
|
187
|
+
const rel = relative(root, target);
|
|
188
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
189
|
+
throw new Error(`Refusing to operate outside workspace: ${target}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// keep tsc happy if no other refs
|
|
193
|
+
void writeFile;
|
|
@@ -15,6 +15,7 @@ export async function ensureProjectDirectories(projectPath) {
|
|
|
15
15
|
await mkdir(join(projectPath, 'architecture'), { recursive: true });
|
|
16
16
|
await mkdir(join(projectPath, 'chapters'), { recursive: true });
|
|
17
17
|
await mkdir(join(projectPath, 'chapters/.versions'), { recursive: true });
|
|
18
|
+
await mkdir(join(projectPath, 'story-bible-versions'), { recursive: true });
|
|
18
19
|
await mkdir(join(projectPath, 'memory'), { recursive: true });
|
|
19
20
|
await mkdir(join(projectPath, 'reviews'), { recursive: true });
|
|
20
21
|
await mkdir(join(projectPath, 'reviews/chapter'), { recursive: true });
|
|
@@ -31,10 +32,22 @@ export async function archiveChapterVersion(projectPath, chapterRelative, versio
|
|
|
31
32
|
return undefined;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
export async function archiveStoryBible(projectPath, versionRelative) {
|
|
36
|
+
const sourcePath = join(projectPath, 'story-bible.md');
|
|
37
|
+
try {
|
|
38
|
+
const existing = await readFile(sourcePath, 'utf8');
|
|
39
|
+
return saveMarkdownFile(projectPath, versionRelative, existing);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
34
45
|
export async function createProject(input) {
|
|
35
46
|
const workspaceRoot = resolve(input.workspaceRoot);
|
|
36
47
|
const baseDir = input.outputDir || 'novels';
|
|
37
|
-
const
|
|
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))));
|
|
38
51
|
const baseSlug = makeProjectSlug(input.prompt.slice(0, 48));
|
|
39
52
|
const suffix = randomBytes(3).toString('hex');
|
|
40
53
|
const slug = `${baseSlug}-${suffix}`;
|
|
@@ -48,6 +61,7 @@ export async function createProject(input) {
|
|
|
48
61
|
initialPrompt: input.prompt,
|
|
49
62
|
language: input.language || 'zh-CN',
|
|
50
63
|
targetChapters,
|
|
64
|
+
plannedTotalChapters,
|
|
51
65
|
currentStep: 'novel_metadata',
|
|
52
66
|
currentChapter: 1,
|
|
53
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
|
-
-
|
|
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.
|
|
@@ -101,6 +150,18 @@ Output valid JSON only, in this shape:
|
|
|
101
150
|
"order": 1
|
|
102
151
|
}
|
|
103
152
|
],
|
|
153
|
+
"volumePacing": [
|
|
154
|
+
{
|
|
155
|
+
"volumeId": "v1",
|
|
156
|
+
"start": "Volume starting state: protagonist/world/conflict",
|
|
157
|
+
"promise": "Core reader promise or question for this volume",
|
|
158
|
+
"keyTurns": ["Key turn 1", "Key turn 2"],
|
|
159
|
+
"midpoint": "Midpoint turn or changed understanding",
|
|
160
|
+
"climax": "Volume climax",
|
|
161
|
+
"payoffs": ["Threads or promises this volume plans to pay off"],
|
|
162
|
+
"lingeringMysteries": ["Mysteries intentionally left open at volume end"]
|
|
163
|
+
}
|
|
164
|
+
],
|
|
104
165
|
"chapters": [
|
|
105
166
|
{
|
|
106
167
|
"chapterNumber": 1,
|
|
@@ -114,38 +175,120 @@ Output valid JSON only, in this shape:
|
|
|
114
175
|
|
|
115
176
|
Rules:
|
|
116
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.
|
|
117
179
|
- chapterNumber must start at 1 and increase contiguously.
|
|
118
180
|
- volumeId must reference an id from volumes.
|
|
181
|
+
- volumePacing must provide one pacing board for every volume.
|
|
119
182
|
- requiredBeats must include at least one concrete, actionable beat.
|
|
120
183
|
${strictJsonOutputRules()}`,
|
|
121
184
|
};
|
|
122
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
|
+
}
|
|
123
251
|
function buildChapterPrompt(input) {
|
|
252
|
+
const ch = input.state.currentChapter;
|
|
253
|
+
const isFirstChapter = ch <= 1;
|
|
124
254
|
return {
|
|
125
255
|
purpose: 'chapter',
|
|
126
256
|
expectedFormat: 'Markdown',
|
|
127
|
-
prompt: `You are a professional long-form fiction writer. Write chapter ${
|
|
257
|
+
prompt: `You are a professional long-form fiction writer. Write chapter ${ch} directly.
|
|
128
258
|
|
|
129
259
|
## Priority Order
|
|
130
|
-
1. Strictly follow the current chapter architecture, user additions, story bible hard constraints, and previous-chapter continuity.
|
|
131
|
-
2. Use relevant memory
|
|
260
|
+
1. Strictly follow the current chapter architecture, user additions, story bible hard constraints, style guide, and previous-chapter continuity.
|
|
261
|
+
2. Use relevant memory, prior text evidence, and active foreshadow threads.
|
|
132
262
|
3. Treat full-book and volume plans as distant planning context only. Do not write concrete future events early.
|
|
133
263
|
|
|
134
|
-
##
|
|
264
|
+
## Length Target
|
|
265
|
+
- Default target: ~2500 words (±20%). If the chapter architecture specifies targetWords, follow it.
|
|
266
|
+
- Do not pad to hit the target; do not under-write to be brief at the cost of conflict.
|
|
267
|
+
|
|
268
|
+
## Structure
|
|
269
|
+
${isFirstChapter
|
|
270
|
+
? '- This is chapter 1. No recap needed. Open with character and situation directly.'
|
|
271
|
+
: '- Start with a 2-3 sentence recap or bridge so a reader who skipped the last chapter can re-enter (unless the chapter architecture has requireRecap=false). Make it natural, not meta-narration like "previously...".'}
|
|
272
|
+
- The chapter must end on a clear hook: cliffhanger, mystery, emotional resonance, reveal, or volume close — per the chapter architecture endHookFocus. Default: cliffhanger.
|
|
273
|
+
|
|
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.
|
|
135
277
|
- Match the novel's genre, world, character identities, and emotional tone.
|
|
136
|
-
-
|
|
137
|
-
- Dialogue
|
|
138
|
-
- Important emotion
|
|
139
|
-
- Scene description
|
|
140
|
-
-
|
|
278
|
+
- Natural, stable, readable language; prioritize narrative progress, character work, and emotional accumulation.
|
|
279
|
+
- Dialogue fits each character's identity, relationship, and situation.
|
|
280
|
+
- Important emotion comes through action, body language, pacing, and subtext.
|
|
281
|
+
- Scene description has useful sensory detail without stalling.
|
|
282
|
+
- POV: strictly follow the chapter architecture povCharacter (if set). No mid-chapter POV switch.
|
|
141
283
|
|
|
142
284
|
## Execution Rules
|
|
143
285
|
- Write only what the current chapter architecture authorizes.
|
|
144
|
-
- Do not introduce unauthorized major characters. Functional background characters
|
|
145
|
-
- Keep names, items, places, abilities,
|
|
146
|
-
- If the previous chapter ends mid-action
|
|
286
|
+
- Do not introduce unauthorized major characters. Functional background characters stay light.
|
|
287
|
+
- Keep names, items, places, abilities, timelines, injuries, relationships, and knowledge boundaries consistent.
|
|
288
|
+
- If the previous chapter ends mid-action or mid-scene, this chapter must continue from that point.
|
|
289
|
+
- Active foreshadow threads may be advanced or paid off this chapter, but **never silently dropped** — even if you choose not to touch them, leave them coherent.
|
|
147
290
|
- Avoid cost-free power jumps, forced stupidity, mechanical twists, info-dumps, and empty lyricism.
|
|
148
|
-
- Do not output summaries, bullet points, lectures, or
|
|
291
|
+
- Do not output summaries, bullet points, lectures, explanatory prefaces, or meta-text like "what I changed".
|
|
149
292
|
|
|
150
293
|
${input.context ? `## Generation Context\n${input.context}\n` : ''}## Output Requirements
|
|
151
294
|
- Output Markdown.
|
|
@@ -185,18 +328,46 @@ Output valid JSON only, in this shape:
|
|
|
185
328
|
"after": "After"
|
|
186
329
|
}
|
|
187
330
|
],
|
|
188
|
-
"openThreads": ["Unresolved promise, danger, question, or plot thread"]
|
|
331
|
+
"openThreads": ["Unresolved promise, danger, question, or plot thread"],
|
|
332
|
+
"wordCount": <approximate word count of this chapter as an integer>,
|
|
333
|
+
"threadActions": [
|
|
334
|
+
{
|
|
335
|
+
"kind": "plant | build | pay | drop",
|
|
336
|
+
"threadId": "id of an existing active thread (required for build/pay/drop; leave empty for plant — the system will assign one)",
|
|
337
|
+
"description": "for plant: what the new thread is; for others: one sentence on how this chapter advanced/paid/dropped that thread"
|
|
338
|
+
}
|
|
339
|
+
],
|
|
340
|
+
"characterUpdates": [
|
|
341
|
+
{
|
|
342
|
+
"name": "Character name",
|
|
343
|
+
"role": "Role if confirmed or changed this chapter",
|
|
344
|
+
"goal": "Current goal at chapter end",
|
|
345
|
+
"belief": "Core belief or understanding driving them at chapter end",
|
|
346
|
+
"relationships": [
|
|
347
|
+
{ "name": "Related character", "dynamic": "Relationship state at chapter end" }
|
|
348
|
+
],
|
|
349
|
+
"abilities": ["Abilities, resources, or limits confirmed at chapter end"],
|
|
350
|
+
"secrets": ["Secrets still hidden or only partially known at chapter end"],
|
|
351
|
+
"emotionalState": "Emotional state at chapter end"
|
|
352
|
+
}
|
|
353
|
+
]
|
|
189
354
|
}
|
|
190
355
|
|
|
191
356
|
Rules:
|
|
192
357
|
- Record only information that happened or was confirmed in this chapter.
|
|
193
358
|
- Do not speculate about future plot.
|
|
194
359
|
- Make facts and stateChanges concrete enough for later chapter reference.
|
|
360
|
+
- wordCount: approximate word count (English) or character count (CJK). An integer estimate is fine.
|
|
361
|
+
- threadActions is critical:
|
|
362
|
+
· For any active foreshadow thread in the context's "Active Foreshadow Threads" section, if this chapter advanced it emit kind="build"; if this chapter paid it off emit kind="pay"; if this chapter abandoned it emit kind="drop". threadId is required.
|
|
363
|
+
· For any new thread this chapter plants, emit kind="plant" with a clear description.
|
|
364
|
+
· If an active thread was not touched, no action needed — but never silently delete it. Without a drop action, the thread stays active.
|
|
365
|
+
- characterUpdates maintains a separate character state table. Emit only important characters whose state changed or was reconfirmed in this chapter; goal, belief, relationships, abilities, secrets, and emotionalState must reflect the chapter ending.
|
|
195
366
|
${strictJsonOutputRules()}`,
|
|
196
367
|
};
|
|
197
368
|
}
|
|
198
369
|
function buildContinuityReviewPrompt(input) {
|
|
199
|
-
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);
|
|
200
371
|
return {
|
|
201
372
|
purpose: 'continuity_review',
|
|
202
373
|
expectedFormat: 'JSON matching ContinuityReviewSchema',
|
|
@@ -241,6 +412,14 @@ function buildChapterReviewPrompt(input) {
|
|
|
241
412
|
prompt: `You are a strict editor reviewing a single chapter of a serial novel for in-chapter problems and conflicts with established context.
|
|
242
413
|
|
|
243
414
|
${input.context ? `## Review Context\n${input.context}\n` : ''}## Review Focus
|
|
415
|
+
- This is a mandatory chapter acceptance gate. If any acceptance item fails, status must be "issues_found" and the workflow must revise before continuing.
|
|
416
|
+
- Whether every requiredBeat is fulfilled; missing beats must appear in acceptance.requiredBeats.missingBeats.
|
|
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.
|
|
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.
|
|
421
|
+
- Whether the ending has a clear hook that matches the chapter architecture endHookFocus.
|
|
422
|
+
- Whether it repeats prior chapter beats, conflict patterns, reveals, or dialogue functions.
|
|
244
423
|
- Character voice, motivation, and state vs the story bible and prior memory.
|
|
245
424
|
- World rules, item ownership, and ability limits.
|
|
246
425
|
- Timeline, location, and continuity with the previous chapter ending.
|
|
@@ -252,6 +431,41 @@ Output valid JSON only, in this shape:
|
|
|
252
431
|
{
|
|
253
432
|
"chapterNumber": ${chapter},
|
|
254
433
|
"status": "clean",
|
|
434
|
+
"acceptance": {
|
|
435
|
+
"requiredBeats": {
|
|
436
|
+
"status": "pass | fail",
|
|
437
|
+
"evidence": "Evidence for each requiredBeat",
|
|
438
|
+
"missingBeats": []
|
|
439
|
+
},
|
|
440
|
+
"narrativeProgress": {
|
|
441
|
+
"status": "pass | fail",
|
|
442
|
+
"evidence": "How this chapter advances the main line or phase objective"
|
|
443
|
+
},
|
|
444
|
+
"characterProgress": {
|
|
445
|
+
"status": "pass | fail",
|
|
446
|
+
"evidence": "How this chapter changes or confirms key character goal, belief, relationship, ability, secret, or emotion"
|
|
447
|
+
},
|
|
448
|
+
"foreshadowProgress": {
|
|
449
|
+
"status": "pass | fail",
|
|
450
|
+
"evidence": "How this chapter plants, advances, pays, or deliberately preserves foreshadow threads"
|
|
451
|
+
},
|
|
452
|
+
"storyBibleConsistency": {
|
|
453
|
+
"status": "pass | fail",
|
|
454
|
+
"evidence": "Whether it matches the story bible, character state table, and world rules"
|
|
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
|
+
},
|
|
460
|
+
"endingHook": {
|
|
461
|
+
"status": "pass | fail",
|
|
462
|
+
"evidence": "The ending hook passage and its function"
|
|
463
|
+
},
|
|
464
|
+
"repetition": {
|
|
465
|
+
"status": "pass | fail",
|
|
466
|
+
"evidence": "Whether it repeats prior beats; if not, explain why"
|
|
467
|
+
}
|
|
468
|
+
},
|
|
255
469
|
"issues": [
|
|
256
470
|
{
|
|
257
471
|
"severity": "low | medium | high",
|
|
@@ -266,6 +480,8 @@ Output valid JSON only, in this shape:
|
|
|
266
480
|
Rules:
|
|
267
481
|
- If there are no issues, use status "clean" with an empty issues array.
|
|
268
482
|
- Otherwise use status "issues_found".
|
|
483
|
+
- status may be "clean" only when every acceptance item is "pass".
|
|
484
|
+
- If any acceptance item is "fail", include a matching issue with a concrete fix.
|
|
269
485
|
- evidence must be specific; do not write "possibly" or "maybe".
|
|
270
486
|
${strictJsonOutputRules()}`,
|
|
271
487
|
};
|
|
@@ -335,8 +551,12 @@ function buildPromptForStep(input) {
|
|
|
335
551
|
return buildMetadataPrompt(input);
|
|
336
552
|
case 'story_bible':
|
|
337
553
|
return buildStoryBiblePrompt(input);
|
|
554
|
+
case 'style_guide':
|
|
555
|
+
return buildStyleGuidePrompt(input);
|
|
338
556
|
case 'architecture':
|
|
339
557
|
return buildArchitecturePrompt(input);
|
|
558
|
+
case 'architecture_extension':
|
|
559
|
+
return buildArchitectureExtensionPrompt(input);
|
|
340
560
|
case 'chapter':
|
|
341
561
|
return buildChapterPrompt(input);
|
|
342
562
|
case 'memory_card':
|
|
@@ -349,6 +569,17 @@ function buildPromptForStep(input) {
|
|
|
349
569
|
return buildChapterRevisionPrompt(input);
|
|
350
570
|
case 'cross_chapter_review':
|
|
351
571
|
return buildCrossChapterReviewPrompt(input);
|
|
572
|
+
case 'story_bible_amend':
|
|
573
|
+
return {
|
|
574
|
+
purpose: 'story_bible',
|
|
575
|
+
expectedFormat: 'Markdown',
|
|
576
|
+
prompt: `Based on the current story bible and the amendment context, output the FULL revised story bible Markdown.
|
|
577
|
+
|
|
578
|
+
${input.context ? `## Amendment Context\n${input.context}\n` : ''}## Output Requirements
|
|
579
|
+
- Output the entire story-bible.md content — it replaces the old one (old version auto-archived under story-bible-versions/).
|
|
580
|
+
- Preserve everything that still holds; modify / add / remove only what the amendment context justifies.
|
|
581
|
+
- Do not output diff markers, change logs, or bullet summaries — just the new full bible.`,
|
|
582
|
+
};
|
|
352
583
|
case 'complete':
|
|
353
584
|
return {
|
|
354
585
|
purpose: 'continuity_review',
|