From d4ff79f32619fa8eb5f91f592a4c3cff2b46357c Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Wed, 24 Jun 2026 10:41:56 +0800 Subject: [PATCH] =?UTF-8?q?chore(agent):=20=E6=96=B0=E5=A2=9E=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E6=97=A5=E5=BF=97/checkpoint=20skill=20=E4=B8=8E=20po?= =?UTF-8?q?st-commit=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGENTS.md 新增变更日志 Skill 规范:变更后增量更新 document/work-log,更新前先做 Git 拉取检查 - 新增 .codex/skills 下 agent-change-log、git-checkpoint-commit 两个 skill - 新增 .githooks/post-commit 与 tools/agent-change-log 自动化脚本,提供提交后最低限度日志追加 --- .codex/skills/agent-change-log/SKILL.md | 135 +++++++++++ .../agent-change-log/agents/openai.yaml | 4 + .githooks/post-commit | 9 + AGENTS.md | 9 + .../domain/write-development-docs/SKILL.md | 109 +++++++++ .../write-development-docs/agents/openai.yaml | 4 + .../write-development-docs/assets/CONCEPT.md | 132 ++++++++++ .../write-development-docs/assets/TODO.md | 71 ++++++ tools/agent-change-log/README.md | 22 ++ .../install_post_commit_hook.sh | 13 + tools/agent-change-log/update_change_log.py | 227 ++++++++++++++++++ 11 files changed, 735 insertions(+) create mode 100644 .codex/skills/agent-change-log/SKILL.md create mode 100644 .codex/skills/agent-change-log/agents/openai.yaml create mode 100755 .githooks/post-commit create mode 100644 hermes/skills/domain/write-development-docs/SKILL.md create mode 100644 hermes/skills/domain/write-development-docs/agents/openai.yaml create mode 100644 hermes/skills/domain/write-development-docs/assets/CONCEPT.md create mode 100644 hermes/skills/domain/write-development-docs/assets/TODO.md create mode 100644 tools/agent-change-log/README.md create mode 100755 tools/agent-change-log/install_post_commit_hook.sh create mode 100755 tools/agent-change-log/update_change_log.py diff --git a/.codex/skills/agent-change-log/SKILL.md b/.codex/skills/agent-change-log/SKILL.md new file mode 100644 index 0000000..e5e157a --- /dev/null +++ b/.codex/skills/agent-change-log/SKILL.md @@ -0,0 +1,135 @@ +--- +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. +--- + +# 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. + +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. +- 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. + +## Log Location + +Use one file per day: + +```text +document/work-log/YYYY-MM-DD.md +``` + +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. + +## Required Structure + +Each daily file must use these sections in this order: + +```markdown +# YYYY-MM-DD 工作日志 + +## 当日工作内容 + +## 遗留问题 + +## TODO +``` + +Keep this order stable. Add entries under the existing headings instead of creating duplicate headings. + +## Required Git Check + +Before writing or updating the daily log, manually check Git for upstream and local-ahead commits from other agents. + +Run: + +```bash +date '+%Y-%m-%d %H:%M:%S %Z' +git fetch --all --prune +git status -sb +git rev-parse --abbrev-ref --symbolic-full-name @{u} +git log --oneline --decorate --max-count=20 HEAD..@{u} +git log --oneline --decorate --max-count=20 @{u}..HEAD +``` + +Rules: + +- Treat `git fetch --all --prune` as the default safe "pull check"; it updates remote refs without merging into a dirty worktree. +- 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. +- 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 + +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,证据:...) + ``` + +## Writing Style + +- Write in Simplified Chinese. +- Be specific and a little human: "我把...", "这次先...", "还需要留意..." are good. +- 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 + +```markdown +- HH:MM:我完成了 <本次修改目标>。 + - Git 提交检查:。 + - 修改:<文件/模块>,<做了什么>。 + - 操作:<运行了什么命令、迁移了什么状态、重启了什么服务等>。 + - 验证:<测试/构建/检查结果;如果没跑,说明原因>。 + - 影响:<用户可见变化或工程边界变化>。 +``` + +## 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. diff --git a/.codex/skills/agent-change-log/agents/openai.yaml b/.codex/skills/agent-change-log/agents/openai.yaml new file mode 100644 index 0000000..2289e2a --- /dev/null +++ b/.codex/skills/agent-change-log/agents/openai.yaml @@ -0,0 +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." diff --git a/.githooks/post-commit b/.githooks/post-commit new file mode 100755 index 0000000..4f1b024 --- /dev/null +++ b/.githooks/post-commit @@ -0,0 +1,9 @@ +#!/bin/sh +# Auto-append a minimal X-Financial agent work-log entry after each commit. + +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 \ + --event "post-commit hook" \ + >/tmp/x-financial-agent-change-log-hook.log 2>&1 || true diff --git a/AGENTS.md b/AGENTS.md index f7f34d6..4c11874 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,15 @@ - 所有分析、解释、计划、提交说明和最终回复默认使用简体中文。 - 技术结论要直击重点,必要时给出可验证的文件、命令或测试结果。 +## 变更日志 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,完成项勾选并用删除线保留历史。 +- 记录要写清楚具体时间、改了什么、操作了什么、验证了什么、还遗留什么问题,以及建议下一步怎么处理。 + ## 通用代码拆分规范 无论写前端、后端还是算法代码,都必须主动避免“所有方法堆在一个类里 / 一个组件里 / 一个模块里”的写法。遇到类、组件或核心模块持续变大时,优先按职责拆分,而不是继续追加方法和状态。 diff --git a/hermes/skills/domain/write-development-docs/SKILL.md b/hermes/skills/domain/write-development-docs/SKILL.md new file mode 100644 index 0000000..bfdbd0d --- /dev/null +++ b/hermes/skills/domain/write-development-docs/SKILL.md @@ -0,0 +1,109 @@ +--- +name: write-development-docs +description: 为 X-Financial 落地开发文档。Use when a task asks to write, create, update, or standardize planning/development documentation under document/development, especially when the expected output is a CONCEPT.md capability document plus a TODO.md implementation checklist following this repository's existing development-doc format. +--- + +# Write Development Docs + +## 目标 + +使用本技能为一个功能、重构、算法、页面或业务能力创建标准开发文档。 +默认输出位置: + +```text +document/development// +├── CONCEPT.md +└── TODO.md +``` + +## 工作流 + +1. 先阅读相邻或同类文档,优先参考 `document/development/*/CONCEPT.md` 和 `document/development/*/TODO.md`。 +2. 读取与本次能力相关的代码、接口、页面、测试或历史文档,不凭空写实现细节。 +3. 创建或更新 `CONCEPT.md`,先写清能力边界,再写方案和验收。 +4. 创建或更新 `TODO.md`,每个任务必须能回链到 `CONCEPT.md` 的章节。 +5. 如果已有实现,TODO 可以勾选 `[x]`,但必须写证据;没有验证的任务保持 `[ ]`。 +6. 完成后检查两份文档是否互相一致,避免 TODO 承诺了 CONCEPT 没定义的能力。 + +## 命名规则 + +- 目录名使用小写 kebab-case,例如 `receipt-folder`、`agent-trace-center`。 +- 能力名在正文中使用清晰中文,例如“票据夹功能”“Agent 链路追踪中心”。 +- 两个文件名固定为 `CONCEPT.md` 和 `TODO.md`。 +- 不额外创建 README、CHANGELOG、SUMMARY 等文件,除非用户明确要求。 + +## CONCEPT.md 格式 + +参考 `assets/CONCEPT.md` 模板。 + +必须包含: + +- 标题:`# <功能名> 概念文档` +- `更新时间:YYYY-MM-DD` +- `## 功能一句话` +- `## 背景与问题` +- `## 目标与非目标` +- `## 用户与场景` +- `## 功能能力` +- `## 方案设计` +- `## 算法与公式` +- `## 测试方案` +- `## 指标与验收` +- `## 风险与开放问题` + +如任务已经实现或正在收口,可追加: + +- `## 本轮实现记录` + +写法要求: + +- 先讲业务问题和边界,再讲技术方案。 +- 目标与非目标必须分开写,避免需求无限扩张。 +- 方案设计按前端、后端、算法/规则、数据、权限、降级策略分块;没有的块可以写“当前不涉及”。 +- 算法与公式必须明确“不涉及”或写出公式、变量说明和适用边界。 +- 验收标准要可验证,不写空泛口号。 + +## TODO.md 格式 + +参考 `assets/TODO.md` 模板。 + +必须包含: + +- 标题:`# <功能名> 开发 TODO` +- `更新时间:YYYY-MM-DD` +- `## 使用规则` +- 分阶段 checklist。 + +TODO 条目规则: + +- 每条用 `- [ ]` 或 `- [x]`。 +- 每条必须包含 `[CONCEPT: <章节名>]`。 +- 已完成项必须补证据,格式为 `证据:<文件、接口、命令或验证结果>`。 +- 没有真实证据时不得勾选 `[x]`。 +- 阶段建议使用: + - `## 1. 调研与边界` + - `## 2. 契约与设计` + - `## 3. 后端实现` + - `## 4. 算法/规则实现` + - `## 5. 前端实现` + - `## 6. 测试与验证` + - `## 7. 文档收尾` + +## 更新既有文档 + +更新已有 `CONCEPT.md` / `TODO.md` 时: + +- 先读两份文件的全文。 +- 新需求先补 `CONCEPT.md`,再补 `TODO.md`。 +- 如果实现发生变化,同步更新“非目标”“风险与开放问题”“本轮实现记录”。 +- 不删除历史证据;除非证据已经明显错误,才替换为新证据。 + +## 验收检查 + +交付前检查: + +- `CONCEPT.md` 和 `TODO.md` 都存在。 +- TODO 的 `[CONCEPT: ...]` 都能在 `CONCEPT.md` 找到对应章节或语义段落。 +- 已勾选项都有证据。 +- 文档没有遗留模板占位符,例如 `<功能名>`、`TODO`、`待补充`。 +- 文档路径位于 `document/development//`。 diff --git a/hermes/skills/domain/write-development-docs/agents/openai.yaml b/hermes/skills/domain/write-development-docs/agents/openai.yaml new file mode 100644 index 0000000..8e6ae5f --- /dev/null +++ b/hermes/skills/domain/write-development-docs/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "开发文档落地" + short_description: "按项目格式生成 CONCEPT 和 TODO 文档" + default_prompt: "Use $write-development-docs to create a development CONCEPT and TODO document for a new X-Financial feature." diff --git a/hermes/skills/domain/write-development-docs/assets/CONCEPT.md b/hermes/skills/domain/write-development-docs/assets/CONCEPT.md new file mode 100644 index 0000000..baea3f4 --- /dev/null +++ b/hermes/skills/domain/write-development-docs/assets/CONCEPT.md @@ -0,0 +1,132 @@ +# <功能名> 概念文档 + +更新时间: + +## 功能一句话 + +用一句话说明这个能力解决什么问题、服务谁、交付什么结果。 + +## 背景与问题 + +- 当前现状: +- 用户痛点: +- 业务影响: +- 为什么现在需要做: + +## 目标与非目标 + +### 目标 + +- [G1] +- [G2] +- [G3] + +### 非目标 + +- [NG1] 本轮不做: +- [NG2] 本轮不改变: +- [NG3] 后续再评估: + +## 用户与场景 + +- 目标用户: +- 使用入口: +- 核心场景: + 1. + 2. + 3. +- 异常场景: + - + +## 功能能力 + +- [C1] 输入能力: +- [C2] 处理能力: +- [C3] 输出能力: +- [C4] 状态与权限: +- [C5] 边界与降级: + +## 方案设计 + +### 前端 + +- 页面/组件: +- 交互状态: +- 展示规则: +- 降级处理: + +### 后端 + +- 接口/服务: +- 权限与校验: +- 持久化: +- 降级处理: + +### 算法与规则 + +- 输入: +- 流程: +- 输出: +- 解释: + +### 数据与契约 + +- 核心字段: +- 状态枚举: +- 兼容策略: +- 版本/审计: + +## 算法与公式 + +如果当前功能不涉及公式,写明: + +当前功能不涉及显式数学公式。 + +如果涉及公式,使用如下格式: + +$$ +metric = input_a + input_b +$$ + +变量说明: + +- $metric$: +- $input_a$: +- $input_b$: + +## 测试方案 + +后端: + +- + +前端: + +- + +集成: + +- + +手工验证: + +- + +## 指标与验收 + +- [A1] 功能验收: +- [A2] 性能指标: +- [A3] 质量指标: +- [A4] 安全/权限指标: +- [A5] 可观测性: + +## 风险与开放问题 + +- 风险: +- 已处理依赖: +- 待确认: +- 降级策略: + +## 本轮实现记录 + +- diff --git a/hermes/skills/domain/write-development-docs/assets/TODO.md b/hermes/skills/domain/write-development-docs/assets/TODO.md new file mode 100644 index 0000000..dc6b2ac --- /dev/null +++ b/hermes/skills/domain/write-development-docs/assets/TODO.md @@ -0,0 +1,71 @@ +# <功能名> 开发 TODO + +更新时间: + +## 使用规则 + +- 每个 TODO 必须对应 `CONCEPT.md` 中的目标、能力、方案或验收点。 +- 只有完成并验证后,才能把 `[ ]` 改成 `[x]`。 +- 勾选时在任务后补充简短证据,例如文件、接口、命令或验证结果。 +- 如果需求发生变化,先更新 `CONCEPT.md`,再调整本 TODO。 + +## 1. 调研与边界 + +- [ ] [CONCEPT: 背景与问题] 阅读相关页面、接口、服务、测试和历史文档,记录当前实现事实。 + 证据: +- [ ] [CONCEPT: 目标与非目标] 确认本轮开发范围,写清楚不做项。 + 证据: +- [ ] [CONCEPT: 风险与开放问题] 标记无法立即确认的依赖、风险和假设。 + 证据: + +## 2. 契约与设计 + +- [ ] [CONCEPT: 功能能力] 定义输入、输出、状态、权限和边界条件。 + 证据: +- [ ] [CONCEPT: 方案设计] 明确前端、后端、算法、数据的职责边界。 + 证据: +- [ ] [CONCEPT: 算法与公式] 补全公式、变量解释或明确当前不涉及公式。 + 证据: +- [ ] [CONCEPT: 指标与验收] 把验收标准转成可验证的检查点。 + 证据: + +## 3. 后端实现 + +- [ ] [CONCEPT: 后端] 新增或调整 schema、service、endpoint、权限和持久化逻辑。 + 证据: +- [ ] [CONCEPT: 数据与契约] 保持响应结构、状态枚举和兼容策略清晰。 + 证据: + +## 4. 算法/规则实现 + +- [ ] [CONCEPT: 算法与规则] 实现核心处理流程、规则判断或计算逻辑。 + 证据: +- [ ] [CONCEPT: 结果解释] 输出可读解释、证据链、贡献项或降级原因。 + 证据: + +## 5. 前端实现 + +- [ ] [CONCEPT: 前端] 新增或调整页面、组件、服务 API 和视图模型。 + 证据: +- [ ] [CONCEPT: 前端] 实现加载、空态、错误态、权限态和刷新。 + 证据: +- [ ] [CONCEPT: 前端] 对齐现有企业级直角、低饱和、密集信息风格。 + 证据: + +## 6. 测试与验证 + +- [ ] [CONCEPT: 测试方案] 补充后端 service/API 定向测试,容器内运行,超时控制在 60s 内。 + 证据: +- [ ] [CONCEPT: 测试方案] 补充前端视图模型、路由、组件或构建验证。 + 证据: +- [ ] [CONCEPT: 指标与验收] 记录验证命令、结果和未覆盖风险。 + 证据: + +## 7. 文档收尾 + +- [ ] [CONCEPT: 指标与验收] 回看所有验收点,确认均有实现或验证证据。 + 证据: +- [ ] [CONCEPT: 风险与开放问题] 更新剩余风险、后续任务和明确不做项。 + 证据: +- [ ] [CONCEPT: 功能一句话] 确认最终实现没有偏离原始目标。 + 证据: diff --git a/tools/agent-change-log/README.md b/tools/agent-change-log/README.md new file mode 100644 index 0000000..f307d5f --- /dev/null +++ b/tools/agent-change-log/README.md @@ -0,0 +1,22 @@ +# Agent Change Log Automation + +这个目录提供 `agent-change-log` 的可执行部分。 + +## 手动写入一条日志 + +```bash +python3 tools/agent-change-log/update_change_log.py --event "manual" +``` + +## 安装提交后自动日志 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`。 + +注意:hook 只能覆盖“提交后自动记录”。没有提交的普通文件修改,仍需要执行代理在任务完成前按 +`AGENTS.md` 和 `agent-change-log` 主动记录。 diff --git a/tools/agent-change-log/install_post_commit_hook.sh b/tools/agent-change-log/install_post_commit_hook.sh new file mode 100755 index 0000000..90d5417 --- /dev/null +++ b/tools/agent-change-log/install_post_commit_hook.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Install the versioned agent change-log post-commit hook into this checkout. + +set -eu + +repo_root="$(git rev-parse --show-toplevel)" +hook_path="$(git rev-parse --git-path hooks/post-commit)" + +mkdir -p "$(dirname "$hook_path")" +cp "$repo_root/.githooks/post-commit" "$hook_path" +chmod +x "$hook_path" + +printf 'installed_hook=%s\n' "$hook_path" diff --git a/tools/agent-change-log/update_change_log.py b/tools/agent-change-log/update_change_log.py new file mode 100755 index 0000000..5f4a072 --- /dev/null +++ b/tools/agent-change-log/update_change_log.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""Append an incremental X-Financial agent work-log entry.""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + + +SECTION_WORK = "## 当日工作内容" +SECTION_ISSUES = "## 遗留问题" +SECTION_TODO = "## TODO" + + +@dataclass +class CommandResult: + args: list[str] + returncode: int + stdout: str + stderr: str + + @property + def ok(self) -> bool: + return self.returncode == 0 + + @property + def message(self) -> str: + return (self.stderr.strip() or self.stdout.strip()).strip() + + +def run(args: list[str], cwd: Path, timeout: int = 60) -> CommandResult: + try: + result = subprocess.run( + args, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + ) + except subprocess.TimeoutExpired as exc: + return CommandResult(args, 124, exc.stdout or "", exc.stderr or "command timed out") + return CommandResult(args, result.returncode, result.stdout, result.stderr) + + +def git(args: list[str], cwd: Path, timeout: int = 60) -> CommandResult: + return run(["git", *args], cwd, timeout=timeout) + + +def repo_root() -> Path: + result = run(["git", "rev-parse", "--show-toplevel"], Path.cwd()) + if not result.ok: + raise SystemExit(f"Not inside a git repository: {result.message}") + return Path(result.stdout.strip()) + + +def one_line(value: str) -> str: + return " ".join(value.strip().split()) + + +def indent_block(text: str, limit: int = 8) -> list[str]: + lines = [line.strip() for line in text.splitlines() if line.strip()] + if not lines: + return ["未发现"] + shown = lines[:limit] + if len(lines) > limit: + shown.append(f"... 另有 {len(lines) - limit} 条") + 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 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 collect_git(root: Path, *, no_fetch: bool, max_commits: int) -> dict[str, str | bool]: + data: dict[str, str | bool] = {} + + fetch_result = CommandResult(["git", "fetch"], 0, "", "") + if no_fetch: + data["fetch"] = "已跳过 fetch(--no-fetch)" + else: + fetch_result = git(["fetch", "--all", "--prune"], root, timeout=60) + data["fetch"] = "成功" if fetch_result.ok else f"失败:{one_line(fetch_result.message)}" + + status = git(["status", "-sb"], root) + data["status"] = status.stdout.strip() if status.ok else f"读取失败:{one_line(status.message)}" + + upstream = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root) + if upstream.ok: + data["upstream"] = upstream.stdout.strip() + behind = git(["log", "--oneline", "--decorate", f"--max-count={max_commits}", "HEAD..@{u}"], root) + ahead = git(["log", "--oneline", "--decorate", f"--max-count={max_commits}", "@{u}..HEAD"], root) + data["behind"] = behind.stdout.strip() if behind.ok else f"读取失败:{one_line(behind.message)}" + data["ahead"] = ahead.stdout.strip() if ahead.ok else f"读取失败:{one_line(ahead.message)}" + else: + data["upstream"] = "" + data["behind"] = "无 upstream 分支" + data["ahead"] = "无 upstream 分支" + + head_hash = git(["rev-parse", "--short", "HEAD"], root) + 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: + 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" + 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" - 修改:最近提交为 `{head_hash} {head_subject}`。", + f" - 操作:{event} 触发 `tools/agent-change-log/update_change_log.py`,自动读取 Git 状态并写入当天日志。", + " - 验证:自动脚本只记录提交和 Git 状态,不替代业务测试;业务验证仍以本次任务实际运行结果为准。", + " - 影响:提交后即使执行代理忘记手动写日志,也会留下最低限度的时间、提交和分支状态记录。", + ] + ) + + +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 parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + 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("--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.") + return parser.parse_args() + + +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" + + 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')}" + + if args.dry_run: + print(entry) + if issue_entry: + print() + print(issue_entry) + 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 + + +if __name__ == "__main__": + raise SystemExit(main())