Преглед на файлове

fix: stabilize project root resolution and align write/init/plan references

lingfengQAQ преди 3 месеца
родител
ревизия
adf23511da

+ 40 - 0
.claude/scripts/data_modules/tests/test_project_locator.py

@@ -64,3 +64,43 @@ def test_resolve_project_root_finds_default_subdir_within_git_root(tmp_path):
     resolved = resolve_project_root(cwd=nested)
     assert resolved == default_project.resolve()
 
+
+def test_resolve_project_root_uses_workspace_pointer(tmp_path):
+    _ensure_scripts_on_path()
+
+    from project_locator import resolve_project_root, write_current_project_pointer
+
+    workspace = tmp_path / "workspace"
+    (workspace / ".claude").mkdir(parents=True, exist_ok=True)
+
+    project_root = workspace / "凡人资本论"
+    (project_root / ".webnovel").mkdir(parents=True, exist_ok=True)
+    (project_root / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
+
+    pointer_file = write_current_project_pointer(project_root, workspace_root=workspace)
+    assert pointer_file is not None
+    assert pointer_file.is_file()
+
+    resolved = resolve_project_root(cwd=workspace)
+    assert resolved == project_root.resolve()
+
+
+def test_resolve_project_root_ignores_stale_pointer_and_fallbacks(tmp_path):
+    _ensure_scripts_on_path()
+
+    from project_locator import resolve_project_root
+
+    workspace = tmp_path / "workspace"
+    (workspace / ".claude").mkdir(parents=True, exist_ok=True)
+    # stale pointer
+    (workspace / ".claude" / ".webnovel-current-project").write_text(
+        str(workspace / "missing-project"), encoding="utf-8"
+    )
+
+    default_project = workspace / "webnovel-project"
+    (default_project / ".webnovel").mkdir(parents=True, exist_ok=True)
+    (default_project / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
+
+    resolved = resolve_project_root(cwd=workspace)
+    assert resolved == default_project.resolve()
+

+ 9 - 0
.claude/scripts/init_project.py

@@ -28,6 +28,7 @@ import re
 
 # 安全修复:导入安全工具函数
 from security_utils import sanitize_commit_message, atomic_write_json, is_git_available
+from project_locator import write_current_project_pointer
 
 
 # Windows 编码兼容性修复
@@ -706,6 +707,14 @@ __pycache__/
             except subprocess.CalledProcessError as e:
                 print(f"Git init failed (non-fatal): {e}")
 
+    # 记录工作区默认项目指针(非阻断)
+    try:
+        pointer_file = write_current_project_pointer(project_path)
+        if pointer_file is not None:
+            print(f"Default project pointer updated: {pointer_file}")
+    except Exception as e:
+        print(f"Default project pointer update failed (non-fatal): {e}")
+
     print(f"\nProject initialized at: {project_path}")
     print("Key files:")
     print(" - .webnovel/state.json")

+ 67 - 0
.claude/scripts/project_locator.py

@@ -18,6 +18,7 @@ from typing import Iterable, Optional
 
 
 DEFAULT_PROJECT_DIR_NAMES: tuple[str, ...] = ("webnovel-project",)
+CURRENT_PROJECT_POINTER_REL: Path = Path(".claude") / ".webnovel-current-project"
 
 
 def _find_git_root(cwd: Path) -> Optional[Path]:
@@ -45,6 +46,66 @@ def _is_project_root(path: Path) -> bool:
     return (path / ".webnovel" / "state.json").is_file()
 
 
+def _pointer_candidates(cwd: Path, *, stop_at: Optional[Path] = None) -> Iterable[Path]:
+    """Yield candidate pointer files from cwd up to parents (bounded by stop_at when provided)."""
+    for candidate in (cwd, *cwd.parents):
+        yield candidate / CURRENT_PROJECT_POINTER_REL
+        if stop_at is not None and candidate == stop_at:
+            break
+
+
+def _resolve_project_root_from_pointer(cwd: Path, *, stop_at: Optional[Path] = None) -> Optional[Path]:
+    """
+    Resolve project root from workspace pointer file.
+
+    Pointer file format:
+    - plain text absolute path, one line.
+    - relative path is also supported (resolved relative to pointer's `.claude/` dir).
+    """
+    for pointer_file in _pointer_candidates(cwd, stop_at=stop_at):
+        if not pointer_file.is_file():
+            continue
+        raw = pointer_file.read_text(encoding="utf-8").strip()
+        if not raw:
+            continue
+        target = Path(raw).expanduser()
+        if not target.is_absolute():
+            target = (pointer_file.parent / target).resolve()
+        if _is_project_root(target):
+            return target.resolve()
+    return None
+
+
+def _find_workspace_root_with_claude(start: Path) -> Optional[Path]:
+    """Find nearest ancestor containing `.claude/`."""
+    for candidate in (start, *start.parents):
+        if (candidate / ".claude").is_dir():
+            return candidate
+    return None
+
+
+def write_current_project_pointer(project_root: Path, *, workspace_root: Optional[Path] = None) -> Optional[Path]:
+    """
+    Write workspace-level current project pointer and return pointer file path.
+
+    If no workspace root with `.claude/` can be found, returns None (non-fatal).
+    """
+    root = Path(project_root).expanduser().resolve()
+    if not _is_project_root(root):
+        raise FileNotFoundError(f"Not a webnovel project root (missing .webnovel/state.json): {root}")
+
+    ws_root = Path(workspace_root).expanduser().resolve() if workspace_root else _find_workspace_root_with_claude(root)
+    if ws_root is None:
+        ws_root = _find_workspace_root_with_claude(Path.cwd().resolve())
+    if ws_root is None:
+        return None
+
+    pointer_file = ws_root / CURRENT_PROJECT_POINTER_REL
+    pointer_file.parent.mkdir(parents=True, exist_ok=True)
+    pointer_file.write_text(str(root), encoding="utf-8")
+    return pointer_file
+
+
 def resolve_project_root(explicit_project_root: Optional[str] = None, *, cwd: Optional[Path] = None) -> Path:
     """
     Resolve the webnovel project root directory (the directory containing `.webnovel/state.json`).
@@ -76,6 +137,12 @@ def resolve_project_root(explicit_project_root: Optional[str] = None, *, cwd: Op
 
     base = (cwd or Path.cwd()).resolve()
     git_root = _find_git_root(base)
+
+    # Workspace pointer fallback (for layouts where `.claude` is in workspace root and projects are subdirs).
+    pointer_root = _resolve_project_root_from_pointer(base, stop_at=git_root)
+    if pointer_root is not None:
+        return pointer_root
+
     for candidate in _candidate_roots(base, stop_at=git_root):
         if _is_project_root(candidate):
             return candidate.resolve()

+ 38 - 6
.claude/scripts/workflow_manager.py

@@ -47,19 +47,29 @@ def now_iso() -> str:
     return datetime.now().isoformat()
 
 
-def find_project_root() -> Path:
-    """Resolve project root (containing .webnovel/state.json)."""
+def find_project_root(override: Optional[Path] = None) -> Path:
+    """Resolve project root (containing .webnovel/state.json).
+
+    Args:
+        override: If provided, use this path directly instead of auto-detecting.
+    """
+    if override is not None:
+        return Path(override).resolve()
     return resolve_project_root()
 
 
+# Global variable to hold CLI-provided project root
+_cli_project_root: Optional[Path] = None
+
+
 def get_workflow_state_path() -> Path:
     """Absolute path to workflow_state.json."""
-    project_root = find_project_root()
+    project_root = find_project_root(_cli_project_root)
     return project_root / ".webnovel" / "workflow_state.json"
 
 
 def get_call_trace_path() -> Path:
-    project_root = find_project_root()
+    project_root = find_project_root(_cli_project_root)
     return project_root / ".webnovel" / "observability" / "call_trace.jsonl"
 
 
@@ -716,37 +726,59 @@ if __name__ == "__main__":
     import argparse
 
     parser = argparse.ArgumentParser(description="工作流状态管理")
+    parser.add_argument(
+        "--project-root",
+        dest="global_project_root",
+        help="项目根目录(可选,默认自动检测)",
+    )
     subparsers = parser.add_subparsers(dest="action", help="操作类型")
 
+    def add_project_root_arg(subparser):
+        """Allow --project-root after subcommand for compatibility."""
+        subparser.add_argument("--project-root", help="项目根目录(可选,默认自动检测)")
+
     p_start_task = subparsers.add_parser("start-task", help="开始新任务")
+    add_project_root_arg(p_start_task)
     p_start_task.add_argument("--command", required=True, help="命令名称")
     p_start_task.add_argument("--chapter", type=int, help="章节号")
 
     p_start_step = subparsers.add_parser("start-step", help="开始 Step")
+    add_project_root_arg(p_start_step)
     p_start_step.add_argument("--step-id", required=True, help="Step ID")
     p_start_step.add_argument("--step-name", required=True, help="Step 名称")
     p_start_step.add_argument("--note", help="进度备注")
 
     p_complete_step = subparsers.add_parser("complete-step", help="完成 Step")
+    add_project_root_arg(p_complete_step)
     p_complete_step.add_argument("--step-id", required=True, help="Step ID")
     p_complete_step.add_argument("--artifacts", help="Artifacts JSON")
 
     p_complete_task = subparsers.add_parser("complete-task", help="完成任务")
+    add_project_root_arg(p_complete_task)
     p_complete_task.add_argument("--artifacts", help="Final artifacts JSON")
 
     p_fail_task = subparsers.add_parser("fail-task", help="标记任务失败")
+    add_project_root_arg(p_fail_task)
     p_fail_task.add_argument("--reason", default="manual_fail", help="失败原因")
 
-    subparsers.add_parser("detect", help="检测中断")
+    p_detect = subparsers.add_parser("detect", help="检测中断")
+    add_project_root_arg(p_detect)
 
     p_cleanup = subparsers.add_parser("cleanup", help="清理 artifacts")
+    add_project_root_arg(p_cleanup)
     p_cleanup.add_argument("--chapter", type=int, required=True, help="章节号")
     p_cleanup.add_argument("--confirm", action="store_true", help="确认执行删除与 Git 重置(高风险)")
 
-    subparsers.add_parser("clear", help="清除中断任务")
+    p_clear = subparsers.add_parser("clear", help="清除中断任务")
+    add_project_root_arg(p_clear)
 
     args = parser.parse_args()
 
+    # Set global project root if provided (support both before/after subcommand).
+    project_root_arg = getattr(args, "project_root", None) or getattr(args, "global_project_root", None)
+    if project_root_arg:
+        _cli_project_root = Path(project_root_arg)
+
     if args.action == "start-task":
         start_task(args.command, {"chapter_num": args.chapter})
     elif args.action == "start-step":

+ 1 - 1
.claude/skills/webnovel-init/references/creativity/creativity-constraints.md

@@ -283,7 +283,7 @@
 
 ## 九、输出格式
 
-### 10.1 创意卡片格式
+### 9.1 创意卡片格式
 
 ```markdown
 ## 创意 #{N}: {title}

+ 9 - 6
.claude/skills/webnovel-init/references/creativity/market-positioning.md

@@ -1,12 +1,15 @@
 # 市场定位分析参考指南
 
+> 时效说明:本文中的平台画像与“2025年趋势”仅作为历史样例。
+> 执行 Step 1 平台定位时,必须先用 WebSearch/WebFetch 核验当期平台规则与榜单信号,再决定是否采纳。
+
 ## 核心定位
 
 精准识别目标平台与读者群体,制定差异化竞争策略,确保作品在市场中找到最佳位置。
 
 ---
 
-## 1. 主流平台分析框架(2025)
+## 1. 主流平台分析框架(2025样例
 
 ### 1.1 起点中文网
 
@@ -26,7 +29,7 @@
 - 推荐位竞争激烈(新书榜、分类榜)
 - 读者互动活跃(本章说、书评区)
 
-**2025年趋势**:
+**2025年趋势样例**:
 - 🔥 多元宇宙/平行世界设定
 - 🔥 硬核升级流(数值可视化)
 - 🔥 群像多主角(非单一视角)
@@ -50,7 +53,7 @@
 - 免费模式(字数越多收益越高)
 - 短篇友好(5-20万字完本)
 
-**2025年趋势**:
+**2025年趋势样例**:
 - 🔥 知乎体短篇(2-3万字)
 - 🔥 多题材融合(都市+玄幻)
 - 🔥 反套路创新(反转传统爽文)
@@ -165,7 +168,7 @@
 
 ---
 
-## 3. 市场趋势识别(2025)
+## 3. 市场趋势识别(2025样例
 
 ### 3.1 题材生命周期判断
 
@@ -178,7 +181,7 @@
 | **成熟期** | 市场饱和,竞争激烈 | 必须有独特卖点 |
 | **衰退期** | 读者审美疲劳,新作难出头 | 避免入场或深度创新 |
 
-**2025年题材生命周期速查**:
+**2025年题材生命周期样例速查**:
 ```
 萌芽期:AI觉醒流、元宇宙冒险
 成长期:规则怪谈、无限流女频
@@ -188,7 +191,7 @@
 
 ### 3.2 新兴热门元素
 
-**2025年趋势关键词**:
+**2025年趋势关键词样例**:
 - 🔥 **群像多视角**:不再单一主角,多线叙事
 - 🔥 **反转真相**:悬念铺设+真相揭秘
 - 🔥 **规则怪谈**:SCP风格,规则破解

+ 6 - 3
.claude/skills/webnovel-init/references/creativity/selling-points.md

@@ -1,5 +1,8 @@
 # 核心卖点定位参考指南
 
+> 时效说明:本文中的“2025”数据与示例仅作历史样例。
+> 执行 Step 5 卖点筛选时,若涉及市场判断,必须先用 WebSearch/WebFetch 核验当期趋势后再采纳。
+
 ## 核心定位
 
 精准识别并定义作品的核心吸引力,确保所有创作要素(主角、金手指、世界观、情节)都围绕这个卖点展开,为读者提供独特且一致的阅读体验。
@@ -71,7 +74,7 @@
 
 ## 2. 市场热点与创新机会识别
 
-### 2.1 热门卖点生命周期分析(2025)
+### 2.1 热门卖点生命周期分析(2025样例
 
 | 卖点类型 | 生命周期阶段 | 市场饱和度 | 创新机会 | 建议策略 |
 |---------|------------|-----------|---------|---------|
@@ -91,13 +94,13 @@
 - [ ] 热门卖点 + 反套路设定 = 是否足够新颖?
 - [ ] 跨题材融合(如:都市+规则怪谈) = 是否有成功案例?
 
-**市场空白示例**(2025年潜力方向):
+**市场空白示例**(2025年潜力方向样例):
 - 规则怪谈 + 都市背景(现代城市中的规则世界)
 - 重生 + 配角视角(重生成原著配角,改写剧情)
 - 系统 + 群像多主角(多人都有系统,相互博弈)
 - 反派洗白 + 女频甜宠(反派大佬洗白后追妻)
 
-### 2.3 读者群体迁移趋势(2025)
+### 2.3 读者群体迁移趋势(2025样例
 
 **从"纯爽"到"爽+烧脑"**:
 - 读者越来越期待智商在线的主角

+ 76 - 10
.claude/skills/webnovel-plan/SKILL.md

@@ -6,6 +6,7 @@ description: Builds volume and chapter outlines from the total outline, inherits
 # Outline Planning
 
 Purpose: refine 总纲 into volume + chapter outlines. Do not redesign the global story.
+Setting policy: 先基于 init 产出的总纲+世界观补齐设定集基线;再在卷纲完成后,直接对现有设定集做增量补充。
 
 ## Project Root Guard
 - Must run inside a project containing `.webnovel/state.json`.
@@ -17,8 +18,11 @@ Purpose: refine 总纲 into volume + chapter outlines. Do not redesign the globa
 - Step 4(必读,题材配置):[genre-profiles.md](../../references/genre-profiles.md)
 - Step 4(必读,Strand 节奏):[strand-weave-pattern.md](../../references/shared/strand-weave-pattern.md)
 - Step 4(可选,爽点结构需要细化):[cool-points-guide.md](../../references/shared/cool-points-guide.md)
+- Step 5/6(可选,冲突强度分层):[conflict-design.md](references/outlining/conflict-design.md)
 - Step 5(可选,需要钩子/节奏细分):[reading-power-taxonomy.md](../../references/reading-power-taxonomy.md)
+- Step 6(可选,章节微结构细化):[chapter-planning.md](references/outlining/chapter-planning.md)
 - Step 4/5(可选,电竞/直播文/克苏鲁):[genre-volume-pacing.md](references/outlining/genre-volume-pacing.md)
+- 归档(不进主流程):`references/outlining/outline-structure.md`、`references/outlining/plot-frameworks.md`
 
 ## Reference Loading Levels (strict, lazy)
 
@@ -29,11 +33,13 @@ Use progressive disclosure and load only what current step requires:
 
 ## Workflow
 1. Load project data.
-2. Select volume and confirm scope.
-3. Generate volume beat sheet (节拍表).
-4. Generate volume skeleton.
-5. Generate chapter outlines in batches.
-6. Validate + save + update state.
+2. Build setting baseline from 总纲 + 世界观 (in-place incremental).
+3. Select volume and confirm scope.
+4. Generate volume beat sheet (节拍表).
+5. Generate volume skeleton.
+6. Generate chapter outlines in batches.
+7. Enrich existing setting files from volume outline (in-place incremental).
+8. Validate + save + update state.
 
 ## 1) Load project data
 ```bash
@@ -45,16 +51,40 @@ Optional (only if they exist):
 - `设定集/主角组.md`
 - `设定集/女主卡.md`
 - `设定集/反派设计.md`
+- `设定集/世界观.md`
+- `设定集/力量体系.md`
+- `设定集/主角卡.md`
 - `.webnovel/idea_bank.json` (inherit constraints)
 
 If 总纲.md lacks volume ranges / core conflict / climax, ask the user to fill those before proceeding.
 
-## 2) Select volume
+## 2) Build setting baseline from 总纲 + 世界观
+目标:在不推翻现有内容的前提下,让设定集从“骨架模板”进入“可规划可写作”的基线状态。
+
+输入来源:
+- `大纲/总纲.md`
+- `设定集/世界观.md`
+- `设定集/力量体系.md`
+- `设定集/主角卡.md`
+- `设定集/反派设计.md`
+
+执行规则(必须):
+- 只做增量补齐,不清空、不重写整文件。
+- 优先补齐“可执行字段”:角色定位、势力关系、能力边界、代价规则、反派层级映射。
+- 若总纲与现有设定冲突,先列冲突并阻断,等待用户裁决后再改。
+
+基线补齐最小要求:
+- `设定集/世界观.md`:世界规则边界、社会结构、关键地点用途。
+- `设定集/力量体系.md`:境界链/能力限制/代价与冷却。
+- `设定集/主角卡.md`:欲望、缺陷、初始资源与限制。
+- `设定集/反派设计.md`:小/中/大反派层级与主角镜像关系。
+
+## 3) Select volume
 - Offer choices from 总纲.md (卷名 + 章节范围).
 - Confirm any special requirement (tone, POV emphasis, romance, etc.).
 If 总纲缺少卷名/章节范围/核心冲突/卷末高潮,先补问并更新总纲,再继续。
 
-## 3) Generate volume beat sheet (节拍表)
+## 4) Generate volume beat sheet (节拍表)
 目标:先把本卷“承诺→危机递增→中段反转→最低谷→大兑现+新钩子”钉死,避免卷中段漂移。
 
 Load template:
@@ -78,7 +108,7 @@ Completion criteria:
 - `大纲/第{volume_id}卷-节拍表.md` 存在且非空
 - Step 4/5 能直接引用 Catalyst / 中段反转 / 最低谷 / 大兑现 / 新钩子来锚定节奏
 
-## 4) Generate volume skeleton
+## 5) Generate volume skeleton
 Load genre profile and apply standards:
 ```bash
 cat "${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md"
@@ -90,6 +120,11 @@ Optional (only if爽点结构需要细化):
 cat "${CLAUDE_PLUGIN_ROOT}/references/shared/cool-points-guide.md"
 ```
 
+Optional (only if需要补强卷级冲突链与强度分层):
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/outlining/conflict-design.md"
+```
+
 Load beat sheet (must exist):
 ```bash
 cat "$PROJECT_ROOT/大纲/第{volume_id}卷-节拍表.md"
@@ -180,7 +215,7 @@ Use this template and fill from 总纲 + idea_bank:
 - 硬约束:贯穿全卷
 ```
 
-## 5) Generate chapter outlines (batched)
+## 6) Generate chapter outlines (batched)
 Batching rule:
 - ≤20 章:1 批
 - 21–40 章:2 批
@@ -192,6 +227,11 @@ Optional (only if需要钩子/节奏细分):
 cat "${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md"
 ```
 
+Optional (only if需要章节微结构/标题策略细化):
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/outlining/chapter-planning.md"
+```
+
 ### Chapter generation strategy
 For each chapter, determine:
 
@@ -261,7 +301,25 @@ Save after each batch:
 '@ | Add-Content -Encoding UTF8 "$PROJECT_ROOT/大纲/第{volume_id}卷-详细大纲.md"
 ```
 
-## 6) Validate + save
+## 7) Enrich existing setting files from volume outline
+目标:卷纲写完后,把本卷新增事实写回“现有设定集文件”,确保后续写作可直接读取。
+
+输入来源:
+- `大纲/第{volume_id}卷-节拍表.md`
+- `大纲/第{volume_id}卷-详细大纲.md`
+- 现有设定集文件(世界观/力量体系/主角卡/主角组/女主卡/反派设计)
+
+写回策略(必须):
+- 仅增量补充相关段落,不覆盖整文件。
+- 新增角色:写入对应角色卡或角色组条目(含首次出场章、关系、红线)。
+- 新增势力/地点/规则:写入世界观或力量体系对应章节。
+- 新增反派层级信息:写入反派设计并保持小/中/大层级一致。
+
+冲突处理(硬规则):
+- 若卷纲新增信息与总纲或已确认设定冲突,标记 `BLOCKER` 并停止 state 更新。
+- 只有冲突裁决完成后,才允许继续更新设定并进入保存步骤。
+
+## 8) Validate + save
 ### Validation checks (must pass all)
 
 **1. 爽点密度检查**
@@ -302,6 +360,11 @@ Every chapter must have:
 - 章末未闭合问题(30 字以内)
 - 钩子(类型 + 30 字描述)
 
+**6. 设定补全检查(新增)**
+- 本卷涉及的新角色/势力/规则已回写到现有设定集文件
+- 所有新增条目可回溯到本卷章纲章节
+- `BLOCKER` 数量为 0;若 >0,必须先裁决,不得进入 state 更新
+
 Update state (include chapters range):
 ```bash
 python "${CLAUDE_PLUGIN_ROOT}/scripts/update_state.py" \
@@ -313,6 +376,7 @@ python "${CLAUDE_PLUGIN_ROOT}/scripts/update_state.py" \
 Final check:
 - 节拍表文件已写入:`大纲/第{volume_id}卷-节拍表.md`
 - 章纲文件已写入:`大纲/第{volume_id}卷-详细大纲.md`
+- 设定集已完成基线补齐与本卷增量补充(原文件内可见)
 - 每章包含:目标/阻力/代价/爽点/Strand/反派层级/视角/关键实体/本章变化/章末未闭合问题/钩子
 - 与总纲冲突/高潮一致,约束触发频率合理(如有 idea_bank)
 
@@ -322,6 +386,8 @@ Final check:
 - 章纲文件不存在或为空
 - 任一章节缺少:目标/阻力/代价/爽点/Strand/反派层级/视角/关键实体/本章变化/章末未闭合问题/钩子
 - 与总纲核心冲突或卷末高潮明显冲突
+- 设定集基线未补齐,或本卷增量未回写到现有设定集
+- 存在 `BLOCKER` 未裁决
 - 约束触发频率不足(当 idea_bank 启用时)
 
 ### Rollback / recovery

+ 3 - 0
.claude/skills/webnovel-plan/references/outlining/outline-structure.md

@@ -1,5 +1,8 @@
 # 大纲结构设计指南 (Outline Structure)
 
+> 归档说明:本文件为历史方法论参考,不在 `/webnovel-plan` 主流程自动加载。
+> 如需接入,必须先在 `SKILL.md` 中补充明确的步骤触发条件与读取指令。
+
 > **核心原则**: 大纲是地图,不是枷锁。好的大纲能指引方向,但也要留白,给灵感留空间。
 
 ---

+ 3 - 0
.claude/skills/webnovel-plan/references/outlining/plot-frameworks.md

@@ -1,5 +1,8 @@
 # 剧情框架模板库 (Plot Frameworks)
 
+> 归档说明:本文件为历史方法论参考,不在 `/webnovel-plan` 主流程自动加载。
+> 如需接入,必须先在 `SKILL.md` 中补充明确的步骤触发条件与读取指令。
+
 > **核心原则**: 不要重复造轮子。经典剧情框架已被验证有效,关键是如何包装出新意。
 
 ---

+ 48 - 16
.claude/skills/webnovel-write/SKILL.md

@@ -105,27 +105,58 @@ allowed-tools: Read Write Edit Grep Bash Task
 - 校验项目根:`.webnovel/state.json` 存在。
 - 校验核心输入:`大纲/总纲.md`、`.claude/scripts/extract_chapter_context.py` 存在。
 - 规范化变量:
-  - `PROJECT_ROOT`:当前项目绝对路径
+  - `PROJECT_ROOT`:当前项目绝对路径(必须是用户小说项目,如 `/path/to/凡人资本论`)
+  - `SKILL_ROOT`:skill 所在目录(即本 SKILL.md 所在的 `.claude/skills/webnovel-write`)
+  - `SCRIPTS_DIR`:脚本目录(按存在性自动探测,优先项目内 `.claude/scripts`,其次上级目录,再次 `${CLAUDE_PLUGIN_ROOT}/scripts`)
   - `chapter_num`:当前章号(整数)
   - `chapter_padded`:四位章号(如 `0007`)
 
+环境设置(bash 命令执行前):
+```bash
+# 示例:假设 PROJECT_ROOT=/d/wk/xiaoshuo/凡人资本论
+export PROJECT_ROOT="/d/wk/xiaoshuo/凡人资本论"
+
+if [ -d "${PROJECT_ROOT}/.claude/skills/webnovel-write" ]; then
+  export SKILL_ROOT="${PROJECT_ROOT}/.claude/skills/webnovel-write"
+elif [ -d "${PROJECT_ROOT}/../.claude/skills/webnovel-write" ]; then
+  export SKILL_ROOT="${PROJECT_ROOT}/../.claude/skills/webnovel-write"
+elif [ -n "${CLAUDE_PLUGIN_ROOT}" ] && [ -d "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write" ]; then
+  export SKILL_ROOT="${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write"
+else
+  echo "ERROR: 未找到 webnovel-write skill 目录" >&2
+  exit 1
+fi
+
+if [ -d "${PROJECT_ROOT}/.claude/scripts" ]; then
+  export SCRIPTS_DIR="${PROJECT_ROOT}/.claude/scripts"
+elif [ -d "${PROJECT_ROOT}/../.claude/scripts" ]; then
+  export SCRIPTS_DIR="${PROJECT_ROOT}/../.claude/scripts"
+elif [ -n "${CLAUDE_PLUGIN_ROOT}" ] && [ -d "${CLAUDE_PLUGIN_ROOT}/scripts" ]; then
+  export SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT}/scripts"
+else
+  echo "ERROR: 未找到 scripts 目录" >&2
+  exit 1
+fi
+```
+
 输出:
 - “已就绪输入”与“缺失输入”清单;缺失则阻断并提示先补齐。
 
 ### Step 0.5:工作流断点记录(best-effort,不阻断)
 
 ```bash
+# workflow_manager 必须传 --project-root 参数
 # 开始整条任务
-python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" start-task --command webnovel-write --chapter {chapter_num} || true
+python "${SCRIPTS_DIR}/workflow_manager.py" --project-root "${PROJECT_ROOT}" start-task --command webnovel-write --chapter {chapter_num} || true
 
 # 进入某一步(示例:Step 1)
-python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" start-step --step-id "Step 1" --step-name "Context Agent" || true
+python "${SCRIPTS_DIR}/workflow_manager.py" --project-root "${PROJECT_ROOT}" start-step --step-id "Step 1" --step-name "Context Agent" || true
 
 # Step 1 完成后记录(每个 Step 结束都要调用)
-python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" complete-step --step-id "Step 1" --artifacts '{"ok":true}' || true
+python "${SCRIPTS_DIR}/workflow_manager.py" --project-root "${PROJECT_ROOT}" complete-step --step-id "Step 1" --artifacts '{"ok":true}' || true
 
 # 全部 Step 结束后,再结束整条任务
-python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" complete-task --artifacts '{"ok":true}' || true
+python "${SCRIPTS_DIR}/workflow_manager.py" --project-root "${PROJECT_ROOT}" complete-task --artifacts '{"ok":true}' || true
 ```
 
 要求:
@@ -156,7 +187,7 @@ python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" complete-task --artif
 
 执行前必须加载:
 ```bash
-cat "${CLAUDE_PLUGIN_ROOT}/references/shared/core-constraints.md"
+cat "${SKILL_ROOT}/../../references/shared/core-constraints.md"
 ```
 
 硬要求:
@@ -172,7 +203,7 @@ cat "${CLAUDE_PLUGIN_ROOT}/references/shared/core-constraints.md"
 
 执行前加载:
 ```bash
-cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/style-adapter.md"
+cat "${SKILL_ROOT}/references/style-adapter.md"
 ```
 
 硬要求:
@@ -186,7 +217,7 @@ cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/style-adapter.md"
 
 执行前加载:
 ```bash
-cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/step-3-review-gate.md"
+cat "${SKILL_ROOT}/references/step-3-review-gate.md"
 ```
 
 调用约束:
@@ -210,7 +241,8 @@ cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/step-3-review-gate.m
 
 审查指标落库(必做):
 ```bash
-python -m data_modules.index_manager save-review-metrics --data '{...}' --project-root "${PROJECT_ROOT}"
+# 必须先 cd 到脚本目录,否则 Python 找不到 data_modules
+cd "${SCRIPTS_DIR}" && python -m data_modules.index_manager --project-root "${PROJECT_ROOT}" save-review-metrics --data '{...}'
 ```
 
 硬要求:
@@ -221,8 +253,8 @@ python -m data_modules.index_manager save-review-metrics --data '{...}' --projec
 
 执行前必须加载:
 ```bash
-cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/polish-guide.md"
-cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/writing/typesetting.md"
+cat "${SKILL_ROOT}/references/polish-guide.md"
+cat "${SKILL_ROOT}/references/writing/typesetting.md"
 ```
 
 执行顺序:
@@ -284,11 +316,11 @@ git commit -m "Ch{chapter_num}: {title}"
 执行检查:
 
 ```bash
-test -f ".webnovel/state.json"
-test -f "正文/第{chapter_padded}章.md"
-test -f ".webnovel/summaries/ch{chapter_padded}.md"
-python -m data_modules.index_manager get-recent-review-metrics --limit 1 --project-root "${PROJECT_ROOT}"
-tail -n 1 ".webnovel/observability/data_agent_timing.jsonl" || true
+test -f "${PROJECT_ROOT}/.webnovel/state.json"
+test -f "${PROJECT_ROOT}/正文/第${chapter_padded}章.md"
+test -f "${PROJECT_ROOT}/.webnovel/summaries/ch${chapter_padded}.md"
+cd "${SCRIPTS_DIR}" && python -m data_modules.index_manager --project-root "${PROJECT_ROOT}" get-recent-review-metrics --limit 1
+tail -n 1 "${PROJECT_ROOT}/.webnovel/observability/data_agent_timing.jsonl" || true
 ```
 
 成功标准: