Просмотр исходного кода

docs: update README for v0.6.0 and untrack docs/

- Add Planner, Composer, Observer, Reflector, Normalizer to agent table
- Document structured state (story/state/*.json), JSON delta, Zod validation
- Document SQLite memory.db, cross-chapter repetition detection
- Document maxTokens cap and extra key stripping
- Untrack docs/plans/ (already in .gitignore)
Ma 7 месяцев назад
Родитель
Сommit
19db2c839b

+ 17 - 4
README.en.md

@@ -207,13 +207,17 @@ Supports any OpenAI-compatible endpoint (`--provider custom`). Stream auto-fallb
 
 ### Reliability
 
-Every chapter creates an automatic state snapshot — `inkos write rewrite` rolls back any chapter to its pre-write state. The Writer outputs a pre-write checklist (context scope, resources, pending hooks, risks) and a post-write settlement table; the Auditor cross-validates both. File locking prevents concurrent writes. Post-write validator enforces 11 hard rules with auto spot-fix.
+Every chapter creates an automatic state snapshot — `inkos write rewrite` rolls back any chapter to its pre-write state. The Writer outputs a pre-write checklist (context scope, resources, pending hooks, risks) and a post-write settlement table; the Auditor cross-validates both. File locking prevents concurrent writes. Post-write validator includes cross-chapter repetition detection and 11 hard rules with auto spot-fix.
+
+The hook system uses Zod schema validation — `lastAdvancedChapter` must be an integer, `status` can only be open/progressing/deferred/resolved. JSON deltas from the LLM are processed through `applyRuntimeStateDelta` (immutable update) and `validateRuntimeState` (structural check) before persistence. Corrupted data is rejected, not propagated.
+
+User-configured `INKOS_LLM_MAX_TOKENS` now acts as a global cap on all API calls. Reserved keys in `llm.extra` (max_tokens, temperature, etc.) are automatically stripped to prevent accidental overrides.
 
 ---
 
 ## How It Works
 
-Each chapter is produced by five agents in sequence:
+Each chapter is produced by multiple agents in sequence, with zero human intervention:
 
 <p align="center">
   <img src="assets/screenshot-pipeline.png" width="800" alt="Pipeline diagram">
@@ -221,9 +225,14 @@ Each chapter is produced by five agents in sequence:
 
 | Agent | Responsibility |
 |-------|---------------|
-| **Radar** | Scans platform trends and reader preferences to inform story direction. Pluggable via `RadarSource` interface — skip it or bring your own data source |
+| **Radar** | Scans platform trends and reader preferences to inform story direction (pluggable, skippable) |
+| **Planner** | Reads author intent + current focus + memory retrieval results, produces chapter intent (must-keep / must-avoid) |
+| **Composer** | Selects relevant context from all truth files by relevance, compiles rule stack and runtime artifacts |
 | **Architect** | Plans chapter structure: outline, scene beats, pacing targets |
-| **Writer** | Produces prose from the plan + current world state (two-phase: creative writing → state settlement) |
+| **Writer** | Produces prose from the composed context (length-governed, dialogue-driven) |
+| **Observer** | Over-extracts 9 categories of facts from the chapter text (characters, locations, resources, relationships, emotions, information, hooks, time, physical state) |
+| **Reflector** | Outputs a JSON delta (not full markdown); code-layer applies Zod schema validation then immutable write |
+| **Normalizer** | Single-pass compress/expand to bring chapter length into the target band |
 | **Continuity Auditor** | Validates the draft against 7 canonical truth files, 33-dimension check |
 | **Reviser** | Fixes issues found by the auditor — auto-fixes critical problems, flags others for human review |
 
@@ -245,6 +254,10 @@ Every book maintains 7 truth files as the single source of truth:
 
 The Continuity Auditor checks every draft against these files. If a character "remembers" something they never witnessed, or pulls a weapon they lost two chapters ago, the auditor catches it.
 
+Since 0.6.0, the authoritative source for truth files has moved from markdown to `story/state/*.json` (Zod schema validated). The Settler no longer outputs full markdown files — it produces a JSON delta that is immutably applied and structurally validated before persistence. Markdown files are retained as human-readable projections. Existing books auto-migrate on first run.
+
+On Node 22+, a SQLite temporal memory database (`story/memory.db`) is automatically enabled, supporting relevance-based retrieval of historical facts, hooks, and chapter summaries — preventing context bloat from full-file injection.
+
 <p align="center">
   <img src="assets/screenshot-state.png" width="800" alt="Truth files snapshot">
 </p>

+ 16 - 3
README.md

@@ -177,13 +177,17 @@ inkos compose chapter 吞天魔帝
 
 ### 可靠性保障
 
-每章自动创建状态快照,`inkos write rewrite` 可回滚任意章节。写手动笔前输出自检表(上下文、资源、伏笔、风险),写完输出结算表,审计员交叉验证。文件锁防止并发写入。写后验证器 11 条硬规则自动 spot-fix。
+每章自动创建状态快照,`inkos write rewrite` 可回滚任意章节。写手动笔前输出自检表(上下文、资源、伏笔、风险),写完输出结算表,审计员交叉验证。文件锁防止并发写入。写后验证器含跨章重复检测和 11 条硬规则自动 spot-fix。
+
+伏笔系统使用 Zod schema 校验——`lastAdvancedChapter` 必须是整数,`status` 只能是 open/progressing/deferred/resolved。LLM 输出的 JSON delta 在写入前经过 `applyRuntimeStateDelta` 做 immutable 更新 + `validateRuntimeState` 结构校验。坏数据直接拒绝,不会滚雪球。
+
+用户设置的 `INKOS_LLM_MAX_TOKENS` 作为全局上限生效,`llm.extra` 中的保留键(max_tokens、temperature 等)被自动过滤,防止意外覆盖。
 
 ---
 
 ## 工作原理
 
-每一章由五个 Agent 接力完成:
+每一章由多个 Agent 接力完成,全程零人工干预:
 
 <p align="center">
   <img src="assets/screenshot-pipeline.png" width="800" alt="管线流程图">
@@ -192,8 +196,13 @@ inkos compose chapter 吞天魔帝
 | Agent | 职责 |
 |-------|------|
 | **雷达 Radar** | 扫描平台趋势和读者偏好,指导故事方向(可插拔,可跳过) |
+| **规划师 Planner** | 读取作者意图 + 当前焦点 + 记忆检索结果,产出本章意图(must-keep / must-avoid) |
+| **编排师 Composer** | 从全量真相文件中按相关性选择上下文,编译规则栈和运行时产物 |
 | **建筑师 Architect** | 规划章节结构:大纲、场景节拍、节奏控制 |
-| **写手 Writer** | 根据大纲 + 当前世界状态生成正文(两阶段:创意写作 → 状态结算) |
+| **写手 Writer** | 基于编排后的精简上下文生成正文(字数治理 + 对话引导) |
+| **观察者 Observer** | 从正文中过度提取 9 类事实(角色、位置、资源、关系、情感、信息、伏笔、时间、物理状态) |
+| **反射器 Reflector** | 输出 JSON delta(而非全量 markdown),由代码层做 Zod schema 校验后 immutable 写入 |
+| **归一化器 Normalizer** | 单 pass 压缩/扩展,将章节字数拉入允许区间 |
 | **连续性审计员 Auditor** | 对照 7 个真相文件验证草稿,33 维度检查 |
 | **修订者 Reviser** | 修复审计发现的问题 — 关键问题自动修复,其他标记给人工审核 |
 
@@ -215,6 +224,10 @@ inkos compose chapter 吞天魔帝
 
 连续性审计员对照这些文件检查每一章草稿。如果角色"记起"了从未亲眼见过的事,或者拿出了两章前已经丢失的武器,审计员会捕捉到。
 
+从 0.6.0 起,真相文件的权威来源从 markdown 迁移到 `story/state/*.json`(Zod schema 校验)。Settler 不再输出完整 markdown 文件,而是输出 JSON delta,由代码层做 immutable apply + 结构校验后写入。markdown 文件仍然保留作为人类可读的投影。旧书首次运行时自动从 markdown 迁移到结构化 JSON,零人工操作。
+
+Node 22+ 环境下自动启用 SQLite 时序记忆数据库(`story/memory.db`),支持按相关性检索历史事实、伏笔和章节摘要,避免全量注入导致的上下文膨胀。
+
 <p align="center">
   <img src="assets/screenshot-state.png" width="800" alt="长期记忆快照">
 </p>

+ 0 - 247
docs/plans/2026-03-22-length-governance-design.md

@@ -1,247 +0,0 @@
-# Length Governance Redesign
-
-## Goal
-
-Reduce chapter-length drift in InkOS without resorting to hard truncation, so user-specified targets like `--words 2200` converge toward a reasonable range instead of expanding unpredictably across writer and reviser passes.
-
-## Problem Statement
-
-Issue `#58` is not a single prompt bug. It is a system-level drift problem caused by multiple layers:
-
-1. Writer prompts currently frame length as a lower bound (`不少于 X 字`) instead of a target range.
-2. Writer runtime budgeting uses a loose token ceiling, not an explicit output-governance mechanism.
-3. Length accounting currently uses raw string length, which is not a stable user-facing counting model.
-4. Reviser and spot-fix passes can expand or compress the chapter again, but they do not share a common length guardrail with the writer.
-
-Observed behavior on merged `master` confirmed the drift:
-
-- `draft --words 500` produced `1186`
-- a later revise pass expanded the same chapter to `1805`
-
-This makes length control a combined model-capability and system-governance problem, not something that can be solved by lowering `maxTokens` or chopping the tail off the chapter.
-
-## External Constraint
-
-The 2026 ecosystem direction is consistent:
-
-- model APIs expose upper bounds, not exact output length controls
-- prompt wording alone is not enough for precise length following
-- stable length control requires runtime feedback or structured planning
-
-For InkOS, that means the right fix is an inference-time governor inside the pipeline, not hard truncation.
-
-## Scope
-
-In scope:
-
-- introduce a conservative `LengthSpec`
-- unify counting semantics for Chinese and English
-- enforce the same length policy across writer and reviser
-- add a single-pass length normalization step when output leaves the allowed band
-- record length drift in logs/results
-
-Out of scope:
-
-- exact-length generation
-- paragraph/scene-level budget planning
-- provider-specific fine-tuning
-- hard-cut truncation of finished chapters
-
-## Chosen Approach
-
-Use a conservative runtime governor shared by the full chapter pipeline.
-
-Three approaches were considered:
-
-1. Prompt-only range wording
-2. Conservative runtime governor
-3. Planner-driven scene budgets
-
-Approach `2` is the chosen design because it materially reduces drift without requiring a large planner/writer rewrite. Approach `3` remains the long-term path if tighter control is needed later.
-
-## Design Principles
-
-1. User target length remains authoritative.
-2. The system should aim for a band, not an exact number.
-3. Quality and continuity take priority over exact length.
-4. Normalization is corrective, not recursive.
-5. Counting must match user expectations as closely as possible.
-
-## LengthSpec
-
-Introduce a lightweight runtime object shared by pipeline stages:
-
-```ts
-interface LengthSpec {
-  target: number;
-  softMin: number;
-  softMax: number;
-  hardMin: number;
-  hardMax: number;
-  countingMode: "zh_chars" | "en_words";
-  normalizeMode: "expand" | "compress" | "none";
-}
-```
-
-### Source of Truth
-
-`target` must come from user-visible inputs in this order:
-
-1. `--words`
-2. `book.chapterWordCount`
-3. project default
-
-The system must not invent a target independently of user or book configuration.
-
-### Conservative Defaults
-
-Example for `target = 2200`:
-
-- `softMin = 1900`
-- `softMax = 2500`
-- `hardMin = 1600`
-- `hardMax = 2800`
-
-The exact ratios can be implementation details, but the behavior should remain conservative: pull drift inward without over-correcting.
-
-## Counting Model
-
-Length accounting must stop using raw `.length` as the user-facing truth.
-
-Use:
-
-- `zh_chars` for Chinese writing
-- `en_words` for English writing
-
-Definitions:
-
-- `zh_chars`: count CJK characters as the primary unit and ignore Markdown headings/metadata outside chapter body
-- `en_words`: count word tokens in prose, not raw characters
-
-This gives InkOS a stable public contract for what “2200 字” or “2200 words” actually means.
-
-## Runtime Flow
-
-### 1. Pipeline entry
-
-`PipelineRunner.writeDraft()` and `PipelineRunner.writeNextChapter()` build a `LengthSpec` before invoking writer or reviser.
-
-### 2. Writer pass
-
-Writer receives `LengthSpec` and shifts the prompt contract from:
-
-- `正文不少于 X 字`
-
-to something equivalent to:
-
-- target `X`
-- acceptable range `[softMin, softMax]`
-- keep the chapter complete while staying near the target band
-
-### 3. Post-writer normalization
-
-If the draft lands outside the soft range, the pipeline runs one corrective `normalize-length` pass:
-
-- if too long: compress without deleting required plot beats
-- if too short: expand without adding new branches or consuming future outline nodes
-
-### 4. Audit / revise
-
-Reviser must consume the same `LengthSpec`. Fixes must preserve the target band unless the requested correction genuinely forces a change.
-
-### 5. Post-reviser normalization
-
-If revise/spot-fix pushes the chapter back outside the soft range, run one final normalization pass.
-
-### 6. Final persistence
-
-Persist the chapter even if it still misses the hard range, but emit a visible warning instead of silently pretending the target was met.
-
-## Single Normalize Rule
-
-`normalize-length` is itself an LLM pass, so it must not recurse.
-
-Rule:
-
-- at most one normalization after writer
-- at most one normalization after reviser
-- no loops
-- if a normalization pass still fails to re-enter the desired band, save the chapter and attach a warning
-
-This prevents runaway cost and “rewrite forever to chase a number” behavior.
-
-## Normalizer Responsibilities
-
-The normalizer is not another reviser. It has a narrower contract:
-
-- preserve chapter intent
-- preserve hard facts
-- preserve current plot beats
-- preserve final hook / scene completeness
-- change length only through local compression or elaboration
-
-It must not:
-
-- add a new subplot
-- consume a future reveal
-- retcon facts
-- silently delete a required resolution beat
-
-## Logging and Telemetry
-
-Each chapter should expose a compact length trace:
-
-- `target`
-- `softMin`
-- `softMax`
-- `hardMin`
-- `hardMax`
-- `countingMode`
-- `writerCount`
-- `postWriterNormalizeCount`
-- `postReviseCount`
-- `finalCount`
-- `normalizeApplied`
-- `lengthWarning`
-
-This should be visible enough for debugging and issue triage, even if the full trace stays internal.
-
-## Failure Strategy
-
-Conservative failure policy:
-
-1. outside soft range -> try one normalize pass
-2. still outside soft range -> continue
-3. outside hard range at final save -> persist plus warning
-
-InkOS should not hard-truncate the prose and should not re-run the whole chapter indefinitely.
-
-## Testing Strategy
-
-Minimum coverage:
-
-1. Writer over target triggers normalization
-2. Reviser drift triggers post-revise normalization
-3. Normalizer runs at most once per boundary
-4. Chinese and English counting modes differ correctly
-5. Final hard-range miss records a warning instead of causing destructive fallback
-
-## Rollout
-
-This should ship as default pipeline behavior, not as a separate user feature flag.
-
-Reason:
-
-- issue `#58` affects baseline usability
-- the conservative governor is additive and low risk
-- the system already accepts that length is approximate, so tightening approximation is a compatibility improvement
-
-## Future Work
-
-If the conservative governor is still too loose, the next step is not more prompt tweaking. The next step is scene-level planning:
-
-- planner-generated scene budgets
-- scene-aware writer generation
-- explicit remaining-budget tracking inside the write loop
-
-That is intentionally deferred out of this MVP.

+ 0 - 446
docs/plans/2026-03-22-length-governance-implementation-plan.md

@@ -1,446 +0,0 @@
-# Length Governance Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Add conservative chapter-length governance so InkOS treats user-specified chapter size as a bounded runtime target across writer and reviser passes without hard truncation.
-
-**Architecture:** Introduce a shared `LengthSpec` plus counting helpers, then thread that spec through writer, reviser, and `PipelineRunner`. Add a dedicated `LengthNormalizerAgent` that can run once after writer and once after reviser when output drifts outside the soft range.
-
-**Tech Stack:** TypeScript, Node.js, Commander, Vitest, existing agent pipeline, Markdown chapter output
-
----
-
-### Task 1: Define runtime length-governance types
-
-**Files:**
-- Create: `packages/core/src/models/length-governance.ts`
-- Modify: `packages/core/src/index.ts`
-- Test: `packages/core/src/__tests__/models.test.ts`
-
-**Step 1: Write the failing test**
-
-Add model tests for:
-
-- `LengthSpec`
-- `LengthTelemetry`
-- `LengthWarning`
-
-Example assertion:
-
-```ts
-const spec = LengthSpecSchema.parse({
-  target: 2200,
-  softMin: 1900,
-  softMax: 2500,
-  hardMin: 1600,
-  hardMax: 2800,
-  countingMode: "zh_chars",
-  normalizeMode: "compress",
-});
-
-expect(spec.softMin).toBe(1900);
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/models.test.ts`
-
-Expected: missing imports/schema failures because the length-governance model does not exist yet.
-
-**Step 3: Write minimal implementation**
-
-- Create `LengthSpecSchema`
-- Add lightweight telemetry/warning schemas
-- Export the new types from `packages/core/src/index.ts`
-
-**Step 4: Run test to verify it passes**
-
-Run the same test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/models/length-governance.ts packages/core/src/index.ts packages/core/src/__tests__/models.test.ts
-git commit -m "feat: add length governance models"
-```
-
-### Task 2: Add counting and range helpers
-
-**Files:**
-- Create: `packages/core/src/utils/length-metrics.ts`
-- Create: `packages/core/src/__tests__/length-metrics.test.ts`
-- Modify: `packages/core/src/index.ts`
-
-**Step 1: Write the failing test**
-
-Add tests for:
-
-- Chinese chapter counting uses `zh_chars`
-- English chapter counting uses `en_words`
-- `buildLengthSpec(target, language)` derives conservative soft/hard ranges
-
-Example assertions:
-
-```ts
-expect(countChapterLength("他抬头看天。", "zh_chars")).toBe(6);
-expect(countChapterLength("He looked at the sky.", "en_words")).toBe(5);
-
-const spec = buildLengthSpec(2200, "zh");
-expect(spec.softMin).toBeLessThan(spec.target);
-expect(spec.hardMax).toBeGreaterThan(spec.softMax);
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/length-metrics.test.ts`
-
-Expected: module missing / helper missing failures.
-
-**Step 3: Write minimal implementation**
-
-- Implement `countChapterLength()`
-- Implement `buildLengthSpec()`
-- Implement helper predicates such as:
-  - `isOutsideSoftRange()`
-  - `isOutsideHardRange()`
-  - `chooseNormalizeMode()`
-
-**Step 4: Run test to verify it passes**
-
-Run the same targeted test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/utils/length-metrics.ts packages/core/src/__tests__/length-metrics.test.ts packages/core/src/index.ts
-git commit -m "feat: add length counting helpers"
-```
-
-### Task 3: Thread LengthSpec into writer prompts and output accounting
-
-**Files:**
-- Modify: `packages/core/src/agents/writer.ts`
-- Modify: `packages/core/src/agents/writer-prompts.ts`
-- Modify: `packages/core/src/agents/writer-parser.ts`
-- Modify: `packages/core/src/__tests__/writer-prompts.test.ts`
-- Modify: `packages/core/src/__tests__/writer-parser.test.ts`
-
-**Step 1: Write the failing test**
-
-Add tests asserting:
-
-- writer prompt uses target-range wording instead of `不少于 X 字`
-- parser/output accounting can use shared counting helpers instead of raw `.length`
-
-Example prompt assertion:
-
-```ts
-expect(prompt).toContain("目标字数");
-expect(prompt).toContain("允许区间");
-expect(prompt).not.toContain("不少于2200字");
-```
-
-**Step 2: Run tests to verify they fail**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/writer-prompts.test.ts src/__tests__/writer-parser.test.ts`
-
-Expected: old wording and old counting behavior still present.
-
-**Step 3: Write minimal implementation**
-
-- Extend writer input to accept `LengthSpec`
-- Replace minimum-only wording with target-range wording
-- Use counting helpers for reported chapter length
-- Keep token ceiling logic as a ceiling, but stop presenting it as the real length control
-
-**Step 4: Run tests to verify they pass**
-
-Run the same targeted test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/agents/writer.ts packages/core/src/agents/writer-prompts.ts packages/core/src/agents/writer-parser.ts packages/core/src/__tests__/writer-prompts.test.ts packages/core/src/__tests__/writer-parser.test.ts
-git commit -m "refactor: thread length spec into writer"
-```
-
-### Task 4: Build the LengthNormalizerAgent
-
-**Files:**
-- Create: `packages/core/src/agents/length-normalizer.ts`
-- Create: `packages/core/src/__tests__/length-normalizer.test.ts`
-- Modify: `packages/core/src/index.ts`
-
-**Step 1: Write the failing test**
-
-Add tests for:
-
-- compress mode shortens a long draft while preserving a required marker
-- expand mode lengthens a short draft without inserting forbidden markers
-- agent exposes a single-pass correction contract
-
-Example assertion:
-
-```ts
-expect(result.normalizedContent.length).toBeGreaterThan(0);
-expect(result.applied).toBe(true);
-expect(result.mode).toBe("compress");
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/length-normalizer.test.ts`
-
-Expected: module missing / agent missing.
-
-**Step 3: Write minimal implementation**
-
-- Create `LengthNormalizerAgent`
-- Input:
-  - chapter content
-  - `LengthSpec`
-  - chapter intent / reduced control block when available
-- Output:
-  - normalized content
-  - final count
-  - applied/not-applied
-  - warning string if still outside range
-
-**Step 4: Run test to verify it passes**
-
-Run the same targeted test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/agents/length-normalizer.ts packages/core/src/__tests__/length-normalizer.test.ts packages/core/src/index.ts
-git commit -m "feat: add length normalizer agent"
-```
-
-### Task 5: Normalize once after writer when draft leaves the soft band
-
-**Files:**
-- Modify: `packages/core/src/pipeline/runner.ts`
-- Modify: `packages/core/src/__tests__/pipeline-runner.test.ts`
-
-**Step 1: Write the failing test**
-
-Add tests asserting:
-
-- writer output above `softMax` triggers one post-writer normalize pass
-- writer output below `softMin` triggers one post-writer normalize pass
-- normalized content replaces the original draft before audit
-
-Example assertion:
-
-```ts
-expect(normalizeSpy).toHaveBeenCalledTimes(1);
-expect(auditorSpy).toHaveBeenCalledWith(
-  expect.anything(),
-  "normalized body",
-  expect.anything(),
-  expect.anything(),
-  expect.anything(),
-);
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/pipeline-runner.test.ts`
-
-Expected: pipeline currently sends raw writer output straight into audit.
-
-**Step 3: Write minimal implementation**
-
-- Build `LengthSpec` at pipeline entry
-- Count writer output with the shared helper
-- If outside soft range, run `LengthNormalizerAgent` once
-- Record telemetry fields for writer count and post-normalize count
-
-**Step 4: Run test to verify it passes**
-
-Run the same targeted test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/pipeline/runner.ts packages/core/src/__tests__/pipeline-runner.test.ts
-git commit -m "feat: normalize draft length before audit"
-```
-
-### Task 6: Enforce shared length governance in reviser and spot-fix paths
-
-**Files:**
-- Modify: `packages/core/src/agents/reviser.ts`
-- Modify: `packages/core/src/pipeline/runner.ts`
-- Modify: `packages/core/src/__tests__/pipeline-runner.test.ts`
-
-**Step 1: Write the failing test**
-
-Add tests asserting:
-
-- reviser receives `LengthSpec`
-- reviser prompt includes target-range preservation
-- post-revise output outside soft range triggers one final normalize pass
-
-Example assertion:
-
-```ts
-expect(reviserSpy).toHaveBeenCalledWith(
-  expect.anything(),
-  expect.anything(),
-  expect.anything(),
-  expect.anything(),
-  expect.anything(),
-  expect.objectContaining({
-    lengthSpec: expect.objectContaining({ target: 2200 }),
-  }),
-);
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/pipeline-runner.test.ts`
-
-Expected: reviser path ignores any shared length policy.
-
-**Step 3: Write minimal implementation**
-
-- Extend reviser options with `lengthSpec`
-- Tell reviser to preserve the target band
-- After spot-fix or revise, if the output still leaves the soft band, run a single final normalize pass
-
-**Step 4: Run test to verify it passes**
-
-Run the same targeted test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/agents/reviser.ts packages/core/src/pipeline/runner.ts packages/core/src/__tests__/pipeline-runner.test.ts
-git commit -m "feat: apply length governance to reviser"
-```
-
-### Task 7: Add single-pass guardrails, warnings, and telemetry output
-
-**Files:**
-- Modify: `packages/core/src/pipeline/runner.ts`
-- Modify: `packages/core/src/models/chapter.ts`
-- Modify: `packages/core/src/__tests__/pipeline-runner.test.ts`
-
-**Step 1: Write the failing test**
-
-Add tests asserting:
-
-- normalizer never runs more than once per boundary
-- final hard-range miss does not recurse
-- final result includes a visible length warning/telemetry block
-
-Example assertion:
-
-```ts
-expect(normalizeSpy).toHaveBeenCalledTimes(1);
-expect(result.lengthWarnings).toContain("outside hard range");
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/pipeline-runner.test.ts`
-
-Expected: no single-pass guard and no structured warning output.
-
-**Step 3: Write minimal implementation**
-
-- Add explicit booleans/counters to prevent recursive normalize loops
-- Attach length warning metadata to pipeline results
-- Ensure persistence still succeeds even if the final output remains outside the hard band
-
-**Step 4: Run test to verify it passes**
-
-Run the same targeted test command and confirm green.
-
-**Step 5: Commit**
-
-```bash
-git add packages/core/src/pipeline/runner.ts packages/core/src/models/chapter.ts packages/core/src/__tests__/pipeline-runner.test.ts
-git commit -m "feat: add length drift warnings"
-```
-
-### Task 8: Document the new chapter-length behavior
-
-**Files:**
-- Modify: `README.md`
-- Modify: `README.en.md`
-
-**Step 1: Write the failing documentation checklist**
-
-Create a short checklist in the task notes:
-
-- README explains that `--words` is a target band, not an exact promise
-- README explains Chinese vs English counting modes
-- README explains that InkOS may run one corrective normalization pass instead of truncating prose
-
-**Step 2: Verify docs are currently missing it**
-
-Run: `rg -n "target band|normalize-length|counting mode|字数区间" README.md README.en.md`
-
-Expected: no relevant matches.
-
-**Step 3: Write minimal documentation**
-
-- Update both READMEs with a short note in CLI usage / writing behavior sections
-
-**Step 4: Verify the docs contain the new guidance**
-
-Run the same `rg` command and confirm matches exist.
-
-**Step 5: Commit**
-
-```bash
-git add README.md README.en.md
-git commit -m "docs: explain length governance behavior"
-```
-
-### Task 9: Run full verification
-
-**Files:**
-- No code changes required
-
-**Step 1: Run targeted core tests**
-
-Run: `pnpm --filter @actalk/inkos-core test -- src/__tests__/length-metrics.test.ts src/__tests__/length-normalizer.test.ts src/__tests__/writer-prompts.test.ts src/__tests__/writer-parser.test.ts src/__tests__/pipeline-runner.test.ts`
-
-Expected: all green.
-
-**Step 2: Run core typecheck**
-
-Run: `pnpm --filter @actalk/inkos-core typecheck`
-
-Expected: exit 0.
-
-**Step 3: Run workspace verification**
-
-Run:
-
-```bash
-pnpm typecheck
-pnpm test
-pnpm build
-```
-
-Expected: all exit 0.
-
-**Step 4: Optional smoke check**
-
-Run a real CLI path:
-
-```bash
-inkos draft <book> --words 2200
-inkos revise <book> <chapter>
-```
-
-Confirm the final chapter lands inside the conservative band or emits a warning instead of drifting silently.
-
-**Step 5: Commit**
-
-No commit unless verification reveals a missing follow-up fix.