agent-exec transport 8112713bc92d127e3ad4155251672a88/be7ee7a3e957
This commit is contained in:
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user