novel-writer-cli 0.0.1

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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/agents/chapter-writer.md +142 -0
  4. package/agents/character-weaver.md +117 -0
  5. package/agents/consistency-auditor.md +85 -0
  6. package/agents/plot-architect.md +128 -0
  7. package/agents/quality-judge.md +232 -0
  8. package/agents/style-analyzer.md +109 -0
  9. package/agents/style-refiner.md +97 -0
  10. package/agents/summarizer.md +128 -0
  11. package/agents/world-builder.md +161 -0
  12. package/dist/__tests__/character-voice.test.js +445 -0
  13. package/dist/__tests__/commit-prototype-pollution.test.js +45 -0
  14. package/dist/__tests__/engagement.test.js +382 -0
  15. package/dist/__tests__/foreshadow-visibility.test.js +131 -0
  16. package/dist/__tests__/hook-ledger.test.js +1028 -0
  17. package/dist/__tests__/naming-lint.test.js +132 -0
  18. package/dist/__tests__/narrative-health-injection.test.js +359 -0
  19. package/dist/__tests__/next-step-prejudge-guardrails.test.js +325 -0
  20. package/dist/__tests__/next-step-title-fix.test.js +153 -0
  21. package/dist/__tests__/platform-profile.test.js +274 -0
  22. package/dist/__tests__/promise-ledger.test.js +189 -0
  23. package/dist/__tests__/readability-lint.test.js +209 -0
  24. package/dist/__tests__/text-utils.test.js +39 -0
  25. package/dist/__tests__/title-policy.test.js +147 -0
  26. package/dist/advance.js +75 -0
  27. package/dist/character-voice.js +805 -0
  28. package/dist/checkpoint.js +126 -0
  29. package/dist/cli.js +563 -0
  30. package/dist/cliche-lint.js +515 -0
  31. package/dist/commit.js +1460 -0
  32. package/dist/consistency-auditor.js +684 -0
  33. package/dist/engagement.js +687 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/fingerprint.js +16 -0
  36. package/dist/foreshadow-visibility.js +214 -0
  37. package/dist/fs-utils.js +68 -0
  38. package/dist/hook-ledger.js +721 -0
  39. package/dist/hook-policy.js +107 -0
  40. package/dist/instruction-gates.js +51 -0
  41. package/dist/instructions.js +406 -0
  42. package/dist/latest-summary-loader.js +29 -0
  43. package/dist/lock.js +121 -0
  44. package/dist/naming-lint.js +531 -0
  45. package/dist/ner.js +73 -0
  46. package/dist/next-step.js +408 -0
  47. package/dist/novel-ask.js +270 -0
  48. package/dist/output.js +9 -0
  49. package/dist/platform-constraints.js +518 -0
  50. package/dist/platform-profile.js +325 -0
  51. package/dist/prejudge-guardrails.js +370 -0
  52. package/dist/project.js +40 -0
  53. package/dist/promise-ledger.js +723 -0
  54. package/dist/readability-lint.js +555 -0
  55. package/dist/safe-parse.js +36 -0
  56. package/dist/safe-path.js +29 -0
  57. package/dist/scoring-weights.js +290 -0
  58. package/dist/steps.js +60 -0
  59. package/dist/text-utils.js +18 -0
  60. package/dist/title-policy.js +251 -0
  61. package/dist/type-guards.js +6 -0
  62. package/dist/validate.js +131 -0
  63. package/docs/user/README.md +17 -0
  64. package/docs/user/guardrails.md +179 -0
  65. package/docs/user/interactive-gates.md +124 -0
  66. package/docs/user/novel-cli.md +289 -0
  67. package/docs/user/ops.md +123 -0
  68. package/docs/user/quick-start.md +97 -0
  69. package/docs/user/spec-system.md +166 -0
  70. package/docs/user/storylines.md +144 -0
  71. package/package.json +48 -0
  72. package/schemas/README.md +18 -0
  73. package/schemas/character-voice-drift.schema.json +135 -0
  74. package/schemas/character-voice-profiles.schema.json +141 -0
  75. package/schemas/engagement-metrics.schema.json +38 -0
  76. package/schemas/hook-ledger.schema.json +108 -0
  77. package/schemas/platform-profile.schema.json +235 -0
  78. package/schemas/promise-ledger.schema.json +97 -0
  79. package/scripts/calibrate-quality-judge.sh +91 -0
  80. package/scripts/compare-regression-runs.sh +86 -0
  81. package/scripts/lib/_common.py +131 -0
  82. package/scripts/lib/calibrate_quality_judge.py +312 -0
  83. package/scripts/lib/compare_regression_runs.py +142 -0
  84. package/scripts/lib/run_regression.py +621 -0
  85. package/scripts/lint-blacklist.sh +201 -0
  86. package/scripts/lint-cliche.sh +370 -0
  87. package/scripts/lint-readability.sh +404 -0
  88. package/scripts/query-foreshadow.sh +252 -0
  89. package/scripts/run-ner.sh +669 -0
  90. package/scripts/run-regression.sh +122 -0
  91. package/skills/cli-step/SKILL.md +158 -0
  92. package/skills/continue/SKILL.md +348 -0
  93. package/skills/continue/references/context-contracts.md +169 -0
  94. package/skills/continue/references/continuity-checks.md +187 -0
  95. package/skills/continue/references/file-protocols.md +64 -0
  96. package/skills/continue/references/foreshadowing.md +130 -0
  97. package/skills/continue/references/gate-decision.md +53 -0
  98. package/skills/continue/references/periodic-maintenance.md +46 -0
  99. package/skills/novel-writing/SKILL.md +77 -0
  100. package/skills/novel-writing/references/quality-rubric.md +140 -0
  101. package/skills/novel-writing/references/style-guide.md +145 -0
  102. package/skills/start/SKILL.md +458 -0
  103. package/skills/start/references/quality-review.md +86 -0
  104. package/skills/start/references/setting-update.md +44 -0
  105. package/skills/start/references/vol-planning.md +61 -0
  106. package/skills/start/references/vol-review.md +58 -0
  107. package/skills/status/SKILL.md +116 -0
  108. package/skills/status/references/sample-output.md +60 -0
  109. package/templates/ai-blacklist.json +79 -0
  110. package/templates/brief-template.md +46 -0
  111. package/templates/genre-weight-profiles.json +90 -0
  112. package/templates/novel-ask/example.answer.json +12 -0
  113. package/templates/novel-ask/example.question.json +51 -0
  114. package/templates/platform-profile.json +148 -0
  115. package/templates/style-profile-template.json +58 -0
  116. package/templates/web-novel-cliche-lint.json +41 -0
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Regression runner for M2 outputs (M3).
4
+ #
5
+ # Usage:
6
+ # run-regression.sh --project <novel_project_dir> [--labels <labels.jsonl>] [--runs-dir <dir>] [--no-archive]
7
+ #
8
+ # Output:
9
+ # stdout JSON (exit 0 on success)
10
+ #
11
+ # Exit codes:
12
+ # 0 = success (valid JSON emitted to stdout)
13
+ # 1 = validation failure (bad args, missing files, invalid JSON)
14
+ # 2 = script exception (unexpected runtime error)
15
+ #
16
+ # Notes:
17
+ # - Reads existing project outputs (evaluations/logs/etc) and summarizes regression-friendly metrics.
18
+ # - Archives outputs under eval/runs/<timestamp>/ by default (recommended to be gitignored).
19
+
20
+ set -euo pipefail
21
+
22
+ usage() {
23
+ cat >&2 <<'EOF'
24
+ Usage:
25
+ run-regression.sh --project <novel_project_dir> [--labels <labels.jsonl>] [--runs-dir <dir>] [--no-archive]
26
+
27
+ Options:
28
+ --project <dir> Novel project directory (must contain evaluations/)
29
+ --labels <file> Optional: labeled dataset JSONL (for traceability; future metrics can use it)
30
+ --runs-dir <dir> Output base dir for archived runs (default: eval/runs)
31
+ --no-archive Do not write run artifacts; only print JSON to stdout
32
+ --no-continuity Skip reading logs/continuity/latest.json even if present
33
+ --no-foreshadowing Skip reading foreshadowing/global.json even if present
34
+ --no-style Skip reading style-drift.json even if present
35
+ -h, --help Show help
36
+ EOF
37
+ }
38
+
39
+ project_dir=""
40
+ labels_path=""
41
+ runs_dir="eval/runs"
42
+ archive=1
43
+ include_continuity=1
44
+ include_foreshadowing=1
45
+ include_style=1
46
+
47
+ while [ "$#" -gt 0 ]; do
48
+ case "$1" in
49
+ --project)
50
+ [ "$#" -ge 2 ] || { echo "run-regression.sh: error: --project requires a value" >&2; exit 1; }
51
+ project_dir="$2"
52
+ shift 2
53
+ ;;
54
+ --labels)
55
+ [ "$#" -ge 2 ] || { echo "run-regression.sh: error: --labels requires a value" >&2; exit 1; }
56
+ labels_path="$2"
57
+ shift 2
58
+ ;;
59
+ --runs-dir)
60
+ [ "$#" -ge 2 ] || { echo "run-regression.sh: error: --runs-dir requires a value" >&2; exit 1; }
61
+ runs_dir="$2"
62
+ shift 2
63
+ ;;
64
+ --no-archive)
65
+ archive=0
66
+ shift 1
67
+ ;;
68
+ --no-continuity)
69
+ include_continuity=0
70
+ shift 1
71
+ ;;
72
+ --no-foreshadowing)
73
+ include_foreshadowing=0
74
+ shift 1
75
+ ;;
76
+ --no-style)
77
+ include_style=0
78
+ shift 1
79
+ ;;
80
+ -h|--help)
81
+ usage
82
+ exit 0
83
+ ;;
84
+ *)
85
+ echo "run-regression.sh: unknown arg: $1" >&2
86
+ usage
87
+ exit 1
88
+ ;;
89
+ esac
90
+ done
91
+
92
+ if [ -z "$project_dir" ]; then
93
+ echo "run-regression.sh: --project is required" >&2
94
+ usage
95
+ exit 1
96
+ fi
97
+
98
+ if [ ! -d "$project_dir" ]; then
99
+ echo "run-regression.sh: project dir not found: $project_dir" >&2
100
+ exit 1
101
+ fi
102
+
103
+ if [ -n "$labels_path" ] && [ ! -f "$labels_path" ]; then
104
+ echo "run-regression.sh: labels file not found: $labels_path" >&2
105
+ exit 1
106
+ fi
107
+
108
+ if ! command -v python3 >/dev/null 2>&1; then
109
+ echo "run-regression.sh: python3 is required but not found" >&2
110
+ exit 1
111
+ fi
112
+
113
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
114
+ python3 "$SCRIPT_DIR/lib/run_regression.py" \
115
+ "$project_dir" \
116
+ "$labels_path" \
117
+ "$runs_dir" \
118
+ "$archive" \
119
+ "$include_continuity" \
120
+ "$include_foreshadowing" \
121
+ "$include_style"
122
+
@@ -0,0 +1,158 @@
1
+ # `novel` CLI 单步适配器(Claude Code)
2
+
3
+ 你是 Claude Code 的执行器适配层:你不做确定性编排逻辑,只调用 `novel` CLI 获取 step + instruction packet,然后派发对应 subagent 写入 `staging/**`,最后在断点处停下让用户 review。
4
+
5
+ ## 运行约束
6
+
7
+ - **可用工具**:Bash, Task, Read, Write, Edit, Glob, Grep, AskUserQuestion
8
+ - **原则**:只跑 1 个 step;不自动 commit;执行完必须停在断点并提示用户下一条命令
9
+
10
+ ## 执行流程
11
+
12
+ ### Step 0: 前置检查
13
+
14
+ - 必须在小说项目目录内(存在 `.checkpoint.json`)
15
+ - 如不在项目目录:提示用户 `cd` 到项目根目录后重试
16
+ - 若 `dist/` 不存在:先执行 `npm ci && npm run build`
17
+
18
+ ### Step 1: 计算下一步 step id
19
+
20
+ 优先使用已安装的 `novel`:
21
+ ```bash
22
+ novel next --json
23
+ ```
24
+
25
+ 若 `novel` 不在 PATH(开发态/未发布),使用仓库内 CLI:
26
+ ```bash
27
+ node dist/cli.js next --json
28
+ ```
29
+
30
+ 解析 stdout 的单对象 JSON:取 `data.step` 得到类似 `chapter:048:draft` 的 step id。
31
+
32
+ ### Step 2: 生成 instruction packet(并落盘 manifest)
33
+
34
+ ```bash
35
+ novel instructions "<STEP_ID>" --json --write-manifest
36
+ ```
37
+
38
+ 同样解析 stdout JSON:取 `data.packet`(以及可选的 `data.written_manifest_path`)。
39
+ > 注意:若 packet 携带 `novel_ask` gate,后续需要读取 `data.written_manifest_path`(packet JSON 文件路径)用于校验与恢复;因此建议总是使用 `--write-manifest`。
40
+
41
+ ### Step 3: (可选)NOVEL_ASK gate:AskUserQuestion 采集 + AnswerSpec 落盘
42
+
43
+ 若 packet 同时包含:
44
+ - `novel_ask`(QuestionSpec)
45
+ - `answer_path`(project-relative)
46
+
47
+ 则在派发 subagent 前必须先满足 gate:收集回答 → 写入 AnswerSpec → 校验通过后才继续。
48
+
49
+ #### Step 3.1: 检查是否已存在可用 AnswerSpec(可恢复语义)
50
+
51
+ 若 `answer_path` 已存在且通过校验:直接进入 Step 4。
52
+
53
+ 校验命令(会做 questionSpec↔answerSpec cross-validate;缺失则 exit 2):
54
+ ```bash
55
+ PACKET_JSON="<data.written_manifest_path>" node --input-type=module - <<'EOF'
56
+ import fs from "node:fs/promises";
57
+ import { extractNovelAskGate, loadNovelAskAnswerIfPresent } from "./dist/instruction-gates.js";
58
+
59
+ const packet = JSON.parse(await fs.readFile(process.env.PACKET_JSON, "utf8"));
60
+ const gate = extractNovelAskGate(packet);
61
+ if (!gate) process.exit(0);
62
+ const answer = await loadNovelAskAnswerIfPresent(process.cwd(), gate);
63
+ if (answer) {
64
+ console.error("NOVEL_ASK gate: OK");
65
+ process.exit(0);
66
+ }
67
+ console.error(`NOVEL_ASK gate: missing AnswerSpec at ${gate.answer_path}`);
68
+ process.exit(2);
69
+ EOF
70
+ ```
71
+
72
+ > 若 AnswerSpec 存在但无效:上述命令会报错并阻断;此时不得继续执行 step,应提示用户修复/删除该文件后重试。
73
+
74
+ #### Step 3.2: 用 AskUserQuestion 逐题采集 answers 映射
75
+
76
+ 对 `novel_ask.questions[]` 按顺序提问,并构造 `answers: {[id]: value}`:
77
+
78
+ > AskUserQuestion 对 `options[]` 有硬限制(每次 2-4 个)。当 QuestionSpec 的 options 过多时,必须用“分页/循环”拆成多轮 AskUserQuestion,保证每轮 options 不超过上限。
79
+
80
+ - `single_choice`:
81
+ - 若 `options.length <= 4`:直接 AskUserQuestion 单选;保存为 string(选项 label 或 allow_other 的自定义字符串)
82
+ - 若 `options.length > 4`:分页展示(每页最多 3 个真实 option + 1 个控制项 `__more__`,`__more__` 不写入 answers;用户选到真实 option 才结束)
83
+ - 若存在 `default`:把 default 对应的 option **排到当前页第一位**,并在 description 里标注 Recommended(不要修改 label,否则会导致校验不通过)
84
+ - `allow_other=false` 风险:AskUserQuestion UI 可能提供 “Other” 自定义输入;若用户选择 Other 且输入不在 option labels 内,会被校验拒绝 → 需要重新提问直到得到合法答案
85
+
86
+ - `multi_choice`:循环 AskUserQuestion 单选累积为 string[]
87
+ - 目标:每轮 `options[]` 总数 **不超过 4**(AskUserQuestion 限制)
88
+ - 若当前“可选的真实 option”(排除已选项)`<= 3`:本轮直接展示 **全部真实 option + `__done__`**(不需要 `__more__`)
89
+ - 若当前“可选的真实 option”`> 3`:分页展示(每页最多 **2 个真实 option + `__more__` + `__done__`**;控制项不写入 answers)
90
+ - `__more__`:切换到下一页(循环);仅在 options 过多时使用(`<=3` 时不需要)
91
+ - `__done__`:结束选择并写入 answers
92
+ - required=true:必须至少选 1 个再 `__done__`,否则视为未回答(blocked)
93
+ - required=false:若最终 0 选择,则不要写入该 question id(不要写空数组)
94
+ - `allow_other=false` 风险同上:若用户通过 Other 输入自定义值,会被校验拒绝,需要重新提问
95
+
96
+ - `free_text`:AskUserQuestion 本身是“选项式交互”,要采集自由文本需要依赖 UI 的 “Other” 输入(或退化为普通消息输入)
97
+ - required=false:先 AskUserQuestion 让用户选 “Skip / Provide”;Skip 则不写入 answers;Provide 则让用户用 Other 输入文本(或在下一条消息直接粘贴文本)
98
+ - required=true:AskUserQuestion 提示用户用 Other 输入文本(或在下一条消息直接粘贴文本),并将该文本写入 answers[id]
99
+
100
+ #### Step 3.3: 写入 AnswerSpec 到 answer_path,并校验通过
101
+
102
+ 1) 将 answers 暂存到 `staging/novel-ask/answers.json`(结构:`{ "answers": { ... } }`)
103
+ 2) 用下面命令构造 + 写入 AnswerSpec(`answered_by="claude_code"`),并二次校验:
104
+
105
+ ```bash
106
+ mkdir -p staging/novel-ask
107
+ PACKET_JSON="<data.written_manifest_path>" ANSWERS_JSON="staging/novel-ask/answers.json" node --input-type=module - <<'EOF'
108
+ import fs from "node:fs/promises";
109
+ import { dirname } from "node:path";
110
+ import { extractNovelAskGate, requireNovelAskAnswer } from "./dist/instruction-gates.js";
111
+ import { parseNovelAskAnswerSpec, validateNovelAskAnswerAgainstQuestionSpec } from "./dist/novel-ask.js";
112
+ import { resolveProjectRelativePath } from "./dist/safe-path.js";
113
+
114
+ const packet = JSON.parse(await fs.readFile(process.env.PACKET_JSON, "utf8"));
115
+ const gate = extractNovelAskGate(packet);
116
+ if (!gate) process.exit(0);
117
+
118
+ const raw = JSON.parse(await fs.readFile(process.env.ANSWERS_JSON, "utf8"));
119
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("ANSWERS_JSON must be a JSON object.");
120
+ if (typeof raw.answers !== "object" || raw.answers === null || Array.isArray(raw.answers)) throw new Error("ANSWERS_JSON.answers must be an object.");
121
+
122
+ const answerSpec = parseNovelAskAnswerSpec({
123
+ version: gate.novel_ask.version,
124
+ topic: gate.novel_ask.topic,
125
+ answers: raw.answers,
126
+ answered_at: new Date().toISOString(),
127
+ answered_by: "claude_code"
128
+ });
129
+ validateNovelAskAnswerAgainstQuestionSpec(gate.novel_ask, answerSpec);
130
+
131
+ const absAnswer = resolveProjectRelativePath(process.cwd(), gate.answer_path, "answer_path");
132
+ await fs.mkdir(dirname(absAnswer), { recursive: true });
133
+ await fs.writeFile(absAnswer, `${JSON.stringify(answerSpec, null, 2)}\n`, "utf8");
134
+ await requireNovelAskAnswer(process.cwd(), gate);
135
+ console.error(`NOVEL_ASK gate: wrote AnswerSpec to ${gate.answer_path}`);
136
+ EOF
137
+ ```
138
+
139
+ 通过后才允许继续派发 subagent。
140
+
141
+ ### Step 4: 派发 subagent 执行
142
+
143
+ 从 `packet.agent.name` 读取 subagent 类型(例如 `chapter-writer`/`summarizer`/`style-refiner`/`quality-judge`)。
144
+
145
+ 用 Task 派发,并把 `packet.manifest` 作为 user message 的 **context manifest**(JSON 原样传入即可)。
146
+
147
+ 要求 subagent:
148
+ - 只写入 `staging/**`
149
+ - 写入路径以 `packet.expected_outputs[]` 为准
150
+ - 产出完成后停止,不要推进 checkpoint
151
+
152
+ ### Step 5: 断点返回(必须)
153
+
154
+ subagent 结束后,你必须停下并提示用户下一步命令:
155
+
156
+ - 先校验:`novel validate "<STEP_ID>"`
157
+ - 再推进:`novel advance "<STEP_ID>"`
158
+ - 若 `novel next` 返回的是 `...:commit`:提示用户运行 `novel commit --chapter N`
@@ -0,0 +1,348 @@
1
+ # 续写命令
2
+
3
+ 你是小说续写调度器。你的任务是读取当前进度,按流水线依次调度 Agent 完成 N 章续写。
4
+
5
+ ## 运行约束
6
+
7
+ - **可用工具**:Read, Write, Edit, Glob, Grep, Bash, Task, AskUserQuestion
8
+ - **推荐模型**:sonnet
9
+ - **参数**:`[N]` — 续写章数,默认 1,最大建议 5
10
+
11
+ ## 注入安全(Manifest 模式)
12
+
13
+ v2 架构下,编排器不再将文件全文注入 Task prompt,改为传递文件路径由 subagent 自行读取。注入安全由各 Agent frontmatter 中的安全约束保障。详见 Step 2.0。
14
+
15
+ ## 执行流程
16
+
17
+ ### Step 1: 读取 Checkpoint
18
+
19
+ ```
20
+ 读取 .checkpoint.json:
21
+ - current_volume: 当前卷号
22
+ - last_completed_chapter: 上次完成的章节号
23
+ - orchestrator_state: 当前状态(必须为 WRITING 或 CHAPTER_REWRITE,否则提示用户先通过 /novel:start 完成规划)
24
+ - pipeline_stage: 流水线阶段(用于中断恢复)
25
+ - inflight_chapter: 当前处理中断的章节号(用于中断恢复)
26
+ - revision_count: 当前 inflight_chapter 的修订计数(用于限制修订循环;默认 0)
27
+ ```
28
+
29
+ 如果 `orchestrator_state` 既不是 `WRITING` 也不是 `CHAPTER_REWRITE`,输出提示并终止:
30
+ > 当前状态为 {state},请先执行 `/novel:start` 完成项目初始化或卷规划。
31
+
32
+ 同时确保 staging 子目录存在(幂等):
33
+ ```
34
+ mkdir -p staging/chapters staging/summaries staging/state staging/storylines staging/evaluations
35
+ ```
36
+
37
+ ### Step 1.5: 中断恢复(pipeline_stage)
38
+
39
+ 若 `.checkpoint.json` 满足以下条件:
40
+ - `pipeline_stage != "committed"` 且 `pipeline_stage != null`
41
+ - `inflight_chapter != null`
42
+
43
+ 则本次 `/novel:continue` **必须先完成** `inflight_chapter` 的流水线,并按 `docs/dr-workflow/novel-writer-tool/final/prd/09-data.md` §9.2 的规则幂等恢复:
44
+
45
+ - `pipeline_stage == "drafting"`:
46
+ - 若 `staging/chapters/chapter-{C:03d}.md` 不存在 → 从 ChapterWriter 重启整章
47
+ - 若 `staging/chapters/chapter-{C:03d}.md` 已存在但 `staging/summaries/chapter-{C:03d}-summary.md` 不存在 → 从 Summarizer 恢复
48
+ - `pipeline_stage == "drafted"` → 跳过 ChapterWriter/Summarizer,从 StyleRefiner 恢复
49
+ - `pipeline_stage == "refined"` → 从 QualityJudge 恢复
50
+ - `pipeline_stage == "judged"` → 直接执行 commit 阶段
51
+ - `pipeline_stage == "revising"` → 修订中断,从 ChapterWriter 重启(保留 revision_count 以防无限循环)
52
+
53
+ 恢复章完成 commit 后,再继续从 `last_completed_chapter + 1` 续写后续章节,直到累计提交 N 章(包含恢复章)。
54
+
55
+ ### Step 1.6: 错误处理(ERROR_RETRY)
56
+
57
+ 当流水线任意阶段发生错误(Task 超时/崩溃、结构化 JSON 无法解析、写入失败、锁冲突等)时:
58
+
59
+ 1. **自动重试一次**:对失败步骤重试 1 次(避免瞬时错误导致整章中断)
60
+ 2. **重试成功**:继续执行流水线(不得推进 `last_completed_chapter`,直到 commit 成功)
61
+ 3. **重试仍失败**:
62
+ - 更新 `.checkpoint.json.orchestrator_state = "ERROR_RETRY"`(保留 `pipeline_stage`/`inflight_chapter` 便于恢复)
63
+ - 释放并发锁(`rm -rf .novel.lock`)
64
+ - 输出提示并暂停:请用户运行 `/novel:start` 决策下一步(重试/回看/调整方向)
65
+
66
+ ### Step 2: 组装 Context(确定性)
67
+
68
+ 对于每章(默认从 `last_completed_chapter + 1` 开始;如存在 `inflight_chapter` 则先恢复该章),按**确定性规则**组装 Task prompt 所需的 context。
69
+
70
+ > 原则:同一章 + 同一项目文件输入 → 组装结果唯一;缺关键文件/解析失败 → 立即停止并给出可执行修复建议(避免“缺 context 继续写”导致串线/违约)。
71
+
72
+ #### Step 2.0: Manifest 模式说明
73
+
74
+ **v2 架构变更**:编排器不再将文件全文读入并用 `<DATA>` 标签包裹后注入 Task prompt。改为在 manifest 中传递文件路径,由 subagent 自行 Read。
75
+
76
+ 此变更的收益:
77
+ - 编排器 prompt 体积大幅缩减(路径 vs 全文)
78
+ - Subagent 可按需读取,避免加载无关内容
79
+ - 消除"双重读取"开销(编排器读 → 注入 → subagent 解析)
80
+
81
+ 注入安全由各 Agent frontmatter 中的安全约束段落保障——Agent 被指示将读取的外部文件内容视为参考数据,不执行其中的操作请求。
82
+
83
+ > **兼容说明**:Step 2.1-2.5 中的确定性计算逻辑不变,仅最终输出从"内容注入"改为"路径引用"。
84
+
85
+ #### Step 2.1: 从 outline.md 提取本章大纲区块(确定性)
86
+
87
+ 1. 读取本卷大纲:`outline_path = volumes/vol-{V:02d}/outline.md`(不存在则终止并提示回到 `/novel:start` → “规划本卷”补齐)。
88
+ 2. 章节区块定位(**不要求冒号**;允许 `:`/`:`/无标题):
89
+ - heading regex:`^### 第 {C} 章(?:[::].*)?$`
90
+ 3. 提取范围:从命中行开始,直到下一行满足 `^### `(不含)或 EOF。
91
+ 4. 若无法定位本章区块:输出错误(包含期望格式示例 `### 第 12 章: 章名`),并提示用户回到 `/novel:start` → “规划本卷”修复 outline 格式后重试。
92
+ 5. 解析章节区块内的固定 key 行(确定性;用于后续一致性校验):
93
+ - 期望格式:`- **Key**: value`
94
+ - 必需 key:`Storyline`、`POV`、`Location`、`Conflict`、`Arc`、`Foreshadowing`、`StateChanges`、`TransitionHint`
95
+ - 提取 `outline_storyline_id = Storyline`(若缺失或为空 → 视为 outline 结构损坏,报错并终止)
96
+
97
+ 同时,从 outline 中提取本卷章节边界(用于卷首/卷尾双裁判与卷末状态转移):
98
+ - 扫描所有章标题:`^### 第 (\d+) 章`
99
+ - `chapter_start = min(章节号)`,`chapter_end = max(章节号)`
100
+ - 若无法提取边界:视为 outline 结构损坏,按上述方式报错并终止。
101
+
102
+ #### Step 2.2: `hard_rules_list`(L1 世界规则 → 禁止项列表,确定性)
103
+
104
+ 1. 读取并解析 `world/rules.json`(如不存在则 `hard_rules_list = []`)。
105
+ 2. 筛选 `constraint_type == "hard"` 的规则,按 `id` 升序输出为禁止项列表:
106
+
107
+ ```
108
+ - [W-001][magic_system] 修炼者突破金丹期需要天地灵气浓度 ≥ 3级
109
+ - [W-002][geography] 禁止在“幽暗森林”使用火系法术(exceptions: ...)
110
+ ```
111
+
112
+ 该列表用于 ChapterWriter(禁止项提示)与 QualityJudge(逐条验收)。
113
+
114
+ #### Step 2.3: `entity_id_map`(从角色 JSON 构建,确定性)
115
+
116
+ 1. `Glob("characters/active/*.json")` 获取活跃角色结构化档案。
117
+ 2. 对每个文件:
118
+ - `slug_id` 默认取文件名(去掉 `.json`)
119
+ - `display_name` 取 JSON 中的 `display_name`
120
+ 3. 构建 `entity_id_map = {slug_id → display_name}`(并在本地临时构建反向表 `display_name → slug_id` 供裁剪/映射使用)。
121
+
122
+ 该映射传给 Summarizer,用于把正文中的中文显示名规范化为 ops path 的 slug ID(如 `characters.lin-feng.location`)。
123
+
124
+ #### Step 2.4: L2 角色契约裁剪(确定性)
125
+
126
+ 前置:读取并解析本章 L3 章节契约(缺失则终止并提示回到 `/novel:start` → “规划本卷”补齐):
127
+ - `chapter_contract_path = volumes/vol-{V:02d}/chapter-contracts/chapter-{C:03d}.json`
128
+
129
+ 裁剪规则:
130
+
131
+ - 若存在 `chapter_contract.preconditions.character_states`:
132
+ - 仅加载这些 preconditions 中涉及的角色(**无硬上限**;交汇事件章可 > 10)
133
+ - 注意:`character_states` 的键为中文显示名,需要用 `entity_id_map` 反向映射到 `slug_id`
134
+ - 否则:
135
+ - 最多加载 15 个活跃角色(按“最近出场”排序截断)
136
+ - “最近出场”计算:扫描近 10 章 `summaries/`(从新到旧),命中 `display_name` 的第一次出现即视为最近;未命中视为最旧
137
+ - 排序规则:`last_seen_chapter` 降序 → `slug_id` 升序(保证确定性)
138
+
139
+ 加载内容:
140
+ - `character_contracts`:记录 `characters/active/{slug_id}.json` 路径列表(写入 manifest.paths.character_contracts)
141
+ - `character_profiles`:记录 `characters/active/{slug_id}.md` 路径列表(如存在;写入 QualityJudge manifest.paths.character_profiles)
142
+
143
+ #### Step 2.5: storylines context + memory 注入(确定性)
144
+
145
+ 1. 读取 `volumes/vol-{V:02d}/storyline-schedule.json`(如存在则解析;用于判定 dormant_storylines 与交汇事件 involved_storylines)。
146
+ 2. 读取 `storylines/storyline-spec.json`(如存在;注入给 QualityJudge 做 LS 验收)。
147
+ 3. 章节契约与大纲一致性校验(确定性;不通过则终止,避免“拿错契约继续写”导致串线/违约):
148
+ - `chapter_contract.chapter == C`
149
+ - `chapter_contract.storyline_id == outline_storyline_id`
150
+ - `chapter_contract.objectives` 至少 1 条 `required: true`
151
+ 4. 以 `chapter_contract` 为优先来源确定:
152
+ - `storyline_id`(本章所属线)
153
+ - `storyline_context`(含 `last_chapter_summary` / `chapters_since_last` / `line_arc_progress` / `concurrent_state`)
154
+ - `transition_hint`(如存在)
155
+ 5. memory 路径策略:
156
+ - 当前线 `storylines/{storyline_id}/memory.md`:如存在,写入 manifest.paths.storyline_memory
157
+ - 相邻线:
158
+ - 若 `transition_hint.next_storyline` 存在 → 将该线 memory 路径加入 manifest.paths.adjacent_memories(若不在 `dormant_storylines`)
159
+ - 若当前章落在任一 `convergence_events.chapter_range` 内 → 将 `involved_storylines` 中除当前线外的 memory 路径加入 manifest.paths.adjacent_memories(过滤 `dormant_storylines`)
160
+ - 冻结线(`dormant_storylines`):**不加入 memory 路径**,仅保留 `concurrent_state` 一句话状态(inline)
161
+ 6. `foreshadowing_tasks` 组装(确定性):
162
+ - 数据来源:
163
+ - 事实层:`foreshadowing/global.json`(如不存在则视为空)
164
+ - 计划层:`volumes/vol-{V:02d}/foreshadowing.json`(如不存在则视为空)
165
+ - 优先确定性脚本(M3+ 扩展点;见 `docs/dr-workflow/novel-writer-tool/final/spec/06-extensions.md`):
166
+ - 若存在 `${NOVEL_CLI_ROOT}/scripts/query-foreshadow.sh`:
167
+ - 执行(超时 10 秒):`timeout 10 bash ${NOVEL_CLI_ROOT}/scripts/query-foreshadow.sh {C}`
168
+ - 若退出码为 0 且 stdout 为合法 JSON 且 `.items` 为 list → `foreshadowing_tasks = .items`
169
+ - 否则(脚本缺失/失败/输出非 JSON)→ 回退规则过滤(不得阻断流水线)
170
+ - 规则过滤回退(确定性;详见 `references/foreshadowing.md`):
171
+ a. 读取并解析 global 与本卷计划 JSON(允许 schema 为 object.foreshadowing[];缺失则视为空)。
172
+ b. 选取候选(按 `id` 去重;输出按 `id` 升序):
173
+ - **计划命中**:本卷计划中满足以下任一条件的未回收条目:
174
+ - `planted_chapter == C`(本章计划埋设)
175
+ - `target_resolve_range` 覆盖 `C`(本章处于计划推进/回收窗口)
176
+ - **事实命中**:global 中满足以下任一条件的未回收条目:
177
+ - `target_resolve_range` 覆盖 `C`
178
+ - `scope=="short"` 且 `target_resolve_range` 存在且 `C > target_resolve_range[1]`(超期 short)
179
+ c. 合并字段(不覆盖事实):
180
+ - 若某 `id` 同时存在于 global 与 plan:以 global 为主,仅在 global 缺失时从 plan 回填 `description/scope/target_resolve_range`。
181
+ d. 得到 `foreshadowing_tasks`(list;为空则 `[]`)。
182
+
183
+ #### Step 2.6: Agent Context Manifest 组装
184
+
185
+ 按 Agent 类型组装 **context manifest**(内联计算值 + 文件路径),字段契约详见 `references/context-contracts.md`。
186
+
187
+ **Manifest 模式**:编排器不再读取文件全文注入 Task prompt,而是计算文件路径并传入 manifest。Subagent 在执行时用 Read 工具自行读取所需文件。
188
+
189
+ 编排器仍需完成的**确定性计算**(作为 inline 字段直接写入 manifest):
190
+ - `chapter_outline_block`:从 outline.md 提取的本章区块文本(Step 2.1 已完成)
191
+ - `hard_rules_list`:从 rules.json 筛选的禁止项列表(Step 2.2 已完成)
192
+ - `entity_id_map`:从角色 JSON 构建的 slug↔display_name 映射(Step 2.3 已完成)
193
+ - `foreshadowing_tasks`:跨文件聚合的伏笔子集(Step 2.5 已完成)
194
+ - `foreshadow_light_touch_tasks`(可选):基于 `foreshadowing/global.json` 的沉默度超阈值提醒(非剧透、不兑现;为空则省略)
195
+ - `foreshadow_light_touch_degraded`(可选):若为 true 表示“轻触提醒”注入降级(如伏笔数据不可读),不等同于“没有需要提醒的条目”
196
+ - `storyline_context` / `concurrent_state` / `transition_hint`:从 contract/schedule 解析(Step 2.5 已完成)
197
+ - `ai_blacklist_top10`:有效黑名单前 10 词(从 ai-blacklist.json 快速提取)
198
+ - `style_drift_directives`:从 style-drift.json 提取的纠偏指令列表(Step 2.7;仅 active=true 时)
199
+
200
+ 编排器需完成的**路径计算**(作为 paths 字段写入 manifest):
201
+ - 根据 Step 2.4 裁剪规则确定 `character_contracts[]` 和 `character_profiles[]` 的文件路径列表
202
+ - 根据 Step 2.5 注入策略确定 `storyline_memory` / `adjacent_memories[]` 的路径(过滤 dormant 线)
203
+ - 确定 `recent_summaries[]`(近 3 章摘要路径,按时间倒序)
204
+ - 其余路径为固定模式(如 `style-profile.json`、`ai-blacklist.json`)
205
+
206
+ 关键原则:
207
+ - 同一输入 → 同一 manifest(确定性)
208
+ - 可选路径对应的文件不存在时,不加入 manifest(非 null)
209
+ - **不再使用 `<DATA>` 标签包裹**:subagent 自行读取文件,agent frontmatter 中的安全约束已覆盖注入防护
210
+
211
+ #### Step 2.7: M3 风格漂移与黑名单(文件协议)
212
+
213
+ 定义 `style-drift.json`、`ai-blacklist.json` 扩展字段、`lint-blacklist.sh` 脚本接口。
214
+
215
+ 详见 `references/file-protocols.md`。
216
+
217
+ ### Step 3: 逐章流水线
218
+
219
+ 对每一章执行以下 Agent 链:
220
+
221
+ ```
222
+ for chapter_num in range(start, start + remaining_N):
223
+ # remaining_N = N - (1 if inflight_chapter was recovered else 0)
224
+
225
+ 0. 获取并发锁(见 `docs/dr-workflow/novel-writer-tool/final/prd/10-protocols.md` §10.7):
226
+ - 原子获取:mkdir .novel.lock(已存在则失败)
227
+ - 获取失败:
228
+ - 读取 `.novel.lock/info.json` 报告持有者信息(pid/started/chapter)
229
+ - 若 `started` 距当前时间 > 30 分钟,视为僵尸锁 → `rm -rf .novel.lock` 后重试一次
230
+ - 否则提示用户存在并发执行,拒绝继续(避免 staging 写入冲突)
231
+ - 写入 `.novel.lock/info.json`:`{"pid": <PID>, "started": "<ISO-8601>", "chapter": <N>}`
232
+ 更新 checkpoint: pipeline_stage = "drafting", inflight_chapter = chapter_num
233
+
234
+ 1. ChapterWriter Agent → 生成初稿
235
+ 输入: chapter_writer_manifest(inline 计算值 + 文件路径;Agent 自行 Read 文件)
236
+ 输出: staging/chapters/chapter-{C:03d}.md(+ 可选 hints,自然语言状态提示)
237
+
238
+ 2. Summarizer Agent → 生成摘要 + 权威状态增量 + 串线检测
239
+ 输入: summarizer_manifest(inline 计算值 + 文件路径)
240
+ 输出: staging/summaries/chapter-{C:03d}-summary.md + staging/state/chapter-{C:03d}-delta.json + staging/state/chapter-{C:03d}-crossref.json + staging/storylines/{storyline_id}/memory.md
241
+ 更新 checkpoint: pipeline_stage = "drafted"
242
+
243
+ 3. StyleRefiner Agent → 去 AI 化润色
244
+ 输入: style_refiner_manifest(inline 计算值 + 文件路径)
245
+ 输出: staging/chapters/chapter-{C:03d}.md(覆盖)
246
+ 更新 checkpoint: pipeline_stage = "refined"
247
+
248
+ 4. QualityJudge Agent → 质量评估(双轨验收)
249
+ (可选确定性工具)中文 NER 实体抽取(用于一致性/LS-001 辅助信号):
250
+ - 若存在 `${NOVEL_CLI_ROOT}/scripts/run-ner.sh`:
251
+ - 执行:`bash ${NOVEL_CLI_ROOT}/scripts/run-ner.sh staging/chapters/chapter-{C:03d}.md`
252
+ - 若退出码为 0 且 stdout 为合法 JSON → 记为 `ner_entities_json`,写入 quality_judge_manifest.ner_entities
253
+ - 若脚本不存在/失败/输出非 JSON → `ner_entities_json = null`,不得阻断流水线(QualityJudge 回退 LLM 抽取 + confidence)
254
+ (可选)注入最近一致性检查摘要(供 LS-001 参考,不直接替代正文判断):
255
+ - 若存在 `logs/continuity/latest.json`:
256
+ - Read 并裁剪为小体积 JSON(仅保留 scope/chapter_range + 与 timeline/location 相关的 high/medium issues,最多 5 条,含 evidence)
257
+ - 注入到 quality_judge_manifest.continuity_report_summary
258
+ - 若文件不存在/读取失败/JSON 无效 → continuity_report_summary = null,不得阻断流水线
259
+ (可选确定性工具)黑名单精确命中统计:
260
+ - 若存在 `${NOVEL_CLI_ROOT}/scripts/lint-blacklist.sh`:
261
+ - 执行:`bash ${NOVEL_CLI_ROOT}/scripts/lint-blacklist.sh staging/chapters/chapter-{C:03d}.md ai-blacklist.json`
262
+ - 若退出码为 0 且 stdout 为合法 JSON → 记为 `blacklist_lint_json`,写入 quality_judge_manifest.blacklist_lint
263
+ - 若脚本不存在/失败/输出非 JSON → `blacklist_lint_json = null`,不得阻断流水线(回退 LLM 估计)
264
+ 输入: quality_judge_manifest(inline 计算值 + 文件路径;cross_references 来自 staging/state/chapter-{C:03d}-crossref.json)
265
+ 返回: 结构化 eval JSON(QualityJudge 只读,不落盘)
266
+ 关键章双裁判:
267
+ - 关键章判定:
268
+ - 卷首章:chapter_num == chapter_start
269
+ - 卷尾章:chapter_num == chapter_end
270
+ - 交汇事件章:chapter_num 落在任一 storyline_schedule.convergence_events.chapter_range(含边界)内(若某 event 的 chapter_range 缺失或为 null,跳过该 event)
271
+ - 若为关键章:使用 Task(subagent_type="quality-judge", model="opus") 再调用一次 QualityJudge 得到 secondary_eval
272
+ - 最坏情况合并(用于门控):
273
+ - overall_final = min(primary_eval.overall, secondary_eval.overall)
274
+ - has_high_confidence_violation = high_violation(primary_eval) OR high_violation(secondary_eval)
275
+ - eval_used = overall 更低的一次(primary/secondary;若相等,优先使用 secondary_eval——更强模型的判断)
276
+ - 记录:primary/secondary 的 model + overall + eval_used + overall_final(写入 eval metadata 与 logs,便于回溯差异与成本)
277
+ 普通章:
278
+ - overall_final = primary_eval.overall
279
+ - has_high_confidence_violation = high_violation(primary_eval)
280
+ - eval_used = primary_eval
281
+ 更新 checkpoint: pipeline_stage = "judged"
282
+
283
+ 5. 质量门控决策(Gate Decision Engine):
284
+ 门控决策(详见 `references/gate-decision.md`):
285
+ - overall ≥ 4.0 且无 high-confidence violation → pass
286
+ - overall ≥ 3.5 → polish(StyleRefiner 二次润色)
287
+ - overall ≥ 3.0 → revise(ChapterWriter Opus 修订,最多 2 轮)
288
+ - overall ≥ 2.0 → review(暂停,通知用户审核)
289
+ - overall < 2.0 → rewrite(强制重写,暂停)
290
+ - 修订上限 2 次后 overall ≥ 3.0 → force_passed
291
+
292
+ 6. 事务提交(staging → 正式目录):
293
+ - 移动 staging/chapters/chapter-{C:03d}.md → chapters/chapter-{C:03d}.md
294
+ - 移动 staging/summaries/chapter-{C:03d}-summary.md → summaries/
295
+ - 移动 staging/evaluations/chapter-{C:03d}-eval.json → evaluations/
296
+ - 移动 staging/storylines/{storyline_id}/memory.md → storylines/{storyline_id}/memory.md
297
+ - 移动 staging/state/chapter-{C:03d}-crossref.json → state/chapter-{C:03d}-crossref.json(保留跨线泄漏审计数据)
298
+ - 合并 state delta: 校验 ops(§10.6)→ 逐条应用 → state_version += 1 → 追加 state/changelog.jsonl
299
+ - 更新 foreshadowing/global.json(从 foreshadow ops 提取;幂等合并,详见 `references/foreshadowing.md`):
300
+ - 读取 `staging/state/chapter-{C:03d}-delta.json`,筛选 `ops[]` 中 `op=="foreshadow"` 的记录
301
+ - 读取 `foreshadowing/global.json`(不存在则初始化为 `{"foreshadowing":[]}`)
302
+ - 读取(可选)`volumes/vol-{V:02d}/foreshadowing.json`(用于在 global 缺条目/缺元数据时回填 `description/scope/target_resolve_range`;不得覆盖既有事实字段)
303
+ - 对每条 foreshadow op(按 ops 顺序)更新对应条目:
304
+ - `history` 以 `{chapter:C, action:value}` 去重后追加 `{chapter, action, detail}`
305
+ - `status` 单调推进(resolved > advanced > planted;不得降级)
306
+ - `planted_chapter`/`planted_storyline` 仅在 planted/缺失时回填;`last_updated_chapter` 取 max
307
+ - 写回 `foreshadowing/global.json`(JSON,UTF-8)
308
+ - 处理 unknown_entities: 从 Summarizer 输出提取 unknown_entities,追加写入 logs/unknown-entities.jsonl;若累计 ≥ 3 个未注册实体,在本章输出中警告用户
309
+ - 更新 .checkpoint.json(last_completed_chapter + 1, pipeline_stage = "committed", inflight_chapter = null, revision_count = 0)
310
+ - 状态转移:
311
+ - 若 chapter_num == chapter_end:更新 `.checkpoint.json.orchestrator_state = “VOL_REVIEW”` 并提示用户运行 `/novel:start` 执行卷末回顾
312
+ - 否则:更新 `.checkpoint.json.orchestrator_state = “WRITING”`(若本章来自 CHAPTER_REWRITE,则回到 WRITING)
313
+ - 写入 logs/chapter-{C:03d}-log.json(stages 耗时/模型、gate_decision、revisions、force_passed;关键章额外记录 primary/secondary judge 的 model+overall 与 overall_final;token/cost 为估算值或 null,见降级说明)
314
+ - 清空 staging/ 本章文件
315
+ - 释放并发锁: rm -rf .novel.lock
316
+
317
+ - **Step 3.7: M3 周期性维护(非阻断,详见 `references/periodic-maintenance.md`)**
318
+ - AI 黑名单动态维护:从 QualityJudge suggestions 读取候选 → 自动追加(confidence medium+high, count≥3, words<80)或记录候选
319
+ - 风格漂移检测(每 5 章):StyleAnalyzer 提取 metrics → 与基线对比 → 漂移则写入 style-drift.json / 回归则清除 / 超时(>15章)则 stale_timeout
320
+
321
+ 7. 输出本章结果:
322
+ > 第 {C} 章已生成({word_count} 字),评分 {overall_final}/5.0,门控 {gate_decision},修订 {revision_count} 次 {pass_icon}
323
+ ```
324
+
325
+ ### Step 4: 定期检查触发
326
+
327
+ - 每完成 5 章(last_completed_chapter % 5 == 0):输出质量简报(均分 + 低分章节 + 主要风险)+ 风格漂移检测结果(是否生成/清除 style-drift.json)+ 一致性滑窗审计(stride=5, window=10,更新 `logs/continuity/latest.json`),并提示用户可运行 `/novel:start` 进入“质量回顾/调整方向”
328
+ - 每完成 10 章(last_completed_chapter % 10 == 0):触发周期性盘点提醒(建议运行 `/novel:start` → “质量回顾”,将汇总展示:
329
+ - 一致性报告:`logs/continuity/latest.json` 与 `logs/continuity/continuity-report-*.json`(每 5 章自动更新)
330
+ - 伏笔可见度/盘点与桥梁检查:`logs/foreshadowing/latest.json`(可见度)/`logs/foreshadowing/foreshadowing-check-latest.json`(盘点)、`logs/storylines/broken-bridges-latest.json`
331
+ - 故事线节奏分析:`logs/storylines/rhythm-latest.json`)
332
+ - 到达本卷末尾章节:提示用户执行 `/novel:start` 进行卷末回顾
333
+
334
+ ### Step 5: 汇总输出
335
+
336
+ 多章模式下汇总:
337
+ ```
338
+ 续写完成:
339
+ Ch {X}: {字数}字 {分数} {状态} | Ch {X+1}: {字数}字 {分数} {状态} | ...
340
+ ```
341
+
342
+ ## 约束
343
+
344
+ - 每章严格按 ChapterWriter → Summarizer → StyleRefiner → QualityJudge 顺序
345
+ - 质量不达标时自动修订最多 2 次
346
+ - 写入使用 staging → commit 事务模式(详见 Step 2-6)
347
+ - **Agent 写入边界**:所有 Agent(ChapterWriter/Summarizer/StyleRefiner)仅写入 `staging/` 目录,正式目录(`chapters/`、`summaries/`、`state/`、`storylines/`、`evaluations/`)由入口 Skill 在 commit 阶段操作。QualityJudge 为只读,不写入任何文件
348
+ - 所有输出使用中文