commit d6706f7c69c6c5e5ad574dd20a2d997b501a1cc2 Author: QiuSW <105186638@qq.com> Date: Fri Aug 7 23:11:25 2026 +0800 chore: initialize DevHarness template diff --git a/.gitea/issue_template/epic.md b/.gitea/issue_template/epic.md new file mode 100644 index 0000000..d908c1e --- /dev/null +++ b/.gitea/issue_template/epic.md @@ -0,0 +1,31 @@ +## 背景 + + + +## 目标 + +- + +## 非目标 + +- + +## 总体方案 + + + +## 阶段路线 + +1. + +## MVP 与任务索引 + +- [ ] # MVP 工单 + +## 依赖、风险和回退 + +- + +## 最终验收标准 + +- [ ] diff --git a/.gitea/issue_template/mvp.md b/.gitea/issue_template/mvp.md new file mode 100644 index 0000000..eba1f36 --- /dev/null +++ b/.gitea/issue_template/mvp.md @@ -0,0 +1,27 @@ +## 基本信息 + +- 所属 Epic:# + +## MVP 目标 + + + +## 包含范围 + +- + +## 排除范围 + +- + +## 阶段与单元任务 + +- [ ] # 单元任务 + +## 集成风险和回退 + +- + +## MVP 验收标准 + +- [ ] diff --git a/.gitea/issue_template/task.md b/.gitea/issue_template/task.md new file mode 100644 index 0000000..998627f --- /dev/null +++ b/.gitea/issue_template/task.md @@ -0,0 +1,36 @@ +## 基本信息 + +- 类型:需求 / 缺陷 / 重构 +- 所属 Epic:# +- 所属 MVP / 版本:# +- 阶段: + +## 要解决什么 + + + +## 做什么 / 不做什么 + +- 做: +- 不做: + +## 已确认方案 + + + +预计修改文件: + +- + +## 验收标准 + +- [ ] +- [ ] + +## 验证方式 + + + +## 风险和回退 + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..135e39b --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# 本地环境与凭据 +.env +.env.* +!.env.example +*.local + +# 编辑器与系统文件 +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# 常见构建、测试缓存 +__pycache__/ +.pytest_cache/ +.coverage +coverage/ +dist/ +build/ +node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..44efe61 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# Agent 开发规则 + +本仓库采用 DevHarness 工作流:Gitea 工单是实施期间的事实来源,Git 是代码变更记录,`docs/task` 是完成后的最终归档。人负责确认方案与验收,Agent 负责检查、实现、测试和留下证据。 + +开始工作前先阅读 [项目档案](docs/00-project-profile.md) 和任务涉及目录中的 `AGENTS.md`。目录越深的规则越具体,但不得削弱上级安全规则。 + +## 1. 永久规则 + +- 不把密码、令牌、Cookie、私钥、个人数据或生产数据写入代码、日志、工单和文档。 +- 不执行未经用户明确授权的发布、付款、删除数据、破坏性迁移或其他不可逆操作。 +- 保留用户已有和任务无关的工作区改动,不擅自重置、覆盖或混入提交。 +- 发现需求与安全规则、已确认方案或现有数据冲突时,先停止实施并说明影响。 +- 测试结果必须真实;未执行或无法覆盖的验证必须明确记录。 + +项目专用红线写入本文件的“项目专用规则”或对应子目录的 `AGENTS.md`,不要散落在聊天记录中。 + +## 2. 哪些改动需要工单 + +新功能、缺陷修复、重构以及任何用户可感知或改变程序行为的修改,必须先建立单元任务工单。 + +以下小改动可以直接提交,不要求工单和任务归档: + +- 只改错别字、注释或文档措辞; +- 只做格式化、导入排序或不跨文件的内部变量改名; +- 补充类型标注或文档字符串且不改变行为; +- 删除已经确认无人使用的死代码。 + +只要涉及接口、数据库、状态、权限、安全、并发、用户界面,或者无法确定是否改变行为,就必须建工单。 + +## 3. 需求到实施 + +1. 先复述目标,阅读相关代码、日志和文档,区分事实与假设。 +2. 给出目标、非目标、方案、影响范围、风险、回退方式和验证方法。 +3. 方案没有得到用户明确确认前,只做只读诊断和方案整理,不实施正式代码。 +4. 方案确认后,先建立单元任务工单,再修改代码。 +5. 开始实施前检查分支和工作区,明确哪些现有改动不属于本任务。 +6. 严格按工单范围实现;新发现的问题先记录,不顺手混入当前任务。 +7. 执行与风险相称的测试,把关键结果和未验证部分更新到工单。 + +Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明确授权,不得默认绕过建单。 + +## 4. 工单层级 + +```text +Epic:完整产品目标和长期路线 +└── MVP:第一个可交付版本 + └── 单元任务:唯一的实施单位 +``` + +- Epic 维护总体目标、范围、路线、风险和所有任务索引。 +- MVP 维护首个可交付范围、阶段、集成风险和任务清单。 +- 单元任务记录具体方案、修改范围、验收标准、过程和测试结果。 +- 父工单只维护 `- [ ] #编号` 或 `- [x] #编号` 的索引和汇总,不复制子工单全文。 +- 新任务先建单元工单,再把编号同步到所属 MVP 和 Epic。 + +工单模板位于 `.gitea/issue_template/`。 + +## 5. 实施中的变化 + +- 范围、接口、数据结构、依赖、验收标准或风险变化时,先更新单元工单。 +- 变化影响 MVP 或 Epic 时,同时更新父工单。 +- 会改变用户已确认结果的变化,更新工单后必须再次等待用户确认。 +- 阻塞、失败方案和新发现根因不能只留在聊天或代码注释中。 +- 工单状态应使用:待确认、待实施、进行中、阻塞、待验收、已完成。 + +## 6. Git 与验证 + +- 提交只包含当前工单相关文件。 +- 实现提交信息引用工单号,例如:`fix: 修复登录超时 (#123)`。 +- 不为流程制造空提交。 +- 优先运行项目档案中记录的格式检查、静态检查、单元测试和必要的集成测试。 +- 不能验证的真机、生产、迁移或并发行为必须写入工单和归档。 + +## 7. 完成、验收和归档 + +1. 实现完成后逐项检查验收标准,并提交代码。 +2. 更新单元工单:最终方案、方案差异、测试结果、提交哈希和遗留问题。 +3. 工单保持“待验收”,用户没有明确验收通过前不得关闭。 +4. 按 `docs/templates/task-archive.md` 创建 `docs/task/<编号>-<短标题>.md`。 +5. 归档文档单独提交,例如:`docs: 归档任务 #123`。 +6. 把归档路径和提交哈希回写工单。 +7. 用户验收通过后关闭单元工单,并同步更新 MVP 和 Epic。 + +MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才能关闭 MVP。Epic 的全部范围完成后才能关闭 Epic。 + +## 8. 可维护性 + +- 优先使用直白、常见的实现;不要为了少写几行引入晦涩技巧。 +- 类和函数保持单一职责,名称表达业务含义。 +- 注释解释原因、边界和风险,不逐行翻译代码。 +- 错误必须可定位,不静默吞掉失败。 +- 文档先写结论和用途,再写步骤;示例命令应可直接复制。 +- 面向初级维护者说明从哪里开始读、怎样运行和怎样验证。 + +## 9. 引导提交例外 + +从本模板创建全新仓库时,Gitea 远端和工单尚不存在,允许一次不带工单号的初始引导提交。该提交只能包含仓库骨架、Harness 规则和远端配置准备,不能包含产品功能。 + +远端建立并推送后,这个例外立即失效。 + +## 10. 项目专用规则 + + + +- 尚未配置。开始产品开发前必须填写项目档案,并删除本行。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f6a846 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# DevHarness + +DevHarness 是一个以 Gitea 工单为事实来源、以 Git 提交为变更记录、由人负责确认和验收的 AI 辅助开发模板。 + +它约束的是开发过程,不限制项目使用 Python、Go、JavaScript 或其他技术栈。 + +## 工作闭环 + +```text +讨论需求或缺陷 + -> 阅读代码并提出方案 + -> 人工确认方案 + -> 创建 Epic / MVP / 单元任务工单 + -> Agent 实现并测试 + -> 提交代码并更新工单 + -> 人工验收 + -> 归档 docs/task + -> 关闭工单并更新父工单 +``` + +## 快速开始 + +1. 复制或克隆本仓库,并修改仓库名称。 +2. 填写 [项目档案](docs/00-project-profile.md),特别是 Gitea 地址、仓库名和验证命令。 +3. 把项目不可违反的安全规则写入根目录或子项目的 `AGENTS.md`。 +4. 创建 Gitea 远端仓库并推送当前引导提交。 +5. 使用 `.gitea/issue_template/` 中的模板创建第一个 Epic、MVP 和单元任务。 +6. 开始产品代码前运行: + + ```powershell + python scripts/check_harness.py --strict + ``` + +新仓库在 Gitea 尚未建立前允许一次不关联工单的引导提交。远端和工单系统配置完成后,所有改变程序行为的工作都必须先有单元任务工单。 + +## 目录 + +```text +AGENTS.md Agent 的通用工作规则 +.gitea/issue_template/ Epic、MVP、单元任务工单模板 +docs/00-project-profile.md 每个项目需要填写的档案 +docs/01-workflow.md 人和 Agent 都能阅读的流程说明 +docs/templates/task-archive.md 完成后的本地归档模板 +docs/task/ 已完成任务的最终记录 +scripts/check_harness.py 模板和归档的最小自检 +scripts/new_task_archive.py 创建任务归档文件 +``` + +## 设计原则 + +- 人决定目标、范围和验收结果,Agent 负责检查、实现和验证。 +- 工单记录实施过程,`docs/task` 只保存完成后的最终事实。 +- 一个单元工单只解决一个可独立测试和回退的问题。 +- 实现提交与归档提交分开,便于审查与追溯。 +- 凭据、个人数据和生产数据不得进入代码、工单或归档。 diff --git a/docs/00-project-profile.md b/docs/00-project-profile.md new file mode 100644 index 0000000..0ec62e3 --- /dev/null +++ b/docs/00-project-profile.md @@ -0,0 +1,50 @@ +# 项目档案 + +复制模板后先填写本页。这里保存不经常变化、所有维护者都需要知道的信息。 + +## 基本信息 + +| 项目 | 内容 | +|---|---| +| 项目名称 | `<填写>` | +| 一句话目标 | `<填写>` | +| Gitea 地址 | `<例如 https://gitea.example.com>` | +| 仓库 | `` | +| 默认分支 | `main` | +| 主要维护者 | `<填写>` | + +## 技术栈 + +| 部分 | 技术 | 规则文件 | +|---|---|---| +| `<子项目或服务>` | `<语言、框架、版本>` | `<路径/AGENTS.md>` | + +## 常用命令 + +所有命令默认从仓库根目录执行。 + +| 用途 | 命令 | 预期结果 | +|---|---|---| +| 安装依赖 | `<填写>` | `<填写>` | +| 启动开发环境 | `<填写>` | `<填写>` | +| 格式检查 | `<填写>` | `<填写>` | +| 静态检查 | `<填写>` | `<填写>` | +| 单元测试 | `<填写>` | `<填写>` | +| 集成测试 | `<填写或写“不适用”>` | `<填写>` | + +## 目录边界 + +| 目录 | 职责 | 不应放入 | +|---|---|---| +| `<路径>` | `<填写>` | `<填写>` | + +## 环境与凭据 + +- 本地配置文件:`<填写>` +- 配置示例文件:`<填写>` +- 凭据保存位置:`<只写保存方式,不填写真实凭据>` +- 日志和构建产物位置:`<填写>` + +## 项目专用验收要求 + +- `<填写>` diff --git a/docs/01-workflow.md b/docs/01-workflow.md new file mode 100644 index 0000000..f34611f --- /dev/null +++ b/docs/01-workflow.md @@ -0,0 +1,72 @@ +# 开发工作流 + +## 一次任务怎样完成 + +### 1. 讨论 + +用户描述需求或故障。Agent 先检查现状,再给出目标、非目标、方案、风险、回退和验证方法。存在不同实现方向时,说明取舍,让用户确认。 + +### 2. 建单 + +方案确认后,使用 `.gitea/issue_template/task.md` 创建单元任务工单。没有工单号之前不修改产品代码。 + +新产品或较大版本先建立 Epic,再建立 MVP: + +```text +[Epic] 产品或长期目标 +└── [MVP] 第一个可交付版本 + ├── #101 单元任务 + ├── #102 单元任务 + └── #103 单元任务 +``` + +每个单元任务都应目标单一,能够独立测试、提交和回退。 + +### 3. 实施 + +Agent 检查工作区,只修改工单范围内的文件。发现新问题时先记录到工单;如果它不影响当前验收,另建工单,不扩大当前任务。 + +重要进度应及时写回工单: + +- 已确认的根因; +- 方案或范围变化; +- 测试结果; +- 阻塞和未验证内容; +- Git 提交哈希。 + +### 4. 待验收 + +实现和测试完成后,Agent 提交代码并将工单更新为待验收。用户验收前工单保持开启。 + +### 5. 归档和关闭 + +使用以下命令创建归档草稿: + +```powershell +python scripts/new_task_archive.py 123 "修复登录超时" +``` + +填写实际结果后单独提交归档,再把路径和提交哈希写回工单。用户明确验收通过后,关闭单元工单并勾选父工单中的任务。 + +## 什么时候重新确认方案 + +以下变化必须先更新工单,再由用户确认: + +- 交付结果或用户操作发生变化; +- 增加或删除接口、数据库字段或迁移; +- 安全边界、权限或不可逆操作发生变化; +- 原方案不可行,需要更换主要技术路线; +- 任务范围明显扩大。 + +普通内部实现细节不需要反复确认,但重要取舍应记录在工单中。 + +## 工单与文档分别写什么 + +| 信息 | Gitea 工单 | `docs/task` | +|---|---:|---:| +| 讨论过程和临时方案 | 是 | 否 | +| 实施进度和阻塞 | 是 | 否 | +| 最终实现方案 | 是 | 是 | +| 测试结果与未验证内容 | 是 | 是 | +| 提交哈希 | 是 | 是 | +| 长期有效的最终结论 | 可链接 | 是 | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..79c3c45 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# 文档索引 + +- [项目档案](00-project-profile.md):仓库、技术栈、命令和负责人等稳定信息。 +- [开发工作流](01-workflow.md):从需求讨论到工单关闭的完整顺序。 +- [任务归档模板](templates/task-archive.md):任务完成后的固定格式。 +- `task/`:已经完成并与代码版本对应的任务记录。 + +临时进度、方案讨论和待办事项写入 Gitea 工单,不写进长期文档。 diff --git a/docs/task/.gitkeep b/docs/task/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/task/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/templates/task-archive.md b/docs/templates/task-archive.md new file mode 100644 index 0000000..66061ac --- /dev/null +++ b/docs/templates/task-archive.md @@ -0,0 +1,40 @@ +# <工单号> <标题> + +- 类型:需求 / 缺陷 / 重构 +- 所属 Epic:# +- 所属 MVP / 版本:# +- 状态:待验收 / 已完成 +- 日期:YYYY-MM-DD +- Gitea 工单:<链接> + +## 背景与目标 + + + +## 最终方案 + + + +## 修改文件 + +- `<文件>`:<改动说明> + +## 验收结果 + +| 验收标准 | 结果 | +|---|---| +| | 通过 / 未通过 | + +## 测试 + +- 执行命令:`<命令>` +- 结果: +- **未验证部分**: + +## 遗留问题 + + + +## 相关提交 + +- `<提交哈希>` <提交说明> diff --git a/scripts/check_harness.py b/scripts/check_harness.py new file mode 100644 index 0000000..0859962 --- /dev/null +++ b/scripts/check_harness.py @@ -0,0 +1,87 @@ +"""检查 DevHarness 必需文件和任务归档的基本结构。""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +REQUIRED_FILES = ( + "AGENTS.md", + "README.md", + "docs/00-project-profile.md", + "docs/01-workflow.md", + "docs/templates/task-archive.md", + ".gitea/issue_template/epic.md", + ".gitea/issue_template/mvp.md", + ".gitea/issue_template/task.md", +) +ARCHIVE_HEADINGS = ( + "## 背景与目标", + "## 最终方案", + "## 修改文件", + "## 验收结果", + "## 测试", + "## 相关提交", +) + + +def check_required_files(errors: list[str]) -> None: + for relative_path in REQUIRED_FILES: + if not (ROOT / relative_path).is_file(): + errors.append(f"缺少必需文件:{relative_path}") + + +def check_project_profile(errors: list[str], warnings: list[str], strict: bool) -> None: + profile = ROOT / "docs" / "00-project-profile.md" + if not profile.is_file(): + return + if "<填写" in profile.read_text(encoding="utf-8"): + message = "项目档案仍有未填写内容" + (errors if strict else warnings).append(message) + + +def check_archives(errors: list[str]) -> None: + task_dir = ROOT / "docs" / "task" + for path in task_dir.glob("*.md"): + if not re.match(r"^\d+-.+\.md$", path.name): + errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}") + content = path.read_text(encoding="utf-8") + for heading in ARCHIVE_HEADINGS: + if heading not in content: + errors.append(f"{path.name} 缺少章节:{heading}") + if "**未验证部分**:" not in content: + errors.append(f"{path.name} 没有记录未验证部分") + + +def main() -> int: + parser = argparse.ArgumentParser(description="检查 DevHarness 项目结构") + parser.add_argument( + "--strict", + action="store_true", + help="项目档案有占位内容时返回失败", + ) + args = parser.parse_args() + + errors: list[str] = [] + warnings: list[str] = [] + check_required_files(errors) + check_project_profile(errors, warnings, args.strict) + check_archives(errors) + + for warning in warnings: + print(f"警告:{warning}") + for error in errors: + print(f"错误:{error}") + + if errors: + print(f"检查失败:{len(errors)} 个问题") + return 1 + print("DevHarness 检查通过") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/new_task_archive.py b/scripts/new_task_archive.py new file mode 100644 index 0000000..538e372 --- /dev/null +++ b/scripts/new_task_archive.py @@ -0,0 +1,56 @@ +"""根据模板创建任务归档草稿。""" + +from __future__ import annotations + +import argparse +import re +from datetime import date +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "docs" / "templates" / "task-archive.md" +TASK_DIR = ROOT / "docs" / "task" + + +def safe_title(title: str) -> str: + """把标题转换为适合文件名的短文本。""" + + cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip()) + cleaned = re.sub(r"\s+", "-", cleaned) + return cleaned.strip(".-") + + +def create_archive(issue_number: str, title: str) -> Path: + """创建归档草稿;目标文件存在时拒绝覆盖。""" + + short_title = safe_title(title) + if not issue_number.isdigit(): + raise ValueError("工单号必须是数字") + if not short_title: + raise ValueError("标题不能为空") + + target = TASK_DIR / f"{issue_number}-{short_title}.md" + if target.exists(): + raise FileExistsError(f"文件已存在:{target}") + + content = TEMPLATE.read_text(encoding="utf-8") + content = content.replace("<工单号>", issue_number, 1) + content = content.replace("<标题>", title.strip(), 1) + content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1) + target.write_text(content, encoding="utf-8") + return target + + +def main() -> None: + parser = argparse.ArgumentParser(description="创建 docs/task 任务归档草稿") + parser.add_argument("issue_number", help="Gitea 工单号,例如 123") + parser.add_argument("title", help="简短任务标题") + args = parser.parse_args() + + target = create_archive(args.issue_number, args.title) + print(f"已创建:{target.relative_to(ROOT)}") + + +if __name__ == "__main__": + main()