Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c442a9a
fix(planner): support chapter range outline anchors
Mar 31, 2026
2061c7e
fix(planner): harden exact and range outline matching
Mar 31, 2026
b9c05e1
fix(planner): demote stale current focus below outline anchors
Mar 31, 2026
6be56cf
feat(planner): emit structured writing directives
Mar 31, 2026
8a05275
feat(governed): inject explicit history and canon evidence into writer
Mar 31, 2026
db55742
fix(hooks): promote stale debt and explicit hook agenda in writer
Mar 31, 2026
c5af09f
fix(titles): regenerate duplicates before suffix fallback
Mar 31, 2026
1fa53d3
feat(planner): emit mood directive when consecutive chapters are high…
Mar 31, 2026
7316ed7
feat(style): extract writing style in fanfic/continuation/import flows
Mar 31, 2026
5f187f9
feat(review): reject rolls back state and discards downstream chapters
Mar 31, 2026
48ea3e3
fix(review): preserve committed snapshots on approval
Mar 31, 2026
f9faefc
fix(style): degrade gracefully when fingerprint extraction fails
Mar 31, 2026
b98bc78
fix(studio): persist manual theme selection
Mar 31, 2026
b688531
fix(studio): reload latest llm config per request
Mar 31, 2026
bbb059b
fix(architect): tolerate section label format drift
Mar 31, 2026
2e2264f
fix(pipeline): recover from state validation failures
Apr 1, 2026
e689a83
feat(cli): expose degraded-state recovery flow
Apr 1, 2026
aeb64b6
feat(llm): support custom HTTP headers via INKOS_LLM_HEADERS env var
Apr 1, 2026
d6855e4
feat(pipeline): add semantic hook lifecycle guidance
Apr 1, 2026
6b12293
feat(pipeline): add structured hook pressure map
Apr 1, 2026
c608cd3
feat(pipeline): unify chapter cadence pressure analysis
Apr 1, 2026
2638f22
fix(pipeline): isolate audit drift guidance from state
Apr 1, 2026
f0110e8
feat(pipeline): align hook audit with lifecycle semantics
Apr 1, 2026
198cc94
feat(pipeline): make hook agenda pressure-aware
Apr 1, 2026
3f85287
feat(pipeline): make hook retrieval lifecycle-aware
Apr 1, 2026
ba7f7c5
refactor(pipeline): unify legacy context windows
Apr 1, 2026
2b91181
refactor(pipeline): centralize hook policy constants
Apr 1, 2026
82c2842
refactor(pipeline): align hook activity thresholds
Apr 1, 2026
822708e
refactor(prompt): soften governed creative guidance
Apr 1, 2026
71a860e
refactor(pipeline): centralize cadence pressure policy
Apr 1, 2026
b80056e
refactor(prompt): naturalize hook debt briefs
Apr 1, 2026
f304400
feat(pipeline): auto-repair collapsed chapter titles
Apr 1, 2026
ef7d4eb
refactor(pipeline): extract hook agenda from memory retrieval
Apr 1, 2026
616f807
refactor(pipeline): extract chapter state recovery
Apr 1, 2026
4248e1d
refactor(prompt): compact governed writer control surface
Apr 1, 2026
eb288b3
refactor(prompt): share compact governed process briefs
Apr 1, 2026
06070ca
refactor(state): share story markdown parsers
Apr 1, 2026
2a5ab2c
refactor(pipeline): extract persisted governed plan loader
Apr 1, 2026
6759867
fix(test): update hook table assertions for payoff_timing column
Apr 1, 2026
e2bf6cb
refactor(pipeline): extract chapter review cycle
Apr 1, 2026
f989204
refactor(pipeline): extract chapter persistence tail
Apr 1, 2026
16cbf27
refactor(pipeline): extract chapter truth validation
Apr 1, 2026
86ea01a
revert: undo prompt softening and compaction (iter-002 regression fix)
Apr 2, 2026
fb8e1aa
fix(pipeline): exclude sequence-level fatigue from revision blockingC…
Apr 2, 2026
f4ee25b
fix(writer): override LLM-hallucinated chapter numbers in state delta
Apr 2, 2026
b4f5cc6
fix(pipeline): scope revision blockers and normalize delta chapters
Apr 2, 2026
66bb890
feat(hooks): replace lifecycle pressure system with seed excerpt appr…
Apr 2, 2026
2e64055
fix(state): anchor chapter progress to contiguous durable chapters
Apr 2, 2026
47512e7
merge: integrate writing-pipeline-systemic-repair into master
Apr 2, 2026
54d090c
fix(state): close remaining chapter-number pollution paths
Apr 2, 2026
b4f8e2d
feat(pipeline): add hook budget hint and chapter ending trail
Apr 3, 2026
160f3a2
feat(architect): add foundation reviewer and new-spacetime requirements
Apr 3, 2026
e771dee
fix(pipeline): wire foundation review retries and hook budget
Apr 3, 2026
90a26ea
chore: bump version to 1.1.0, update CHANGELOG and README
Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# Changelog

