Files
Agent-System-Dev/scripts/task.sh
T
weichen a073b8712c Teach task-finish to recycle submodule-bearing worktrees
Why: git refuses to remove a worktree with initialized submodules; the first
real dogfood run of task-finish hit this on the empty-list fix task.
Evidence: the fix task's worktree recycled only after manual force-remove;
deinit-then-remove path exercised on the same state afterwards.
Impact: finish deinitializes submodules, removes, prunes, and deletes the
remote branch; unmerged-result guard unchanged.
Refs: .task/fix-empty-list-json.md
2026-09-30 03:27:06 +00:00

144 lines
7.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 submodule deinit --all -f >/dev/null 2>&1 || true
git -C "$mainrepo" worktree remove "$PWD" 2>/dev/null || { echo "finish: worktree remove 失败(含未清理内容)——已验证合入,可手动 rm -rf 后 git worktree prune" >&2; exit 1; }
git -C "$mainrepo" branch -d "$branch" 2>/dev/null || git -C "$mainrepo" branch -D "$branch"
git -C "$mainrepo" worktree prune
git push -q --delete origin "$branch" 2>/dev/null || true
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