chore(logs): split agent change log outputs

This commit is contained in:
caoxiaozhu
2026-06-25 09:35:18 +08:00
parent 4d8a606cd6
commit 6b0756a55f
9 changed files with 653 additions and 154 deletions

View File

@@ -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/<feature-point>/CONCEPT.md + TODO.md
├── dev-logs/bugs/<bug-slug>.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/<bug-slug>.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 名称>" \
--bug-slug <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,28 +76,60 @@ 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 `<bug-slug>.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/<feature-point>/CONCEPT.md` and `TODO.md` current instead of writing a bug log.
## Writing Style
- Write in Simplified Chinese.
@@ -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 修复:<bug 名称>。
- Git 提交检查:<git fetch 后 HEAD..upstream 与 upstream..HEAD 的结果;没有就写未发现 upstream 或本地 ahead 新提交>。
- 修改:<文件/模块><做了什么>。
- 操作:<运行了什么命令、迁移了什么状态、重启了什么服务等>。
@@ -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/<feature-point>` 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.

View File

@@ -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/<date>/dev-logs/bugs or work-logs.med."

View File

@@ -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

View File

@@ -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/<bug-slug>.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`
## 通用代码拆分规范

View File

@@ -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/<bug-slug>.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/<bug-slug>.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 commitsummary 模式扫描 `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/<bug-slug>.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。

View File

@@ -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`

View File

@@ -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/<bug-slug>.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` 沉淀

View File

@@ -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()

View File

@@ -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__":