## v1.1.0

写作管线全面升级。通过 Meta-Harness 方法论驱动的多轮 autoresearch 实验,从零模式质量从 75 分提升至 92 分,同人模式从 39 分提升至 82+ 分。

### 新功能

- **Foundation Reviewer**:建书时新增独立审核 Agent,5 维度百分制打分(原作 DNA 保留、新叙事空间、核心冲突、开篇节奏、节奏可行性),不达 80 分自动驳回并将审核意见反馈给 Architect 重新生成
- **新时空要求**:同人模式(canon/au/ooc/cp)必须设计原创分岔点,不允许复述原作剧情。要求明确分岔点、独立核心冲突、5 章内引爆、50% 新鲜场景
- **Hook Seed Excerpt**:伏笔回收时,Composer 从 chapter_summaries 提取原始种子场景的原文片段注入 Writer 上下文,Writer 基于具体叙事素材写回收场景,而非对着 hook ID 干猜。替代了复杂的 lifecycle pressure 系统
- **Review Reject 回滚**:`inkos review reject` 现在回滚 state 到被拒章节之前的快照,丢弃下游章节和记忆索引,防止坏草稿污染后续生成
- **State Validation Recovery**:state 校验失败时自动重试 settler(带 validation 反馈),仍失败则降级保存(正文保留,state 不推进),支持 `inkos write repair-state` 手动修复
- **Audit Drift 隔离**:审计纠偏写入独立的 `audit_drift.md`,不再追加到 `current_state.md`,防止 settler 把审计元数据当叙事事实复述到正文
- **标题坍缩修复**:检测近期标题的主题聚集(如连续 3 个标题含"盘"),尝试从章节正文提取新关键词重生标题
- **Hook 预算提示**:活跃伏笔 ≥10 时在 Hook Agenda 中显示预算警告,引导 Writer 优先回收旧债
- **章节结尾摘要**:Composer 提取最近 3 章的结尾句注入上下文,防止结构性重复(如连续 4 章以"被埋在废墟下"结尾)
- **情绪/节奏检测**:long-span-fatigue 新增 mood 单调和标题聚集检测,序列级 warning 不计入修订 blockingCount
- **同人风格提取**:`fanfic init` 和 `import chapters` 自动生成 `style_guide.md` + `style_profile.json`
- **Governed 路径补全**:续写/同人的 `parent_canon.md` 和 `fanfic_canon.md` 现在通过 Governed 路径注入 Writer

### Bug Fixes

- **章节号污染修复**:叙事文本中的数字(如"第 141 号文明"、"1988 年")不再被误解析为章节进度。章节号唯一权威来源为连续的章节文件 + index.json,markdown 数字不参与进度计算
- **hook 排序修复**:`mustAdvance` 从降序(选最近推进的)修正为升序(选最久未推进的)
- **Outline 匹配修复**:`findOutlineNode` 支持章节范围格式(如"Chapter 1-20"、"第 1-20 章"),防止 Chapter 1 误匹配 Chapter 10
- **deriveGoal 优先级修正**:outline 节点优先于 current_focus,用户可通过 `## Local Override` 段显式覆盖
- **approve 不覆盖快照**:`review approve` 不再重新 snapshot,保护 reject 回滚的目标快照
- **style 提取 graceful degrade**:风格指纹提取失败不中断建书/导入流程
- **LLM Headers 支持**:`INKOS_LLM_HEADERS` 环境变量注入自定义 HTTP 头(如 User-Agent),解决部分 API 提供方的 403 问题

### 内部改进

- Planner structured directives:arc/scene/mood/title 四维预写指令
- Chapter cadence 统一分析模块
- Runner 大文件拆分:chapter-state-recovery、chapter-review-cycle、chapter-persistence、chapter-truth-validation、persisted-governed-plan 独立模块
- story-markdown 共享解析器
- Hook agenda 独立模块(从 memory-retrieval 提取)

