@oh-story/dsh 0.1.4 → 0.1.6

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 (479) hide show
  1. package/README.md +56 -12
  2. package/lib/client.js +2707 -268
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +136 -101
  5. package/lib/drama/skills/short-drama/SKILL.md +61 -1
  6. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
  8. package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
  9. package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
  10. package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
  11. package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
  12. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  13. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  14. package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
  15. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  16. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
  17. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
  18. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  19. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
  21. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
  22. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
  23. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
  24. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
  25. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  26. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  27. package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
  28. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  29. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  30. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  31. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  32. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  33. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  34. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
  35. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  36. package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
  37. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  38. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
  39. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  40. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
  41. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
  42. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
  43. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  44. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
  45. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  46. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  47. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
  50. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
  51. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  53. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
  54. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  55. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  56. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  57. package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
  58. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  59. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  60. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  61. package/lib/index.js +865 -33
  62. package/lib/index.js.map +4 -4
  63. package/lib/novel-to-game/LICENSE +21 -0
  64. package/lib/novel-to-game/README.md +180 -0
  65. package/lib/novel-to-game/README_ZH.md +178 -0
  66. package/lib/novel-to-game/VERSION +1 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  91. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  92. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  93. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  94. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  95. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  96. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  97. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  98. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  99. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  100. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  101. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  102. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  103. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  104. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  128. package/lib/novel-to-game/manifest.json +508 -0
  129. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  130. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  131. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  132. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  133. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  134. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  135. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  136. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  137. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  138. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  139. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  140. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  141. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  142. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  143. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  144. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  145. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  146. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  147. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  148. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  149. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  150. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  151. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  152. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  153. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  154. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  155. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  156. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  157. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  158. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  159. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  160. package/lib/oh-story/manifest.json +415 -305
  161. package/lib/oh-story/roles/consistency-checker.md +1 -1
  162. package/lib/oh-story/roles/narrative-writer.md +32 -33
  163. package/lib/oh-story/roles/story-architect.md +16 -26
  164. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  165. package/lib/oh-story/skills/story/SKILL.md +16 -9
  166. package/lib/oh-story/skills/story/VERSION +1 -1
  167. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  168. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  169. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  170. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  171. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  172. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  173. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  174. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  175. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  176. package/lib/oh-story/skills/story-import/SKILL.md +19 -10
  177. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  180. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  181. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  182. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  183. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  184. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  185. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  186. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  187. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  188. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  189. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  190. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  191. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  192. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  193. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  194. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  195. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  196. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  197. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  198. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
  199. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
  200. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  201. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  202. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  203. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  204. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  205. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  206. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
  207. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  208. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
  209. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  210. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  211. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  212. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  213. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  214. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  215. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  216. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  217. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  218. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  219. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  220. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  221. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  222. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  223. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  224. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  225. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  226. package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
  227. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  228. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  229. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  230. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  236. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  237. package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
  238. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  239. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  243. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  244. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  245. package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
  246. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  247. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  248. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  249. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  250. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  251. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  252. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  253. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  254. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  255. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  257. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  258. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  259. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  260. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  261. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  262. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  263. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  264. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  265. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  266. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  267. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  268. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  270. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  271. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  272. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  273. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  274. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  276. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  277. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
  278. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  279. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  280. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
  281. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
  282. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  283. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  284. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  285. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  286. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  287. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  288. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  289. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  290. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  291. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  292. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  293. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  294. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  295. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  296. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  297. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  298. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  299. package/lib/types/client/drama-production-view.d.ts +34 -0
  300. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  301. package/lib/types/client/drama-production.d.ts +92 -0
  302. package/lib/types/client/drama-production.d.ts.map +1 -0
  303. package/lib/types/client/file-activity.d.ts +4 -2
  304. package/lib/types/client/file-activity.d.ts.map +1 -1
  305. package/lib/types/client/index.d.ts +1 -1
  306. package/lib/types/client/index.d.ts.map +1 -1
  307. package/lib/types/client/production-intents.d.ts +9 -0
  308. package/lib/types/client/production-intents.d.ts.map +1 -0
  309. package/lib/types/client/production-prompts.d.ts +9 -0
  310. package/lib/types/client/production-prompts.d.ts.map +1 -0
  311. package/lib/types/client/production-runtime.d.ts +58 -0
  312. package/lib/types/client/production-runtime.d.ts.map +1 -0
  313. package/lib/types/client/video-studio.d.ts +41 -0
  314. package/lib/types/client/video-studio.d.ts.map +1 -0
  315. package/lib/types/client/workbench-ui.d.ts +5 -0
  316. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  317. package/lib/types/game-verification.d.ts +16 -0
  318. package/lib/types/game-verification.d.ts.map +1 -0
  319. package/lib/types/index.d.ts +3 -1
  320. package/lib/types/index.d.ts.map +1 -1
  321. package/lib/types/production-intent.d.ts +21 -0
  322. package/lib/types/production-intent.d.ts.map +1 -0
  323. package/lib/types/production-tool.d.ts +5 -0
  324. package/lib/types/production-tool.d.ts.map +1 -0
  325. package/lib/types/role-tool.d.ts.map +1 -1
  326. package/lib/types/skill-provider.d.ts +7 -0
  327. package/lib/types/skill-provider.d.ts.map +1 -1
  328. package/lib/types/video-project.d.ts +42 -0
  329. package/lib/types/video-project.d.ts.map +1 -0
  330. package/lib/types/workspace-request-trust.d.ts +6 -0
  331. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  332. package/lib/types/workspace-route.d.ts +20 -0
  333. package/lib/types/workspace-route.d.ts.map +1 -1
  334. package/lib/video-recap/CHANGELOG.md +282 -0
  335. package/lib/video-recap/LICENSE +21 -0
  336. package/lib/video-recap/README.en.md +257 -0
  337. package/lib/video-recap/README.md +255 -0
  338. package/lib/video-recap/manifest.json +684 -0
  339. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  340. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  341. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  342. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  343. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  344. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  345. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  346. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  347. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  348. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  349. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  350. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  351. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  352. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  353. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  354. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  355. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  356. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  357. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  358. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  359. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  360. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  361. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  362. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  363. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  364. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  365. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  366. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  367. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  368. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  369. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  370. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  371. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  372. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  373. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  374. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  375. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  376. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  377. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  378. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  379. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  380. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  381. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  382. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  383. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  384. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  385. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  386. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  387. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  388. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  389. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  390. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  391. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  392. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  393. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  394. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  395. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  396. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  397. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  398. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  399. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  400. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  401. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  402. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  403. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  404. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  405. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  406. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  407. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  408. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  409. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  410. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  411. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  412. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  413. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  414. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  415. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  416. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  417. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  418. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  419. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  420. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  421. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  422. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  423. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  424. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  425. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  426. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  427. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  428. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  429. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  430. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  431. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  432. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  433. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  434. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  435. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  436. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  437. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  438. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  439. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  440. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  441. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  442. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  443. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  444. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  445. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  446. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  447. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  448. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  449. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  450. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  451. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  452. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  453. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  454. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  455. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  456. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  457. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  458. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  459. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  460. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  461. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  462. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  463. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  464. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  465. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  466. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  467. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  468. package/package.json +55 -40
  469. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  470. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  471. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  472. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  473. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  474. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  475. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  476. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  477. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  478. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  479. /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
