diff --git a/.task/delivery-mechanism.md b/.task/delivery-mechanism.md new file mode 100644 index 00000000..872a0790 --- /dev/null +++ b/.task/delivery-mechanism.md @@ -0,0 +1,25 @@ +# feature/delivery-mechanism 任务卡 + +任务主记录:;状态:进行中;owner:weichen + +## 基线与授权 + +- 权威仓:weichen/Agent-System-Dev;目标分支:main;R0(initial_base_sha)= 3092715bf68b(历史迁移后新基线) +- 工作区:主检出续用(阶段 1 迁移窗口独占,未建 worktree——偏差已记录);分支:feature/delivery-mechanism +- 授权与交付终点:落地任务启动门禁机制(task.sh/Makefile/hooks/AGENTS.md);未授权:其他仓库的内容变更(属阶段 3/4) + +## 目标与非目标 + +目标:precheck/start/sync/finish 四命令 + pre-commit/commit-msg 硬闸 + AGENTS.md 门禁条文 + guidelines gitlink 推进(draft.7)。 +非目标:子仓入口文档迁移(阶段 3)、空列表修复(阶段 4)。 + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | 3092715bf68b... | (见提交) | 2026-09-30 | + +## 结果与验证 + +- 验证:临时真实仓库正负例 19/19 PASS(precheck 三态/start 拒绝与建立/hook 三类拒绝与放行/sync/finish 回收与 abandon 双确认);完整输出见任务执行记录。 +| 同步前验收 | 3092715bf68bc4cefb6515c157cd90cf9b5cba2e | f31fd5f62d3dd09780689e53f16c7da94215cbd1 | 2026-09-30T03:12:18Z | diff --git a/AGENTS.md b/AGENTS.md index 6571a25c..878de0d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,24 @@ -# Agent 工作说明(Agent-System-Core) +# Agent 工作说明(Agent-System-Dev) -本仓开发遵循 Development-Guidelines(见 Dev 仓 `development/guidelines`,gitlink 固定版本): +本仓开发遵循 Development-Guidelines(见 `deps/development-guidelines`,gitlink 固定版本): 核心规则与术语见其 `core/`,按任务触发阅读 `standards/` 专题;日常交付走 `workflows/delivery.md`。 -仓内规则: +## 任务启动门禁(强制) + +- 任务启动或恢复后,**第一次写操作前**运行 `make precheck` 并附完整输出;verdict 为 + `SHARED_WORKSPACE_BUSY / SYNC_REQUIRED` 时不得编辑。 +- 按其必读清单完成阅读,然后 `make task-start NAME=feature/` 建立独立 worktree + + feature 分支(自动记录 R0 与任务卡);任务中途不重复运行 precheck。 +- **验收、请求审核、合入前**各运行一次 `make task-sync`;完成声明附 precheck、task-sync、 + 测试三段输出。 +- 合入后运行 `make task-finish` 回收 worktree 与分支;放弃未合入工作用 + `make task-finish ABANDON="--abandon <原因>"` 并记录原因。 + +## 提交与 PR + +- 主题行:英文祈使句一行(≤100 列,不以句点结尾);body 写 `Why: / Evidence: / Impact: / Refs:` + 四段;任务卡与 PR 描述为同一份文档。 +- 跨仓 gitlink 变更须经组合验证(`make test`)后在 Dev 仓推进;受保护分支经 PR 合入。 - 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。 -- 提交信息用英文祈使句一行;跨仓 gitlink 变更须经组合验证后在 Dev 仓推进。 - 验证命令见 PROJECT.md「验证」节;完成声明须附实际命令输出证据。 +- 首次克隆本仓后运行 `make setup` 启用本地 pre-commit 钩子。 diff --git a/Makefile b/Makefile index 1fb55fdd..b09432d8 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,27 @@ BIN := bin/agent-system EMBED_DIR := $(BACKEND)/web/dist PLUGINS := plugins -.PHONY: help sync status build frontend test test-backend test-frontend test-core dev pin clean +.PHONY: help sync status build frontend test test-backend test-frontend test-core dev pin clean setup precheck task-start task-sync task-finish + +TASK := scripts/task.sh + +setup: + git config core.hooksPath scripts/hooks + chmod +x scripts/hooks/pre-commit scripts/task.sh + @echo "OK: pre-commit 钩子已启用(core.hooksPath=scripts/hooks)" + +precheck: + @bash $(TASK) precheck + +task-start: + @if [ -z "$(NAME)" ]; then echo "usage: make task-start NAME=feature/"; exit 2; fi + @bash $(TASK) start "$(NAME)" + +task-sync: + @bash $(TASK) sync + +task-finish: + @bash $(TASK) finish $(ABANDON) help: @echo "make sync 物化全部 submodule(递归)" diff --git a/deps/development-guidelines b/deps/development-guidelines index 26b562d2..d9576ed4 160000 --- a/deps/development-guidelines +++ b/deps/development-guidelines @@ -1 +1 @@ -Subproject commit 26b562d2e34fbc560711296a309c91c74ffc55b1 +Subproject commit d9576ed4d560764c8faddf218122f44b076eec23 diff --git a/scripts/hooks/commit-msg b/scripts/hooks/commit-msg new file mode 100755 index 00000000..d442c2aa --- /dev/null +++ b/scripts/hooks/commit-msg @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# 本地硬闸(commit-msg):主题行宽度/句点。git 将消息文件作为 $1 传入—— +# 注意不能用 pre-commit 做此事:-m 消息在 prepare-commit-msg 才写入,pre-commit 阶段尚不存在。 +msgfile="$1" +[ -n "$msgfile" ] && [ -f "$msgfile" ] || exit 0 +subject=$(head -1 "$msgfile") +[ -n "$subject" ] || exit 0 +w=$(python3 -c 'import sys,unicodedata;print(sum(2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in sys.argv[1]))' "$subject") +[ "$w" -le 100 ] || { echo "commit-msg: 拒绝——主题行宽 ${w} > 100 列:${subject}" >&2; exit 1; } +case "$subject" in + *.) echo "commit-msg: 拒绝——主题行不以句点结尾:${subject}" >&2; exit 1 ;; +esac +exit 0 diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 00000000..c2eebd7b --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# 本地硬闸(pre-commit):集成分支直提拒绝 + 分支命名。权威准入在 PR(baseline.md:26)。 +branch=$(git rev-parse --abbrev-ref HEAD) +case "$branch" in + main|master) + echo "pre-commit: 拒绝——集成分支 ${branch} 不得直接提交,走 feature 分支 + PR(collaboration.md)" >&2 + exit 1 ;; +esac +echo "$branch" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || { + echo "pre-commit: 拒绝——分支名须为 feature|fix|hotfix|release/,当前:${branch}" >&2 + exit 1 +} +exit 0 diff --git a/scripts/task.sh b/scripts/task.sh new file mode 100755 index 00000000..e03ec77a --- /dev/null +++ b/scripts/task.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# 任务启动门禁:precheck / start / sync / finish(配套 Makefile 同名目标)。 +# 规范依据:collaboration.md 共同基线与同步节点、治理清理;delivery.md §1/§5。 +set -euo pipefail + +TARGET_BRANCH="${TASK_TARGET_BRANCH:-main}" + +die() { echo "ERROR: $*" >&2; exit 1; } +need_repo() { git rev-parse --git-dir >/dev/null 2>&1 || die "not inside a git repository"; } +width() { python3 -c 'import sys,unicodedata;print(sum(2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in sys.argv[1]))' "$1"; } + +cmd_precheck() { + need_repo + git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true + local branch head rmain dirty sub_dirty + branch=$(git rev-parse --abbrev-ref HEAD) + head=$(git rev-parse HEAD) + rmain=$(git rev-parse --verify -q "origin/${TARGET_BRANCH}" 2>/dev/null || echo "$head") + dirty=$(git status --porcelain | { grep -v '^.. \.zcode/' || true; } | wc -l) + sub_dirty=$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || true; }) + local behind ahead + behind=$(git rev-list --count "HEAD..origin/${TARGET_BRANCH}" 2>/dev/null || echo 0) + ahead=$(git rev-list --count "origin/${TARGET_BRANCH}..HEAD" 2>/dev/null || echo 0) + echo "[repo] branch=${branch} head=$(echo "$head" | cut -c1-10)" + echo "[remote] origin/${TARGET_BRANCH}=$(echo "$rmain" | cut -c1-10) ahead=${ahead} behind=${behind}" + echo "[tree] $([ "$dirty" -eq 0 ] && echo clean || echo "dirty(${dirty})") submodules_dirty=${sub_dirty}" + local wt merged + wt=$(git worktree list | tail -n +2 | wc -l) + merged=$(git branch --merged "origin/${TARGET_BRANCH}" 2>/dev/null | { grep -vE 'main|master|\*' || true; } | tr -d ' +') + echo "[worktrees] linked=${wt}" + [ -n "$merged" ] && echo "[recycle] merged un-recycled branches: $(echo $merged)" + echo "[required-reading] core/baseline.md, workflows/delivery.md §1$([ "$dirty" -gt 0 ] && echo ", standards/collaboration.md(树脏=他人或未收尾现场,不得写入)")" + echo "[R0] $(echo "$rmain" | cut -c1-10)(完整:$rmain)" + if [ "$dirty" -gt 0 ] || [ "$sub_dirty" -gt 0 ]; then + echo "[verdict] SHARED_WORKSPACE_BUSY — 工作树不干净,不得写入;先收尾或经 owner 确认" + exit 3 + fi + if [ "$behind" -gt 0 ] && [ "$branch" = "${TARGET_BRANCH}" ]; then + echo "[verdict] SYNC_REQUIRED — 本地 ${TARGET_BRANCH} 落后远端 ${behind} 提交;先 git pull --ff-only" + exit 4 + fi + echo "[verdict] READY — 可建任务工作区:make task-start NAME=feature/your-branch" +} + +cmd_start() { + need_repo + local name="${1:-}"; [ -n "$name" ] || die "usage: task.sh start /" + echo "$name" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "branch name must match (feature|fix|hotfix|release)/" + git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true + [ "$(git status --porcelain | grep -v '^.. \.zcode/' | wc -l)" -eq 0 ] || die "dirty tree — resolve first (precheck verdict BUSY)" + git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "local ${TARGET_BRANCH} behind origin — pull --ff-only first" + local rmain slug wt + rmain=$(git rev-parse "origin/${TARGET_BRANCH}") + slug=$(echo "$name" | tr '/' '-') + wt="$(dirname "$(pwd)")/$(basename "$(pwd)")-wt-${slug}" + [ -e "$wt" ] && die "worktree path exists: $wt" + git worktree add -q "$wt" -b "$name" "$rmain" + (cd "$wt" && git submodule update --init --recursive >/dev/null 2>&1 || echo "[warn] submodule init incomplete in worktree") + local card="$wt/.task/${slug}.md" + mkdir -p "$wt/.task" + cat > "$card" <;状态:进行中;owner:$(git config user.name || echo unknown) + +## 基线与授权 + +- 权威仓:$(git -C "$wt" remote get-url origin 2>/dev/null || echo '(local)');目标分支:${TARGET_BRANCH};R0(initial_base_sha)= ${rmain} +- 工作区:${wt}(worktree);分支:${name} +- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> + +## 目标与非目标 + +<预期结果、受影响入口/模块/契约;明确不做什么。> + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | ${rmain} | $(git -C "$wt" rev-parse HEAD) | $(date -u +%FT%TZ) | + +## 结果与验证(完成后填写) + +- 结果: +- 验证: +- 影响与剩余项: +EOF + git -C "$wt" add .task && git -C "$wt" commit -q -m "Record the task card for ${slug}" && echo "[card] $card(已随分支提交)" + echo "[started] branch=${name} worktree=${wt} R0=$(echo "$rmain" | cut -c1-10)" +} + +cmd_sync() { + need_repo + git fetch -q origin "${TARGET_BRANCH}" + [ "$(git status --porcelain | grep -v '^.. \.zcode/' | wc -l)" -eq 0 ] || die "dirty tree — 先显式提交;不隐式 stash(collaboration.md:11)" + git merge --no-edit "origin/${TARGET_BRANCH}" + git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "sync incomplete" + local base head card + base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD) + card=$(ls .task/*.md 2>/dev/null | head -1 || true) + if [ -n "$card" ]; then + printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card" + git add "$card" + git commit -q -m "Record the sync checkpoint" || true + fi + echo "[synced] sync_base=$(echo "$base" | cut -c1-10) tested_head=$(echo "$head" | cut -c1-10) checked_at=$(date -u +%FT%TZ)" + echo "[next] 跑正式验收(make test 等),证据写入任务卡" +} + +cmd_finish() { + need_repo + local abandon="${1:-}" + local branch head common mainrepo + branch=$(git rev-parse --abbrev-ref HEAD) + head=$(git rev-parse HEAD) + common=$(git rev-parse --git-common-dir); common=$(cd "$common" && pwd) + mainrepo=$(dirname "$common") + git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true + if [ "$abandon" = "--abandon" ]; then + local reason="${2:-}"; [ -n "$reason" ] || die "--abandon 需要原因:task.sh finish --abandon " + local untracked; untracked=$(git status --porcelain | grep -c '^??' || true) + [ "$untracked" -gt 0 ] && die "存在 ${untracked} 个未跟踪文件 — 先处置或用 task-finish FORCE_ABANDON=1" + [ "${FORCE_ABANDON:-0}" = "1" ] || { echo "[confirm] 放弃未合入工作属破坏性动作:重跑 FORCE_ABANDON=1 task.sh finish --abandon \"${reason}\""; exit 5; } + echo "[abandoned] branch=${branch} reason=${reason} recorded_at=$(date -u +%FT%TZ)" + else + git merge-base --is-ancestor "$head" "origin/${TARGET_BRANCH}" || die "分支未合入 origin/${TARGET_BRANCH}——先走 PR 合入;放弃则用 --abandon " + echo "[verified] ${branch} 已包含于 origin/${TARGET_BRANCH};成果已落盘" + fi + git -C "$mainrepo" worktree remove "$PWD" && git -C "$mainrepo" branch -d "$branch" 2>/dev/null || git -C "$mainrepo" branch -D "$branch" + git -C "$mainrepo" worktree prune + echo "[recycled] worktree 与分支 ${branch} 已回收($(date -u +%FT%TZ))" +} + +case "${1:-}" in + precheck) shift; cmd_precheck "$@" ;; + start) shift; cmd_start "$@" ;; + sync) shift; cmd_sync "$@" ;; + finish) shift; cmd_finish "$@" ;; + *) echo "usage: task.sh [args]"; exit 2 ;; +esac