---

## v1.0.2

### Bug Fixes
Expand Down
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,21 @@ inkos export 吞天魔帝 --format epub # 导出 EPUB(手机/Kindle 阅读)

## 核心特性

### 基础设定审核 (v1.1.0)

建书时新增独立的 Foundation Reviewer Agent。Architect 生成基础设定后,Reviewer 从 5 个维度百分制评审(原作 DNA 保留、新叙事空间、核心冲突、开篇节奏、节奏可行性),不达 80 分自动驳回,将审核意见反馈给 Architect 重新生成。同人/系列模式强制要求原创分岔点——不允许复述原作剧情。

### 伏笔种子回收 (v1.1.0)

伏笔回收时,系统从 chapter_summaries 提取每个待回收伏笔的**原始种子场景原文**注入 Writer 上下文。Writer 看到"种于第 2 章:萧炎右手碰到戒面时,有一丝温热渗出",就能写出接续性的回收场景,而非对着 hook ID 敷衍。

### 审核后回写 (v1.1.0)

`inkos review reject` 回滚 state 到被拒章节之前的快照,丢弃下游章节和记忆索引。审计不通过的章节阻止继续写下一章,防止坏草稿污染后续生成。`inkos write repair-state` 可手动修复降级章节。

### 多维度审计 + 去 AI 味

连续性审计员从 33 个维度检查每一章草稿:角色记忆、物资连续性、伏笔回收、大纲偏离、叙事节奏、情感弧线等。内置 AI 痕迹检测维度,自动识别"LLM 味"表达(高频词、句式单调、过度总结),审计不通过自动进入修订循环。
连续性审计员从 33 个维度检查每一章草稿:角色记忆、物资连续性、伏笔回收、大纲偏离、叙事节奏、情感弧线等。内置 AI 痕迹检测维度,自动识别"LLM 味"表达(高频词、句式单调、过度总结),审计不通过自动进入修订循环。新增跨章情绪单调、标题聚集、章节结尾重复检测。

去 AI 味规则内置于写手 agent 的 prompt 层——词汇疲劳词表、禁用句式、文风指纹注入,从源头减少 AI 生成痕迹。`revise --mode anti-detect` 可对已有章节做专门的反检测改写。

Expand Down Expand Up @@ -174,13 +186,13 @@ inkos compose chapter 吞天魔帝
- 如果正文超出允许区间,InkOS 最多只会追加 1 次纠偏归一化(压缩或补足),不会直接硬截断正文
- 如果 1 次纠偏后仍然超出 hard range,章节照常保存,但会在结果和 chapter index 里留下长度 warning / telemetry

### 续写已有作品
### 续写已有作品 / 系列

`inkos import chapters` 从已有小说文本导入章节,自动逆向工程 7 个真相文件(世界状态、角色矩阵、资源账本、伏笔钩子等),支持 `第X章` 和自定义分割模式、断点续导。导入后 `inkos write next` 无缝接续创作。
`inkos import chapters` 从已有小说文本导入章节,自动逆向工程 7 个真相文件(世界状态、角色矩阵、资源账本、伏笔钩子等),支持 `第X章` 和自定义分割模式、断点续导。导入后自动生成原作风格指纹(`style_guide.md`),`inkos write next` 无缝接续创作。续写/系列/前传均可——基于同一世界观写独立新故事

### 同人创作

`inkos fanfic init --from source.txt --mode canon` 从原作素材创建同人书。支持四种模式:canon(正典延续)、au(架空世界)、ooc(性格重塑)、cp(CP 向)。内置正典导入器、同人专属审计维度和信息边界管控——确保设定不矛盾
`inkos fanfic init --from source.txt --mode canon` 从原作素材创建同人书。支持四种模式:canon(正典延续)、au(架空世界)、ooc(性格重塑)、cp(CP 向)。v1.1.0 起强制要求**新时空设定**——必须设计原创分岔点和独立核心冲突,不允许复述原作剧情。内置正典导入器、同人专属审计维度、信息边界管控和自动风格仿写

