Add the task-startup gate: precheck/task-start/task-sync/task-finish and local hooks #1

Merged
weichen merged 3 commits from feature/delivery-mechanism into main 2026-09-30 03:13:07 +00:00
7 changed files with 232 additions and 6 deletions
+25
View File
@@ -0,0 +1,25 @@
# feature/delivery-mechanism 任务卡
任务主记录:<PR 创建后回填>;状态:进行中;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 |
+19 -4
View File
@@ -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/<slug>` 建立独立 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 钩子。
+21 -1
View File
@@ -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/<slug>"; exit 2; fi
@bash $(TASK) start "$(NAME)"
task-sync:
@bash $(TASK) sync
task-finish:
@bash $(TASK) finish $(ABANDON)
help:
@echo "make sync 物化全部 submodule(递归)"
+13
View File
@@ -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
+13
View File
@@ -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/<slug>,当前:${branch}" >&2
exit 1
}
exit 0
+140
View File
@@ -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 <feature|fix|hotfix>/<slug>"
echo "$name" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "branch name must match (feature|fix|hotfix|release)/<slug>"
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" <<EOF
# ${name} 任务卡
任务主记录:<PR 链接,task-finish 时填写>;状态:进行中;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 <reason>"
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 <reason>"
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 <precheck|start|sync|finish> [args]"; exit 2 ;;
esac