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
|
@@ -12,6 +12,16 @@ export const chapterHandler: StepHandler = async (state, content) => {
|
|
|
12
12
|
return {
|
|
13
13
|
savedPaths: [path],
|
|
14
14
|
fileEntries: { [`chapter-${state.currentChapter}`]: relative },
|
|
15
|
-
next: {
|
|
15
|
+
next: {
|
|
16
|
+
kind: 'linear',
|
|
17
|
+
nextStep: 'chapter_review',
|
|
18
|
+
statePatch: {
|
|
19
|
+
pendingAction: {
|
|
20
|
+
step: 'chapter_review',
|
|
21
|
+
mode: 'gate',
|
|
22
|
+
chapterNumber: state.currentChapter,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
16
26
|
};
|
|
17
27
|
};
|
|
@@ -6,12 +6,39 @@ import { StepHandler, parseJson } from './types.js';
|
|
|
6
6
|
|
|
7
7
|
export const chapterReviewHandler: StepHandler = async (state, content) => {
|
|
8
8
|
const parsed = ChapterReviewSchema.parse(parseJson(content));
|
|
9
|
+
const hasFailedAcceptance = Object.values(parsed.acceptance).some((check) => check.status === 'fail');
|
|
9
10
|
const target = state.pendingAction?.chapterNumber ?? parsed.chapterNumber;
|
|
10
11
|
const relative = join('reviews/chapter', chapterReviewFileName(target));
|
|
11
12
|
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
13
|
+
if (state.pendingAction?.mode === 'side_track') {
|
|
14
|
+
return {
|
|
15
|
+
savedPaths: [path],
|
|
16
|
+
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
17
|
+
next: { kind: 'sideTrackReturn' },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (parsed.status === 'clean' && !hasFailedAcceptance) {
|
|
22
|
+
return {
|
|
23
|
+
savedPaths: [path],
|
|
24
|
+
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
25
|
+
next: { kind: 'linear', nextStep: 'memory_card' },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
return {
|
|
13
30
|
savedPaths: [path],
|
|
14
31
|
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
15
|
-
next: {
|
|
32
|
+
next: {
|
|
33
|
+
kind: 'linear',
|
|
34
|
+
nextStep: 'chapter_revision',
|
|
35
|
+
statePatch: {
|
|
36
|
+
pendingAction: {
|
|
37
|
+
step: 'chapter_revision',
|
|
38
|
+
mode: 'gate',
|
|
39
|
+
chapterNumber: target,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
16
43
|
};
|
|
17
44
|
};
|
|
@@ -13,6 +13,24 @@ export const chapterRevisionHandler: StepHandler = async (state, content) => {
|
|
|
13
13
|
const savedPaths = archived ? [archived] : [];
|
|
14
14
|
savedPaths.push(await saveMarkdownFile(state.projectPath, chapterRelative, content));
|
|
15
15
|
await indexChapter(state.projectPath, target, content);
|
|
16
|
+
if (state.pendingAction?.mode === 'gate') {
|
|
17
|
+
return {
|
|
18
|
+
savedPaths,
|
|
19
|
+
fileEntries: { [`chapter-${target}`]: chapterRelative },
|
|
20
|
+
next: {
|
|
21
|
+
kind: 'linear',
|
|
22
|
+
nextStep: 'chapter_review',
|
|
23
|
+
statePatch: {
|
|
24
|
+
pendingAction: {
|
|
25
|
+
step: 'chapter_review',
|
|
26
|
+
mode: 'gate',
|
|
27
|
+
chapterNumber: target,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
return {
|
|
17
35
|
savedPaths,
|
|
18
36
|
fileEntries: { [`chapter-${target}`]: chapterRelative },
|
package/src/core/steps/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WorkflowStep } from '../types.js';
|
|
2
|
+
import { architectureExtensionHandler } from './architectureExtension.js';
|
|
2
3
|
import { architectureHandler } from './architecture.js';
|
|
3
4
|
import { chapterHandler } from './chapter.js';
|
|
4
5
|
import { chapterReviewHandler } from './chapterReview.js';
|
|
@@ -7,6 +8,7 @@ import { continuityReviewHandler } from './continuityReview.js';
|
|
|
7
8
|
import { crossChapterReviewHandler } from './crossChapterReview.js';
|
|
8
9
|
import { memoryCardHandler } from './memoryCard.js';
|
|
9
10
|
import { novelMetadataHandler } from './novelMetadata.js';
|
|
11
|
+
import { styleGuideHandler } from './styleGuide.js';
|
|
10
12
|
import { storyBibleHandler } from './storyBible.js';
|
|
11
13
|
import { StepHandler } from './types.js';
|
|
12
14
|
|
|
@@ -15,7 +17,9 @@ export type { StepApplyNext, StepApplyResult, StepHandler } from './types.js';
|
|
|
15
17
|
export const STEP_HANDLERS: Partial<Record<WorkflowStep, StepHandler>> = {
|
|
16
18
|
novel_metadata: novelMetadataHandler,
|
|
17
19
|
story_bible: storyBibleHandler,
|
|
20
|
+
style_guide: styleGuideHandler,
|
|
18
21
|
architecture: architectureHandler,
|
|
22
|
+
architecture_extension: architectureExtensionHandler,
|
|
19
23
|
chapter: chapterHandler,
|
|
20
24
|
memory_card: memoryCardHandler,
|
|
21
25
|
continuity_review: continuityReviewHandler,
|
|
@@ -1,22 +1,48 @@
|
|
|
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';
|
|
4
5
|
import { memoryFileName } from '../fileNames.js';
|
|
5
6
|
import { indexMemoryCard } from '../retrieval/index.js';
|
|
7
|
+
import { ingestMemoryCardThreads } from '../threadStore.js';
|
|
8
|
+
import { applyCharacterUpdates } from '../characterStore.js';
|
|
6
9
|
import { StepHandler, parseJson } from './types.js';
|
|
7
10
|
|
|
11
|
+
async function maxPlannedChapter(projectPath: string): Promise<number> {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(join(projectPath, 'architecture/chapters.json'), 'utf8');
|
|
14
|
+
const chapters = JSON.parse(raw) as Array<{ chapterNumber?: number }>;
|
|
15
|
+
return chapters.reduce((max, chapter) => {
|
|
16
|
+
const value = Number(chapter.chapterNumber);
|
|
17
|
+
return Number.isFinite(value) && value > max ? value : max;
|
|
18
|
+
}, 0);
|
|
19
|
+
} catch {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
export const memoryCardHandler: StepHandler = async (state, content) => {
|
|
9
25
|
const parsed = MemoryCardSchema.parse(parseJson(content));
|
|
10
26
|
const relative = join('memory', memoryFileName(state.currentChapter));
|
|
11
27
|
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
12
28
|
await indexMemoryCard(state.projectPath, state.currentChapter, parsed);
|
|
29
|
+
await ingestMemoryCardThreads(state.projectPath, state.currentChapter, parsed.threadActions);
|
|
30
|
+
await applyCharacterUpdates(state.projectPath, state.currentChapter, parsed.characterUpdates);
|
|
13
31
|
const nextChapter = state.currentChapter + 1;
|
|
32
|
+
const plannedTotalChapters = state.plannedTotalChapters ?? state.targetChapters;
|
|
33
|
+
const plannedMax = await maxPlannedChapter(state.projectPath);
|
|
34
|
+
const nextStep =
|
|
35
|
+
nextChapter > plannedTotalChapters
|
|
36
|
+
? 'continuity_review'
|
|
37
|
+
: nextChapter > plannedMax
|
|
38
|
+
? 'architecture_extension'
|
|
39
|
+
: 'chapter';
|
|
14
40
|
return {
|
|
15
41
|
savedPaths: [path],
|
|
16
42
|
fileEntries: { [`memory-${state.currentChapter}`]: relative },
|
|
17
43
|
next: {
|
|
18
44
|
kind: 'linear',
|
|
19
|
-
nextStep
|
|
45
|
+
nextStep,
|
|
20
46
|
statePatch: { currentChapter: nextChapter },
|
|
21
47
|
},
|
|
22
48
|
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { NovelMetadataSchema } from '../schemas.js';
|
|
2
2
|
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { initializeCharacterStates } from '../characterStore.js';
|
|
3
4
|
import { StepHandler, parseJson } from './types.js';
|
|
4
5
|
|
|
5
6
|
export const novelMetadataHandler: StepHandler = async (state, content) => {
|
|
6
7
|
const parsed = NovelMetadataSchema.parse(parseJson(content));
|
|
7
8
|
const path = await saveJsonFile(state.projectPath, 'novel.json', parsed);
|
|
9
|
+
const charactersPath = await initializeCharacterStates(state.projectPath, parsed.coreCast);
|
|
8
10
|
return {
|
|
9
|
-
savedPaths: [path],
|
|
10
|
-
fileEntries: { novel: 'novel.json' },
|
|
11
|
+
savedPaths: [path, charactersPath],
|
|
12
|
+
fileEntries: { novel: 'novel.json', characters: 'characters.json' },
|
|
11
13
|
next: { kind: 'linear', nextStep: 'story_bible' },
|
|
12
14
|
};
|
|
13
15
|
};
|
|
@@ -9,6 +9,6 @@ export const storyBibleHandler: StepHandler = async (state, content) => {
|
|
|
9
9
|
return {
|
|
10
10
|
savedPaths: [path],
|
|
11
11
|
fileEntries: { storyBible: 'story-bible.md' },
|
|
12
|
-
next: { kind: 'linear', nextStep: '
|
|
12
|
+
next: { kind: 'linear', nextStep: 'style_guide' },
|
|
13
13
|
};
|
|
14
14
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StyleGuideSchema } from '../schemas.js';
|
|
2
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { StepHandler, parseJson } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const styleGuideHandler: StepHandler = async (state, content) => {
|
|
6
|
+
const parsed = StyleGuideSchema.parse(parseJson(content));
|
|
7
|
+
const path = await saveJsonFile(state.projectPath, 'style-guide.json', parsed);
|
|
8
|
+
return {
|
|
9
|
+
savedPaths: [path],
|
|
10
|
+
fileEntries: { styleGuide: 'style-guide.json' },
|
|
11
|
+
next: { kind: 'linear', nextStep: 'architecture' },
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Thread, ThreadAction, ThreadStatus } from './types.js';
|
|
5
|
+
|
|
6
|
+
const THREADS_FILE = 'threads.json';
|
|
7
|
+
|
|
8
|
+
export interface ThreadsBundle {
|
|
9
|
+
threads: Thread[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function loadThreads(projectPath: string): Promise<Thread[]> {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(join(projectPath, THREADS_FILE), 'utf8');
|
|
15
|
+
const parsed = JSON.parse(raw) as ThreadsBundle;
|
|
16
|
+
return Array.isArray(parsed.threads) ? parsed.threads : [];
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function saveThreads(projectPath: string, threads: Thread[]): Promise<string> {
|
|
23
|
+
const fullPath = join(projectPath, THREADS_FILE);
|
|
24
|
+
const bundle: ThreadsBundle = { threads };
|
|
25
|
+
await writeFile(fullPath, `${JSON.stringify(bundle, null, 2)}\n`, 'utf8');
|
|
26
|
+
return fullPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function newThreadId(existing: Set<string>): string {
|
|
30
|
+
let candidate = `t_${randomBytes(3).toString('hex')}`;
|
|
31
|
+
while (existing.has(candidate)) {
|
|
32
|
+
candidate = `t_${randomBytes(3).toString('hex')}`;
|
|
33
|
+
}
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findByDescription(threads: Thread[], description: string): Thread | undefined {
|
|
38
|
+
const target = description.trim();
|
|
39
|
+
return threads.find((t) => t.description.trim() === target);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply the threadActions emitted by a memory_card for chapter `chapterNumber`.
|
|
44
|
+
* Behavior:
|
|
45
|
+
* - 'plant' → create new thread (or reuse if identical description already planted)
|
|
46
|
+
* - 'build' → mark existing thread status = 'building', bump lastTouchedAt
|
|
47
|
+
* - 'pay' → mark existing thread status = 'paid', set paidOffAt
|
|
48
|
+
* - 'drop' → mark existing thread status = 'dropped', set droppedAt
|
|
49
|
+
* Unknown threadIds for non-plant actions are tolerated (a new thread is created and marked appropriately, so we never lose user intent).
|
|
50
|
+
*/
|
|
51
|
+
export function applyThreadActions(
|
|
52
|
+
existing: Thread[],
|
|
53
|
+
chapterNumber: number,
|
|
54
|
+
actions: ThreadAction[]
|
|
55
|
+
): Thread[] {
|
|
56
|
+
if (!actions || !actions.length) return existing;
|
|
57
|
+
const next: Thread[] = existing.map((t) => ({ ...t }));
|
|
58
|
+
const byId = new Map(next.map((t) => [t.id, t]));
|
|
59
|
+
const usedIds = new Set(next.map((t) => t.id));
|
|
60
|
+
|
|
61
|
+
for (const action of actions) {
|
|
62
|
+
if (action.kind === 'plant') {
|
|
63
|
+
const dup = findByDescription(next, action.description);
|
|
64
|
+
if (dup) {
|
|
65
|
+
dup.lastTouchedAt = Math.max(dup.lastTouchedAt, chapterNumber);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const id = action.threadId && !usedIds.has(action.threadId)
|
|
69
|
+
? action.threadId
|
|
70
|
+
: newThreadId(usedIds);
|
|
71
|
+
usedIds.add(id);
|
|
72
|
+
const planted: Thread = {
|
|
73
|
+
id,
|
|
74
|
+
description: action.description.trim(),
|
|
75
|
+
status: 'planted',
|
|
76
|
+
plantedAt: chapterNumber,
|
|
77
|
+
lastTouchedAt: chapterNumber,
|
|
78
|
+
};
|
|
79
|
+
next.push(planted);
|
|
80
|
+
byId.set(id, planted);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// build / pay / drop need an existing thread
|
|
85
|
+
let target = action.threadId ? byId.get(action.threadId) : undefined;
|
|
86
|
+
if (!target) {
|
|
87
|
+
target = findByDescription(next, action.description);
|
|
88
|
+
}
|
|
89
|
+
if (!target) {
|
|
90
|
+
// Create a placeholder so the user intent is captured; mark planted+touched on this chapter
|
|
91
|
+
const id = newThreadId(usedIds);
|
|
92
|
+
usedIds.add(id);
|
|
93
|
+
target = {
|
|
94
|
+
id,
|
|
95
|
+
description: action.description.trim(),
|
|
96
|
+
status: 'planted',
|
|
97
|
+
plantedAt: chapterNumber,
|
|
98
|
+
lastTouchedAt: chapterNumber,
|
|
99
|
+
notes: `Auto-created from a ${action.kind} action without a known threadId.`,
|
|
100
|
+
};
|
|
101
|
+
next.push(target);
|
|
102
|
+
byId.set(id, target);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
target.lastTouchedAt = chapterNumber;
|
|
106
|
+
if (action.kind === 'build') {
|
|
107
|
+
target.status = 'building';
|
|
108
|
+
} else if (action.kind === 'pay') {
|
|
109
|
+
target.status = 'paid';
|
|
110
|
+
target.paidOffAt = chapterNumber;
|
|
111
|
+
} else if (action.kind === 'drop') {
|
|
112
|
+
target.status = 'dropped';
|
|
113
|
+
target.droppedAt = chapterNumber;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function activeThreads(threads: Thread[]): Thread[] {
|
|
121
|
+
return threads.filter((t) => t.status === 'planted' || t.status === 'building');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function ingestMemoryCardThreads(
|
|
125
|
+
projectPath: string,
|
|
126
|
+
chapterNumber: number,
|
|
127
|
+
actions: ThreadAction[] | undefined
|
|
128
|
+
): Promise<Thread[]> {
|
|
129
|
+
if (!actions || !actions.length) return loadThreads(projectPath);
|
|
130
|
+
const existing = await loadThreads(projectPath);
|
|
131
|
+
const next = applyThreadActions(existing, chapterNumber, actions);
|
|
132
|
+
await saveThreads(projectPath, next);
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface UpdateThreadPatch {
|
|
137
|
+
status?: ThreadStatus;
|
|
138
|
+
plannedPayoffAt?: number | null;
|
|
139
|
+
paidOffAt?: number | null;
|
|
140
|
+
droppedAt?: number | null;
|
|
141
|
+
description?: string;
|
|
142
|
+
notes?: string | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function updateThread(
|
|
146
|
+
projectPath: string,
|
|
147
|
+
id: string,
|
|
148
|
+
patch: UpdateThreadPatch
|
|
149
|
+
): Promise<Thread> {
|
|
150
|
+
const existing = await loadThreads(projectPath);
|
|
151
|
+
const target = existing.find((t) => t.id === id);
|
|
152
|
+
if (!target) throw new Error(`Thread not found: ${id}`);
|
|
153
|
+
if (patch.status) target.status = patch.status;
|
|
154
|
+
if (patch.description) target.description = patch.description.trim();
|
|
155
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'plannedPayoffAt')) {
|
|
156
|
+
if (patch.plannedPayoffAt === null) delete target.plannedPayoffAt;
|
|
157
|
+
else if (typeof patch.plannedPayoffAt === 'number') target.plannedPayoffAt = patch.plannedPayoffAt;
|
|
158
|
+
}
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'paidOffAt')) {
|
|
160
|
+
if (patch.paidOffAt === null) delete target.paidOffAt;
|
|
161
|
+
else if (typeof patch.paidOffAt === 'number') target.paidOffAt = patch.paidOffAt;
|
|
162
|
+
}
|
|
163
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'droppedAt')) {
|
|
164
|
+
if (patch.droppedAt === null) delete target.droppedAt;
|
|
165
|
+
else if (typeof patch.droppedAt === 'number') target.droppedAt = patch.droppedAt;
|
|
166
|
+
}
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'notes')) {
|
|
168
|
+
if (patch.notes === null) delete target.notes;
|
|
169
|
+
else if (typeof patch.notes === 'string') target.notes = patch.notes;
|
|
170
|
+
}
|
|
171
|
+
await saveThreads(projectPath, existing);
|
|
172
|
+
return target;
|
|
173
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -1,28 +1,60 @@
|
|
|
1
1
|
export type WorkflowStep =
|
|
2
2
|
| 'novel_metadata'
|
|
3
3
|
| 'story_bible'
|
|
4
|
+
| 'style_guide'
|
|
4
5
|
| 'architecture'
|
|
6
|
+
| 'architecture_extension'
|
|
5
7
|
| 'chapter'
|
|
6
8
|
| 'memory_card'
|
|
7
9
|
| 'continuity_review'
|
|
8
10
|
| 'chapter_review'
|
|
9
11
|
| 'chapter_revision'
|
|
10
12
|
| 'cross_chapter_review'
|
|
13
|
+
| 'story_bible_amend'
|
|
11
14
|
| 'complete';
|
|
12
15
|
|
|
13
16
|
export type ReviewSeverity = 'low' | 'medium' | 'high';
|
|
14
17
|
|
|
15
18
|
export interface ChapterReviewIssue {
|
|
16
19
|
severity: ReviewSeverity;
|
|
17
|
-
category:
|
|
20
|
+
category:
|
|
21
|
+
| 'character'
|
|
22
|
+
| 'world'
|
|
23
|
+
| 'timeline'
|
|
24
|
+
| 'item'
|
|
25
|
+
| 'knowledge'
|
|
26
|
+
| 'pacing'
|
|
27
|
+
| 'style'
|
|
28
|
+
| 'architecture'
|
|
29
|
+
| 'plot'
|
|
30
|
+
| 'foreshadow'
|
|
31
|
+
| 'hook'
|
|
32
|
+
| 'repetition';
|
|
18
33
|
description: string;
|
|
19
34
|
evidence: string;
|
|
20
35
|
suggestion: string;
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
export interface ChapterAcceptanceCheck {
|
|
39
|
+
status: 'pass' | 'fail';
|
|
40
|
+
evidence: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ChapterAcceptanceGate {
|
|
44
|
+
requiredBeats: ChapterAcceptanceCheck & { missingBeats: string[] };
|
|
45
|
+
narrativeProgress: ChapterAcceptanceCheck;
|
|
46
|
+
characterProgress: ChapterAcceptanceCheck;
|
|
47
|
+
foreshadowProgress: ChapterAcceptanceCheck;
|
|
48
|
+
storyBibleConsistency: ChapterAcceptanceCheck;
|
|
49
|
+
proseRhythm: ChapterAcceptanceCheck;
|
|
50
|
+
endingHook: ChapterAcceptanceCheck;
|
|
51
|
+
repetition: ChapterAcceptanceCheck;
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
export interface ChapterReview {
|
|
24
55
|
chapterNumber: number;
|
|
25
56
|
status: 'clean' | 'issues_found';
|
|
57
|
+
acceptance: ChapterAcceptanceGate;
|
|
26
58
|
issues: ChapterReviewIssue[];
|
|
27
59
|
}
|
|
28
60
|
|
|
@@ -53,6 +85,23 @@ export interface NovelMetadata {
|
|
|
53
85
|
coreCast: CoreCastMember[];
|
|
54
86
|
}
|
|
55
87
|
|
|
88
|
+
export interface StyleGuide {
|
|
89
|
+
narrativeVoice: string;
|
|
90
|
+
pacing: string;
|
|
91
|
+
diction: string;
|
|
92
|
+
dialogueRules: string[];
|
|
93
|
+
prohibitedPatterns: string[];
|
|
94
|
+
proseRhythm: {
|
|
95
|
+
sentenceRhythm: string;
|
|
96
|
+
paragraphing: string;
|
|
97
|
+
interiorityMode: string;
|
|
98
|
+
emphasisBudget: string;
|
|
99
|
+
antiPatterns: string[];
|
|
100
|
+
};
|
|
101
|
+
sampleParagraph: string;
|
|
102
|
+
consistencyChecks: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
export interface VolumeArchitecture {
|
|
57
106
|
id: string;
|
|
58
107
|
title: string;
|
|
@@ -60,20 +109,95 @@ export interface VolumeArchitecture {
|
|
|
60
109
|
order: number;
|
|
61
110
|
}
|
|
62
111
|
|
|
112
|
+
export interface VolumePacingBoard {
|
|
113
|
+
volumeId: string;
|
|
114
|
+
start: string;
|
|
115
|
+
promise: string;
|
|
116
|
+
keyTurns: string[];
|
|
117
|
+
midpoint: string;
|
|
118
|
+
climax: string;
|
|
119
|
+
payoffs: string[];
|
|
120
|
+
lingeringMysteries: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type EndHookFocus = 'cliffhanger' | 'mystery' | 'emotional' | 'reveal' | 'volume_close' | 'gentle';
|
|
124
|
+
|
|
63
125
|
export interface ChapterArchitecture {
|
|
64
126
|
chapterNumber: number;
|
|
65
127
|
title: string;
|
|
66
128
|
volumeId: string;
|
|
67
129
|
summary: string;
|
|
68
130
|
requiredBeats: string[];
|
|
131
|
+
targetWords?: number;
|
|
132
|
+
requireRecap?: boolean;
|
|
133
|
+
endHookFocus?: EndHookFocus;
|
|
134
|
+
povCharacter?: string;
|
|
69
135
|
}
|
|
70
136
|
|
|
71
137
|
export interface ArchitecturePayload {
|
|
72
138
|
full: string;
|
|
73
139
|
volumes: VolumeArchitecture[];
|
|
140
|
+
volumePacing?: VolumePacingBoard[];
|
|
141
|
+
chapters: ChapterArchitecture[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ArchitectureExtensionPayload {
|
|
145
|
+
fullUpdate?: string;
|
|
146
|
+
volumes?: VolumeArchitecture[];
|
|
147
|
+
volumePacing?: VolumePacingBoard[];
|
|
74
148
|
chapters: ChapterArchitecture[];
|
|
75
149
|
}
|
|
76
150
|
|
|
151
|
+
export type ThreadStatus = 'planted' | 'building' | 'paid' | 'dropped';
|
|
152
|
+
|
|
153
|
+
export type ThreadActionKind = 'plant' | 'build' | 'pay' | 'drop';
|
|
154
|
+
|
|
155
|
+
export interface ThreadAction {
|
|
156
|
+
kind: ThreadActionKind;
|
|
157
|
+
threadId?: string; // existing thread id; required for build/pay/drop
|
|
158
|
+
description: string; // for plant: the new thread description; for others: how this chapter touched it
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface Thread {
|
|
162
|
+
id: string;
|
|
163
|
+
description: string;
|
|
164
|
+
status: ThreadStatus;
|
|
165
|
+
plantedAt: number;
|
|
166
|
+
lastTouchedAt: number;
|
|
167
|
+
plannedPayoffAt?: number;
|
|
168
|
+
paidOffAt?: number;
|
|
169
|
+
droppedAt?: number;
|
|
170
|
+
notes?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface CharacterRelationshipState {
|
|
174
|
+
name: string;
|
|
175
|
+
dynamic: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface CharacterState {
|
|
179
|
+
name: string;
|
|
180
|
+
role?: string;
|
|
181
|
+
goal: string;
|
|
182
|
+
belief: string;
|
|
183
|
+
relationships: CharacterRelationshipState[];
|
|
184
|
+
abilities: string[];
|
|
185
|
+
secrets: string[];
|
|
186
|
+
emotionalState: string;
|
|
187
|
+
lastUpdatedAt: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface CharacterStateUpdate {
|
|
191
|
+
name: string;
|
|
192
|
+
role?: string;
|
|
193
|
+
goal?: string;
|
|
194
|
+
belief?: string;
|
|
195
|
+
relationships?: CharacterRelationshipState[];
|
|
196
|
+
abilities?: string[];
|
|
197
|
+
secrets?: string[];
|
|
198
|
+
emotionalState?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
77
201
|
export interface MemoryCard {
|
|
78
202
|
summary: string;
|
|
79
203
|
keyEvents: string[];
|
|
@@ -81,10 +205,14 @@ export interface MemoryCard {
|
|
|
81
205
|
facts: Array<{ subject: string; predicate: string; object: string }>;
|
|
82
206
|
stateChanges: Array<{ entity: string; before: string; after: string }>;
|
|
83
207
|
openThreads: string[];
|
|
208
|
+
wordCount?: number;
|
|
209
|
+
threadActions?: ThreadAction[];
|
|
210
|
+
characterUpdates?: CharacterStateUpdate[];
|
|
84
211
|
}
|
|
85
212
|
|
|
86
213
|
export interface PendingAction {
|
|
87
214
|
step: 'chapter_review' | 'chapter_revision' | 'cross_chapter_review';
|
|
215
|
+
mode?: 'side_track' | 'gate';
|
|
88
216
|
chapterNumber?: number;
|
|
89
217
|
range?: { start: number; end: number };
|
|
90
218
|
feedback?: string;
|
|
@@ -95,7 +223,12 @@ export interface AgentState {
|
|
|
95
223
|
projectPath: string;
|
|
96
224
|
initialPrompt: string;
|
|
97
225
|
language: 'zh-CN' | 'en-US';
|
|
226
|
+
/**
|
|
227
|
+
* Number of chapters to plan in each architecture batch.
|
|
228
|
+
* The whole-book target lives in plannedTotalChapters.
|
|
229
|
+
*/
|
|
98
230
|
targetChapters: number;
|
|
231
|
+
plannedTotalChapters: number;
|
|
99
232
|
currentStep: WorkflowStep;
|
|
100
233
|
currentChapter: number;
|
|
101
234
|
completedSteps: WorkflowStep[];
|
package/src/core/workflow.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface RequestSideTrackInput {
|
|
|
37
37
|
type ContextRecipe = (state: AgentState) => Omit<BuildContextInput, 'projectPath'>;
|
|
38
38
|
|
|
39
39
|
const CONTEXT_RECIPES: Partial<Record<WorkflowStep, ContextRecipe>> = {
|
|
40
|
+
style_guide: () => ({ purpose: 'style_guide' }),
|
|
41
|
+
architecture_extension: (s) => ({ purpose: 'architecture_extension', chapterNumber: s.currentChapter }),
|
|
40
42
|
chapter: (s) => ({ purpose: 'chapter_generation', chapterNumber: s.currentChapter }),
|
|
41
43
|
memory_card: (s) => ({ purpose: 'memory_extraction', chapterNumber: s.currentChapter }),
|
|
42
44
|
continuity_review: () => ({ purpose: 'continuity_review' }),
|
|
@@ -132,17 +134,17 @@ function buildPendingAction(state: AgentState, input: RequestSideTrackInput): Pe
|
|
|
132
134
|
switch (input.step) {
|
|
133
135
|
case 'chapter_review': {
|
|
134
136
|
if (!input.chapterNumber) throw new Error('chapter_review requires chapterNumber');
|
|
135
|
-
return { step: 'chapter_review', chapterNumber: input.chapterNumber };
|
|
137
|
+
return { step: 'chapter_review', mode: 'side_track', chapterNumber: input.chapterNumber };
|
|
136
138
|
}
|
|
137
139
|
case 'chapter_revision': {
|
|
138
140
|
if (!input.chapterNumber) throw new Error('chapter_revision requires chapterNumber');
|
|
139
|
-
return { step: 'chapter_revision', chapterNumber: input.chapterNumber, feedback: input.feedback };
|
|
141
|
+
return { step: 'chapter_revision', mode: 'side_track', chapterNumber: input.chapterNumber, feedback: input.feedback };
|
|
140
142
|
}
|
|
141
143
|
case 'cross_chapter_review': {
|
|
142
144
|
const max = maxExistingChapter(state);
|
|
143
145
|
const range = input.range ?? { start: 1, end: max || state.currentChapter };
|
|
144
146
|
if (range.start < 1 || range.end < range.start) throw new Error('Invalid range');
|
|
145
|
-
return { step: 'cross_chapter_review', range };
|
|
147
|
+
return { step: 'cross_chapter_review', mode: 'side_track', range };
|
|
146
148
|
}
|
|
147
149
|
default:
|
|
148
150
|
throw new Error(`Unknown side-track step: ${(input as RequestSideTrackInput).step}`);
|