### 多模型路由

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@actalk/inkos",
"version": "1.0.2",
"version": "1.1.0",
"description": "Autonomous AI novel writing CLI agent — 10-agent pipeline that writes, audits, and revises novels with continuity tracking. Supports LitRPG, Progression Fantasy, Isekai, Romantasy, Sci-Fi and more.",
"keywords": [
"ai-novel-writing",
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ describe("computeAnalytics", () => {
expect(result.auditPassRate).toBe(67);
});

it("counts state-degraded chapters as audited but not passed", () => {
const chapters = [
{ number: 1, status: "approved", wordCount: 3000, auditIssues: [] },
{ number: 2, status: "state-degraded", wordCount: 2800, auditIssues: ["[warning] state validation drift"] },
{ number: 3, status: "drafted", wordCount: 2600, auditIssues: [] },
];
const result = computeAnalytics("book-state-degraded", chapters);
expect(result.auditPassRate).toBe(50);
expect(result.statusDistribution).toEqual({
approved: 1,
"state-degraded": 1,
drafted: 1,
});
});

it("extracts issue categories from formatted strings", () => {
const chapters = [
{
Expand Down
113 changes: 113 additions & 0 deletions packages/cli/src/__tests__/cli-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,50 @@ describe("CLI integration", () => {
expect(output).not.toContain("7字");
});

it("shows degraded chapter counts and issues explicitly", async () => {
const bookDir = join(projectDir, "books", "degraded-status");
await mkdir(join(bookDir, "chapters"), { recursive: true });
await writeFile(
join(bookDir, "book.json"),
JSON.stringify({
id: "degraded-status",
title: "Degraded Status Book",
platform: "other",
genre: "other",
status: "active",
targetChapters: 10,
chapterWordCount: 2200,
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
}, null, 2),
"utf-8",
);
await writeFile(
join(bookDir, "chapters", "index.json"),
JSON.stringify([
{
number: 1,
title: "Broken State",
status: "state-degraded",
wordCount: 1800,
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
auditIssues: ["[warning] state validation still failed after retry"],
lengthWarnings: [],
},
], null, 2),
"utf-8",
);

const output = run(["status", "degraded-status", "--chapters"]);
expect(output).toContain("Degraded: 1");
expect(output).toContain('Ch.1 "Broken State" | 1800字 | state-degraded');
expect(output).toContain("[warning] state validation still failed after retry");

const json = JSON.parse(run(["status", "degraded-status", "--json"]));
expect(json.books[0]?.degraded).toBe(1);
});

it("shows a migration hint for legacy pre-v0.6 books", async () => {
const bookDir = join(projectDir, "books", "legacy-status-hint");
const storyDir = join(bookDir, "story");
Expand Down Expand Up @@ -620,6 +664,75 @@ describe("CLI integration", () => {
});
});

describe("inkos review", () => {
it("preserves the original chapter snapshot when approving review", async () => {
const configPath = join(projectDir, "inkos.json");
const initialized = await stat(configPath).then(() => true).catch(() => false);
if (!initialized) run(["init"]);

const state = new StateManager(projectDir);
const bookId = "review-approve-cli";
const bookDir = join(projectDir, "books", bookId);
const storyDir = join(bookDir, "story");
const chaptersDir = join(bookDir, "chapters");
await mkdir(chaptersDir, { recursive: true });
await mkdir(storyDir, { recursive: true });

await writeFile(
join(bookDir, "book.json"),
JSON.stringify({
id: bookId,
title: "Review Approve CLI",
platform: "other",
genre: "other",
status: "active",
targetChapters: 10,
chapterWordCount: 2200,
createdAt: "2026-03-22T00:00:00.000Z",
updatedAt: "2026-03-22T00:00:00.000Z",
}, null, 2),
"utf-8",
);
await writeFile(join(storyDir, "current_state.md"), "State at ch1", "utf-8");
await writeFile(join(storyDir, "pending_hooks.md"), "Hooks at ch1", "utf-8");
await writeFile(join(chaptersDir, "0001_ch1.md"), "# Chapter 1\n\nContent 1", "utf-8");
await writeFile(
join(chaptersDir, "index.json"),
JSON.stringify([
{
number: 1,
title: "Ch1",
status: "ready-for-review",
wordCount: 100,
createdAt: "",
updatedAt: "",
auditIssues: [],
lengthWarnings: [],
},
], null, 2),
"utf-8",
);

await state.snapshotState(bookId, 1);

await writeFile(join(storyDir, "current_state.md"), "State at ch3", "utf-8");
await writeFile(join(storyDir, "pending_hooks.md"), "Hooks at ch3", "utf-8");

const output = run(["review", "approve", bookId, "1"]);
expect(output).toContain("Chapter 1 approved");

await expect(
readFile(join(storyDir, "snapshots", "1", "current_state.md"), "utf-8"),
).resolves.toBe("State at ch1");
await expect(
readFile(join(storyDir, "snapshots", "1", "pending_hooks.md"), "utf-8"),
).resolves.toBe("Hooks at ch1");

const index = await state.loadChapterIndex(bookId);
expect(index[0]?.status).toBe("approved");
});
});

describe("inkos plan/compose", () => {
beforeAll(async () => {
const configPath = join(projectDir, "inkos.json");
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export const upCommand = new Command("up")
cooldownAfterChapterMs: config.daemon.cooldownAfterChapterMs,
maxChaptersPerDay: config.daemon.maxChaptersPerDay,
onChapterComplete: (bookId, chapter, status) => {
const icon = status === "ready-for-review" ? "+" : "!";
const icon = status === "ready-for-review"
? "+"
: status === "state-degraded"
? "x"
: "!";
log(` [${icon}] ${bookId} Ch.${chapter} — ${status}`);
},
onError: (bookId, error) => {
Expand Down
52 changes: 39 additions & 13 deletions packages/cli/src/commands/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function parseBookAndChapter(

reviewCommand
.command("approve")
.description("Approve a chapter: approve [book-id] <chapter>")
.description("Approve a chapter and commit its state: approve [book-id] <chapter>")
.argument("<args...>", "Book ID (optional) and chapter number")
.option("--json", "Output JSON")
.action(async (args: ReadonlyArray<string>, opts) => {
Expand All @@ -132,7 +132,7 @@ reviewCommand
if (opts.json) {
log(JSON.stringify({ bookId, chapter: chapterNum, status: "approved" }));
} else {
log(`Chapter ${chapterNum} approved.`);
log(`Chapter ${chapterNum} approved (state committed).`);
}
} catch (e) {
if (opts.json) {
Expand Down Expand Up @@ -186,9 +186,10 @@ reviewCommand

reviewCommand
.command("reject")
.description("Reject a chapter: reject [book-id] <chapter>")
.description("Reject a chapter and roll back state: reject [book-id] <chapter>")
.argument("<args...>", "Book ID (optional) and chapter number")
.option("--reason <reason>", "Rejection reason")
.option("--keep-subsequent", "Only reject this chapter, do not discard subsequent chapters")
.option("--json", "Output JSON")
.action(async (args: ReadonlyArray<string>, opts) => {
try {
Expand All @@ -197,24 +198,49 @@ reviewCommand
const bookId = await resolveBookId(bookIdArg, root);

const state = new StateManager(root);
const index = [...(await state.loadChapterIndex(bookId))];
const index = await state.loadChapterIndex(bookId);
const idx = index.findIndex((ch) => ch.number === chapterNum);
if (idx === -1) {
throw new Error(`Chapter ${chapterNum} not found in "${bookId}"`);
}

index[idx] = {
...index[idx]!,
status: "rejected",
reviewNote: opts.reason ?? "Rejected without reason",
updatedAt: new Date().toISOString(),
};
await state.saveChapterIndex(bookId, index);
if (opts.keepSubsequent) {
// Legacy behavior: only mark as rejected, no state rollback
const updated = [...index];
updated[idx] = {
...updated[idx]!,
status: "rejected",
reviewNote: opts.reason ?? "Rejected without reason",
updatedAt: new Date().toISOString(),
};
await state.saveChapterIndex(bookId, updated);

if (opts.json) {
log(JSON.stringify({ bookId, chapter: chapterNum, status: "rejected", discarded: [] }));
} else {
log(`Chapter ${chapterNum} rejected (state not rolled back).`);
}
return;
}

// Default: roll back state to before the rejected chapter and discard
// it along with all subsequent chapters that depend on its state.
const rollbackTarget = chapterNum - 1;
const discarded = await state.rollbackToChapter(bookId, rollbackTarget);

if (opts.json) {
log(JSON.stringify({ bookId, chapter: chapterNum, status: "rejected" }));
log(JSON.stringify({
bookId,
chapter: chapterNum,
status: "rejected",
rolledBackTo: rollbackTarget,
discarded,
}));
} else {
log(`Chapter ${chapterNum} rejected.`);
log(`Chapter ${chapterNum} rejected. State rolled back to chapter ${rollbackTarget}.`);
if (discarded.length > 1) {
log(` Also discarded ${discarded.length - 1} subsequent chapter(s): ${discarded.filter((n) => n !== chapterNum).join(", ")}`);
}
}
} catch (e) {
if (opts.json) {
Expand Down
Loading
Loading