Make development gates preserve work and verifiable evidence

Why: cleanup, component synchronization, receipt recording and credential handling could contradict the documented development boundaries.

Evidence: SELF 75 isolated governance tests and source checks are captured in .task/feature-governance-reliability.md; combination and CI verification follow.

Impact: hardens local task and CI tooling, aligns policy and component documentation, and adds continuous regression tests without changing product behavior.

Refs: .task/feature-governance-reliability.md; policy/governance.md; policy/platform-admission.md
This commit is contained in:
2026-10-05 12:36:02 +00:00
parent 46a1770798
commit 8af945a1d5
24 changed files with 2297 additions and 228 deletions
+119 -45
View File
@@ -4,24 +4,36 @@
set -euo pipefail
TARGET_BRANCH="${TASK_TARGET_BRANCH:-main}"
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
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"; }
fetch_target() {
git check-ref-format "refs/heads/${TARGET_BRANCH}" >/dev/null || die "invalid target branch"
if ! git fetch -q origin "refs/heads/${TARGET_BRANCH}:refs/remotes/origin/${TARGET_BRANCH}"; then
echo "[verdict] REMOTE_UNVERIFIED — cannot refresh authoritative origin/${TARGET_BRANCH}; preserve local work and retry" >&2
return 6
fi
git rev-parse --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null \
|| { echo "[verdict] REMOTE_UNVERIFIED — target commit missing" >&2; return 6; }
}
check_worktree() { python3 "${SCRIPT_DIR}/check-worktree.py" "$1"; }
main_worktree_root() { git worktree list --porcelain -z | python3 -c 'import os,sys; print(os.fsdecode(sys.stdin.buffer.read().split(bytes([0]),1)[0][9:]))'; }
cmd_precheck() {
need_repo
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
fetch_target
local branch head rmain dirty sub_drift sub_uninit
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")
rmain=$(git rev-parse --verify "origin/${TARGET_BRANCH}")
dirty=$(git status --porcelain | { grep -v '^.. \.zcode/' || true; } | wc -l)
sub_drift=$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || true; })
sub_uninit=$(git submodule status --recursive 2>/dev/null | { grep -cE '^-' || 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)
behind=$(git rev-list --count "HEAD..origin/${TARGET_BRANCH}")
ahead=$(git rev-list --count "origin/${TARGET_BRANCH}..HEAD")
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_drift=${sub_drift} uninit=${sub_uninit}"
@@ -93,8 +105,12 @@ cmd_precheck() {
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"
if ! check_worktree before-sync; then
echo "[verdict] SHARED_WORKSPACE_BUSY — component or active operation check failed; preserve the workspace"
exit 3
fi
if [ "$behind" -gt 0 ]; then
echo "[verdict] SYNC_REQUIRED — ${branch} 缺少 origin/${TARGET_BRANCH} 的 ${behind} 个提交;主检出 pull --ff-only,任务分支 make task-sync"
exit 4
fi
echo "[verdict] READY — 可建任务工作区:make task-start NAME=feature/your-branch"
@@ -104,19 +120,34 @@ 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
fetch_target
[ -n "${PRECHECK_RECEIPT:-}" ] || die "PRECHECK_RECEIPT required: capture RECEIPT=/tmp/task-precheck.json CMD='make precheck' make evidence first"
PRECHECK_RECEIPT=$(realpath -- "$PRECHECK_RECEIPT")
VALIDATE_RECEIPT="$PRECHECK_RECEIPT" bash "${SCRIPT_DIR}/evidence.sh"
check_worktree before-sync
[ "$(git status --porcelain | { grep -v '^.. \.zcode/' || true; } | wc -l)" -eq 0 ] || die "dirty tree — resolve first (precheck verdict BUSY)"
[ "$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || true; })" -eq 0 ] || die "submodule drift/conflict — 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 mainroot
local rmain slug wt mainroot start_ref parent_head="" parent_note=""
rmain=$(git rev-parse "origin/${TARGET_BRANCH}")
start_ref=$rmain
if [ -n "${TASK_PARENT_BRANCH:-}${TASK_PARENT_PR:-}" ]; then
[ -n "${TASK_PARENT_BRANCH:-}" ] && [ -n "${TASK_PARENT_PR:-}" ] || die "stacked task requires TASK_PARENT_BRANCH and TASK_PARENT_PR"
echo "$TASK_PARENT_BRANCH" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent task branch"
echo "$TASK_PARENT_PR" | grep -qE '^https?://[^[:space:]]+/pulls/[0-9]+$' || die "TASK_PARENT_PR must be a parent PR URL"
case "${name#*/}" in "${TASK_PARENT_BRANCH#*/}"-?*) ;; *) die "stacked child slug must start with parent slug plus '-'" ;; esac
parent_head=$(git rev-parse --verify "refs/heads/${TASK_PARENT_BRANCH}^{commit}") || die "parent branch missing locally"
git merge-base --is-ancestor "$rmain" "$parent_head" || die "parent branch does not include current origin/${TARGET_BRANCH}; sync it first"
start_ref=$parent_head
parent_note="- 堆叠父任务:${TASK_PARENT_BRANCH};父 PR:${TASK_PARENT_PR};parent_head_sha=${parent_head};父合入后 task-sync 并将 PR base 改回 ${TARGET_BRANCH}"
fi
slug=$(echo "$name" | tr '/' '-')
# 落点须为主检出的平级目录:$PWD 推算在子目录(或任务 worktree 内)运行时会算错,
# 改用 git-common-dir 的父目录定位主检出(与 cmd_finish 同款惯用法)。
mainroot=$(dirname "$(cd "$(git rev-parse --git-common-dir)" && pwd)")
# 以 Git 登记表定位主检出,兼容 linked worktree 与独立 git-dir。
mainroot=$(main_worktree_root)
wt="$(dirname "$mainroot")/$(basename "$mainroot")-wt-${slug}"
[ -e "$wt" ] && die "worktree path exists: $wt"
git worktree add -q "$wt" -b "$name" "$rmain"
git worktree add -q "$wt" -b "$name" "$start_ref"
# 物化后按 submodule status 实际状态核对,不只信退出码:网络抖动或嵌套指针
# 漂移时 update 可能静默留坑(2026-09-30 实测深嵌套层带病通过,构建期才炸)。
(cd "$wt" && git submodule update --init --recursive >/dev/null 2>&1) \
@@ -130,12 +161,13 @@ cmd_start() {
cat > "$card" <<EOF
# ${name} 任务卡
任务主记录:<PR 链接,task-finish 时填写>;状态:进行中;owner:$(git config user.name || echo unknown)
任务主记录:<PR 链接,交付前填写>;状态:进行中;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}
${parent_note}
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
## 目标与非目标
@@ -160,16 +192,31 @@ cmd_start() {
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
EOF
git -C "$wt" add .task && git -C "$wt" commit -q -m "Record the task card for ${slug}" && echo "[card] $card(已随分支提交)"
(cd "$wt" && IMPORT_RECEIPT="$PRECHECK_RECEIPT" bash "${SCRIPT_DIR}/evidence.sh")
git -C "$wt" add .task
git -C "$wt" commit -q -m "Record the task card for ${slug}" \
-m "Why: establish the authorized task workspace and its initial baseline." \
-m "Evidence: preserved precheck receipt in .task/${slug}.md; R0=${rmain}." \
-m "Impact: task metadata only; the feature workspace starts at ${start_ref}." \
-m "Refs: .task/${slug}.md"
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}"
fetch_target
check_worktree before-sync
git merge --no-edit "origin/${TARGET_BRANCH}" \
-m "Merge authoritative ${TARGET_BRANCH} into the task branch" \
-m "Why: include the current integration baseline before verification." \
-m "Evidence: fetched origin/${TARGET_BRANCH}=$(git rev-parse "origin/${TARGET_BRANCH}")." \
-m "Impact: integrate upstream changes without rewriting task history." \
-m "Refs: task-sync"
git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "sync incomplete"
check_worktree before-update
check_worktree materialize
check_worktree after-sync
local base head card branch slug pc
base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD)
branch=$(git rev-parse --abbrev-ref HEAD)
@@ -188,7 +235,10 @@ cmd_sync() {
if [ -n "$card" ] && [ -f "$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
git commit -q -m "Record the sync checkpoint" \
-m "Why: bind the task validation checkpoint to the latest integration baseline." \
-m "Evidence: SELF recursive component checks; sync_base=${base}; candidate=${head}." \
-m "Impact: task metadata only." -m "Refs: ${card}"
# [evidence] 同步改变了被测对象:卡片证据表中旧 HEAD 的行,其后若有非 .task 变更则失效须重跑。
local eh stale_ev="" short_head changed
short_head=$(echo "$head" | cut -c1-10)
@@ -252,7 +302,7 @@ cmd_close() {
need_repo
local fold=0
[ "${1:-}" = "--fold" ] && fold=1
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
fetch_target
# 默认值取远端通告的 Web 面(git push 输出的 PR 建议链接);可用 TASK_CARD_WEB_BASE 覆盖。
# git-panel 前缀已失效(TLS 握手失败),默认值改为实测可用的 git-home(AGENTS.md 访问事实节)。
local web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
@@ -358,37 +408,61 @@ for p in json.load(sys.stdin):
cmd_finish() {
need_repo
local abandon="${1:-}"
local branch head common mainrepo
branch=$(git rev-parse --abbrev-ref HEAD)
local abandon="${1:-}" branch head root mainrepo remote_head="" remote_status
root=$(git rev-parse --show-toplevel)
check_worktree finish
[ "${TASK_WRITERS_STOPPED:-}" = 1 ] || die "TASK_WRITERS_STOPPED=1 required: the workspace owner must first confirm all writers have stopped"
branch=$(git symbolic-ref --quiet --short HEAD) || die "detached task worktree must be preserved"
case "$branch" in feature/*|fix/*|hotfix/*|release/*) ;; *) die "refuse to recycle non-task branch ${branch}" ;; esac
head=$(git rev-parse HEAD)
common=$(git rev-parse --git-common-dir); common=$(cd "$common" && pwd)
mainrepo=$(dirname "$common")
# 防护(T0 事故教训):finish 必须在任务 worktree 内运行——本脚本把 $PWD 当
# worktree 回收(rm -rf);从主检出运行会误删主检出。mainrepo==PWD 即主检出。
[ "$(cd "$mainrepo" && pwd)" = "$PWD" ] && die "task-finish 须在任务 worktree 内运行(cd 到 worktree 再执行),防止误删主检出"
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
mainrepo=$(main_worktree_root)
fetch_target
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)"
local reason="${2:-}"; [ -n "$reason" ] || die "--abandon requires a reason"
# An explicit abandon removes only the clean checkout; commits stay reachable.
git notes --ref=refs/notes/task-abandon append -m "Task ${branch} abandoned at $(date -u +%FT%TZ): ${reason}" "$head"
echo "[abandoned] branch=${branch}; commits retained; reason recorded in refs/notes/task-abandon"
elif [ -n "$abandon" ]; then
die "usage: task.sh finish [--abandon <reason>]"
else
git merge-base --is-ancestor "$head" "origin/${TARGET_BRANCH}" || die "分支未合入 origin/${TARGET_BRANCH}——先走 PR 合入;放弃则用 --abandon <reason>"
echo "[verified] ${branch} 已包含于 origin/${TARGET_BRANCH};成果已落盘"
git merge-base --is-ancestor "$head" "origin/${TARGET_BRANCH}" || die "branch has not merged into origin/${TARGET_BRANCH}; preserve it or explicitly abandon the checkout"
remote_status=$(git ls-remote --heads origin "refs/heads/${branch}") || die "cannot verify remote task branch; preserve workspace"
if [ -n "$remote_status" ]; then
# Refresh its object, then use that exact observation as a deletion lease.
git fetch -q origin "refs/heads/${branch}" || die "cannot fetch remote task branch; preserve workspace"
remote_head=$(git rev-parse --verify FETCH_HEAD)
git merge-base --is-ancestor "$remote_head" "origin/${TARGET_BRANCH}" || die "remote task branch has unmerged work; preserve workspace and remote branch"
fi
echo "[verified] ${branch} is included in origin/${TARGET_BRANCH}"
fi
# git 对含 gitlink 的 worktree 无条件拒绝 remove(查索引),回收走 rm -rf + prune。
# 此处不得跑 submodule deinit:submodule.<name>.url 写在共享的 .git/config 里,deinit
# 会把其他检出/worktree 的子模块一并标成未初始化(2026-09-30 实测 finish 后主检出 uninit=12)。
git -C "$mainrepo" worktree remove "$PWD" 2>/dev/null || { rm -rf "$PWD"; git -C "$mainrepo" worktree prune; }
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))"
# 合入成功→自动闭合任务卡(含本任务卡;经 feature/card-close-* 分支+PR,合入权在所有者)
( cd "$mainrepo" && bash scripts/task.sh close ) \
|| echo "[next] 卡片自动闭合未完成——在主检出运行 make task-close"
# Re-check after fetch. --force only compensates Git's gitlink limitation after all
# registered-root, owner, lock, recursive dirt and ignored-file checks have passed.
check_worktree finish
local -a removal=(worktree remove)
if git ls-files --stage | grep -q '^160000 '; then
removal+=(--force)
fi
if ! git -C "$mainrepo" "${removal[@]}" "$root"; then
die "Git refused worktree removal; workspace and branch retained. Owner must inspect the refusal; no rm fallback or additional force level is permitted."
fi
cd "$mainrepo"
if [ "$abandon" = "--abandon" ]; then
echo "[recycled] worktree removed; local branch=${branch} retained; remote=NOT_RUN"
elif git branch -d "$branch"; then
if [ -z "$remote_head" ]; then
echo "[recycled] worktree and local branch=${branch} removed; remote=ALREADY_ABSENT"
elif git push -q "--force-with-lease=refs/heads/${branch}:${remote_head}" origin ":refs/heads/${branch}"; then
echo "[recycled] worktree and local branch=${branch} removed; remote=DELETED observed_head=${remote_head}"
else
echo "[partial] local worktree/branch removed; remote=${branch} retained or result unknown; query remote before retrying" >&2
return 1
fi
else
echo "[partial] worktree removed; local branch=${branch} retained because safe branch deletion was refused; remote=NOT_RUN" >&2
return 1
fi
# Keep card closure separate: its push/PR side effects are not implied by local disposal.
echo "[next] inspect task-card closure with make task-close in the main checkout when authorized"
}
case "${1:-}" in