@@ -3,25 +3,40 @@
3
3
 
4
4
  The language model supplies compact semantic JSON. This tool validates and
5
5
  merges that input in memory, renders every derived view, then atomically writes
6
- ``_tracking-state.json`` last as the single commit point. One book project has
7
- one serial writer; concurrent commits are intentionally unsupported.
6
+ ``_tracking-state.json`` last as the single commit point. Per-project locking
7
+ serializes concurrent writers before revision and wordcount checks.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
12
  import argparse
13
13
  import copy
14
+ import importlib.util
14
15
  import json
15
16
  import os
16
17
  import re
17
18
  import stat
18
19
  import sys
19
20
  import tempfile
21
+ import time
20
22
  import unicodedata
23
+ from contextlib import contextmanager
21
24
  from pathlib import Path
22
25
  from typing import Any
23
26
 
24
27
 
28
+ _WORDCOUNT_CORE_PATH = Path(__file__).with_name("wordcount_core.py")
29
+ if not _WORDCOUNT_CORE_PATH.is_file(): # pragma: no cover - broken deployment
30
+ raise RuntimeError("TOOL_UNAVAILABLE: wordcount_core.py")
31
+ _WORDCOUNT_CORE_SPEC = importlib.util.spec_from_file_location(
32
+ "story_wordcount_core", _WORDCOUNT_CORE_PATH
33
+ )
34
+ if _WORDCOUNT_CORE_SPEC is None or _WORDCOUNT_CORE_SPEC.loader is None: # pragma: no cover
35
+ raise RuntimeError("unable to load wordcount core")
36
+ wordcount_core = importlib.util.module_from_spec(_WORDCOUNT_CORE_SPEC)
37
+ _WORDCOUNT_CORE_SPEC.loader.exec_module(wordcount_core)
38
+
39
+
25
40
  INPUT_SCHEMA_VERSION = 1
26
41
  TRACKING_SCHEMA_VERSION = 4
27
42
  DELTA_TARGET_BYTES = 1536
@@ -74,6 +89,13 @@ def require(condition: bool, message: str) -> None:
74
89
  raise TrackingError(message)
75
90
 
76
91
 
92
+ def wordcount_value(function: Any, *args: Any, **kwargs: Any) -> Any:
93
+ try:
94
+ return function(*args, **kwargs)
95
+ except wordcount_core.WordcountError as exc:
96
+ raise TrackingError(str(exc)) from exc
97
+
98
+
77
99
  def as_mapping(value: object, label: str) -> dict[str, Any]:
78
100
  require(isinstance(value, dict), f"{label} must be a JSON object")
79
101
  return value
@@ -188,6 +210,34 @@ def state_path(project: Path) -> Path:
188
210
  return tracking_root(project) / "_tracking-state.json"
189
211
 
190
212
 
213
+ @contextmanager
214
+ def project_write_lock(project: Path, *, timeout_seconds: float = 10.0):
215
+ tracking = tracking_root(project)
216
+ tracking.mkdir(parents=True, exist_ok=True)
217
+ path = tracking / ".tracking-commit.lock"
218
+ deadline = time.monotonic() + timeout_seconds
219
+ descriptor: int | None = None
220
+ while descriptor is None:
221
+ try:
222
+ descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
223
+ except FileExistsError:
224
+ if time.monotonic() >= deadline:
225
+ raise TrackingError(
226
+ "tracking commit lock is busy or stale; retry, or remove 追踪/.tracking-commit.lock after confirming no commit is running"
227
+ )
228
+ time.sleep(0.05)
229
+ try:
230
+ os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
231
+ os.fsync(descriptor)
232
+ yield
233
+ finally:
234
+ os.close(descriptor)
235
+ try:
236
+ path.unlink()
237
+ except FileNotFoundError:
238
+ pass
239
+
240
+
191
241
  def delta_path(tracking: Path, chapter: int) -> Path:
192
242
  width = max(3, len(str(chapter)))
193
243
  return tracking / "逐章记录" / f"第{chapter:0{width}d}章.md"
@@ -735,6 +785,18 @@ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: se
735
785
  return payload
736
786
 
737
787
 
788
+ def normalize_wordcount_records(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
789
+ records = as_mapping(value, "tracking state.wordcount_records")
790
+ normalized: dict[str, dict[str, Any]] = {}
791
+ for raw_chapter, raw_record in records.items():
792
+ require(isinstance(raw_chapter, str) and re.fullmatch(r"[1-9]\d*", raw_chapter) is not None,
793
+ "wordcount record chapter key is invalid")
794
+ chapter = int(raw_chapter)
795
+ require(chapter <= last_chapter, "wordcount record exceeds last committed chapter")
796
+ normalized[raw_chapter] = wordcount_value(wordcount_core.normalize_wordcount_record, raw_record)
797
+ return normalized
798
+
799
+
738
800
  def normalize_state(document: object) -> dict[str, Any]:
739
801
  root = as_mapping(document, "tracking state")
740
802
  require_known_keys(
@@ -742,6 +804,7 @@ def normalize_state(document: object) -> dict[str, Any]:
742
804
  {
743
805
  "schema_version", "book_title", "last_committed_chapter", "imported_through_chapter",
744
806
  "state_revision", "context", "characters", "foreshadow", "timeline",
807
+ "wordcount_records",
745
808
  },
746
809
  "tracking state",
747
810
  )
@@ -766,16 +829,19 @@ def normalize_state(document: object) -> dict[str, Any]:
766
829
  if last_chapter == 0:
767
830
  require(not foreshadow, "a chapter-0 project cannot have planted foreshadow facts")
768
831
  require(not timeline, "a chapter-0 project cannot have established timeline facts")
832
+ state_revision = as_int(root.get("state_revision"), "tracking state.state_revision")
833
+ wordcount_records = normalize_wordcount_records(root.get("wordcount_records", {}), last_chapter)
769
834
  return {
770
835
  "schema_version": TRACKING_SCHEMA_VERSION,
771
836
  "book_title": clean_text(root.get("book_title"), "tracking state.book_title", max_bytes=240),
772
837
  "last_committed_chapter": last_chapter,
773
838
  "imported_through_chapter": imported_through,
774
- "state_revision": as_int(root.get("state_revision"), "tracking state.state_revision"),
839
+ "state_revision": state_revision,
775
840
  "context": context,
776
841
  "characters": characters,
777
842
  "foreshadow": foreshadow,
778
843
  "timeline": timeline,
844
+ "wordcount_records": wordcount_records,
779
845
  }
780
846
 
781
847
 
@@ -789,7 +855,10 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
789
855
  root = as_mapping(document, "init input")
790
856
  require_known_keys(
791
857
  root,
792
- {"schema_version", "book_title", "last_chapter", "context", "character_snapshots", "foreshadow", "timeline_events"},
858
+ {
859
+ "schema_version", "book_title", "last_chapter", "context", "character_snapshots",
860
+ "foreshadow", "timeline_events",
861
+ },
793
862
  "init input",
794
863
  )
795
864
  require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "init input schema_version is unsupported")
