From d6829afb9520d71dec42103b27c7f3b9402374ba Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 09:51:35 +0000 Subject: [PATCH 01/39] Record the task card for feature-norm-remediation-u2 Why: establish the authorized task workspace and its initial baseline. Evidence: preserved precheck receipt in .task/feature-norm-remediation-u2.md; R0=b5e23daed2f0ea46261979de1d62e4de0cfa88f9. Impact: task metadata only; the feature workspace starts at 01d9f50ee8aba1c40cb9b1769598698c91c30c4b. Refs: .task/feature-norm-remediation-u2.md --- .task/feature-norm-remediation-u2.md | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .task/feature-norm-remediation-u2.md diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md new file mode 100644 index 00000000..54576649 --- /dev/null +++ b/.task/feature-norm-remediation-u2.md @@ -0,0 +1,93 @@ +# feature/norm-remediation-u2 任务卡 + +任务主记录:;状态:进行中;owner:weichen + +- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= b5e23daed2f0ea46261979de1d62e4de0cfa88f9 +- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2(worktree);分支:feature/norm-remediation-u2 +- 分支类型:child;initiative_id:feature-norm-remediation;root_target:main;immediate_target:feature/norm-remediation;accepting_requirements:false;parent_branch:feature/norm-remediation;parent_head_sha:01d9f50ee8aba1c40cb9b1769598698c91c30c4b;parent_pr:无;parent_source:origin +- 需求分流:免(父 initiative 子任务登记已列出 feature/norm-remediation-u2;parent_head_sha=01d9f50ee8aba1c40cb9b1769598698c91c30c4b) + +- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进) +- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md) +- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) +- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位) + +## 目标与非目标 + +<预期结果、受影响入口/模块/契约;明确不做什么。> + +## 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 | — | + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | b5e23daed2f0ea46261979de1d62e4de0cfa88f9 | 01d9f50ee8aba1c40cb9b1769598698c91c30c4b | 2026-10-18T09:51:35Z | + +## 证据(Evidence) + + +| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | +|---|---|---|---|---|---| + +## 结果与验证(完成后填写) + +- 结果: +- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> +- 影响与剩余项: + +## 合入与清理 + +- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据) +- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测) +- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果) +- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) +- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置) + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T09:51:00.301997+00:00", + "command": "make precheck", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev", + "before_head": "b5e23daed2f0ea46261979de1d62e4de0cfa88f9", + "after_head": "b5e23daed2f0ea46261979de1d62e4de0cfa88f9", + "branch": "main", + "target_branch": "main", + "target_head": "b5e23daed2f0ea46261979de1d62e4de0cfa88f9", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev'", + "[repo] branch=main head=b5e23daed2", + "[remote] origin/main=b5e23daed2 ahead=0 behind=0", + "[tree] clean submodules_drift=0 uninit=3", + "[warn] 3 个子模块未初始化——构建/测试前需物化(不阻断写入)", + "[worktrees] linked=11", + "[recycle] merged un-recycled branches: feature/layered-python-env", + "[initiatives] local cards (not authoritative): none", + "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)", + "[facts] policy/facts.md 6 条事实均在有效期内", + "[required-reading] core/baseline.md, workflows/delivery.md §1, policy/delivery.md", + "[R0] b5e23daed2(完整:b5e23daed2f0ea46261979de1d62e4de0cfa88f9)", + "[worktree] before-sync: recursive component checks passed", + "[verdict] READY — 保留本次 precheck receipt 后建任务工作区:PRECHECK_RECEIPT=/tmp/task-precheck.json make task-start NAME=feature/your-branch", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev'" + ], + "output_sha256": "d33d43216a047c642360e2afa99b4d72781f81698ca933e8687cc8127afde332", + "receipt_sha256": "57db39b3f66caf60040427f50f45423bb2be546f4930be3696b8218f1c29f148", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/57db39b3f66caf60040427f50f45423bb2be546f4930be3696b8218f1c29f148.json" +} +``` From fc3a740ad1b2d248bd000d3a5d885b6347b6338f Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 09:52:07 +0000 Subject: [PATCH 02/39] Define safe local continuation scope (U2) Why: Specify the authorized continuation boundary before implementation begins. Evidence: Task card records U2 files, hard stops, and acceptance criteria from the approved plan. Impact: New-task remote triage and formal delivery gates remain unchanged. Refs: Development Norm Remediation U2. --- .task/feature-norm-remediation-u2.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md index 54576649..2a60b9ed 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/feature-norm-remediation-u2.md @@ -7,7 +7,7 @@ - 分支类型:child;initiative_id:feature-norm-remediation;root_target:main;immediate_target:feature/norm-remediation;accepting_requirements:false;parent_branch:feature/norm-remediation;parent_head_sha:01d9f50ee8aba1c40cb9b1769598698c91c30c4b;parent_pr:无;parent_source:origin - 需求分流:免(父 initiative 子任务登记已列出 feature/norm-remediation-u2;parent_head_sha=01d9f50ee8aba1c40cb9b1769598698c91c30c4b) -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:用户于 2026-10-18 授权按规范整改计划实施 U2。仅允许安全既有任务的本地续作,并保留新任务启动及正式交付门禁。未授权扩展:改平台保护、host fallback、worker 权限、凭据轮换或其他 U-ID。 - 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进) - 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md) - 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) @@ -15,7 +15,13 @@ ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +目标:当远端暂时不可用或落后时,已有且仍获授权的任务只有在身份、记录基线、本地工作区和请求范围均可核验时,才能进入 `LOCAL_READY` 继续有界本地工作;本地收据不得被解释为远端新鲜或正式交付证据。 + +范围:`scripts/task.sh`、`scripts/evidence.py`、`policy/governance.md`、`tests/test_task.py`、`tests/test_task_recovery.py`、`tests/test_evidence.py`。测试可按现有代码模式作必要的最小补充,但不得改动 `task-pr-check`、`task-sync`、CI 或平台准入语义。 + +硬停止:关闭/放弃任务、授权或身份缺失/冲突、请求超范围、R0 本地对象缺失、dirty/共享工作区状态不安全、活动 Git 操作、未知外部副作用或未对账状态均拒绝续作。 + +验收:有效既有任务可在远端失败时继续本地工作;新任务仍需新鲜远端 triage;`LOCAL_READY` 不能成为 `READY`、`REPLAY`、`GATE` 或 merge check 的依据;远端恢复后 Review/merge 仍需同步权威当前目标。 ## Agent 实施收据 From 9632c8f85932195eb754a6f94d1436d2ac169bfe Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 10:57:36 +0000 Subject: [PATCH 03/39] docs(task): record blocked U2 attempt evidence Why: Preserve the exact failure provenance and prepare a clean recovery record. Evidence: The original agent-exec receipt shows overlapping edits in one Cursor session, terminal blocked, no transport, and no host fallback. Impact: The next U2 work will use new run IDs and single-writer, short task packets. Refs: Development Norm Remediation U2 recovery. --- .task/feature-norm-remediation-u2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md index 2a60b9ed..47a59093 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/feature-norm-remediation-u2.md @@ -29,7 +29,7 @@ | 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | |---|---|---|---|---|---|---|---|---|---|---| -| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — | +| U2 original | local recovery behavior | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | blocked | none | 0 | /tmp/norm-remediation-u2-receipt.json | ## 同步记录 @@ -45,9 +45,9 @@ ## 结果与验证(完成后填写) -- 结果: -- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> -- 影响与剩余项: +- 结果:原 U2 attempt 被 fail-stop;新的 U2 attempt 将拆分为短小串行 packets,并沿用 user-authorized scope。 +- 验证:worker 报告基线聚焦测试 `python3 -m unittest tests/test_task_recovery.py tests/test_evidence.py` 为 PASS;实现后 `tests/test_task_recovery.py` 因该 worker 未写入的并发测试文件变化而 FAIL(9 failures)。这只是 worker 自检,不是集成验证;host 未运行测试,正式验证 `NOT_RUN`。 +- 影响与剩余项:执行器 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`。原始 CLI 事件确认,同一 Cursor session 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms;单 writer 约束缺失导致文件竞争,未发现另一条 agent-exec run 写该 worktree。该 attempt `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`,保留在 `/tmp/agent-exec-1000/af20482c3e24cc32f3873d1451043481/wt-b04896bf17a7` 作诊断证据。Development-Manager PR #26 已增加单 writer 提示并合入 `b178dec`;待父 initiative 更新 pin 后,使用新的短单元执行,不恢复原 attempt。 ## 合入与清理 From e5a1881dfa21446b8be861e9d937c5434aeba64a Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 10:58:22 +0000 Subject: [PATCH 04/39] Record the sync checkpoint Why: bind the task validation checkpoint to the integration baseline. Evidence: SELF recursive component checks; sync_base=74bd05a876136ed8506332c76d535a6eb6356433; before_record=f65d0169f61addfc0b2a21ef3813f38b55deb687. Impact: task metadata only. Refs: .task/feature-norm-remediation-u2.md --- .task/feature-norm-remediation-u2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md index 47a59093..43943a7a 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/feature-norm-remediation-u2.md @@ -97,3 +97,4 @@ "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/57db39b3f66caf60040427f50f45423bb2be546f4930be3696b8218f1c29f148.json" } ``` +| 同步(记录前对象) | 74bd05a876136ed8506332c76d535a6eb6356433 | f65d0169f61addfc0b2a21ef3813f38b55deb687 | 2026-10-18T10:58:22Z | From 30a582372b4c2f6f9b639da40b714c6b828666c3 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 11:08:18 +0000 Subject: [PATCH 05/39] agent-exec transport af2c39009af24615b572532ee5c44eaf/4f8f2959f25a --- tests/test_task_recovery.py | 58 ++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index f37dc9c5..50ac0cfa 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -43,6 +43,24 @@ class TaskRecoveryTests(unittest.TestCase): f.git(self.wt, "add", str(self.card.relative_to(self.wt))) f.git(self.wt, "commit", "-m", "Update fixture registration") + def local_authorization_scope(self): + """Capture the explicit registered in-progress task used as local-mode authorization. + + The card carries the task scope (worktree path, branch, target branch, and R0 + initial baseline) that authorize LOCAL_READY under a registered task, regardless + of whether the authoritative origin is currently reachable. This small synthetic + fixture exposes that scope as a single value so offline-local tests can name it.""" + f = self.fixture + branch = f.git(self.wt, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip() + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + return { + "wt": self.wt, + "card": self.card, + "branch": branch, + "target_branch": "main", + "r0": r0, + } + def test_local_precheck_allows_known_lag_without_changing_head(self): f = self.fixture head = f.git(self.wt, "rev-parse", "HEAD").stdout @@ -88,17 +106,43 @@ class TaskRecoveryTests(unittest.TestCase): self.save_card_change() self.assertNotEqual(self.precheck().returncode, 0) - def test_local_precheck_rejects_dirty_state_and_unknown_remote(self): + def test_local_precheck_distinguishes_dirty_local_ready_and_remote_unverified(self): + """A registered in-progress task with explicit authorization and scope distinguishes + SHARED_WORKSPACE_BUSY (dirty), LOCAL_READY (offline, registered, HEAD unchanged), + and REMOTE_UNVERIFIED (delivery, origin unavailable).""" f = self.fixture + scope = self.local_authorization_scope() + self.assertEqual(scope["branch"], "feature/test") + self.assertTrue(scope["card"].is_file()) + self.assertEqual(scope["target_branch"], "main") + + # (1) Dirty workspace still yields SHARED_WORKSPACE_BUSY under the registered scope + # even when the authoritative origin is reachable. Clean before (2). (self.wt / "tracked").write_text("preserve current edit\n") - result = self.precheck() - self.assertNotEqual(result.returncode, 0) - self.assertIn("SHARED_WORKSPACE_BUSY", result.stdout) + dirty = self.precheck() + self.assertNotEqual(dirty.returncode, 0, dirty.stdout) + self.assertIn("[verdict] SHARED_WORKSPACE_BUSY", dirty.stdout) + self.assertNotIn("[verdict] LOCAL_READY", dirty.stdout) f.git(self.wt, "commit", "-am", "Save owner edit") + + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout f.git(self.wt, "remote", "set-url", "origin", str(f.root / "missing.git")) - result = self.precheck() - self.assertNotEqual(result.returncode, 0) - self.assertIn("REMOTE_UNVERIFIED", result.stdout) + + # (2) Same unavailable origin; local mode against a registered in-progress task + # returns LOCAL_READY and leaves HEAD untouched. + local = self.precheck(mode="local") + self.assertEqual(local.returncode, 0, local.stdout) + self.assertIn("[verdict] LOCAL_READY", local.stdout) + self.assertNotIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + + # (3) Same unavailable origin; delivery mode continues to refuse with + # REMOTE_UNVERIFIED because no read has refreshed the authoritative target. + delivery = self.precheck(mode="delivery") + self.assertNotEqual(delivery.returncode, 0, delivery.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", delivery.stdout) + self.assertNotIn("[verdict] LOCAL_READY", delivery.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) def test_unknown_work_mode_is_rejected(self): result = self.precheck(mode="anything") From d6df427e39e2dd8f57d51d04318dedffff96523a Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 11:12:05 +0000 Subject: [PATCH 06/39] agent-exec transport 3fe48cd812763be825a67eebc5ea78f6/a9ce16b6d684 --- scripts/task.sh | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/scripts/task.sh b/scripts/task.sh index a64e6d01..2d908fd8 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -31,19 +31,31 @@ cmd_precheck() { need_repo local mode mode=$(work_mode) - fetch_target - local branch head rmain dirty sub_drift sub_uninit + # local 模式刷新失败只标记远端新鲜度不可用,不拿缓存远端引用冒充最新;delivery 保持 REMOTE_UNVERIFIED。 + local remote_fresh=1 fetch_err="" + if [ "$mode" = local ]; then + git check-ref-format "refs/heads/${TARGET_BRANCH}" >/dev/null || die "invalid target branch" + fetch_err=$(fetch_target 2>&1 >/dev/null) || remote_fresh=0 + else + fetch_target + fi + 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}") + [ "$remote_fresh" -eq 1 ] && 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") + local behind=0 ahead=0 echo "[repo] branch=${branch} head=$(echo "$head" | cut -c1-10)" - echo "[remote] origin/${TARGET_BRANCH}=$(echo "$rmain" | cut -c1-10) ahead=${ahead} behind=${behind}" + if [ "$remote_fresh" -eq 1 ]; then + behind=$(git rev-list --count "HEAD..origin/${TARGET_BRANCH}") + ahead=$(git rev-list --count "origin/${TARGET_BRANCH}..HEAD") + echo "[remote] origin/${TARGET_BRANCH}=$(echo "$rmain" | cut -c1-10) ahead=${ahead} behind=${behind}" + else + echo "[remote] origin/${TARGET_BRANCH}=unavailable — refresh failed; remote freshness unverified (cached ref not used)" + [ -n "$fetch_err" ] && printf '%s\n' "$fetch_err" | sed -e '/^\[verdict\]/d' -e 's/^/[remote] /' + fi 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) @@ -116,7 +128,9 @@ cmd_precheck() { 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 + if [ "$mode" = local ] && [ "$remote_fresh" -eq 0 ]; then + echo "[target] unavailable(remote freshness unverified; task initial baseline is reported separately)" + elif [ "$mode" = local ]; then echo "[target] ${rmain}(observed; task initial baseline is reported separately)" else echo "[R0] $(echo "$rmain" | cut -c1-10)(完整:$rmain)" @@ -127,6 +141,7 @@ cmd_precheck() { fi if [ "$mode" = local ]; then python3 "${SCRIPT_DIR}/evidence.py" --local-context || return 2 + [ "$remote_fresh" -eq 1 ] || echo "[remote-freshness] unavailable — LOCAL_READY rests on local task context only" echo "[verdict] LOCAL_READY — existing task may continue authorized local work; final delivery still requires latest-target sync and checks" return 0 fi From 2bbe4d8fcf15b4f5efb414a30eb8aaca9595d218 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 11:16:55 +0000 Subject: [PATCH 07/39] Record local task resume evidence Why: Preserve the validated LOCAL_READY resume receipt before starting a worker. Evidence: make precheck returned LOCAL_READY; make task-resume imported the receipt. Impact: SELF-only task metadata; delivery still requires authoritative sync and checks. Refs: feature/norm-remediation-u2 --- .task/feature-norm-remediation-u2.md | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md index 43943a7a..4b57f5b8 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/feature-norm-remediation-u2.md @@ -98,3 +98,43 @@ } ``` | 同步(记录前对象) | 74bd05a876136ed8506332c76d535a6eb6356433 | f65d0169f61addfc0b2a21ef3813f38b55deb687 | 2026-10-18T10:58:22Z | + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T11:14:41.060001+00:00", + "command": "make precheck", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2", + "before_head": "d6df427e39e2dd8f57d51d04318dedffff96523a", + "after_head": "d6df427e39e2dd8f57d51d04318dedffff96523a", + "branch": "feature/norm-remediation-u2", + "target_branch": "main", + "target_head": "ba81d638a6c845d0acde6896f4001bed096ba26c", + "root_target": "main", + "immediate_target": "feature/norm-remediation", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2'", + "[repo] branch=feature/norm-remediation-u2 head=d6df427e39", + "[remote] origin/main=ba81d638a6 ahead=10 behind=12", + "[tree] clean submodules_drift=0 uninit=0", + "[worktrees] linked=11", + "[recycle] merged un-recycled branches: feature/layered-python-env", + "[initiatives] local cards (not authoritative): none", + "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)", + "[facts] policy/facts.md 6 条事实均在有效期内", + "[required-reading] core/baseline.md, workflows/delivery.md §1, policy/delivery.md", + "[target] ba81d638a6c845d0acde6896f4001bed096ba26c(observed; task initial baseline is reported separately)", + "[task-state] registered=/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2 initial_base=b5e23daed2f0ea46261979de1d62e4de0cfa88f9", + "[verdict] LOCAL_READY — existing task may continue authorized local work; final delivery still requires latest-target sync and checks", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2'" + ], + "output_sha256": "9875968d327058ec22a07453a7c748f5c7b3557fdc8d05355b907b8458d8fa6d", + "receipt_sha256": "00e543451c4435f6669c74d51a6a678db2cd40a0498a56ae377447044adce7f8", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/00e543451c4435f6669c74d51a6a678db2cd40a0498a56ae377447044adce7f8.json" +} +``` From 210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 11:24:49 +0000 Subject: [PATCH 08/39] agent-exec transport 9c7c1417e62f511b5f90669f58d69da9/682c27506bb4 --- policy/governance.md | 5 ++ scripts/evidence.py | 26 +++++++++- tests/test_evidence.py | 113 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/policy/governance.md b/policy/governance.md index 41904cbe..faf4b235 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -54,6 +54,11 @@ TASK_WORK_MODE=local make task-resume PRECHECK_RECEIPT=/tmp/my-local-resume.json LOCAL_READY 只用于局部恢复;新任务入口始终校验严格 READY,正式验收与合入另行同步。 恢复核验实际 worktree/卡片/分支、R0 祖先关系及源码外原始收据;脏现场、Git 操作、组件漂移和未知远端仍拒绝。 +`resume-local` 只接受 LOCAL_READY 收据,且须同时满足:卡片位于本 worktree 的 `.task/` 且状态未以“已”开头 +(已完成/已放弃等闭合任务拒绝);收据内 `[task-state]` 的登记路径与 R0 等于当前登记任务;收据前后与当前 HEAD +一致、工作区干净且组件状态未变;收据 `root_target/immediate_target` 等于卡片范围;目标只允许快进落后,回退或分叉拒绝。 +`resume-delivery` 仍要求 READY 且收据目标等于当前权威目标。LOCAL_READY 始终是 SELF 本地工作记录, +不能用于建任务、正式交付、Review、合入、CI、GATE 或 REPLAY。 旧收据允许作为历史证据原样导入,不自动成为当前恢复或交付准入;格式继续为 schema 1。 堆叠(standalone)任务使用 `PARENT=feature/parent PARENT_PR=<父PR链接>`;子任务改用 `KIND=child`。父分支须包含本批主干 R0; diff --git a/scripts/evidence.py b/scripts/evidence.py index a40a5e84..afbbd4ca 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -27,6 +27,7 @@ BRANCH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,199}\Z") # 可选的分支目标字段:两者须同时出现或同时缺省(旧收据没有)。 TARGET_FIELDS = ("root_target", "immediate_target") PRE_MERGE_CLOSED = "已完成(合入前闭合)" +TASK_STATE = re.compile(r"^\[task-state\] registered=(.+) initial_base=([0-9a-f]+)$", re.M) BASE_FIELDS = {"schema", "source", "timestamp", "command", "command_sha256", "cwd", "common_dir", "before", "after", "after_error", "exit_code", "output", "output_sha256", "receipt_sha256"} @@ -314,6 +315,25 @@ def validate_precheck(receipt, purpose="start"): git("merge-base", "--is-ancestor", after["target_head"], current["target_head"]) except EvidenceError: raise EvidenceError("Precheck target history has rewound or diverged") from None + validate_local_scope(receipt, current, context) + + +def validate_local_scope(receipt, current, context): + """LOCAL_READY continues one active registered task; it remains SELF local-work evidence only.""" + card = find_card() + status = re.search(r";状态:\s*([^;\s][^;\n]*)", card.read_text(encoding="utf-8")) + if card.resolve().parent != context["root"] / ".task" or not status or status.group(1).startswith("已"): + raise EvidenceError("Local continuation requires an active registered task") + states = TASK_STATE.findall(receipt["output"]) + if len(states) != 1 or Path(states[0][0]) != context["root"] or states[0][1] != context["r0"]: + raise EvidenceError("Precheck belongs to a different task identity or initial baseline") + for obj in (receipt["before"], receipt["after"]): + if obj["status"] or obj["untracked"] or obj["tracked_diff_sha256"] != digest(b""): + raise EvidenceError("Precheck workspace was not clean") + if obj["submodules"] != current["submodules"]: + raise EvidenceError("Precheck component state has changed") + if tuple(receipt.get(key) for key in TARGET_FIELDS) != task_targets(): + raise EvidenceError("Precheck authorization scope differs from the registered task") def find_card(): @@ -457,8 +477,10 @@ def main(): if imported or validated: receipt = read_receipt(imported or validated) if validated: - validate_precheck(receipt, os.environ.get("VALIDATE_RECEIPT_PURPOSE", "start")) - report(receipt, "precheck validated") + purpose = os.environ.get("VALIDATE_RECEIPT_PURPOSE", "start") + validate_precheck(receipt, purpose) + report(receipt, "local continuation validated (local work only; not delivery evidence)" + if purpose == "resume-local" else "precheck validated") else: card = find_card() append_card(card, receipt) diff --git a/tests/test_evidence.py b/tests/test_evidence.py index 01fc018b..08810a5f 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -477,6 +477,119 @@ class EvidenceTests(unittest.TestCase): self.assertNotEqual(self.import_receipt().returncode, 0) self.assertEqual(self.card.read_text(), "# Test task\n") + def local_task(self, status="进行中"): + """Register a clean linked task worktree whose precheck prints the real local task context.""" + wt = self.base.resolve() / "task-wt" + self.git("worktree", "add", "-qb", "feature/local", str(wt), self.head) + (wt / ".task").mkdir() + (wt / ".task/feature-local.md").write_text( + f"# Local task\n任务主记录:fixture;状态:{status};owner:Evidence Test\n" + f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {self.head}\n" + f"- 工作区:{wt}(worktree);分支:feature/local\n") + (wt / "Makefile").write_text( + f"VERDICT ?= LOCAL_READY\nprecheck:\n\t@python3 {SCRIPTS / 'evidence.py'} --local-context\n" + "\t@echo '[verdict] $(VERDICT) — fixture'\n") + self.git("add", ".task", "Makefile", cwd=wt) + self.git("commit", "-qm", "Register local task", cwd=wt) + return wt + + def validate(self, wt, purpose="resume-local", **values): + return self.run_evidence(cwd=wt, VALIDATE_RECEIPT=self.receipt, + VALIDATE_RECEIPT_PURPOSE=purpose, **values) + + def advance_target(self): + head = self.git("commit-tree", "-p", self.head, "-m", "Advance target", self.head + "^{tree}") + self.git("update-ref", "refs/remotes/origin/main", head) + + def test_local_continuation_accepts_registered_task_with_known_lag(self): + wt = self.local_task() + self.capture("make precheck", cwd=wt) + result = self.validate(wt) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("local continuation validated", result.stdout) + self.assertIn("src=SELF", result.stdout) + self.advance_target() + lagging = self.validate(wt) + self.assertEqual(lagging.returncode, 0, lagging.stderr) + + def test_local_ready_never_authorizes_start_delivery_or_promoted_source(self): + wt = self.local_task() + self.capture("make precheck", cwd=wt) + for purpose in ("start", "resume-delivery"): + with self.subTest(purpose=purpose): + result = self.validate(wt, purpose) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Precheck receipt is not READY", result.stderr) + for source in ("GATE", "REPLAY"): + with self.subTest(source=source): + result = self.validate(wt, SRC=source) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Local evidence is SELF", result.stderr) + + def test_delivery_resume_still_requires_ready_and_current_target(self): + wt = self.local_task() + self.capture("make precheck", cwd=wt, VERDICT="READY") + local = self.validate(wt) + self.assertNotEqual(local.returncode, 0) + self.assertIn("Precheck receipt is not LOCAL_READY", local.stderr) + current = self.validate(wt, "resume-delivery") + self.assertEqual(current.returncode, 0, current.stderr) + self.assertIn("precheck validated", current.stdout) + self.advance_target() + stale = self.validate(wt, "resume-delivery") + self.assertNotEqual(stale.returncode, 0) + self.assertIn("authoritative target has changed", stale.stderr) + + def test_local_continuation_rejects_closed_task(self): + wt = self.local_task(status="已完成(合入前闭合)") + self.capture("make precheck", cwd=wt) + result = self.validate(wt) + self.assertNotEqual(result.returncode, 0) + self.assertIn("active registered task", result.stderr) + + def test_local_continuation_rejects_dirty_workspace_or_changed_head(self): + wt = self.local_task() + self.capture("make precheck", cwd=wt) + (wt / "extra").write_text("unsaved\n") + dirty = self.validate(wt) + self.assertNotEqual(dirty.returncode, 0) + self.assertIn("Task workspace is not ready", dirty.stderr) + (wt / "extra").unlink() + self.git("commit", "--allow-empty", "-qm", "Advance task", cwd=wt) + moved = self.validate(wt) + self.assertNotEqual(moved.returncode, 0) + self.assertIn("source HEAD has changed", moved.stderr) + + def test_local_continuation_rejects_mismatched_identity_baseline_workspace_or_scope(self): + wt = self.local_task() + original = self.capture("make precheck", cwd=wt) + identity = "different task identity or initial baseline" + cases = ( + ("task identity", identity, + lambda d: d.update(output=d["output"].replace(str(wt), str(self.base / "other")))), + ("initial baseline", identity, + lambda d: d.update(output=d["output"].replace(self.head, "0" * 40))), + ("missing identity", identity, + lambda d: d.update(output="[verdict] LOCAL_READY — fixture\n")), + ("dirty receipt", "workspace was not clean", + lambda d: d["after"].update(status="?? extra\n", untracked={"extra": None})), + ("tracked diff", "workspace was not clean", + lambda d: d["before"].update(tracked_diff_sha256="1" * 64)), + ("target scope", "authorization scope", + lambda d: d.update(immediate_target="feature/other")), + ("missing scope", "authorization scope", + lambda d: [d.pop(key) for key in ("root_target", "immediate_target")]), + ) + for name, message, change in cases: + with self.subTest(case=name): + data = json.loads(json.dumps(original)) + change(data) + data["output_sha256"] = hashlib.sha256(data["output"].encode()).hexdigest() + self.resign(data) + result = self.validate(wt) + self.assertNotEqual(result.returncode, 0) + self.assertIn(message, result.stderr) + if __name__ == "__main__": unittest.main() From 1e26b1cccdcfe8ac83a004915cad591d27eed91c Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 11:27:37 +0000 Subject: [PATCH 09/39] Record local recovery implementation results (U2) Why: Preserve worker transport outcomes and host verification evidence for U2. Evidence: 66 focused evidence and task-recovery tests passed on the recorded HEAD. Impact: Records SELF verification and the observed worker edit-serialization deviation; formal review and CI remain NOT_RUN. Refs: feature/norm-remediation-u2 --- .task/feature-norm-remediation-u2.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md index 4b57f5b8..4b7dbb76 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/feature-norm-remediation-u2.md @@ -30,6 +30,9 @@ | 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | |---|---|---|---|---|---|---|---|---|---|---| | U2 original | local recovery behavior | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | blocked | none | 0 | /tmp/norm-remediation-u2-receipt.json | +| U2 test-only | offline local precheck regression | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 30a582372b4c | 0 | /tmp/norm-remediation-u2-offline-test-receipt.json | +| U2 task precheck | local precheck fallback | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | d6df427e39e2 | 0 | /tmp/norm-remediation-u2-precheck-impl-receipt.json | +| U2 local receipt | resume-local receipt validation | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 210bb031d4ac | 0 | /tmp/norm-remediation-u2-evidence-receipt-2.json | ## 同步记录 @@ -42,12 +45,14 @@ | 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | |---|---|---|---|---|---| +| 2026-10-18T11:24Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8 | 0 | SELF | d196e73da7f33421 | ## 结果与验证(完成后填写) -- 结果:原 U2 attempt 被 fail-stop;新的 U2 attempt 将拆分为短小串行 packets,并沿用 user-authorized scope。 -- 验证:worker 报告基线聚焦测试 `python3 -m unittest tests/test_task_recovery.py tests/test_evidence.py` 为 PASS;实现后 `tests/test_task_recovery.py` 因该 worker 未写入的并发测试文件变化而 FAIL(9 failures)。这只是 worker 自检,不是集成验证;host 未运行测试,正式验证 `NOT_RUN`。 -- 影响与剩余项:执行器 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`。原始 CLI 事件确认,同一 Cursor session 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms;单 writer 约束缺失导致文件竞争,未发现另一条 agent-exec run 写该 worktree。该 attempt `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`,保留在 `/tmp/agent-exec-1000/af20482c3e24cc32f3873d1451043481/wt-b04896bf17a7` 作诊断证据。Development-Manager PR #26 已增加单 writer 提示并合入 `b178dec`;待父 initiative 更新 pin 后,使用新的短单元执行,不恢复原 attempt。 +- 结果:U2 两个实现切片与一项 test-only 切片均已通过成功 transport 集成;回归测试确认仅既有、登记且安全的任务可取得 `LOCAL_READY` 并续作,正式交付仍要求 `READY` 与最新目标同步。 +- 验证:host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 66/66 通过(SELF,HEAD `210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8`);worker 自检 evidence 48/48、recovery 18/18 通过。完整治理套件、CI、独立 Review、PR 与合入尚未运行(NOT_RUN)。 +- 执行偏差:原 U2 Cursor attempt 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms,并被 fail-stop;新的 evidence 单元明确要求串行编辑,但 worker 仍在同一批次向 `scripts/evidence.py` 发出两次编辑调用(未重叠,最终 diff 已审阅且验证通过)。不能据此声称提示已强制串行;后续规范计划须区分提示、可观测证据与工具级并发防护。 +- 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 ## 合入与清理 From 0815ab9f195324201e6f9bea91364eeac55499b2 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 11:42:52 +0000 Subject: [PATCH 10/39] Record final U2 focused test evidence Why: Preserve the final synchronized candidate's local verification receipt before closing its task card. Evidence: python3 -m unittest tests.test_evidence tests.test_task_recovery passed 66 tests on the synchronized candidate. Impact: SELF evidence only; independent review and CI remain required for formal delivery. Refs: feature/norm-remediation-u2 --- .task/feature-norm-remediation-u2.md | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.task/feature-norm-remediation-u2.md b/.task/feature-norm-remediation-u2.md index 4b7dbb76..2b58a556 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/feature-norm-remediation-u2.md @@ -143,3 +143,34 @@ "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/00e543451c4435f6669c74d51a6a678db2cd40a0498a56ae377447044adce7f8.json" } ``` + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T11:39:55.756192+00:00", + "command": "python3 -m unittest tests.test_evidence tests.test_task_recovery", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2", + "before_head": "1e26b1cccdcfe8ac83a004915cad591d27eed91c", + "after_head": "1e26b1cccdcfe8ac83a004915cad591d27eed91c", + "branch": "feature/norm-remediation-u2", + "target_branch": "main", + "target_head": "ba81d638a6c845d0acde6896f4001bed096ba26c", + "root_target": "main", + "immediate_target": "feature/norm-remediation", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "..................................................................", + "----------------------------------------------------------------------", + "Ran 66 tests in 18.749s", + "", + "OK" + ], + "output_sha256": "ed93f5670caa3576929bab841735c1ad873975985fcf1a6575ce88bad5510aa7", + "receipt_sha256": "d08508edc20bdd35b340355c5b1a9295ddce5f866d1706f34afc8f76dcf1d939", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/d08508edc20bdd35b340355c5b1a9295ddce5f866d1706f34afc8f76dcf1d939.json" +} +``` From e022948747c92d907cc11bfe412089055feb8599 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 11:44:00 +0000 Subject: [PATCH 11/39] Fold-close the task card before merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: close the task card inside its own PR so landing it needs no post-merge backfill or separate close PR. Evidence: .task/feature-norm-remediation-u2.md -> .task/archive/feature-norm-remediation-u2.md; status 已完成(合入前闭合); PR #618 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/618. Impact: task metadata only; the card is archived within PR #618 and lands with its merge commit. Refs: task.sh close --self --- .task/{ => archive}/feature-norm-remediation-u2.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename .task/{ => archive}/feature-norm-remediation-u2.md (96%) diff --git a/.task/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md similarity index 96% rename from .task/feature-norm-remediation-u2.md rename to .task/archive/feature-norm-remediation-u2.md index 2b58a556..95bc2b0f 100644 --- a/.task/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -1,6 +1,6 @@ # feature/norm-remediation-u2 任务卡 -任务主记录:;状态:进行中;owner:weichen +任务主记录:PR #618 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/618;状态:已完成(合入前闭合);owner:weichen - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= b5e23daed2f0ea46261979de1d62e4de0cfa88f9 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2(worktree);分支:feature/norm-remediation-u2 @@ -47,7 +47,7 @@ |---|---|---|---|---|---| | 2026-10-18T11:24Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8 | 0 | SELF | d196e73da7f33421 | -## 结果与验证(完成后填写) +## 结果与验证 - 结果:U2 两个实现切片与一项 test-only 切片均已通过成功 transport 集成;回归测试确认仅既有、登记且安全的任务可取得 `LOCAL_READY` 并续作,正式交付仍要求 `READY` 与最新目标同步。 - 验证:host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 66/66 通过(SELF,HEAD `210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8`);worker 自检 evidence 48/48、recovery 18/18 通过。完整治理套件、CI、独立 Review、PR 与合入尚未运行(NOT_RUN)。 @@ -174,3 +174,9 @@ "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/d08508edc20bdd35b340355c5b1a9295ddce5f866d1706f34afc8f76dcf1d939.json" } ``` + +## 闭合记录(合入前折叠) + +- 本卡在 PR #618(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/618)合入前闭合并归档,随该 PR 一同合入。 +- 该 PR 的合并提交即本任务的落地记录。 +- 合入后无需回填,也无需单独的闭合 PR。 From dcbd36656bb33831edc3987467f6b057db721f8b Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 11:53:49 +0000 Subject: [PATCH 12/39] agent-exec transport b091b4ca30479a9e3b114d35fa32630d/c9ea726dc405 --- tests/test_evidence.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_evidence.py b/tests/test_evidence.py index 08810a5f..871e65b0 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -512,6 +512,19 @@ class EvidenceTests(unittest.TestCase): lagging = self.validate(wt) self.assertEqual(lagging.returncode, 0, lagging.stderr) + def test_local_continuation_accepts_registered_task_without_remote_tracking_ref(self): + wt = self.local_task() + self.capture("make precheck", cwd=wt) + task_head = self.git("rev-parse", "HEAD", cwd=wt) + self.git("update-ref", "-d", "refs/remotes/origin/main") + # The R0 commit recorded in the task card must remain reachable locally + # even after the remote-tracking ref is removed. + self.git("cat-file", "-e", self.head) + result = self.validate(wt) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("local continuation validated", result.stdout) + self.assertEqual(self.git("rev-parse", "HEAD", cwd=wt), task_head) + def test_local_ready_never_authorizes_start_delivery_or_promoted_source(self): wt = self.local_task() self.capture("make precheck", cwd=wt) From a43aa171ea7f836ff1ec5f56c5f0ad67f9ade6ae Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 11:57:29 +0000 Subject: [PATCH 13/39] agent-exec transport 5a7bc332a82e8c04309dc5fcac926434/4bad7f41a3dc --- scripts/evidence.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/evidence.py b/scripts/evidence.py index afbbd4ca..e630f523 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -241,7 +241,7 @@ def read_receipt(filename): return value -def task_context(current=None): +def task_context(current=None, require_target=True): """Verify an existing task checkout; this is not an approval identity.""" current = snapshot() if current is None else current root = Path(git_text("rev-parse", "--show-toplevel")).resolve() @@ -270,10 +270,11 @@ def task_context(current=None): if len(bases) != 1 or bases[0][0] != current["target_branch"] or not GIT_SHA.fullmatch(bases[0][1]): raise EvidenceError("Task card has no valid target and initial baseline") r0 = bases[0][1] - if not current["target_head"]: + if not current["target_head"] and require_target: raise EvidenceError("Task target is unverified; fetch it first") for tip in (current["head"], current["target_head"]): - git("merge-base", "--is-ancestor", r0, tip) + if tip: + git("merge-base", "--is-ancestor", r0, tip) check = subprocess.run([sys.executable, str(Path(__file__).with_name("check-worktree.py")), "before-sync"], capture_output=True, text=True) if check.returncode: @@ -301,7 +302,7 @@ def validate_precheck(receipt, purpose="start"): if purpose != "resume-local" and after["target_head"] != current["target_head"]: raise EvidenceError("Precheck authoritative target has changed") if purpose.startswith("resume-"): - context = task_context(current) + context = task_context(current, require_target=purpose != "resume-local") try: Path(receipt["cwd"]).resolve().relative_to(context["root"]) except ValueError: @@ -311,10 +312,11 @@ def validate_precheck(receipt, purpose="start"): raise EvidenceError("Precheck belongs to a different task branch") git("merge-base", "--is-ancestor", context["r0"], after["target_head"]) if purpose == "resume-local": - try: - git("merge-base", "--is-ancestor", after["target_head"], current["target_head"]) - except EvidenceError: - raise EvidenceError("Precheck target history has rewound or diverged") from None + if current["target_head"]: + try: + git("merge-base", "--is-ancestor", after["target_head"], current["target_head"]) + except EvidenceError: + raise EvidenceError("Precheck target history has rewound or diverged") from None validate_local_scope(receipt, current, context) From eb6be71cfc348d487da9cceaa3a2745197081059 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 11:59:50 +0000 Subject: [PATCH 14/39] agent-exec transport ed920f07e90db7046ab42bec91f8095a/ed5fe66131b3 --- tests/test_evidence.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_evidence.py b/tests/test_evidence.py index 871e65b0..827c91ad 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -3,6 +3,7 @@ import hashlib import json import os from pathlib import Path +import re import subprocess import tempfile import unittest @@ -525,6 +526,31 @@ class EvidenceTests(unittest.TestCase): self.assertIn("local continuation validated", result.stdout) self.assertEqual(self.git("rev-parse", "HEAD", cwd=wt), task_head) + def test_local_precheck_captures_without_remote_tracking_ref(self): + wt = self.local_task() + head_before = self.git("rev-parse", "HEAD", cwd=wt) + # Remove the remote-tracking ref BEFORE capturing, so the precheck fixture + # itself must tolerate the missing refs/remotes/origin/main. The existing + # --local-context rejects this and the capture fails; recording that + # expected red result here. + self.git("update-ref", "-d", "refs/remotes/origin/main") + # The R0 commit recorded on the task card must remain reachable locally + # even after the remote-tracking ref is removed. + self.git("cat-file", "-e", self.head) + result = self.run_evidence("make precheck", cwd=wt, RECEIPT=self.receipt) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + self.assertTrue(self.receipt.exists(), "capture must write a receipt") + data = json.loads(self.receipt.read_text()) + self.assertEqual(data["exit_code"], 0, data["output"]) + self.assertIn("[verdict] LOCAL_READY", data["output"]) + states = re.findall(r"^\[task-state\] registered=(.+) initial_base=([0-9a-f]+)$", + data["output"], re.M) + self.assertEqual(len(states), 1, data["output"]) + registered, initial_base = states[0] + self.assertEqual(Path(registered).resolve(), wt.resolve()) + self.assertEqual(initial_base, self.head) + self.assertEqual(self.git("rev-parse", "HEAD", cwd=wt), head_before) + def test_local_ready_never_authorizes_start_delivery_or_promoted_source(self): wt = self.local_task() self.capture("make precheck", cwd=wt) From d2986f1f5fee0e0ad151fd813b9306873131c805 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 12:03:23 +0000 Subject: [PATCH 15/39] agent-exec transport 2633f16b2b409f0873abdc9c2fc89d4f/40f7d143ac3c --- scripts/evidence.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/evidence.py b/scripts/evidence.py index e630f523..e53f6861 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -297,7 +297,8 @@ def validate_precheck(receipt, purpose="start"): raise EvidenceError("Precheck object capture is incomplete") if receipt["before"]["head"] != current["head"] or after["head"] != current["head"]: raise EvidenceError("Precheck source HEAD has changed") - if after["target_branch"] != current["target_branch"] or not after["target_head"]: + if after["target_branch"] != current["target_branch"] or ( + not after["target_head"] and purpose != "resume-local"): raise EvidenceError("Precheck authoritative target has changed") if purpose != "resume-local" and after["target_head"] != current["target_head"]: raise EvidenceError("Precheck authoritative target has changed") @@ -310,9 +311,10 @@ def validate_precheck(receipt, purpose="start"): if any(obj["branch"] != current["branch"] or obj["target_branch"] != current["target_branch"] for obj in (receipt["before"], after)): raise EvidenceError("Precheck belongs to a different task branch") - git("merge-base", "--is-ancestor", context["r0"], after["target_head"]) + if after["target_head"]: + git("merge-base", "--is-ancestor", context["r0"], after["target_head"]) if purpose == "resume-local": - if current["target_head"]: + if after["target_head"] and current["target_head"]: try: git("merge-base", "--is-ancestor", after["target_head"], current["target_head"]) except EvidenceError: @@ -466,7 +468,7 @@ def main(): if os.environ.get("SRC", "SELF") not in ("", "SELF"): raise EvidenceError("Local evidence is SELF; GATE/REPLAY require external execution identity") if sys.argv[1:] == ["--local-context"]: - context = task_context() + context = task_context(require_target=False) print("[task-state] registered=" + str(context["root"]) + " initial_base=" + context["r0"]) return 0 if sys.argv[1:]: From 74e4b06e5e8b1c4e4484f082256b5cb5d1df74c2 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 12:13:18 +0000 Subject: [PATCH 16/39] agent-exec transport fc1ce3b002ab4e66a7985a9417b6839b/73e49d7b97cc --- policy/governance.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/policy/governance.md b/policy/governance.md index faf4b235..a869f17e 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -53,10 +53,13 @@ TASK_WORK_MODE=local make task-resume PRECHECK_RECEIPT=/tmp/my-local-resume.json ``` LOCAL_READY 只用于局部恢复;新任务入口始终校验严格 READY,正式验收与合入另行同步。 -恢复核验实际 worktree/卡片/分支、R0 祖先关系及源码外原始收据;脏现场、Git 操作、组件漂移和未知远端仍拒绝。 +恢复核验实际 worktree/卡片/分支、R0 祖先关系及源码外原始收据;脏现场、Git 操作和组件漂移仍拒绝。 `resume-local` 只接受 LOCAL_READY 收据,且须同时满足:卡片位于本 worktree 的 `.task/` 且状态未以“已”开头 (已完成/已放弃等闭合任务拒绝);收据内 `[task-state]` 的登记路径与 R0 等于当前登记任务;收据前后与当前 HEAD 一致、工作区干净且组件状态未变;收据 `root_target/immediate_target` 等于卡片范围;目标只允许快进落后,回退或分叉拒绝。 +远端跟踪目标 ref 缺失时,local 模式的 precheck 与 `resume-local` 只在当前登记任务身份、本地 R0 对象、R0 为本地 HEAD 祖先、 +工作区干净、分支/worktree、组件与范围全部核验通过时继续。缺失或过期的远端 ref 从不证明新鲜度;只要有任一目标历史可用, +其祖先关系须一致。正式 Review 与合入仍须先完成新鲜的权威同步。 `resume-delivery` 仍要求 READY 且收据目标等于当前权威目标。LOCAL_READY 始终是 SELF 本地工作记录, 不能用于建任务、正式交付、Review、合入、CI、GATE 或 REPLAY。 旧收据允许作为历史证据原样导入,不自动成为当前恢复或交付准入;格式继续为 schema 1。 From c88311bbf64bc58aeb91f3d0dc7d85701d45a7b7 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 12:15:07 +0000 Subject: [PATCH 17/39] Record missing-ref coverage and final U2 tests Why: Capture the final offline recovery cases and host verification on the PR candidate. Evidence: 68 focused evidence and task-recovery tests pass; two broader task-start failures reproduce on the parent baseline. Impact: Local evidence remains SELF; exact-head CI and independent review are still required. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index 95bc2b0f..4f7ef8a2 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -33,6 +33,11 @@ | U2 test-only | offline local precheck regression | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 30a582372b4c | 0 | /tmp/norm-remediation-u2-offline-test-receipt.json | | U2 task precheck | local precheck fallback | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | d6df427e39e2 | 0 | /tmp/norm-remediation-u2-precheck-impl-receipt.json | | U2 local receipt | resume-local receipt validation | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 210bb031d4ac | 0 | /tmp/norm-remediation-u2-evidence-receipt-2.json | +| U2 missing-ref test | resume-local with missing tracking ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | dcbd36656bb3 | 0 | /tmp/norm-remediation-u2-no-tracking-test-receipt.json | +| U2 missing-ref fix | local receipt with missing tracking ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | a43aa171ea7f | 0 | /tmp/norm-remediation-u2-no-tracking-fix-receipt.json | +| U2 offline-capture test | capture LOCAL_READY without tracking ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | eb6be71cfc34 | 0 | /tmp/norm-remediation-u2-no-tracking-capture-test-receipt.json | +| U2 offline-capture fix | capture and resume without tracking ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | d2986f1f5fee | 0 | /tmp/norm-remediation-u2-offline-capture-fix-receipt.json | +| U2 offline-recovery docs | clarify missing-ref local recovery | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 74e4b06e5e8b | 0 | /tmp/norm-remediation-u2-offline-doc-receipt.json | ## 同步记录 @@ -46,12 +51,15 @@ | 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | |---|---|---|---|---|---| | 2026-10-18T11:24Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8 | 0 | SELF | d196e73da7f33421 | +| 2026-10-18T12:07Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | d2986f1f5fee0e0ad151fd813b9306873131c805 | 0 | SELF | 85197f351f8458a8 | +| 2026-10-18T12:03Z | `python3 -m unittest tests.test_task tests.test_task_recovery tests.test_evidence` | d2986f1f5fee0e0ad151fd813b9306873131c805 | 1 | SELF | 9d19d33489e15d4d | ## 结果与验证 - 结果:U2 两个实现切片与一项 test-only 切片均已通过成功 transport 集成;回归测试确认仅既有、登记且安全的任务可取得 `LOCAL_READY` 并续作,正式交付仍要求 `READY` 与最新目标同步。 -- 验证:host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 66/66 通过(SELF,HEAD `210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8`);worker 自检 evidence 48/48、recovery 18/18 通过。完整治理套件、CI、独立 Review、PR 与合入尚未运行(NOT_RUN)。 +- 验证:最终改动后 host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 68/68 通过(SELF,HEAD `d2986f1f5fee0e0ad151fd813b9306873131c805`);worker 自检 evidence 50/50 通过。扩展套件 `tests.test_task tests.test_task_recovery tests.test_evidence` 共 162 项中 160 项通过,`tests.test_task` 的两项 planned-child 启动测试失败;同两项在干净父基线 `74bd05a876136ed8506332c76d535a6eb6356433` 上复现,判为既有基线失败,与本次 local-only 改动无关。PR #618 旧 head `e022948747c92d907cc11bfe412089055feb8599` 的 GATE `ci / test` 已通过;后续提交会改变 head,最终 candidate CI 必须重跑。独立 Review 与合入仍 NOT_RUN。 - 执行偏差:原 U2 Cursor attempt 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms,并被 fail-stop;新的 evidence 单元明确要求串行编辑,但 worker 仍在同一批次向 `scripts/evidence.py` 发出两次编辑调用(未重叠,最终 diff 已审阅且验证通过)。不能据此声称提示已强制串行;后续规范计划须区分提示、可观测证据与工具级并发防护。 +- 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 - 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 ## 合入与清理 From d5b8e74c43062575a148548c5cc7a84627ef599b Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 12:31:40 +0000 Subject: [PATCH 18/39] agent-exec transport ba8f131d4f2b8d718c2fa2b84ec799b9/b503d085cc5b --- tests/test_evidence.py | 30 ++++++++++++++++++++++ tests/test_task_recovery.py | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/tests/test_evidence.py b/tests/test_evidence.py index 827c91ad..f37c1209 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -526,6 +526,36 @@ class EvidenceTests(unittest.TestCase): self.assertIn("local continuation validated", result.stdout) self.assertEqual(self.git("rev-parse", "HEAD", cwd=wt), task_head) + def test_local_continuation_accepts_when_target_ref_reappears(self): + """A resume-local receipt captured with no remote-tracking target ref + validates after a forward target ref reappears, provided R0 stays locally + reachable and the reappeared target is a forward descendant of R0. + + The receipt records target_head=None; the rewind check is skipped because + either side is missing, and the reappeared ref (a forward descendant of R0) + must satisfy the ancestry guard that the registered task is still current.""" + wt = self.local_task() + head_before = self.git("rev-parse", "HEAD", cwd=wt) + # Drop refs/remotes/origin/main BEFORE capture so the receipt records + # target_head=None. R0 (the recorded initial baseline) must remain + # reachable as a local object. + self.git("update-ref", "-d", "refs/remotes/origin/main") + self.git("cat-file", "-e", self.head) + self.capture("make precheck", cwd=wt) + captured = json.loads(self.receipt.read_text()) + self.assertIsNone(captured["after"]["target_head"]) + # Recreate refs/remotes/origin/main as a forward descendant of R0. + forward = self.git("commit-tree", "-p", self.head, + "-m", "Forward target ref reappeared", self.head + "^{tree}") + self.git("update-ref", "refs/remotes/origin/main", forward) + # Validation must succeed: receipt has no target_head (rewind check is + # skipped) and the reappeared target descends from R0 (ancestry guard + # accepts a forward target ref). + result = self.validate(wt) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("local continuation validated", result.stdout) + self.assertEqual(self.git("rev-parse", "HEAD", cwd=wt), head_before) + def test_local_precheck_captures_without_remote_tracking_ref(self): wt = self.local_task() head_before = self.git("rev-parse", "HEAD", cwd=wt) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 50ac0cfa..86ed8b96 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -144,6 +144,56 @@ class TaskRecoveryTests(unittest.TestCase): self.assertNotIn("[verdict] LOCAL_READY", delivery.stdout) self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + def test_local_precheck_rejects_closed_task(self): + """A closed registered task (已完成(合入前闭合)) must not advertise LOCAL_READY. + + The current local precheck emits LOCAL_READY for any registered in-progress + card text and never inspects the card status, so this test records the + expected red result: the verdict is wrong and the exit code is wrong.""" + f = self.fixture + self.card.write_text(self.card.read_text().replace("状态:进行中", + "状态:已完成(合入前闭合)")) + self.save_card_change() + self.assertIn("已完成(合入前闭合)", self.card.read_text()) + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + local = self.precheck() + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + + def test_local_precheck_rejects_reachable_target_rewind(self): + """Origin is reachable but its target was rewound after the task cached a + newer descendant. Local precheck must not emit LOCAL_READY under that + divergence — the cached refs/remotes/origin/main is no longer trustworthy. + + The current implementation lets fetch_target() surface a non-FFREMOTE_UNVERIFIED + line, then unconditionally prints LOCAL_READY in local mode. The exit code + is wrong and the verdict is wrong; this test records both expected failures.""" + f = self.fixture + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # (1) Advance the remote and let local precheck refresh the cached origin/main. + f.advance_remote() + refresh = self.precheck() + self.assertEqual(refresh.returncode, 0, refresh.stdout) + forward = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + self.assertNotEqual(forward, r0) + # (2) Rewind the bare remote target back to R0; the worktree cache still + # points at the forward descendant because we have not fetched since. + f.git(f.remote, "update-ref", "refs/heads/main", r0, forward) + self.assertEqual(f.git(f.remote, "rev-parse", "refs/heads/main").stdout.strip(), r0) + self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward) + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + # (3) Local precheck sees a reachable origin whose target is older than the + # cache. fetch_target() refuses the non-fast-forward update; the precheck + # must still refuse LOCAL_READY. + local = self.precheck() + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + # After the failed fetch the cache still points at the forward descendant + # so a follow-up run can repeat the same check. + self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward) + def test_unknown_work_mode_is_rejected(self): result = self.precheck(mode="anything") self.assertNotEqual(result.returncode, 0, result.stdout) From 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 12:41:18 +0000 Subject: [PATCH 19/39] agent-exec transport a41de1816057eb4de14b0175a36c9c68/dfebc3695180 --- scripts/evidence.py | 15 +++++++++++---- scripts/task.sh | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/scripts/evidence.py b/scripts/evidence.py index e53f6861..3e3d7e92 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -322,12 +322,17 @@ def validate_precheck(receipt, purpose="start"): validate_local_scope(receipt, current, context) -def validate_local_scope(receipt, current, context): - """LOCAL_READY continues one active registered task; it remains SELF local-work evidence only.""" +def require_active_task(root): + """Closed, archived or abandoned cards cannot authorize local continuation.""" card = find_card() status = re.search(r";状态:\s*([^;\s][^;\n]*)", card.read_text(encoding="utf-8")) - if card.resolve().parent != context["root"] / ".task" or not status or status.group(1).startswith("已"): + if card.resolve().parent != root / ".task" or not status or status.group(1).startswith("已"): raise EvidenceError("Local continuation requires an active registered task") + + +def validate_local_scope(receipt, current, context): + """LOCAL_READY continues one active registered task; it remains SELF local-work evidence only.""" + require_active_task(context["root"]) states = TASK_STATE.findall(receipt["output"]) if len(states) != 1 or Path(states[0][0]) != context["root"] or states[0][1] != context["r0"]: raise EvidenceError("Precheck belongs to a different task identity or initial baseline") @@ -467,8 +472,10 @@ def main(): try: if os.environ.get("SRC", "SELF") not in ("", "SELF"): raise EvidenceError("Local evidence is SELF; GATE/REPLAY require external execution identity") - if sys.argv[1:] == ["--local-context"]: + if sys.argv[1:] in (["--local-context"], ["--local-context", "--active"]): context = task_context(require_target=False) + if sys.argv[2:]: + require_active_task(context["root"]) print("[task-state] registered=" + str(context["root"]) + " initial_base=" + context["r0"]) return 0 if sys.argv[1:]: diff --git a/scripts/task.sh b/scripts/task.sh index 2d908fd8..02a0607d 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -18,6 +18,15 @@ fetch_target() { git rev-parse --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null \ || { echo "[verdict] REMOTE_UNVERIFIED — target commit missing" >&2; return 6; } } +target_rewound() { # 0 仅当 origin 通告的目标提交本地可用且不是缓存 origin/ 的后代;不可达或无法比较返回 1 + local cached advertised rc=0 + cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || return 1 + advertised=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>/dev/null \ + | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') || return 1 + [ -n "$advertised" ] && git cat-file -e "${advertised}^{commit}" 2>/dev/null || return 1 + git merge-base --is-ancestor "$cached" "$advertised" 2>/dev/null || rc=$? + [ "$rc" -eq 1 ] +} 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() { @@ -140,7 +149,11 @@ cmd_precheck() { exit 3 fi if [ "$mode" = local ]; then - python3 "${SCRIPT_DIR}/evidence.py" --local-context || return 2 + python3 "${SCRIPT_DIR}/evidence.py" --local-context --active || return 2 + if [ "$remote_fresh" -eq 0 ] && target_rewound; then + echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the cached target; preserve local work and reconcile the target history" + return 6 + fi [ "$remote_fresh" -eq 1 ] || echo "[remote-freshness] unavailable — LOCAL_READY rests on local task context only" echo "[verdict] LOCAL_READY — existing task may continue authorized local work; final delivery still requires latest-target sync and checks" return 0 From f2c24015b09cd393db70fcc4a7286ae4ae07b582 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 12:52:11 +0000 Subject: [PATCH 20/39] Record U2 review fixes and governance tests Why: Preserve the closed-task and remote-target rewind corrections with their regression and governance test evidence. Evidence: 71 focused tests and all 482 governance tests passed; the previous exact-head CI failed in an unrelated backend runtime test and requires a fresh run. Impact: Review findings are fixed in scope; independent final review and current-head CI remain required. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index 4f7ef8a2..e534f5ba 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -38,6 +38,8 @@ | U2 offline-capture test | capture LOCAL_READY without tracking ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | eb6be71cfc34 | 0 | /tmp/norm-remediation-u2-no-tracking-capture-test-receipt.json | | U2 offline-capture fix | capture and resume without tracking ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | d2986f1f5fee | 0 | /tmp/norm-remediation-u2-offline-capture-fix-receipt.json | | U2 offline-recovery docs | clarify missing-ref local recovery | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 74e4b06e5e8b | 0 | /tmp/norm-remediation-u2-offline-doc-receipt.json | +| U2 review regression tests | closed task, reachable rewind, reappearing target | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | d5b8e74c4306 | 0 | /tmp/norm-remediation-u2-review-fix-tests-receipt.json | +| U2 review fixes | reject closed task and known target rewind/divergence | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 05a9a3bc89b6 | 0 | /tmp/norm-remediation-u2-review-fixes-receipt.json | ## 同步记录 @@ -53,12 +55,16 @@ | 2026-10-18T11:24Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 210bb031d4acc5b9bde6cde66e7fa0f735bcf9f8 | 0 | SELF | d196e73da7f33421 | | 2026-10-18T12:07Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | d2986f1f5fee0e0ad151fd813b9306873131c805 | 0 | SELF | 85197f351f8458a8 | | 2026-10-18T12:03Z | `python3 -m unittest tests.test_task tests.test_task_recovery tests.test_evidence` | d2986f1f5fee0e0ad151fd813b9306873131c805 | 1 | SELF | 9d19d33489e15d4d | +| 2026-10-18T12:42Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 2d19085b0725fc46 | +| 2026-10-18T12:43Z | `make test-governance` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 018194c0a804d1b9 | ## 结果与验证 -- 结果:U2 两个实现切片与一项 test-only 切片均已通过成功 transport 集成;回归测试确认仅既有、登记且安全的任务可取得 `LOCAL_READY` 并续作,正式交付仍要求 `READY` 与最新目标同步。 -- 验证:最终改动后 host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 68/68 通过(SELF,HEAD `d2986f1f5fee0e0ad151fd813b9306873131c805`);worker 自检 evidence 50/50 通过。扩展套件 `tests.test_task tests.test_task_recovery tests.test_evidence` 共 162 项中 160 项通过,`tests.test_task` 的两项 planned-child 启动测试失败;同两项在干净父基线 `74bd05a876136ed8506332c76d535a6eb6356433` 上复现,判为既有基线失败,与本次 local-only 改动无关。PR #618 旧 head `e022948747c92d907cc11bfe412089055feb8599` 的 GATE `ci / test` 已通过;后续提交会改变 head,最终 candidate CI 必须重跑。独立 Review 与合入仍 NOT_RUN。 +- 结果:U2 允许既有安全任务在真正远端不可用或目标跟踪 ref 缺失时继续有界本地工作;闭合任务和已证实的远端 target 回退/分叉均拒绝 `LOCAL_READY`。新任务和正式交付仍要求严格 READY/权威同步。 +- 验证:最终修复后 host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 71/71 通过(SELF,HEAD `05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71`);worker evidence 自检 51/51、task recovery 20/20;`make test-governance` 482 项通过、18 项 skip。先前 PR CI run 3884/job 6150 在 `make test` 的 Go 后端测试 `TestToolCallDeltaBeforeExecution` 失败(`modules/backend/internal/runtime/llm_test.go:1084`,事件顺序为 `[tool.call.delta tool.call tool.result]`);该文件不在变更范围,旧 head `e022948...` GATE 曾通过。当前 head 已不同,须在最终提交上重跑 CI。 +- Review 发现与闭合:正确性审阅指出闭合任务曾能在 precheck 收到 LOCAL_READY;可靠性审阅指出可达远端回退可能被当作离线。已分别加入红灯回归并修复。测试审阅提出的目标 ref 重现和 receipt target branch 错配边界也已覆盖。最终候选独立 Review 待完成。 - 执行偏差:原 U2 Cursor attempt 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms,并被 fail-stop;新的 evidence 单元明确要求串行编辑,但 worker 仍在同一批次向 `scripts/evidence.py` 发出两次编辑调用(未重叠,最终 diff 已审阅且验证通过)。不能据此声称提示已强制串行;后续规范计划须区分提示、可观测证据与工具级并发防护。 +- 新增执行偏差:修复闭合任务与目标回退时,worker 在同一批次向 `scripts/evidence.py` 和 `scripts/task.sh` 发出两次互不重叠编辑,违反了任务包的串行编辑要求;最终 diff 已逐项审阅,定向与完整治理测试均通过,但该结果不能证明提示具备工具级强制力。 - 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 - 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 From 2e0da0dadb73db1b6a70f8545dbfd5df3a8e4d00 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 13:00:18 +0000 Subject: [PATCH 21/39] agent-exec transport 15c16d890ff16327f11293fe6ecbfdde/f7c42d8ea545 --- tests/test_evidence.py | 28 +++++++++++++++++++ tests/test_task_recovery.py | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/tests/test_evidence.py b/tests/test_evidence.py index f37c1209..fac6bae3 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -659,6 +659,34 @@ class EvidenceTests(unittest.TestCase): self.assertNotEqual(result.returncode, 0) self.assertIn(message, result.stderr) + def test_local_continuation_rejects_receipt_target_branch_mismatch(self): + """Resume-local must refuse a receipt whose before.target_branch or + after.target_branch points at a different branch than the registered + task target. Re-sign the fixture receipt with the existing helper so + the digest still validates, and assert the existing production + rejection that flags the receipt as belonging to a different task + branch (or, when the after snapshot already disagrees, the + authoritative-target-changed guard that fires first).""" + wt = self.local_task() + original = self.capture("make precheck", cwd=wt) + cases = ( + ("before.target_branch", + "Precheck belongs to a different task branch", + lambda d: d["before"].update(target_branch="feature/other")), + ("after.target_branch", + "Precheck authoritative target has changed", + lambda d: d["after"].update(target_branch="feature/other")), + ) + for name, message, change in cases: + with self.subTest(case=name): + data = json.loads(json.dumps(original)) + change(data) + data["output_sha256"] = hashlib.sha256(data["output"].encode()).hexdigest() + self.resign(data) + result = self.validate(wt) + self.assertNotEqual(result.returncode, 0) + self.assertIn(message, result.stderr) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 86ed8b96..57cdfbfe 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -194,6 +194,61 @@ class TaskRecoveryTests(unittest.TestCase): # so a follow-up run can repeat the same check. self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward) + def test_local_precheck_rejects_reachable_target_divergence(self): + """Origin is reachable but its target was replaced by a divergent commit + that shares R0 with the cached forward target without descending from it. + Local precheck must refuse LOCAL_READY and surface the divergence so the + owner can reconcile the target history instead of trusting the cache. + + Both the cached forward target and the divergent commit are locally + available so target_rewound() can compare them via merge-base.""" + f = self.fixture + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # (1) Advance the remote and let local precheck refresh the cached origin/main. + f.advance_remote() + refresh = self.precheck() + self.assertEqual(refresh.returncode, 0, refresh.stdout) + forward = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + self.assertNotEqual(forward, r0) + # (2) Build a divergent commit on top of R0 in the main checkout; it shares + # R0 with the cached forward target but does not descend from it. + f.git(f.repo, "switch", "--detach", r0) + (f.repo / "divergent-main").write_text("different target history\n") + f.git(f.repo, "add", "divergent-main") + f.git(f.repo, "commit", "-m", "Create divergent fixture history") + divergent = f.git(f.repo, "rev-parse", "HEAD").stdout.strip() + # Both targets must share R0 (otherwise they would just be unrelated refs). + self.assertEqual(f.git(f.repo, "merge-base", forward, divergent).stdout.strip(), r0) + # The cached forward target must NOT be an ancestor of the divergent commit. + self.assertEqual(f.git(f.repo, "merge-base", "--is-ancestor", forward, + divergent, check=False).returncode, 1) + # Both commits must be locally available so target_rewound() can inspect them. + self.assertEqual(f.git(self.wt, "cat-file", "-e", forward + "^{commit}", + check=False).returncode, 0) + self.assertEqual(f.git(self.wt, "cat-file", "-e", divergent + "^{commit}", + check=False).returncode, 0) + # (3) Push the divergent commit so the bare remote's object database sees it, + # then replace refs/heads/main on the bare remote with the divergent commit. + f.git(f.repo, "push", "origin", "HEAD:refs/heads/divergent") + f.git(f.remote, "update-ref", "refs/heads/main", divergent, forward) + self.assertEqual(f.git(f.remote, "rev-parse", "refs/heads/main").stdout.strip(), divergent) + # The worktree cache still points at the forward descendant because we have + # not fetched since. + self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward) + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + # (4) Local precheck: fetch_target() refuses the non-fast-forward update to + # the divergent commit, target_rewound() then surfaces the divergence, and + # the precheck must not advertise LOCAL_READY. + local = self.precheck() + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertIn("rewound or diverged", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + # After the failed fetch the cache still points at the forward descendant + # so a follow-up run can repeat the same check. + self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward) + def test_unknown_work_mode_is_rejected(self): result = self.precheck(mode="anything") self.assertNotEqual(result.returncode, 0, result.stdout) From b492bea7c37c725a761081e16485a000e4d41879 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 13:08:30 +0000 Subject: [PATCH 22/39] Record U2 final edge coverage and governance run Why: Preserve the final test matrix and review corrections for the U2 candidate. Evidence: All 484 governance tests pass (18 skipped); the targeted evidence and recovery cases pass on the same implementation. Impact: SELF evidence only; final-head CI and independent review remain required. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index e534f5ba..f9d5777a 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -40,6 +40,7 @@ | U2 offline-recovery docs | clarify missing-ref local recovery | caefcb72 | implementation | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 74e4b06e5e8b | 0 | /tmp/norm-remediation-u2-offline-doc-receipt.json | | U2 review regression tests | closed task, reachable rewind, reappearing target | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | d5b8e74c4306 | 0 | /tmp/norm-remediation-u2-review-fix-tests-receipt.json | | U2 review fixes | reject closed task and known target rewind/divergence | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 05a9a3bc89b6 | 0 | /tmp/norm-remediation-u2-review-fixes-receipt.json | +| U2 final edge tests | target divergence and receipt target branch mismatch | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 2e0da0dadb73 | 0 | /tmp/norm-remediation-u2-final-edge-tests-receipt.json | ## 同步记录 @@ -57,14 +58,16 @@ | 2026-10-18T12:03Z | `python3 -m unittest tests.test_task tests.test_task_recovery tests.test_evidence` | d2986f1f5fee0e0ad151fd813b9306873131c805 | 1 | SELF | 9d19d33489e15d4d | | 2026-10-18T12:42Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 2d19085b0725fc46 | | 2026-10-18T12:43Z | `make test-governance` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 018194c0a804d1b9 | +| 2026-10-18T13:01Z | `make test-governance` | 2e0da0dadb73db1b6a70f8545dbfd5df3a8e4d00 | 0 | SELF | a44db826c4be80fb | ## 结果与验证 - 结果:U2 允许既有安全任务在真正远端不可用或目标跟踪 ref 缺失时继续有界本地工作;闭合任务和已证实的远端 target 回退/分叉均拒绝 `LOCAL_READY`。新任务和正式交付仍要求严格 READY/权威同步。 -- 验证:最终修复后 host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 71/71 通过(SELF,HEAD `05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71`);worker evidence 自检 51/51、task recovery 20/20;`make test-governance` 482 项通过、18 项 skip。先前 PR CI run 3884/job 6150 在 `make test` 的 Go 后端测试 `TestToolCallDeltaBeforeExecution` 失败(`modules/backend/internal/runtime/llm_test.go:1084`,事件顺序为 `[tool.call.delta tool.call tool.result]`);该文件不在变更范围,旧 head `e022948...` GATE 曾通过。当前 head 已不同,须在最终提交上重跑 CI。 -- Review 发现与闭合:正确性审阅指出闭合任务曾能在 precheck 收到 LOCAL_READY;可靠性审阅指出可达远端回退可能被当作离线。已分别加入红灯回归并修复。测试审阅提出的目标 ref 重现和 receipt target branch 错配边界也已覆盖。最终候选独立 Review 待完成。 +- 验证:host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 71/71 通过(SELF,HEAD `05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71`);最新 `make test-governance` 484 项通过、18 项 skip(SELF,HEAD `2e0da0dadb73db1b6a70f8545dbfd5df3a8e4d00`)。worker 自检包含 `tests.test_evidence` 52/52、`tests.test_task_recovery` 21/21。先前 PR CI run 3884/job 6150 在 `make test` 的 Go 后端测试 `TestToolCallDeltaBeforeExecution` 失败(`modules/backend/internal/runtime/llm_test.go:1084`,事件顺序为 `[tool.call.delta tool.call tool.result]`);该文件不在变更范围;随后 head `f2c2401...` 的 `ci / test` GATE 通过。最终 head `2e0da0d...` 仍需重跑 CI。 +- Review 发现与闭合:正确性审阅指出闭合任务曾能在 precheck 收到 LOCAL_READY;可靠性审阅指出可达远端回退/分叉可能被当作离线。已分别加入 test-first 回归并修复。测试审阅要求的目标 ref 重现、receipt target branch 错配及可达远端 target 分叉场景也已覆盖。项目规范/可维护性审阅无 finding。最终 head 的完整 Review 仍在进行。 - 执行偏差:原 U2 Cursor attempt 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms,并被 fail-stop;新的 evidence 单元明确要求串行编辑,但 worker 仍在同一批次向 `scripts/evidence.py` 发出两次编辑调用(未重叠,最终 diff 已审阅且验证通过)。不能据此声称提示已强制串行;后续规范计划须区分提示、可观测证据与工具级并发防护。 - 新增执行偏差:修复闭合任务与目标回退时,worker 在同一批次向 `scripts/evidence.py` 和 `scripts/task.sh` 发出两次互不重叠编辑,违反了任务包的串行编辑要求;最终 diff 已逐项审阅,定向与完整治理测试均通过,但该结果不能证明提示具备工具级强制力。 +- Review 路由记录:第一次 PR 对抗性 cross-model job 因 orchestrator 使用了不匹配的约束文件名,在 provider egress 前跳过,未把代码发送给外部模型;job 目录已清理。该审阅不计入 Review 覆盖,最终 head 使用本地 adversarial reviewer。 - 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 - 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 From 95d17188585c74e2e1470952876d52b8ea9466c1 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 13:19:42 +0000 Subject: [PATCH 23/39] agent-exec transport c8c8cc86667d1b3309eb41b4a2808131/d60aee058c18 --- tests/test_task_recovery.py | 80 +++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 57cdfbfe..f2627c1d 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -1,6 +1,8 @@ """Local recovery is task-scoped and cannot grant delivery readiness.""" import json import os +import shlex +import subprocess import sys import unittest @@ -389,6 +391,84 @@ class TaskRecoveryTests(unittest.TestCase): "VALIDATE_RECEIPT_PURPOSE": "ignore"}) self.assertNotEqual(result.returncode, 0, result.stdout) + def test_local_precheck_rejects_reachable_uncomparable_target(self): + """Origin advertises a target SHA but the advertised object is + unavailable locally, so ancestry cannot be compared against the + cached refs/remotes/origin/main. Local precheck must not advertise + LOCAL_READY under that unverifiable state — the cached target + cannot be trusted as authoritative without an ancestry check. + + A disposable test-only git wrapper fails `fetch`, makes + `ls-remote origin refs/heads/main` advertise a synthetic SHA, and + makes `cat-file -e ^{commit}` fail; all other git calls + delegate to the real binary so the registered in-progress task + context stays observable. + + The current implementation lets target_rewound() bail out at the + cat-file -e step (return 1), then unconditionally prints + LOCAL_READY in local mode because the `&& target_rewound` guard + short-circuits. The verdict is wrong; this test records the + expected red result.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + cached_before = f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout + + # Find the real git to delegate everything but the three intercepted calls. + real_git = subprocess.check_output( + ["bash", "-c", "command -v git"], env=f.env, text=True).strip() + synthetic = "1" * 40 + bin_dir = f.root / "uncomparable-bin" + bin_dir.mkdir(exist_ok=True) + wrapper = bin_dir / "git" + wrapper.write_text( + "#!/usr/bin/env bash\n" + "# Disposable fixture wrapper for the reachable-but-uncomparable\n" + "# scenario: `fetch` fails, `ls-remote origin refs/heads/main`\n" + "# advertises a synthetic SHA, `cat-file -e ^{commit}`\n" + "# fails. Anything else delegates to the real git so the\n" + "# registered task context is observable normally.\n" + f"REAL_GIT={shlex.quote(real_git)}\n" + f"SYNTHETIC={shlex.quote(synthetic)}\n" + "WRAPPER_BIN=$(cd \"$(dirname \"$0\")\" && pwd)\n" + "stripped=\"\"\n" + "old_ifs=\"$IFS\"\n" + "IFS=':'\n" + "for p in $PATH; do\n" + " [ \"$p\" = \"$WRAPPER_BIN\" ] && continue\n" + " if [ -z \"$stripped\" ]; then stripped=\"$p\"; else stripped=\"$stripped:$p\"; fi\n" + "done\n" + "IFS=\"$old_ifs\"\n" + "export PATH=\"$stripped\"\n" + "case \"$1\" in\n" + " fetch)\n" + " echo 'fatal: unable to access origin' >&2\n" + " exit 128 ;;\n" + " ls-remote)\n" + " if [ \"${2:-}\" = origin ] && [ \"${3:-}\" = refs/heads/main ]; then\n" + " printf '%s\\trefs/heads/main\\n' \"$SYNTHETIC\"\n" + " exit 0\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + " cat-file)\n" + " if [ \"${2:-}\" = -e ] && [ \"${3:-}\" = \"${SYNTHETIC}^{commit}\" ]; then\n" + " echo \"fatal: Not a valid object name ${SYNTHETIC}^{commit}.\" >&2\n" + " exit 1\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + "esac\n" + "exec \"$REAL_GIT\" \"$@\"\n") + wrapper.chmod(0o755) + wrapper_path = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": wrapper_path} + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + self.assertEqual(f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout, + cached_before) + if __name__ == "__main__": unittest.main() From 9c0e7657083259759f5bd1bed6921471411dba2c Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 13:37:32 +0000 Subject: [PATCH 24/39] agent-exec transport 3d1dd4ef1d7a2e761c3af308dc662a98/0cfaf24c9ad2 --- policy/governance.md | 4 +++- scripts/task.sh | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/policy/governance.md b/policy/governance.md index a869f17e..93bbe34b 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -59,7 +59,9 @@ LOCAL_READY 只用于局部恢复;新任务入口始终校验严格 READY, 一致、工作区干净且组件状态未变;收据 `root_target/immediate_target` 等于卡片范围;目标只允许快进落后,回退或分叉拒绝。 远端跟踪目标 ref 缺失时,local 模式的 precheck 与 `resume-local` 只在当前登记任务身份、本地 R0 对象、R0 为本地 HEAD 祖先、 工作区干净、分支/worktree、组件与范围全部核验通过时继续。缺失或过期的远端 ref 从不证明新鲜度;只要有任一目标历史可用, -其祖先关系须一致。正式 Review 与合入仍须先完成新鲜的权威同步。 +其祖先关系须一致。local precheck 刷新失败时,远端不可查询或目标 ref 缺失按上述本地核验继续;远端可查询且已有缓存目标时, +通告提交须本地存在且为缓存目标后代才继续,回退/分叉或通告提交本地缺失无法比较均判 REMOTE_UNVERIFIED。 +正式 Review 与合入仍须先完成新鲜的权威同步。 `resume-delivery` 仍要求 READY 且收据目标等于当前权威目标。LOCAL_READY 始终是 SELF 本地工作记录, 不能用于建任务、正式交付、Review、合入、CI、GATE 或 REPLAY。 旧收据允许作为历史证据原样导入,不自动成为当前恢复或交付准入;格式继续为 schema 1。 diff --git a/scripts/task.sh b/scripts/task.sh index 02a0607d..16a3827e 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -18,14 +18,15 @@ fetch_target() { git rev-parse --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null \ || { echo "[verdict] REMOTE_UNVERIFIED — target commit missing" >&2; return 6; } } -target_rewound() { # 0 仅当 origin 通告的目标提交本地可用且不是缓存 origin/ 的后代;不可达或无法比较返回 1 +target_history() { # 输出 ok(远端不可查询/目标 ref 缺失/无缓存目标/通告提交为缓存后代)、rewound(回退或分叉)或 uncomparable(通告提交本地缺失或无法比较) local cached advertised rc=0 - cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || return 1 + cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || { echo ok; return 0; } advertised=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>/dev/null \ - | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') || return 1 - [ -n "$advertised" ] && git cat-file -e "${advertised}^{commit}" 2>/dev/null || return 1 + | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') || { echo ok; return 0; } + [ -n "$advertised" ] || { echo ok; return 0; } + git cat-file -e "${advertised}^{commit}" 2>/dev/null || { echo uncomparable; return 0; } git merge-base --is-ancestor "$cached" "$advertised" 2>/dev/null || rc=$? - [ "$rc" -eq 1 ] + case "$rc" in 0) echo ok ;; 1) echo rewound ;; *) echo uncomparable ;; esac } 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:]))'; } @@ -150,9 +151,15 @@ cmd_precheck() { fi if [ "$mode" = local ]; then python3 "${SCRIPT_DIR}/evidence.py" --local-context --active || return 2 - if [ "$remote_fresh" -eq 0 ] && target_rewound; then - echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the cached target; preserve local work and reconcile the target history" - return 6 + if [ "$remote_fresh" -eq 0 ]; then + case "$(target_history)" in + rewound) + echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the cached target; preserve local work and reconcile the target history" + return 6 ;; + uncomparable) + echo "[verdict] REMOTE_UNVERIFIED — advertised origin/${TARGET_BRANCH} commit is missing locally and cannot be compared with the cached target; preserve local work and retry the refresh" + return 6 ;; + esac fi [ "$remote_fresh" -eq 1 ] || echo "[remote-freshness] unavailable — LOCAL_READY rests on local task context only" echo "[verdict] LOCAL_READY — existing task may continue authorized local work; final delivery still requires latest-target sync and checks" From 5b50d50b2ae61ce5ac6ae44ff6d75f50118e427c Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 13:48:10 +0000 Subject: [PATCH 25/39] Record U2 unreachable and uncomparable target distinction Why: Preserve the final guard for a reachable advertised target whose commit object cannot be compared locally. Evidence: The regression test, 74 focused evidence/recovery tests, and all 485 governance tests pass. Impact: Network outages retain LOCAL_READY; known but unverified target history returns REMOTE_UNVERIFIED. Exact-head CI remains pending. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index f9d5777a..306580bd 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -41,6 +41,8 @@ | U2 review regression tests | closed task, reachable rewind, reappearing target | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | d5b8e74c4306 | 0 | /tmp/norm-remediation-u2-review-fix-tests-receipt.json | | U2 review fixes | reject closed task and known target rewind/divergence | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 05a9a3bc89b6 | 0 | /tmp/norm-remediation-u2-review-fixes-receipt.json | | U2 final edge tests | target divergence and receipt target branch mismatch | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 2e0da0dadb73 | 0 | /tmp/norm-remediation-u2-final-edge-tests-receipt.json | +| U2 uncomparable-target test | reachable advertised SHA missing locally | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 95d17188585c | 0 | /tmp/norm-remediation-u2-uncomparable-target-test-receipt.json | +| U2 target-history fix | block known but locally uncomparable history | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 9c0e76570832 | 0 | /tmp/norm-remediation-u2-uncomparable-target-fix-receipt.json | ## 同步记录 @@ -59,12 +61,13 @@ | 2026-10-18T12:42Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 2d19085b0725fc46 | | 2026-10-18T12:43Z | `make test-governance` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 018194c0a804d1b9 | | 2026-10-18T13:01Z | `make test-governance` | 2e0da0dadb73db1b6a70f8545dbfd5df3a8e4d00 | 0 | SELF | a44db826c4be80fb | +| 2026-10-18T13:40Z | `bash -n scripts/task.sh && make test-governance` | 9c0e7657083259759f5bd1bed6921471411dba2c | 0 | SELF | 872bbc3437ae83e3 | ## 结果与验证 - 结果:U2 允许既有安全任务在真正远端不可用或目标跟踪 ref 缺失时继续有界本地工作;闭合任务和已证实的远端 target 回退/分叉均拒绝 `LOCAL_READY`。新任务和正式交付仍要求严格 READY/权威同步。 -- 验证:host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 71/71 通过(SELF,HEAD `05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71`);最新 `make test-governance` 484 项通过、18 项 skip(SELF,HEAD `2e0da0dadb73db1b6a70f8545dbfd5df3a8e4d00`)。worker 自检包含 `tests.test_evidence` 52/52、`tests.test_task_recovery` 21/21。先前 PR CI run 3884/job 6150 在 `make test` 的 Go 后端测试 `TestToolCallDeltaBeforeExecution` 失败(`modules/backend/internal/runtime/llm_test.go:1084`,事件顺序为 `[tool.call.delta tool.call tool.result]`);该文件不在变更范围;随后 head `f2c2401...` 的 `ci / test` GATE 通过。最终 head `2e0da0d...` 仍需重跑 CI。 -- Review 发现与闭合:正确性审阅指出闭合任务曾能在 precheck 收到 LOCAL_READY;可靠性审阅指出可达远端回退/分叉可能被当作离线。已分别加入 test-first 回归并修复。测试审阅要求的目标 ref 重现、receipt target branch 错配及可达远端 target 分叉场景也已覆盖。项目规范/可维护性审阅无 finding。最终 head 的完整 Review 仍在进行。 +- 验证:最终 host `bash -n scripts/task.sh && make test-governance` 通过(485 项通过、18 项 skip,SELF,HEAD `9c0e7657083259759f5bd1bed6921471411dba2c`);worker 自检 evidence 52/52、task recovery 22/22,覆盖可达但 SHA 对象本地缺失的情景。此前最终聚焦 evidence/recovery 74/74 通过。PR CI head `b492bea...` 曾 GATE 通过;在此之后的脚本边界修复尚未运行精确 head CI。 +- Review 发现与闭合:正确性 reviewer 发现闭合任务仍可在 precheck 收到 LOCAL_READY、以及可达但不可比较的 target 仍被当作离线;可靠性 reviewer 发现缓存 target 回退/分叉问题。已分别加入 test-first 回归并修复。测试 reviewer 建议的目标 ref 重现、receipt target branch 错配、远端分叉和不可比较 target 场景均已覆盖;项目规范/可维护性审阅无 finding。旧 CI run 3884 曾因变更外 Go 测试 `TestToolCallDeltaBeforeExecution` 失败,后续 `b492bea...` CI 通过;当前最终 head 的 GATE 与新候选完整 Review 待执行。 - 执行偏差:原 U2 Cursor attempt 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms,并被 fail-stop;新的 evidence 单元明确要求串行编辑,但 worker 仍在同一批次向 `scripts/evidence.py` 发出两次编辑调用(未重叠,最终 diff 已审阅且验证通过)。不能据此声称提示已强制串行;后续规范计划须区分提示、可观测证据与工具级并发防护。 - 新增执行偏差:修复闭合任务与目标回退时,worker 在同一批次向 `scripts/evidence.py` 和 `scripts/task.sh` 发出两次互不重叠编辑,违反了任务包的串行编辑要求;最终 diff 已逐项审阅,定向与完整治理测试均通过,但该结果不能证明提示具备工具级强制力。 - Review 路由记录:第一次 PR 对抗性 cross-model job 因 orchestrator 使用了不匹配的约束文件名,在 provider egress 前跳过,未把代码发送给外部模型;job 目录已清理。该审阅不计入 Review 覆盖,最终 head 使用本地 adversarial reviewer。 From fbb2aacb71ba907476748419b5e1c59a4156fb08 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:04:13 +0000 Subject: [PATCH 26/39] agent-exec transport 973ce511e924ce5ab0953e16e46a42ec/7671c49f7667 --- tests/test_task_recovery.py | 203 ++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index f2627c1d..b24bf541 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -469,6 +469,209 @@ class TaskRecoveryTests(unittest.TestCase): self.assertEqual(f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout, cached_before) + def write_no_cache_wrapper(self, bin_dir, advertised, log_name="git.log"): + """Create a disposable git wrapper for the missing-tracking-ref scenarios. + + Intercepts: + fetch -> fail (origin unreachable; the wrapper itself exits 128 + and writes the standard "unable to access origin" stderr line + so the precheck captures the same `fetch_err` it would observe + against a real unreachable remote) + ls-remote origin refs/heads/main -> configured ADVERTISED (or + nothing when ADVERTISED is empty, simulating an origin that + has no configured target branch) + branch --merged origin/main -> empty output (the cached + tracking ref is deleted locally; the real git would otherwise + fail the pipeline under set -e/pipefail and the precheck would + silently exit before the verdict is printed) + + All other git calls delegate to the real binary via the resolved + REAL_GIT absolute path (and a stripped PATH so the wrapper cannot + recurse into itself) so the registered task context stays + observable normally. + + Returns (bin_dir, wrapper_log_path).""" + f = self.fixture + real_git = subprocess.check_output( + ["bash", "-c", "command -v git"], env=f.env, text=True).strip() + bin_dir.mkdir(exist_ok=True) + wrapper_log = bin_dir / log_name + wrapper = bin_dir / "git" + wrapper.write_text( + "#!/usr/bin/env bash\n" + "# Disposable fixture wrapper for the missing-tracking-ref\n" + "# scenarios:\n" + "# fetch -> fail (128)\n" + "# ls-remote origin refs/heads/main -> $ADVERTISED\n" + "# branch --merged origin/main -> empty\n" + "# Anything else delegates to the real binary so the registered\n" + "# task context (HEAD, branch, R0, worktree, .task/) stays\n" + "# observable normally.\n" + f"REAL_GIT={shlex.quote(real_git)}\n" + f"LOG={shlex.quote(str(wrapper_log))}\n" + f"ADVERTISED={shlex.quote(advertised)}\n" + "WRAPPER_BIN=$(cd \"$(dirname \"$0\")\" && pwd)\n" + "echo \"configured advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + "stripped=\"\"\n" + "old_ifs=\"$IFS\"\n" + "IFS=':'\n" + "for p in $PATH; do\n" + " [ \"$p\" = \"$WRAPPER_BIN\" ] && continue\n" + " if [ -z \"$stripped\" ]; then stripped=\"$p\"; else stripped=\"$stripped:$p\"; fi\n" + "done\n" + "IFS=\"$old_ifs\"\n" + "export PATH=\"$stripped\"\n" + "case \"$1\" in\n" + " fetch)\n" + " echo \"fetch-intercepted\" >> \"$LOG\"\n" + " echo 'fatal: unable to access origin' >&2\n" + " exit 128 ;;\n" + " ls-remote)\n" + " if [ \"${2:-}\" = origin ] && [ \"${3:-}\" = refs/heads/main ]; then\n" + " echo \"ls-remote-served advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + " if [ -n \"$ADVERTISED\" ]; then\n" + " printf '%s\\trefs/heads/main\\n' \"$ADVERTISED\"\n" + " fi\n" + " exit 0\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + " branch)\n" + " # refs/remotes/origin/main is deleted locally; the real git\n" + " # `git branch --merged origin/main` would fail and the\n" + " # precheck pipeline (with pipefail + set -e) would silently\n" + " # exit. Stub it so the verdict is actually emitted.\n" + " if [ \"${2:-}\" = --merged ] && [ \"${3:-}\" = origin/main ]; then\n" + " echo \"branch-merged-stub no-merged-branches\" >> \"$LOG\"\n" + " exit 0\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + "esac\n" + "exec \"$REAL_GIT\" \"$@\"\n") + wrapper.chmod(0o755) + return bin_dir, wrapper_log + + def test_local_precheck_without_cached_ref_rejects_uncomparable_remote(self): + """Cached refs/remotes/origin/main is absent; origin advertises a + SHA whose object is unavailable locally. Local precheck must not + advertise LOCAL_READY because ancestry cannot be compared against + any locally known target — the cached target cannot be trusted + as authoritative without an ancestry check, and the advertised + SHA is not locally reachable so no forward-descendant + relationship can be established either. + + The current implementation lets target_history() short-circuit at + the missing cached-ref step (return "ok") and unconditionally + print LOCAL_READY in local mode. The verdict is wrong; this test + records the expected red result.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # Detach the registered branch from its (now stale) upstream so + # no in-process code accidentally queries HEAD@{u} / remote.merge + # against the deleted cached ref. + f.git(self.wt, "branch", "--unset-upstream", "feature/test") + f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") + + synthetic = "1" * 40 + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "no-cache-uncomparable-bin", synthetic) + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": path_env} + + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + + # Proof: the wrapper was loaded (configured marker) and served + # the failure case (fetch intercepted). ls-remote is served by + # the wrapper if and when the post-fix implementation queries it + # for the missing cached-ref path; the current implementation + # short-circuits before ls-remote, so the assertion below does + # not require an ls-remote entry. + log = wrapper_log.read_text() + self.assertIn(f"configured advertised-target={synthetic}", log) + self.assertIn("fetch-intercepted", log) + + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + + def test_local_precheck_without_cached_ref_rejects_missing_remote_target(self): + """Cached refs/remotes/origin/main is absent; origin returns no + configured target branch (ls-remote outputs nothing). Local + precheck must not advertise LOCAL_READY because nothing can be + verified against the registered task's R0 — the cached target + is the only handle on what origin claimed, and without it (and + without an ls-remote response) the precheck has no + authoritative signal to confirm the target even exists. + + The current implementation lets target_history() short-circuit at + the missing cached-ref step (return "ok") and unconditionally + print LOCAL_READY in local mode. The verdict is wrong; this test + records the expected red result.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + f.git(self.wt, "branch", "--unset-upstream", "feature/test") + f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") + + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "no-cache-missing-target-bin", "") + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": path_env} + + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + + log = wrapper_log.read_text() + # Empty advertised value means the wrapper emits no ls-remote line. + self.assertIn("configured advertised-target=", log) + self.assertIn("fetch-intercepted", log) + + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + + def test_local_precheck_without_cached_ref_allows_comparable_forward_target(self): + """Cached refs/remotes/origin/main is absent; origin advertises + the task's R0 (or a forward descendant that is locally + available). Local precheck must continue to advertise + LOCAL_READY — the explicit missing-tracking-ref continuation + contract — because the registered task context is the same as + the cached ref would have represented, and the ls-remote + response can be compared against the registered baseline to + confirm origin is at or past the task's initial state. + + The current implementation short-circuits at the missing + cached-ref step (return "ok"), which coincidentally returns + LOCAL_READY for any registered task. The verdict is right; this + test records the expected green result so the post-fix + implementation preserves the contract when the ls-remote check + is added against the registered R0.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + f.git(self.wt, "branch", "--unset-upstream", "feature/test") + f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") + + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "no-cache-comparable-bin", r0) + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": path_env} + + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + + log = wrapper_log.read_text() + self.assertIn(f"configured advertised-target={r0}", log) + self.assertIn("fetch-intercepted", log) + + self.assertEqual(local.returncode, 0, local.stdout) + self.assertIn("[verdict] LOCAL_READY", local.stdout) + self.assertNotIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + if __name__ == "__main__": unittest.main() From 8d8585afec6671c82ddf8c9d80063e4499ae7f2f Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:17:22 +0000 Subject: [PATCH 27/39] agent-exec transport 2cd24625d2b60b7c6db7aa1bc628a9c0/af7e0b3bb4d6 --- tests/test_task_recovery.py | 128 ++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index b24bf541..7e5856ac 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -391,6 +391,134 @@ class TaskRecoveryTests(unittest.TestCase): "VALIDATE_RECEIPT_PURPOSE": "ignore"}) self.assertNotEqual(result.returncode, 0, result.stdout) + def test_local_resume_continues_when_origin_is_unreachable(self): + """Standalone: capture a valid TASK_WORK_MODE=local precheck receipt, + point the disposable worktree's origin URL at a nonexistent path, + then assert (a) `task resume` in local mode imports the receipt and + leaves HEAD unchanged and (b) the same receipt under delivery mode + rejects with REMOTE_UNVERIFIED and does not import. + + Currently RED on (a): cmd_resume calls fetch_target unconditionally, + so the unreachable origin aborts the resume before the receipt can + be imported (set -e trips, returncode != 0, no [resumed], receipt + bytes unchanged, card untouched). Post-fix the local path must + skip or survive the unreachable fetch and import the receipt on + the registered in-progress task context. + + Delivery must remain strict under the same offline origin — the + RED-capture only records what (a) requires; (b) stays correct.""" + f = self.fixture + receipt = self.capture() + original = receipt.read_bytes() + receipt_sha = json.loads(original)["receipt_sha256"] + head = f.git(self.wt, "rev-parse", "HEAD").stdout + # Break the disposable worktree's origin URL so the offline + # scenario is real (the fetch in cmd_resume loses its target). + # The bare-remote fixture and the registered HEAD remain + # identifiable on the worktree fs; only the authoritative URL + # is changed for the duration of this test. + f.git(self.wt, "remote", "set-url", "origin", str(f.root / "missing.git")) + # (1) Local mode must continue authorized work by importing the + # captured receipt under the registered in-progress task context. + local = self.resume(receipt, mode="local") + self.assertEqual(local.returncode, 0, + f"local resume should import the captured receipt under the " + f"registered task (RED: fetch_target aborts the resume before " + f"IMPORT_RECEIPT; returncode={local.returncode}); stdout={local.stdout}") + self.assertIn("[resumed]", local.stdout) + self.assertIn("work_mode=local", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head) + self.assertEqual(receipt.read_bytes(), original) + self.assertIn(receipt_sha, self.card.read_text()) + # (2) Same receipt under delivery mode must stay strict under + # the same offline origin: reject with REMOTE_UNVERIFIED, no + # `[resumed]` line, the card is not updated to claim delivery + # authorization, and HEAD remains untouched. + card_before_delivery = self.card.read_bytes() + delivery = self.resume(receipt, mode="delivery") + self.assertNotEqual(delivery.returncode, 0, delivery.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", delivery.stdout) + self.assertNotIn("[resumed]", delivery.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head) + self.assertEqual(self.card.read_bytes(), card_before_delivery) + + def test_local_child_resume_uses_local_parent_when_origin_is_unreachable(self): + """Child: configure the task card as child with + immediate_target=feature/parent, create that parent branch locally + at the recorded parent_head_sha, capture a local receipt, make + origin unreachable, and assert that `task resume` in local mode + imports the receipt using the authorized local parent reference + (the local refs/heads/feature/parent tip = parent_head_sha). + + Currently RED: cmd_resume reaches fetch_target before resolving + the parent reference, so the unreachable origin aborts the + resume before the receipt can be imported (returncode != 0, no + `[resumed]`, receipt bytes unchanged, child card untouched). + Post-fix the local path must skip or survive the unreachable + fetch AND let resolve_parent_ref fall back to the authorized + local parent branch when origin is unqueriable.""" + f = self.fixture + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # (1) Create the parent initiative branch in the main repo so + # the parent has a deterministic tip commit, push it to the + # bare remote so the worktree can fetch it once at the start, + # then create the child branch on top of the parent inside the + # worktree and switch the worktree to it. + f.git(f.repo, "checkout", "-b", "feature/parent") + (f.repo / "parent-tip").write_text("authorized parent initiative tip\n") + f.git(f.repo, "add", "parent-tip") + f.git(f.repo, "commit", "-m", "Parent initiative tip commit") + parent_head = f.git(f.repo, "rev-parse", "HEAD").stdout.strip() + f.git(f.repo, "push", "origin", "feature/parent") + # Bring the parent branch into the worktree, branch off the + # parent at the recorded tip, then check the worktree out on + # the child branch. + f.git(self.wt, "fetch", "origin", + "feature/parent:refs/remotes/origin/feature/parent") + f.git(self.wt, "branch", "feature/parent-child", parent_head) + f.git(self.wt, "checkout", "feature/parent-child") + # (2) Write the child card on the child branch so branch_card + # finds it (the file is named after the branch slug) and the + # resume's immediate_target + parent_head_sha fields resolve + # to feature/parent and the recorded parent tip. + child_card = self.wt / ".task" / "feature-parent-child.md" + self.assertFalse(child_card.exists()) + child_card.parent.mkdir(exist_ok=True) + child_card.write_text( + "# feature/parent-child\n" + "任务主记录:fixture;状态:进行中;owner:Lifecycle Test\n" + f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {r0}\n" + f"- 工作区:{self.wt}(worktree);分支:feature/parent-child\n" + "- 分支类型:child;initiative_id:feature-parent;root_target:main;" + "immediate_target:feature/parent;accepting_requirements:false;" + "parent_branch:feature/parent;" + f"parent_head_sha:{parent_head};" + "parent_pr:无;parent_source:local(非正式准入)\n" + ) + f.git(self.wt, "add", str(child_card.relative_to(self.wt))) + f.git(self.wt, "commit", "-m", "Register child fixture task") + # (3) Capture a local receipt on the child branch. + receipt = self.capture() + original = receipt.read_bytes() + receipt_sha = json.loads(original)["receipt_sha256"] + head = f.git(self.wt, "rev-parse", "HEAD").stdout + # (4) Break the origin URL so the offline scenario is real. + f.git(self.wt, "remote", "set-url", "origin", str(f.root / "missing.git")) + # (5) Local resume must import the receipt using the + # authorized local parent reference and leave HEAD unchanged. + result = self.resume(receipt, mode="local") + self.assertEqual(result.returncode, 0, + f"local child resume should import the captured receipt via the " + f"local parent ref (RED: fetch_target aborts the resume before " + f"resolve_parent_ref / IMPORT_RECEIPT; returncode={result.returncode}); " + f"stdout={result.stdout}") + self.assertIn("[resumed]", result.stdout) + self.assertIn("work_mode=local", result.stdout) + self.assertIn(f"observed_target={parent_head}", result.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head) + self.assertEqual(receipt.read_bytes(), original) + self.assertIn(receipt_sha, child_card.read_text()) + def test_local_precheck_rejects_reachable_uncomparable_target(self): """Origin advertises a target SHA but the advertised object is unavailable locally, so ancestry cannot be compared against the From f81326bd0836caf2df53cbb73cb537acb8e7e9af Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:25:10 +0000 Subject: [PATCH 28/39] agent-exec transport 7da469a725ec41f651cf89a8616adaa7/78f40f5a0c57 --- policy/governance.md | 5 +++- scripts/task.sh | 54 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/policy/governance.md b/policy/governance.md index 93bbe34b..b744beab 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -42,7 +42,10 @@ make task-start NAME=feature/my-task TRIAGE_RECEIPT=/tmp/my-task-triage.json PRE 恢复已有登记任务先捕获本次启动检查,再在原任务 worktree 用 `make task-resume PRECHECK_RECEIPT=/tmp/my-task-resume.json` 验证并原样导入;不重新建分支。 默认 `TASK_WORK_MODE=delivery`,继续要求严格 READY 和当前目标一致。 -工作区忙或落后时先由 owner 保存现场并同步;远端不可达时只继续不依赖新鲜基线的阅读和分析。 +工作区忙或落后时先由 owner 保存现场并同步。远端不可达时,delivery 恢复仍以 REMOTE_UNVERIFIED 拒绝; +显式 `TASK_WORK_MODE=local` 可在先核验收据、活动任务、原 R0/HEAD/工作区范围后导入 LOCAL_READY 收据, +继续已授权局部工作(子任务以已登记的本地父分支为参考,且须包含记录的 `parent_head_sha`)。远端可达但目标缺失、 +回退/分叉或无法比较时仍拒绝。离线不得建任务、同步、PR 检查、Review/合入或交付,本地参考不视为最新或可交付。 局部恢复接口已具备,正式交付仍按[合入调度](delivery.md#合入调度)的单一最终窗口执行。 启用后,既有任务可以显式选择 local,保持原 R0,标记落后,继续已授权局部工作: diff --git a/scripts/task.sh b/scripts/task.sh index 16a3827e..caade4e3 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -18,9 +18,10 @@ fetch_target() { git rev-parse --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null \ || { echo "[verdict] REMOTE_UNVERIFIED — target commit missing" >&2; return 6; } } -target_history() { # 输出 ok(远端不可查询/目标 ref 缺失/无缓存目标/通告提交为缓存后代)、rewound(回退或分叉)或 uncomparable(通告提交本地缺失或无法比较) +target_history() { # $1=可选基线(无缓存目标时代替比较,如任务 R0)→ 输出 ok(远端不可查询/目标 ref 缺失/无缓存目标/通告提交为缓存后代)、rewound(回退或分叉)或 uncomparable(通告提交本地缺失或无法比较) local cached advertised rc=0 - cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || { echo ok; return 0; } + cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || cached=${1:-} + [ -n "$cached" ] || { echo ok; return 0; } advertised=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>/dev/null \ | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') || { echo ok; return 0; } [ -n "$advertised" ] || { echo ok; return 0; } @@ -477,9 +478,9 @@ cmd_resume() { need_repo local mode mode=$(work_mode) - fetch_target + [ "$mode" = local ] || fetch_target [ -n "${PRECHECK_RECEIPT:-}" ] || die "PRECHECK_RECEIPT required: capture the current task precheck before resume" - local receipt + local receipt remote_fresh=1 receipt=$(realpath -- "$PRECHECK_RECEIPT") VALIDATE_RECEIPT="$receipt" VALIDATE_RECEIPT_PURPOSE="resume-${mode}" bash "${SCRIPT_DIR}/evidence.sh" # 立即目标:子任务为其父 initiative(delivery 下取远端头),其余为根目标。 @@ -487,14 +488,44 @@ cmd_resume() { branch=$(git rev-parse --abbrev-ref HEAD) card=$(branch_card "$branch") [ -n "$card" ] && immediate=$(card_immediate_target "$card") + if [ "$mode" = local ]; then + # local 先核验收据与活动任务再刷新:刷新成功按新目标复核;失败只在远端确实不可达时以登记 R0 继续, + # 远端可达而目标缺失、回退/分叉或无法比较时保持 REMOTE_UNVERIFIED,不导入收据。 + git check-ref-format "refs/heads/${TARGET_BRANCH}" >/dev/null || die "invalid target branch" + if fetch_target 2>/dev/null; then + VALIDATE_RECEIPT="$receipt" VALIDATE_RECEIPT_PURPOSE=resume-local bash "${SCRIPT_DIR}/evidence.sh" + else + remote_fresh=0 + local r0 ls_rc=0 + [ -n "$card" ] || die "no registered task card for ${branch}; receipt not imported" + r0=$(sed -n 's/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/\1/p' "$card" | head -1) + [ -n "$r0" ] || die "task card has no initial baseline; receipt not imported" + git ls-remote --exit-code --heads origin "refs/heads/${TARGET_BRANCH}" >/dev/null 2>&1 || ls_rc=$? + if [ "$ls_rc" -eq 2 ]; then + echo "[verdict] REMOTE_UNVERIFIED — origin is reachable but ${TARGET_BRANCH} is missing; preserve local work, receipt not imported" >&2 + return 6 + fi + case "$(target_history "$r0")" in + rewound) + echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the known target; preserve local work, receipt not imported" >&2 + return 6 ;; + uncomparable) + echo "[verdict] REMOTE_UNVERIFIED — advertised origin/${TARGET_BRANCH} cannot be compared with the known target; preserve local work, receipt not imported" >&2 + return 6 ;; + esac + echo "[remote-freshness] unavailable — resuming on registered R0=${r0} for authorized local work only; delivery still requires a fresh target" + fi + fi 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 + elif [ "$remote_fresh" -eq 1 ]; then observed=$(git rev-parse "origin/${TARGET_BRANCH}") + else + observed=unavailable 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}" @@ -630,7 +661,7 @@ 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 取远端与本地包含对方的一侧) +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 上刚取回的父分支头:本地领先/分叉意味着未准入的提交,不得被同步或合入。 @@ -652,8 +683,15 @@ resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验 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 + # 远端不可查询时只把已登记的本地父分支作为局部续作参考;不声称其为最新或可交付,调用方仍核验 parent_head_sha。 + local_ref=$(git rev-parse -q --verify "refs/heads/$1^{commit}" 2>/dev/null || true) + if [ -z "$local_ref" ]; then + echo "[verdict] REMOTE_UNVERIFIED — cannot query origin/$1 and no local $1 exists; preserve local work and retry" >&2 + return 6 + fi + echo "[remote-freshness] origin/$1 unavailable — using local $1 for authorized local continuation only; not current or delivery-ready" >&2 + printf '%s\n' "$local_ref" + return 0 fi local_ref=$(git rev-parse -q --verify "refs/heads/$1^{commit}" 2>/dev/null || true) if [ -n "$remote" ] && [ -n "$local_ref" ]; then From 1ff6444d3352c7e13c2b75cefe7d65c631181454 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:34:23 +0000 Subject: [PATCH 29/39] agent-exec transport 2f332e8b99c2b13e0f8734136a777328/db30d3293d24 --- policy/governance.md | 4 ++-- scripts/task.sh | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/policy/governance.md b/policy/governance.md index b744beab..00f5ed5f 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -62,8 +62,8 @@ LOCAL_READY 只用于局部恢复;新任务入口始终校验严格 READY, 一致、工作区干净且组件状态未变;收据 `root_target/immediate_target` 等于卡片范围;目标只允许快进落后,回退或分叉拒绝。 远端跟踪目标 ref 缺失时,local 模式的 precheck 与 `resume-local` 只在当前登记任务身份、本地 R0 对象、R0 为本地 HEAD 祖先、 工作区干净、分支/worktree、组件与范围全部核验通过时继续。缺失或过期的远端 ref 从不证明新鲜度;只要有任一目标历史可用, -其祖先关系须一致。local precheck 刷新失败时,远端不可查询或目标 ref 缺失按上述本地核验继续;远端可查询且已有缓存目标时, -通告提交须本地存在且为缓存目标后代才继续,回退/分叉或通告提交本地缺失无法比较均判 REMOTE_UNVERIFIED。 +其祖先关系须一致。local precheck 刷新失败时,远端不可查询按上述本地核验继续;远端可查询时,以缓存目标(无缓存时为活动任务卡 +登记的 R0)为基线,通告提交须本地存在且为基线后代才继续,目标 ref 缺失、回退/分叉或通告提交本地缺失无法比较均判 REMOTE_UNVERIFIED。 正式 Review 与合入仍须先完成新鲜的权威同步。 `resume-delivery` 仍要求 READY 且收据目标等于当前权威目标。LOCAL_READY 始终是 SELF 本地工作记录, 不能用于建任务、正式交付、Review、合入、CI、GATE 或 REPLAY。 diff --git a/scripts/task.sh b/scripts/task.sh index caade4e3..9d1f6fee 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -18,13 +18,13 @@ fetch_target() { git rev-parse --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null \ || { echo "[verdict] REMOTE_UNVERIFIED — target commit missing" >&2; return 6; } } -target_history() { # $1=可选基线(无缓存目标时代替比较,如任务 R0)→ 输出 ok(远端不可查询/目标 ref 缺失/无缓存目标/通告提交为缓存后代)、rewound(回退或分叉)或 uncomparable(通告提交本地缺失或无法比较) - local cached advertised rc=0 +target_history() { # $1=可选基线(无缓存目标时代替比较,如任务 R0)→ 输出 ok(远端不可查询/通告提交为基线后代)、rewound(回退或分叉)或 uncomparable(远端可查询但目标 ref 缺失、无基线、通告提交本地缺失或无法比较) + local cached listing advertised rc=0 cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || cached=${1:-} - [ -n "$cached" ] || { echo ok; return 0; } - advertised=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>/dev/null \ - | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') || { echo ok; return 0; } - [ -n "$advertised" ] || { echo ok; return 0; } + listing=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>/dev/null) || { echo ok; return 0; } + advertised=$(printf '%s\n' "$listing" \ + | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') + [ -n "$advertised" ] && [ -n "$cached" ] || { echo uncomparable; return 0; } git cat-file -e "${advertised}^{commit}" 2>/dev/null || { echo uncomparable; return 0; } git merge-base --is-ancestor "$cached" "$advertised" 2>/dev/null || rc=$? case "$rc" in 0) echo ok ;; 1) echo rewound ;; *) echo uncomparable ;; esac @@ -153,12 +153,13 @@ cmd_precheck() { if [ "$mode" = local ]; then python3 "${SCRIPT_DIR}/evidence.py" --local-context --active || return 2 if [ "$remote_fresh" -eq 0 ]; then - case "$(target_history)" in + # 无缓存目标时只以活动任务卡登记的 R0 作比较基线,不把它当作远端最新值。 + case "$(target_history "$(card_r0 "$(branch_card "$branch")")")" in rewound) - echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the cached target; preserve local work and reconcile the target history" + echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the known target; preserve local work and reconcile the target history" return 6 ;; uncomparable) - echo "[verdict] REMOTE_UNVERIFIED — advertised origin/${TARGET_BRANCH} commit is missing locally and cannot be compared with the cached target; preserve local work and retry the refresh" + echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} is missing or its advertised commit cannot be compared with the known target; preserve local work and retry the refresh" return 6 ;; esac fi @@ -498,7 +499,7 @@ cmd_resume() { remote_fresh=0 local r0 ls_rc=0 [ -n "$card" ] || die "no registered task card for ${branch}; receipt not imported" - r0=$(sed -n 's/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/\1/p' "$card" | head -1) + r0=$(card_r0 "$card") [ -n "$r0" ] || die "task card has no initial baseline; receipt not imported" git ls-remote --exit-code --heads origin "refs/heads/${TARGET_BRANCH}" >/dev/null 2>&1 || ls_rc=$? if [ "$ls_rc" -eq 2 ]; then @@ -604,6 +605,7 @@ card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; } # 两级分支元数据行:'- 分支类型:;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_r0() { sed -n 's/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/\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}}"; } From 2fb7d2d30ff41495f5cde656c3ac73b727c2e3c9 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 14:55:31 +0000 Subject: [PATCH 30/39] Record offline resume and R0 comparison evidence Why: Preserve host verification and worker transport records for offline root and child task resume. Evidence: 79 evidence/recovery tests passed; governance validation is being rerun on the final implementation candidate. Impact: LOCAL_READY remains local-only; exact-head CI and final independent review remain required. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index 306580bd..f4c3f37c 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -43,6 +43,14 @@ | U2 final edge tests | target divergence and receipt target branch mismatch | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 2e0da0dadb73 | 0 | /tmp/norm-remediation-u2-final-edge-tests-receipt.json | | U2 uncomparable-target test | reachable advertised SHA missing locally | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 95d17188585c | 0 | /tmp/norm-remediation-u2-uncomparable-target-test-receipt.json | | U2 target-history fix | block known but locally uncomparable history | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 9c0e76570832 | 0 | /tmp/norm-remediation-u2-uncomparable-target-fix-receipt.json | +| U2 no-cache target tests | target reachable/missing or forward with no tracking ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | fbb2aacb71ba | 0 | /tmp/norm-remediation-u2-no-cache-history-tests-receipt.json | +| U2 offline resume tests | standalone/child local resume during origin outage | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 8d8585afec66 | 0 | /tmp/norm-remediation-u2-offline-resume-tests-receipt.json | +| U2 offline resume fix | local-only receipt resume with target history and parent R0 checks | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | f81326bd0836 | 0 | /tmp/norm-remediation-u2-offline-resume-fix-receipt.json | +| U2 no-cache R0 fix | compare reachable targets to registered R0 without cached ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 1ff6444d3352 | 0 | /tmp/norm-remediation-u2-precheck-r0-fix-receipt.json | +| U2 precheck R0 regression tests | reachable/missing/forward target without cached ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | fbb2aacb71ba | 0 | /tmp/norm-remediation-u2-no-cache-history-tests-receipt.json | +| U2 offline resume regression tests | root/child receipt import while origin unreachable | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 8d8585afec66 | 0 | /tmp/norm-remediation-u2-offline-resume-tests-receipt.json | +| U2 offline resume implementation | local resume on R0/local registered parent | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | f81326bd0836 | 0 | /tmp/norm-remediation-u2-offline-resume-fix-receipt.json | +| U2 precheck R0 implementation | compare remote advertisement without cached tracking ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 1ff6444d3352 | 0 | /tmp/norm-remediation-u2-precheck-r0-fix-receipt.json | ## 同步记录 @@ -62,15 +70,21 @@ | 2026-10-18T12:43Z | `make test-governance` | 05a9a3bc89b65eebd92cac6f82c3e4f28e13ab71 | 0 | SELF | 018194c0a804d1b9 | | 2026-10-18T13:01Z | `make test-governance` | 2e0da0dadb73db1b6a70f8545dbfd5df3a8e4d00 | 0 | SELF | a44db826c4be80fb | | 2026-10-18T13:40Z | `bash -n scripts/task.sh && make test-governance` | 9c0e7657083259759f5bd1bed6921471411dba2c | 0 | SELF | 872bbc3437ae83e3 | +| 2026-10-18T14:41Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | bf12f030894e1530 | +| 2026-10-18T14:40Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 8e81c5353849fbd5 | +| 2026-10-18T14:46Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 3821724d4545beda | +| 2026-10-18T14:47Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 048ce6f79e025863 | ## 结果与验证 - 结果:U2 允许既有安全任务在真正远端不可用或目标跟踪 ref 缺失时继续有界本地工作;闭合任务和已证实的远端 target 回退/分叉均拒绝 `LOCAL_READY`。新任务和正式交付仍要求严格 READY/权威同步。 -- 验证:最终 host `bash -n scripts/task.sh && make test-governance` 通过(485 项通过、18 项 skip,SELF,HEAD `9c0e7657083259759f5bd1bed6921471411dba2c`);worker 自检 evidence 52/52、task recovery 22/22,覆盖可达但 SHA 对象本地缺失的情景。此前最终聚焦 evidence/recovery 74/74 通过。PR CI head `b492bea...` 曾 GATE 通过;在此之后的脚本边界修复尚未运行精确 head CI。 +- 验证:host `python3 -m unittest tests.test_evidence tests.test_task_recovery` 79/79 通过(SELF,HEAD `1ff6444d3352c7e13c2b75cefe7d65c631181454`),覆盖 root/child 离线 task-resume 与严格 delivery 拒绝;host `bash -n scripts/task.sh && make test-governance` 490 项通过、18 项 skip(SELF,同 HEAD)。不含后续主脚本改动的 transport。local precheck no-cache 的 2 个红灯回归已由紧随其后的 `1ff6444d` 修复,三条 no-cache 状态单测 3/3 通过;最终候选完整测试仍待重跑。 - Review 发现与闭合:正确性 reviewer 发现闭合任务仍可在 precheck 收到 LOCAL_READY、以及可达但不可比较的 target 仍被当作离线;可靠性 reviewer 发现缓存 target 回退/分叉问题。已分别加入 test-first 回归并修复。测试 reviewer 建议的目标 ref 重现、receipt target branch 错配、远端分叉和不可比较 target 场景均已覆盖;项目规范/可维护性审阅无 finding。旧 CI run 3884 曾因变更外 Go 测试 `TestToolCallDeltaBeforeExecution` 失败,后续 `b492bea...` CI 通过;当前最终 head 的 GATE 与新候选完整 Review 待执行。 - 执行偏差:原 U2 Cursor attempt 对 `tests/test_task_recovery.py` 的两个 edit 调用重叠约 449 ms,并被 fail-stop;新的 evidence 单元明确要求串行编辑,但 worker 仍在同一批次向 `scripts/evidence.py` 发出两次编辑调用(未重叠,最终 diff 已审阅且验证通过)。不能据此声称提示已强制串行;后续规范计划须区分提示、可观测证据与工具级并发防护。 - 新增执行偏差:修复闭合任务与目标回退时,worker 在同一批次向 `scripts/evidence.py` 和 `scripts/task.sh` 发出两次互不重叠编辑,违反了任务包的串行编辑要求;最终 diff 已逐项审阅,定向与完整治理测试均通过,但该结果不能证明提示具备工具级强制力。 - Review 路由记录:第一次 PR 对抗性 cross-model job 因 orchestrator 使用了不匹配的约束文件名,在 provider egress 前跳过,未把代码发送给外部模型;job 目录已清理。该审阅不计入 Review 覆盖,最终 head 使用本地 adversarial reviewer。 +- 后续 Review 补充(覆盖前述状态):关闭任务 precheck、reachable-but-uncomparable/缺失 target、无 tracking ref 时需查询 advertised target、以及离线 `cmd_resume` 的 fetch-first 缺口,均已补 test-first 回归与修复。root 离线 resume 使用登记 R0;child resume 仅使用已登记、包含 `parent_head_sha` 的本地 parent 作为局部参考,delivery 仍 fetch-first。最终候选完整 Review 与 exact-head CI 待完成。 +- 离线 resume 已端到端验证:root 与 child 任务在捕获有效 LOCAL_READY 收据后、origin 不可达时可用 `TASK_WORK_MODE=local make task-resume` 导入;child 仅使用本地已登记 parent 并核对 `parent_head_sha`。同一环境下 delivery resume 仍以 REMOTE_UNVERIFIED 拒绝。 - 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 - 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 From 5b5537aa0fc40349793822f9a5d2b861b689a84f Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:58:04 +0000 Subject: [PATCH 31/39] agent-exec transport 0579081fa29ea1bd6d1d532a7a177e36/f55c4a162b49 --- tests/test_task_recovery.py | 139 ++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 7e5856ac..12eb08cc 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -519,6 +519,145 @@ class TaskRecoveryTests(unittest.TestCase): self.assertEqual(receipt.read_bytes(), original) self.assertIn(receipt_sha, child_card.read_text()) + def test_local_child_resume_rejects_reachable_missing_parent(self): + """Child: configure the task card as child with + immediate_target=feature/parent, create that parent branch on the + bare remote AND locally at the recorded parent_head_sha, capture + a local receipt, then remove ONLY refs/heads/feature/parent from + the bare remote while keeping origin reachable (the bare remote + still serves refs/heads/main). `task resume` in local mode must + refuse with REMOTE_UNVERIFIED instead of silently accepting the + local parent reference: the registered immediate parent branch + is absent remotely, so the local-only reference cannot be + reconciled against the authoritative origin even though it + exists in the worktree. + + Currently RED: resolve_parent_ref() does not distinguish + `ls-remote rc=2` (ref missing on a reachable origin) from the + unreachable branch. The function falls through the + `elif [ $rc -ne 2 ]` guard and lets `local_ref` satisfy the + resolver, so cmd_resume reaches IMPORT_RECEIPT and prints + `[resumed]` even though origin is fully reachable and just + missing the registered parent ref. Post-fix the local path must + print `[verdict] REMOTE_UNVERIFIED — origin is reachable but + feature/parent is missing; preserve local work, receipt not + imported` and refuse to import the receipt. + + The fixture mirrors + test_local_child_resume_uses_local_parent_when_origin_is_unreachable + but adds the local refs/heads/feature/parent branch and only + deletes the bare remote ref — the worktree URL stays on the + disposable bare remote and main remains fetchable, so the only + authority gap is the absent remote parent ref.""" + f = self.fixture + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # (1) Create the parent initiative branch in the main repo and + # push it to the bare remote so the worktree can fetch it once + # at the start, then create the local refs/heads/feature/parent + # branch inside the worktree (the post-fix path must refuse + # even when this local branch exists), branch off the parent + # tip into feature/parent-child, then switch the worktree to + # the child branch. + f.git(f.repo, "checkout", "-b", "feature/parent") + (f.repo / "parent-tip").write_text("authorized parent initiative tip\n") + f.git(f.repo, "add", "parent-tip") + f.git(f.repo, "commit", "-m", "Parent initiative tip commit") + parent_head = f.git(f.repo, "rev-parse", "HEAD").stdout.strip() + f.git(f.repo, "push", "origin", "feature/parent") + f.git(self.wt, "fetch", "origin", + "feature/parent:refs/remotes/origin/feature/parent") + # The shared refs/heads namespace means the worktree already + # sees refs/heads/feature/parent (it was created in f.repo via + # `checkout -b feature/parent`); the test must reject even + # when this local branch exists, so leave it intact. + self.assertEqual( + f.git(self.wt, "rev-parse", "--verify", + "refs/heads/feature/parent").stdout.strip(), parent_head) + f.git(self.wt, "branch", "feature/parent-child", parent_head) + f.git(self.wt, "checkout", "feature/parent-child") + # (2) Write the child card on the child branch so branch_card + # finds it (the file is named after the branch slug) and the + # resume's immediate_target + parent_head_sha fields resolve + # to feature/parent and the recorded parent tip. + child_card = self.wt / ".task" / "feature-parent-child.md" + self.assertFalse(child_card.exists()) + child_card.parent.mkdir(exist_ok=True) + child_card.write_text( + "# feature/parent-child\n" + "任务主记录:fixture;状态:进行中;owner:Lifecycle Test\n" + f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {r0}\n" + f"- 工作区:{self.wt}(worktree);分支:feature/parent-child\n" + "- 分支类型:child;initiative_id:feature-parent;root_target:main;" + "immediate_target:feature/parent;accepting_requirements:false;" + "parent_branch:feature/parent;" + f"parent_head_sha:{parent_head};" + "parent_pr:无;parent_source:local(非正式准入)\n" + ) + f.git(self.wt, "add", str(child_card.relative_to(self.wt))) + f.git(self.wt, "commit", "-m", "Register child fixture task") + # (3) Capture a local receipt on the child branch BEFORE + # removing the bare remote ref, so the receipt reflects the + # state where the remote parent ref is still present. + receipt = self.capture() + original = receipt.read_bytes() + receipt_sha = json.loads(original)["receipt_sha256"] + head = f.git(self.wt, "rev-parse", "HEAD").stdout + child_card_before = child_card.read_bytes() + # (4) Remove ONLY refs/heads/feature/parent from the bare + # remote. The worktree's origin URL still points at the + # disposable bare remote and refs/heads/main stays intact, so + # the only gap in authority is the absent remote parent ref. + # The local refs/heads/feature/parent branch remains in the + # worktree (this is the "even if a local parent branch + # exists" half of the scenario). + self.assertEqual( + f.git(f.remote, "rev-parse", "--verify", + "refs/heads/feature/parent").stdout.strip(), parent_head) + f.git(f.remote, "update-ref", "-d", "refs/heads/feature/parent") + # Confirm the bare remote now refuses to serve the parent ref + # while still serving main (so origin is reachable, just + # missing the registered immediate parent ref). + self.assertNotEqual( + f.git(f.remote, "show-ref", "--verify", "--hash", + "refs/heads/feature/parent", check=False).returncode, 0) + self.assertEqual( + f.git(f.remote, "rev-parse", "--verify", + "refs/heads/main").stdout.strip(), r0) + # (5) Local resume must refuse with REMOTE_UNVERIFIED instead + # of silently accepting the local parent ref. No [resumed] + # line, the receipt is not imported (card bytes unchanged and + # receipt_sha256 not embedded), the receipt file is byte-for- + # byte unchanged, and HEAD is preserved. + result = self.resume(receipt, mode="local") + self.assertNotEqual(result.returncode, 0, + f"local child resume must reject when origin is reachable " + f"but the registered parent ref is missing; current code " + f"falls through resolve_parent_ref's rc=2 case and accepts " + f"the local parent, returning 0 and printing [resumed]; " + f"stdout={result.stdout}") + self.assertIn("[verdict] REMOTE_UNVERIFIED", result.stdout, + "the resume must surface REMOTE_UNVERIFIED for the reachable-but-" + "missing-parent scenario so the owner can reconcile the parent " + f"initiative instead of silently resuming on an unsynced tip; " + f"stdout={result.stdout}") + self.assertNotIn("[resumed]", result.stdout, + "the resume must NOT import the receipt under a reachable " + "origin whose registered parent ref is missing — accepting " + "the local-only parent would advertise delivery readiness " + f"for an initiative the worktree cannot reconcile; stdout={result.stdout}") + self.assertEqual(child_card.read_bytes(), child_card_before, + "the child card must not be updated with receipt_sha256 when " + "the resume refuses to import the receipt") + self.assertEqual(receipt.read_bytes(), original, + "the receipt file must be byte-for-byte unchanged when the " + "resume refuses to import it") + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head, + "the child HEAD must not move when the resume refuses to " + "import the receipt") + self.assertNotIn(receipt_sha, child_card.read_text(), + "the child card must not embed the receipt sha256 when " + "the resume refuses to import the receipt") + def test_local_precheck_rejects_reachable_uncomparable_target(self): """Origin advertises a target SHA but the advertised object is unavailable locally, so ancestry cannot be compared against the From 105901595805cc5fa5ce19f1a6d48317b950d6af Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 15:05:22 +0000 Subject: [PATCH 32/39] agent-exec transport d47c50b0ed7fa9d2d7e0b4d58d5bd564/a79c762899de --- policy/governance.md | 4 ++-- scripts/task.sh | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/policy/governance.md b/policy/governance.md index 00f5ed5f..3fe29728 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -44,8 +44,8 @@ make task-start NAME=feature/my-task TRIAGE_RECEIPT=/tmp/my-task-triage.json PRE 默认 `TASK_WORK_MODE=delivery`,继续要求严格 READY 和当前目标一致。 工作区忙或落后时先由 owner 保存现场并同步。远端不可达时,delivery 恢复仍以 REMOTE_UNVERIFIED 拒绝; 显式 `TASK_WORK_MODE=local` 可在先核验收据、活动任务、原 R0/HEAD/工作区范围后导入 LOCAL_READY 收据, -继续已授权局部工作(子任务以已登记的本地父分支为参考,且须包含记录的 `parent_head_sha`)。远端可达但目标缺失、 -回退/分叉或无法比较时仍拒绝。离线不得建任务、同步、PR 检查、Review/合入或交付,本地参考不视为最新或可交付。 +继续已授权局部工作(子任务仅在远端不可查询时以已登记的本地父分支为参考,且须包含记录的 `parent_head_sha`)。 +远端可达但目标或已登记父分支缺失(即使本地同名分支存在)、回退/分叉或无法比较时仍以 REMOTE_UNVERIFIED 拒绝,不导入收据。离线不得建任务、同步、PR 检查、Review/合入或交付,本地参考不视为最新或可交付。 局部恢复接口已具备,正式交付仍按[合入调度](delivery.md#合入调度)的单一最终窗口执行。 启用后,既有任务可以显式选择 local,保持原 R0,标记落后,继续已授权局部工作: diff --git a/scripts/task.sh b/scripts/task.sh index 9d1f6fee..da30cc4a 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -663,7 +663,7 @@ 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 取远端与本地包含对方的一侧,远端不可查询时取本地) +resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验的提交(delivery 仅远端;local 取远端与本地包含对方的一侧,远端可达但父分支缺失返回 6,仅远端不可查询时取本地) git check-ref-format "refs/heads/$1" >/dev/null || die "invalid parent branch $1" if [ "$(work_mode)" = delivery ]; then # delivery 只认 origin 上刚取回的父分支头:本地领先/分叉意味着未准入的提交,不得被同步或合入。 @@ -684,7 +684,11 @@ resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验 return 6 fi remote=$(git rev-parse --verify "refs/remotes/origin/$1^{commit}") - elif [ "$rc" -ne 2 ]; then + elif [ "$rc" -eq 2 ]; then + # 远端可达而登记的父分支缺失:权威侧已不认该父分支,本地同名分支不得替代。 + echo "[verdict] REMOTE_UNVERIFIED — origin is reachable but $1 is missing; preserve local work and reconcile the parent initiative" >&2 + return 6 + else # 远端不可查询时只把已登记的本地父分支作为局部续作参考;不声称其为最新或可交付,调用方仍核验 parent_head_sha。 local_ref=$(git rev-parse -q --verify "refs/heads/$1^{commit}" 2>/dev/null || true) if [ -z "$local_ref" ]; then From 06b6ceea5f232ab472b8640cbe5eae11e7296e36 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 15:16:42 +0000 Subject: [PATCH 33/39] Record U2 offline resume and parent-reference closure Why: Preserve final root/child offline resume behavior and its reachable-parent-missing guard. Evidence: 80 evidence/recovery tests and all 491 governance tests pass (18 expected skips) on the code candidate. Impact: Local resume remains bounded; known missing remote parent refuses, network outage may use the registered local parent. Final-head CI remains required. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index f4c3f37c..1bd67c43 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -47,6 +47,8 @@ | U2 offline resume tests | standalone/child local resume during origin outage | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 8d8585afec66 | 0 | /tmp/norm-remediation-u2-offline-resume-tests-receipt.json | | U2 offline resume fix | local-only receipt resume with target history and parent R0 checks | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | f81326bd0836 | 0 | /tmp/norm-remediation-u2-offline-resume-fix-receipt.json | | U2 no-cache R0 fix | compare reachable targets to registered R0 without cached ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 1ff6444d3352 | 0 | /tmp/norm-remediation-u2-precheck-r0-fix-receipt.json | +| U2 reachable-missing-parent test | reject known-absent remote child target | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 5b5537aa0fc4 | 0 | /tmp/norm-remediation-u2-child-missing-parent-test-receipt-2.json | +| U2 reachable-missing-parent fix | local child resume only falls back on remote outage | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 105901595805 | 0 | /tmp/norm-remediation-u2-child-missing-parent-fix-receipt.json | | U2 precheck R0 regression tests | reachable/missing/forward target without cached ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | fbb2aacb71ba | 0 | /tmp/norm-remediation-u2-no-cache-history-tests-receipt.json | | U2 offline resume regression tests | root/child receipt import while origin unreachable | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 8d8585afec66 | 0 | /tmp/norm-remediation-u2-offline-resume-tests-receipt.json | | U2 offline resume implementation | local resume on R0/local registered parent | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | f81326bd0836 | 0 | /tmp/norm-remediation-u2-offline-resume-fix-receipt.json | @@ -72,6 +74,10 @@ | 2026-10-18T13:40Z | `bash -n scripts/task.sh && make test-governance` | 9c0e7657083259759f5bd1bed6921471411dba2c | 0 | SELF | 872bbc3437ae83e3 | | 2026-10-18T14:41Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | bf12f030894e1530 | | 2026-10-18T14:40Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 8e81c5353849fbd5 | +| 2026-10-18T15:06Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | cf56a1870daa88cc | +| 2026-10-18T15:06Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 3249de89a278379f | +| 2026-10-18T15:06Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 105901595805cc5fa5ce19f1a6d48317b950d6af | 0 | SELF | cf56a1870daa88cc | +| 2026-10-18T15:06Z | `bash -n scripts/task.sh && make test-governance` | 105901595805cc5fa5ce19f1a6d48317b950d6af | 0 | SELF | 3249de89a278379f | | 2026-10-18T14:46Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 3821724d4545beda | | 2026-10-18T14:47Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 048ce6f79e025863 | @@ -84,6 +90,10 @@ - 新增执行偏差:修复闭合任务与目标回退时,worker 在同一批次向 `scripts/evidence.py` 和 `scripts/task.sh` 发出两次互不重叠编辑,违反了任务包的串行编辑要求;最终 diff 已逐项审阅,定向与完整治理测试均通过,但该结果不能证明提示具备工具级强制力。 - Review 路由记录:第一次 PR 对抗性 cross-model job 因 orchestrator 使用了不匹配的约束文件名,在 provider egress 前跳过,未把代码发送给外部模型;job 目录已清理。该审阅不计入 Review 覆盖,最终 head 使用本地 adversarial reviewer。 - 后续 Review 补充(覆盖前述状态):关闭任务 precheck、reachable-but-uncomparable/缺失 target、无 tracking ref 时需查询 advertised target、以及离线 `cmd_resume` 的 fetch-first 缺口,均已补 test-first 回归与修复。root 离线 resume 使用登记 R0;child resume 仅使用已登记、包含 `parent_head_sha` 的本地 parent 作为局部参考,delivery 仍 fetch-first。最终候选完整 Review 与 exact-head CI 待完成。 +- 最新 child 边界修复:origin 可达但已登记 parent ref 缺失时,local resume 不再回退本地 parent;origin 确实不可查询时仍可使用本地 parent,并验证 `parent_head_sha`。 +- 最新代码验证:host `tests.test_evidence tests.test_task_recovery` 80/80 通过,`bash -n scripts/task.sh && make test-governance` 491 项通过、18 项 skip,测试对象为 `105901595805cc5fa5ce19f1a6d48317b950d6af`。此前 CI green head 尚早于这些离线恢复修复,须以最终 PR head 重跑 GATE。 +- 最新验证覆盖 80 个 evidence/recovery 测试与 491 项治理测试(18 项 skip),以上命令都成功;测试对象为当前实现代码 `105901595805cc5fa5ce19f1a6d48317b950d6af`。若后续只改任务卡,代码测试可复用但 CI head 会变,须在最终提交 head 重新等待 GATE。 +- child 父目标补充:若 origin 可达但已登记 parent branch 不存在,local resume 返回 REMOTE_UNVERIFIED,不再使用本地 parent;只有远端确实不可查询时才回退本地 parent,并仍核验 `parent_head_sha`。delivery 仍保持 fetch-first。 - 离线 resume 已端到端验证:root 与 child 任务在捕获有效 LOCAL_READY 收据后、origin 不可达时可用 `TASK_WORK_MODE=local make task-resume` 导入;child 仅使用本地已登记 parent 并核对 `parent_head_sha`。同一环境下 delivery resume 仍以 REMOTE_UNVERIFIED 拒绝。 - 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 - 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 From 59c27869bf778431ae468622355ffabdd9708b7b Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 15:26:49 +0000 Subject: [PATCH 34/39] agent-exec transport adeb3ec7854d8724c54dcac15493e3b5/523f5b51ad61 --- tests/test_task_recovery.py | 366 +++++++++++++++++++++++++++++++----- 1 file changed, 315 insertions(+), 51 deletions(-) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 12eb08cc..56846b50 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -736,21 +736,42 @@ class TaskRecoveryTests(unittest.TestCase): self.assertEqual(f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout, cached_before) - def write_no_cache_wrapper(self, bin_dir, advertised, log_name="git.log"): - """Create a disposable git wrapper for the missing-tracking-ref scenarios. + def write_no_cache_wrapper(self, bin_dir, advertised, log_name="git.log", + auth_fail=False): + """Create a disposable git wrapper for the missing-tracking-ref and + reachable-auth-failure scenarios. The wrapper's intercept set is + parameterised so a single helper produces both no-cache (default) + and auth-failure fixtures; each test still owns its own bin_dir so + no fixtures are shared across tests. - Intercepts: - fetch -> fail (origin unreachable; the wrapper itself exits 128 - and writes the standard "unable to access origin" stderr line - so the precheck captures the same `fetch_err` it would observe - against a real unreachable remote) - ls-remote origin refs/heads/main -> configured ADVERTISED (or + When auth_fail=False (default, no-cache scenario): + fetch -> fail (128); the wrapper + writes the standard "unable to access origin" stderr line so + the precheck captures the same `fetch_err` it would observe + against a real unreachable remote + ls-remote origin refs/heads/main -> serve ADVERTISED (or nothing when ADVERTISED is empty, simulating an origin that has no configured target branch) - branch --merged origin/main -> empty output (the cached - tracking ref is deleted locally; the real git would otherwise - fail the pipeline under set -e/pipefail and the precheck would - silently exit before the verdict is printed) + branch --merged origin/main -> DELEGATED to REAL_GIT. + The earlier stub masked a production-pipeline bug: with the + cached refs/remotes/origin/main deleted, the real git call + fails under set -e/pipefail and the precheck silently exits + before the verdict is printed. The stub has been removed so + the tests now exercise the actual missing-ref strict-shell + path; the post-fix implementation must reach LOCAL_READY (or + REMOTE_UNVERIFIED) without the helper stub, and the test + records the expected red result on current code. + + When auth_fail=True (reachable-but-auth-failure scenario): + fetch -> fail (128) and write + "fatal: Authentication failed for origin (HTTP 401)" + ls-remote origin refs/heads/main -> fail (128) and write + "fatal: Authentication failed (HTTP 401)"; no value served. + This simulates a reachable origin that refuses to disclose + its target head — the production code must NOT promote the + registered task to LOCAL_READY (or [resumed] for resume) just + because ls-remote returned an auth error instead of an + advertised SHA. All other git calls delegate to the real binary via the resolved REAL_GIT absolute path (and a stripped PATH so the wrapper cannot @@ -764,21 +785,52 @@ class TaskRecoveryTests(unittest.TestCase): bin_dir.mkdir(exist_ok=True) wrapper_log = bin_dir / log_name wrapper = bin_dir / "git" + if auth_fail: + header_log = "echo \"configured auth-failure-mode\" >> \"$LOG\"\n" + fetch_block = ( + " fetch)\n" + " echo \"fetch-auth-failure\" >> \"$LOG\"\n" + " echo 'fatal: Authentication failed for origin (HTTP 401)' >&2\n" + " exit 128 ;;\n" + ) + ls_remote_block = ( + " ls-remote)\n" + " if [ \"${2:-}\" = origin ] && [ \"${3:-}\" = refs/heads/main ]; then\n" + " echo \"ls-remote-auth-failure advertised-target=\" >> \"$LOG\"\n" + " echo 'fatal: Authentication failed (HTTP 401)' >&2\n" + " exit 128\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + ) + else: + header_log = f"echo \"configured advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + fetch_block = ( + " fetch)\n" + " echo \"fetch-intercepted\" >> \"$LOG\"\n" + " echo 'fatal: unable to access origin' >&2\n" + " exit 128 ;;\n" + ) + ls_remote_block = ( + " ls-remote)\n" + " if [ \"${2:-}\" = origin ] && [ \"${3:-}\" = refs/heads/main ]; then\n" + " echo \"ls-remote-served advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + " if [ -n \"$ADVERTISED\" ]; then\n" + " printf '%s\\trefs/heads/main\\n' \"$ADVERTISED\"\n" + " fi\n" + " exit 0\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + ) wrapper.write_text( "#!/usr/bin/env bash\n" - "# Disposable fixture wrapper for the missing-tracking-ref\n" - "# scenarios:\n" - "# fetch -> fail (128)\n" - "# ls-remote origin refs/heads/main -> $ADVERTISED\n" - "# branch --merged origin/main -> empty\n" - "# Anything else delegates to the real binary so the registered\n" - "# task context (HEAD, branch, R0, worktree, .task/) stays\n" - "# observable normally.\n" + "# Disposable fixture wrapper for the missing-tracking-ref and\n" + "# reachable-auth-failure scenarios. See write_no_cache_wrapper\n" + "# docstring for the intercept set.\n" f"REAL_GIT={shlex.quote(real_git)}\n" f"LOG={shlex.quote(str(wrapper_log))}\n" f"ADVERTISED={shlex.quote(advertised)}\n" "WRAPPER_BIN=$(cd \"$(dirname \"$0\")\" && pwd)\n" - "echo \"configured advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + + header_log + "stripped=\"\"\n" "old_ifs=\"$IFS\"\n" "IFS=':'\n" @@ -789,29 +841,7 @@ class TaskRecoveryTests(unittest.TestCase): "IFS=\"$old_ifs\"\n" "export PATH=\"$stripped\"\n" "case \"$1\" in\n" - " fetch)\n" - " echo \"fetch-intercepted\" >> \"$LOG\"\n" - " echo 'fatal: unable to access origin' >&2\n" - " exit 128 ;;\n" - " ls-remote)\n" - " if [ \"${2:-}\" = origin ] && [ \"${3:-}\" = refs/heads/main ]; then\n" - " echo \"ls-remote-served advertised-target=$ADVERTISED\" >> \"$LOG\"\n" - " if [ -n \"$ADVERTISED\" ]; then\n" - " printf '%s\\trefs/heads/main\\n' \"$ADVERTISED\"\n" - " fi\n" - " exit 0\n" - " fi\n" - " exec \"$REAL_GIT\" \"$@\" ;;\n" - " branch)\n" - " # refs/remotes/origin/main is deleted locally; the real git\n" - " # `git branch --merged origin/main` would fail and the\n" - " # precheck pipeline (with pipefail + set -e) would silently\n" - " # exit. Stub it so the verdict is actually emitted.\n" - " if [ \"${2:-}\" = --merged ] && [ \"${3:-}\" = origin/main ]; then\n" - " echo \"branch-merged-stub no-merged-branches\" >> \"$LOG\"\n" - " exit 0\n" - " fi\n" - " exec \"$REAL_GIT\" \"$@\" ;;\n" + + fetch_block + ls_remote_block + "esac\n" "exec \"$REAL_GIT\" \"$@\"\n") wrapper.chmod(0o755) @@ -848,15 +878,18 @@ class TaskRecoveryTests(unittest.TestCase): local = f.task(self.wt, "precheck", check=False, env=wrapper_env | {"TASK_WORK_MODE": "local"}) - # Proof: the wrapper was loaded (configured marker) and served - # the failure case (fetch intercepted). ls-remote is served by - # the wrapper if and when the post-fix implementation queries it - # for the missing cached-ref path; the current implementation - # short-circuits before ls-remote, so the assertion below does - # not require an ls-remote entry. + # Proof the wrapper was actually used: the configured marker + # names the advertised SHA the test asked the wrapper to serve, + # `fetch` was rejected (so the unreachable remote is observable + # to the production pipeline), and `ls-remote origin refs/heads/main` + # was answered with the synthetic SHA the wrapper was configured + # to serve. The last assertion is required because reachable + # advertised cases must verify the wrapper served the expected + # ls-remote response, not only that the wrapper initialized. log = wrapper_log.read_text() self.assertIn(f"configured advertised-target={synthetic}", log) self.assertIn("fetch-intercepted", log) + self.assertIn(f"ls-remote-served advertised-target={synthetic}", log) self.assertNotEqual(local.returncode, 0, local.stdout) self.assertNotIn("[verdict] LOCAL_READY", local.stdout) @@ -891,9 +924,14 @@ class TaskRecoveryTests(unittest.TestCase): env=wrapper_env | {"TASK_WORK_MODE": "local"}) log = wrapper_log.read_text() - # Empty advertised value means the wrapper emits no ls-remote line. + # Empty advertised value means the wrapper serves no ls-remote line. + # The configured marker still records the empty advertised value, + # and the served marker is logged with an empty advertised-target so + # the test can confirm the wrapper was actually invoked for the + # ls-remote call (not only that the wrapper binary was loaded). self.assertIn("configured advertised-target=", log) self.assertIn("fetch-intercepted", log) + self.assertIn("ls-remote-served advertised-target=", log) self.assertNotEqual(local.returncode, 0, local.stdout) self.assertNotIn("[verdict] LOCAL_READY", local.stdout) @@ -933,12 +971,238 @@ class TaskRecoveryTests(unittest.TestCase): log = wrapper_log.read_text() self.assertIn(f"configured advertised-target={r0}", log) self.assertIn("fetch-intercepted", log) + # Reachable advertised cases must verify the wrapper actually + # served the expected ls-remote response (the registered R0), not + # only that the wrapper binary was loaded. + self.assertIn(f"ls-remote-served advertised-target={r0}", log) self.assertEqual(local.returncode, 0, local.stdout) self.assertIn("[verdict] LOCAL_READY", local.stdout) self.assertNotIn("[verdict] REMOTE_UNVERIFIED", local.stdout) self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + def test_local_child_resume_rejects_missing_parent_sha(self): + """Child: configure the task card as child with + immediate_target=feature/parent and parent_branch=feature/parent, + create that parent branch locally (no origin fetch), branch off + it into feature/parent-child, but DELIBERATELY OMIT + parent_head_sha from the child card so the recorded parent head + is empty. Make origin unreachable, capture a local receipt, + then assert `task resume` in local mode refuses to import the + receipt. + + The registered task cannot be reconciled against any + authoritative parent head when parent_head_sha is missing + AND origin is unreachable: the local branch tip is the only + handle, but without a recorded parent_head_sha the production + code cannot verify the local tip is the tip the child branched + off of. The resume must refuse with REMOTE_UNVERIFIED, must + not emit `[resumed]`, must not import the receipt (the child + card stays byte-for-byte unchanged and the receipt_sha256 is + not embedded), and HEAD must remain untouched. + + Currently RED on current code: cmd_resume only runs the + parent_head_sha ancestry check when `[ -n "$recorded" ]` is + true; an empty parent_head_sha short-circuits the check and + lets the resume fall through to IMPORT_RECEIPT and the + `[resumed]` line. This test records the expected red result so + the post-fix implementation enforces the parent_head_sha + presence contract under the local-outage fallback path. + + Preserves test_local_child_resume_uses_local_parent_when_origin_is_unreachable + (the real-outage-with-valid-parent case) and + test_local_child_resume_rejects_reachable_missing_parent (the + reachable-known-missing-parent case).""" + f = self.fixture + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # (1) Create the parent initiative branch in the main repo so + # the parent has a deterministic tip commit, push it to the + # bare remote so the worktree can fetch it once at the start, + # then create the child branch on top of the parent inside the + # worktree and switch the worktree to it. We do NOT push or + # fetch parent in the worktree because the test explicitly + # makes origin unreachable before the resume step. + f.git(f.repo, "checkout", "-b", "feature/parent") + (f.repo / "parent-tip").write_text("authorized parent initiative tip\n") + f.git(f.repo, "add", "parent-tip") + f.git(f.repo, "commit", "-m", "Parent initiative tip commit") + parent_head = f.git(f.repo, "rev-parse", "HEAD").stdout.strip() + f.git(f.repo, "push", "origin", "feature/parent") + # The worktree shares the refs/heads namespace with f.repo, + # so `checkout -b feature/parent` in f.repo already materialises + # refs/heads/feature/parent inside the worktree. The test must + # refuse even when this local branch exists, so leave it intact. + f.git(self.wt, "branch", "feature/parent-child", parent_head) + f.git(self.wt, "checkout", "feature/parent-child") + # (2) Write the child card on the child branch so branch_card + # finds it (the file is named after the branch slug) and the + # resume's immediate_target + parent_branch fields resolve to + # feature/parent. The card DELIBERATELY OMITS parent_head_sha so + # the recorded parent head is empty. + child_card = self.wt / ".task" / "feature-parent-child.md" + self.assertFalse(child_card.exists()) + child_card.parent.mkdir(exist_ok=True) + child_card.write_text( + "# feature/parent-child\n" + "任务主记录:fixture;状态:进行中;owner:Lifecycle Test\n" + f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {r0}\n" + f"- 工作区:{self.wt}(worktree);分支:feature/parent-child\n" + "- 分支类型:child;initiative_id:feature-parent;root_target:main;" + "immediate_target:feature/parent;accepting_requirements:false;" + "parent_branch:feature/parent;" + "parent_pr:无;parent_source:local(非正式准入)\n" + ) + f.git(self.wt, "add", str(child_card.relative_to(self.wt))) + f.git(self.wt, "commit", "-m", "Register child fixture task without parent_head_sha") + # (3) Capture a local receipt on the child branch BEFORE + # making origin unreachable. + receipt = self.capture() + original = receipt.read_bytes() + receipt_sha = json.loads(original)["receipt_sha256"] + head = f.git(self.wt, "rev-parse", "HEAD").stdout + child_card_before = child_card.read_bytes() + # (4) Break the origin URL so the offline scenario is real. + f.git(self.wt, "remote", "set-url", "origin", str(f.root / "missing.git")) + # Confirm the local parent branch still exists at the recorded + # tip (resolve_parent_ref would otherwise fall through to a + # bare error before the missing parent_head_sha check matters). + self.assertEqual( + f.git(self.wt, "rev-parse", "--verify", + "refs/heads/feature/parent").stdout.strip(), parent_head) + # (5) Local resume must refuse with REMOTE_UNVERIFIED because + # the recorded parent_head_sha is empty: the local parent tip + # alone is not a sufficient handle for authorized continuation + # when origin is unreachable. No [resumed] line, the receipt is + # not imported (card bytes unchanged and receipt_sha256 not + # embedded), the receipt file is byte-for-byte unchanged, and + # HEAD is preserved. + result = self.resume(receipt, mode="local") + self.assertNotEqual(result.returncode, 0, + f"local child resume must reject when origin is unreachable " + f"and the recorded parent_head_sha is empty; current code " + f"short-circuits the empty recorded check and lets IMPORT_RECEIPT " + f"run, returning 0 and printing [resumed]; " + f"stdout={result.stdout}") + self.assertNotIn("[resumed]", result.stdout, + "the resume must NOT import the receipt when the registered " + "parent_head_sha is empty and origin is unreachable — the local " + "parent tip alone is not a sufficient handle for authorized " + f"continuation under the offline fallback path; stdout={result.stdout}") + self.assertIn("[verdict] REMOTE_UNVERIFIED", result.stdout, + "the resume must surface REMOTE_UNVERIFIED for the unreachable-" + "origin-and-missing-parent_head_sha scenario so the owner can " + "backfill the recorded parent head before retrying; " + f"stdout={result.stdout}") + self.assertEqual(child_card.read_bytes(), child_card_before, + "the child card must not be updated with receipt_sha256 when " + "the resume refuses to import the receipt") + self.assertEqual(receipt.read_bytes(), original, + "the receipt file must be byte-for-byte unchanged when the " + "resume refuses to import it") + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head, + "the child HEAD must not move when the resume refuses to " + "import the receipt") + self.assertNotIn(receipt_sha, child_card.read_text(), + "the child card must not embed the receipt sha256 when " + "the resume refuses to import the receipt") + + def test_local_resume_rejects_reachable_auth_failure(self): + """Standalone: capture a valid TASK_WORK_MODE=local precheck + receipt against the real bare remote (origin is reachable and + serves refs/heads/main normally), then point the worktree at a + disposable wrapper that makes `fetch` and + `ls-remote origin refs/heads/main` both fail with explicit + "Authentication failed (HTTP 401)" and run `task resume` in + local mode. + + The wrapper proves the wrapper was actually used (configured + marker, fetch-auth-failure, ls-remote-auth-failure). The resume + must NOT yield the local-continuation positive signal: it + must refuse with REMOTE_UNVERIFIED, must not emit `[resumed]`, + must not import the receipt (the standalone card stays byte- + for-byte unchanged and the receipt_sha256 is not embedded), + the receipt file must be byte-for-byte unchanged, and HEAD + must remain untouched. + + Currently RED on current code: when fetch and ls-remote both + fail (auth error), `target_history` sees an empty listing and + short-circuits to "ok", so the unreachable-fallback branch + falls through to IMPORT_RECEIPT and the `[resumed]` line even + though the authoritative target head cannot be confirmed. + This test records the expected red result so the post-fix + implementation refuses the auth-failure scenario instead of + silently promoting the registered task to local continuation. + + The test owns its own disposable git wrapper so the auth + failure scenario does not leak into the wrapper shared with + the no-cache tests; the existing + test_local_resume_continues_when_origin_is_unreachable + (unreachable origin with the bare remote URL rewritten) and + test_local_child_resume_uses_local_parent_when_origin_is_unreachable + (unreachable origin with a child task) are preserved.""" + f = self.fixture + # (1) Capture a valid local receipt while origin is reachable + # so the receipt reflects the registered task state under the + # disposable bare remote. The capture itself runs without the + # wrapper so the receipt is a real artifact, not a fake. + receipt = self.capture() + original = receipt.read_bytes() + receipt_sha = json.loads(original)["receipt_sha256"] + head = f.git(self.wt, "rev-parse", "HEAD").stdout + card_before = self.card.read_bytes() + # (2) Build the disposable auth-failure wrapper so the wrapper + # intercepts fetch (128, Authentication failed) and + # ls-remote origin refs/heads/main (128, Authentication failed, + # no value served) while everything else delegates to the real + # git. The wrapper's own marker line proves it was loaded. + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "auth-failure-bin", "", log_name="auth-failure.log", + auth_fail=True) + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = (f.env | {"PATH": path_env} + | {"TASK_WORK_MODE": "local", + "PRECHECK_RECEIPT": str(receipt)}) + # (3) Local resume under the auth-failure wrapper must refuse. + result = f.task(self.wt, "resume", check=False, env=wrapper_env) + # Proof the wrapper was actually used. + log = wrapper_log.read_text() + self.assertIn("configured auth-failure-mode", log) + self.assertIn("fetch-auth-failure", log) + self.assertIn("ls-remote-auth-failure", log) + # Auth-failure scenarios must assert the wrapper actually + # served the expected (auth) response, not only that the + # wrapper initialized. + self.assertNotEqual(result.returncode, 0, + f"local resume must reject when fetch and ls-remote both " + f"return Authentication failed (HTTP 401); current code " + f"falls through target_history's empty-listing short-" + f"circuit and reaches IMPORT_RECEIPT, returning 0 and " + f"printing [resumed]; stdout={result.stdout}") + self.assertIn("[verdict] REMOTE_UNVERIFIED", result.stdout, + "the resume must surface REMOTE_UNVERIFIED when the reachable " + "origin returns Authentication failed (HTTP 401) instead of an " + "advertised target head; the post-fix implementation must not " + "promote the registered task to local continuation under an " + f"authoritative-but-unverifiable remote; stdout={result.stdout}") + self.assertNotIn("[resumed]", result.stdout, + "the resume must NOT import the receipt when the reachable " + "origin returns Authentication failed (HTTP 401); accepting " + "the registered task would advertise delivery readiness for a " + f"target head that cannot be confirmed against origin; " + f"stdout={result.stdout}") + self.assertEqual(self.card.read_bytes(), card_before, + "the registered task card must not be updated with " + "receipt_sha256 when the resume refuses to import the receipt") + self.assertEqual(receipt.read_bytes(), original, + "the receipt file must be byte-for-byte unchanged when the " + "resume refuses to import it") + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head, + "the task HEAD must not move when the resume refuses to " + "import the receipt") + self.assertNotIn(receipt_sha, self.card.read_text(), + "the registered task card must not embed the receipt sha256 " + "when the resume refuses to import the receipt") + if __name__ == "__main__": unittest.main() From bc304111a47f45c5ce7b485b826d4ec6ca6e5feb Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 15:41:58 +0000 Subject: [PATCH 35/39] agent-exec transport ba0e59b31b8b4e18bc7e8df0ab680c80/ccf1734d8c5b --- tests/test_task_recovery.py | 101 +++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 7 deletions(-) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 56846b50..ded3c923 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -940,8 +940,9 @@ class TaskRecoveryTests(unittest.TestCase): def test_local_precheck_without_cached_ref_allows_comparable_forward_target(self): """Cached refs/remotes/origin/main is absent; origin advertises - the task's R0 (or a forward descendant that is locally - available). Local precheck must continue to advertise + a forward descendant of the task's R0 (the descendant is + locally available so ancestry can be compared against the + registered baseline). Local precheck must continue to advertise LOCAL_READY — the explicit missing-tracking-ref continuation contract — because the registered task context is the same as the cached ref would have represented, and the ls-remote @@ -957,11 +958,20 @@ class TaskRecoveryTests(unittest.TestCase): f = self.fixture head_before = f.git(self.wt, "rev-parse", "HEAD").stdout r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # Advance the bare remote so the advertised target is a real + # forward descendant of R0 (not R0 itself). The descendant must + # be locally available so the wrapper can advertise it AND the + # production `cat-file -e` check inside target_history() succeeds + # when the descendant is compared against the registered R0. + f.advance_remote() + forward = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + self.assertNotEqual(forward, r0) + self.assertEqual(f.git(f.repo, "merge-base", r0, forward).stdout.strip(), r0) f.git(self.wt, "branch", "--unset-upstream", "feature/test") f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") bin_dir, wrapper_log = self.write_no_cache_wrapper( - f.root / "no-cache-comparable-bin", r0) + f.root / "no-cache-comparable-bin", forward) path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" wrapper_env = f.env | {"PATH": path_env} @@ -969,12 +979,12 @@ class TaskRecoveryTests(unittest.TestCase): env=wrapper_env | {"TASK_WORK_MODE": "local"}) log = wrapper_log.read_text() - self.assertIn(f"configured advertised-target={r0}", log) + self.assertIn(f"configured advertised-target={forward}", log) self.assertIn("fetch-intercepted", log) # Reachable advertised cases must verify the wrapper actually - # served the expected ls-remote response (the registered R0), not - # only that the wrapper binary was loaded. - self.assertIn(f"ls-remote-served advertised-target={r0}", log) + # served the expected ls-remote response (a forward descendant of + # the registered R0), not only that the wrapper binary was loaded. + self.assertIn(f"ls-remote-served advertised-target={forward}", log) self.assertEqual(local.returncode, 0, local.stdout) self.assertIn("[verdict] LOCAL_READY", local.stdout) @@ -1203,6 +1213,83 @@ class TaskRecoveryTests(unittest.TestCase): "the registered task card must not embed the receipt sha256 " "when the resume refuses to import the receipt") + def test_local_precheck_rejects_reachable_auth_failure(self): + """Standalone: capture a valid TASK_WORK_MODE=local precheck + receipt against the real bare remote (origin is reachable and + serves refs/heads/main normally), then point the worktree at a + disposable wrapper that makes `fetch` and + `ls-remote origin refs/heads/main` both fail with explicit + "Authentication failed (HTTP 401)" and run `task precheck` in + local mode. + + The wrapper proves the wrapper was actually used (configured + marker, fetch-auth-failure, ls-remote-auth-failure). The + precheck must NOT yield the local-continuation positive signal: + it must refuse with REMOTE_UNVERIFIED, must not advertise + LOCAL_READY, and HEAD must remain untouched. + + Currently RED on current code: when fetch and ls-remote both + fail (auth error), `target_history` sees an empty listing and + short-circuits to "ok", so the unreachable-fallback branch falls + through to LOCAL_READY even though the authoritative target + head cannot be confirmed. This test records the expected red + result so the post-fix implementation refuses the auth-failure + scenario instead of silently promoting the registered task to + local continuation. + + The test owns its own disposable git wrapper so the auth + failure scenario does not leak into the wrapper shared with + the no-cache tests; the existing + test_local_resume_rejects_reachable_auth_failure (unreachable + origin under resume) and test_local_child_resume_rejects_missing_parent_sha + (unreachable origin with a child task missing parent_head_sha) + are preserved.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + # (1) Build the disposable auth-failure wrapper so the wrapper + # intercepts fetch (128, Authentication failed) and + # ls-remote origin refs/heads/main (128, Authentication failed, + # no value served) while everything else delegates to the real + # git. The wrapper's own marker line proves it was loaded. + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "auth-failure-precheck-bin", "", + log_name="auth-failure-precheck.log", + auth_fail=True) + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = (f.env | {"PATH": path_env} + | {"TASK_WORK_MODE": "local"}) + # (2) Local precheck under the auth-failure wrapper must refuse. + local = f.task(self.wt, "precheck", check=False, env=wrapper_env) + # Proof the wrapper was actually used. + log = wrapper_log.read_text() + self.assertIn("configured auth-failure-mode", log) + self.assertIn("fetch-auth-failure", log) + self.assertIn("ls-remote-auth-failure", log) + # Auth-failure scenarios must assert the wrapper actually + # served the expected (auth) response, not only that the + # wrapper initialized. + self.assertNotEqual(local.returncode, 0, + f"local precheck must reject when fetch and ls-remote both " + f"return Authentication failed (HTTP 401); current code " + f"falls through target_history's empty-listing short-" + f"circuit and reaches LOCAL_READY, returning 0; " + f"stdout={local.stdout}") + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout, + "the precheck must surface REMOTE_UNVERIFIED when the reachable " + "origin returns Authentication failed (HTTP 401) instead of an " + "advertised target head; the post-fix implementation must not " + "promote the registered task to local continuation under an " + f"authoritative-but-unverifiable remote; stdout={local.stdout}") + self.assertNotIn("[verdict] LOCAL_READY", local.stdout, + "the precheck must NOT advertise LOCAL_READY when the " + "reachable origin returns Authentication failed (HTTP 401); " + "accepting the registered task would advertise local " + "continuation for a target head that cannot be confirmed " + f"against origin; stdout={local.stdout}") + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before, + "the task HEAD must not move when the precheck refuses to " + "advertise LOCAL_READY") + if __name__ == "__main__": unittest.main() From 471948c59cfabff36c018f219998139fea3757fe Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 15:46:44 +0000 Subject: [PATCH 36/39] agent-exec transport 7bc7519fb1dc6e9f12e152359a795c89/731d7f5ba3a5 --- scripts/task.sh | 51 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/scripts/task.sh b/scripts/task.sh index da30cc4a..164cafa4 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -18,10 +18,30 @@ fetch_target() { git rev-parse --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null \ || { echo "[verdict] REMOTE_UNVERIFIED — target commit missing" >&2; return 6; } } -target_history() { # $1=可选基线(无缓存目标时代替比较,如任务 R0)→ 输出 ok(远端不可查询/通告提交为基线后代)、rewound(回退或分叉)或 uncomparable(远端可查询但目标 ref 缺失、无基线、通告提交本地缺失或无法比较) +origin_transport_unavailable() { # $1=失败的远端命令 stderr → 仅明确的传输失败(DNS/连接超时/拒绝/重置/无路由,或本地缺失仓库路径)返回 0;认证/授权/权限与未知错误返回 1 + local err=$1 url path + if printf '%s\n' "$err" | grep -qiE 'authentication failed|permission denied|unauthori[sz]ed|forbidden|HTTP[^0-9]*40[13]|\b40[13]\b|could not read (username|password)|terminal prompts disabled|invalid credentials|access denied|host key verification failed'; then + return 1 + fi + if printf '%s\n' "$err" | grep -qiE 'could not resolve (host|hostname|proxy)|name or service not known|temporary failure in name resolution|nodename nor servname|connection timed out|operation timed out|timed out after|connection refused|connection reset|no route to host|network is unreachable|couldn.t connect to server'; then + return 0 + fi + printf '%s\n' "$err" | grep -qF 'does not appear to be a git repository' || return 1 + url=$(git remote get-url origin 2>/dev/null) || return 1 + case "$url" in + file://*) path=${url#file://} ;; + /*|./*|../*) path=$url ;; + *) return 1 ;; + esac + [ ! -e "$path" ] +} +target_history() { # $1=可选基线(无缓存目标时代替比较,如任务 R0)→ 输出 ok(远端传输不可达/通告提交为基线后代)、rewound(回退或分叉)或 uncomparable(认证/授权/未知错误、目标 ref 缺失、无基线、通告提交本地缺失或无法比较) local cached listing advertised rc=0 cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || cached=${1:-} - listing=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>/dev/null) || { echo ok; return 0; } + if ! listing=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>&1); then + if origin_transport_unavailable "$listing"; then echo ok; else echo uncomparable; fi + return 0 + fi advertised=$(printf '%s\n' "$listing" \ | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') [ -n "$advertised" ] && [ -n "$cached" ] || { echo uncomparable; return 0; } @@ -70,7 +90,10 @@ cmd_precheck() { 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 ' +') + merged="" + if git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" >/dev/null 2>&1; then + merged=$(git branch --merged "origin/${TARGET_BRANCH}" 2>/dev/null | { grep -vE 'main|master|\*' || true; } | tr -d ' +') + fi [ "${sub_uninit:-0}" -gt 0 ] && echo "[warn] ${sub_uninit} 个子模块未初始化——构建/测试前需物化(不阻断写入)" echo "[worktrees] linked=${wt}" [ -n "$merged" ] && echo "[recycle] merged un-recycled branches: $(echo $merged)" @@ -518,8 +541,18 @@ cmd_resume() { fi fi 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 [ "$mode" = local ]; then + # local 续作只以登记的父头为比较基准:缺失、非完整 SHA 或本地无此提交时不得回落到本地父分支。 + local sha_len=40 + [ "$(git rev-parse --show-object-format 2>/dev/null)" = sha256 ] && sha_len=64 + if ! [[ "$recorded" =~ ^[0-9a-f]+$ ]] || [ "${#recorded}" -ne "$sha_len" ] \ + || ! git cat-file -e "${recorded}^{commit}" 2>/dev/null; then + echo "[verdict] REMOTE_UNVERIFIED — registered parent_head_sha for ${immediate} is missing, malformed or not available locally; preserve local work, receipt not imported" >&2 + return 6 + fi + fi + observed=$(resolve_parent_ref "$immediate") || die "immediate target ${immediate} unavailable; receipt not imported" 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 @@ -676,8 +709,8 @@ resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验 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=$? + local remote="" local_ref="" rc=0 ls_err + ls_err=$(git ls-remote --exit-code --heads origin "refs/heads/$1" 2>&1 >/dev/null) || 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 @@ -688,8 +721,12 @@ resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验 # 远端可达而登记的父分支缺失:权威侧已不认该父分支,本地同名分支不得替代。 echo "[verdict] REMOTE_UNVERIFIED — origin is reachable but $1 is missing; preserve local work and reconcile the parent initiative" >&2 return 6 + elif ! origin_transport_unavailable "$ls_err"; then + # 认证/授权/权限或未知错误不等于远端不可达:权威侧状态未知,不得以本地父分支替代。 + echo "[verdict] REMOTE_UNVERIFIED — cannot verify origin/$1 (non-transport remote error); preserve local work and retry" >&2 + return 6 else - # 远端不可查询时只把已登记的本地父分支作为局部续作参考;不声称其为最新或可交付,调用方仍核验 parent_head_sha。 + # 远端传输不可达时只把已登记的本地父分支作为局部续作参考;不声称其为最新或可交付,调用方仍核验 parent_head_sha。 local_ref=$(git rev-parse -q --verify "refs/heads/$1^{commit}" 2>/dev/null || true) if [ -z "$local_ref" ]; then echo "[verdict] REMOTE_UNVERIFIED — cannot query origin/$1 and no local $1 exists; preserve local work and retry" >&2 From 398d12141c84453fb6db3dba2eaf9a208342e29f Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 15:58:11 +0000 Subject: [PATCH 37/39] Record final U2 recovery gates and test evidence Why: Preserve the final network-error classification, child-parent identity gate, and full governance test result. Evidence: 83 focused tests and all 494 governance tests passed (18 skips) on the code candidate; child outage/missing-parent contrast tests passed. Impact: New task and formal delivery gates remain strict; current PR head CI and final review are still required. Refs: U2 / PR #618 --- .task/archive/feature-norm-remediation-u2.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index 1bd67c43..a4507db1 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -49,9 +49,10 @@ | U2 no-cache R0 fix | compare reachable targets to registered R0 without cached ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 1ff6444d3352 | 0 | /tmp/norm-remediation-u2-precheck-r0-fix-receipt.json | | U2 reachable-missing-parent test | reject known-absent remote child target | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 5b5537aa0fc4 | 0 | /tmp/norm-remediation-u2-child-missing-parent-test-receipt-2.json | | U2 reachable-missing-parent fix | local child resume only falls back on remote outage | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 105901595805 | 0 | /tmp/norm-remediation-u2-child-missing-parent-fix-receipt.json | -| U2 precheck R0 regression tests | reachable/missing/forward target without cached ref | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | fbb2aacb71ba | 0 | /tmp/norm-remediation-u2-no-cache-history-tests-receipt.json | -| U2 offline resume regression tests | root/child receipt import while origin unreachable | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 8d8585afec66 | 0 | /tmp/norm-remediation-u2-offline-resume-tests-receipt.json | -| U2 offline resume implementation | local resume on R0/local registered parent | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | f81326bd0836 | 0 | /tmp/norm-remediation-u2-offline-resume-fix-receipt.json | +| U2 offline resume tests | root/child receipt import during origin outage | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | 8d8585afec66 | 0 | /tmp/norm-remediation-u2-offline-resume-tests-receipt.json | +| U2 offline resume fix | local resume on R0/local registered parent | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | f81326bd0836 | 0 | /tmp/norm-remediation-u2-offline-resume-fix-receipt.json | +| U2 review-proof tests | no-ref paths, auth failure, and parent identity | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | bc304111a47f | 0 | /tmp/norm-remediation-u2-final-review-tests-receipt.json | +| U2 final offline guards | missing cache, auth failures, and parent SHA checks | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 471948c59cfa | 0 | /tmp/norm-remediation-u2-final-guards-receipt.json | | U2 precheck R0 implementation | compare remote advertisement without cached tracking ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 1ff6444d3352 | 0 | /tmp/norm-remediation-u2-precheck-r0-fix-receipt.json | ## 同步记录 @@ -74,12 +75,8 @@ | 2026-10-18T13:40Z | `bash -n scripts/task.sh && make test-governance` | 9c0e7657083259759f5bd1bed6921471411dba2c | 0 | SELF | 872bbc3437ae83e3 | | 2026-10-18T14:41Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | bf12f030894e1530 | | 2026-10-18T14:40Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 8e81c5353849fbd5 | -| 2026-10-18T15:06Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | cf56a1870daa88cc | -| 2026-10-18T15:06Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 3249de89a278379f | -| 2026-10-18T15:06Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 105901595805cc5fa5ce19f1a6d48317b950d6af | 0 | SELF | cf56a1870daa88cc | -| 2026-10-18T15:06Z | `bash -n scripts/task.sh && make test-governance` | 105901595805cc5fa5ce19f1a6d48317b950d6af | 0 | SELF | 3249de89a278379f | -| 2026-10-18T14:46Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 3821724d4545beda | -| 2026-10-18T14:47Z | `bash -n scripts/task.sh && make test-governance` | 1ff6444d3352c7e13c2b75cefe7d65c631181454 | 0 | SELF | 048ce6f79e025863 | +| 2026-10-18T15:47Z | `python3 -m unittest tests.test_evidence tests.test_task_recovery` | 471948c59cfabff36c018f219998139fea3757fe | 0 | SELF | 7620bd82fed319e7 | +| 2026-10-18T15:48Z | `bash -n scripts/task.sh && make test-governance` | 471948c59cfabff36c018f219998139fea3757fe | 0 | SELF | 5a3346fd73b80243 | ## 结果与验证 @@ -94,6 +91,8 @@ - 最新代码验证:host `tests.test_evidence tests.test_task_recovery` 80/80 通过,`bash -n scripts/task.sh && make test-governance` 491 项通过、18 项 skip,测试对象为 `105901595805cc5fa5ce19f1a6d48317b950d6af`。此前 CI green head 尚早于这些离线恢复修复,须以最终 PR head 重跑 GATE。 - 最新验证覆盖 80 个 evidence/recovery 测试与 491 项治理测试(18 项 skip),以上命令都成功;测试对象为当前实现代码 `105901595805cc5fa5ce19f1a6d48317b950d6af`。若后续只改任务卡,代码测试可复用但 CI head 会变,须在最终提交 head 重新等待 GATE。 - child 父目标补充:若 origin 可达但已登记 parent branch 不存在,local resume 返回 REMOTE_UNVERIFIED,不再使用本地 parent;只有远端确实不可查询时才回退本地 parent,并仍核验 `parent_head_sha`。delivery 仍保持 fetch-first。 +- 最终边界修复及验证:`target_history` 仅将已识别的 DNS/连接类传输错误视为 outage;认证/权限/未知错误不再降级。无 tracking ref 时 precheck 以 active task R0 比较 advertised target;reachable target/parent 缺失或不可比均拒绝。child local resume 要求有效且本地可用的 `parent_head_sha`。去掉了无 tracking-ref 时会触发 strict-shell 退出的 merged-branch 查询。 +- 最终已测生产代码 head `471948c59cfabff36c018f219998139fea3757fe`:host evidence+recovery 83/83 通过;`bash -n scripts/task.sh && make test-governance` 494 项通过、18 项 skip。随后 `105901595805cc5fa5ce19f1a6d48317b950d6af` 又通过 targeted unreachable-vs-reachable missing-parent 对照测试;其后未再改生产逻辑。exact final-head CI 与当前候选 Review 尚待重跑。 - 离线 resume 已端到端验证:root 与 child 任务在捕获有效 LOCAL_READY 收据后、origin 不可达时可用 `TASK_WORK_MODE=local make task-resume` 导入;child 仅使用本地已登记 parent 并核对 `parent_head_sha`。同一环境下 delivery resume 仍以 REMOTE_UNVERIFIED 拒绝。 - 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 - 影响与剩余项:原失败 receipt `/tmp/norm-remediation-u2-receipt.json`,run_id `af20482c3e24cc32f3873d1451043481`;其 worktree 保留为诊断证据。该 run `blocked`、`transport_ref=null`、`host_fallback_authorizations=0`。Development-Manager PR #26 已合入 `b178dec`;Dev gitlink adoption 仍待 initiative 集成,不在本 U2 单元内。 From a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 16:13:01 +0000 Subject: [PATCH 38/39] agent-exec transport dfbdcf8f9879479cec6791f6db4182b1/9e213bfbef61 --- scripts/task.sh | 19 +++++++++++++------ tests/test_task_recovery.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/scripts/task.sh b/scripts/task.sh index 164cafa4..4cfdccea 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -35,13 +35,20 @@ origin_transport_unavailable() { # $1=失败的远端命令 stderr → 仅明确 esac [ ! -e "$path" ] } -target_history() { # $1=可选基线(无缓存目标时代替比较,如任务 R0)→ 输出 ok(远端传输不可达/通告提交为基线后代)、rewound(回退或分叉)或 uncomparable(认证/授权/未知错误、目标 ref 缺失、无基线、通告提交本地缺失或无法比较) - local cached listing advertised rc=0 - cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || cached=${1:-} +target_history() { # $1=可选基线(如任务 R0;无缓存目标时代替比较)→ 输出 ok(通告提交为基线后代;传输不可达时缓存目标为基线后代或无缓存目标)、rewound(回退或分叉)或 uncomparable(认证/授权/未知错误、目标 ref 缺失、无基线、提交本地缺失或无法比较) + local base=${1:-} cached listing advertised rc=0 + cached=$(git rev-parse -q --verify "refs/remotes/origin/${TARGET_BRANCH}^{commit}" 2>/dev/null) || cached= if ! listing=$(git ls-remote origin "refs/heads/${TARGET_BRANCH}" 2>&1); then - if origin_transport_unavailable "$listing"; then echo ok; else echo uncomparable; fi + if ! origin_transport_unavailable "$listing"; then echo uncomparable + elif [ -z "$cached" ]; then echo ok + elif [ -z "$base" ]; then echo uncomparable + else + git merge-base --is-ancestor "$base" "$cached" 2>/dev/null || rc=$? + case "$rc" in 0) echo ok ;; 1) echo rewound ;; *) echo uncomparable ;; esac + fi return 0 fi + [ -n "$cached" ] || cached=$base advertised=$(printf '%s\n' "$listing" \ | awk -v ref="refs/heads/${TARGET_BRANCH}" '$2 == ref && !seen { print $1; seen = 1 }') [ -n "$advertised" ] && [ -n "$cached" ] || { echo uncomparable; return 0; } @@ -174,9 +181,8 @@ cmd_precheck() { exit 3 fi if [ "$mode" = local ]; then - python3 "${SCRIPT_DIR}/evidence.py" --local-context --active || return 2 if [ "$remote_fresh" -eq 0 ]; then - # 无缓存目标时只以活动任务卡登记的 R0 作比较基线,不把它当作远端最新值。 + # 以活动任务卡登记的 R0 为比较基线(不把它当作远端最新值);先于任务上下文核验,使缓存目标与 R0 矛盾时给出明确判定。 case "$(target_history "$(card_r0 "$(branch_card "$branch")")")" in rewound) echo "[verdict] REMOTE_UNVERIFIED — origin/${TARGET_BRANCH} rewound or diverged from the known target; preserve local work and reconcile the target history" @@ -186,6 +192,7 @@ cmd_precheck() { return 6 ;; esac fi + python3 "${SCRIPT_DIR}/evidence.py" --local-context --active || return 2 [ "$remote_fresh" -eq 1 ] || echo "[remote-freshness] unavailable — LOCAL_READY rests on local task context only" echo "[verdict] LOCAL_READY — existing task may continue authorized local work; final delivery still requires latest-target sync and checks" return 0 diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index ded3c923..ab2966db 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -1290,6 +1290,37 @@ class TaskRecoveryTests(unittest.TestCase): "the task HEAD must not move when the precheck refuses to " "advertise LOCAL_READY") + def test_local_outage_rejects_cached_target_diverged_from_r0(self): + """Origin transport is unavailable and the cached origin/main does not + descend from the registered R0. Local precheck must surface + REMOTE_UNVERIFIED and local resume must not import the receipt, instead + of continuing on a cache that already contradicts the task baseline.""" + f = self.fixture + receipt = self.capture() + original = receipt.read_bytes() + card_before = self.card.read_bytes() + r0 = self.local_authorization_scope()["r0"] + divergent = f.git(self.wt, "commit-tree", r0 + "^{tree}", "-m", + "Unrelated cached target").stdout.strip() + f.git(self.wt, "update-ref", "refs/remotes/origin/main", divergent) + self.assertEqual(f.git(self.wt, "merge-base", "--is-ancestor", r0, divergent, + check=False).returncode, 1) + f.git(self.wt, "remote", "set-url", "origin", str(f.root / "missing.git")) + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + + local = self.precheck() + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertIn("rewound or diverged", local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + + resumed = self.resume(receipt) + self.assertNotEqual(resumed.returncode, 0, resumed.stdout) + self.assertNotIn("[resumed]", resumed.stdout) + self.assertEqual(self.card.read_bytes(), card_before) + self.assertEqual(receipt.read_bytes(), original) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + if __name__ == "__main__": unittest.main() From b6e5bba91db29c6515a7390c400f13a240cf4b30 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 16:33:54 +0000 Subject: [PATCH 39/39] Record U2 cached target review evidence Why: Record the cached-target outage fix, host review, and final local verification for U2.\nEvidence: SELF receipts cover 84 focused tests and 495 governance tests; 18 checks skipped.\nImpact: Preserve exact-head delivery traceability without changing implementation.\nRefs: PR #618 --- .task/archive/feature-norm-remediation-u2.md | 79 +++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/.task/archive/feature-norm-remediation-u2.md b/.task/archive/feature-norm-remediation-u2.md index a4507db1..d49aa673 100644 --- a/.task/archive/feature-norm-remediation-u2.md +++ b/.task/archive/feature-norm-remediation-u2.md @@ -54,6 +54,8 @@ | U2 review-proof tests | no-ref paths, auth failure, and parent identity | caefcb72 | test | pi, agy, cursor | 1 | pi / MiniMax-M3 | success | bc304111a47f | 0 | /tmp/norm-remediation-u2-final-review-tests-receipt.json | | U2 final offline guards | missing cache, auth failures, and parent SHA checks | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 471948c59cfa | 0 | /tmp/norm-remediation-u2-final-guards-receipt.json | | U2 precheck R0 implementation | compare remote advertisement without cached tracking ref | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | 1ff6444d3352 | 0 | /tmp/norm-remediation-u2-precheck-r0-fix-receipt.json | +| U2 cached-target outage R0 regression/fix | reject cached target known to diverge from registered R0 during origin outage | caefcb72 | repair | cursor, agy, pi | 1 | cursor-agent-stable / auto | success | a06209ff0a2b | 0 | /tmp/norm-u2-cached-target-outage-fix-2.json | +| U2 cached-target independent review | independent review of exact implementation diff | caefcb72 | review | pi, antigravity, cursor, codex | 1 | pi / MiniMax-M3 | failed-unknown | none | 0 | /tmp/norm-u2-cached-target-outage-review.json | ## 同步记录 @@ -92,6 +94,9 @@ - 最新验证覆盖 80 个 evidence/recovery 测试与 491 项治理测试(18 项 skip),以上命令都成功;测试对象为当前实现代码 `105901595805cc5fa5ce19f1a6d48317b950d6af`。若后续只改任务卡,代码测试可复用但 CI head 会变,须在最终提交 head 重新等待 GATE。 - child 父目标补充:若 origin 可达但已登记 parent branch 不存在,local resume 返回 REMOTE_UNVERIFIED,不再使用本地 parent;只有远端确实不可查询时才回退本地 parent,并仍核验 `parent_head_sha`。delivery 仍保持 fetch-first。 - 最终边界修复及验证:`target_history` 仅将已识别的 DNS/连接类传输错误视为 outage;认证/权限/未知错误不再降级。无 tracking ref 时 precheck 以 active task R0 比较 advertised target;reachable target/parent 缺失或不可比均拒绝。child local resume 要求有效且本地可用的 `parent_head_sha`。去掉了无 tracking-ref 时会触发 strict-shell 退出的 merged-branch 查询。 +- 最终安全复审阻塞修复:当 origin 传输不可达但本地缓存 `origin/` 已知分叉于登记 R0 时,`target_history` 现在返回 `rewound`;缓存存在但 ancestry 无法比较时返回 `uncomparable`,仅兼容缓存或无缓存才允许继续判断。新增回归覆盖 local precheck 拒绝 `LOCAL_READY`、resume 不导入 receipt,且 HEAD、卡片和 receipt 保持不变。修复经 agent-exec transport `a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c` 集成。 +- 修复验证(SELF,HEAD `a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c`):`python3 -m unittest tests.test_task_recovery tests.test_evidence` 84/84 通过;`bash -n scripts/task.sh && make test-governance` 495 项通过、18 项 skip。对应完整脱敏收据由 `IMPORT_RECEIPT` 附于本卡。 +- Review:PI 独立 Review attempt `a44c5c3e758a08f3ebe5d8a59bd241fd` 以 `failed-unknown` 结束,未形成可核验结论且未产生改动;当前 host 对 `398d12141c84453fb6db3dba2eaf9a208342e29f..a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c` 的实际 diff 进行独立代码审查,未发现阻塞问题。旧 head `398d12141c84453fb6db3dba2eaf9a208342e29f` 的 CI run 3959 `ci / test` 失败;原因尚未从旧 run 日志确认,修复 head 的 exact CI 尚待执行。 - 最终已测生产代码 head `471948c59cfabff36c018f219998139fea3757fe`:host evidence+recovery 83/83 通过;`bash -n scripts/task.sh && make test-governance` 494 项通过、18 项 skip。随后 `105901595805cc5fa5ce19f1a6d48317b950d6af` 又通过 targeted unreachable-vs-reachable missing-parent 对照测试;其后未再改生产逻辑。exact final-head CI 与当前候选 Review 尚待重跑。 - 离线 resume 已端到端验证:root 与 child 任务在捕获有效 LOCAL_READY 收据后、origin 不可达时可用 `TASK_WORK_MODE=local make task-resume` 导入;child 仅使用本地已登记 parent 并核对 `parent_head_sha`。同一环境下 delivery resume 仍以 REMOTE_UNVERIFIED 拒绝。 - 验收补充:对照计划 U2 的“remote-tracking ref 不存在但 R0 对象本地可用”场景,host Review 发现首个修复只覆盖 receipt 捕获后 ref 消失,未覆盖离线 precheck 捕获时 ref 已缺失;随后新增 test-first 回归覆盖两种时序,并修复 `--local-context` 与 `resume-local` 的有界验证。`start` 与 `resume-delivery` 继续要求当前权威目标。 @@ -99,7 +104,7 @@ ## 合入与清理 -- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据) +- Review 记录:host Review 完成(base `398d12141c84453fb6db3dba2eaf9a208342e29f`,head `a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c`;结论:无阻塞 finding;独立 PI attempt failed-unknown,不计为通过) - 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测) - CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果) - 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) @@ -223,3 +228,75 @@ - 本卡在 PR #618(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/618)合入前闭合并归档,随该 PR 一同合入。 - 该 PR 的合并提交即本任务的落地记录。 - 合入后无需回填,也无需单独的闭合 PR。 + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T16:13:57.369094+00:00", + "command": "python3 -m unittest tests.test_task_recovery tests.test_evidence", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2", + "before_head": "a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c", + "after_head": "a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c", + "branch": "feature/norm-remediation-u2", + "target_branch": "main", + "target_head": "eef7e5e778cabcb6216f74b74213a3d7511f383b", + "root_target": "main", + "immediate_target": "feature/norm-remediation", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "....................................................................................", + "----------------------------------------------------------------------", + "Ran 84 tests in 31.295s", + "", + "OK" + ], + "output_sha256": "7cf18f380130a11f0494ce26e3e331cb9c6532d790a869414b68b3cb5a8161f1", + "receipt_sha256": "4054bd2917a76495eec89a929dea0929ed7ab7af633ca99424950baf300a56b4", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/4054bd2917a76495eec89a929dea0929ed7ab7af633ca99424950baf300a56b4.json" +} +``` + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T16:14:30.451262+00:00", + "command": "bash -n scripts/task.sh && make test-governance", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2", + "before_head": "a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c", + "after_head": "a06209ff0a2b23fb750528a4bec4e7e64b3f2d6c", + "branch": "feature/norm-remediation-u2", + "target_branch": "main", + "target_head": "eef7e5e778cabcb6216f74b74213a3d7511f383b", + "root_target": "main", + "immediate_target": "feature/norm-remediation", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "test_feature_missing_parent_is_allowed (test_write_guard.WriteGuardTests.test_feature_missing_parent_is_allowed) ... ok", + "test_invalid_json_is_denied (test_write_guard.WriteGuardTests.test_invalid_json_is_denied) ... ok", + "test_missing_parent_on_main_is_denied (test_write_guard.WriteGuardTests.test_missing_parent_on_main_is_denied) ... ok", + "test_missing_unknown_and_malformed_write_input_are_denied (test_write_guard.WriteGuardTests.test_missing_unknown_and_malformed_write_input_are_denied) ... ok", + "test_non_git_destination_is_explicitly_outside_scope (test_write_guard.WriteGuardTests.test_non_git_destination_is_explicitly_outside_scope) ... ok", + "test_nonwrite_tools_are_outside_scope (test_write_guard.WriteGuardTests.test_nonwrite_tools_are_outside_scope) ... ok", + "test_relative_path_uses_hook_cwd_not_process_cwd (test_write_guard.WriteGuardTests.test_relative_path_uses_hook_cwd_not_process_cwd) ... ok", + "test_symlink_into_main_with_missing_parents_is_denied (test_write_guard.WriteGuardTests.test_symlink_into_main_with_missing_parents_is_denied) ... ok", + "test_valid_multi_file_patch_on_feature_is_allowed (test_write_guard.WriteGuardTests.test_valid_multi_file_patch_on_feature_is_allowed) ... ok", + "", + "----------------------------------------------------------------------", + "Ran 495 tests in 364.208s", + "", + "OK (skipped=18)", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-wt-feature-norm-remediation-u2'" + ], + "output_sha256": "3edcb54df736b840127f6b466cac1f4046632e81f38fa02fe96cdb95cee0b87f", + "receipt_sha256": "ae37da5e99243a166bafa46ff6bf95cb704de194338a669d929f409cb540768f", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/ae37da5e99243a166bafa46ff6bf95cb704de194338a669d929f409cb540768f.json" +} +```