1057 lines
56 KiB
Bash
Executable File
1057 lines
56 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 任务启动门禁:precheck / triage / start / resume / sync / close / finish(配套 Makefile 同名目标)。
|
||
# 规范依据:collaboration.md 共同基线与同步节点、治理清理;delivery.md §1/§5。
|
||
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:]))'; }
|
||
work_mode() {
|
||
case "${TASK_WORK_MODE:-delivery}" in
|
||
delivery|local) printf '%s\n' "${TASK_WORK_MODE:-delivery}" ;;
|
||
*) die "TASK_WORK_MODE must be delivery or local" ;;
|
||
esac
|
||
}
|
||
|
||
cmd_precheck() {
|
||
need_repo
|
||
local mode
|
||
mode=$(work_mode)
|
||
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 "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}")
|
||
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}"
|
||
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 ' +')
|
||
[ "${sub_uninit:-0}" -gt 0 ] && echo "[warn] ${sub_uninit} 个子模块未初始化——构建/测试前需物化(不阻断写入)"
|
||
echo "[worktrees] linked=${wt}"
|
||
[ -n "$merged" ] && echo "[recycle] merged un-recycled branches: $(echo $merged)"
|
||
local open_cards="" pc pb pst
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
pb=$(card_of_branch "$pc"); [ -n "$pb" ] || continue
|
||
pst=$(card_status "$pc"); case "$pst" in 已完成*) continue ;; esac
|
||
card_pending "${pc##*/}" && continue
|
||
# card_merge 兼容 merge 提交与 squash 落地(squash 走 Gitea API 兜底,无 token 时只认 merge 提交);
|
||
# 其内部以完整消费输出取首行,避免 grep -q 早退令 git log 吃 SIGPIPE(141 under pipefail)。
|
||
if card_merged_into "$pc" "$pb" >/dev/null; then
|
||
open_cards="${open_cards} ${pc##*/}"
|
||
fi
|
||
done
|
||
[ -n "$open_cards" ] && echo "[cards] 已合入未闭合任务卡:$(echo $open_cards)——make task-close 回填归档(主检出运行)"
|
||
local done_open="" stale="" now ts ref
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
case "$(card_status "$pc")" in
|
||
已完成*) done_open="${done_open} ${pc##*/}" ;;
|
||
esac
|
||
done
|
||
[ -n "$done_open" ] && echo "[cards] 已完成但未归档:$(echo $done_open)"
|
||
local initiatives
|
||
initiatives=$(active_initiatives | cut -d'|' -f1)
|
||
echo "[initiatives] local cards (not authoritative): $([ -n "$initiatives" ] && echo $initiatives || echo none)"
|
||
now=$(date +%s)
|
||
while read -r ref ts; do
|
||
[ -n "$ref" ] || continue
|
||
git merge-base --is-ancestor "$ref" "origin/${TARGET_BRANCH}" 2>/dev/null && continue
|
||
if [ $((now - ts)) -gt 86400 ]; then
|
||
stale="${stale} ${ref#origin/}"
|
||
fi
|
||
done < <(git for-each-ref --format='%(refname:short) %(committerdate:unix)' "refs/remotes/origin/feature/card-close-*")
|
||
[ -n "$stale" ] && echo "[cards] 闭合分支超过 24 小时未合入:$(echo $stale)"
|
||
# [hooks] 声明≠能力:core.hooksPath 未指向 scripts/hooks 时,"钩子已启用"只是声明。
|
||
local hp; hp=$(git config core.hooksPath 2>/dev/null || true)
|
||
if [ "$hp" = "scripts/hooks" ] && [ -x scripts/hooks/pre-commit ]; then
|
||
echo "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)"
|
||
else
|
||
echo "[hooks] WARN core.hooksPath='${hp:-未设置}'——本地钩子未生效,先 make setup;本检出提交将绕过本地闸(远端准入仍在)"
|
||
fi
|
||
# [facts] 外部事实登记表过期项(policy/facts.md;引用前先跑其验证命令,AGENTS.md 证据与验证门禁)。
|
||
if [ -f policy/facts.md ]; then
|
||
local fid lv ttl expired="" ftotal=0
|
||
while read -r factline; do
|
||
# 单元格内转义竖线(\|)先中和,与 facts-verify 的还原约定一致,避免命令列把日期列顶偏
|
||
factline=${factline//\\|/$'\x01'}
|
||
IFS='|' read -r _ fid _ _ lv ttl _ <<<"$factline"
|
||
fid=$(echo "${fid:-}" | tr -d ' '); case "$fid" in F[0-9]*) ;; *) continue ;; esac
|
||
ftotal=$((ftotal+1))
|
||
lv=$(echo "$lv" | tr -d ' '); ttl=$(echo "$ttl" | tr -d ' ')
|
||
case "$lv" in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; *) expired="$expired ${fid}(日期格式)"; continue ;; esac
|
||
case "$ttl" in ''|*[!0-9]*) ttl=30 ;; esac
|
||
if [ $(( $(date +%s) - $(date -d "$lv" +%s) )) -gt $(( ttl*86400 )) ]; then
|
||
expired="$expired ${fid}(last=${lv},ttl=${ttl}d)"
|
||
fi
|
||
done < <(grep -E '^\| *F[0-9]+ ' policy/facts.md || true)
|
||
if [ -n "$expired" ]; then
|
||
echo "[facts] WARN 过期事实:$(echo $expired)——引用前先 make facts-verify 重验并更新登记表"
|
||
else
|
||
echo "[facts] policy/facts.md ${ftotal} 条事实均在有效期内"
|
||
fi
|
||
else
|
||
echo "[facts] WARN policy/facts.md 不存在——外部事实登记表缺失(AGENTS.md 证据与验证门禁)"
|
||
fi
|
||
echo "[required-reading] core/baseline.md, workflows/delivery.md §1$([ -f policy/delivery.md ] && echo ", policy/delivery.md")$([ "$dirty" -gt 0 ] && echo ", standards/collaboration.md(树脏=他人或未收尾现场,不得写入)")"
|
||
if [ "$mode" = local ]; then
|
||
echo "[target] ${rmain}(observed; task initial baseline is reported separately)"
|
||
else
|
||
echo "[R0] $(echo "$rmain" | cut -c1-10)(完整:$rmain)"
|
||
fi
|
||
if [ "$dirty" -gt 0 ] || [ "${sub_drift:-0}" -gt 0 ]; then
|
||
echo "[verdict] SHARED_WORKSPACE_BUSY — 工作树不干净,不得写入;先收尾或经 owner 确认"
|
||
exit 3
|
||
fi
|
||
if [ "$mode" = local ]; then
|
||
python3 "${SCRIPT_DIR}/evidence.py" --local-context || return 2
|
||
echo "[verdict] LOCAL_READY — existing task may continue authorized local work; final delivery still requires latest-target sync and checks"
|
||
return 0
|
||
fi
|
||
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 — 保留本次 precheck receipt 后建任务工作区:PRECHECK_RECEIPT=/tmp/task-precheck.json make task-start NAME=feature/your-branch"
|
||
}
|
||
|
||
cmd_start() {
|
||
need_repo
|
||
local name="${1:-}"; [ -n "$name" ] || die "usage: task.sh start <feature|fix|hotfix|release>/<slug>"
|
||
echo "$name" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "branch name must match (feature|fix|hotfix|release)/<slug>"
|
||
local kind="${TASK_BRANCH_KIND:-standalone}" initiative="${TASK_INITIATIVE:-}"
|
||
case "$kind" in standalone|initiative|child) ;; *) die "TASK_BRANCH_KIND must be standalone, initiative or child" ;; esac
|
||
if [ "$kind" = child ]; then
|
||
[ -n "$initiative" ] || die "child task requires INITIATIVE=<parent initiative branch>"
|
||
echo "$initiative" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent initiative branch"
|
||
case "${name#*/}" in "${initiative#*/}"-?*) ;; *) die "child slug must start with parent initiative slug plus '-'" ;; esac
|
||
fi
|
||
local slug planned_child=0
|
||
slug=$(echo "$name" | tr '/' '-')
|
||
if [ -z "${TRIAGE_RECEIPT:-}" ]; then
|
||
[ "$kind" = child ] || die "TRIAGE_RECEIPT required: make task-triage REQ='<需求>' TRIAGE_RECEIPT=<path> and decide first (no branch created)"
|
||
planned_child=1
|
||
fi
|
||
fetch_target
|
||
if [ -z "${PRECHECK_RECEIPT:-}" ]; then
|
||
PRECHECK_RECEIPT=$(mktemp "${TMPDIR:-/tmp}/task-precheck-${slug}.XXXXXX")
|
||
# evidence 捕获只接受未占用路径(独占创建):mktemp 只用来取唯一名。
|
||
rm -f -- "$PRECHECK_RECEIPT"
|
||
# start 只接受严格 READY 的 delivery precheck,local 模式的 LOCAL_READY 不能用于建任务。
|
||
(cd "$(git rev-parse --show-toplevel)" \
|
||
&& TASK_WORK_MODE=delivery RECEIPT="$PRECHECK_RECEIPT" CMD='make precheck' bash "${SCRIPT_DIR}/evidence.sh") \
|
||
|| die "auto precheck is not READY (receipt=${PRECHECK_RECEIPT}); resolve the verdict above first (no branch created)"
|
||
echo "[precheck] auto receipt=${PRECHECK_RECEIPT}"
|
||
fi
|
||
PRECHECK_RECEIPT=$(realpath -- "$PRECHECK_RECEIPT")
|
||
VALIDATE_RECEIPT="$PRECHECK_RECEIPT" VALIDATE_RECEIPT_PURPOSE=start bash "${SCRIPT_DIR}/evidence.sh"
|
||
local triage_out triage_line triage_head="" triage_verdict triage_init triage_rsha triage_digest triage_note
|
||
local parent_head="" parent_source="" parent_card
|
||
if [ "$planned_child" -eq 1 ]; then
|
||
# 计划内子任务免分流:父 initiative 卡片的子任务登记已列出本分支(delivery 读 origin,local 读本地父分支)。
|
||
if [ "$(work_mode)" = delivery ]; then
|
||
parent_head=$(remote_parent_head "$initiative") || die "remote parent initiative ${initiative} unavailable; no worktree or branch was created"
|
||
parent_source=origin
|
||
else
|
||
parent_head=$(git rev-parse --verify "refs/heads/${initiative}^{commit}" 2>/dev/null) || die "parent initiative branch ${initiative} missing locally"
|
||
parent_source="local(非正式准入)"
|
||
fi
|
||
parent_card=".task/$(echo "$initiative" | tr '/' '-').md"
|
||
git show "${parent_head}:${parent_card}" 2>/dev/null | registered_child "$name" \
|
||
|| die "${name} is not registered in ${initiative} ${parent_card} ## 子任务登记; register it in the parent card first or provide TRIAGE_RECEIPT (no worktree or branch was created)"
|
||
triage_note="- 需求分流:免(父 initiative 子任务登记已列出 ${name};parent_head_sha=${parent_head})"
|
||
else
|
||
# 分流核验必须先于任何 worktree/分支创建:收据与当前远端候选不一致则无副作用拒绝。
|
||
[ -f "$TRIAGE_RECEIPT" ] || die "triage receipt missing: ${TRIAGE_RECEIPT} (no branch created)"
|
||
TRIAGE_RECEIPT=$(realpath -- "$TRIAGE_RECEIPT")
|
||
local -a triage_args=(verify --receipt "$TRIAGE_RECEIPT" --kind "$kind")
|
||
if [ "$kind" = child ]; then triage_args+=(--initiative "$initiative"); fi
|
||
if ! triage_out=$(python3 "${SCRIPT_DIR}/task_triage.py" "${triage_args[@]}"); then
|
||
printf '%s\n' "$triage_out" >&2
|
||
die "triage receipt rejected; no worktree or branch was created"
|
||
fi
|
||
triage_line=$(printf '%s\n' "$triage_out" | sed -n 's/^\[triage-verified\] //p')
|
||
triage_head=$(triage_field selected_head_sha "$(printf '%s\n' "$triage_out" | sed -n 's/^\[triage-head\] //p')")
|
||
triage_verdict=$(triage_field verdict "$triage_line")
|
||
triage_init=$(triage_field selected_initiative "$triage_line")
|
||
triage_rsha=$(triage_field requirement_sha256 "$triage_line")
|
||
triage_digest=$(triage_field receipt_sha256 "$triage_line")
|
||
[ -n "$triage_verdict" ] && [ -n "$triage_rsha" ] && [ -n "$triage_digest" ] || die "triage verification output unparsable; no branch created"
|
||
triage_note="- 需求分流:verdict=${triage_verdict};selected_initiative=${triage_init:-无};requirement_sha256=${triage_rsha};triage_receipt=${TRIAGE_RECEIPT};receipt_sha256=${triage_digest}"
|
||
fi
|
||
check_worktree before-sync
|
||
# 已合入的同名分支不得重建(2026-10-05 实测:合入后再 task-start 同名会从新 main 长出
|
||
# 第二段历史,空卡覆盖已合入任务卡,两轮证据差点丢失)。复核/追加沿用原分支或换新 slug。
|
||
local guard_ref
|
||
for guard_ref in "refs/heads/${name}" "refs/remotes/origin/${name}"; do
|
||
if git rev-parse -q --verify "${guard_ref}" >/dev/null \
|
||
&& git merge-base --is-ancestor "${guard_ref}" "origin/${TARGET_BRANCH}"; then
|
||
die "branch ${name} already merged into origin/${TARGET_BRANCH} — reuse the original branch or pick a new slug (task-start must not recreate merged cards)"
|
||
fi
|
||
done
|
||
[ "$(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 wt mainroot start_ref parent_note=""
|
||
rmain=$(git rev-parse "origin/${TARGET_BRANCH}")
|
||
start_ref=$rmain
|
||
local branch_meta
|
||
case "$kind" in
|
||
initiative)
|
||
branch_meta="- 分支类型:initiative;initiative_id:${slug};root_target:${TARGET_BRANCH};immediate_target:${TARGET_BRANCH};accepting_requirements:true" ;;
|
||
child)
|
||
if [ "$planned_child" -eq 1 ]; then
|
||
: # 父头已在登记核验时解析(delivery 为刚取回的 origin 头)。
|
||
elif [ "$(work_mode)" = delivery ]; then
|
||
# 交付模式:父 initiative 只认 origin 上刚取回的头,且必须等于分流收据核验的头(未准入的本地提交不算)。
|
||
[ -n "$triage_head" ] || die "triage receipt carries no selected initiative head; re-run make task-triage (no worktree or branch was created)"
|
||
parent_head=$(remote_parent_head "$initiative") || die "remote parent initiative ${initiative} unavailable; no worktree or branch was created"
|
||
[ "$parent_head" = "$triage_head" ] || die "remote ${initiative} is ${parent_head} but triage verified ${triage_head}; re-run make task-triage (no worktree or branch was created)"
|
||
parent_source=origin
|
||
else
|
||
parent_head=$(git rev-parse --verify "refs/heads/${initiative}^{commit}" 2>/dev/null) || die "parent initiative branch ${initiative} missing locally"
|
||
parent_source="local(非正式准入)"
|
||
fi
|
||
start_ref=$parent_head
|
||
branch_meta="- 分支类型:child;initiative_id:$(echo "$initiative" | tr '/' '-');root_target:${TARGET_BRANCH};immediate_target:${initiative};accepting_requirements:false;parent_branch:${initiative};parent_head_sha:${parent_head};parent_pr:${TASK_PARENT_PR:-无};parent_source:${parent_source}" ;;
|
||
*)
|
||
branch_meta="- 分支类型:standalone;root_target:${TARGET_BRANCH};immediate_target:${TARGET_BRANCH};accepting_requirements:false" ;;
|
||
esac
|
||
if [ "$kind" != child ] && [ -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
|
||
# 落点须为主检出的平级目录:$PWD 推算在子目录(或任务 worktree 内)运行时会算错,
|
||
# 以 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" "$start_ref"
|
||
# 物化后按 submodule status 实际状态核对,不只信退出码:网络抖动或嵌套指针
|
||
# 漂移时 update 可能静默留坑(2026-09-30 实测深嵌套层带病通过,构建期才炸)。
|
||
(cd "$wt" && git submodule update --init --recursive --jobs 8 >/dev/null 2>&1) \
|
||
|| echo "[warn] submodule update 非零退出,见下方状态核对"
|
||
local sub_drift
|
||
sub_drift=$(git -C "$wt" submodule status --recursive 2>/dev/null | grep -cE '^[-+U]' || true)
|
||
[ "${sub_drift:-0}" -gt 0 ] \
|
||
&& echo "[warn] worktree 子模块物化不完整:${sub_drift} 项漂移/未初始化——构建前先核对(git -C $wt submodule status --recursive)"
|
||
local card="$wt/.task/${slug}.md"
|
||
mkdir -p "$wt/.task"
|
||
# 模板:initiative→章程卡;child 与独立 fix/hotfix→精简卡;其余独立任务→完整卡。机读行在三者中逐字一致。
|
||
local template=full
|
||
case "$kind" in
|
||
initiative) template=charter ;;
|
||
child) template=lite ;;
|
||
*) case "$name" in fix/*|hotfix/*) template=lite ;; esac ;;
|
||
esac
|
||
local origin_url owner
|
||
origin_url=$(git -C "$wt" remote get-url origin 2>/dev/null || echo '(local)')
|
||
owner=$(git config user.name || echo unknown)
|
||
{
|
||
cat <<EOF
|
||
# ${name} 任务卡
|
||
|
||
任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:${owner}
|
||
|
||
EOF
|
||
[ "$template" = lite ] || printf '## 基线与授权\n\n'
|
||
cat <<EOF
|
||
- 权威仓:${origin_url};目标分支:${TARGET_BRANCH};R0(initial_base_sha)= ${rmain}
|
||
- 工作区:${wt}(worktree);分支:${name}
|
||
${branch_meta}
|
||
${triage_note}
|
||
${parent_note}
|
||
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
|
||
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
|
||
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
|
||
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
|
||
EOF
|
||
[ "$template" = lite ] || cat <<'EOF'
|
||
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
|
||
- 当前阶段:需求拆解;当前增量:I1(待定义)
|
||
EOF
|
||
cat <<'EOF'
|
||
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
|
||
|
||
## 目标与非目标
|
||
|
||
EOF
|
||
if [ "$template" = charter ]; then
|
||
cat <<'EOF'
|
||
<章程:用途与目标、接纳范围与非目标、方案/计划链接、分级测试策略、验收条件、追加领域规则、生命周期。>
|
||
|
||
## 子任务登记
|
||
|
||
| 子任务分支 | 必需 | 状态 | PR |
|
||
|---|---|---|---|
|
||
| 待登记 | 否 | — | — |
|
||
EOF
|
||
else
|
||
echo '<预期结果、受影响入口/模块/契约;明确不做什么。>'
|
||
fi
|
||
cat <<'EOF'
|
||
|
||
## Agent 实施收据
|
||
|
||
路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;每次尝试一行,原始日志留在源码外收据路径。
|
||
|
||
| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|
||
|---|---|---|---|---|---|---|---|---|---|---|
|
||
| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |
|
||
EOF
|
||
[ "$template" = lite ] || cat <<'EOF'
|
||
|
||
## 关联方向与规划
|
||
|
||
- 适用状态:待核对(无适用规划时说明原因)
|
||
- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文)
|
||
- 对应里程碑:待核对
|
||
|
||
## 需求分析
|
||
|
||
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|
||
|---|---|---|---|---|---|---|
|
||
| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 |
|
||
|
||
## 方案与计划
|
||
|
||
- 方案引用:待填写(含对象版本)
|
||
- 实施计划引用:待填写(含增量与需求 ID)
|
||
- 测试计划引用:待填写(含需求 ID 与验收条件)
|
||
|
||
## 阶段记录
|
||
|
||
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|
||
|---|---|---|---|---|---|
|
||
| 需求拆解 | 待定 | 待执行 | — | — | — |
|
||
| 需求评估 | 待定 | 待执行 | — | — | — |
|
||
| 需求表确认 | 待定 | 待执行 | — | — | — |
|
||
| 方案设计 | 待定 | 待执行 | — | — | — |
|
||
| 实施计划 | 待定 | 待执行 | — | — | — |
|
||
| 实施与自检 | 待定 | 待执行 | — | — | — |
|
||
| 正式测试与验收 | 待定 | 待执行 | — | — | — |
|
||
| 代码 Review | 待定 | 待执行 | — | — | — |
|
||
| 合入 | 待定 | 待执行 | — | — | — |
|
||
| 合入核验与收口 | 待定 | 待执行 | — | — | — |
|
||
|
||
## 探索试验与迭代
|
||
|
||
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
|
||
EOF
|
||
cat <<EOF
|
||
|
||
## 同步记录
|
||
|
||
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|
||
|---|---|---|---|
|
||
| 启动 | ${rmain} | $(git -C "$wt" rev-parse HEAD) | $(date -u +%FT%TZ) |
|
||
|
||
## 证据(Evidence)
|
||
|
||
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
|
||
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|
||
|---|---|---|---|---|---|
|
||
EOF
|
||
[ "$template" = lite ] || cat <<'EOF'
|
||
|
||
## 交付调度
|
||
|
||
- 调度状态:未入队;调度记录:无
|
||
- entry_id:无;attempt_id:无;窗口执行者:无
|
||
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
|
||
- 调度依据:policy/merge-queue.md;新模式启用前按既有交付门禁
|
||
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
|
||
EOF
|
||
cat <<'EOF'
|
||
|
||
## 结果与验证(完成后填写)
|
||
|
||
- 结果:
|
||
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
|
||
- 影响与剩余项:
|
||
EOF
|
||
[ "$template" = lite ] || cat <<'EOF'
|
||
|
||
## 规划进展与下一步
|
||
|
||
- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划)
|
||
- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务)
|
||
- 推荐依据:待填写
|
||
- 前置条件/待确认:待核对
|
||
- 预期产物/验收条件:待定义
|
||
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
|
||
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
|
||
EOF
|
||
cat <<'EOF'
|
||
|
||
## 合入与清理
|
||
|
||
- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据)
|
||
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
|
||
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
|
||
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
|
||
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
|
||
EOF
|
||
} > "$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_resume() {
|
||
need_repo
|
||
local mode
|
||
mode=$(work_mode)
|
||
fetch_target
|
||
[ -n "${PRECHECK_RECEIPT:-}" ] || die "PRECHECK_RECEIPT required: capture the current task precheck before resume"
|
||
local receipt
|
||
receipt=$(realpath -- "$PRECHECK_RECEIPT")
|
||
VALIDATE_RECEIPT="$receipt" VALIDATE_RECEIPT_PURPOSE="resume-${mode}" bash "${SCRIPT_DIR}/evidence.sh"
|
||
# 立即目标:子任务为其父 initiative(delivery 下取远端头),其余为根目标。
|
||
local branch card immediate="${TARGET_BRANCH}" observed recorded
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
card=$(branch_card "$branch")
|
||
[ -n "$card" ] && immediate=$(card_immediate_target "$card")
|
||
if [ "$immediate" != "${TARGET_BRANCH}" ]; then
|
||
observed=$(resolve_parent_ref "$immediate") || die "immediate target ${immediate} unavailable; receipt not imported"
|
||
recorded=$(card_field "$card" parent_head_sha)
|
||
if [ -n "$recorded" ] && ! git merge-base --is-ancestor "$recorded" "$observed"; then
|
||
die "immediate target ${immediate} no longer contains recorded parent head ${recorded}; receipt not imported"
|
||
fi
|
||
else
|
||
observed=$(git rev-parse "origin/${TARGET_BRANCH}")
|
||
fi
|
||
IMPORT_RECEIPT="$receipt" bash "${SCRIPT_DIR}/evidence.sh"
|
||
echo "[resumed] work_mode=${mode} source_head=$(git rev-parse HEAD) observed_target=${observed} immediate_target=${immediate}"
|
||
}
|
||
|
||
cmd_sync() {
|
||
need_repo
|
||
fetch_target
|
||
check_worktree before-sync
|
||
local base head card branch changed=0 ancestry_status immediate="${TARGET_BRANCH}"
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
card=$(branch_card "$branch")
|
||
[ -n "$card" ] && immediate=$(card_immediate_target "$card")
|
||
if [ "$immediate" != "${TARGET_BRANCH}" ]; then
|
||
base=$(resolve_parent_ref "$immediate")
|
||
else
|
||
base=$(git rev-parse "origin/${TARGET_BRANCH}")
|
||
fi
|
||
if git merge-base --is-ancestor "$base" HEAD; then
|
||
:
|
||
else
|
||
ancestry_status=$?
|
||
[ "$ancestry_status" -eq 1 ] || die "cannot verify target ancestry"
|
||
if [ "$immediate" != "${TARGET_BRANCH}" ]; then
|
||
git merge --no-edit "$base" \
|
||
-m "Merge parent initiative ${immediate} into the task branch" \
|
||
-m "Why: include the current parent initiative baseline before verification." \
|
||
-m "Evidence: resolved ${immediate}=${base}." \
|
||
-m "Impact: integrate parent initiative changes without rewriting task history." \
|
||
-m "Refs: task-sync"
|
||
else
|
||
git merge --no-edit "$base" \
|
||
-m "Merge authoritative ${TARGET_BRANCH} into the task branch" \
|
||
-m "Why: include the current integration baseline before verification." \
|
||
-m "Evidence: fetched origin/${TARGET_BRANCH}=${base}." \
|
||
-m "Impact: integrate upstream changes without rewriting task history." \
|
||
-m "Refs: task-sync"
|
||
fi
|
||
changed=1
|
||
fi
|
||
git merge-base --is-ancestor "$base" HEAD || die "sync incomplete"
|
||
check_worktree before-update
|
||
check_worktree materialize
|
||
check_worktree after-sync
|
||
head=$(git rev-parse HEAD)
|
||
if [ -n "$card" ] && [ -f "$card" ]; then
|
||
if [ "$changed" -eq 1 ]; then
|
||
printf '| 同步(记录前对象) | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
|
||
git add "$card"
|
||
git commit -q -m "Record the sync checkpoint" \
|
||
-m "Why: bind the task validation checkpoint to the integration baseline." \
|
||
-m "Evidence: SELF recursive component checks; sync_base=${base}; before_record=${head}." \
|
||
-m "Impact: task metadata only." -m "Refs: ${card}" -- "$card"
|
||
fi
|
||
# JSON receipts carry before/after state; older table rows remain readable.
|
||
python3 "${SCRIPT_DIR}/check-evidence.py" "$card"
|
||
else
|
||
echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2
|
||
fi
|
||
head=$(git rev-parse HEAD)
|
||
echo "[synced] sync_base=${base} tested_head=${head} checked_at=$(date -u +%FT%TZ) changed=${changed} sync_target=${immediate}"
|
||
echo "[next] 跑正式验收(make test 等),证据写入任务卡"
|
||
}
|
||
|
||
# ---- 卡片闭合(防欠账)----
|
||
# 依据:delivery.md §5 治理清理;机制化 2026-09-30 治理缺口(11 张已合入卡片未闭合)。
|
||
# 检测 .task/*.md 中"分支已合入 origin/<target> 但状态非已完成"的卡片:回填状态、主记录
|
||
# (PR 链接+合并 SHA)与闭合记录,并 git mv 到 .task/archive/。变更落点:主检出运行→
|
||
# 新建 feature/card-close-* 分支推送(集成分支受保护,不直推);任务分支内运行须 --fold
|
||
# (随当前任务 PR 合入)。合并判定用合并提交主题(分支删除后仍可追溯),不依赖分支存活。
|
||
# 默认闭合方式是 close --self:任务分支在合入前把本分支的卡改为"已完成(合入前闭合)"并归档,
|
||
# 随本 PR 合入;上述合入后回填只用于遗留卡。
|
||
|
||
card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$1" | head -1; }
|
||
card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; }
|
||
# 两级分支元数据行:'- 分支类型:<kind>;initiative_id:…;root_target:…;immediate_target:…;accepting_requirements:…'。
|
||
# 旧卡没有该行:按 standalone、root/immediate 均为目标分支、不接收新需求处理。
|
||
card_field() { sed -n "s/^- 分支类型:.*;$2:\([^;]*\).*/\1/p" "$1" 2>/dev/null | head -1; }
|
||
card_branch_kind() { local v; v=$(sed -n 's/^- 分支类型:\([^;]*\).*/\1/p' "$1" 2>/dev/null | head -1); printf '%s\n' "${v:-standalone}"; }
|
||
card_initiative_id() { card_field "$1" initiative_id; }
|
||
card_root_target() { local v; v=$(card_field "$1" root_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
||
card_immediate_target() { local v; v=$(card_field "$1" immediate_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
||
card_accepting_requirements() { local v; v=$(card_field "$1" accepting_requirements); printf '%s\n' "${v:-false}"; }
|
||
PRE_MERGE_CLOSED="已完成(合入前闭合)"
|
||
branch_card() { # $1=branch → 当前检出中该分支的任务卡路径(无则空)
|
||
# 顶层优先;顶层无卡时只认 .task/archive/ 中状态为"已完成(合入前闭合)"的卡(close --self 合入前已归档)。
|
||
local pc=".task/$(echo "$1" | tr '/' '-').md"
|
||
[ -f "$pc" ] && { printf '%s\n' "$pc"; return 0; }
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
[ "$(card_of_branch "$pc")" = "$1" ] && { printf '%s\n' "$pc"; return 0; }
|
||
done
|
||
for pc in .task/archive/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
[ "$(card_of_branch "$pc")" = "$1" ] || continue
|
||
case "$(card_status "$pc")" in "${PRE_MERGE_CLOSED}"*) printf '%s\n' "$pc"; return 0 ;; esac
|
||
done
|
||
return 0
|
||
}
|
||
active_initiatives() { # → 每行 "branch|initiative_id|card":根目标为集成分支、未完成、仍接收需求且未合入/待闭合
|
||
local pc pb
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
[ "$(card_branch_kind "$pc")" = initiative ] || continue
|
||
[ "$(card_root_target "$pc")" = "${TARGET_BRANCH}" ] || continue
|
||
[ "$(card_accepting_requirements "$pc")" = true ] || continue
|
||
case "$(card_status "$pc")" in 已完成*) continue ;; esac
|
||
pb=$(card_of_branch "$pc"); [ -n "$pb" ] || continue
|
||
card_pending "${pc##*/}" && continue
|
||
card_merged_into "$pc" "$pb" >/dev/null && continue
|
||
printf '%s|%s|%s\n' "$pb" "$(card_initiative_id "$pc")" "$pc"
|
||
done
|
||
}
|
||
remote_parent_head() { # $1=父 initiative 分支 → 打印刚从 origin 取回的头(不看本地);不可查询返回 6,远端缺失返回 1
|
||
git check-ref-format "refs/heads/$1" >/dev/null || die "invalid parent branch $1"
|
||
local rc=0
|
||
git ls-remote --exit-code --heads origin "refs/heads/$1" >/dev/null 2>&1 || rc=$?
|
||
if [ "$rc" -eq 2 ]; then echo "ERROR: parent branch $1 not found on origin" >&2; return 1; fi
|
||
if [ "$rc" -ne 0 ]; then echo "[verdict] REMOTE_UNVERIFIED — cannot query origin/$1; preserve local work and retry" >&2; return 6; fi
|
||
if ! git fetch -q origin "refs/heads/$1:refs/remotes/origin/$1"; then
|
||
echo "[verdict] REMOTE_UNVERIFIED — cannot refresh origin/$1; preserve local work and retry" >&2
|
||
return 6
|
||
fi
|
||
git rev-parse --verify "refs/remotes/origin/$1^{commit}"
|
||
}
|
||
registered_child() { # stdin=父 initiative 卡片;$1=子任务分支 → 子任务登记表(task_pr_check 同一解析)列出该分支返回 0
|
||
python3 -c 'import sys
|
||
sys.path.insert(0, sys.argv[1])
|
||
from task_pr_check import Stop, child_registry
|
||
try:
|
||
rows = child_registry(sys.stdin.read())
|
||
except Stop as error:
|
||
sys.exit("ERROR: " + error.message)
|
||
sys.exit(0 if any(row["子任务分支"].strip("`").strip() == sys.argv[2] for row in rows) else 1)' "$SCRIPT_DIR" "$1"
|
||
}
|
||
resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验的提交(delivery 仅远端;local 取远端与本地包含对方的一侧)
|
||
git check-ref-format "refs/heads/$1" >/dev/null || die "invalid parent branch $1"
|
||
if [ "$(work_mode)" = delivery ]; then
|
||
# delivery 只认 origin 上刚取回的父分支头:本地领先/分叉意味着未准入的提交,不得被同步或合入。
|
||
local remote_head local_head
|
||
remote_head=$(remote_parent_head "$1") || return $?
|
||
local_head=$(git rev-parse -q --verify "refs/heads/$1^{commit}" 2>/dev/null || true)
|
||
if [ -n "$local_head" ] && ! git merge-base --is-ancestor "$local_head" "$remote_head"; then
|
||
die "local $1 has commits origin lacks; push or reconcile them first (delivery mode uses origin only)"
|
||
fi
|
||
printf '%s\n' "$remote_head"
|
||
return 0
|
||
fi
|
||
local remote="" local_ref="" rc=0
|
||
git ls-remote --exit-code --heads origin "refs/heads/$1" >/dev/null 2>&1 || rc=$?
|
||
if [ "$rc" -eq 0 ]; then
|
||
if ! git fetch -q origin "refs/heads/$1:refs/remotes/origin/$1"; then
|
||
echo "[verdict] REMOTE_UNVERIFIED — cannot refresh origin/$1; preserve local work and retry" >&2
|
||
return 6
|
||
fi
|
||
remote=$(git rev-parse --verify "refs/remotes/origin/$1^{commit}")
|
||
elif [ "$rc" -ne 2 ]; then
|
||
echo "[verdict] REMOTE_UNVERIFIED — cannot query origin/$1; preserve local work and retry" >&2
|
||
return 6
|
||
fi
|
||
local_ref=$(git rev-parse -q --verify "refs/heads/$1^{commit}" 2>/dev/null || true)
|
||
if [ -n "$remote" ] && [ -n "$local_ref" ]; then
|
||
if git merge-base --is-ancestor "$remote" "$local_ref"; then printf '%s\n' "$local_ref"
|
||
elif git merge-base --is-ancestor "$local_ref" "$remote"; then printf '%s\n' "$remote"
|
||
else die "parent branch $1 diverged between local and origin; reconcile it first"
|
||
fi
|
||
elif [ -n "$remote" ]; then printf '%s\n' "$remote"
|
||
elif [ -n "$local_ref" ]; then printf '%s\n' "$local_ref"
|
||
else die "parent branch $1 not found locally or on origin"
|
||
fi
|
||
}
|
||
|
||
# 需求分流以 origin 为准(scripts/task_triage.py):远端 open PR + 远端分支卡片;本地 .task/ 不参与发现。
|
||
# 计数不自动选择 JOIN;候选存在时必须经 triage-decide 记录显式决策。
|
||
cmd_triage() {
|
||
need_repo
|
||
[ -n "${TRIAGE_RECEIPT:-}" ] || die "TRIAGE_RECEIPT required: make task-triage REQ='<需求>' TRIAGE_RECEIPT=/tmp/<name>.json"
|
||
local req="${*:-}"
|
||
[ -n "$req" ] || die "requirement text required: make task-triage REQ='<需求>' TRIAGE_RECEIPT=<path>"
|
||
python3 "${SCRIPT_DIR}/task_triage.py" discover --receipt "$TRIAGE_RECEIPT" --requirement "$req"
|
||
}
|
||
|
||
cmd_triage_decide() {
|
||
need_repo
|
||
[ -n "${TRIAGE_RECEIPT:-}" ] || die "TRIAGE_RECEIPT required: make task-triage-decide TRIAGE_RECEIPT=<path> VERDICT=JOIN|NEW REASON='<理由>'"
|
||
local -a args=(decide --receipt "$TRIAGE_RECEIPT" --verdict "${VERDICT:-}" --reason "${REASON:-}")
|
||
if [ -n "${INITIATIVE:-}" ]; then args+=(--initiative "$INITIATIVE"); fi
|
||
python3 "${SCRIPT_DIR}/task_triage.py" "${args[@]}"
|
||
}
|
||
|
||
# triage_field KEY "key=value key2=value2" → value(值不含空格)
|
||
triage_field() { printf '%s\n' "$2" | tr ' ' '\n' | sed -n "s/^$1=//p"; }
|
||
|
||
card_merge() { # $1=branch → "sha|subject|pr"(该分支合入 origin/<target> 的记录;兼容 merge 提交与 squash 落地)
|
||
local hit pr
|
||
hit=$(git log --merges --format='%H|%s' "origin/$2" 2>/dev/null \
|
||
| grep -F " from $1 into $3" | sed -n '1p') || hit=""
|
||
if [ -n "$hit" ]; then
|
||
pr=$(printf '%s' "$hit" | sed -n 's/.*(#\([0-9]\+\)) from.*/\1/p')
|
||
printf '%s|%s\n' "$hit" "${pr:-unknown}"
|
||
return 0
|
||
fi
|
||
card_merge_squash "$1" "$2"
|
||
}
|
||
# 卡片闭合判定:子任务优先核直接目标(父 initiative 仍在远端);父已合入并删除则回落到根目标历史。
|
||
# 打印 "目标分支|merge_sha|subject|pr";未合入或不可核则失败。参数:$1=card $2=branch
|
||
card_merged_into() {
|
||
local card="$1" branch="$2" imm root rc=0 hit
|
||
imm=$(card_immediate_target "$card"); root=$(card_root_target "$card")
|
||
if [ "$imm" != "$root" ]; then
|
||
git ls-remote --exit-code --heads origin "refs/heads/${imm}" >/dev/null 2>&1 || rc=$?
|
||
case "$rc" in
|
||
0)
|
||
git fetch -q origin "refs/heads/${imm}:refs/remotes/origin/${imm}" 2>/dev/null || return 1
|
||
hit=$(card_merge "$branch" "$imm" "$imm") || return 1
|
||
printf '%s|%s\n' "$imm" "$hit"
|
||
return 0 ;;
|
||
2) ;; # 父 initiative 已不在远端:子任务合入记录仍在根目标历史中
|
||
*) return 1 ;;
|
||
esac
|
||
fi
|
||
hit=$(card_merge "$branch" "$root" "$imm") || return 1
|
||
printf '%s|%s\n' "$root" "$hit"
|
||
}
|
||
|
||
card_merge_squash() { # $1=branch $2=ref → squash/rebase 落地没有 merge 提交,经 Gitea API 按 head 分支找 merged PR。
|
||
# 返回 "merge_commit_sha|PR标题|PR号";sha 必须可核为 origin/<target> 祖先,否则不认(不可核即不闭合)。
|
||
# 仅查最近 5 页(250 条):card_merge 的 merge 提交路径仍是主路径,此兜底面向新近任务卡。
|
||
local token="${GITEA_TOKEN:-}" web_base url repo owner api payload pg found sha
|
||
[ -n "$token" ] || return 1
|
||
web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
|
||
url=$(git remote get-url origin 2>/dev/null) || return 1
|
||
url="${url%.git}"
|
||
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
|
||
api="${web_base}/api/v1/repos/${owner}/${repo}/pulls"
|
||
found=""
|
||
for pg in 1 2 3 4 5; do
|
||
payload=$(curl -fsS -m 15 -H "Authorization: token ${token}" "${api}?state=all&limit=50&page=${pg}" 2>/dev/null) || payload=""
|
||
[ -n "$payload" ] || break
|
||
found=$(printf '%s' "$payload" | CARD_BRANCH="$1" python3 -c '
|
||
import json, os, sys
|
||
branch = os.environ.get("CARD_BRANCH", "")
|
||
try:
|
||
entries = json.load(sys.stdin)
|
||
except Exception:
|
||
entries = []
|
||
for p in entries if isinstance(entries, list) else []:
|
||
if not isinstance(p, dict):
|
||
continue
|
||
head = p.get("head") or {}
|
||
ref = head.get("ref") or ""
|
||
label = head.get("label") or ""
|
||
# head 分支被删后 Gitea 把 ref 换成 refs/pull/N/head,原始分支名留在 label
|
||
# (同仓为裸分支名,fork 为 owner:branch)。
|
||
if branch != ref and branch != label and not label.endswith(":" + branch):
|
||
continue
|
||
if not p.get("merged"):
|
||
continue
|
||
sha, title, num = p.get("merge_commit_sha") or "", p.get("title") or "", str(p.get("number") or "")
|
||
if sha and title and num:
|
||
print(f"{sha}|{title}|{num}")
|
||
break
|
||
') && [ -n "$found" ] && break
|
||
done
|
||
[ -n "$found" ] || return 1
|
||
sha="${found%%|*}"
|
||
git merge-base --is-ancestor "$sha" "origin/$2" 2>/dev/null || return 1
|
||
printf '%s\n' "$found"
|
||
}
|
||
card_pending() { # $1=卡片文件名——闭合是否已在某 origin/feature/card-close-* 分支待合入
|
||
local cb
|
||
for cb in $(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/feature/card-close-*"); do
|
||
git cat-file -e "${cb}:.task/archive/$1" 2>/dev/null && return 0
|
||
done
|
||
return 1
|
||
}
|
||
card_backfill() { # $1=card $2=pr_num $3=merge_sha $4=merge_subject $5=web_base $6=contained_in_target(原地改写)
|
||
python3 - "$1" "$2" "$3" "$4" "$5" "$6" <<'PY'
|
||
import re, sys
|
||
path, pr, sha, subject, base, target = sys.argv[1:7]
|
||
s = open(path, encoding='utf-8').read()
|
||
s = re.sub(r'状态:[^;\n]*', '状态:已完成(回填)', s, count=1)
|
||
m = re.search(r'任务主记录:([^;\n]*)', s)
|
||
if m and 'pulls/' not in m.group(1):
|
||
s = s.replace(m.group(1), f'PR #{pr} {base}/pulls/{pr}(merge {sha[:10]})', 1)
|
||
s = s.replace('## 结果与验证(完成后填写)', '## 结果与验证', 1)
|
||
s = s.rstrip('\n') + f'''
|
||
|
||
## 闭合记录(card-autoclose 回填)
|
||
|
||
- 依据:merge {sha}(PR #{pr})已包含于 origin/{target},分支成果已落盘;本段为合入后回填。
|
||
- 合并主题:{subject}
|
||
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
|
||
'''
|
||
open(path, 'w', encoding='utf-8').write(s)
|
||
PY
|
||
}
|
||
|
||
self_close_pr() { # $1=branch → 打印 Gitea 上 head 为该分支的开放 PR 号(空格分隔;token 只在进程内使用)
|
||
CLOSE_BRANCH="$1" TASK_SCRIPT_DIR="$SCRIPT_DIR" python3 - <<'PY'
|
||
import os, subprocess, sys
|
||
sys.path.insert(0, os.environ["TASK_SCRIPT_DIR"])
|
||
from task_triage import Stop, api_base, open_pulls, read_token, repo_slug
|
||
branch = os.environ["CLOSE_BRANCH"]
|
||
try:
|
||
origin = subprocess.run(["git", "remote", "get-url", "origin"], capture_output=True, text=True)
|
||
if origin.returncode != 0:
|
||
raise Stop(6, "origin remote is not configured")
|
||
token = read_token()
|
||
if not token:
|
||
raise Stop(6, "no Gitea token: set GITEA_TOKEN or ~/.secrets/gitea ACCESS_TOKEN")
|
||
pulls = open_pulls(api_base(), repo_slug(origin.stdout.strip()), token)
|
||
except Stop as stop:
|
||
print(f"ERROR: cannot look up the open PR for {branch}: {stop.message}", file=sys.stderr)
|
||
sys.exit(6)
|
||
numbers = []
|
||
for pr in pulls:
|
||
if not isinstance(pr, dict) or pr.get("state", "open") != "open":
|
||
continue
|
||
head = pr.get("head") if isinstance(pr.get("head"), dict) else {}
|
||
if head.get("ref") == branch and str(pr.get("number", "")).isdigit():
|
||
numbers.append(str(pr["number"]))
|
||
print(" ".join(numbers))
|
||
PY
|
||
}
|
||
|
||
card_self_close() { # $1=card $2=pr_num $3=pr_url(原地改写:状态、主记录、结果节标题、合入前闭合记录)
|
||
python3 - "$1" "$2" "$3" <<'PY'
|
||
import re, sys
|
||
path, pr, link = sys.argv[1:4]
|
||
s = open(path, encoding='utf-8').read()
|
||
s, n = re.subn(r';状态:[^;\n]*', ';状态:已完成(合入前闭合)', s, count=1)
|
||
if n != 1:
|
||
sys.exit(f"ERROR: {path} has no ';状态:' field; refuse to fold-close")
|
||
s = re.sub(r'任务主记录:[^;\n]*', f'任务主记录:PR #{pr} {link}', s, count=1)
|
||
s = s.replace('## 结果与验证(完成后填写)', '## 结果与验证', 1)
|
||
s = s.rstrip('\n') + f'''
|
||
|
||
## 闭合记录(合入前折叠)
|
||
|
||
- 本卡在 PR #{pr}({link})合入前闭合并归档,随该 PR 一同合入。
|
||
- 该 PR 的合并提交即本任务的落地记录。
|
||
- 合入后无需回填,也无需单独的闭合 PR。
|
||
'''
|
||
open(path, 'w', encoding='utf-8').write(s)
|
||
PY
|
||
}
|
||
|
||
cmd_close_self() {
|
||
local branch card dest pr nums web_base url owner repo link msg
|
||
branch=$(git symbolic-ref --quiet --short HEAD) || die "close --self 须在任务分支上运行(当前为 detached HEAD)"
|
||
[ "$branch" != "${TARGET_BRANCH}" ] || die "close --self 不能在 ${TARGET_BRANCH} 上运行;请在任务 worktree 内对本分支的卡运行"
|
||
[ -z "$(git status --porcelain | grep -v '^.. \.zcode/' || true)" ] || die "任务工作区不干净——先提交或处置现场再 close --self"
|
||
card=$(branch_card "$branch")
|
||
[ -n "$card" ] || die "找不到分支 ${branch} 的任务卡"
|
||
case "$card" in .task/archive/*) die "${card} 已在合入前闭合归档,无需重复闭合" ;; esac
|
||
dest=".task/archive/${card##*/}"
|
||
[ ! -e "$dest" ] || die "归档目标已存在:${dest}"
|
||
if [ -n "${TASK_PR:-}" ]; then
|
||
case "$TASK_PR" in *[!0-9]*) die "TASK_PR 只能是 PR 号(纯数字)" ;; esac
|
||
pr="$TASK_PR"
|
||
else
|
||
nums=$(self_close_pr "$branch") || die "无法经 Gitea 查询 ${branch} 的开放 PR;可用 TASK_PR=<号> 显式指定"
|
||
case "$nums" in
|
||
''|*' '*) die "需要恰好一个 head 为 ${branch} 的开放 PR(查到:${nums:-无});先建 PR,或用 TASK_PR=<号> 指定" ;;
|
||
esac
|
||
pr="$nums"
|
||
fi
|
||
web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
|
||
url=$(git remote get-url origin); url="${url%.git}"
|
||
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
|
||
link="${web_base}/${owner}/${repo}/pulls/${pr}"
|
||
card_self_close "$card" "$pr" "$link"
|
||
mkdir -p .task/archive
|
||
git mv "$card" "$dest"
|
||
git add -- "$dest"
|
||
msg=$(mktemp)
|
||
{ echo "Fold-close the task card before merge"
|
||
echo
|
||
echo "Why: close the task card inside its own PR so landing it needs no post-merge backfill or separate close PR."
|
||
echo "Evidence: ${card} -> ${dest}; status ${PRE_MERGE_CLOSED}; PR #${pr} ${link}."
|
||
echo "Impact: task metadata only; the card is archived within PR #${pr} and lands with its merge commit."
|
||
echo "Refs: task.sh close --self"
|
||
} > "$msg"
|
||
git commit -q -F "$msg"; rm -f "$msg"
|
||
# push 失败重试一次:家服隧道间歇抖动(同 cmd_close)。
|
||
git push -q -u origin HEAD || { sleep 5; git push -q -u origin HEAD; }
|
||
echo "[closed] ${card} 已在 PR #${pr} 合入前闭合并归档;合入后无需 task-close"
|
||
}
|
||
|
||
cmd_close() {
|
||
need_repo
|
||
if [ "${1:-}" = "--self" ]; then shift; cmd_close_self "$@"; return; fi
|
||
local fold=0
|
||
[ "${1:-}" = "--fold" ] && fold=1
|
||
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}"
|
||
local url owner repo
|
||
url=$(git remote get-url origin); url="${url%.git}"
|
||
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
|
||
local branch c b st m cards="" own pr_base="${TARGET_BRANCH}"
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
if [ "$fold" = 1 ]; then
|
||
# 任务分支内折叠:PR 的 base 必须是本分支的立即目标(子任务为父 initiative,而非 main)。
|
||
own=$(branch_card "$branch")
|
||
if [ -n "$own" ]; then pr_base=$(card_immediate_target "$own"); fi
|
||
fi
|
||
for c in .task/*.md; do
|
||
[ -f "$c" ] || continue
|
||
b=$(card_of_branch "$c"); [ -n "$b" ] || continue
|
||
st=$(card_status "$c")
|
||
done_unarchived=0
|
||
case "$st" in 已完成*) done_unarchived=1 ;; esac
|
||
if card_pending "${c##*/}"; then
|
||
echo "[close] ${c##*/} 已有待合入的闭合分支,跳过(等其 PR 合入)"
|
||
continue
|
||
fi
|
||
m=$(card_merged_into "$c" "$b") || m=""
|
||
if [ -z "$m" ]; then
|
||
[ "$done_unarchived" = 1 ] || continue
|
||
m="ARCHIVE"
|
||
fi
|
||
cards="$cards
|
||
$c|$b|$m"
|
||
done
|
||
[ -n "$cards" ] || { echo "[close] 无已合入未闭合的任务卡"; exit 0; }
|
||
# 落点:主检出→feature/card-close-* 分支(提交过 pre-commit 分支闸);任务分支须显式 --fold
|
||
local newbranch=""
|
||
if [ "$branch" = "${TARGET_BRANCH}" ]; then
|
||
[ -z "$(git status --porcelain | grep -v '^.. \.zcode/' || true)" ] || die "主检出树脏——先收尾再 close(防混入未提交现场)"
|
||
newbranch="feature/card-close-$(date -u +%Y%m%d-%H%M%SZ)"
|
||
git rev-parse --verify -q "refs/heads/${newbranch}" >/dev/null && die "分支已存在:${newbranch}"
|
||
git checkout -q -b "$newbranch" "origin/${TARGET_BRANCH}"
|
||
elif [ "$fold" != "1" ]; then
|
||
die "close 在任务分支内须加 --fold(并入当前任务 PR);或到主检出运行(将建 feature/card-close-* 分支+PR)"
|
||
fi
|
||
local n=0 sha subject pr tgt closed_list=""
|
||
while IFS='|' read -r c b m; do
|
||
[ -n "$c" ] || continue
|
||
tgt="${m%%|*}"; m="${m#*|}"
|
||
if [ "$m" = "ARCHIVE" ]; then
|
||
mkdir -p .task/archive
|
||
git mv "$c" ".task/archive/${c##*/}"
|
||
closed_list="$closed_list ${c##*/}(already-complete)"
|
||
n=$((n+1))
|
||
continue
|
||
fi
|
||
sha="${m%%|*}"; rest="${m#*|}"; pr="${rest##*|}"; subject="${rest%|*}"
|
||
case "$pr" in "$rest"|''|*[!0-9]*) pr=unknown ;; esac
|
||
card_backfill "$c" "$pr" "$sha" "$subject" "${web_base}/${owner}/${repo}" "$tgt"
|
||
mkdir -p .task/archive
|
||
git mv "$c" ".task/archive/${c##*/}"
|
||
closed_list="$closed_list ${c##*/}(PR#${pr} into ${tgt})"
|
||
n=$((n+1))
|
||
done <<EOF
|
||
$cards
|
||
EOF
|
||
local msg; msg=$(mktemp)
|
||
{ echo "Close ${n} merged task card(s) and archive them"
|
||
echo
|
||
echo "Why: branches merged into their immediate or root target had left their task cards open; backfill status and archive to prevent bookkeeping debt."
|
||
echo "Evidence: $(echo $closed_list) — merges verified via git log --merges plus Gitea API squash lookup against each card's origin target."
|
||
echo "Impact: .task/*.md moved to .task/archive/ with status backfilled; no code or submodule change."
|
||
echo "Refs: task.sh close (card-autoclose-r1)"
|
||
} > "$msg"
|
||
git add .task
|
||
git commit -q -F "$msg"; rm -f "$msg"
|
||
# push 失败重试一次:家服隧道间歇抖动,单次尝试曾让钩子闭合半途而废(2026-09-30)。
|
||
git push -q -u origin HEAD || { sleep 5; git push -q -u origin HEAD; }
|
||
local headbr pr_url=""
|
||
headbr=$(git rev-parse --abbrev-ref HEAD)
|
||
if [ -n "${GITEA_TOKEN:-}" ]; then
|
||
# HTTPS 面偶发 broken pipe / 超时(AGENTS.md 访问事实节),单发 curl 会让 PR 静默
|
||
# 缺失(闭合分支推了、PR 没建,2026-09-30 三次踩中):退避重试 + 失败可见 + 终局
|
||
# 按 head 查回可能已存在的 PR(post-merge 钩子与手动 close 并发时 PR 会重复请求)。
|
||
local resp attempt api="${web_base}/api/v1/repos/${owner}/${repo}/pulls"
|
||
for attempt in 1 2 3 4; do
|
||
resp=$(curl -sS -m 25 -X POST -H "Authorization: token ${GITEA_TOKEN}" -H 'Content-Type: application/json' \
|
||
"$api" \
|
||
-d "{\"head\":\"${headbr}\",\"base\":\"${pr_base}\",\"title\":\"Close merged task cards (${headbr})\",\"body\":\"Auto-generated by task.sh close; see commit message for evidence.\"}" 2>&1) || resp=""
|
||
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("html_url","") if isinstance(d,dict) else "")' 2>/dev/null || true)
|
||
[ -n "$pr_url" ] && break
|
||
echo "[close] 建 PR 第 ${attempt} 次未成功${resp:+($(printf '%s' "$resp" | head -c 120))},退避重试" >&2
|
||
[ "$attempt" = 4 ] || sleep $((attempt * 3))
|
||
done
|
||
if [ -z "$pr_url" ]; then
|
||
resp=$(curl -sS -m 25 -H "Authorization: token ${GITEA_TOKEN}" "$api?state=open" 2>/dev/null || true)
|
||
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys
|
||
for p in json.load(sys.stdin):
|
||
if p["head"]["ref"] == sys.argv[1]:
|
||
print(p["html_url"]); break' "$headbr" 2>/dev/null || true)
|
||
fi
|
||
fi
|
||
if [ -n "$newbranch" ]; then git checkout -q "${TARGET_BRANCH}"; fi
|
||
if [ -n "$pr_url" ]; then
|
||
echo "[pr] ${pr_url}(智能体按 policy/delivery.md 完成 Review、解决冲突且当前 HEAD 的 CI 通过后合入)"
|
||
else
|
||
echo "[next] 建 PR:${web_base}/${owner}/${repo}/pulls/new?head=${headbr}(智能体按 policy/delivery.md 完成 Review、解决冲突且当前 HEAD 的 CI 通过后合入)"
|
||
fi
|
||
echo "[closed] ${n} 张已合入卡片已回填归档,提交于 ${headbr} 并推送"
|
||
}
|
||
|
||
cmd_finish() {
|
||
need_repo
|
||
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)
|
||
mainrepo=$(main_worktree_root)
|
||
fetch_target
|
||
local card immediate="${TARGET_BRANCH}" merged_into
|
||
card=$(branch_card "$branch")
|
||
[ -n "$card" ] && immediate=$(card_immediate_target "$card")
|
||
merged_into="origin/${TARGET_BRANCH}"
|
||
if [ "$immediate" != "${TARGET_BRANCH}" ] && [ "$abandon" != "--abandon" ]; then
|
||
merged_into=$(resolve_parent_ref "$immediate")
|
||
fi
|
||
if [ "$abandon" = "--abandon" ]; then
|
||
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" "$merged_into" || die "branch has not merged into ${immediate}; 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" "$merged_into" || die "remote task branch has unmerged work; preserve workspace and remote branch"
|
||
fi
|
||
if [ "$immediate" != "${TARGET_BRANCH}" ]; then
|
||
echo "[verified] ${branch} is included in parent initiative ${immediate}"
|
||
else
|
||
echo "[verified] ${branch} is included in origin/${TARGET_BRANCH}"
|
||
fi
|
||
fi
|
||
# 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"
|
||
local deleted=1
|
||
if [ "$abandon" = "--abandon" ]; then
|
||
:
|
||
elif [ "$immediate" != "${TARGET_BRANCH}" ]; then
|
||
# git branch -d compares with the main checkout HEAD; the child is merged into its parent instead.
|
||
git update-ref -d "refs/heads/${branch}" "$head" && deleted=0
|
||
else
|
||
git branch -d "$branch" && deleted=0
|
||
fi
|
||
if [ "$abandon" = "--abandon" ]; then
|
||
echo "[recycled] worktree removed; local branch=${branch} retained; remote=NOT_RUN"
|
||
elif [ "$deleted" -eq 0 ]; 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] cards are normally fold-closed before merge with make task-close SELF=1; only legacy cards reported by precheck [cards] need make task-close in the main checkout when authorized"
|
||
}
|
||
|
||
case "${1:-}" in
|
||
precheck) shift; cmd_precheck "$@" ;;
|
||
triage) shift; cmd_triage "$@" ;;
|
||
triage-decide) shift; cmd_triage_decide "$@" ;;
|
||
start) shift; cmd_start "$@" ;;
|
||
resume) shift; cmd_resume "$@" ;;
|
||
sync) shift; cmd_sync "$@" ;;
|
||
close) shift; cmd_close "$@" ;;
|
||
finish) shift; cmd_finish "$@" ;;
|
||
*) echo "usage: task.sh <precheck|triage|triage-decide|start|resume|sync|close|finish> [args]"; exit 2 ;;
|
||
esac
|