From d1f55f9df3096e31637814e3383e40aea55c80c0 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Sun, 16 Aug 2026 19:21:36 +0800 Subject: [PATCH] feat: export task archives on demand (#14) --- AGENTS.md | 23 +-- README.md | 13 +- dev_scripts/check_harness.py | 17 ++- dev_scripts/export_task_archives.py | 131 +++++++++++++++++ dev_scripts/new_task_archive.py | 24 +--- dev_scripts/sync_wiki_docs.py | 4 +- dev_scripts/wiki_docs.py | 40 +++++- docs/00-project-profile.md | 18 +-- docs/01-workflow.md | 50 ++++--- docs/07-new-project-documentation-setup.md | 10 +- docs/README.md | 12 +- tests/test_harness_docs.py | 6 + tests/test_wiki_docs.py | 158 ++++++++++++++++++++- wiki-docs.json | 52 ------- 14 files changed, 424 insertions(+), 134 deletions(-) create mode 100644 dev_scripts/export_task_archives.py diff --git a/AGENTS.md b/AGENTS.md index 020e9e3..519e403 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Agent 开发规则 -本仓库采用 DevHarness 工作流:Gitea 工单是任务过程的事实来源,Gitea Wiki 是长期开发文档和任务归档的事实来源,Git 是代码与版本绑定资料的变更记录,`docs/` 只保存 Wiki 的只读镜像。人负责确认方案与验收,Agent 负责检查、实现、测试和留下证据。 +本仓库采用 DevHarness 工作流:Gitea 工单是任务过程的事实来源,Gitea Wiki 是长期开发文档和任务归档的事实来源,Git 是代码与版本绑定资料的变更记录。`docs/` 默认保存核心 Wiki 的只读镜像,`docs/task/` 只保存人工按需导出的任务归档快照。人负责确认方案与验收,Agent 负责检查、实现、测试和留下证据。 开始工作前先阅读 [项目档案](docs/00-project-profile.md) 和任务涉及目录中的 `AGENTS.md`。目录越深的规则越具体,但不得削弱上级安全规则。 @@ -37,7 +37,7 @@ 6. 开始实施前检查分支和工作区,明确哪些现有改动不属于本任务。 7. 严格按工单范围实现;新发现的问题先记录,不顺手混入当前任务。 8. 执行与风险相称的测试,把关键结果和未验证部分更新到工单。实施过程中出现计划外、当前无法解除的问题时才标记“阻塞”。 -9. 长期文档必须先修改 Wiki、读取确认,再运行 `python dev_scripts/sync_wiki_docs.py` 导出本地镜像;不得直接编辑 `docs/` 后反向覆盖 Wiki。 +9. 长期核心文档必须先修改 Wiki、读取确认,再运行 `python dev_scripts/sync_wiki_docs.py` 导出本地镜像;任务归档默认只更新 Wiki,不自动导出本地。不得直接编辑镜像后反向覆盖 Wiki。 Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明确授权,不得默认绕过建单。 @@ -51,10 +51,12 @@ Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明 - `建工单并做`:依次建单和执行,`建工单,做`、`建工单,做` 含义相同;停在“待验收”。 - `继续工单 #N`:核对工单、Git 和 Wiki 证据,从首个未完成步骤继续,不重复仍然有效的检查。 - `检查工单 #N`:只读核对范围、验收、测试和证据并输出报告;不自动修复。 -- `同步文档`:读取 Wiki、导出 `docs/` 并检查一致性;不修改 Wiki、不自动提交。 -- `#N 验收通过`:仅在用户明确验收后,更新归档、同步并提交镜像、推送、同步父工单并关闭任务。 +- `同步文档`:读取 Wiki、导出核心 `docs/` 并检查一致性,不处理任务归档;不修改 Wiki、不自动提交。 +- `导出任务归档`:人工触发 `python dev_scripts/export_task_archives.py`,只导出新增或 revision 已变化的任务归档;不删除本地文件、不自动提交。 +- `导出全部任务归档`:人工触发 `python dev_scripts/export_task_archives.py --all`,读取并导出全部线上任务归档;不删除本地文件、不自动提交。 +- `#N 验收通过`:仅在用户明确验收后,更新 Wiki 归档、同步必要的核心文档、推送、同步父工单并关闭任务;不自动导出任务归档。 -方案未确认或前置依赖未满足时,实施类指令必须停在对应门禁;除 `#N 验收通过` 外,快捷指令不得关闭待验收工单。Gitea 工单不导出全文,本地只保存 Wiki 任务归档镜像。详细语义见 [开发工作流](docs/01-workflow.md)。 +方案未确认或前置依赖未满足时,实施类指令必须停在对应门禁;除 `#N 验收通过` 外,快捷指令不得关闭待验收工单。任务归档导出必须由用户明确提出,其他指令不得隐式执行。Gitea 工单不导出全文,`docs/task/` 只是可能不完整的 Wiki 任务归档快照。详细语义见 [开发工作流](docs/01-workflow.md)。 ### 需求记录与流转 @@ -62,7 +64,7 @@ Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明 - 工单中的目标、非目标、已确认方案、验收标准和文档影响构成确认后的正式任务需求。 - 影响范围、接口、数据、风险或验收的需求变化必须记录日期、内容、原因和用户确认;会改变已确认结果时先更新工单并等待再次确认。 - 不复制完整聊天,不保存 Agent 内部推理,不写入密码、令牌、个人数据或生产数据;包含敏感信息的原话必须删除敏感部分或改写为脱敏摘要。 -- 长期有效的产品需求、业务规则和系统边界进入对应 Wiki 主题页并导出 `docs/`;完成结果进入 Wiki 任务归档并导出 `docs/task/`。Gitea 工单全文不导出到仓库。 +- 长期有效的产品需求、业务规则和系统边界进入对应 Wiki 主题页并导出核心 `docs/`;完成结果进入 Wiki 任务归档,仅在用户明确要求时导出 `docs/task/`。Gitea 工单全文不导出到仓库。 详细记录边界见 [开发工作流](docs/01-workflow.md) 与 [业务规则和术语](docs/03-business-rules-and-glossary.md)。 @@ -126,8 +128,8 @@ Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明 1. 逐项完成验收、测试和实现提交,并把最终方案、差异、结果、提交及遗留问题写回工单。 2. 工单保持“待验收”,用户没有明确验收通过前不得关闭。 -3. 运行 `python dev_scripts/new_task_archive.py <编号> "<短标题>"`,先创建 Wiki 归档,再登记并导出本地镜像。 -4. 读取确认 Wiki,运行 `python dev_scripts/sync_wiki_docs.py --check`;归档镜像单独提交,并把页面、revision、路径和提交哈希写回工单。 +3. 运行 `python dev_scripts/new_task_archive.py <编号> "<短标题>"`,只创建 Wiki 任务归档,不登记或导出本地镜像。 +4. 读取确认 Wiki,运行 `python dev_scripts/sync_wiki_docs.py --check` 检查核心镜像,并把任务归档页面、revision 和提交哈希写回工单。只有用户明确提出时才增量或全量导出任务归档。 5. 用户验收通过后关闭单元工单,并同步更新 MVP 和 Epic。 MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才能关闭 MVP。Epic 的全部范围完成后才能关闭 Epic。 @@ -166,8 +168,9 @@ MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才 -- 长期开发文档以 Gitea Wiki 为事实来源,`docs/` 是显式映射生成的只读镜像。 -- Wiki 与镜像的固定顺序是:修改 Wiki → 读取确认 → 导出 `docs` → 校验差异 → 提交镜像。 +- 长期开发文档以 Gitea Wiki 为事实来源,`docs/` 默认保存显式映射生成的核心只读镜像;`docs/task/` 是人工按需快照,可能不完整或不是最新状态。 +- 核心 Wiki 与镜像的固定顺序是:修改 Wiki → 读取确认 → 导出核心 `docs` → 校验差异 → 提交镜像。 +- 任务归档默认只保存在 Wiki;`导出任务归档` 或 `导出全部任务归档` 必须由用户明确提出,且不得自动传播删除或重命名。 - 同步配置只允许写入 `docs/` 下的 Markdown;发现镜像有未提交修改时必须停止。 - Gitea 凭据只通过进程环境或 MCP 安全配置提供,不得写入仓库。 - `dev_scripts/` 只存放 DevHarness 自身工具;业务项目的通用脚本必须使用独立目录,不得混放。 diff --git a/README.md b/README.md index 9b294d2..cd45e2e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ DevHarness 是一个以 Gitea 工单管理任务过程、以 Gitea Wiki 管理 -> Agent 实现并测试 -> 提交代码并更新工单 -> 人工验收 - -> 先归档 Wiki,再导出 docs/task 镜像 + -> 归档 Wiki;任务快照仅在人工提出时导出 -> 关闭工单并更新父工单 ``` @@ -44,17 +44,18 @@ docs/00-project-profile.md Wiki 项目档案的只读镜像 docs/01-workflow.md Wiki 开发工作流的只读镜像 docs/02-07*.md 代码地图、业务、验证、修改、排错和初始化镜像 docs/templates/task-archive.md Wiki 任务归档模板的只读镜像 -docs/task/ Wiki 任务归档页的只读镜像 -wiki-docs.json Wiki 页面到本地镜像的显式映射 +docs/task/ 人工按需导出的 Wiki 任务归档快照 +wiki-docs.json 核心 Wiki 页面到本地镜像的显式映射 dev_scripts/check_harness.py 模板和归档的最小自检 dev_scripts/sync_wiki_docs.py 单向导出或检查 Wiki 镜像 -dev_scripts/new_task_archive.py 先创建 Wiki 任务归档,再导出镜像 +dev_scripts/new_task_archive.py 只创建 Wiki 任务归档 +dev_scripts/export_task_archives.py 人工增量或全量导出任务归档 ``` ## 设计原则 - 人决定目标、范围和验收结果,Agent 负责检查、实现和验证。 -- 工单记录实施过程,Wiki 保存长期有效的最终事实,`docs/` 只保存可审查的镜像。 +- 工单记录实施过程,Wiki 保存长期有效的最终事实,`docs/` 默认保存核心镜像;任务归档快照按需导出。 - 一个单元工单只解决一个可独立测试和回退的问题。 -- 实现提交与归档提交分开,便于审查与追溯。 +- 实现提交与必要的核心文档镜像提交分开,便于审查与追溯。 - 凭据、个人数据和生产数据不得进入代码、工单或归档。 diff --git a/dev_scripts/check_harness.py b/dev_scripts/check_harness.py index 159b811..3739c2c 100644 --- a/dev_scripts/check_harness.py +++ b/dev_scripts/check_harness.py @@ -141,6 +141,7 @@ REQUIRED_FILES = ( "wiki-docs.json", "dev_scripts/wiki_docs.py", "dev_scripts/sync_wiki_docs.py", + "dev_scripts/export_task_archives.py", ".gitea/issue_template/epic.md", ".gitea/issue_template/mvp.md", ".gitea/issue_template/task.md", @@ -176,6 +177,15 @@ def check_archives(errors: list[str]) -> None: if not re.match(r"^\d+-.+\.md$", path.name): errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}") content = path.read_text(encoding="utf-8") + try: + metadata, _ = parse_mirror(content) + except WikiDocsError as exc: + errors.append(f"{path.name} 的任务镜像无效:{exc}") + continue + if re.fullmatch(r"Task-\d+-.+", metadata.get("wiki_page", "")) is None: + errors.append(f"{path.name} 的 wiki_page 不是任务归档页面") + if re.fullmatch(r"[0-9a-f]{40,64}", metadata.get("wiki_revision", "")) is None: + errors.append(f"{path.name} 的 wiki_revision 无效") for heading in ARCHIVE_HEADINGS: if heading not in content: errors.append(f"{path.name} 缺少章节:{heading}") @@ -252,7 +262,7 @@ def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: "单元任务是唯一正式实施单位", "高风险修改必须停止", "用户没有明确验收通过前不得关闭", - "长期文档必须先修改 Wiki", + "长期核心文档必须先修改 Wiki", "提交只包含当前工单相关文件", "### 自然语言快捷指令", "`只分析`", @@ -262,6 +272,8 @@ def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: "`继续工单 #N`", "`检查工单 #N`", "`同步文档`", + "`导出任务归档`", + "`导出全部任务归档`", "`#N 验收通过`", "### 需求记录与流转", "不得臆造用户原话", @@ -309,7 +321,7 @@ def core_mapping_errors(configured_mappings: dict[str, str]) -> list[str]: def check_wiki_mirrors(errors: list[str]) -> None: - """检查每份本地文档都有显式映射和可追踪的镜像头。""" + """检查核心映射与镜像头;任务快照由 check_archives 单独检查。""" try: config = load_config() @@ -323,6 +335,7 @@ def check_wiki_mirrors(errors: list[str]) -> None: mapped_paths = {mapping.path for mapping in config.mappings} actual_paths = { path.relative_to(ROOT).as_posix() for path in (ROOT / "docs").rglob("*.md") + if path.parent != ROOT / "docs" / "task" } for path in sorted(actual_paths - mapped_paths): errors.append(f"docs 中存在未登记的 Wiki 镜像:{path}") diff --git a/dev_scripts/export_task_archives.py b/dev_scripts/export_task_archives.py new file mode 100644 index 0000000..8923af0 --- /dev/null +++ b/dev_scripts/export_task_archives.py @@ -0,0 +1,131 @@ +"""把 Gitea Wiki 任务归档人工按需导出到 docs/task。""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path +from typing import Any + +from new_task_archive import safe_title +from wiki_docs import ( + DEFAULT_CONFIG, + ROOT, + WikiClient, + WikiDocsError, + dirty_paths, + load_config, + parse_mirror, + write_mirror, +) + + +TASK_PAGE_PATTERN = re.compile(r"^Task-(?P\d+)-(?P.+)$") + + +def task_revision(metadata: dict[str, Any], page_name: str) -> str: + last_commit = metadata.get("last_commit") + revision = last_commit.get("sha") if isinstance(last_commit, dict) else None + if not isinstance(revision, str) or not revision: + raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}") + return revision + + +def existing_task_mirrors(root: Path = ROOT) -> dict[str, Path]: + """按镜像头匹配已有文件,兼容历史自定义文件名。""" + + mirrors: dict[str, Path] = {} + task_dir = root / "docs" / "task" + if not task_dir.is_dir(): + return mirrors + for path in task_dir.glob("*.md"): + try: + metadata, _ = parse_mirror(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, WikiDocsError) as exc: + raise WikiDocsError(f"已有任务镜像无效 {path.name}:{exc}") from exc + page_name = metadata.get("wiki_page", "") + if not TASK_PAGE_PATTERN.fullmatch(page_name): + raise WikiDocsError(f"已有任务镜像页面名无效 {path.name}:{page_name}") + if page_name in mirrors: + raise WikiDocsError(f"任务页面存在重复本地镜像:{page_name}") + mirrors[page_name] = path + return mirrors + + +def task_target(page_name: str, root: Path = ROOT) -> Path: + match = TASK_PAGE_PATTERN.fullmatch(page_name) + if match is None: + raise WikiDocsError(f"不是任务归档页面:{page_name}") + title = safe_title(match.group("title")) + if not title: + raise WikiDocsError(f"任务归档标题无效:{page_name}") + return root / "docs" / "task" / f"{match.group('number')}-{title}.md" + + +def export_task_archives( + client: WikiClient, *, export_all: bool = False, root: Path = ROOT +) -> list[str]: + """增量或全量读取任务归档;绝不删除本地文件。""" + + dirty = dirty_paths(["docs/task"], root) + if dirty: + raise WikiDocsError( + "本地任务镜像存在未提交改动,已停止以防覆盖:\n" + "\n".join(dirty) + ) + + existing = existing_task_mirrors(root) + pages = [] + for metadata in client.list_pages(): + title = metadata.get("title") + if isinstance(title, str) and TASK_PAGE_PATTERN.fullmatch(title): + pages.append((int(title.split("-", 2)[1]), title, metadata)) + pages.sort(key=lambda item: (item[0], item[1])) + + messages: list[str] = [] + targets: set[Path] = set() + for _, page_name, metadata in pages: + target = existing.get(page_name, task_target(page_name, root)) + if target in targets: + raise WikiDocsError(f"多个任务页面映射到同一本地路径:{target.name}") + targets.add(target) + revision = task_revision(metadata, page_name) + if not export_all and target.is_file(): + local_metadata, _ = parse_mirror(target.read_text(encoding="utf-8")) + if ( + local_metadata.get("wiki_page") == page_name + and local_metadata.get("wiki_revision") == revision + ): + messages.append(f"跳过:{target.relative_to(root)} <- {page_name}@{revision[:12]}") + continue + page = client.get_page_from_metadata(metadata, page_name) + changed = write_mirror(target, page) + action = "已导出" if changed else "无变化" + messages.append(f"{action}:{target.relative_to(root)} <- {page_name}@{revision[:12]}") + return messages + + +def main() -> int: + parser = argparse.ArgumentParser(description="人工按需导出 Gitea Wiki 任务归档") + parser.add_argument( + "--all", action="store_true", help="全量读取全部线上任务归档;默认按 revision 增量" + ) + parser.add_argument( + "--config", default=str(DEFAULT_CONFIG), help="核心 Wiki 映射配置" + ) + args = parser.parse_args() + try: + config = load_config(Path(args.config).resolve()) + messages = export_task_archives( + WikiClient(config), export_all=args.all + ) + except WikiDocsError as exc: + print(f"错误:{exc}") + return 1 + for message in messages: + print(message) + print("任务归档全量导出完成" if args.all else "任务归档增量导出完成") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dev_scripts/new_task_archive.py b/dev_scripts/new_task_archive.py index a1a01ad..25b88fe 100644 --- a/dev_scripts/new_task_archive.py +++ b/dev_scripts/new_task_archive.py @@ -1,4 +1,4 @@ -"""先在 Gitea Wiki 创建任务归档,再登记并导出本地镜像。""" +"""只在 Gitea Wiki 创建任务归档;本地镜像由人工按需导出。""" from __future__ import annotations @@ -9,12 +9,9 @@ from pathlib import Path from wiki_docs import ( DEFAULT_CONFIG, - Mapping, WikiClient, WikiDocsError, - append_mapping, load_config, - sync_all, ) @@ -43,7 +40,7 @@ def build_archive( def main() -> int: parser = argparse.ArgumentParser( - description="在 Gitea Wiki 创建任务归档并导出 docs/task 镜像" + description="在 Gitea Wiki 创建任务归档,不自动导出本地镜像" ) parser.add_argument("issue_number", help="Gitea 工单号,例如 123") parser.add_argument("title", help="简短任务标题") @@ -61,15 +58,9 @@ def main() -> int: try: config = load_config(Path(args.config).resolve()) page_name = f"Task-{args.issue_number}-{short_title}" - local_path = f"docs/task/{args.issue_number}-{short_title}.md" - mapping = Mapping(page=page_name, path=local_path) - if any( - item.page == mapping.page or item.path == mapping.path - for item in config.mappings - ): - raise WikiDocsError(f"任务归档已经登记:{page_name}") - client = WikiClient(config) + if any(item.get("title") == page_name for item in client.list_pages()): + raise WikiDocsError(f"任务归档已经存在:{page_name}") template = client.get_page("Task-Archive-Template").text issue_url = ( f"{config.gitea_url}/{config.owner}/{config.repository}/issues/" @@ -83,17 +74,12 @@ def main() -> int: content, f"docs: 创建任务 #{args.issue_number} 归档草稿", ) - append_mapping(config, mapping) - updated_config = load_config(config.path) - messages = sync_all(updated_config, client) except WikiDocsError as exc: print(f"错误:{exc}") return 1 print(f"已创建 Wiki:{page.html_url}") - for message in messages: - print(message) - print(f"已登记镜像:{local_path}") + print("未导出本地任务归档;需要时运行 export_task_archives.py") return 0 diff --git a/dev_scripts/sync_wiki_docs.py b/dev_scripts/sync_wiki_docs.py index e44b9d6..25a30cf 100644 --- a/dev_scripts/sync_wiki_docs.py +++ b/dev_scripts/sync_wiki_docs.py @@ -1,4 +1,4 @@ -"""从 Gitea Wiki 单向导出本地 docs 镜像。""" +"""从 Gitea Wiki 单向导出配置中的核心 docs 镜像。""" from __future__ import annotations @@ -9,7 +9,7 @@ from wiki_docs import DEFAULT_CONFIG, WikiClient, WikiDocsError, load_config, sy def main() -> int: - parser = argparse.ArgumentParser(description="从 Gitea Wiki 单向同步 docs 镜像") + parser = argparse.ArgumentParser(description="从 Gitea Wiki 单向同步核心 docs 镜像") parser.add_argument( "--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件" ) diff --git a/dev_scripts/wiki_docs.py b/dev_scripts/wiki_docs.py index 5f111fd..6b1008c 100644 --- a/dev_scripts/wiki_docs.py +++ b/dev_scripts/wiki_docs.py @@ -210,6 +210,14 @@ class WikiClient: raise WikiDocsError( f"Wiki 页面不存在:{page_name};不会自动删除或重命名本地镜像" ) + return self.get_page_from_metadata(metadata, page_name) + + def get_page_from_metadata( + self, metadata: dict[str, Any], page_name: str | None = None + ) -> WikiPage: + """使用页面列表元数据读取正文,避免重复获取完整页面列表。""" + + resolved_name = page_name or _required_string(metadata, "title") sub_url = _required_string(metadata, "sub_url") page = self._request( "GET", @@ -218,20 +226,22 @@ class WikiClient: f"{quote(sub_url, safe='%')}", ) if not isinstance(page, dict): - raise WikiDocsError(f"Wiki 页面响应格式无效:{page_name}") + raise WikiDocsError(f"Wiki 页面响应格式无效:{resolved_name}") encoded_content = page.get("content_base64") if not isinstance(encoded_content, str): - raise WikiDocsError(f"Wiki 页面没有 content_base64:{page_name}") + raise WikiDocsError(f"Wiki 页面没有 content_base64:{resolved_name}") try: text = base64.b64decode(encoded_content, validate=True).decode("utf-8") except (ValueError, UnicodeDecodeError) as exc: - raise WikiDocsError(f"Wiki 页面不是有效的 UTF-8 Markdown:{page_name}") from exc + raise WikiDocsError( + f"Wiki 页面不是有效的 UTF-8 Markdown:{resolved_name}" + ) from exc last_commit = page.get("last_commit") revision = last_commit.get("sha") if isinstance(last_commit, dict) else None if not isinstance(revision, str) or not revision: - raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}") + raise WikiDocsError(f"Wiki 页面缺少 revision:{resolved_name}") title = page.get("title") - resolved_title = title if isinstance(title, str) and title else page_name + resolved_title = title if isinstance(title, str) and title else resolved_name html_url = ( f"{self.config.gitea_url}/{quote(self.config.owner, safe='')}/" f"{quote(self.config.repository, safe='')}/wiki/{quote(sub_url, safe='%')}" @@ -303,7 +313,14 @@ def render_mirror(page: WikiPage, existing: str | None = None) -> str: def dirty_mirror_paths(config: Config, root: Path = ROOT) -> list[str]: - paths = [mapping.path for mapping in config.mappings] + return dirty_paths([mapping.path for mapping in config.mappings], root) + + +def dirty_paths(paths: list[str], root: Path = ROOT) -> list[str]: + """返回指定路径中已有、修改或未跟踪的工作区条目。""" + + if not paths: + return [] result = subprocess.run( ["git", "status", "--porcelain", "--", *paths], cwd=root, @@ -329,6 +346,17 @@ def _write_atomic(path: Path, content: str) -> None: raise +def write_mirror(path: Path, page: WikiPage) -> bool: + """写入一份 Wiki 镜像;内容无变化时返回 False。""" + + existing = path.read_text(encoding="utf-8") if path.is_file() else None + rendered = render_mirror(page, existing) + if existing == rendered: + return False + _write_atomic(path, rendered) + return True + + def check_mirror(mapping: Mapping, page: WikiPage, path: Path) -> list[str]: if not path.is_file(): return [f"缺少镜像:{mapping.path}"] diff --git a/docs/00-project-profile.md b/docs/00-project-profile.md index b5b5495..10db76b 100644 --- a/docs/00-project-profile.md +++ b/docs/00-project-profile.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Project-Profile wiki_url: http://ilaer.eicp.net:8418/opc/dev_harness/wiki/Project-Profile.- -wiki_revision: c35c1737161ea428c487c066cb39eba4923535d2 -synchronized_at: 2026-08-10T11:02:51Z +wiki_revision: 62d1251f807f7d1b60017790c35d46186ffb9617 +synchronized_at: 2026-08-16T11:18:50Z <!-- gitea-wiki-mirror:end --> # 项目档案 @@ -63,9 +63,11 @@ synchronized_at: 2026-08-10T11:02:51Z | 查看工作区 | `git status --short --branch` | 显示分支且没有无关修改 | | 检查模板结构 | `python dev_scripts/check_harness.py --strict` | 输出“DevHarness 检查通过” | | 运行单元测试 | `python -m unittest discover -s tests -v` | 所有测试通过 | -| 导出 Wiki 镜像 | `python dev_scripts/sync_wiki_docs.py` | 映射页面写入 `docs/` | -| 检查 Wiki 镜像 | `python dev_scripts/sync_wiki_docs.py --check` | 输出镜像与 Wiki 一致 | -| 创建任务归档 | `python dev_scripts/new_task_archive.py 123 "修复登录超时"` | 先创建 Wiki 归档页,再登记并导出本地镜像 | +| 导出核心 Wiki 镜像 | `python dev_scripts/sync_wiki_docs.py` | 核心页面写入 `docs/`,不处理任务归档 | +| 检查核心 Wiki 镜像 | `python dev_scripts/sync_wiki_docs.py --check` | 输出核心镜像与 Wiki 一致 | +| 创建任务归档 | `python dev_scripts/new_task_archive.py 123 "修复登录超时"` | 只创建 Wiki 归档页,不写入本地 | +| 增量导出任务归档 | `python dev_scripts/export_task_archives.py` | 只导出新增或 revision 已变化的任务归档 | +| 全量导出任务归档 | `python dev_scripts/export_task_archives.py --all` | 读取并导出全部线上任务归档 | ## 目录边界 @@ -73,13 +75,13 @@ synchronized_at: 2026-08-10T11:02:51Z |---|---|---| | `.gitea/issue_template/` | Gitea 工单模板 | 凭据、任务最终归档 | | `docs/` | Wiki 自动导出的只读镜像 | 人工直接维护的长期文档 | -| `docs/task/` | Wiki 任务归档页的只读镜像 | 讨论过程和临时方案 | +| `docs/task/` | 人工按需导出的 Wiki 任务归档只读快照,可能不是完整历史 | 讨论过程和临时方案 | | `dev_scripts/` | Harness 检查、Wiki 同步和归档工具 | 产品功能代码 | | `tests/` | Harness 工具自动化测试 | 生产数据 | ## 环境、配置与凭据 -- Wiki 同步配置:仓库根目录 `wiki-docs.json`。 +- 核心 Wiki 同步配置:仓库根目录 `wiki-docs.json`;任务归档不逐页登记,由按需导出工具动态发现。 - Gitea 地址可由配置提供,也可通过 `GITEA_URL` 覆盖。 - Gitea PAT 仅通过 `GITEA_TOKEN` 或 MCP 安全配置提供,不写入仓库。 - Token 至少需要读取仓库权限;创建或更新 Wiki 时还需要写仓库权限。 @@ -90,7 +92,7 @@ synchronized_at: 2026-08-10T11:02:51Z ## 项目专用验收要求 -- 长期文档必须先更新 Wiki,再导出本地镜像。 +- 长期核心文档必须先更新 Wiki,再导出本地镜像;任务归档默认只保存在 Wiki,用户明确要求时才增量或全量导出。 - 镜像必须包含来源页面、revision 和同步时间。 - 页面删除、重命名和映射变更必须人工确认。 - 新增核心文档时必须更新 Home、显式映射和 Harness 检查。 diff --git a/docs/01-workflow.md b/docs/01-workflow.md index 6940054..aa1daf2 100644 --- a/docs/01-workflow.md +++ b/docs/01-workflow.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Development-Workflow wiki_url: http://ilaer.eicp.net:8418/opc/dev_harness/wiki/Development-Workflow.- -wiki_revision: ea3912217443b40f0fee66a6dc8e951d06e2c2f9 -synchronized_at: 2026-08-10T03:51:34Z +wiki_revision: 3f50238b637bc2f896ceab6caa5ab8ab1ded2a4b +synchronized_at: 2026-08-16T11:18:52Z <!-- gitea-wiki-mirror:end --> # 开发工作流 @@ -67,13 +67,13 @@ Agent 检查分支和工作区,只修改工单范围内的文件。发现新 - Git 提交哈希; - 相关 Wiki 页面及 revision。 -长期文档遵循唯一顺序: +长期核心文档遵循唯一顺序: ```text -修改 Wiki → 读取确认 → 导出 docs → 校验差异 → 提交镜像 +修改 Wiki → 读取确认 → 导出核心 docs → 校验差异 → 提交镜像 ``` -不得先编辑 `docs/` 再反向覆盖 Wiki。 +任务归档默认只更新 Wiki,不自动导出到 `docs/task/`。不得先编辑本地镜像再反向覆盖 Wiki。 ### 4. 待验收 @@ -81,21 +81,32 @@ Agent 检查分支和工作区,只修改工单范围内的文件。发现新 ### 5. 归档和关闭 -使用以下命令在 Wiki 创建任务归档页、登记显式映射并导出本地镜像: +使用以下命令只在 Wiki 创建任务归档页: ```powershell python dev_scripts/new_task_archive.py 123 "修复登录超时" ``` -归档内容以 Wiki 页面为主源;本地 `docs/task/<编号>-<短标题>.md` 是镜像。归档镜像单独提交,再把 Wiki 页面、revision、镜像路径和提交哈希写回工单。用户明确验收通过后,关闭单元工单并勾选父工单中的任务。 +归档内容以 Wiki 页面为事实来源。默认不修改 `wiki-docs.json`,也不写入 `docs/task/`。把 Wiki 页面、revision 和实现提交哈希写回工单;用户明确验收通过后,关闭单元工单并勾选父工单中的任务。 + +只有用户明确提出时才导出任务归档: + +```powershell +python dev_scripts/export_task_archives.py # 增量:新增或 revision 变化 +python dev_scripts/export_task_archives.py --all # 全量:读取全部线上任务归档 +``` + +导出不得自动删除本地文件。`docs/task/` 只是人工按需生成的只读快照,可能不是完整或最新的任务历史。 ## 文档同步规则 -- 映射保存在 `wiki-docs.json`,每个 Wiki 页面对应唯一仓库路径。 -- 同步脚本只实现 Wiki → `docs/`,不提供反向同步。 +- 核心页面映射保存在 `wiki-docs.json`;普通同步只处理这些核心长期文档。 +- 任务归档不逐页登记映射,由按需导出工具根据 `Task-<编号>-<标题>` 动态发现;已有镜像优先按镜像头匹配原页面。 +- 所有同步和导出只实现 Wiki → `docs/`,不提供反向同步。 - 镜像头必须记录页面名、页面地址、revision 和同步时间。 - 已跟踪镜像存在未提交改动时,同步必须停止;确认改动来源后再处理。 -- `--check` 只检查,不写文件;页面缺失、revision 不一致或正文不一致均失败。 +- 核心同步的 `--check` 只检查核心镜像,不要求线上任务归档全部存在于本地。 +- 已经导出的任务镜像仍必须具有来源页面、revision 和同步时间,并通过 Harness 格式检查。 - 页面删除和重命名不会自动传播,必须先更新工单并人工确认映射变化。 - Wiki 更新成功而导出失败时,在工单记录部分完成状态,不得把任务标为完成。 - 与具体代码版本强绑定的接口或迁移资料可直接随代码维护,但必须在 Wiki 提供入口或适用版本说明。 @@ -149,14 +160,14 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" | 关键原始需求、确认后的单次任务需求 | Gitea 单元任务工单 | 无 | | 讨论、决定和需求变化 | Gitea 工单正文或评论 | 无 | | 长期有效的产品需求、业务规则和系统边界 | 对应 Gitea Wiki 主题页 | `docs/` | -| 完成后的实现、验证和遗留问题 | Wiki 任务归档 | `docs/task/` | +| 完成后的实现、验证和遗留问题 | Wiki 任务归档 | 人工按需导出的 `docs/task/` 快照(可能不完整) | 任务产生长期结论时,先更新对应 Wiki 主题页,再导出本地镜像。Gitea 工单全文不导出到仓库,避免形成第二份任务过程记录。 ## 稳定文档与任务归档 - Home、项目档案、代码地图、业务规则、开发验证、常见修改和故障排查描述项目现在怎样工作。 -- 工单和任务归档解释某次为什么修改、实际改了什么以及如何验证。 +- 工单和 Wiki 任务归档解释某次为什么修改、实际改了什么以及如何验证;本地任务快照不是完整历史。 - 新人先读稳定主题页,只有追查历史原因时才读任务归档。 - 任务产生的长期结论必须合并到主题页,不能只留在归档。 @@ -204,16 +215,19 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" | `建工单并做` | 依次执行“建工单”和“执行工单”;`建工单,做`、`建工单,做` 含义相同 | 工单保持“待验收” | | `继续工单 #N` | 核对工单、Git 和 Wiki 证据,从首个未完成步骤继续,不重复仍然有效的检查 | 到达该工单当前流程的停止条件 | | `检查工单 #N` | 只读对照范围、验收标准、测试和证据,报告通过项、缺失项及未验证部分 | 输出检查报告;不自动修复 | -| `同步文档` | 读取 Wiki,导出已映射的 `docs/` 镜像并检查一致性 | 显示结果和差异;不修改 Wiki、不自动提交 | -| `#N 验收通过` | 记录明确验收,更新 Wiki 归档为“已完成”,导出并提交镜像,推送、同步父工单并关闭任务 | 工单“已完成”并关闭 | +| `同步文档` | 读取 Wiki,导出核心长期文档镜像并检查一致性,不处理任务归档 | 显示结果和差异;不修改 Wiki、不自动提交 | +| `导出任务归档` | 人工触发增量导出,只写入新增或 revision 已变化的任务归档 | 显示导出或跳过结果;不删除本地文件、不自动提交 | +| `导出全部任务归档` | 人工触发全量读取并导出线上全部任务归档 | 显示导出结果;不删除本地文件、不自动提交 | +| `#N 验收通过` | 记录明确验收,更新 Wiki 归档为“已完成”,同步必要的核心文档,推送、同步父工单并关闭任务;不自动导出任务归档 | 工单“已完成”并关闭 | 补充边界: - 方案未确认时,`建工单`、`建工单并做` 和 `执行工单 #N` 不得绕过确认;Agent 应停在方案确认。 - 前置依赖未满足且不允许并行时,实施类指令停在“待实施”。 - `#N 验收通过` 必须来自用户明确表达;其他快捷指令不得关闭待验收工单。 -- `同步文档` 发现镜像有未提交改动时停止,不覆盖现有修改。 -- Gitea 工单保留讨论和过程,不把工单全文导出到本地;`docs/task/` 只保存 Wiki 最终任务归档的镜像。 +- `同步文档` 或任务归档导出发现目标镜像有未提交改动时停止,不覆盖现有修改。 +- `导出任务归档` 和 `导出全部任务归档` 必须由用户明确提出,其他快捷指令不隐式执行。 +- Gitea 工单保留讨论和过程,不把工单全文导出到本地;`docs/task/` 只保存人工按需导出的 Wiki 最终任务归档快照。 ## 什么时候重新确认方案 @@ -235,6 +249,6 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时" | 讨论过程和临时方案 | 是 | 否 | 否 | | 实施进度和阻塞 | 是 | 否 | 否 | | 长期有效的最终方案 | 链接 | 是 | 镜像 | -| 测试结果与未验证内容 | 是 | 任务归档 | 镜像 | -| 提交哈希 | 是 | 任务归档 | 镜像 | +| 测试结果与未验证内容 | 是 | 任务归档 | 按需镜像 | +| 提交哈希 | 是 | 任务归档 | 按需镜像 | | 与具体代码版本绑定的说明 | 可链接 | 提供入口 | 是 | diff --git a/docs/07-new-project-documentation-setup.md b/docs/07-new-project-documentation-setup.md index 969cba9..29a7366 100644 --- a/docs/07-new-project-documentation-setup.md +++ b/docs/07-new-project-documentation-setup.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: New-Project-Documentation-Setup wiki_url: http://ilaer.eicp.net:8418/opc/dev_harness/wiki/New-Project-Documentation-Setup.- -wiki_revision: a443c206780ecd40ae45732ca10686427f393349 -synchronized_at: 2026-08-10T11:03:03Z +wiki_revision: cf49e125e7e73d8c818df61153d76abac6abfaaa +synchronized_at: 2026-08-16T11:19:05Z <!-- gitea-wiki-mirror:end --> # 新项目文档初始化 @@ -46,9 +46,9 @@ synchronized_at: 2026-08-10T11:03:03Z ### 4. 修改镜像配置 -把 `wiki-docs.json` 中的地址、owner 和 repository 改成新项目;核心主题映射保留。 +把 `wiki-docs.json` 中的地址、owner 和 repository 改成新项目;只保留核心主题映射,任务归档不逐页登记。 -确认当前目录确实是新项目副本、且 DevHarness 历史归档不需要保留后,移除属于 DevHarness 的任务归档映射和对应 `docs/task/` 镜像。不要在原 DevHarness 仓库或已有业务项目中执行这项清理。 +确认当前目录确实是新项目副本、且 DevHarness 历史归档快照不需要保留后,可以移除对应 `docs/task/` 文件。不要在原 DevHarness 仓库或已有业务项目中执行这项清理。 不要把 PAT 写入配置。 @@ -115,7 +115,7 @@ python dev_scripts/sync_wiki_docs.py --check python -m unittest discover -s tests -v ``` -只有线上 Wiki 确认后才导出 `docs/`。旧项目的任务归档不能带入新项目历史。 +只有线上 Wiki 确认后才导出核心 `docs/`。任务归档默认不导出;用户明确要求时再运行 `python dev_scripts/export_task_archives.py` 或加 `--all`。旧项目的任务归档快照不能带入新项目历史。 ## 完成标准 diff --git a/docs/README.md b/docs/README.md index 117b4e2..48b7ae0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Home wiki_url: http://ilaer.eicp.net:8418/opc/dev_harness/wiki/Home -wiki_revision: 55d89b7aec659f55a61e7ceef881ef34a62d9379 -synchronized_at: 2026-08-10T06:58:38Z +wiki_revision: 52e9a6f8fe4fe03db8c65792ef500cc9a5084384 +synchronized_at: 2026-08-16T11:18:48Z <!-- gitea-wiki-mirror:end --> # DevHarness 文档中心 @@ -65,7 +65,8 @@ python dev_scripts/sync_wiki_docs.py --check | 任务状态、讨论、阻塞、验收过程 | Gitea 工单 | | 架构、业务规则、开发规范、操作手册、交付文档、任务归档 | Gitea Wiki | | 源码和与特定代码版本强绑定的文档 | Git 仓库 | -| 离线浏览副本 | Git 仓库中的 `docs/` Wiki 镜像 | +| 核心长期文档的离线浏览副本 | Git 仓库中的 `docs/` Wiki 镜像 | +| 完整任务归档 | Gitea Wiki;`docs/task/` 仅是人工按需导出的快照 | 本地 `docs/` 不是编辑入口。长期文档必须先修改 Wiki,读取确认后再导出镜像。 @@ -84,9 +85,10 @@ python dev_scripts/sync_wiki_docs.py --check 修改 Wiki → 读取确认 → 导出 docs → 校验差异 → 提交镜像 ``` -- 页面和本地路径通过仓库中的 `wiki-docs.json` 显式映射。 +- 核心页面和本地路径通过仓库中的 `wiki-docs.json` 显式映射;普通同步不处理任务归档。 +- 任务归档默认只保存在 Wiki,只有用户明确提出时才增量或全量导出到 `docs/task/`。 - 镜像头记录来源页面、Wiki revision 和同步时间。 - 已映射镜像存在未提交修改时同步必须停止。 - 页面删除、重命名和映射变更必须人工确认。 -- Wiki 或导出失败时,相关任务不能标记为完成。 +- 核心 Wiki 或必要同步失败时,相关任务不能标记为完成;未请求任务归档导出不阻止任务完成。 - 凭据、个人数据和生产数据不得进入 Wiki 或镜像。 diff --git a/tests/test_harness_docs.py b/tests/test_harness_docs.py index 649a02e..41855b5 100644 --- a/tests/test_harness_docs.py +++ b/tests/test_harness_docs.py @@ -43,6 +43,12 @@ class CoreDocumentTests(unittest.TestCase): for page, path in CORE_PAGE_PATHS.items(): self.assertEqual(mappings.get(page), path) + def test_task_archives_are_not_core_mappings(self) -> None: + config = load_config() + self.assertFalse( + any(mapping.path.startswith("docs/task/") for mapping in config.mappings) + ) + def test_existing_project_adoption_guide_is_core_document(self) -> None: path = "docs/08-existing-project-adoption.md" self.assertEqual( diff --git a/tests/test_wiki_docs.py b/tests/test_wiki_docs.py index 4d8c19a..c902096 100644 --- a/tests/test_wiki_docs.py +++ b/tests/test_wiki_docs.py @@ -12,7 +12,16 @@ from unittest.mock import Mock, patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "dev_scripts")) -from new_task_archive import build_archive, safe_title # noqa: E402 +from new_task_archive import ( # noqa: E402 + build_archive, + main as new_archive_main, + safe_title, +) +from export_task_archives import ( # noqa: E402 + existing_task_mirrors, + export_task_archives, + task_target, +) from wiki_docs import ( # noqa: E402 Config, Mapping, @@ -158,6 +167,153 @@ class ArchiveTests(unittest.TestCase): self.assertIn("Task-12-login", result) self.assertNotIn("YYYY-MM-DD", result) + @patch("new_task_archive.WikiClient") + def test_create_archive_does_not_change_core_mapping(self, client_class) -> None: + with tempfile.TemporaryDirectory() as directory: + config_path = Path(directory) / "wiki-docs.json" + original = json.dumps( + { + "schema_version": 1, + "gitea_url": "http://gitea.example", + "owner": "o", + "repository": "r", + "mappings": [ + {"page": "Home", "path": "docs/README.md"} + ], + } + ) + config_path.write_text(original, encoding="utf-8") + client = client_class.return_value + client.list_pages.return_value = [] + client.get_page.return_value = WikiPage( + title="Task-Archive-Template", + sub_url="Task-Archive-Template.-", + text="# <工单号> <标题>\nYYYY-MM-DD\n<链接>\n<页面名>\n", + revision="a" * 40, + html_url="http://gitea.example/wiki/template", + ) + client.create_page.return_value = WikiPage( + title="Task-14-按需导出", + sub_url="Task-14.-", + text="# 14 按需导出\n", + revision="b" * 40, + html_url="http://gitea.example/wiki/task-14", + ) + with patch.object( + sys, + "argv", + [ + "new_task_archive.py", + "14", + "按需导出", + "--config", + str(config_path), + ], + ): + result = new_archive_main() + self.assertEqual(config_path.read_text(encoding="utf-8"), original) + self.assertEqual(result, 0) + client.create_page.assert_called_once() + + def test_task_target_uses_stable_safe_name(self) -> None: + with tempfile.TemporaryDirectory() as directory: + target = task_target("Task-14-修复:导出", Path(directory)) + self.assertEqual(target.name, "14-修复-导出.md") + + def test_existing_mirror_keeps_historical_custom_filename(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + path = root / "docs" / "task" / "2-初级维护者文档体系.md" + path.parent.mkdir(parents=True) + page = WikiPage( + title="Task-2-Junior-Maintainer-Docs", + sub_url="Task-2-Junior-Maintainer-Docs.-", + text="# 2 文档\n", + revision="c" * 40, + html_url="http://gitea.example/wiki/task-2", + ) + path.write_text(render_mirror(page), encoding="utf-8") + mirrors = existing_task_mirrors(root) + self.assertEqual( + mirrors["Task-2-Junior-Maintainer-Docs"].name, + "2-初级维护者文档体系.md", + ) + + @patch("export_task_archives.dirty_paths", return_value=[]) + def test_incremental_export_skips_same_revision(self, _dirty) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + path = root / "docs" / "task" / "14-按需导出.md" + path.parent.mkdir(parents=True) + page = WikiPage( + title="Task-14-按需导出", + sub_url="Task-14.-", + text="# 14 按需导出\n", + revision="d" * 40, + html_url="http://gitea.example/wiki/task-14", + ) + path.write_text(render_mirror(page), encoding="utf-8") + client = Mock() + client.list_pages.return_value = [ + { + "title": page.title, + "sub_url": page.sub_url, + "last_commit": {"sha": page.revision}, + } + ] + messages = export_task_archives(client, root=root) + self.assertTrue(messages[0].startswith("跳过:")) + client.get_page_from_metadata.assert_not_called() + + @patch( + "export_task_archives.dirty_paths", + return_value=[" M docs/task/14-按需导出.md"], + ) + def test_export_stops_before_wiki_read_when_task_mirror_is_dirty( + self, _dirty + ) -> None: + client = Mock() + with self.assertRaisesRegex(WikiDocsError, "未提交改动"): + export_task_archives(client) + client.list_pages.assert_not_called() + + @patch("export_task_archives.dirty_paths", return_value=[]) + def test_full_export_reads_all_and_never_deletes_extra_file(self, _dirty) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + task_dir = root / "docs" / "task" + task_dir.mkdir(parents=True) + extra = task_dir / "99-历史快照.md" + extra_page = WikiPage( + title="Task-99-历史快照", + sub_url="Task-99.-", + text="# 99 历史快照\n", + revision="e" * 40, + html_url="http://gitea.example/wiki/task-99", + ) + extra.write_text(render_mirror(extra_page), encoding="utf-8") + page = WikiPage( + title="Task-14-按需导出", + sub_url="Task-14.-", + text="# 14 按需导出\n", + revision="f" * 40, + html_url="http://gitea.example/wiki/task-14", + ) + client = Mock() + metadata = { + "title": page.title, + "sub_url": page.sub_url, + "last_commit": {"sha": page.revision}, + } + client.list_pages.return_value = [metadata] + client.get_page_from_metadata.return_value = page + messages = export_task_archives(client, export_all=True, root=root) + exported = root / "docs" / "task" / "14-按需导出.md" + self.assertTrue(exported.is_file()) + self.assertTrue(extra.is_file()) + self.assertTrue(messages[0].startswith("已导出:")) + client.get_page_from_metadata.assert_called_once_with(metadata, page.title) + if __name__ == "__main__": unittest.main() diff --git a/wiki-docs.json b/wiki-docs.json index cb1b54c..46c87a7 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -55,58 +55,6 @@ { "page": "Task-Archive-Template", "path": "docs/templates/task-archive.md" - }, - { - "page": "Task-1-Wiki-文档主源", - "path": "docs/task/1-Wiki-文档主源.md" - }, - { - "page": "Task-2-Junior-Maintainer-Docs", - "path": "docs/task/2-初级维护者文档体系.md" - }, - { - "page": "Task-3-Dev-Scripts-Rename", - "path": "docs/task/3-dev_scripts目录重命名.md" - }, - { - "page": "Task-4-Agent-Efficiency-Scope", - "path": "docs/task/4-Agent效率与范围控制.md" - }, - { - "page": "Task-5-Simplify-Agents", - "path": "docs/task/5-精简AGENTS重复说明.md" - }, - { - "page": "Task-6-优化ClaudeCode规则入口", - "path": "docs/task/6-优化ClaudeCode规则入口.md" - }, - { - "page": "Task-7-ClaudeCode模型路由", - "path": "docs/task/7-ClaudeCode模型路由.md" - }, - { - "page": "Task-8-最小工单依赖规则", - "path": "docs/task/8-最小工单依赖规则.md" - }, - { - "page": "Task-9-Agent自然语言快捷指令", - "path": "docs/task/9-Agent自然语言快捷指令.md" - }, - { - "page": "Task-10-需求记录与流转规则", - "path": "docs/task/10-需求记录与流转规则.md" - }, - { - "page": "Task-11-交付文档指南与岗位文档模板", - "path": "docs/task/11-交付文档指南与岗位文档模板.md" - }, - { - "page": "Task-12-已有项目接入DevHarness指南", - "path": "docs/task/12-已有项目接入DevHarness指南.md" - }, - { - "page": "Task-13-多子项目与独立交付单元", - "path": "docs/task/13-多子项目与独立交付单元.md" } ] }