@@ -826,17 +895,18 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
826
895
  "characters": snapshots,
827
896
  "foreshadow": foreshadow,
828
897
  "timeline": timeline,
898
+ "wordcount_records": {},
829
899
  }
830
900
  )
831
901
 
832
902
 
833
- def normalize_transaction(state: dict[str, Any], document: object) -> dict[str, Any]:
903
+ def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
834
904
  root = as_mapping(document, "transaction")
835
905
  require_known_keys(
836
906
  root,
837
907
  {
838
908
  "schema_version", "mode", "chapter", "chapter_title", "expected_state_revision",
839
- "delta", "context", "character_snapshots",
909
+ "delta", "context", "character_snapshots", "wordcount",
840
910
  },
841
911
  "transaction",
842
912
  )
@@ -845,6 +915,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
845
915
  require(mode in {"append", "revision"}, "mode must be append or revision")
846
916
  chapter = as_int(root.get("chapter"), "chapter", minimum=1)
847
917
  expected_revision = as_int(root.get("expected_state_revision"), "expected_state_revision")
918
+ wordcount_input = root.get("wordcount")
848
919
  require(expected_revision == state["state_revision"], "tracking state changed since this transaction was prepared")
849
920
  last = state["last_committed_chapter"]
850
921
  if mode == "append":
@@ -864,6 +935,11 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
864
935
  snapshots=snapshots,
865
936
  existing_core_names=existing_names,
866
937
  )
938
+ wordcount = None
939
+ if wordcount_input is not None:
940
+ wordcount = wordcount_value(
941
+ wordcount_core.validate_current_wordcount_record, project, chapter, wordcount_input
942
+ )
867
943
  return {
868
944
  "mode": mode,
869
945
  "chapter": chapter,
@@ -871,6 +947,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
871
947
  "delta": delta,
872
948
  "context": context,
873
949
  "snapshots": snapshots,
950
+ "wordcount": wordcount,
874
951
  }
875
952
 
876
953
 
@@ -891,6 +968,8 @@ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dic
891
968
  next_state["last_committed_chapter"] = chapter
892
969
  next_state["state_revision"] += 1
893
970
  next_state["characters"].update(transaction["snapshots"])
971
+ if transaction["wordcount"] is not None:
972
+ next_state["wordcount_records"][str(chapter)] = transaction["wordcount"]
894
973
 
895
974
  next_context = transaction["context"]
896
975
  # 退役说的是「从此刻起离开当前状态」,只有 append 的逐章记录代表此刻;
@@ -1013,7 +1092,7 @@ def warn_sizes(views: dict[str, str], delta_payload: str | None = None) -> None:
1013
1092
  )
1014
1093
 
1015
1094
 
1016
- def initialize(project: Path, document: object) -> dict[str, Any]:
1095
+ def _initialize_locked(project: Path, document: object) -> dict[str, Any]:
1017
1096
  tracking = tracking_root(project)
1018
1097
  require(not state_path(project).exists(), "tracking state already exists; init never overwrites project state")
1019
1098
  state = normalize_initial_document(document)
@@ -1036,11 +1115,16 @@ def initialize(project: Path, document: object) -> dict[str, Any]:
1036
1115
  return state
1037
1116
 
1038
1117
 
1039
- def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1118
+ def initialize(project: Path, document: object) -> dict[str, Any]:
1119
+ with project_write_lock(project):
1120
+ return _initialize_locked(project, document)
1121
+
1122
+
1123
+ def _apply_transaction_locked(project: Path, document: object) -> dict[str, Any]:
1040
1124
  tracking = tracking_root(project)
1041
1125
  require_no_retired_tracking_paths(tracking)
1042
1126
  state = load_state(project)
1043
- transaction = normalize_transaction(state, document)
1127
+ transaction = normalize_transaction(project, state, document)
1044
1128
  next_state = merge_transaction(state, transaction)
1045
1129
 
