agent-exec transport 8112713bc92d127e3ad4155251672a88/be7ee7a3e957

This commit is contained in:
agent-exec-transport
2026-10-18 02:45:45 +00:00
parent fb4c89d9dc
commit 9a3ef83c7f
3 changed files with 254 additions and 57 deletions
+2 -2
View File
@@ -46,7 +46,7 @@ task-triage-decide:
@VERDICT="$(VERDICT)" INITIATIVE="$(INITIATIVE)" REASON="$(REASON)" bash $(TASK) triage-decide
task-start:
@if [ -z "$(NAME)" ]; then echo "usage: make task-start NAME=feature/<slug> TRIAGE_RECEIPT=<path>"; exit 2; fi
@if [ -z "$(NAME)" ]; then echo "usage: make task-start NAME=feature/<slug> TRIAGE_RECEIPT=<path> [PRECHECK_RECEIPT=<path>](KIND=child 已登记在父卡时可省略 TRIAGE_RECEIPT)"; exit 2; fi
@bash $(TASK) start "$(NAME)"
task-resume:
@@ -103,7 +103,7 @@ help:
@echo "make agent-exec-admit 校验暂存路径具备 transport 来源或属于 host 豁免"
@echo "make task-triage REQ='<需求>' TRIAGE_RECEIPT=<path> 以远端为准发现接纳中的 initiative 并写分流收据(候选存在时 exit 5,待决策)"
@echo "make task-triage-decide TRIAGE_RECEIPT=<path> VERDICT=JOIN|NEW [INITIATIVE=<branch>] REASON='<理由>' 记录显式分流决策(不按数量自动 JOIN)"
@echo "make task-start NAME=feature/<slug> TRIAGE_RECEIPT=<path> [KIND=...] [INITIATIVE=...] 建任务前核验分流收据(失败不建分支)"
@echo "make task-start NAME=feature/<slug> TRIAGE_RECEIPT=<path> [KIND=...] [INITIATIVE=...] [PRECHECK_RECEIPT=<path>] 建任务前核验分流收据(失败不建分支);KIND=child 已登记在父卡「子任务登记」时可省略 TRIAGE_RECEIPT;省略 PRECHECK_RECEIPT 时自动运行启动检查"
@echo "make task-pr-check [PR=N] [FINAL=1] 核验当前任务分支 PR 的 base/head 门禁(远端为准;FINAL=1 用于 initiative 收口)"
@echo "make ci-wait [SHA=<完整 SHA>] [TIMEOUT=1200] [INTERVAL=15] [CONTEXTS=a,b] 阻塞等待该 SHA 的 CI 结论(失败即停)"
+113 -29
View File
@@ -152,29 +152,59 @@ cmd_start() {
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
[ -n "${TRIAGE_RECEIPT:-}" ] || die "TRIAGE_RECEIPT required: make task-triage REQ='<需求>' TRIAGE_RECEIPT=<path> and decide first (no branch created)"
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
[ -n "${PRECHECK_RECEIPT:-}" ] || die "PRECHECK_RECEIPT required: capture RECEIPT=/tmp/task-precheck.json CMD='make precheck' make evidence first"
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"
# 分流核验必须先于任何 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
local triage_out triage_line triage_head triage_verdict triage_init triage_rsha triage_digest triage_note
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"
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
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}"
check_worktree before-sync
# 已合入的同名分支不得重建(2026-10-05 实测:合入后再 task-start 同名会从新 main 长出
# 第二段历史,空卡覆盖已合入任务卡,两轮证据差点丢失)。复核/追加沿用原分支或换新 slug。
@@ -188,16 +218,17 @@ cmd_start() {
[ "$(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 start_ref parent_head="" parent_source="" parent_note=""
local rmain wt mainroot start_ref parent_note=""
rmain=$(git rev-parse "origin/${TARGET_BRANCH}")
start_ref=$rmain
slug=$(echo "$name" | tr '/' '-')
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 [ "$(work_mode)" = delivery ]; then
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"
@@ -238,14 +269,26 @@ cmd_start() {
&& echo "[warn] worktree 子模块物化不完整:${sub_drift} 项漂移/未初始化——构建前先核对(git -C $wt submodule status --recursive)"
local card="$wt/.task/${slug}.md"
mkdir -p "$wt/.task"
cat > "$card" <<EOF
# 模板: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:$(git config user.name || echo unknown)
任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:${owner}
## 基线与授权
- 权威仓:$(git -C "$wt" remote get-url origin 2>/dev/null || echo '(local)');目标分支:${TARGET_BRANCH};R0(initial_base_sha)= ${rmain}
EOF
[ "$template" = lite ] || printf '## 基线与授权\n\n'
cat <<EOF
- 权威仓:${origin_url};目标分支:${TARGET_BRANCH};R0(initial_base_sha)= ${rmain}
- 工作区:${wt}(worktree);分支:${name}
${branch_meta}
${triage_note}
@@ -254,21 +297,41 @@ ${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 实施收据
按根目录 AGENT-EXECUTION.md 路由;外部 Agent 一单元一新进程/工作区。实施、修复与测试的正常候选仅为 Cursor、Antigravity(agy)和 PI;每个候选对安全可恢复失败最多 3 次全新尝试,然后才换下一候选。三者都安全耗尽后,才允许一次全新的 Codex CLI 紧急尝试。Codex 平时只用于规划、方案设计、实施计划或独立 Review。显式 fail-stop(failed-unknown、scope-expansion、blocked、cancelled、无效配置、未对账 Git 状态与安全失败)立即停止。不因推理时长、工具次数、规划方式或首次写文件时间判定失败;尝试只在显式终态失败、进程退出、总超时,或 stdin/stdout/stderr 均无活动的空闲超时时失败。CLI 可在目标、范围与安全边界内自主检查、计划、测试并迭代。实施集成必须是成功 transport 上的 scripts/agent-exec apply,或 pre-commit admit 的精确内容来源。host 直接管理的豁免仅为 .task/**、docs/superpowers/specs/** 与 docs/superpowers/plans/**。fallback 列只记录该行是否为 Codex 紧急尝试。安全耗尽不发出 host-fallback-authorized,终态为 unavailable 或 blocked。不授权 host 直接撰写该单元。原始日志保留在源码外收据路径。
路由与 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'
## 关联方向与规划
@@ -308,6 +371,8 @@ ${parent_note}
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
EOF
cat <<EOF
## 同步记录
@@ -320,6 +385,8 @@ ${parent_note}
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
EOF
[ "$template" = lite ] || cat <<'EOF'
## 交付调度
@@ -328,12 +395,16 @@ ${parent_note}
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/merge-queue.md;新模式启用前按既有交付门禁
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
EOF
cat <<'EOF'
## 结果与验证(完成后填写)
- 结果:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
EOF
[ "$template" = lite ] || cat <<'EOF'
## 规划进展与下一步
@@ -344,6 +415,8 @@ ${parent_note}
- 预期产物/验收条件:待定义
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
EOF
cat <<'EOF'
## 合入与清理
@@ -353,6 +426,7 @@ ${parent_note}
- 合入记录: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}" \
@@ -511,6 +585,16 @@ remote_parent_head() { # $1=父 initiative 分支 → 打印刚从 origin 取回
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
+139 -26
View File
@@ -1,6 +1,7 @@
"""Lifecycle regressions against real disposable Git repositories and remotes."""
import os
import json
import re
import hashlib
import http.server
import threading
@@ -15,6 +16,10 @@ import urllib.parse
SOURCE = Path(__file__).resolve().parents[1]
TRIAGE_COMMANDS = ("start", "triage", "triage-decide")
ROUTING_LINE = ("路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;"
"每次尝试一行,原始日志留在源码外收据路径。")
LITE_SECTIONS = ["## 目标与非目标", "## Agent 实施收据", "## 同步记录", "## 证据(Evidence)",
"## 结果与验证(完成后填写)", "## 合入与清理"]
class FakeGitea:
@@ -89,6 +94,8 @@ class TaskLifecycleTests(unittest.TestCase):
"GIT_CONFIG_COUNT": "2", "GIT_CONFIG_KEY_0": "gc.auto",
"GIT_CONFIG_VALUE_0": "0", "GIT_CONFIG_KEY_1": "maintenance.auto",
"GIT_CONFIG_VALUE_1": "false",
# task-start 自动 precheck 收据落在 TMPDIR:限定在夹具目录内随测试清理。
"TMPDIR": str(self.root),
# 桩位于 127.0.0.1:禁止经代理转发,保证测试不触达外部网络。
"NO_PROXY": "127.0.0.1,localhost", "no_proxy": "127.0.0.1,localhost"})
for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GITEA_TOKEN", "GITEA_API_URL",
@@ -771,12 +778,30 @@ class TaskLifecycleTests(unittest.TestCase):
else:
self.gitea.prs.append(FakeGitea.pr(len(self.gitea.prs) + 1, branch, sha))
def test_start_requires_precheck_receipt_before_creating_worktree(self):
def test_start_without_precheck_receipt_runs_and_imports_auto_precheck(self):
triage = self.triage_receipt()
result = self.task(self.repo, "start", "feature/new",
env=self.env | {"TRIAGE_RECEIPT": str(triage)})
line = next(row for row in result.stdout.splitlines() if row.startswith("[precheck] auto receipt="))
receipt = Path(line.split("=", 1)[1])
self.assertEqual(receipt.parent, self.root)
self.assertIn("feature-new", receipt.name)
data = json.loads(receipt.read_text())
self.assertEqual(data["command"], "make precheck")
self.assertIn("[verdict] READY", data["output"])
card = (self.root / "repo-wt-feature-new/.task/feature-new.md").read_text()
self.assertIn(data["receipt_sha256"], card)
def test_start_refuses_busy_auto_precheck_without_side_effects(self):
triage = self.triage_receipt()
(self.repo / "stray").write_text("unfinished\n")
result = self.task(self.repo, "start", "feature/new", check=False,
env=self.env | {"TRIAGE_RECEIPT": str(triage)})
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("SHARED_WORKSPACE_BUSY", result.stdout)
self.assertIn("auto precheck is not READY", result.stdout)
self.assertFalse((self.root / "repo-wt-feature-new").exists(), result.stdout)
self.assertEqual(self.git(self.repo, "branch", "--list", "feature/new").stdout.strip(), "")
def test_start_preserves_merged_local_and_remote_branch_names(self):
receipt = self.capture_precheck()
@@ -833,23 +858,9 @@ class TaskLifecycleTests(unittest.TestCase):
"| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |",
agent_receipts,
)
self.assertIn("Cursor、Antigravity(agy)和 PI", agent_receipts)
self.assertIn("最多 3 次全新尝试", agent_receipts)
self.assertIn("一次全新的 Codex CLI 紧急尝试", agent_receipts)
self.assertIn("规划、方案设计、实施计划或独立 Review", agent_receipts)
self.assertIn("显式 fail-stop", agent_receipts)
self.assertIn("不因推理时长、工具次数、规划方式或首次写文件时间判定失败", agent_receipts)
self.assertIn("stdin/stdout/stderr 均无活动的空闲超时", agent_receipts)
self.assertIn("自主检查、计划、测试并迭代", agent_receipts)
self.assertIn("scripts/agent-exec apply", agent_receipts)
self.assertIn("pre-commit admit", agent_receipts)
self.assertIn(".task/**", agent_receipts)
self.assertIn("docs/superpowers/specs/**", agent_receipts)
self.assertIn("docs/superpowers/plans/**", agent_receipts)
self.assertIn("fallback 列只记录该行是否为 Codex 紧急尝试", agent_receipts)
self.assertIn("不授权 host 直接撰写该单元", agent_receipts)
self.assertIn("安全耗尽不发出 host-fallback-authorized", agent_receipts)
self.assertIn("终态为 unavailable 或 blocked", agent_receipts)
self.assertIn(ROUTING_LINE, agent_receipts)
self.assertNotIn("Cursor、Antigravity(agy)和 PI", agent_receipts)
self.assertNotIn("## 子任务登记", card)
for field in ("当前阶段", "当前增量", "方案引用", "实施计划引用", "测试计划引用"):
self.assertIn(field + ":", card)
self.assertIn("## 交付调度\n", card)
@@ -1252,6 +1263,8 @@ class TaskLifecycleTests(unittest.TestCase):
self.assertEqual(self.card_helper("card_immediate_target", card), "feature/alpha")
self.assertEqual(self.card_helper("card_initiative_id", card), "feature-alpha")
self.assertEqual(self.card_helper("card_root_target", card), "main")
self.assertIn("- 需求分流:verdict=JOIN;selected_initiative=feature/alpha;", text)
self.assert_lite_card(text, "feature/alpha-login")
def test_start_child_rejects_missing_initiative_or_wrong_slug(self):
self.start_initiative()
@@ -1263,6 +1276,104 @@ class TaskLifecycleTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertFalse(wt.exists())
def register_child(self, parent, child, initiative="feature/alpha", publish=True):
card = parent / ".task" / (initiative.replace("/", "-") + ".md")
text = card.read_text()
self.assertIn("| 待登记 | 否 | — | — |", text)
card.write_text(text.replace("| 待登记 | 否 | — | — |", f"| `{child}` | 是 | 进行中 | — |", 1))
self.git(parent, "add", ".task")
self.git(parent, "commit", "-m", f"Register {child}")
if publish:
self.publish_initiative(initiative)
return self.git(parent, "rev-parse", "HEAD").stdout.strip()
def planned_child_env(self):
return self.env | {"TASK_BRANCH_KIND": "child", "TASK_INITIATIVE": "feature/alpha",
"TASK_PARENT_PR": "https://example.invalid/repo/pulls/7"}
def assert_lite_card(self, text, name):
self.assertEqual([line for line in text.splitlines() if line.startswith("## ")], LITE_SECTIONS)
self.assertEqual(text.splitlines()[0], f"# {name} 任务卡")
self.assertRegex(text, r"(?m)^任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:.+$")
self.assertRegex(text, r"(?m)^- 权威仓:.+;目标分支:main;R0(initial_base_sha)= [0-9a-f]{40}$")
self.assertRegex(text, rf"(?m)^- 工作区:.+(worktree);分支:{re.escape(name)}$")
for prefix in ("- 分支类型:", "- 需求分流:", "- 授权与交付终点:",
"- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)",
"- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)",
"- 合入执行:agent(",
"- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)"):
self.assertRegex(text, "(?m)^" + re.escape(prefix))
self.assertIn(ROUTING_LINE, text)
self.assertIn("| 待定义 | — | NOT_RUN | NOT_RUN |", text)
self.assertRegex(text, r"(?m)^\| 启动 \| [0-9a-f]{40} \| [0-9a-f]{40} \| ")
self.assertIn("| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |", text)
for field in ("Review 记录", "冲突记录", "CI 记录", "合入记录", "清理记录"):
self.assertIn(field + ":NOT_RUN", text)
self.assertNotIn("## 阶段记录", text)
def test_start_planned_child_without_triage_when_registered_on_origin(self):
parent = self.start_initiative()
parent_head = self.register_child(parent, "feature/alpha-login")
result = self.task(self.repo, "start", "feature/alpha-login", env=self.planned_child_env())
self.assertIn("[precheck] auto receipt=", result.stdout)
wt = self.root / "repo-wt-feature-alpha-login"
card = wt / ".task/feature-alpha-login.md"
text = card.read_text()
self.assertIn("- 需求分流:免(父 initiative 子任务登记已列出 feature/alpha-login;"
f"parent_head_sha={parent_head})", text)
self.assertIn(f"parent_head_sha:{parent_head};parent_pr:https://example.invalid/repo/pulls/7;"
"parent_source:origin", text)
self.git(wt, "merge-base", "--is-ancestor", parent_head, "HEAD")
self.assert_lite_card(text, "feature/alpha-login")
self.assertEqual(self.card_helper("card_immediate_target", card), "feature/alpha")
def test_start_planned_child_refuses_unregistered_child_without_side_effects(self):
parent = self.start_initiative()
self.publish_initiative("feature/alpha")
for case in ("placeholder-only", "registered-locally-only"):
with self.subTest(case=case):
if case == "registered-locally-only":
self.register_child(parent, "feature/alpha-login", publish=False)
result = self.task(self.repo, "start", "feature/alpha-login", check=False,
env=self.planned_child_env())
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("not registered in feature/alpha", result.stdout)
self.assertIn("provide TRIAGE_RECEIPT", result.stdout)
self.assertFalse((self.root / "repo-wt-feature-alpha-login").exists())
self.assertEqual(self.git(self.repo, "branch", "--list", "feature/alpha-login").stdout.strip(), "")
self.assertEqual(self.git(self.repo, "ls-remote", "--heads", "origin",
"feature/alpha-login").stdout.strip(), "")
def test_start_initiative_card_is_charter_with_child_registry(self):
wt = self.start_initiative()
card = wt / ".task/feature-alpha.md"
text = card.read_text()
goal = text.split("## 目标与非目标\n", 1)[1].split("\n## ", 1)[0]
for item in ("用途与目标", "接纳范围与非目标", "方案/计划链接", "分级测试策略", "验收条件",
"追加领域规则", "生命周期"):
self.assertIn(item, goal)
sections = [line for line in text.splitlines() if line.startswith("## ")]
self.assertEqual(sections[sections.index("## 目标与非目标") + 1], "## 子任务登记")
self.assertIn("## 阶段记录", sections)
self.assertIn(ROUTING_LINE, text)
rows = self.run_cmd(self.repo, "python3", "-c",
"import json, sys; sys.path.insert(0, 'scripts');"
"from task_pr_check import child_registry;"
"print(json.dumps(child_registry(open(sys.argv[1]).read()), ensure_ascii=False))",
str(card)).stdout
self.assertEqual(json.loads(rows), [{"子任务分支": "待登记", "必需": "否", "状态": "—", "PR": "—"}])
def test_start_fix_and_hotfix_cards_are_lite(self):
triage = self.triage_receipt()
for name in ("fix/typo", "hotfix/urgent"):
with self.subTest(name=name):
self.task(self.repo, "start", name, env=self.env | {"TRIAGE_RECEIPT": str(triage)})
slug = name.replace("/", "-")
text = (self.root / f"repo-wt-{slug}/.task/{slug}.md").read_text()
self.assert_lite_card(text, name)
self.assertIn("- 分支类型:standalone;root_target:main;", text)
self.assertIn("- 需求分流:verdict=NEW;", text)
def test_sync_child_merges_new_parent_initiative_commits(self):
parent = self.start_initiative()
wt, _ = self.start_child()
@@ -1315,14 +1426,16 @@ class TaskLifecycleTests(unittest.TestCase):
def test_start_without_triage_receipt_creates_no_worktree_or_branch(self):
receipt = self.capture_precheck()
result = self.task(self.repo, "start", "feature/untriaged", check=False,
env=self.env | {"PRECHECK_RECEIPT": str(receipt)})
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("TRIAGE_RECEIPT required", result.stdout)
self.assertFalse((self.root / "repo-wt-feature-untriaged").exists())
self.assertEqual(self.git(self.repo, "branch", "--list", "feature/untriaged").stdout.strip(), "")
self.assertEqual(self.git(self.repo, "ls-remote", "--heads", "origin",
"feature/untriaged").stdout.strip(), "")
for kind in ("standalone", "initiative"):
with self.subTest(kind=kind):
result = self.task(self.repo, "start", "feature/untriaged", check=False,
env=self.env | {"PRECHECK_RECEIPT": str(receipt), "TASK_BRANCH_KIND": kind})
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("TRIAGE_RECEIPT required", result.stdout)
self.assertFalse((self.root / "repo-wt-feature-untriaged").exists())
self.assertEqual(self.git(self.repo, "branch", "--list", "feature/untriaged").stdout.strip(), "")
self.assertEqual(self.git(self.repo, "ls-remote", "--heads", "origin",
"feature/untriaged").stdout.strip(), "")
def test_start_child_refuses_non_join_receipt_without_side_effects(self):
self.start_initiative("feature/alpha")