diff --git a/.codex/skills/agent-change-log/SKILL.md b/.codex/skills/agent-change-log/SKILL.md index e5e157a..f93980b 100644 --- a/.codex/skills/agent-change-log/SKILL.md +++ b/.codex/skills/agent-change-log/SKILL.md @@ -1,55 +1,63 @@ --- name: agent-change-log -description: Use when working in X-Financial after bug fixes, new features, refactors, behavior changes, documentation edits, config edits, concurrent-agent Git commits, or any codebase modification that should leave an incremental human-readable work log. +description: Use when working in X-Financial after bug fixes that need a split dev log, when maintaining document/development/YYYY-MM-DD/dev-logs/bugs, or when generating the daily 17:00 combined work-logs.med from same-day feature docs and bug logs. --- # Agent Change Log ## Overview -This skill keeps a living, incremental work log for X-Financial. After each meaningful modification batch, write down what changed locally, what other agents already committed upstream, what was operated on, what remains uncertain, and what should happen next. +This skill keeps split development logs for X-Financial. Bug fixes are recorded under the same-day `dev-logs/bugs` folder. Daily summaries combine same-day feature documents and bug records into one `work-logs.med`. The log should sound like a careful teammate writing for tomorrow's teammate: concrete, warm, and honest. ## When To Use -- After fixing a bug, adding a feature, refactoring, changing behavior, editing documentation, or changing config. -- Before the final response for any turn that changed files. +- After fixing a bug. +- When a post-commit hook detects a bug-like commit. +- At the daily 17:00 summary pass. - Before updating the log in a branch where other agents may have pushed commits. - After verification, so the entry can include what was actually checked. - When a failed attempt changed files, generated artifacts, or revealed a risk worth preserving. -Do not use for read-only investigation with no file changes unless the user explicitly asks for a record. +Do not create legacy `document/work-log/YYYY-MM-DD.md` entries for new work. ## Log Location -Use one file per day: +Use the same date root as development documents: ```text -document/work-log/YYYY-MM-DD.md +document/development/YYYY-MM-DD/ +├── feature//CONCEPT.md + TODO.md +├── dev-logs/bugs/.md +└── work-logs.med ``` -If the file does not exist, create it. If it exists, incrementally update it. Never replace the whole file just to add a new entry. +If the date folder exists, reuse it. If not, create it. -## Required Structure +## Bug Log Structure -Each daily file must use these sections in this order: +For bug fixes, create or update one file per bug: -```markdown -# YYYY-MM-DD 工作日志 - -## 当日工作内容 - -## 遗留问题 - -## TODO +```text +document/development/YYYY-MM-DD/dev-logs/bugs/.md ``` -Keep this order stable. Add entries under the existing headings instead of creating duplicate headings. +The file must contain `## 修复记录` and timestamped bullets similar to the old `当日工作内容`. +Do not add `遗留问题` or `TODO` sections to bug logs. + +Use the helper when possible: + +```bash +python3 tools/agent-change-log/update_change_log.py \ + --kind bug \ + --bug-title "" \ + --bug-slug +``` ## Required Git Check -Before writing or updating the daily log, manually check Git for upstream and local-ahead commits from other agents. +Before writing or updating a bug log, manually check Git for upstream and local-ahead commits from other agents. Run: @@ -68,27 +76,59 @@ Rules: - Treat `HEAD..@{u}` as upstream commits not yet in local history. - Treat `@{u}..HEAD` as local commits not yet in upstream history; these may also come from another agent working in the same checkout. - If the worktree is clean and the branch is only behind upstream, `git pull --ff-only` may be used to fast-forward before analysis. -- If the worktree is dirty, diverged, or likely to conflict, do not merge/rebase automatically. Record the upstream commits from `HEAD..@{u}` and the blocker in `遗留问题`. -- If there is no upstream branch, record that fact in `遗留问题` and continue with local-only logging. -- When `HEAD..@{u}` has commits, summarize those commits under `当日工作内容` before describing local edits. Mention commit hash, subject, and inferred impact. +- If the worktree is dirty, diverged, or likely to conflict, do not merge/rebase automatically. Record the upstream commits from `HEAD..@{u}` in the bug fix entry. +- If there is no upstream branch, record that fact in the bug fix entry and continue with local-only logging. +- When `HEAD..@{u}` has commits, summarize those commits in the bug fix entry before describing local edits. Mention commit hash, subject, and inferred impact. - When `@{u}..HEAD` has commits that were not created in the current task, summarize them too, because another local agent may have committed without pushing yet. - When no upstream or local-ahead commits exist, still record "Git 提交检查:未发现 upstream 新提交或本地 ahead 新提交" in the work entry. -## Entry Rules +## Bug Entry Rules 1. Get the current local time first: ```bash date '+%Y-%m-%d %H:%M:%S %Z' ``` 2. Run the required Git check and capture whether upstream or local-ahead has new commits. -3. Append a new timestamped bullet under `## 当日工作内容`. -4. Mention Git commits, changed files or modules, the operation, the intent, and the verification result. -5. Add or update `## 遗留问题` whenever there is risk, uncertainty, skipped verification, design debt, Git divergence, or a likely follow-up. -6. Add or update `## TODO` with checkbox items. -7. When a TODO is completed, keep it in place and mark it as: - ```markdown - - [x] ~~任务内容~~(完成于 HH:MM,证据:...) - ``` +3. Ensure `document/development/YYYY-MM-DD/dev-logs/bugs/` exists. +4. Create or update one `.md` file for the specific bug. +5. Append a new timestamped bullet under `## 修复记录`. +6. Mention Git commits, changed files or modules, the operation, the intent, and the verification result. +7. Do not add leftover issue or TODO sections. + +## Daily Summary + +At 17:00 every day, generate the combined work log: + +```bash +python3 tools/agent-change-log/update_change_log.py --kind summary +``` + +This reads: + +```text +document/development/YYYY-MM-DD/feature/ +document/development/YYYY-MM-DD/dev-logs/bugs/ +``` + +Then writes: + +```text +document/development/YYYY-MM-DD/work-logs.med +``` + +The summary should cover: + +- Today's feature points from `feature/*/CONCEPT.md` and `TODO.md`. +- Today's bug fixes from `dev-logs/bugs/*.md`. +- A concise combined analysis of what changed that day. + +## Entry Rules + +- For each bug entry, append a new timestamped bullet under `## 修复记录`. +- Mention Git commits, changed files or modules, the operation, the intent, and the verification result. +- Do not write `遗留问题`. +- Do not write `TODO`. +- If the change is a feature rather than a bug, use the development document skill to keep `feature//CONCEPT.md` and `TODO.md` current instead of writing a bug log. ## Writing Style @@ -97,12 +137,11 @@ Rules: - Keep the tone factual. Do not turn the log into a victory lap. - Prefer concise file names and module names in prose, but include enough context to find the change. - Work content should be detailed enough that a future agent can continue without asking "你到底改了啥?" -- Leftover issues should include a suggested next step, not only a complaint. -## Work Content Template +## Bug Entry Template ```markdown -- HH:MM:我完成了 <本次修改目标>。 +- HH:MM:记录 bug 修复:。 - Git 提交检查:。 - 修改:<文件/模块>,<做了什么>。 - 操作:<运行了什么命令、迁移了什么状态、重启了什么服务等>。 @@ -110,26 +149,12 @@ Rules: - 影响:<用户可见变化或工程边界变化>。 ``` -## Leftover Issue Template - -```markdown -- HH:MM:<遗留问题或风险>。建议下一步 <具体建议>。 -``` - -## TODO Template - -```markdown -- [ ] <下一步动作>(来源:HH:MM <上下文>) -- [x] ~~<已完成动作>~~(完成于 HH:MM,证据:<命令/文件/结果>) -``` - ## Final Response Checklist Before saying work is complete: -- Today's `document/work-log/YYYY-MM-DD.md` exists. -- The Git check ran, and upstream plus local-ahead commits from other agents were summarized or explicitly marked as absent. -- `当日工作内容` includes this modification batch. -- `遗留问题` is either updated with real risks or explicitly says no new leftover issue for this batch. -- `TODO` contains new follow-ups and completed items are checked with evidence. -- The final response mentions that the work log was updated. +- For bug fixes, today's bug log exists under `document/development/YYYY-MM-DD/dev-logs/bugs/`. +- For non-bug feature work, relevant `feature/` documents are current. +- Git check ran for bug logs, and upstream plus local-ahead commits were summarized or explicitly marked as absent. +- No new legacy `document/work-log/YYYY-MM-DD.md` entry was created. +- The final response mentions whether a bug log, feature document, or daily `work-logs.med` was updated. diff --git a/.codex/skills/agent-change-log/agents/openai.yaml b/.codex/skills/agent-change-log/agents/openai.yaml index 2289e2a..75399c0 100644 --- a/.codex/skills/agent-change-log/agents/openai.yaml +++ b/.codex/skills/agent-change-log/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Agent Change Log" - short_description: "Record X-Financial changes plus upstream/local commits in the daily work log" - default_prompt: "Use $agent-change-log after a bug fix, feature, refactor, upstream/local commit check, or other file modification to update document/work-log/YYYY-MM-DD.md incrementally." + short_description: "Record bug logs and daily work summaries" + default_prompt: "Use $agent-change-log after an X-Financial bug fix or at 17:00 to update document/development//dev-logs/bugs or work-logs.med." diff --git a/.githooks/post-commit b/.githooks/post-commit index 4f1b024..24f8d26 100755 --- a/.githooks/post-commit +++ b/.githooks/post-commit @@ -5,5 +5,6 @@ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0 cd "$repo_root" || exit 0 python3 tools/agent-change-log/update_change_log.py \ + --kind auto \ --event "post-commit hook" \ >/tmp/x-financial-agent-change-log-hook.log 2>&1 || true diff --git a/AGENTS.md b/AGENTS.md index f22a427..9aab5ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,12 +7,12 @@ ## 变更日志 Skill 规范 -- 每次修复 bug、新增功能、重构、修改配置或编辑项目文档后,必须调用项目级 Skill `agent-change-log`,并增量更新 `document/work-log/YYYY-MM-DD.md`。 -- 更新日志前必须先执行 Git 拉取检查:默认运行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;如果工作区干净且只落后上游,可以 `git pull --ff-only`。发现其他智能体已提交到上游或本地 ahead 提交时,要先把这些提交摘要分析进当天日志。 -- 自动化触发由 `tools/agent-change-log/update_change_log.py` 和 `.githooks/post-commit` 提供;新 checkout 需要执行 `tools/agent-change-log/install_post_commit_hook.sh` 安装到本地 `.git/hooks/post-commit` 后,提交后才会自动追加最低限度日志。 -- 如果当前环境无法写 `.git` 或无法执行 `git fetch`,必须把这个限制写进当天日志;不能把 Skill/AGENTS 规则误当成已经有后台自动化。 -- 日志必须保留 `当日工作内容`、`遗留问题`、`TODO` 三块;TODO 使用 Markdown checkbox,完成项勾选并用删除线保留历史。 -- 记录要写清楚具体时间、改了什么、操作了什么、验证了什么、还遗留什么问题,以及建议下一步怎么处理。 +- 每次修复 bug 后,必须调用项目级 Skill `agent-change-log`,并在 `document/development/YYYY-MM-DD/dev-logs/bugs/.md` 记录该 bug 的修复内容。 +- 新增功能、重构、配置或项目文档变更不再写入旧的 `document/work-log/YYYY-MM-DD.md`;功能点默认沉淀到 `document/development/YYYY-MM-DD/feature/<具体功能点>/CONCEPT.md` 和 `TODO.md`。 +- 写 bug 日志前必须先执行 Git 拉取检查:默认运行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`。发现其他智能体已提交到上游或本地 ahead 提交时,要把这些提交摘要写进 bug 修复记录。 +- 自动化触发由 `tools/agent-change-log/update_change_log.py` 和 `.githooks/post-commit` 提供;新 checkout 需要执行 `tools/agent-change-log/install_post_commit_hook.sh` 安装到本地 `.git/hooks/post-commit` 后,提交后才会按 `--kind auto` 自动识别 bug-like commit 并写入 `dev-logs/bugs`。 +- bug 日志只保留 `## 修复记录`,记录具体时间、改了什么、操作了什么、验证了什么和影响;不再写 `遗留问题` 和 `TODO` 两块。 +- 每天 17:00 生成当天综合日志:读取 `document/development/YYYY-MM-DD/feature/` 和 `document/development/YYYY-MM-DD/dev-logs/bugs/`,分析功能点与 bug 修复后写入 `document/development/YYYY-MM-DD/work-logs.med`。 ## 通用代码拆分规范 diff --git a/document/development/2026-06-25/feature/agent-change-log-split/CONCEPT.md b/document/development/2026-06-25/feature/agent-change-log-split/CONCEPT.md new file mode 100644 index 0000000..265b27f --- /dev/null +++ b/document/development/2026-06-25/feature/agent-change-log-split/CONCEPT.md @@ -0,0 +1,116 @@ +# 写日志技能拆分 概念文档 + +更新时间:2026-06-25 + +文档路径:document/development/2026-06-25/feature/agent-change-log-split/CONCEPT.md + +## 功能一句话 + +把原来单文件三段式工作日志拆成按日期聚合的功能点文档、bug 修复日志和每日 17:00 综合工作日志。 + +## 背景与问题 + +- 当前现状:旧 `agent-change-log` 把所有变更追加到 `document/work-log/YYYY-MM-DD.md`,并固定包含 `当日工作内容`、`遗留问题`、`TODO`。 +- 用户痛点:功能点规划、bug 修复和当天综合复盘混在一个日志文件里,后续追溯时难以按功能或问题拆开看。 +- 业务影响:开发资料会越来越长,bug 修复证据和功能点设计边界容易互相干扰。 +- 为什么现在需要做:`write-development-docs` 已经改为 `document/development/YYYY-MM-DD/feature/<功能点>/`,日志能力也需要跟随同一日期根目录拆分。 + +## 目标与非目标 + +### 目标 + +- [G1] bug 修复记录落到 `document/development/YYYY-MM-DD/dev-logs/bugs/.md`。 +- [G2] bug 日志只保留修复记录,不再写 `遗留问题` 和 `TODO` 两块。 +- [G3] 每天 17:00 汇总当天 `feature/` 和 `dev-logs/bugs/`,生成 `work-logs.med`。 +- [G4] 保留 Git 双向检查,继续识别 upstream 新提交和本地 ahead 提交。 + +### 非目标 + +- [NG1] 本轮不迁移历史 `document/work-log/*.md`。 +- [NG2] 本轮不删除旧历史日志,避免破坏既有追溯。 +- [NG3] 本轮不把非 bug 提交强行写入 bug 日志。 + +## 用户与场景 + +- 目标用户:使用 Codex/Agent 维护 X-Financial 的开发者和后续接手的智能体。 +- 使用入口:`agent-change-log` Skill、`tools/agent-change-log/update_change_log.py`、post-commit hook、每日 17:00 Codex automation。 +- 核心场景: + 1. 修复 bug 后写入当天 `dev-logs/bugs/.md`。 + 2. 非 bug 功能点通过 `feature/<功能点>/CONCEPT.md` 和 `TODO.md` 沉淀。 + 3. 每天 17:00 汇总功能点与 bug,生成 `work-logs.med`。 +- 异常场景: + - 没有 feature 或 bug 时,综合日志明确写“未发现”。 + - 提交标题不像 bug 时,post-commit 自动日志跳过。 + +## 功能能力 + +- [C1] 输入能力:支持 `--kind bug`、`--kind auto`、`--kind summary` 三种模式。 +- [C2] 处理能力:按日期创建 `dev-logs/bugs`,按 bug slug 记录修复内容。 +- [C3] 输出能力:输出 bug 修复记录或每日 `work-logs.med`。 +- [C4] 状态与权限:沿用 Git fetch/status/log 检查,不主动 merge/rebase。 +- [C5] 边界与降级:官方 skill 校验脚本不可用时,用脚本单测、frontmatter 和 diff check 兜底。 + +## 方案设计 + +### 前端 + +当前不涉及。 + +### 后端 + +当前不涉及业务后端;只修改仓库级 Skill、脚本和 hook。 + +### 算法与规则 + +- 输入:commit subject、用户传入的 bug title/slug、当天 feature 和 bug 文档。 +- 流程:bug 模式写入 bug 文件;auto 模式识别 bug-like commit;summary 模式扫描 `feature/` 和 `dev-logs/bugs/` 后生成综合日志。 +- 输出:`dev-logs/bugs/*.md` 或 `work-logs.med`。 +- 解释:summary 中保留来源目录和综合分析,便于复盘追溯。 + +### 数据与契约 + +- 核心字段:日期、bug slug、bug title、Git 提交检查、修改、操作、验证、影响。 +- 状态枚举:`auto`、`bug`、`summary`。 +- 兼容策略:保留旧 `document/work-log` 历史,不新增旧格式。 +- 版本/审计:本轮变更通过本地 checkpoint commit 保留。 + +## 算法与公式 + +当前功能不涉及显式数学公式。 + +## 测试方案 + +后端: + +- 当前不涉及后端服务。 + +前端: + +- 当前不涉及前端构建。 + +集成: + +- 运行 `python3 tools/agent-change-log/test_update_change_log.py`,覆盖 bug 日志、非 bug 自动跳过、每日综合日志聚合。 + +手工验证: + +- 运行 `update_change_log.py --kind bug --dry-run` 和 `--kind summary --dry-run` 检查目标路径和输出内容。 + +## 指标与验收 + +- [A1] 功能验收:bug 日志路径为 `document/development/YYYY-MM-DD/dev-logs/bugs/.md`。 +- [A2] 性能指标:脚本单测在 60s 内完成。 +- [A3] 质量指标:不再为新日志写入旧三段式 `document/work-log/YYYY-MM-DD.md`。 +- [A4] 安全/权限指标:脚本不做 merge/rebase,不删除历史日志。 +- [A5] 可观测性:17:00 automation 生成 `work-logs.med` 后报告路径和是否发现 feature/bug。 + +## 风险与开放问题 + +- 风险:`work-logs.med` 是按用户原文保留的扩展名,可能与常见 `.md` 扩展名不一致。 +- 已处理依赖:已创建 Codex automation 执行每日 17:00 summary。 +- 待确认:后续是否需要迁移历史 `document/work-log`。 +- 降级策略:automation 不可用时可手动运行 `python3 tools/agent-change-log/update_change_log.py --kind summary`。 + +## 本轮实现记录 + +- 2026-06-25:重写 `agent-change-log` Skill 和脚本,新增 split log 测试,创建每日 17:00 Codex automation。 diff --git a/document/development/2026-06-25/feature/agent-change-log-split/TODO.md b/document/development/2026-06-25/feature/agent-change-log-split/TODO.md new file mode 100644 index 0000000..02f62ea --- /dev/null +++ b/document/development/2026-06-25/feature/agent-change-log-split/TODO.md @@ -0,0 +1,61 @@ +# 写日志技能拆分 开发 TODO + +更新时间:2026-06-25 + +文档路径:document/development/2026-06-25/feature/agent-change-log-split/TODO.md + +## 使用规则 + +- 每个 TODO 必须对应 `CONCEPT.md` 中的目标、能力、方案或验收点。 +- 只有完成并验证后,才能把 `[ ]` 改成 `[x]`。 +- 勾选时在任务后补充简短证据,例如文件、接口、命令或验证结果。 +- 如果需求发生变化,先更新 `CONCEPT.md`,再调整本 TODO。 + +## 1. 调研与边界 + +- [x] [CONCEPT: 背景与问题] 确认旧日志 Skill、脚本、hook 和 AGENTS 仍指向 `document/work-log/YYYY-MM-DD.md`。 + 证据:`rg -n "document/work-log|当日工作内容|遗留问题|TODO" AGENTS.md .codex/skills/agent-change-log tools/agent-change-log .githooks/post-commit`。 +- [x] [CONCEPT: 目标与非目标] 明确本轮不迁移历史 `document/work-log/*.md`。 + 证据:`CONCEPT.md` 非目标已列明历史不迁移。 + +## 2. 契约与设计 + +- [x] [CONCEPT: 功能能力] 定义 `auto`、`bug`、`summary` 三种脚本模式。 + 证据:`tools/agent-change-log/update_change_log.py` 的 `--kind` 参数。 +- [x] [CONCEPT: 数据与契约] 固定新路径 `document/development/YYYY-MM-DD/dev-logs/bugs` 与 `work-logs.med`。 + 证据:`agent-change-log` Skill、AGENTS 和脚本常量。 + +## 3. 后端实现 + +- [x] [CONCEPT: 后端] 重写日志脚本,支持 bug 记录、auto 跳过非 bug、summary 聚合。 + 证据:`tools/agent-change-log/update_change_log.py`。 +- [x] [CONCEPT: 后端] 更新 post-commit hook,改为 `--kind auto`。 + 证据:`.githooks/post-commit`。 + +## 4. 算法/规则实现 + +- [x] [CONCEPT: 算法与规则] 实现 bug-like commit 识别规则。 + 证据:`looks_like_bug()` 覆盖 `fix`、`bugfix`、`修复`、`失败`、`异常` 等关键词。 +- [x] [CONCEPT: 算法与规则] 实现 feature 和 bug 汇总生成 `work-logs.med`。 + 证据:`build_daily_summary()`。 + +## 5. 前端实现 + +- [x] [CONCEPT: 前端] 当前不涉及前端页面。 + 证据:本轮只修改仓库 Skill、脚本、hook、文档和 automation。 + +## 6. 测试与验证 + +- [x] [CONCEPT: 测试方案] 补充脚本回归测试。 + 证据:`tools/agent-change-log/test_update_change_log.py`。 +- [x] [CONCEPT: 测试方案] 运行脚本单测。 + 证据:`python3 tools/agent-change-log/test_update_change_log.py`,3 tests OK。 +- [x] [CONCEPT: 测试方案] dry-run 验证 bug 路径和 summary 路径。 + 证据:`--kind bug --dry-run` 输出 `document/development/2026-06-25/dev-logs/bugs/draft-preview-disappears.md`;`--kind summary --dry-run` 输出 `document/development/2026-06-25/work-logs.med`。 + +## 7. 文档收尾 + +- [x] [CONCEPT: 指标与验收] 更新 `agent-change-log` Skill、AGENTS 和 README。 + 证据:`.codex/skills/agent-change-log/SKILL.md`、`AGENTS.md`、`tools/agent-change-log/README.md`。 +- [x] [CONCEPT: 指标与验收] 创建每日 17:00 Codex automation。 + 证据:automation id `x-financial-daily-split-work-log`。 diff --git a/tools/agent-change-log/README.md b/tools/agent-change-log/README.md index f307d5f..c514ff9 100644 --- a/tools/agent-change-log/README.md +++ b/tools/agent-change-log/README.md @@ -2,21 +2,51 @@ 这个目录提供 `agent-change-log` 的可执行部分。 -## 手动写入一条日志 +## 记录一次 bug 修复 ```bash -python3 tools/agent-change-log/update_change_log.py --event "manual" +python3 tools/agent-change-log/update_change_log.py \ + --kind bug \ + --bug-title "保存草稿失败后表格消失" \ + --bug-slug draft-preview-disappears ``` -## 安装提交后自动日志 hook +默认写入: + +```text +document/development/YYYY-MM-DD/dev-logs/bugs/.md +``` + +bug 日志只记录修复内容,不再写 `遗留问题` 和 `TODO`。 + +## 生成每日综合日志 + +```bash +python3 tools/agent-change-log/update_change_log.py --kind summary +``` + +默认读取当天: + +```text +document/development/YYYY-MM-DD/feature/ +document/development/YYYY-MM-DD/dev-logs/bugs/ +``` + +然后生成: + +```text +document/development/YYYY-MM-DD/work-logs.med +``` + +## 安装提交后自动 bug 日志 hook ```bash tools/agent-change-log/install_post_commit_hook.sh ``` 安装后,每次 `git commit` 完成,`.githooks/post-commit` 会调用 -`update_change_log.py`,把最近提交、Git 双向提交检查和触发时间追加到 -`document/work-log/YYYY-MM-DD.md`。 +`update_change_log.py --kind auto`。脚本只在提交标题看起来像 bug 修复时写入 +`dev-logs/bugs`,例如 `fix:`、`bugfix:`、包含 `修复`、`缺陷`、`失败`、`异常`。 -注意:hook 只能覆盖“提交后自动记录”。没有提交的普通文件修改,仍需要执行代理在任务完成前按 -`AGENTS.md` 和 `agent-change-log` 主动记录。 +非 bug 提交会跳过,功能点仍通过 `document/development/YYYY-MM-DD/feature/` +下的 `CONCEPT.md` 和 `TODO.md` 沉淀。 diff --git a/tools/agent-change-log/test_update_change_log.py b/tools/agent-change-log/test_update_change_log.py new file mode 100755 index 0000000..c0b8308 --- /dev/null +++ b/tools/agent-change-log/test_update_change_log.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("update_change_log.py").resolve() + + +def run(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + args, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=60, + ) + + +class AgentChangeLogScriptTest(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + run(["git", "init"], self.root).check_returncode() + run(["git", "config", "user.email", "agent@example.test"], self.root).check_returncode() + run(["git", "config", "user.name", "Agent"], self.root).check_returncode() + + def tearDown(self) -> None: + self.tmp.cleanup() + + def commit_file(self, subject: str) -> None: + target = self.root / "file.txt" + target.write_text(subject, encoding="utf-8") + run(["git", "add", "file.txt"], self.root).check_returncode() + run(["git", "commit", "-m", subject], self.root).check_returncode() + + def test_bug_log_written_under_date_dev_logs_bugs(self) -> None: + self.commit_file("fix: keep draft preview after save failure") + + result = run( + [ + sys.executable, + str(SCRIPT), + "--kind", + "bug", + "--date", + "2026-06-25", + "--bug-title", + "保存草稿失败后表格消失", + "--bug-slug", + "draft-preview-disappears", + "--event", + "unit test", + "--no-fetch", + ], + self.root, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + log_path = self.root / "document/development/2026-06-25/dev-logs/bugs/draft-preview-disappears.md" + content = log_path.read_text(encoding="utf-8") + self.assertIn("# 保存草稿失败后表格消失", content) + self.assertIn("## 修复记录", content) + self.assertIn("Git 提交检查", content) + self.assertNotIn("## 遗留问题", content) + self.assertNotIn("## TODO", content) + + def test_auto_skips_non_bug_commit(self) -> None: + self.commit_file("docs: update development guide") + + result = run([sys.executable, str(SCRIPT), "--kind", "auto", "--date", "2026-06-25", "--no-fetch"], self.root) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("skipped_non_bug_commit", result.stdout) + self.assertFalse((self.root / "document/development/2026-06-25/dev-logs/bugs").exists()) + + def test_daily_summary_combines_features_and_bugs(self) -> None: + feature = self.root / "document/development/2026-06-25/feature/receipt-folder-ocr" + feature.mkdir(parents=True) + feature.joinpath("CONCEPT.md").write_text( + "# 票据夹 OCR 概念文档\n\n## 功能一句话\n\n自动识别票据并生成可核对字段。\n", + encoding="utf-8", + ) + feature.joinpath("TODO.md").write_text("- [x] 已完成\n- [ ] 待验证\n", encoding="utf-8") + bug_dir = self.root / "document/development/2026-06-25/dev-logs/bugs" + bug_dir.mkdir(parents=True) + bug_dir.joinpath("draft-preview-disappears.md").write_text( + "# 保存草稿失败后表格消失\n\n## 修复记录\n\n- 09:30:记录 bug 修复。\n", + encoding="utf-8", + ) + self.commit_file("fix: create test commit") + + result = run([sys.executable, str(SCRIPT), "--kind", "summary", "--date", "2026-06-25"], self.root) + + self.assertEqual(result.returncode, 0, result.stderr) + summary = self.root.joinpath("document/development/2026-06-25/work-logs.med").read_text(encoding="utf-8") + self.assertIn("# 2026-06-25 综合工作日志", summary) + self.assertIn("票据夹 OCR 概念文档", summary) + self.assertIn("保存草稿失败后表格消失", summary) + self.assertIn("功能侧沉淀了 1 个功能点,问题侧记录了 1 个 bug 修复", summary) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/agent-change-log/update_change_log.py b/tools/agent-change-log/update_change_log.py index 5f4a072..363b2d2 100755 --- a/tools/agent-change-log/update_change_log.py +++ b/tools/agent-change-log/update_change_log.py @@ -1,20 +1,19 @@ #!/usr/bin/env python3 -"""Append an incremental X-Financial agent work-log entry.""" +"""Write split X-Financial development logs.""" from __future__ import annotations import argparse import os +import re import subprocess -import sys from dataclasses import dataclass from datetime import datetime from pathlib import Path -SECTION_WORK = "## 当日工作内容" -SECTION_ISSUES = "## 遗留问题" -SECTION_TODO = "## TODO" +BUG_SECTION = "## 修复记录" +WORK_LOG_NAME = "work-logs.med" @dataclass @@ -63,7 +62,7 @@ def one_line(value: str) -> str: return " ".join(value.strip().split()) -def indent_block(text: str, limit: int = 8) -> list[str]: +def compact_lines(text: str, limit: int = 8) -> list[str]: lines = [line.strip() for line in text.splitlines() if line.strip()] if not lines: return ["未发现"] @@ -73,35 +72,33 @@ def indent_block(text: str, limit: int = 8) -> list[str]: return shown -def ensure_document(path: Path, date_text: str) -> str: - if path.exists(): - content = path.read_text(encoding="utf-8") - else: - content = f"# {date_text} 工作日志\n\n{SECTION_WORK}\n\n{SECTION_ISSUES}\n\n{SECTION_TODO}\n" - - if not content.endswith("\n"): - content += "\n" - - for heading in (SECTION_WORK, SECTION_ISSUES, SECTION_TODO): - if heading not in content: - content += f"\n{heading}\n" - return content +def slugify(value: str, fallback: str) -> str: + normalized = value.strip().lower() + chars: list[str] = [] + previous_dash = False + for char in normalized: + keep = char.isascii() and char.isalnum() + keep = keep or "\u4e00" <= char <= "\u9fff" + if keep: + chars.append(char) + previous_dash = False + elif not previous_dash: + chars.append("-") + previous_dash = True + slug = "".join(chars).strip("-") + return slug or fallback -def insert_under_section(content: str, section: str, entry: str) -> str: - marker = f"{section}\n" - start = content.find(marker) - if start == -1: - return content.rstrip() + f"\n\n{section}\n\n{entry.rstrip()}\n" +def date_dir(root: Path, date_text: str) -> Path: + return root / "document" / "development" / date_text - insert_at = start + len(marker) - next_section = content.find("\n## ", insert_at) - entry_text = "\n" + entry.rstrip() + "\n" - if next_section == -1: - return content[:insert_at].rstrip() + entry_text - before = content[:next_section].rstrip() - after = content[next_section:] - return before + entry_text + after + +def bugs_dir(root: Path, date_text: str) -> Path: + return date_dir(root, date_text) / "dev-logs" / "bugs" + + +def feature_dir(root: Path, date_text: str) -> Path: + return date_dir(root, date_text) / "feature" def collect_git(root: Path, *, no_fetch: bool, max_commits: int) -> dict[str, str | bool]: @@ -133,56 +130,232 @@ def collect_git(root: Path, *, no_fetch: bool, max_commits: int) -> dict[str, st head_subject = git(["show", "-s", "--format=%s", "HEAD"], root) data["head_hash"] = head_hash.stdout.strip() if head_hash.ok else "" data["head_subject"] = head_subject.stdout.strip() if head_subject.ok else "" - - stat = git(["show", "--stat", "--oneline", "--decorate", "--max-count=1", "HEAD"], root) - data["head_stat"] = stat.stdout.strip() if stat.ok else "" data["fetch_failed"] = not fetch_result.ok return data -def build_work_entry(now: datetime, event: str, git_data: dict[str, str | bool]) -> str: +def git_check_text(git_data: dict[str, str | bool]) -> str: + behind_text = ";".join(compact_lines(str(git_data.get("behind") or ""))) + ahead_text = ";".join(compact_lines(str(git_data.get("ahead") or ""))) + upstream = git_data.get("upstream") or "未配置" + return ( + f"fetch {git_data.get('fetch')};upstream `{upstream}`;" + f"upstream 新提交:{behind_text};本地 ahead 提交:{ahead_text}" + ) + + +def looks_like_bug(subject: str) -> bool: + lower = subject.strip().lower() + if lower.startswith(("fix", "bugfix", "hotfix", "revert")): + return True + return any(keyword in lower for keyword in ("bug", "修复", "缺陷", "故障", "报错", "失败", "异常")) + + +def insert_under_section(content: str, section: str, entry: str) -> str: + marker = f"{section}\n" + start = content.find(marker) + if start == -1: + return content.rstrip() + f"\n\n{section}\n\n{entry.rstrip()}\n" + + insert_at = start + len(marker) + next_section = content.find("\n## ", insert_at) + entry_text = "\n" + entry.rstrip() + "\n" + if next_section == -1: + return content[:insert_at].rstrip() + entry_text + before = content[:next_section].rstrip() + after = content[next_section:] + return before + entry_text + after + + +def ensure_bug_document(path: Path, *, title: str, date_text: str) -> str: + if path.exists(): + content = path.read_text(encoding="utf-8") + else: + rel_path = path.as_posix().split("document/development/", 1)[-1] + content = "\n".join( + [ + f"# {title}", + "", + f"日期:{date_text}", + f"文档路径:document/development/{rel_path}", + "", + BUG_SECTION, + "", + ] + ) + + if not content.endswith("\n"): + content += "\n" + if BUG_SECTION not in content: + content += f"\n{BUG_SECTION}\n" + return content + + +def build_bug_entry(now: datetime, event: str, title: str, git_data: dict[str, str | bool]) -> str: time_text = now.strftime("%H:%M") head_hash = str(git_data.get("head_hash") or "") head_subject = str(git_data.get("head_subject") or "") - behind = str(git_data.get("behind") or "") - ahead = str(git_data.get("ahead") or "") - - behind_lines = indent_block(behind) - ahead_lines = indent_block(ahead) - behind_text = ";".join(behind_lines) - ahead_text = ";".join(ahead_lines) - - marker = f"auto-log:{head_hash}" if head_hash else "auto-log:no-head" + marker = f"bug-log:{head_hash}" if head_hash else f"bug-log:{time_text}" return "\n".join( [ - f"- {time_text}:自动记录 `{head_hash}` 提交后的工作日志。({marker})", - f" - Git 提交检查:fetch {git_data.get('fetch')};upstream `{git_data.get('upstream') or '未配置'}`;upstream 新提交:{behind_text};本地 ahead 提交:{ahead_text}。", + f"- {time_text}:记录 bug 修复:{title}。({marker})", + f" - Git 提交检查:{git_check_text(git_data)}。", f" - 修改:最近提交为 `{head_hash} {head_subject}`。", - f" - 操作:{event} 触发 `tools/agent-change-log/update_change_log.py`,自动读取 Git 状态并写入当天日志。", - " - 验证:自动脚本只记录提交和 Git 状态,不替代业务测试;业务验证仍以本次任务实际运行结果为准。", - " - 影响:提交后即使执行代理忘记手动写日志,也会留下最低限度的时间、提交和分支状态记录。", + f" - 操作:{event} 触发 `tools/agent-change-log/update_change_log.py --kind bug`,写入当天 `dev-logs/bugs`。", + " - 验证:记录修复时应引用本次任务实际运行的测试、构建或手工验证结果;自动脚本不替代业务验证。", + " - 影响:该 bug 会在 17:00 综合日志中与当天功能点一起汇总。", ] ) -def build_issue_entry(now: datetime, git_data: dict[str, str | bool]) -> str | None: - time_text = now.strftime("%H:%M") - issues: list[str] = [] - if git_data.get("fetch_failed"): - issues.append(f"fetch 未成功:{git_data.get('fetch')}") - if not git_data.get("upstream"): - issues.append("当前分支没有 upstream,无法判断其他智能体是否已推送新提交") - if not issues: - return None - return f"- {time_text}:自动日志触发时发现 {';'.join(issues)}。建议后续在有 Git 写权限和网络权限的环境里重新执行拉取检查。" +def write_bug_log(args: argparse.Namespace, root: Path, now: datetime, git_data: dict[str, str | bool]) -> int: + date_text = args.date or now.strftime("%Y-%m-%d") + head_hash = str(git_data.get("head_hash") or "") + head_subject = str(git_data.get("head_subject") or "") + title = args.bug_title or head_subject or "未命名 bug 修复" + slug = slugify(args.bug_slug or title, fallback=f"bug-{head_hash or now.strftime('%H%M%S')}") + path = Path(args.log_path) if args.log_path else bugs_dir(root, date_text) / f"{slug}.md" + entry = build_bug_entry(now, args.event, title, git_data) + marker = f"bug-log:{head_hash}" if head_hash else "" + + if args.dry_run: + print(f"target_log={os.path.relpath(path, root)}") + print(entry) + return 0 + + content = ensure_bug_document(path, title=title, date_text=date_text) + if marker and marker in content and not args.force: + print(f"Bug log already contains {marker}; skipping.") + return 0 + + content = insert_under_section(content, BUG_SECTION, entry) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + print(f"updated_bug_log={os.path.relpath(path, root)}") + return 0 + + +def first_heading(path: Path) -> str: + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith("# "): + return line[2:].strip() + return path.parent.name + + +def section_first_text(content: str, section: str) -> str: + marker = f"## {section}" + start = content.find(marker) + if start == -1: + return "" + tail = content[start + len(marker) :].split("\n## ", 1)[0] + for line in tail.splitlines(): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + return stripped.strip("- ").strip() + return "" + + +def summarize_feature(path: Path) -> str: + concept = path / "CONCEPT.md" + todo = path / "TODO.md" + title = path.name + sentence = "" + if concept.exists(): + concept_text = concept.read_text(encoding="utf-8") + title = first_heading(concept) + sentence = section_first_text(concept_text, "功能一句话") + + todo_text = todo.read_text(encoding="utf-8") if todo.exists() else "" + done_count = len(re.findall(r"^- \[x\]", todo_text, flags=re.MULTILINE | re.IGNORECASE)) + open_count = len(re.findall(r"^- \[ \]", todo_text, flags=re.MULTILINE)) + status = f"TODO 完成 {done_count} 项,未完成 {open_count} 项" if todo.exists() else "未找到 TODO.md" + detail = sentence or "未提取到功能一句话" + return f"- {title}:{detail}({status};目录:`{path.name}`)" + + +def summarize_bug(path: Path) -> str: + title = first_heading(path) + content = path.read_text(encoding="utf-8") + latest = "" + for line in content.splitlines(): + stripped = line.strip() + if stripped.startswith("- ") and ":" in stripped: + latest = stripped[2:] + return f"- {title}:{latest or '已记录修复内容'}(文件:`{path.name}`)" + + +def build_daily_summary(root: Path, date_text: str, now: datetime) -> str: + features = [] + feature_root = feature_dir(root, date_text) + if feature_root.exists(): + for path in sorted(feature_root.iterdir()): + if path.is_dir(): + features.append(summarize_feature(path)) + + bugs = [] + bug_root = bugs_dir(root, date_text) + if bug_root.exists(): + for path in sorted(bug_root.glob("*.md")): + bugs.append(summarize_bug(path)) + + feature_text = "\n".join(features) if features else "- 今日未发现功能点文档。" + bug_text = "\n".join(bugs) if bugs else "- 今日未发现 bug 修复记录。" + analysis_parts = [] + if features: + analysis_parts.append(f"功能侧沉淀了 {len(features)} 个功能点") + if bugs: + analysis_parts.append(f"问题侧记录了 {len(bugs)} 个 bug 修复") + analysis = ",".join(analysis_parts) if analysis_parts else "今日目录下暂无功能点或 bug 记录" + + return "\n".join( + [ + f"# {date_text} 综合工作日志", + "", + f"生成时间:{now.strftime('%Y-%m-%d %H:%M:%S %Z')}", + "来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录", + "", + "## 今日功能点", + "", + feature_text, + "", + "## 今日 Bugs", + "", + bug_text, + "", + "## 综合分析", + "", + f"- {analysis}。", + "- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。", + "", + ] + ) + + +def write_daily_summary(args: argparse.Namespace, root: Path, now: datetime) -> int: + date_text = args.date or now.strftime("%Y-%m-%d") + path = Path(args.log_path) if args.log_path else date_dir(root, date_text) / WORK_LOG_NAME + content = build_daily_summary(root, date_text, now) + + if args.dry_run: + print(f"target_log={os.path.relpath(path, root)}") + print(content) + return 0 + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + print(f"updated_work_log={os.path.relpath(path, root)}") + return 0 def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=("auto", "bug", "summary"), default="auto") parser.add_argument("--event", default="manual", help="Human-readable trigger source.") parser.add_argument("--date", help="Override log date YYYY-MM-DD.") - parser.add_argument("--log-path", help="Override log file path.") - parser.add_argument("--dry-run", action="store_true", help="Print the entry without writing files.") + parser.add_argument("--log-path", help="Override output log path.") + parser.add_argument("--bug-title", help="Human-readable bug title.") + parser.add_argument("--bug-slug", help="Stable bug log file slug.") + parser.add_argument("--dry-run", action="store_true", help="Print output without writing files.") parser.add_argument("--force", action="store_true", help="Write even if this commit marker already exists.") parser.add_argument("--no-fetch", action="store_true", help="Skip git fetch; useful for tests or offline hooks.") parser.add_argument("--max-commits", type=int, default=20, help="Maximum commits to summarize per direction.") @@ -193,34 +366,18 @@ def main() -> int: args = parse_args() root = repo_root() now = datetime.now().astimezone() - date_text = args.date or now.strftime("%Y-%m-%d") - log_path = Path(args.log_path) if args.log_path else root / "document" / "work-log" / f"{date_text}.md" + + if args.kind == "summary": + return write_daily_summary(args, root, now) git_data = collect_git(root, no_fetch=args.no_fetch, max_commits=args.max_commits) - entry = build_work_entry(now, args.event, git_data) - issue_entry = build_issue_entry(now, git_data) - marker = f"auto-log:{git_data.get('head_hash')}" + subject = str(git_data.get("head_subject") or "") - if args.dry_run: - print(entry) - if issue_entry: - print() - print(issue_entry) + if args.kind == "auto" and not looks_like_bug(subject): + print(f"skipped_non_bug_commit={subject}") return 0 - content = ensure_document(log_path, date_text) - if marker in content and not args.force: - print(f"Work log already contains {marker}; skipping.") - return 0 - - content = insert_under_section(content, SECTION_WORK, entry) - if issue_entry: - content = insert_under_section(content, SECTION_ISSUES, issue_entry) - - log_path.parent.mkdir(parents=True, exist_ok=True) - log_path.write_text(content, encoding="utf-8") - print(f"updated_log={os.path.relpath(log_path, root)}") - return 0 + return write_bug_log(args, root, now, git_data) if __name__ == "__main__":