1046
1130
  delta_payload = render_delta(
@@ -1067,6 +1151,11 @@ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1067
1151
  return next_state
1068
1152
 
1069
1153
 
1154
+ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1155
+ with project_write_lock(project):
1156
+ return _apply_transaction_locked(project, document)
1157
+
1158
+
1070
1159
  def check_project(project: Path) -> dict[str, Any]:
1071
1160
  tracking = tracking_root(project)
1072
1161
  require_no_retired_tracking_paths(tracking)
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """Small deterministic wordcount core shared by storyctl and tracking."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ WORDCOUNT_SCHEMA = "story-wordcount-result/v1"
13
+ MEASUREMENT_SCHEMA = "story-wordcount-measurement/v1"
14
+ CHECKPOINT_SCHEMA = "story-wordcount-checkpoint/v1"
15
+ METRIC = "visible_chars_v1"
16
+ RESOLUTIONS = frozenset({"within_user_band", "accepted_current_length"})
17
+
18
+ _WHITE_SPACE_CODEPOINTS = frozenset(
19
+ [*range(0x0009, 0x000E)]
20
+ + [
21
+ 0x0020, 0x0085, 0x00A0, 0x1680, *range(0x2000, 0x200B),
22
+ 0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
23
+ ]
24
+ )
25
+ _FRONTMATTER_KEY_RE = re.compile(r"^[A-Za-z_\u3400-\u9FFF][^:\n]{0,80}:[ \t]*(?:.*)$")
26
+ _LEADING_BLANK_RE = re.compile(r"^[\u0009\u0020\u3000]*$")
27
+ _ATX_HEADING_RE = re.compile(r"^[\u0009\u0020]{0,3}#{1,6}[\u0009\u0020]+\S")
28
+ _POSITIVE_INTEGER_RE = re.compile(r"^[1-9]\d*$")
29
+ _TARGET_LINE_RE = re.compile(r"^[ \t>*-]*字数目标[ \t]*[::][ \t]*([1-9]\d*)[ \t]*(?:字)?[ \t]*$", re.MULTILINE)
30
+ _METRIC_LINE_RE = re.compile(r"^[ \t>*-]*字数口径[ \t]*[::][ \t]*([A-Za-z0-9_-]+)[ \t]*$", re.MULTILINE)
31
+
32
+
33
+ class WordcountError(ValueError):
34
+ """Expected deterministic wordcount contract failure."""
35
+
36
+
37
+ def require(condition: bool, message: str) -> None:
38
+ if not condition:
39
+ raise WordcountError(message)
40
+
41
+
42
+ def normalize_newlines(value: str) -> str:
43
+ return value.replace("\r\n", "\n").replace("\r", "\n")
44
+
45
+
46
+ def strip_recognizable_frontmatter(value: str) -> str:
47
+ lines = value.split("\n")
48
+ if not lines or lines[0] != "---":
49
+ return value
50
+ closing = next((index for index in range(1, min(len(lines), 201)) if lines[index] in {"---", "..."}), -1)
51
+ if closing < 2 or not any(_FRONTMATTER_KEY_RE.match(line) for line in lines[1:closing]):
52
+ return value
53
+ return "\n".join(lines[closing + 1 :])
54
+
55
+
56
+ def visible_body(value: str) -> str:
57
+ if not isinstance(value, str):
58
+ raise TypeError("body must be a string")
59
+ text = normalize_newlines(value)
60
+ if text.startswith("\ufeff"):
61
+ text = text[1:]
62
+ lines = strip_recognizable_frontmatter(text).split("\n")
63
+ while lines and _LEADING_BLANK_RE.match(lines[0]):
64
+ lines.pop(0)
65
+ if lines and _ATX_HEADING_RE.match(lines[0]):
66
+ lines.pop(0)
67
+ return "\n".join(lines)
68
+
69
+
70
+ def count_visible_chars(value: str) -> int:
71
+ return sum(ord(character) not in _WHITE_SPACE_CODEPOINTS for character in visible_body(value))
72
+
73
+
74
+ def parse_target(value: Any) -> int:
75
+ raw = str(value if value is not None else "")
76
+ if not _POSITIVE_INTEGER_RE.fullmatch(raw):
77
+ raise WordcountError("target must be a positive integer")
78
+ target = int(raw)
79
+ require(target <= 9_007_199_254_740_991, "target exceeds Number.MAX_SAFE_INTEGER")
80
+ return target
81
+
82
+
83
+ def compute_wordcount_bands(value: Any) -> dict[str, dict[str, int]]:
84
+ target = parse_target(value)
85
+ return {
86
+ "internal": {"min": (target * 88 + 99) // 100, "max": target * 112 // 100},
87
+ "user": {"min": (target * 85 + 99) // 100, "max": target * 115 // 100},
88
+ }
89
+
90
+
91
+ def invalid_wordcount_result(
92
+ reason: str, *, chapter: Any = None, case_id: Any = None,
93
+ target: int | None = None, actual: int | None = None,
94
+ ) -> dict[str, Any]:
95
+ return {
96
+ "schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
97
+ "target": target, "actual": actual, "internal_band": None, "user_band": None,
98
+ "signed_error_pct": None, "absolute_error_pct": None,
99
+ "status": "invalid", "invalid_reason": reason,
100
+ }
101
+
102
+
103
+ def measure_wordcount(value: str, *, chapter: Any = None, case_id: Any = None) -> dict[str, Any]:
104
+ try:
105
+ actual = count_visible_chars(value)
106
+ except (TypeError, ValueError):
107
+ return {
108
+ "schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
109
+ "case_id": case_id, "actual": None, "status": "invalid", "invalid_reason": "INVALID_BODY",
110
+ }
111
+ return {
112
+ "schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
113
+ "case_id": case_id, "actual": actual, "status": "measured", "invalid_reason": None,
114
+ }
115
+
116
+
117
+ def evaluate_wordcount(
118
+ value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
119
+ ) -> dict[str, Any]:
120
+ try:
121
+ target = parse_target(target_value)
122
+ except (TypeError, ValueError):
123
+ return invalid_wordcount_result("INVALID_TARGET", chapter=chapter, case_id=case_id)
124
+ try:
125
+ actual = count_visible_chars(value)
126
+ except (TypeError, ValueError):
127
+ return invalid_wordcount_result("INVALID_BODY", chapter=chapter, case_id=case_id, target=target)
128
+ if actual == 0:
129
+ return invalid_wordcount_result("EMPTY_BODY", chapter=chapter, case_id=case_id, target=target, actual=actual)
130
+ bands = compute_wordcount_bands(target)
131
+ internal_pass = bands["internal"]["min"] <= actual <= bands["internal"]["max"]
132
+ user_pass = bands["user"]["min"] <= actual <= bands["user"]["max"]
133
+ status = "internal_pass" if internal_pass else (
134
+ "borderline" if user_pass else ("under" if actual < bands["user"]["min"] else "over")
135
+ )
136
+ signed_error = (actual - target) / target
137
+ return {
138
+ "schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
139
+ "target": target, "actual": actual,
140
+ "internal_band": {**bands["internal"], "status": "pass" if internal_pass else "fail"},
141
+ "user_band": {**bands["user"], "status": "pass" if user_pass else "fail"},
142
+ "signed_error_pct": signed_error, "absolute_error_pct": abs(signed_error),
143
+ "status": status, "invalid_reason": None,
144
+ }
145
+
146
+
147
+ def checkpoint_wordcount(
148
+ value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
149
+ ) -> dict[str, Any]:
150
+ target = parse_target(target_value)
151
+ actual = count_visible_chars(value)
152
+ user = compute_wordcount_bands(target)["user"]
153
+ return {
154
+ "schema": CHECKPOINT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
155
+ "target": target, "actual": actual, "user_band": user,
156
+ "remaining_user_range": {
157
+ "min": max(0, user["min"] - actual),
158
+ "max": max(0, user["max"] - actual),
159
+ },
160
+ }
161
+
162
+
163
+ def target_from_outline(value: str) -> int:
164
+ text = normalize_newlines(value)
165
+ if text.startswith("\ufeff"):
166
+ text = text[1:]
167
+ targets = list(dict.fromkeys(_TARGET_LINE_RE.findall(text)))
168
+ metrics = list(dict.fromkeys(_METRIC_LINE_RE.findall(text)))
169
+ require(len(targets) == 1, "字数目标 must appear exactly once with one value")
170
+ require(metrics == [METRIC], f"字数口径 must appear exactly once as {METRIC}")
171
+ return parse_target(targets[0])
172
+
173
+
174
+ def _chapter_number_from_name(name: str, *, outline: bool) -> int | None:
175
+ pattern = r"^细纲_第0*(\d+)章.*\.md$" if outline else r"^第0*(\d+)章(?:[_\-  ].*)?\.md$"
176
+ match = re.match(pattern, name)
177
+ return int(match.group(1)) if match else None
178
+
179
+
180
+ def find_chapter_file(directory: Path, chapter: int, *, outline: bool) -> Path:
181
+ require(isinstance(chapter, int) and not isinstance(chapter, bool) and chapter >= 1, "chapter must be >= 1")
182
+ require(directory.is_dir(), f"chapter directory is missing: {directory}")
183
+ matches = sorted(
184
+ path for path in directory.iterdir()
185
+ if path.is_file() and _chapter_number_from_name(path.name, outline=outline) == chapter
186
+ )
187
+ label = "outline" if outline else "body"
188
+ require(len(matches) == 1, f"chapter {chapter} must have exactly one {label} file")
189
+ return matches[0]
190
+
191
+
192
+ def build_project_wordcount_record(project: Path, chapter: int, *, resolution: str) -> dict[str, Any]:
193
+ require(resolution in RESOLUTIONS, f"unsupported wordcount resolution: {resolution}")
194
+ root = project.resolve()
195
+ outline_path = find_chapter_file(root / "大纲", chapter, outline=True)
196
+ body_path = find_chapter_file(root / "正文", chapter, outline=False)
197
+ try:
198
+ target = target_from_outline(outline_path.read_text(encoding="utf-8"))
199
+ body_bytes = body_path.read_bytes()
200
+ body = body_bytes.decode("utf-8")
201
+ except (OSError, UnicodeError) as exc:
202
+ raise WordcountError(f"unable to read chapter files: {exc}") from exc
203
+ result = evaluate_wordcount(body, target, chapter=chapter)
204
+ require(result["status"] != "invalid", f"invalid chapter wordcount: {result['invalid_reason']}")
205
+ in_user_band = result["status"] in {"internal_pass", "borderline"}
206
+ require(
207
+ (resolution == "within_user_band" and in_user_band)
208
+ or (resolution == "accepted_current_length" and not in_user_band),
209
+ "wordcount resolution does not match the current length",
210
+ )
211
+ return {
212
+ "metric": METRIC,
213
+ "target": result["target"],
214
+ "actual": result["actual"],
215
+ "status": result["status"],
216
+ "resolution": resolution,
217
+ "body_sha256": hashlib.sha256(body_bytes).hexdigest(),
218
+ }
219
+
220
+
221
+ def normalize_wordcount_record(value: object) -> dict[str, Any]:
222
+ require(isinstance(value, dict), "wordcount record must be an object")
223
+ require(
224
+ set(value) == {"metric", "target", "actual", "status", "resolution", "body_sha256"},
225
+ "wordcount record fields are invalid",
226
+ )
227
+ require(value.get("metric") == METRIC, "wordcount metric is unsupported")
228
+ target = parse_target(value.get("target"))
229
+ actual = value.get("actual")
230
+ require(isinstance(actual, int) and not isinstance(actual, bool) and actual >= 1, "wordcount actual is invalid")
231
+ expected = evaluate_wordcount("字" * actual, target)
232
+ require(value.get("status") == expected["status"], "wordcount status does not match target and actual")
233
+ resolution = value.get("resolution")
234
+ require(resolution in RESOLUTIONS, "wordcount resolution is unsupported")
235
+ in_user_band = expected["status"] in {"internal_pass", "borderline"}
236
+ require((resolution == "within_user_band") == in_user_band, "wordcount resolution does not match status")
237
+ digest = value.get("body_sha256")
238
+ require(isinstance(digest, str) and re.fullmatch(r"[0-9a-f]{64}", digest) is not None, "body_sha256 is invalid")
239
+ return dict(value)
240
+
241
+
242
+ def validate_current_wordcount_record(project: Path, chapter: int, value: object) -> dict[str, Any]:
243
+ record = normalize_wordcount_record(value)
244
+ current = build_project_wordcount_record(project, chapter, resolution=record["resolution"])
245
+ require(record == current, "wordcount record is stale for the current body or target")
246
+ return record
@@ -2,16 +2,22 @@
2
2
  name: story-review
3
3
  version: 1.1.1
4
4
  description: "多视角对抗式审查。full/lean 模式在已部署 reviewer agents 时并行 spawn;缺失/异常 agents 或 spawn 失败时自动降级 solo,参考文件不可读时使用内置 rubric fallback。触发方式:/story-review、/审查、「审查一下」「帮我审一下」。"
5
- metadata: {"openclaw":{"source":"https://github.com/worldwonderer/oh-story-claudecode"}}
5
+ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}}
6
6
  ---
7
7
  # story-review:多视角对抗式审查
8
8
 
9
- > Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 25` 不一致时(标记缺失、字段缺失/非整数、小于或大于 25)**照常按文件存在性检查并 spawn**,同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 25)` 并提示重新运行 `/story-setup` 后新开会话;大于 25 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。
9
+ > Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 29` 不一致时(标记缺失、字段缺失/非整数、小于或大于 29)**照常按文件存在性检查并 spawn**,但只检查当前运行时的 canonical 目录;同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 29)` 并提示重新运行 `/story-setup` 后新开会话;大于 29 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。
10
10
 
11
11
  你是审查协调器。你的职责是找出小说文本中的结构、角色、文字、设定问题,并给出可执行修改建议。
12
12
 
13
13
  **执行铁律:审查是找问题,不是验证正确性。**
14
14
 
15
+ ## 作者习惯边界
16
+
17
+ 若作者记忆 state 已存在,审查前用 `scripts/author_memory_commit.py query` 获取本次相关 active 条目(总输出 ≤2KB)。它们只能帮助解释意图和组织报告,不能降低 rubric 严重度、把事实冲突判为无问题或跳过平台门禁;当前请求仍优先。完整规则见 [references/author-memory.md](references/author-memory.md)。
18
+
19
+ 用户对报告格式或协作方式作出稳定声明时,在本轮审查完成后用 `record` 记录并回传回执;重复修正/推断先待确认,一次性要求不记录。审查发现、工具告警和助手建议本身绝不自动学习。
20
+
15
21
  ---
16
22
 
17
23
  ## Review Mode 选择
@@ -28,17 +34,18 @@ metadata: {"openclaw":{"source":"https://github.com/worldwonderer/oh-story-claud
28
34
  1. **确定请求模式**:解析用户输入中的 `full`、`lean`、`solo`;未指定时目标模式为 `full`。
29
35
  2. **确认是否允许 spawn**:如果当前已经在子代理/Agent 内执行,不再递归 spawn,直接降级为 `solo`。
30
36
  3. **识别 ZCode 能力边界**:如果当前运行于 ZCode 且项目使用 `.zcode/`,ZCode 3.3.4 不执行项目/plugin custom agents;不要因为磁盘上存在其他端的 agent 文件就尝试同名 spawn,直接降级 `solo` 并报告 `Fallback: project custom agents unavailable -> solo`。
31
- 4. **检查核心 Agent 部署状态**(检查项目内 agents,同时兼容 Claude Code、OpenCode 和 Codex):
32
- - 优先检查 `.claude/agents/`,其次检查 `.opencode/agents/`,再检查 `.codex/agents/`;三个目录任一存在即视为已部署
33
- - full 必需:Claude/OpenCode 为 `story-architect.md`、`character-designer.md`、`narrative-writer.md`、`consistency-checker.md`;Codex 为同名 `.toml`
34
- - lean 必需:Claude/OpenCode 为 `story-architect.md`、`consistency-checker.md`;Codex 为同名 `.toml`
37
+ 4. **检查核心 Agent 部署状态**(只检查当前运行时的 canonical 目录,不因其他端文件存在而误判):
38
+ - Claude Code 检查 `.claude/agents/`,OpenCode 检查 `.opencode/agents/`,Codex 检查 `.codex/agents/`,Antigravity 检查 `.agents/agents/`
39
+ - full 必需 agent:`story-architect`、`character-designer`、`narrative-writer`、`consistency-checker`
40
+ - lean 必需 agent:`story-architect`、`consistency-checker`
35
41
  - 对每个必需 Agent 文件:
36
42
  - **Claude Code agent(`.claude/agents/`)**:读取 frontmatter,确认 `name:` 与 subagent_type 完全一致;frontmatter 缺失、不可解析或 name 不匹配时视为 malformed agent。
37
43
  - **OpenCode agent(`.opencode/agents/`)**:文件名即 agent 名(OpenCode 不要求在 frontmatter 中写 `name:`),读取 frontmatter 确认 `mode: subagent` 和 `permission` 字段存在且可解析即可;frontmatter 缺失或不可解析视为 malformed。
38
44
  - **Codex agent(`.codex/agents/`)**:文件名为 `{agent}.toml`,TOML 必须可解析,且包含 `name`、`description`、`developer_instructions`;`name` 必须与目标 agent 完全一致。
45
+ - **Antigravity agent(`.agents/agents/`)**:路径为 `.agents/agents/agent-name/agent.md`(`agent-name` 为目标 agent 名),frontmatter 必须可解析,且 `name` 与目标 agent 一致、`mainAgent: false`、`subagent: true`、`tools` 非空;缺失或不匹配视为 malformed。
39
46
  - 如果目标模式所需任一文件缺失或 malformed,**不要尝试 spawn 缺失/异常 Agent**;自动降级为 `solo`,并在报告开头写明:`Fallback: missing agents -> solo` 或 `Fallback: malformed agents -> solo`,列出问题文件,建议用户运行 `/story-setup`。
40
- 5. **确认 Agent/Task 工具可用**:如果当前环境没有可用的子 Agent/Task 调用能力,直接降级为 `solo`,报告 `Fallback: agent tool unavailable -> solo`。
41
- 6. **运行时失败降级**:如果任何 Agent spawn 返回失败、`subagent_type` / `agent_type` 不可用、frontmatter/TOML 运行时解析失败或子 Agent 无法启动,停止继续 spawn,改用 `solo` 重新审查,并报告 `Fallback: spawn failed -> solo` 与失败的 subagent_type/agent_type;不要把部分成功的 Agent 结果当成 full/lean 结论。
47
+ 5. **确认 Agent 工具可用**:Claude/OpenCode/Codex 需要当前运行时的子 Agent/Task 调用能力,Antigravity 需要 `invoke_subagent`;不可用时直接降级为 `solo`,报告 `Fallback: agent tool unavailable -> solo`。
48
+ 6. **运行时失败降级**:如果任何 Agent spawn 返回失败、`subagent_type` / `agent_type` / `TypeName` 不可用、frontmatter/TOML 运行时解析失败或子 Agent 无法启动,停止继续 spawn,改用 `solo` 重新审查,并报告 `Fallback: spawn failed -> solo` 与失败的 agent 名;不要把部分成功的 Agent 结果当成 full/lean 结论。
42
49
  7. **确定实际模式**:报告中必须同时列出 `Requested Mode` 与 `Effective Mode`。
43
50
 
44
51
  ---
@@ -67,16 +74,16 @@ Rubric Source: file | embedded fallback
67
74
  3. `{项目根}/.codex/skills/{规范路径}`(Codex 项目内安装)
68
75
  4. `{项目根}/.zcode/skills/{规范路径}`(ZCode 项目内安装)
69
76
  5. `{项目根}/skills/{规范路径}`(OpenClaw / Reasonix / generic 部署,也是本仓库开发环境)
70
- 6. `{项目根}/.agents/skills/{规范路径}`(Codex / Reasonix 扫描的项目 skill root,通常是指向 `skills/` 的 symlink)
77
+ 6. `{项目根}/.agents/skills/{规范路径}`(Antigravity 项目内真实 skill root;Codex / Reasonix 也可能扫描此目录或其 symlink)
71
78
  7. 当前运行时加载本 skill 的目录,或其可访问的全局 skill 搜索路径中同名 `{skill-name}/...` 目录
72
79
 
73
- > 靠前几层不存在是正常的,不是部署损坏。`/story-setup` 只在 ZCode 的 `.zcode/skills/` 和 OpenClaw / Reasonix / generic 的 `skills/` 下整份复制 skill;Codex 项目部署不复制 skill 本体,本 skill 由 Codex 从 skill root 加载,references 就在其中,通常命中第 6 或第 7 层。不要手工把 `references/` 复制进 `.codex/skills/`——手工副本不受 story-setup 管理,升级后会静默变旧。
80
+ > 靠前几层不存在是正常的,不是部署损坏。`/story-setup` 会为 Antigravity 把 13 个 skill 真实复制到 `.agents/skills/`,为 ZCode 复制到 `.zcode/skills/`,并为 OpenClaw / Reasonix / generic 复制到 `skills/`。Codex 项目部署不复制 skill 本体,本 skill 由 Codex 从 skill root 加载,references 通常命中第 6 或第 7 层。不要手工把 `references/` 复制进 `.codex/skills/`——手工副本不受 story-setup 管理,升级后会静默变旧。
74
81
 
75
82
  规范路径如下;禁止只写裸文件名,禁止跨 skill 误读其他 skill 的 references:
76
83
 
77
84
  | 用途 | 规范路径 |
78
85
  |---|---|
79
- | 通用质量清单 | `story-review/references/quality-checklist.md` |
86
+ | 通用质量清单 | `story-review/references/review-quality.md` |
80
87
  | 通用内容评分 rubric | `story-review/references/quality-rubric.md` |
81
88
  | 去 AI 味方法 | `story-review/references/anti-ai-writing.md` |
82
89
  | 剧情循环/高潮公式 | `story-review/references/plot-core-methods.md` |
@@ -86,6 +93,8 @@ Rubric Source: file | embedded fallback
86
93
  | 平台 rubric | `story-review/references/rubrics/{fanqie,qidian,zhihu}.md` |
87
94
  | 标点预检脚本 | `story-review/scripts/normalize-punctuation.js` |
88
95
  | AI句式预检脚本 | `story-review/scripts/check-ai-patterns.js` |
96
+ | 作者习惯协议 | `story-review/references/author-memory.md` |
97
+ | 作者习惯事务脚本 | `story-review/scripts/author_memory_commit.py` |
89
98
 
90
99
  ### 内置审查基准包(路径不可读时必用)
91
100
 
@@ -174,7 +183,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
174
183
  - 这三个预检脚本只读;`story-review` **不修改正文、设定或大纲文件**,需要自动修复正文时建议转 `/story-deslop`。full / lean 模式只有下方「追踪文件维护」允许修改 `追踪/`;分批审查的所有模式都可按上方契约写 **.story-review/state.md**,solo 除该状态外不写项目内容。
175
184
  - 默认 `--quote-mode keep`,不把知乎盐言短篇的 `「」` 当作问题;只有项目明确指定引号风格时才检查对应转换建议。
176
185
 
177
- **story-explorer 预查询(可选)**。仅当 `Effective Mode` 仍为 `full`/`lean`、当前允许 spawn 且 Agent/Task 工具可用时,才可检查 agent 目录(优先 `.claude/agents/`,其次 `.opencode/agents/`,再检查 `.codex/agents/`)下的 `story-explorer.md` 或 `story-explorer.toml` 并 spawn `story-explorer` 预查设定摘要;`solo` 或子代理递归保护场景下不得 spawn,只能直接 Read/Grep。Prompt 示例:
186
+ **story-explorer 预查询(可选)**。仅当 `Effective Mode` 仍为 `full`/`lean`、当前允许 spawn 且当前运行时的 Agent 工具可用时,才可在对应 canonical agent 目录下确认 `story-explorer` 已部署并 spawn;Antigravity 检查 `.agents/agents/story-explorer/agent.md`,用 `invoke_subagent` + `TypeName: "story-explorer"`。`solo` 或子代理递归保护场景下不得 spawn,只能直接读取/检索。Prompt 示例:
178
187
 
179
188
  ```text
180
189
  项目目录:{dir}
@@ -209,7 +218,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
209
218
 
210
219
  ## Phase 2:并行 Spawn Agent(full/lean 模式)
211
220
 
212
- 使用 Agent/Task 工具并行调用(Codex 原生子代理使用 `agent_type`,Claude Code 兼容面使用 `subagent_type`;实际字段以当前 CLI 暴露的工具为准)。每个 Agent 不继承父对话上下文,prompt 必须自包含项目路径、审查范围、文件路径、必要摘录、审查基准包摘要、Rubric Source 和统一 Findings Schema。
221
+ 使用当前运行时的 Agent 工具并行调用(Codex 原生子代理使用 `agent_type`,Claude Code 兼容面使用 `subagent_type`,Antigravity 使用 `invoke_subagent` + 同名 `TypeName`;实际字段以当前 CLI 暴露的工具为准)。每个 Agent 不继承父对话上下文,prompt 必须自包含项目路径、审查范围、文件路径、必要摘录、审查基准包摘要、Rubric Source 和统一 Findings Schema。
213
222
 
214
223
  **调用规则**:执行 Phase 0 后,只有实际模式仍是 full/lean 时才 spawn。不要 spawn 缺失 Agent。
215
224
 
@@ -226,7 +235,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
226
235
  Rubric Source: file | embedded fallback
227
236
  相关文件路径:{设定/大纲/细纲文件路径}
228
237
  继承的开放项(分批审查必填,无则写「无」):{从 追踪/伏笔.md 提取的、预计回收章 ≤ 本批末章的已埋未回收钩子,连同上一批未解决 findings 摘要}
229
- 可选补充参考:本 Skill 的 `story-review/references/quality-checklist.md`、`story-review/references/plot-core-methods.md`;若不可读,不影响审查。
238
+ 可选补充参考:本 Skill 的 `story-review/references/review-quality.md`、`story-review/references/plot-core-methods.md`;若不可读,不影响审查。
230
239
  检查项:
231
240
  1. 这一章是否推进了故事主题?
232
241
  2. 大纲结构是否完整(钩子/爽点/悬念)?
@@ -289,7 +298,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
289
298
  审查基准包摘要:{Phase 1 形成的 rubric / fallback 摘要,必须内联}
290
299
  Rubric Source: file | embedded fallback
291
300
  AI 味 / 禁用词摘要:{从 anti-ai-writing、banned-words 或内置 fallback 提取,必须内联}
292
- 可选补充参考:本 Skill 的 `story-review/references/anti-ai-writing.md`、`story-review/references/banned-words.md`、`story-review/references/quality-checklist.md`;若不可读,不影响审查。
301
+ 可选补充参考:本 Skill 的 `story-review/references/anti-ai-writing.md`、`story-review/references/banned-words.md`、`story-review/references/review-quality.md`;若不可读,不影响审查。
293
302
  检查项:
294
303
  1. 是否存在禁用词/套话/陈词滥调,或“像/好像/仿佛/如同”式比喻成片堆叠?
295
304
  2. 是否出现 AI 写作指纹、8 种 AI 写作模式(含模式 8 解释腔/上帝视角/安排感)或章末总结体?
@@ -321,7 +330,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
321
330
  继承的开放项(分批审查必填,无则写「无」):{从 追踪/伏笔.md 提取的、预计回收章 ≤ 本批末章的已埋未回收伏笔,连同上一批未解决 findings 摘要}
322
331
  审查基准包摘要:{Phase 1 形成的 rubric / fallback 摘要,必须内联}
323
332
  Rubric Source: file | embedded fallback
324
- 可选补充参考:本 Skill 的 `story-review/references/quality-checklist.md`;若不可读,不影响事实冲突扫描。
333
+ 可选补充参考:本 Skill 的 `story-review/references/review-quality.md`;若不可读,不影响事实冲突扫描。
325
334
  检查项:
326
335
  1. 角色属性是否前后一致?
327
336
  2. 世界规则是否被违反?
@@ -344,7 +353,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
344
353
 
345
354
  1. 收集实际执行的 reviewer VERDICT 和 FINDINGS。
346
355
  2. 合并去重:按 `severity` 排序(S1 > S2 > S3 > S4),同级内按影响范围排序。
347
- 3. **可选事实核查**:如果审查内容涉及需要验证的外部事实(历史年代、地理方位、职业细节等),只有在 `Effective Mode` 仍为 `full`/`lean`、当前不是子 Agent、Agent/Task 工具可用且 agent 目录(优先 `.claude/agents/`,其次 `.opencode/agents/`,再检查 `.codex/agents/`)下的 `story-researcher.md` 或 `story-researcher.toml` 已部署时,才可额外 spawn `story-researcher` 搜索验证;`solo`、missing/malformed/stale/spawn failed 降级或子代理递归保护场景下不得 spawn,只能在报告中标记“需人工事实核查”。
356
+ 3. **可选事实核查**:如果审查内容涉及需要验证的外部事实(历史年代、地理方位、职业细节等),只有在 `Effective Mode` 仍为 `full`/`lean`、当前不是子 Agent、当前运行时的 Agent 工具可用且对应 canonical agent 目录下的 `story-researcher` 已部署时,才可额外 spawn;Antigravity 检查 `.agents/agents/story-researcher/agent.md`,用 `invoke_subagent` + `TypeName: "story-researcher"`。`solo`、missing/malformed/stale/spawn failed 降级或子代理递归保护场景下不得 spawn,只能在报告中标记“需人工事实核查”。
348
357
  4. **分歧呈现**:如果 reviewer 间有冲突意见,明确呈现分歧让用户裁决;不要自动妥协。
349
358
  5. 输出综合审查报告。报告必须列出实际模式、fallback 原因、使用的 rubric、Rubric Source、审查范围和证据不足项。
350
359
 
@@ -282,6 +282,7 @@ AI 喜欢把事情凑成三个以显"完整"。-> 砍到只剩最有力的一条
282
282
  | 升华式收尾 | 结尾对仗拔高、金句点题 -> 用一个动作或一句留白收住,把"意思"压进画面里 |
283
283
  | 抽象命运/开端收束 | 「命运终于露出獠牙」「早已布好的棋局」「这一刻终于明白」「属于他的反击才刚刚开始」-> 改成角色当下可见的文件、动作、对话或物理后果;`check-ai-patterns.js` 报 `abstract-summary-tic` 时优先处理 |
284
284
  | 套词密度过高 | 仿佛/一丝/一抹/深吸一口气/平静无波/指节泛白等成串复现(`cliche-density-tic`)-> 不是同义词轮换,整段回到角色当下证据:文件、动作、对话、物理后果 |
285
+ | 套式反应细节 | 指尖轻叩、袖口里攥紧、指节泛白、目光移开、“语气平静得像在念……”等反应成片(`stock-reaction-tic`)-> 逐处做删除测试;只标注情绪而不改变选择、关系、物件或动作结果的删掉,不换部位和同义动作;有伤势、动作失败或情节后果的身体细节可留 |
285
286
  | 比喻密度过高 | 像/好像/仿佛/如同等比喻标记成片复现(`metaphor-density-tic`)-> 保留最能传递信息或情绪的一两个,其余改回具体动作、物件、声音、后果;不要换成新比喻 |
286
287
  | 系统公告公文腔过密 | 方括号规则/面板/公告行里硬规则词成片(`system-notice-formality-tic`)-> 保留为角色看见的屏幕/公告/规则载体;只在载体内部白话化部分硬词,或补角色当场看懂的具体后果,不改成叙述者解释 |
287
288
 
@@ -575,7 +576,7 @@ AI 喜欢把事情凑成三个以显"完整"。-> 砍到只剩最有力的一条
575
576
  - [ ] **标点不压平**:没有把质问、爆发、犹豫全部压成句号;也没有随机堆砌 `?`/`!`,或用 `……`/`——` 硬造停顿
576
577
  - [ ] **Show Don't Tell**:用行为代替形容词,用细节代替总结
577
578
  - [ ] **句长达标**:叙述默认是逗号长句(逗号之间 8-12 字、整句 20-30 字,规则 3);短句只作偶尔的孤立重拍,用完回到逗号长句;没有连着的 ≤5 字碎片,没有通篇短句像提纲
578
- - [ ] **detector advisory 逐条复核**:`micro-action-tic` / `abstract-summary-tic` / `cliche-density-tic` / `metaphor-density-tic` / `reasoning-chain-tic` / `system-notice-formality-tic` / `overcompressed-prose-tic` / `low-connective-density-tic` / `action-list-tic` 命中时按脚本给出的修法处理:先通读判断是不是机械复现,确属再改;功能性写法保留或标 `[需复核]`,不做同义词轮换、不机械注水
579
+ - [ ] **detector advisory 逐条复核**:`micro-action-tic` / `stock-reaction-tic` / `abstract-summary-tic` / `cliche-density-tic` / `metaphor-density-tic` / `reasoning-chain-tic` / `system-notice-formality-tic` / `overcompressed-prose-tic` / `low-connective-density-tic` / `action-list-tic` 命中时按脚本给出的修法处理:先通读判断是不是机械复现,确属再改;功能性写法保留或标 `[需复核]`,不做同义词轮换、不机械注水
579
580
  - [ ] **不做硬指标投机**:不为反检测强制每句换行、50-60 字一行、对话 50%-60%、英文点省略号,或把 `地/得` 全改成 `的`
580
581
  - [ ] **任务卡点服从原文边界**:抽象总结若改成角色办事被卡住,必须来自原文已有任务/证据/手续/物件缺口;不新增原文没有的事件链
581
582
  - [ ] **去AI三遍法执行**:轻度只做Pass1,中度做Pass1+2,重度完整三遍