novelforge-agent 0.2.0 → 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 +16 -12
- package/dist/src/cli/index.js +10 -2
- package/dist/src/core/contextBuilder.js +32 -0
- package/dist/src/core/projectDiscovery.js +1 -0
- package/dist/src/core/projectOps.js +7 -1
- package/dist/src/core/projectStore.js +4 -1
- package/dist/src/core/prompts/en-US.js +130 -3
- package/dist/src/core/prompts/zh-CN.js +130 -3
- package/dist/src/core/schemas.js +23 -0
- package/dist/src/core/steps/architectureExtension.js +72 -0
- package/dist/src/core/steps/chapterReview.js +2 -1
- package/dist/src/core/steps/index.js +4 -0
- package/dist/src/core/steps/memoryCard.js +22 -1
- package/dist/src/core/steps/storyBible.js +1 -1
- package/dist/src/core/steps/styleGuide.js +12 -0
- package/dist/src/core/workflow.js +2 -0
- package/dist/src/mcp/tools.js +36 -8
- package/package.json +1 -1
- package/src/cli/index.ts +11 -3
- package/src/core/contextBuilder.ts +30 -0
- package/src/core/projectDiscovery.ts +2 -0
- package/src/core/projectOps.ts +9 -1
- package/src/core/projectStore.ts +8 -1
- package/src/core/prompts/en-US.ts +132 -3
- package/src/core/prompts/types.ts +2 -0
- package/src/core/prompts/zh-CN.ts +132 -3
- package/src/core/schemas.ts +25 -0
- package/src/core/steps/architectureExtension.ts +88 -0
- package/src/core/steps/chapterReview.ts +2 -1
- package/src/core/steps/index.ts +4 -0
- package/src/core/steps/memoryCard.ts +23 -1
- package/src/core/steps/storyBible.ts +1 -1
- package/src/core/steps/styleGuide.ts +13 -0
- package/src/core/types.ts +32 -0
- package/src/core/workflow.ts +2 -0
- package/src/mcp/tools.ts +35 -8
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ArchitectureExtensionPayloadSchema } from '../schemas.js';
|
|
4
|
+
import { saveJsonFile, saveMarkdownFile } from '../projectStore.js';
|
|
5
|
+
import { parseJson } from './types.js';
|
|
6
|
+
async function readJsonArray(projectPath, relativePath) {
|
|
7
|
+
try {
|
|
8
|
+
const raw = await readFile(join(projectPath, relativePath), 'utf8');
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function assertContiguousExtension(state, existing, nextChapters) {
|
|
17
|
+
const expectedStart = state.currentChapter;
|
|
18
|
+
const total = state.plannedTotalChapters ?? state.targetChapters;
|
|
19
|
+
const existingNumbers = new Set(existing.map((chapter) => chapter.chapterNumber));
|
|
20
|
+
for (let index = 0; index < nextChapters.length; index += 1) {
|
|
21
|
+
const chapter = nextChapters[index];
|
|
22
|
+
const expected = expectedStart + index;
|
|
23
|
+
if (chapter.chapterNumber !== expected) {
|
|
24
|
+
throw new Error(`architecture_extension chapters must start at chapter ${expectedStart} and be contiguous; expected ${expected}, got ${chapter.chapterNumber}`);
|
|
25
|
+
}
|
|
26
|
+
if (chapter.chapterNumber > total) {
|
|
27
|
+
throw new Error(`architecture_extension chapter ${chapter.chapterNumber} exceeds plannedTotalChapters ${total}`);
|
|
28
|
+
}
|
|
29
|
+
if (existingNumbers.has(chapter.chapterNumber)) {
|
|
30
|
+
throw new Error(`architecture_extension cannot overwrite existing chapter architecture ${chapter.chapterNumber}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function mergeByKey(existing, incoming, keyOf) {
|
|
35
|
+
const map = new Map();
|
|
36
|
+
for (const item of existing)
|
|
37
|
+
map.set(keyOf(item), item);
|
|
38
|
+
for (const item of incoming ?? [])
|
|
39
|
+
map.set(keyOf(item), item);
|
|
40
|
+
return [...map.values()];
|
|
41
|
+
}
|
|
42
|
+
export const architectureExtensionHandler = async (state, content) => {
|
|
43
|
+
const parsed = ArchitectureExtensionPayloadSchema.parse(parseJson(content));
|
|
44
|
+
const existingChapters = await readJsonArray(state.projectPath, 'architecture/chapters.json');
|
|
45
|
+
assertContiguousExtension(state, existingChapters, parsed.chapters);
|
|
46
|
+
const chapters = [...existingChapters, ...parsed.chapters]
|
|
47
|
+
.sort((a, b) => a.chapterNumber - b.chapterNumber);
|
|
48
|
+
const existingVolumes = await readJsonArray(state.projectPath, 'architecture/volumes.json');
|
|
49
|
+
const volumes = mergeByKey(existingVolumes, parsed.volumes, (volume) => volume.id)
|
|
50
|
+
.sort((a, b) => a.order - b.order);
|
|
51
|
+
const existingPacing = await readJsonArray(state.projectPath, 'architecture/volume-pacing.json');
|
|
52
|
+
const volumePacing = mergeByKey(existingPacing, parsed.volumePacing, (board) => board.volumeId);
|
|
53
|
+
const savedPaths = [
|
|
54
|
+
await saveJsonFile(state.projectPath, 'architecture/chapters.json', chapters),
|
|
55
|
+
await saveJsonFile(state.projectPath, 'architecture/volumes.json', volumes),
|
|
56
|
+
];
|
|
57
|
+
const hasVolumePacing = Boolean(parsed.volumePacing || existingPacing.length);
|
|
58
|
+
if (hasVolumePacing) {
|
|
59
|
+
savedPaths.push(await saveJsonFile(state.projectPath, 'architecture/volume-pacing.json', volumePacing));
|
|
60
|
+
}
|
|
61
|
+
if (parsed.fullUpdate) {
|
|
62
|
+
savedPaths.push(await saveMarkdownFile(state.projectPath, 'architecture/full.md', parsed.fullUpdate));
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
savedPaths,
|
|
66
|
+
fileEntries: {
|
|
67
|
+
architecture: 'architecture/chapters.json',
|
|
68
|
+
...(hasVolumePacing ? { volumePacing: 'architecture/volume-pacing.json' } : {}),
|
|
69
|
+
},
|
|
70
|
+
next: { kind: 'linear', nextStep: 'chapter' },
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -5,6 +5,7 @@ import { chapterReviewFileName } from '../fileNames.js';
|
|
|
5
5
|
import { parseJson } from './types.js';
|
|
6
6
|
export const chapterReviewHandler = async (state, content) => {
|
|
7
7
|
const parsed = ChapterReviewSchema.parse(parseJson(content));
|
|
8
|
+
const hasFailedAcceptance = Object.values(parsed.acceptance).some((check) => check.status === 'fail');
|
|
8
9
|
const target = state.pendingAction?.chapterNumber ?? parsed.chapterNumber;
|
|
9
10
|
const relative = join('reviews/chapter', chapterReviewFileName(target));
|
|
10
11
|
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
@@ -15,7 +16,7 @@ export const chapterReviewHandler = async (state, content) => {
|
|
|
15
16
|
next: { kind: 'sideTrackReturn' },
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
if (parsed.status === 'clean') {
|
|
19
|
+
if (parsed.status === 'clean' && !hasFailedAcceptance) {
|
|
19
20
|
return {
|
|
20
21
|
savedPaths: [path],
|
|
21
22
|
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { architectureExtensionHandler } from './architectureExtension.js';
|
|
1
2
|
import { architectureHandler } from './architecture.js';
|
|
2
3
|
import { chapterHandler } from './chapter.js';
|
|
3
4
|
import { chapterReviewHandler } from './chapterReview.js';
|
|
@@ -6,11 +7,14 @@ import { continuityReviewHandler } from './continuityReview.js';
|
|
|
6
7
|
import { crossChapterReviewHandler } from './crossChapterReview.js';
|
|
7
8
|
import { memoryCardHandler } from './memoryCard.js';
|
|
8
9
|
import { novelMetadataHandler } from './novelMetadata.js';
|
|
10
|
+
import { styleGuideHandler } from './styleGuide.js';
|
|
9
11
|
import { storyBibleHandler } from './storyBible.js';
|
|
10
12
|
export const STEP_HANDLERS = {
|
|
11
13
|
novel_metadata: novelMetadataHandler,
|
|
12
14
|
story_bible: storyBibleHandler,
|
|
15
|
+
style_guide: styleGuideHandler,
|
|
13
16
|
architecture: architectureHandler,
|
|
17
|
+
architecture_extension: architectureExtensionHandler,
|
|
14
18
|
chapter: chapterHandler,
|
|
15
19
|
memory_card: memoryCardHandler,
|
|
16
20
|
continuity_review: continuityReviewHandler,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import { join } from 'node:path';
|
|
2
3
|
import { MemoryCardSchema } from '../schemas.js';
|
|
3
4
|
import { saveJsonFile } from '../projectStore.js';
|
|
@@ -6,6 +7,19 @@ import { indexMemoryCard } from '../retrieval/index.js';
|
|
|
6
7
|
import { ingestMemoryCardThreads } from '../threadStore.js';
|
|
7
8
|
import { applyCharacterUpdates } from '../characterStore.js';
|
|
8
9
|
import { parseJson } from './types.js';
|
|
10
|
+
async function maxPlannedChapter(projectPath) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(join(projectPath, 'architecture/chapters.json'), 'utf8');
|
|
13
|
+
const chapters = JSON.parse(raw);
|
|
14
|
+
return chapters.reduce((max, chapter) => {
|
|
15
|
+
const value = Number(chapter.chapterNumber);
|
|
16
|
+
return Number.isFinite(value) && value > max ? value : max;
|
|
17
|
+
}, 0);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
9
23
|
export const memoryCardHandler = async (state, content) => {
|
|
10
24
|
const parsed = MemoryCardSchema.parse(parseJson(content));
|
|
11
25
|
const relative = join('memory', memoryFileName(state.currentChapter));
|
|
@@ -14,12 +28,19 @@ export const memoryCardHandler = async (state, content) => {
|
|
|
14
28
|
await ingestMemoryCardThreads(state.projectPath, state.currentChapter, parsed.threadActions);
|
|
15
29
|
await applyCharacterUpdates(state.projectPath, state.currentChapter, parsed.characterUpdates);
|
|
16
30
|
const nextChapter = state.currentChapter + 1;
|
|
31
|
+
const plannedTotalChapters = state.plannedTotalChapters ?? state.targetChapters;
|
|
32
|
+
const plannedMax = await maxPlannedChapter(state.projectPath);
|
|
33
|
+
const nextStep = nextChapter > plannedTotalChapters
|
|
34
|
+
? 'continuity_review'
|
|
35
|
+
: nextChapter > plannedMax
|
|
36
|
+
? 'architecture_extension'
|
|
37
|
+
: 'chapter';
|
|
17
38
|
return {
|
|
18
39
|
savedPaths: [path],
|
|
19
40
|
fileEntries: { [`memory-${state.currentChapter}`]: relative },
|
|
20
41
|
next: {
|
|
21
42
|
kind: 'linear',
|
|
22
|
-
nextStep
|
|
43
|
+
nextStep,
|
|
23
44
|
statePatch: { currentChapter: nextChapter },
|
|
24
45
|
},
|
|
25
46
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StyleGuideSchema } from '../schemas.js';
|
|
2
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { parseJson } from './types.js';
|
|
4
|
+
export const styleGuideHandler = async (state, content) => {
|
|
5
|
+
const parsed = StyleGuideSchema.parse(parseJson(content));
|
|
6
|
+
const path = await saveJsonFile(state.projectPath, 'style-guide.json', parsed);
|
|
7
|
+
return {
|
|
8
|
+
savedPaths: [path],
|
|
9
|
+
fileEntries: { styleGuide: 'style-guide.json' },
|
|
10
|
+
next: { kind: 'linear', nextStep: 'architecture' },
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -5,6 +5,8 @@ import { buildPromptForStep } from './prompts.js';
|
|
|
5
5
|
import { loadState, saveJsonFile, saveRecoveryFile, saveState } from './projectStore.js';
|
|
6
6
|
import { STEP_HANDLERS } from './steps/index.js';
|
|
7
7
|
const CONTEXT_RECIPES = {
|
|
8
|
+
style_guide: () => ({ purpose: 'style_guide' }),
|
|
9
|
+
architecture_extension: (s) => ({ purpose: 'architecture_extension', chapterNumber: s.currentChapter }),
|
|
8
10
|
chapter: (s) => ({ purpose: 'chapter_generation', chapterNumber: s.currentChapter }),
|
|
9
11
|
memory_card: (s) => ({ purpose: 'memory_extraction', chapterNumber: s.currentChapter }),
|
|
10
12
|
continuity_review: () => ({ purpose: 'continuity_review' }),
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { amendStoryBible, assertProjectPath, buildContext, createProject, deleteChapter, forkProject, getNextStep, getProjectStatus, listProjects, listStoryBibleVersions, loadState, loadThreads, redoStep, requestSideTrack, retrieve, submitStepResult, updateThread, } from '../core/index.js';
|
|
5
|
-
|
|
7
|
+
function packageVersion() {
|
|
8
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
while (true) {
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(join(dir, 'package.json'), 'utf8');
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (typeof parsed.version === 'string' && parsed.version)
|
|
14
|
+
return parsed.version;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// keep walking upward until the package root is found
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir)
|
|
21
|
+
return '0.0.0';
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const MCP_SERVER_VERSION = packageVersion();
|
|
6
26
|
function textResult(value) {
|
|
7
27
|
return {
|
|
8
28
|
content: [{
|
|
@@ -28,14 +48,16 @@ export function createNovelAgentServer(options) {
|
|
|
28
48
|
prompt: z.string().min(1),
|
|
29
49
|
language: z.enum(['zh-CN', 'en-US']).default('zh-CN'),
|
|
30
50
|
outputDir: z.string().default('novels'),
|
|
31
|
-
targetChapters: z.number().int().positive().default(
|
|
32
|
-
|
|
51
|
+
targetChapters: z.number().int().positive().default(5),
|
|
52
|
+
plannedTotalChapters: z.number().int().positive().default(12),
|
|
53
|
+
}, async ({ prompt, language, outputDir, targetChapters, plannedTotalChapters }) => {
|
|
33
54
|
const result = await createProject({
|
|
34
55
|
workspaceRoot: options.workspaceRoot,
|
|
35
56
|
prompt,
|
|
36
57
|
language,
|
|
37
58
|
outputDir,
|
|
38
59
|
targetChapters,
|
|
60
|
+
plannedTotalChapters,
|
|
39
61
|
});
|
|
40
62
|
return textResult({ state: result.state, next: await getNextStep(result.state.projectPath) });
|
|
41
63
|
});
|
|
@@ -49,7 +71,9 @@ export function createNovelAgentServer(options) {
|
|
|
49
71
|
step: z.enum([
|
|
50
72
|
'novel_metadata',
|
|
51
73
|
'story_bible',
|
|
74
|
+
'style_guide',
|
|
52
75
|
'architecture',
|
|
76
|
+
'architecture_extension',
|
|
53
77
|
'chapter',
|
|
54
78
|
'memory_card',
|
|
55
79
|
'continuity_review',
|
|
@@ -64,6 +88,8 @@ export function createNovelAgentServer(options) {
|
|
|
64
88
|
projectPath: z.string().min(1),
|
|
65
89
|
purpose: z.enum([
|
|
66
90
|
'chapter_generation',
|
|
91
|
+
'style_guide',
|
|
92
|
+
'architecture_extension',
|
|
67
93
|
'memory_extraction',
|
|
68
94
|
'continuity_review',
|
|
69
95
|
'revision',
|
|
@@ -177,6 +203,7 @@ export function createNovelAgentServer(options) {
|
|
|
177
203
|
step: z.enum([
|
|
178
204
|
'novel_metadata',
|
|
179
205
|
'story_bible',
|
|
206
|
+
'style_guide',
|
|
180
207
|
'architecture',
|
|
181
208
|
'chapter',
|
|
182
209
|
'memory_card',
|
|
@@ -187,13 +214,14 @@ export function createNovelAgentServer(options) {
|
|
|
187
214
|
// ===== MCP Prompts (slash commands) =====
|
|
188
215
|
server.prompt('nf-start', 'Start a brand new novel project under the configured workspace.', {
|
|
189
216
|
prompt: z.string().describe('User idea / premise / genre, in any language.'),
|
|
190
|
-
chapters: z.string().optional().describe('
|
|
191
|
-
|
|
217
|
+
chapters: z.string().optional().describe('Planning batch size as a string. Defaults to 5.'),
|
|
218
|
+
totalChapters: z.string().optional().describe('Whole-book target chapter count as a string. Defaults to 12.'),
|
|
219
|
+
}, ({ prompt, chapters, totalChapters }) => ({
|
|
192
220
|
messages: [{
|
|
193
221
|
role: 'user',
|
|
194
222
|
content: {
|
|
195
223
|
type: 'text',
|
|
196
|
-
text: `Use the novelforge MCP server. Call start_novel_project with prompt="${prompt}", targetChapters=${chapters ?? '5'}, then enter the autonomous loop: read next.instruction, generate the requested content, call submit_step_result, repeat until currentStep is "complete". Show me the projectPath after start_novel_project returns.`,
|
|
224
|
+
text: `Use the novelforge MCP server. Call start_novel_project with prompt="${prompt}", targetChapters=${chapters ?? '5'}, plannedTotalChapters=${totalChapters ?? '12'}, then enter the autonomous loop: read next.instruction, generate the requested content, call submit_step_result, repeat until currentStep is "complete". Show me the projectPath after start_novel_project returns.`,
|
|
197
225
|
},
|
|
198
226
|
}],
|
|
199
227
|
}));
|
|
@@ -213,7 +241,7 @@ export function createNovelAgentServer(options) {
|
|
|
213
241
|
role: 'user',
|
|
214
242
|
content: {
|
|
215
243
|
type: 'text',
|
|
216
|
-
text: 'Use the novelforge MCP server: call list_projects with no arguments. Show me the result in a compact table (title, currentStep, chaptersWritten/
|
|
244
|
+
text: 'Use the novelforge MCP server: call list_projects with no arguments. Show me the result in a compact table (title, currentStep, chaptersWritten/plannedTotalChapters, updatedAt, projectPath).',
|
|
217
245
|
},
|
|
218
246
|
}],
|
|
219
247
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novelforge-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Local-first long-form novel workflow engine for any MCP host (Claude Code, Codex CLI, …) or CLI. State machine + zod schemas + BM25 retrieval + persistent project state. No LLM dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
package/src/cli/index.ts
CHANGED
|
@@ -54,9 +54,17 @@ export async function runCli(argv = process.argv.slice(2), cwd = process.cwd()):
|
|
|
54
54
|
const prompt = valueAfter(argv, '--prompt') || '';
|
|
55
55
|
if (!prompt.trim()) throw new Error('Missing --prompt');
|
|
56
56
|
const language = parseLanguage(valueAfter(argv, '--language') || 'zh-CN');
|
|
57
|
-
const chapters = Number(valueAfter(argv, '--chapters') ||
|
|
57
|
+
const chapters = Number(valueAfter(argv, '--chapters') || 5);
|
|
58
|
+
const totalChapters = Number(valueAfter(argv, '--total-chapters') || 12);
|
|
58
59
|
const outputDir = valueAfter(argv, '--output') || 'novels';
|
|
59
|
-
const result = await createProject({
|
|
60
|
+
const result = await createProject({
|
|
61
|
+
workspaceRoot: cwd,
|
|
62
|
+
prompt,
|
|
63
|
+
language,
|
|
64
|
+
outputDir,
|
|
65
|
+
targetChapters: chapters,
|
|
66
|
+
plannedTotalChapters: totalChapters,
|
|
67
|
+
});
|
|
60
68
|
const next = await getNextStep(result.state.projectPath);
|
|
61
69
|
console.log(JSON.stringify({ state: result.state, next }, null, 2));
|
|
62
70
|
return;
|
|
@@ -217,7 +225,7 @@ export async function runCli(argv = process.argv.slice(2), cwd = process.cwd()):
|
|
|
217
225
|
if (command === 'redo') {
|
|
218
226
|
if (!projectPath) throw new Error('Missing projectPath');
|
|
219
227
|
const step = valueAfter(argv, '--step') as
|
|
220
|
-
| 'novel_metadata' | 'story_bible' | 'architecture' | 'chapter' | 'memory_card' | 'continuity_review'
|
|
228
|
+
| 'novel_metadata' | 'story_bible' | 'style_guide' | 'architecture' | 'chapter' | 'memory_card' | 'continuity_review'
|
|
221
229
|
| undefined;
|
|
222
230
|
if (!step) throw new Error('Missing --step');
|
|
223
231
|
const chapter = valueAfter(argv, '--chapter');
|
|
@@ -6,6 +6,8 @@ import { activeThreads, loadThreads } from './threadStore.js';
|
|
|
6
6
|
|
|
7
7
|
export type ContextPurpose =
|
|
8
8
|
| 'chapter_generation'
|
|
9
|
+
| 'style_guide'
|
|
10
|
+
| 'architecture_extension'
|
|
9
11
|
| 'memory_extraction'
|
|
10
12
|
| 'continuity_review'
|
|
11
13
|
| 'revision'
|
|
@@ -32,12 +34,14 @@ export async function buildContext(input: BuildContextInput): Promise<string> {
|
|
|
32
34
|
const parts: string[] = [];
|
|
33
35
|
const metadata = await readOptional(join(input.projectPath, 'novel.json'));
|
|
34
36
|
const storyBible = await readOptional(join(input.projectPath, 'story-bible.md'));
|
|
37
|
+
const styleGuideJson = await readOptional(join(input.projectPath, 'style-guide.json'));
|
|
35
38
|
const chaptersJson = await readOptional(join(input.projectPath, 'architecture/chapters.json'));
|
|
36
39
|
const charactersJson = await readOptional(join(input.projectPath, 'characters.json'));
|
|
37
40
|
const volumePacingJson = await readOptional(join(input.projectPath, 'architecture/volume-pacing.json'));
|
|
38
41
|
|
|
39
42
|
if (metadata) parts.push(`## Novel Metadata\n${metadata}`);
|
|
40
43
|
if (storyBible) parts.push(`## Story Bible\n${storyBible.slice(0, 4000)}`);
|
|
44
|
+
if (styleGuideJson) parts.push(`## Style Guide\n${styleGuideJson}`);
|
|
41
45
|
if (charactersJson) parts.push(`## Character State Table\n${charactersJson}`);
|
|
42
46
|
|
|
43
47
|
function addVolumePacing(volumeId?: string): void {
|
|
@@ -113,6 +117,32 @@ export async function buildContext(input: BuildContextInput): Promise<string> {
|
|
|
113
117
|
if (memoryParts.length) parts.push(`## Memory Cards\n${memoryParts.join('\n')}`);
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
if (input.purpose === 'architecture_extension') {
|
|
121
|
+
if (chaptersJson) parts.push(`## Existing Chapter Architecture List\n${chaptersJson}`);
|
|
122
|
+
if (volumePacingJson) parts.push(`## Existing Volume Pacing Boards\n${volumePacingJson}`);
|
|
123
|
+
|
|
124
|
+
const start = Math.max(1, (input.chapterNumber ?? 1) - 5);
|
|
125
|
+
const end = Math.max(0, (input.chapterNumber ?? 1) - 1);
|
|
126
|
+
const memoryParts: string[] = [];
|
|
127
|
+
for (let i = start; i <= end; i += 1) {
|
|
128
|
+
const memory = await readOptional(join(input.projectPath, 'memory', memoryFileName(i)));
|
|
129
|
+
if (memory) memoryParts.push(`### Chapter ${i} Memory\n${memory}`);
|
|
130
|
+
}
|
|
131
|
+
if (memoryParts.length) parts.push(`## Recent Memory Cards\n${memoryParts.join('\n')}`);
|
|
132
|
+
|
|
133
|
+
const allThreads = await loadThreads(input.projectPath);
|
|
134
|
+
const active = activeThreads(allThreads);
|
|
135
|
+
if (active.length) {
|
|
136
|
+
const lines = active.map((t) => {
|
|
137
|
+
const flags: string[] = [`#${t.id}`, `status=${t.status}`, `planted=ch${t.plantedAt}`];
|
|
138
|
+
if (t.plannedPayoffAt) flags.push(`payoff=ch${t.plannedPayoffAt}`);
|
|
139
|
+
if (t.lastTouchedAt !== t.plantedAt) flags.push(`touched=ch${t.lastTouchedAt}`);
|
|
140
|
+
return `- ${t.description} (${flags.join(', ')})`;
|
|
141
|
+
});
|
|
142
|
+
parts.push(`## Active Foreshadow Threads\n${lines.join('\n')}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
if (input.purpose === 'chapter_review' && input.chapterNumber) {
|
|
117
147
|
if (chaptersJson) {
|
|
118
148
|
const chapters = JSON.parse(chaptersJson) as Array<{ chapterNumber: number; title: string; summary: string; requiredBeats?: string[]; volumeId?: string }>;
|
|
@@ -12,6 +12,7 @@ export interface ProjectSummary {
|
|
|
12
12
|
currentStep: WorkflowStep;
|
|
13
13
|
currentChapter: number;
|
|
14
14
|
targetChapters: number;
|
|
15
|
+
plannedTotalChapters: number;
|
|
15
16
|
completedSteps: number;
|
|
16
17
|
chaptersWritten: number;
|
|
17
18
|
updatedAt: string;
|
|
@@ -57,6 +58,7 @@ async function summarizeOne(projectPath: string): Promise<ProjectSummary | undef
|
|
|
57
58
|
currentStep: state.currentStep,
|
|
58
59
|
currentChapter: state.currentChapter,
|
|
59
60
|
targetChapters: state.targetChapters,
|
|
61
|
+
plannedTotalChapters: state.plannedTotalChapters ?? state.targetChapters,
|
|
60
62
|
completedSteps: state.completedSteps.length,
|
|
61
63
|
chaptersWritten: countChapters(state),
|
|
62
64
|
updatedAt: state.updatedAt,
|
package/src/core/projectOps.ts
CHANGED
|
@@ -164,6 +164,7 @@ export interface RedoStepResult {
|
|
|
164
164
|
const STEP_FILE_KEYS: Partial<Record<WorkflowStep, string[]>> = {
|
|
165
165
|
novel_metadata: ['novel'],
|
|
166
166
|
story_bible: ['storyBible'],
|
|
167
|
+
style_guide: ['styleGuide'],
|
|
167
168
|
architecture: ['architecture'],
|
|
168
169
|
continuity_review: ['continuityReview'],
|
|
169
170
|
};
|
|
@@ -171,6 +172,7 @@ const STEP_FILE_KEYS: Partial<Record<WorkflowStep, string[]>> = {
|
|
|
171
172
|
const STEP_FILE_PATHS: Partial<Record<WorkflowStep, string[]>> = {
|
|
172
173
|
novel_metadata: ['novel.json'],
|
|
173
174
|
story_bible: ['story-bible.md'],
|
|
175
|
+
style_guide: ['style-guide.json'],
|
|
174
176
|
architecture: ['architecture/full.md', 'architecture/volumes.json', 'architecture/chapters.json'],
|
|
175
177
|
};
|
|
176
178
|
|
|
@@ -198,7 +200,13 @@ export async function redoStep(input: RedoStepInput): Promise<RedoStepResult> {
|
|
|
198
200
|
state.currentChapter = chapter;
|
|
199
201
|
state.currentStep = input.step;
|
|
200
202
|
state.pendingAction = undefined;
|
|
201
|
-
} else if (
|
|
203
|
+
} else if (
|
|
204
|
+
input.step === 'novel_metadata'
|
|
205
|
+
|| input.step === 'story_bible'
|
|
206
|
+
|| input.step === 'style_guide'
|
|
207
|
+
|| input.step === 'architecture'
|
|
208
|
+
|| input.step === 'continuity_review'
|
|
209
|
+
) {
|
|
202
210
|
const paths = STEP_FILE_PATHS[input.step] ?? [];
|
|
203
211
|
for (const p of paths) {
|
|
204
212
|
if (await tryUnlink(join(state.projectPath, p))) removed.push(p);
|
package/src/core/projectStore.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface CreateProjectInput {
|
|
|
10
10
|
language?: AgentState['language'];
|
|
11
11
|
outputDir?: string;
|
|
12
12
|
targetChapters?: number;
|
|
13
|
+
plannedTotalChapters?: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface CreateProjectResult {
|
|
@@ -61,7 +62,12 @@ export async function archiveStoryBible(projectPath: string, versionRelative: st
|
|
|
61
62
|
export async function createProject(input: CreateProjectInput): Promise<CreateProjectResult> {
|
|
62
63
|
const workspaceRoot = resolve(input.workspaceRoot);
|
|
63
64
|
const baseDir = input.outputDir || 'novels';
|
|
64
|
-
const
|
|
65
|
+
const hasExplicitTargetChapters = input.targetChapters !== undefined;
|
|
66
|
+
const targetChapters = Math.max(1, Math.floor(Number(input.targetChapters || 5)));
|
|
67
|
+
const plannedTotalChapters = Math.max(
|
|
68
|
+
targetChapters,
|
|
69
|
+
Math.floor(Number(input.plannedTotalChapters ?? (hasExplicitTargetChapters ? targetChapters : 12)))
|
|
70
|
+
);
|
|
65
71
|
const baseSlug = makeProjectSlug(input.prompt.slice(0, 48));
|
|
66
72
|
const suffix = randomBytes(3).toString('hex');
|
|
67
73
|
const slug = `${baseSlug}-${suffix}`;
|
|
@@ -76,6 +82,7 @@ export async function createProject(input: CreateProjectInput): Promise<CreatePr
|
|
|
76
82
|
initialPrompt: input.prompt,
|
|
77
83
|
language: input.language || 'zh-CN',
|
|
78
84
|
targetChapters,
|
|
85
|
+
plannedTotalChapters,
|
|
79
86
|
currentStep: 'novel_metadata',
|
|
80
87
|
currentChapter: 1,
|
|
81
88
|
completedSteps: [],
|