Compare commits

..
Author SHA1 Message Date
weichen 21e897b7f0 Record the U1 independent review verdict
ci / test-pg (pull_request) Skipped
ci / svc-realcombo (pull_request) Skipped
ci / test (pull_request) Successful in 8m33s
Why: fresh-session pi-glm review returned REVIEW PASS with no blocking
findings; report landed via transport at
docs/superpowers/reviews/2026-10-22-container-storage-u1-review.md.

Evidence: NOT_RUN (review receipt referenced)

Impact: task card only.
2026-10-22 11:19:48 +00:00
agent-exec-transport e04b7877c3 agent-exec transport 99605f17561d82dabbbf47596ef94de8/b1dcdad669fd 2026-10-22 11:13:45 +00:00
weichen e1f08650a1 Record U1 completion in the task card
Why: unit implemented via antigravity worker transport and verified by
host (80 targeted + 574 affected tests pass).

Evidence: NOT_RUN (verification receipts imported above)

Impact: task card only.
2026-10-22 10:50:26 +00:00
agent-exec-transport 2d75d101a0 agent-exec transport ed39db43befedd8a049053096d6640be/144300034285 2026-10-22 10:41:05 +00:00
weichen 6aa19e0810 Record routing-unit success receipt in U1 card
Why: the antigravity promotion for the U1 unit landed via worker
transport 735148439e276dd92445c591fe459d82 (commit 0c8bea37).

Evidence: NOT_RUN (task card only)

Impact: task card only.
2026-10-22 10:22:16 +00:00
agent-exec-transport 0c8bea3748 agent-exec transport 735148439e276dd92445c591fe459d82/43861c25f995 2026-10-22 10:20:37 +00:00
weichen a094d66e83 Disclose killed run-5 attempt in U1 card
Why: host killed run 5 before its first attempt produced work to avoid
exceeding the cursor attempt cap; private run dir removed, disclosed
here rather than silently dropped.

Evidence: NOT_RUN (task card only)

Impact: task card only.
2026-10-22 10:10:29 +00:00
weichen 0db2645f2d Record U1 attempt-4 receipt and exhaust the cursor candidate
Why: fourth attempt completed the work with passing tests but again
appended prose after the terminal JSON; cursor attempts exhausted,
moving to antigravity per role order.

Evidence: NOT_RUN (task card receipt row only)

Impact: task card only.
2026-10-22 10:09:04 +00:00
weichen 15284f8b45 Record U1 attempt-3 failed-unknown receipt
Why: cursor CLI rejected --model auto at startup with an empty model
list; standalone CLI smoke works, judged transient.

Evidence: NOT_RUN (task card receipt row only)

Impact: task card only.
2026-10-22 10:01:14 +00:00
weichen 0d0d9f91f0 Record U1 attempt-2 failed-unknown receipt
Why: worker completed and verified the unit but appended prose after
the terminal JSON, so the controller could not decode a completed
record; scene preserved, diff identical to attempt-1 patch.

Evidence: NOT_RUN (task card receipt row only)

Impact: task card only.

Refs: /tmp/container-storage-u1-run2-receipt.json
2026-10-22 09:54:51 +00:00
weichen cb72983e02 Record U1 attempt-1 failed-unknown receipt
Why: cursor attempt stalled on transport after completing the work;
scene preserved and reconciled (no external side effects, diff exported
to /tmp/container-storage-u1-attempt1.patch).

Evidence: NOT_RUN (task card receipt row only)

Impact: task card only.

Refs: /tmp/container-storage-u1-run-receipt.json
2026-10-22 09:43:35 +00:00
weichen b73592d4b9 Fill the U1 child task card with goal and scope
Why: record the U1 (environment and data-ownership contract) goal,
boundaries and completion conditions before dispatching the unit.

Evidence: NOT_RUN (task card only)

Impact: task card only.

Refs: docs/superpowers/plans/2026-10-22-container-storage-plan.md
2026-10-22 09:14:24 +00:00
weichen 3f66da541d Record the task card for feature-container-storage-plan-u1
Why: establish the authorized task workspace and its initial baseline.

Evidence: preserved precheck receipt in .task/feature-container-storage-plan-u1.md; R0=955a32de2bb49171348bad5f566cce935df19212.

Impact: task metadata only; the feature workspace starts at b360bcb974.

Refs: .task/feature-container-storage-plan-u1.md
2026-10-22 09:12:44 +00:00
weichen b360bcb974 Add container-storage initiative charter and reviewed plan
ci / svc-realcombo (pull_request) Successful in 2m47s
ci / test (pull_request) Successful in 8m54s
ci / test-pg (pull_request) Successful in 13m2s
Why: register the L1 initiative for the single-binary + containerized
database/knowledge-storage overhaul (U1-U6) and land the reviewed plan
as the charter's authoritative reference.

Evidence: NOT_RUN (planning documents only; no product change)

Impact: docs and task card only; no runtime behavior change.

Refs: docs/superpowers/plans/2026-10-22-container-storage-plan.md
2026-10-22 09:11:10 +00:00
weichen efa0b2af7e Record the task card for feature-container-storage-plan
Why: establish the authorized task workspace and its initial baseline.

Evidence: preserved precheck receipt in .task/feature-container-storage-plan.md; R0=955a32de2bb49171348bad5f566cce935df19212.

Impact: task metadata only; the feature workspace starts at 955a32de2b.

Refs: .task/feature-container-storage-plan.md
2026-10-22 09:09:22 +00:00
48 changed files with 1813 additions and 2253 deletions
+2 -42
View File
@@ -77,9 +77,6 @@ jobs:
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
# 嵌套 pins(RDM/KB 等)仍以权威 SSH 端点声明 URL:把它改写到同一受信
# HTTPS 基址,才能复用既有的 URL 级 token 头而不依赖真实 SSH。
# 本仓 .gitmodules 已是 HTTPS 形式,SSH 允许清单常量保持不变。
}
@@ -181,31 +178,6 @@ jobs:
tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz"
- name: Install house-style Python packages
run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl
- name: Provision the Gitea token file for facts verification
env:
T1: ${{ secrets.GITEA_TOKEN }}
T2: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
echo "provision: GITEA_TOKEN length=${#T1}, GITHUB_TOKEN length=${#T2}"
if [ -n "$T1" ]; then
token="$T1"; source_name=GITEA_TOKEN
elif [ -n "$T2" ]; then
token="$T2"; source_name=GITHUB_TOKEN
else
echo "provision: GITEA_TOKEN/GITHUB_TOKEN both empty"
exit 1
fi
echo "provision: using ${source_name}"
mkdir -p "$HOME/.secrets"
printf 'HOME:\n- ACCESS_TOKEN=%s\n' "$token" > "$HOME/.secrets/gitea"
chmod 600 "$HOME/.secrets/gitea"
code="$(curl -fsS -m 10 -H "Authorization: token ${token}" https://gitea.naitangbaobao.cn/api/v1/user -o /dev/null -w '%{http_code}')" || true
if [ "$code" != "200" ]; then
echo "provision: /api/v1/user self-check failed with http code ${code:-none} (source ${source_name})"
exit 1
fi
echo "provision: /api/v1/user self-check ok (http 200)"
- name: make test (boundaries, pins, libs, core, backend, frontend, ui, agent-command)
run: make test
- name: Build the single binary
@@ -289,9 +261,6 @@ jobs:
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
# 嵌套 pins(RDM/KB 等)仍以权威 SSH 端点声明 URL:把它改写到同一受信
# HTTPS 基址,才能复用既有的 URL 级 token 头而不依赖真实 SSH。
# 本仓 .gitmodules 已是 HTTPS 形式,SSH 允许清单常量保持不变。
}
@@ -435,16 +404,13 @@ jobs:
# https 基址后再取。只追加 pin 的三个 40 位 hex,不 source pin 文件。
SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
scheme="${GITHUB_SERVER_URL%%://*}"
# 嵌套 pins 的 SSH 端点有两代(旧 git-ssh.* 与迁移后的 gitea.*:52222);
# 只映射旧端点会让嵌套克隆退回真实 SSH,CI 无 key 时必然 host key 失败。
# 两个 insteadOf 同键多值,git 会依次匹配。
export GIT_CONFIG_COUNT=4
export GIT_CONFIG_KEY_0="protocol.$scheme.allow"
export GIT_CONFIG_VALUE_0="always"
export GIT_CONFIG_KEY_1="http.$SERVER/.extraheader"
export GIT_CONFIG_VALUE_1="AUTHORIZATION: token $SUBMODULE_TOKEN"
export GIT_CONFIG_KEY_2="url.$SERVER/weichen/.insteadOf"
export GIT_CONFIG_VALUE_2="ssh://git@gitea.naitangbaobao.cn:52222/weichen/"
export GIT_CONFIG_VALUE_2="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
export GIT_CONFIG_KEY_3="core.hooksPath"
export GIT_CONFIG_VALUE_3="/dev/null"
git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm
@@ -620,9 +586,6 @@ jobs:
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
# 嵌套 pins(RDM/KB 等)仍以权威 SSH 端点声明 URL:把它改写到同一受信
# HTTPS 基址,才能复用既有的 URL 级 token 头而不依赖真实 SSH。
# 本仓 .gitmodules 已是 HTTPS 形式,SSH 允许清单常量保持不变。
}
@@ -729,16 +692,13 @@ jobs:
# https 基址后再取。
SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
scheme="${GITHUB_SERVER_URL%%://*}"
# 嵌套 pins 的 SSH 端点有两代(旧 git-ssh.* 与迁移后的 gitea.*:52222);
# 只映射旧端点会让嵌套克隆退回真实 SSH,CI 无 key 时必然 host key 失败。
# 两个 insteadOf 同键多值,git 会依次匹配。
export GIT_CONFIG_COUNT=4
export GIT_CONFIG_KEY_0="protocol.$scheme.allow"
export GIT_CONFIG_VALUE_0="always"
export GIT_CONFIG_KEY_1="http.$SERVER/.extraheader"
export GIT_CONFIG_VALUE_1="AUTHORIZATION: token $SUBMODULE_TOKEN"
export GIT_CONFIG_KEY_2="url.$SERVER/weichen/.insteadOf"
export GIT_CONFIG_VALUE_2="ssh://git@gitea.naitangbaobao.cn:52222/weichen/"
export GIT_CONFIG_VALUE_2="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
export GIT_CONFIG_KEY_3="core.hooksPath"
export GIT_CONFIG_VALUE_3="/dev/null"
git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm
@@ -1,168 +0,0 @@
# feature/catalog-recall-eval-a3 任务卡
任务主记录:PR #713 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/713;状态:已完成(合入前闭合);owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 6ced938a126981ee2a4fab671a2da94f49cb56c5
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-catalog-recall-eval-a3(worktree);分支:feature/catalog-recall-eval-a3
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=bf205afb1f04e503573784c93b52a991095554ed7314d9ac0f6c1996660a3211;triage_receipt=/tmp/triage-a3-1792716554.json;receipt_sha256=1a4ee8fe2133a52386520d35955d0fc600817f9a6bd94d40730eb8fce0904f44
- 授权与交付终点:用户已批准启动 A3 能力目录召回评测(方案 Functional Follow-Ups 第三项:先建脱敏评测集并产出指标基线,"是否需要检索/调模型"的决策由基线数据支撑、另行决策)。范围内连续推进:设计、agent-per-unit 实施、验证、组合、Review、PR/CI/合入。未授权:检索实现、模型/提示词调整、A2 后续、A4、部署。
- 设计决定(2026-10-22,host):
1. 评测落在 agent-intent 既有机制上(cmd/intentctl eval + testdata/corpus.json 共用格式),不新建平行体系。
2. 数据集五类覆盖(否定/引用指令、多意图、错误槽位、过期确认、生成规则一致性),全部合成脱敏、由现有 fixture 规格派生;规模 ≥30 例。
3. 指标层只记录不设质量阈值:top-1 命中率、误执行数(命中≠预期动作)、澄清率、fallback 率、生成 vs 规则一致性(离线 fake generator)。
4. 基线报告(evaluation/BASELINE.md + metrics.json)随库提交,附复跑方式;质量结论由人/后续任务基于数据做出。
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
- 当前阶段:需求拆解;当前增量:I1(待定义)
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
## 目标与非目标
**目标**:脱敏评测集(五类 ≥30 例)+ intentctl eval 指标输出(JSON 与表格)+ 基线报告;离线可复跑(fake generator,无真实模型调用)。
**非目标**:检索/召回增强实现;提示词或模型调整;真实用户数据;控制台;A2 后续/A4。
## 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 | — |
## 关联方向与规划
- 适用状态:待核对(无适用规划时说明原因)
- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文)
- 对应里程碑:待核对
## 需求分析
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|---|---|---|---|---|---|---|
| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 |
## 方案与计划
- 方案引用:待填写(含对象版本)
- 实施计划引用:待填写(含增量与需求 ID)
- 测试计划引用:待填写(含需求 ID 与验收条件)
## 阶段记录
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|---|---|---|---|---|---|
| 需求拆解 | 待定 | 待执行 | — | — | — |
| 需求评估 | 待定 | 待执行 | — | — | — |
| 需求表确认 | 待定 | 待执行 | — | — | — |
| 方案设计 | 待定 | 待执行 | — | — | — |
| 实施计划 | 待定 | 待执行 | — | — | — |
| 实施与自检 | 待定 | 待执行 | — | — | — |
| 正式测试与验收 | 待定 | 待执行 | — | — | — |
| 代码 Review | 待定 | 待执行 | — | — | — |
| 合入 | 待定 | 待执行 | — | — | — |
| 合入核验与收口 | 待定 | 待执行 | — | — | — |
## 探索试验与迭代
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 6ced938a126981ee2a4fab671a2da94f49cb56c5 | 6ced938a126981ee2a4fab671a2da94f49cb56c5 | 2026-10-23T00:51:18Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 交付调度
- 调度状态:未入队;调度记录:无
- entry_id:无;attempt_id:无;窗口执行者:无
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/delivery.md#合入调度
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
## 结果与验证
- 结果:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
## 规划进展与下一步
- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划)
- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务)
- 推荐依据:待填写
- 前置条件/待确认:待核对
- 预期产物/验收条件:待定义
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
## 合入与清理
- 实施收据:a3-recall-eval-dataset(cursor attempt1,五类语料+指标+基线报告,intent 6aa4925,verify-a3 exit=0);gitlink 集成(native d1f72aa/squad a5afe9c/backend 8dc91c1 含嵌套 intent/native 级联,a3git5/a3git10-run.json)。
- 组合验证:全量 make test PASS(verify-a3-combo3.json exit=0)。
- Review 记录:独立 CLI 评审单元 a3-independent-review(收据 /tmp/agent-stack-b2/reviewa3-run.json);结论 APPROVE;报告 .task/reviews/2026-10-22-a3-recall-eval-independent-review.md;被审对象 35524f3d 组合 vs main
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-23T00:50:13.586423+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"after_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"branch": "main",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"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=6ced938a12",
"[remote] origin/main=6ced938a12 ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=8",
"[recycle] merged un-recycled branches: feature/exec-recovery-diagnostics-a2 fix/rdm-pin-advance-latest-2",
"[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] 6ced938a12(完整:6ced938a126981ee2a4fab671a2da94f49cb56c5)",
"[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": "9f6b7e8979503847e2ec5623a17ee9624cbafd5450149556482244a86abe9192",
"receipt_sha256": "1cf2c62aea8ef98b5d211cb94c053174407c70aeab274681c1b77db0a2878db9",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/1cf2c62aea8ef98b5d211cb94c053174407c70aeab274681c1b77db0a2878db9.json"
}
```
## 闭合记录(合入前折叠)
- 本卡在 PR #713(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/713)合入前闭合并归档,随该 PR 一同合入。
- 该 PR 的合并提交即本任务的落地记录。
- 合入后无需回填,也无需单独的闭合 PR。
@@ -1,167 +0,0 @@
# feature/exec-recovery-diagnostics-a2 任务卡
任务主记录:PR #704 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/704;状态:已完成(合入前闭合);owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= c92443280ef8e9ed29bf5bf5250df650c01b4478
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-exec-recovery-diagnostics-a2(worktree);分支:feature/exec-recovery-diagnostics-a2
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=34e72676959d0e970f1a3cb0e90ac2067dfb31f3c2e7a6650b5de9f3f77923c7;triage_receipt=/tmp/triage-a2-1792672248.json;receipt_sha256=7924fdbaeba41d98994c2e11bc96026d4db3cdbd0b28a9d1e381400eae9a6de4
- 授权与交付终点:用户已批准启动 A2 执行与恢复诊断(方案 Functional Follow-Ups 第二项)。范围内连续推进:设计、agent-per-unit 实施、验证、组合、Review、PR/CI/合入。未授权:控制台/前端界面、A3/A4、部署。
- 设计决定(2026-10-22,host):
1. 只读诊断 API:GET /api/runs/{id}/diagnostics(owner/project 授权,复用既有 run 鉴权),响应聚合:状态、当前责任方与等待原因(running / waiting_hitl / confirmation_pending / recovery_hold(原因+created_at) / reconciling / failed+reason / completed)、最后稳定 checkpoint(如可得)、已知副作用与关联 operation 记录(复用 cmdbus 审计信封查询)、下一步动作建议(确认卡片 / 解除 hold / 可安全重试 / 需人工核对 / 无)。
2. 纯读聚合,不改任何执行/恢复行为;复用既有 store 方法(GetRun/RecoveryHold/ListInterruptedSubagentRuns/审计查询)与 U7b 的 operation 查询。
3. 不建控制台、不加写接口;批量列表走既有会话 run 列表。
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
- 当前阶段:需求拆解;当前增量:I1(待定义)
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
## 目标与非目标
**目标**:单只读端点聚合"卡在哪里/谁在等/为何等/最后 checkpoint/已知副作用/下一步",覆盖 HITL 等待、确认等待、恢复 hold(批二 needs-reconciliation)、失败原因等全部可诊断状态。
**非目标**:控制台/UI;任何写操作或状态变更端点;A3/A4; squad sqlite 协作层的跨库聚合(只读后端 store 与审计)。
## 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 | — |
## 关联方向与规划
- 适用状态:待核对(无适用规划时说明原因)
- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文)
- 对应里程碑:待核对
## 需求分析
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|---|---|---|---|---|---|---|
| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 |
## 方案与计划
- 方案引用:待填写(含对象版本)
- 实施计划引用:待填写(含增量与需求 ID)
- 测试计划引用:待填写(含需求 ID 与验收条件)
## 阶段记录
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|---|---|---|---|---|---|
| 需求拆解 | 待定 | 待执行 | — | — | — |
| 需求评估 | 待定 | 待执行 | — | — | — |
| 需求表确认 | 待定 | 待执行 | — | — | — |
| 方案设计 | 待定 | 待执行 | — | — | — |
| 实施计划 | 待定 | 待执行 | — | — | — |
| 实施与自检 | 待定 | 待执行 | — | — | — |
| 正式测试与验收 | 待定 | 待执行 | — | — | — |
| 代码 Review | 待定 | 待执行 | — | — | — |
| 合入 | 待定 | 待执行 | — | — | — |
| 合入核验与收口 | 待定 | 待执行 | — | — | — |
## 探索试验与迭代
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | c92443280ef8e9ed29bf5bf5250df650c01b4478 | c92443280ef8e9ed29bf5bf5250df650c01b4478 | 2026-10-22T12:37:28Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 交付调度
- 调度状态:未入队;调度记录:无
- entry_id:无;attempt_id:无;窗口执行者:无
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/delivery.md#合入调度
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
## 结果与验证
- 结果:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
## 规划进展与下一步
- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划)
- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务)
- 推荐依据:待填写
- 前置条件/待确认:待核对
- 预期产物/验收条件:待定义
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
## 合入与清理
- 实施收据:a2-diagnostics-api(pi attempt1,rundiag 聚合 + GET /api/runs/{id}/diagnostics,backend 06b0c9f,verify-a2b exit=0);架构基线边登记(repair,a2arch-run.json);backend pin 集成(a2git2-run.json,cursor 配额间歇故障重试一次后成功)。
- 组合验证:全量 make test PASS(verify-a2-combo2.json exit=0,本轮 svc 既有 flake 未复现)。
- Review 记录:独立 CLI 评审单元 a2-independent-review(第 2 次派发 success;首轮 pi-glm 终态记录不可解码,现场保留);结论 APPROVE(0 blocker / 0 major);报告 .task/reviews/2026-10-22-a2-diagnostics-independent-review.md;被审对象合并基线 vs main
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T12:30:58.698213+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "c92443280ef8e9ed29bf5bf5250df650c01b4478",
"after_head": "c92443280ef8e9ed29bf5bf5250df650c01b4478",
"branch": "fix/gitea-address-migration",
"target_branch": "main",
"target_head": "c92443280ef8e9ed29bf5bf5250df650c01b4478",
"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=fix/gitea-address-migration head=c92443280e",
"[remote] origin/main=c92443280e ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=7",
"[recycle] merged un-recycled branches: fix/gitea-endpoint-facts",
"[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] c92443280e(完整:c92443280ef8e9ed29bf5bf5250df650c01b4478)",
"[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": "d670ed210b367c72f877fb63bbbeb5b28411e95cb56fab782f27172fa481ed89",
"receipt_sha256": "2829e17065c329449fac8ef2690d50babf82015a1f546dcbfd3f27f67789dd0c",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/2829e17065c329449fac8ef2690d50babf82015a1f546dcbfd3f27f67789dd0c.json"
}
```
## 闭合记录(合入前折叠)
- 本卡在 PR #704(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/704)合入前闭合并归档,随该 PR 一同合入。
- 该 PR 的合并提交即本任务的落地记录。
- 合入后无需回填,也无需单独的闭合 PR。
@@ -1,168 +0,0 @@
# feature/intent-multi-turn-a1 任务卡
任务主记录:PR #700 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/700;状态:已完成(合入前闭合);owner:weichen
## 基线与授权
- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 955a32de2bb49171348bad5f566cce935df19212
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-intent-multi-turn-a1(worktree);分支:feature/intent-multi-turn-a1
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=5dd60900f5e7d69e382c367bfde373c087528dd9dcae7e16199c956ef4f3d9bf;triage_receipt=/tmp/triage-a1-1792658810.json;receipt_sha256=35b32668d143e39f386f069ae4c0004cae9761125d95241bb66e6aff1530d24b
- 授权与交付终点:用户已批准启动 A1 多轮意图闭环(方案 Functional Follow-Ups 第一项)。范围内连续推进:设计、agent-per-unit 实施、验证、组合、Review、PR/CI/合入。未授权:A2-A4、A2A、部署/发布、其余业务动作族扩展。
- 设计决定(2026-10-22,host):
1. 第一类真实业务动作 = **运行取消**("取消刚才的任务"):cancel 命令已注册且为高后果动作,走既有 cmdbus 确认卡;后续动作族另行增量。
2. 会话状态存 Backend 既有 store(新表 intent_session,键 owner+project+conversation),保存挂起预期(action ref/revision/缺失槽位/已累积槽位)与过期时间(30 分钟 TTL);revision 递增防过期回答污染新任务。
3. 库层不动 Stage 1 语义:Backend 实现 ExpectationProvider 数据源并在 intentgate/route 消费 set_slot/change_slot/cancel/expectation 命令(route.go 现按"宿主不消费"转 turn——本任务改变这一点)。
4. 高危动作仍必须经 cmdbus 确认;会话只是槽位与预期的载体,不授予执行权。
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
- 当前阶段:需求拆解;当前增量:I1(待定义)
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
## 目标与非目标
**目标**:多轮对话中:缺槽动作 → 澄清 → 用户回答补槽 → 动作完成(第一类=运行取消);"取消刚才的任务"作用于明确对象;歧义先澄清;过期回答(TTL 外)不影响新任务;双用户隔离;revision 防串轮。拆两个单元:U-A1b 会话状态与命令消费(intentgate/route/server + store 表);U-A1c 取消意图路由到 cmdbus(确认卡 + 明确对象解析)。
**非目标**:A2-A4;A2A;更多业务动作族;前端界面改动;部署。
## 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 | — |
## 关联方向与规划
- 适用状态:待核对(无适用规划时说明原因)
- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文)
- 对应里程碑:待核对
## 需求分析
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|---|---|---|---|---|---|---|
| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 |
## 方案与计划
- 方案引用:待填写(含对象版本)
- 实施计划引用:待填写(含增量与需求 ID)
- 测试计划引用:待填写(含需求 ID 与验收条件)
## 阶段记录
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|---|---|---|---|---|---|
| 需求拆解 | 待定 | 待执行 | — | — | — |
| 需求评估 | 待定 | 待执行 | — | — | — |
| 需求表确认 | 待定 | 待执行 | — | — | — |
| 方案设计 | 待定 | 待执行 | — | — | — |
| 实施计划 | 待定 | 待执行 | — | — | — |
| 实施与自检 | 待定 | 待执行 | — | — | — |
| 正式测试与验收 | 待定 | 待执行 | — | — | — |
| 代码 Review | 待定 | 待执行 | — | — | — |
| 合入 | 待定 | 待执行 | — | — | — |
| 合入核验与收口 | 待定 | 待执行 | — | — | — |
## 探索试验与迭代
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 955a32de2bb49171348bad5f566cce935df19212 | 955a32de2bb49171348bad5f566cce935df19212 | 2026-10-22T08:48:01Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 交付调度
- 调度状态:未入队;调度记录:无
- entry_id:无;attempt_id:无;窗口执行者:无
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/delivery.md#合入调度
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
## 结果与验证
- 结果:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
## 规划进展与下一步
- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划)
- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务)
- 推荐依据:待填写
- 前置条件/待确认:待核对
- 预期产物/验收条件:待定义
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
## 合入与清理
- 实施收据:U-A1b(pi attempt1,intent-session 会话/命令消费,backend 0fb64c4,verify-a1b exit=0);U-A1c(pi attempt1,cancel 经 cmdbus 路由,backend c2e3104→verify-a1c exit=0);架构基线边登记(repair,a1arch4-run.json)与 storage-migrate manifest 补齐(repair,backend 9ee752c,verify-a1sr exit=0)。
- 组合验证:test-governance 存在 svc 环境不稳定失败(两次运行分别挂在 test_addr_change / test_U2_health;addr_change 已在干净 main 复现,见批二后验证),其余全部目标 PASS(verify-a1-rest.json exit=0);CI 环境为最终裁决(批二同代码 CI 全绿)。
- Review 记录:待执行(独立 CLI 评审,随 PR 流程)
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T08:46:59.325275+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "955a32de2bb49171348bad5f566cce935df19212",
"after_head": "955a32de2bb49171348bad5f566cce935df19212",
"branch": "main",
"target_branch": "main",
"target_head": "955a32de2bb49171348bad5f566cce935df19212",
"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=955a32de2b",
"[remote] origin/main=955a32de2b ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=4",
"[recycle] merged un-recycled branches: feature/container-storage-plan",
"[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] 955a32de2b(完整:955a32de2bb49171348bad5f566cce935df19212)",
"[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": "f06e6cc8ddb0c414c2ac92e5100dc58b0b926a2bf7196273056b01be80253217",
"receipt_sha256": "32899ef2b4ebee5bf1725fb79e1bba0382177731155cebab04178821e6da3f1b",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/32899ef2b4ebee5bf1725fb79e1bba0382177731155cebab04178821e6da3f1b.json"
}
```
## 闭合记录(合入前折叠)
- 本卡在 PR #700(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/700)合入前闭合并归档,随该 PR 一同合入。
- 该 PR 的合并提交即本任务的落地记录。
- 合入后无需回填,也无需单独的闭合 PR。
@@ -1,168 +0,0 @@
# feature/recall-misexec-fixes 任务卡
任务主记录:PR #717 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/717;状态:已完成(合入前闭合);owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= bccb88f294038ce52a37fc5592a6daebd10e88b4
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-recall-misexec-fixes(worktree);分支:feature/recall-misexec-fixes
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=5510782fe6debd449a832c3353d191c6efb76f50c77932c5adf387a2f0a27bcd;triage_receipt=/tmp/triage-misexec-1792727028.json;receipt_sha256=914545afa88ab6ce4cbe9bd48240e8a8b2f829c970d245c24fb1a7c0d8ca495c
- 授权与交付终点:用户已批准启动 A3 基线驱动的规则层修复(评测集暴露的 6 次误执行:语义否定被当作肯定 ×5、多意图平分误选寒暄 ×1;另有错误槽位未澄清 3 例)。范围内连续推进至合入;检索/调模型决策留待修复后基线。未授权:A4、部署。
- 设计决定(2026-10-22,host):
1. 否定修复放 compiledSpec.evaluate 匹配层:正命中若被前置否定词(不要/别/不用/先不要/先别/无需/不需要,紧邻或间隔≤2字)限定,且该 spec 的全部正命中均被否定 → reject "negated_match"(确定性,不逐 spec 配置)。
2. 平分裁决加确定性 tie-break:优先级相同 → 已抽取到槽位值多者优先(带槽位信号的动作胜过裸寒暄)→ id 字典序。不引入新配置。
3. 槽位抽取修正:抽取跨度内含多值分隔(和/还是/、/,)→ 视为多候选无效(clarify);规范化后仅剩符号/空 → 无效;字符集违规(如声明字母数字却抽出 abc_def 的部分截取)→ 无效。无效一律走 clarify,不 start。
4. 验收即评测集:上述用例从 record_only FAIL 翻为 PASS;overall mis_exec 须为 0;gen_agree 与其余类别不回退。
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
- 当前阶段:需求拆解;当前增量:I1(待定义)
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
## 目标与非目标
**目标**:消除评测集 6 次误执行 + 3 例错误槽位未澄清:匹配层否定感知、裁决 specificity tie-break、槽位多值/字符集无效化;评测指标 mis_exec 归零且其余类别不回退;既有测试全绿。
**非目标**:检索/召回增强;模型/提示词调整;「刚才那个」跨会话指代解析(A1 会话域);真实语料;gen_agree 的 3 例生成器分歧(回放器行为,另行处理)。
## 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 | — |
## 关联方向与规划
- 适用状态:待核对(无适用规划时说明原因)
- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文)
- 对应里程碑:待核对
## 需求分析
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|---|---|---|---|---|---|---|
| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 |
## 方案与计划
- 方案引用:待填写(含对象版本)
- 实施计划引用:待填写(含增量与需求 ID)
- 测试计划引用:待填写(含需求 ID 与验收条件)
## 阶段记录
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|---|---|---|---|---|---|
| 需求拆解 | 待定 | 待执行 | — | — | — |
| 需求评估 | 待定 | 待执行 | — | — | — |
| 需求表确认 | 待定 | 待执行 | — | — | — |
| 方案设计 | 待定 | 待执行 | — | — | — |
| 实施计划 | 待定 | 待执行 | — | — | — |
| 实施与自检 | 待定 | 待执行 | — | — | — |
| 正式测试与验收 | 待定 | 待执行 | — | — | — |
| 代码 Review | 待定 | 待执行 | — | — | — |
| 合入 | 待定 | 待执行 | — | — | — |
| 合入核验与收口 | 待定 | 待执行 | — | — | — |
## 探索试验与迭代
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | bccb88f294038ce52a37fc5592a6daebd10e88b4 | bccb88f294038ce52a37fc5592a6daebd10e88b4 | 2026-10-23T03:45:01Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 交付调度
- 调度状态:未入队;调度记录:无
- entry_id:无;attempt_id:无;窗口执行者:无
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/delivery.md#合入调度
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
## 结果与验证
- 结果:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
## 规划进展与下一步
- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划)
- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务)
- 推荐依据:待填写
- 前置条件/待确认:待核对
- 预期产物/验收条件:待定义
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
## 合入与清理
- 实施收据:misexec-rules-fix(cursor attempt1,否定感知匹配+specificity tie-break+槽位 fail-closed,intent 7f3ff6a,eval mis_exec=0/37 全过,verify-misexec2 exit=0);pin 级联嵌套提升(host 提交 N4/S4/B4')与 Dev 集成(misexec-git3-run.json)。
- 组合验证:全量 make test PASS(verify-misexec-combo2.json exit=0)。
- Review 记录:独立 CLI 评审单元 misexec-independent-review(收据 /tmp/agent-stack-b2/reviewm-run.json);结论 APPROVE;报告 .task/reviews/2026-10-22-misexec-fixes-independent-review.md;被审对象 8e3f9edb 组合 vs main
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-23T03:44:02.622816+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "bccb88f294038ce52a37fc5592a6daebd10e88b4",
"after_head": "bccb88f294038ce52a37fc5592a6daebd10e88b4",
"branch": "main",
"target_branch": "main",
"target_head": "bccb88f294038ce52a37fc5592a6daebd10e88b4",
"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=bccb88f294",
"[remote] origin/main=bccb88f294 ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=7",
"[recycle] merged un-recycled branches: feature/exec-recovery-diagnostics-a2 fix/gitea-endpoint-facts",
"[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] bccb88f294(完整:bccb88f294038ce52a37fc5592a6daebd10e88b4)",
"[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": "6a6f70969d0b85288dde21bf2011b0485af1251a29fbb677ba7d7d9e94c79785",
"receipt_sha256": "b5fc6e81a92e8ecb97bbf51630a10608507425dfe27220efb75341b4f607bffd",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/b5fc6e81a92e8ecb97bbf51630a10608507425dfe27220efb75341b4f607bffd.json"
}
```
## 闭合记录(合入前折叠)
- 本卡在 PR #717(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/717)合入前闭合并归档,随该 PR 一同合入。
- 该 PR 的合并提交即本任务的落地记录。
- 合入后无需回填,也无需单独的闭合 PR。
@@ -1,100 +0,0 @@
# fix/rdm-pin-advance-latest-2 任务卡
任务主记录:PR #711 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/711;状态:已完成(合入前闭合);owner:weichen
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 6ced938a126981ee2a4fab671a2da94f49cb56c5
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-rdm-pin-advance-latest-2(worktree);分支:fix/rdm-pin-advance-latest-2
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=93558e2670e0b23a79612fed5a161be7cb54de39485d98d72f67b6541ee38a8a;triage_receipt=/tmp/pin-f2-triage.json;receipt_sha256=00841f4dfa7626161dcab13a8a598b10fdd81dcf26679b8de6d8177beaf3d655
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
- 交付模式: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 |
|---|---|---|---|
| 启动 | 6ced938a126981ee2a4fab671a2da94f49cb56c5 | 6ced938a126981ee2a4fab671a2da94f49cb56c5 | 2026-10-23T00:50:42Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(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/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-23T00:49:47.450962+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"after_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"branch": "main",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"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=6ced938a12",
"[remote] origin/main=6ced938a12 ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=7",
"[recycle] merged un-recycled branches: feature/exec-recovery-diagnostics-a2",
"[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] 6ced938a12(完整:6ced938a126981ee2a4fab671a2da94f49cb56c5)",
"[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": "d78ae27fdc16acddac83815947924a7eb3d6031e421381f3d9313f29f3c0fc9e",
"receipt_sha256": "aa32f1bbde2a3b12b355fedc1e5ed3b91dea43758fb2a307e93f56fd2c5ae9af",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/aa32f1bbde2a3b12b355fedc1e5ed3b91dea43758fb2a307e93f56fd2c5ae9af.json"
}
```
| 同步(记录前对象) | c3fbf85732788ba91d32c921783993a645580041 | c3fbf85732788ba91d32c921783993a645580041 | 2026-10-23T01:26:28Z |
## 闭合记录(合入前折叠)
- 本卡在 PR #711(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/711)合入前闭合并归档,随该 PR 一同合入。
- 该 PR 的合并提交即本任务的落地记录。
- 合入后无需回填,也无需单独的闭合 PR。
| 同步(记录前对象) | e62092a4479bbe101e74e91b39e03577ce0786ae | bbd80aa38874dbac8a3188b42bc6ed977a4fb379 | 2026-10-23T02:14:09Z |
@@ -1,98 +0,0 @@
# fix/svc-test-port-governance-batch1 任务卡
任务主记录:PR #702 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/702(merge cb36b59acc);状态:已完成(回填);owner:weichen
- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 3d3474a731f9ef3ee1f41935e63bca3afb70db56
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-svc-test-port-governance-batch1(worktree);分支:fix/svc-test-port-governance-batch1
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=1723e9e3e6bd50bedc041afba3533c35e8b663edb12472f29c067df860df799b;triage_receipt=/tmp/pa1-triage.json;receipt_sha256=71d236166f2a1de8cecd6eb93042ef5199a09e2c49318dbf8eb9d5b5582a99e5
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
- 交付模式: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 |
|---|---|---|---|
| 启动 | 3d3474a731f9ef3ee1f41935e63bca3afb70db56 | 3d3474a731f9ef3ee1f41935e63bca3afb70db56 | 2026-10-22T11:15:53Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(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/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T11:15:04.613824+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "3d3474a731f9ef3ee1f41935e63bca3afb70db56",
"after_head": "3d3474a731f9ef3ee1f41935e63bca3afb70db56",
"branch": "main",
"target_branch": "main",
"target_head": "3d3474a731f9ef3ee1f41935e63bca3afb70db56",
"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=3d3474a731",
"[remote] origin/main=3d3474a731 ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=6",
"[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] 3d3474a731(完整:3d3474a731f9ef3ee1f41935e63bca3afb70db56)",
"[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": "87e58b98732700984715adb3f7a46aeacdc3749aacf51643f68a2ad9ede2451d",
"receipt_sha256": "72f14e2080b227c1fc4094a0a4f573b0dc12fb08ed30c86400265a610fb09b0d",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/72f14e2080b227c1fc4094a0a4f573b0dc12fb08ed30c86400265a610fb09b0d.json"
}
```
| 同步(记录前对象) | c92443280ef8e9ed29bf5bf5250df650c01b4478 | c22b9fb34a3702191a9e5f6007f3538520db694b | 2026-10-22T13:10:38Z |
## 闭合记录(card-autoclose 回填)
- 依据:merge cb36b59accd0418c5ca952912d7c181b83038a4c(PR #702)已包含于 origin/main,分支成果已落盘;本段为合入后回填。
- 合并主题:Merge pull request 'Use dynamic ports across the five test files that shared hardcoded bindings' (#702) from fix/svc-test-port-governance-batch1 into main
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
@@ -1,99 +0,0 @@
# fix/svc-test-port-governance-deploy-txn 任务卡
任务主记录:PR #705 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/705(merge f673c2a15a);状态:已完成(回填);owner:weichen
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= cb36b59accd0418c5ca952912d7c181b83038a4c
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-svc-test-port-governance-deploy-txn(worktree);分支:fix/svc-test-port-governance-deploy-txn
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=37c57479990e37eb048ba81763fe402e1e679a822679f315a4be18e4b965c333;triage_receipt=/tmp/plast-triage.json;receipt_sha256=d403df8b2564bf4e1fe16fb12ecc48cf31c25be4f7472e6b27bce6da3ecd93e0
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
- 交付模式: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 |
|---|---|---|---|
| 启动 | cb36b59accd0418c5ca952912d7c181b83038a4c | cb36b59accd0418c5ca952912d7c181b83038a4c | 2026-10-22T13:39:45Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(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/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T13:28:41.022157+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "cb36b59accd0418c5ca952912d7c181b83038a4c",
"after_head": "cb36b59accd0418c5ca952912d7c181b83038a4c",
"branch": "main",
"target_branch": "main",
"target_head": "cb36b59accd0418c5ca952912d7c181b83038a4c",
"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=cb36b59acc",
"[remote] origin/main=cb36b59acc ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=9",
"[recycle] merged un-recycled branches: fix/gitea-address-migration fix/svc-test-port-governance-batch1",
"[cards] 已合入未闭合任务卡:fix-svc-test-port-governance-batch1.md——make task-close 回填归档(主检出运行)",
"[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] cb36b59acc(完整:cb36b59accd0418c5ca952912d7c181b83038a4c)",
"[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": "9f6260af81b0761dbb20afc5d145a9cb310303d426f655840a50f0879d81a49e",
"receipt_sha256": "eeb4f9ac9bdc0e30a0a7bbaa49176626bb8aca54a20f152b1771e1df8fd855d3",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/eeb4f9ac9bdc0e30a0a7bbaa49176626bb8aca54a20f152b1771e1df8fd855d3.json"
}
```
## 闭合记录(card-autoclose 回填)
- 依据:merge f673c2a15a50bef2c55f2c79a17d7601f82c8a07(PR #705)已包含于 origin/main,分支成果已落盘;本段为合入后回填。
- 合并主题:Merge pull request 'Use a dynamic port in the last hardcoded test binding' (#705) from fix/svc-test-port-governance-deploy-txn into main
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
@@ -1,99 +0,0 @@
# fix/svc-test-port-governance-final 任务卡
任务主记录:PR #698 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/698;状态:已完成(合入前闭合);owner:weichen
- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 955a32de2bb49171348bad5f566cce935df19212
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-svc-test-port-governance-final(worktree);分支:fix/svc-test-port-governance-final
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=04cbe9df900a03163aeadb42fd22e5a86ddeced179901b50ac0f4642ce7f6881;triage_receipt=/tmp/pg3-triage.json;receipt_sha256=aca390790027841efaefe2082123780b1ec8d775f9ca0be80262a80f7121bde9
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
- 交付模式: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 |
|---|---|---|---|
| 启动 | 955a32de2bb49171348bad5f566cce935df19212 | 955a32de2bb49171348bad5f566cce935df19212 | 2026-10-22T08:55:33Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(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/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T08:54:47.013779+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "955a32de2bb49171348bad5f566cce935df19212",
"after_head": "955a32de2bb49171348bad5f566cce935df19212",
"branch": "main",
"target_branch": "main",
"target_head": "955a32de2bb49171348bad5f566cce935df19212",
"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=955a32de2b",
"[remote] origin/main=955a32de2b ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=5",
"[recycle] merged un-recycled branches: feature/container-storage-plan",
"[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] 955a32de2b(完整:955a32de2bb49171348bad5f566cce935df19212)",
"[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": "b63c0d5c11be0292024cc972eee78fbcef7349381ee3bdecb432815b903373e6",
"receipt_sha256": "3ee914c56ff0e9cf2a4d11db1ec0a2e6c74cf745cceb7eff1cefd2f9d938ee89",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/3ee914c56ff0e9cf2a4d11db1ec0a2e6c74cf745cceb7eff1cefd2f9d938ee89.json"
}
```
## 闭合记录(合入前折叠)
- 本卡在 PR #698(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/698)合入前闭合并归档,随该 PR 一同合入。
- 该 PR 的合并提交即本任务的落地记录。
- 合入后无需回填,也无需单独的闭合 PR。
| 同步(记录前对象) | 98ee59c477785b2d5094fcbea3e67a4dc864c388 | a29562ef8315c6cbe71bf531720ce5ffa5ca2717 | 2026-10-22T09:35:11Z |
+178
View File
@@ -0,0 +1,178 @@
# feature/container-storage-plan-u1 任务卡
任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:weichen
- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 955a32de2bb49171348bad5f566cce935df19212
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan-u1(worktree);分支:feature/container-storage-plan-u1
- 分支类型:child;initiative_id:feature-container-storage-plan;root_target:main;immediate_target:feature/container-storage-plan;accepting_requirements:false;parent_branch:feature/container-storage-plan;parent_head_sha:b360bcb9745e20d9d4f9f06c44a2936f47217ae4;parent_pr:无;parent_source:origin
- 需求分流:免(父 initiative 子任务登记已列出 feature/container-storage-plan-u1;parent_head_sha=b360bcb9745e20d9d4f9f06c44a2936f47217ae4)
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
- 授权与交付终点:在父 initiative 授权范围内实施方案 U1(环境与数据归属合同),交付终点为合入父分支 feature/container-storage-plan。不含:生产数据盘点/访问、部署、生产迁移、容器改造实施(U2 起)。
- 当前阶段:实施与自检
## 目标与非目标
**目标(方案 U1)**:扩展现有实例配置与 manifest,绑定基座库、RDM 领域库、知识库三个逻辑数据库、持久文件根和环境身份(prod/test/verify);产出全部必需持久数据所有者的归属清单(类型、源版本、目录、引用形式、唯一写者;不含秘密与业务内容)。
**主要落点**:`scripts/svc_manifest.py`、`scripts/svc.sh`、`docs/svc.md`、`PROJECT.md`;新增环境 schema/示例建议置 `deploy/`。测试扩展 `tests/test_svc_identity.py`、`tests/test_svc_lifecycle_isolation.py`、`tests/test_svc_precheck.py`。
**完成条件**:配置、归属清单和迁移范围有唯一解释;明确需迁移的每个数据所有者;旧配置升级保留身份;错库/错根/foreign marker/缺失必需绑定拒绝;同名项目或冲突端口在修改前失败;在另一 checkout 调用仍定位同一资源。
**非目标**:不实现容器启停(U2)、迁移执行(U3)、备份恢复包(U4)、克隆隔离(U5);不改业务权限模型;不引入第二份 DSN/卷权威(KTD3:manifest 唯一权威)。
## Agent 实施收据
路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;每次尝试一行,原始日志留在源码外收据路径。
| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|---|---|---|---|---|---|---|---|---|---|---|
| container-storage-u1-env-data-ownership | manifest/svc 绑定三库+文件根+环境身份;数据归属清单 | ccceb3bf… | implementation | cursor,antigravity,pi-glm,pi,codex | 1 | cursor(auto) | failed-unknown | 无(失败保留) | 否 | /tmp/container-storage-u1-run-receipt.json |
| container-storage-u1-env-data-ownership | 同上(采用 attempt1 补丁并验证) | ccceb3bf… | implementation | cursor,… | 2 | cursor(auto) | failed-unknown | 无(JSON 后带尾文解码失败,现场保留) | 否 | /tmp/container-storage-u1-run2-receipt.json |
| container-storage-u1-env-data-ownership | 同上 | ccceb3bf… | implementation | cursor,… | 3 | cursor(auto) | failed-unknown | 无(CLI 启动即报 Cannot use this model: auto,模型列表为空;单测 CLI 正常,判瞬时故障) | 否 | /tmp/container-storage-u1-run3-receipt.json |
| container-storage-u1-env-data-ownership | 同上 | ccceb3bf… | implementation | cursor,antigravity,… | 4 | cursor(auto) | failed-unknown | 无(工作完成且 80 测试通过,但终态 JSON 后再次附带尾文解码失败;cursor 累计 4 次尝试耗尽,转下一候选) | 否 | /tmp/container-storage-u1-run4-receipt.json |
| container-storage-u1-env-data-ownership | — | ccceb3bf… | implementation | (run5 于尝试启动前被 host 终止并删除其私有运行目录,无工作产物;目的:避免 cursor 超上限重试) | 5a | — | cancelled | 无 | 否 | 无(无收据;host 披露于本行) |
| container-storage-routing-promote-antigravity | AGENT-EXECUTION.md implementation 顺序临时调整 | ccceb3bf… | implementation | cursor,… | 1(第 2 次运行;第 1 次遇 cursor 模型列表瞬时为空 failed-unknown) | cursor(auto) | completed | apply(0c8bea37) | 否 | /tmp/container-storage-routing-receipt2.json |
| container-storage-u1-env-data-ownership | 同上(采用 attempt1 补丁内容,经 antigravity 全新会话独立重做并验证) | ccceb3bf… | implementation | antigravity,pi-glm,pi,cursor,codex | 1 | agy(gemini-3.8-flash-high) | completed | apply(2d75d101) | 否 | /tmp/container-storage-u1-run7-receipt.json |
| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 955a32de2bb49171348bad5f566cce935df19212 | b360bcb9745e20d9d4f9f06c44a2936f47217ae4 | 2026-10-22T09:12:44Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 结果与验证(完成后填写)
- 结果:U1 完成。manifest/svc 扩展三库+文件根+环境身份绑定与错误拒绝;deploy/data-ownership.md 归属清单与 env-bindings.example.env 示例;文档同步。经 agent-exec transport ed39db43 进入分支(2d75d101)。
- 验证:三套件 80 项 PASS(1 skip,SKIP 为环境缺 DSN 的既有用例);make test-affected 574 项 PASS(18 skip)。证据见上方两个导入收据(SELF)。容器真实运行验证属 U2,NOT_RUN。
- 影响与剩余项:无阻塞。生产数据盘点需授权(方案 U1 生产部分推迟到 O1 前核实)。
## 合入与清理
- Review 记录:REVIEW PASS(无阻塞项;N1—N8 非阻塞,报告 docs/superpowers/reviews/2026-10-22-container-storage-u1-review.md,transport 7f0f... 见 /tmp/container-storage-u1-review-receipt2.json)。审阅者:pi-glm 全新会话(fresh_session,非实施者);base 955a32de、head e1f08650;重放 80 项单测 OK
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T09:11:55.612223+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan",
"before_head": "b360bcb9745e20d9d4f9f06c44a2936f47217ae4",
"after_head": "b360bcb9745e20d9d4f9f06c44a2936f47217ae4",
"branch": "feature/container-storage-plan",
"target_branch": "main",
"target_head": "955a32de2bb49171348bad5f566cce935df19212",
"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-wt-feature-container-storage-plan'",
"[repo] branch=feature/container-storage-plan head=b360bcb974",
"[remote] origin/main=955a32de2b ahead=2 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=6",
"[initiatives] local cards (not authoritative): feature/container-storage-plan",
"[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] 955a32de2b(完整:955a32de2bb49171348bad5f566cce935df19212)",
"[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-wt-feature-container-storage-plan'"
],
"output_sha256": "58ef898d944057a44fb02e1fabb6acc2fd97943beed355977229d13711b2e6b2",
"receipt_sha256": "59e8b2e7b7032c19207a19f9427486439ca15250333e83ddd162db739c5dc395",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/59e8b2e7b7032c19207a19f9427486439ca15250333e83ddd162db739c5dc395.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T10:41:41.286181+00:00",
"command": "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_svc_identity tests.test_svc_lifecycle_isolation tests.test_svc_precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan-u1",
"before_head": "2d75d101a04ce1d1c9fa1d66d9567894222399d5",
"after_head": "2d75d101a04ce1d1c9fa1d66d9567894222399d5",
"branch": "feature/container-storage-plan-u1",
"target_branch": "main",
"target_head": "98ee59c477785b2d5094fcbea3e67a4dc864c388",
"root_target": "main",
"immediate_target": "feature/container-storage-plan",
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"output_tail": [
"........................s.......................................................",
"----------------------------------------------------------------------",
"Ran 80 tests in 66.437s",
"",
"OK (skipped=1)"
],
"output_sha256": "1bc6f83d7e4c64a628d5f29bad07fc4e8cc2468937c66bda4bd9a5e00fc22587",
"receipt_sha256": "bfe74991277ebe308f667532e9b21af8215f453db124afeab1fc3d2115adf67d",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/bfe74991277ebe308f667532e9b21af8215f453db124afeab1fc3d2115adf67d.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T10:42:53.576009+00:00",
"command": "make test-affected",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan-u1",
"before_head": "2d75d101a04ce1d1c9fa1d66d9567894222399d5",
"after_head": "2d75d101a04ce1d1c9fa1d66d9567894222399d5",
"branch": "feature/container-storage-plan-u1",
"target_branch": "main",
"target_head": "98ee59c477785b2d5094fcbea3e67a4dc864c388",
"root_target": "main",
"immediate_target": "feature/container-storage-plan",
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"output_tail": [
"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 574 tests in 400.853s",
"",
"OK (skipped=18)",
"make[2]: Leaving directory '/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan-u1'",
"make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan-u1'"
],
"output_sha256": "d738cef7d9ea78bcc4624e0fe8eb87b651e88712fcd283bb26c21755168604e9",
"receipt_sha256": "0565d5b327cb8d01e48deda867bd7369dd5728b5ad9024b51bbd6cb1a2fdf246",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/0565d5b327cb8d01e48deda867bd7369dd5728b5ad9024b51bbd6cb1a2fdf246.json"
}
```
+177
View File
@@ -0,0 +1,177 @@
# feature/container-storage-plan 任务卡
任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:weichen
## 基线与授权
- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 955a32de2bb49171348bad5f566cce935df19212
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-container-storage-plan(worktree);分支:feature/container-storage-plan
- 分支类型:initiative;initiative_id:feature-container-storage-plan;root_target:main;immediate_target:main;accepting_requirements:true
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=53a3aac8b5638347861577e2ab6f487c182b364e44b1fb0b563df0fdb5834870;triage_receipt=/tmp/container-storage-plan-triage-20261022-b.json;receipt_sha256=31e6accb098e8572e3698e04e3dbafc340fd9ef300df7a09110f1cb249aabb44
- 授权与交付终点:按已审阅方案实施 U1—U6(M1—M3)并交付到 main;含各仓代码/测试/文档变更、PR/CI/合入。不含:生产迁移 O1、生产数据读取/写入、部署到生产、数据库大版本升级。
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
- 当前阶段:实施与自检(M1/U1 起);当前增量:I1=U1
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
## 目标与非目标
### 章程(charter)
**用途与目标**:按已审阅方案(`docs/superpowers/plans/2026-10-22-container-storage-plan.md`)把部署形态改造为:一个完整应用服务二进制、两个 PostgreSQL 容器(业务基座库 + RDM 领域库)、知识库 PostgreSQL/pgvector 容器(第三个逻辑库)、外置持久文件根;生产/测试/验证三套环境独立配置、数据、身份与凭据;形成可验证的完整备份恢复与环境克隆隔离流程(需求 R1—R8、决策 KTD1—KTD7 见方案)。
**接纳范围**:方案 Implementation Units U1—U6 及其测试;各仓仅做阻塞目标的最小接线。U1 环境与数据归属合同;U2 容器数据库与 svc 接线;U3 迁移与文件引用兼容;U4 组合备份与恢复新目标;U5 测试/验证克隆与环境选择;U6 单二进制与组合验收、运维交付。
**非目标**:生产迁移执行(方案 O1,另获窗口授权后单独安排);高可用平台、Kubernetes、托管云数据库、全量对象存储、应用微服务化;零停机/实时复制/PITR;删除既有 SQLite/原生 PG 路径;PG 大版本升级。
**分级测试策略**:L2→L1 用 `make test-affected` 最小直接检查;L1→main 最终窗口跑完整组合(`make test`,含 `test-pg`、`svc-realcombo`、`test-regulations-kb`);容器与文件恢复必须有真实临时环境验证,缺 DSN 的跳过不计通过。
**验收条件**:方案 Definition of Done——单二进制在目标环境启动并符合两容器三库边界;三环境独立可启停重建且隔离实测;完整备份恢复到新目录且原文/引用不变;真实迁移 fixture 对账通过;精确组件组合、测试证据、独立 Review、必需 CI 与规范文档齐备。
**追加领域规则**:manifest 是身份与期望态唯一权威,Compose 只作版本化模板(KTD3);普通启停/重建不隐式删卷、不隐式恢复旧备份、不在数据库失败时回退空 SQLite(R7);镜像按选定 digest/扩展版本逐项核对(KTD7);秘密不入仓不入日志(baseline 硬边界 2)。
**生命周期**:M1(U1→U2)→ M2(U3→U4)→ M3(U5→U6)→ M4 生产切换(O1 另行授权,不在本 initiative 内)。U1 完成后按实测数据规模细化后续工期;全部必需子任务合入后置 `accepting_requirements=false` 收口合入 main。
## 子任务登记
| 子任务分支 | 必需 | 状态 | PR |
|---|---|---|---|
| feature/container-storage-plan-u1 | 是 | 未开始 | — |
| feature/container-storage-plan-u2 | 是 | 未开始 | — |
| feature/container-storage-plan-u3 | 是 | 未开始 | — |
| feature/container-storage-plan-u4 | 是 | 未开始 | — |
| feature/container-storage-plan-u5 | 是 | 未开始 | — |
| feature/container-storage-plan-u6 | 是 | 未开始 | — |
## 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 | — |
## 关联方向与规划
- 适用状态:适用(本 initiative 的方案与实施计划)
- 主记录路径/版本:`docs/superpowers/plans/2026-10-22-container-storage-plan.md`(2026-10-22 审阅快照,含落库状态注记)
- 对应里程碑:M1(U1→U2)→ M2(U3→U4)→ M3(U5→U6);M4/O1 生产切换不在本 initiative
## 需求分析
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|---|---|---|---|---|---|---|
| R1—R8 | 见方案 Requirements 表(R1 单二进制、R2 容器化、R3 环境绑定、R4 恢复可核对、R5 迁移覆盖、R6 克隆隔离、R7 无隐式破坏、R8 操作记录) | 符合(方案已核对现有 svc/manifest/迁移器/KB Compose 能力) | 高 | 见方案各单元完成条件与 Definition of Done | U1←无;U2←U1;U3←U1/U2;U4←U2/U3;U5←U4;U6←U1—U5 | 纳入本增量(U1—U6) |
## 方案与计划
- 方案引用:`docs/superpowers/plans/2026-10-22-container-storage-plan.md`(2026-10-22 审阅快照;Product Contract、KTD1—KTD7、High-Level Technical Design)
- 实施计划引用:同上 Implementation Units U1—U6 与里程碑 M1—M3(M4/O1 不在本 initiative)
- 测试计划引用:同上 Verification Contract 与各单元测试文件/场景清单
## 阶段记录
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|---|---|---|---|---|---|
| 需求拆解 | 方案 Requirements R1—R8 | 已完成 | 主模型(方案会话)+ triage NEW | 2026-10-22 | /tmp/container-storage-plan-triage-20261022-b.json |
| 需求评估 | 方案 Problem Frame/边界假设 | 已完成 | 三视角文档审阅 + 主模型复核 | 2026-10-22 | /tmp/container-storage-plan-20261022/review.md |
| 需求表确认 | R1—R8 纳入 U1—U6 | 已完成 | 主模型 | 2026-10-22 | 方案 Requirements 表 |
| 方案设计 | KTD1—KTD7 | 已完成 | 方案会话(已审阅) | 2026-10-22 | plan.md |
| 实施计划 | U1—U6 + M1—M4 | 已完成 | 方案会话(已审阅) | 2026-10-22 | plan.md Implementation Units |
| 实施与自检 | 待定 | 待执行 | — | — | — |
| 正式测试与验收 | 待定 | 待执行 | — | — | — |
| 代码 Review | 待定 | 待执行 | — | — | — |
| 合入 | 待定 | 待执行 | — | — | — |
| 合入核验与收口 | 待定 | 待执行 | — | — | — |
## 探索试验与迭代
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 955a32de2bb49171348bad5f566cce935df19212 | 955a32de2bb49171348bad5f566cce935df19212 | 2026-10-22T09:09:21Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 交付调度
- 调度状态:未入队;调度记录:无
- entry_id:无;attempt_id:无;窗口执行者:无
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/delivery.md#合入调度
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
## 结果与验证(完成后填写)
- 结果:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
## 规划进展与下一步
- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划)
- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务)
- 推荐依据:待填写
- 前置条件/待确认:待核对
- 预期产物/验收条件:待定义
- 重复任务核对:待核对(已有/在途任务、PR 与规划记录)
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
## 合入与清理
- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据)
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T09:08:28.920468+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev",
"before_head": "955a32de2bb49171348bad5f566cce935df19212",
"after_head": "955a32de2bb49171348bad5f566cce935df19212",
"branch": "main",
"target_branch": "main",
"target_head": "955a32de2bb49171348bad5f566cce935df19212",
"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=955a32de2b",
"[remote] origin/main=955a32de2b ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=5",
"[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] 955a32de2b(完整:955a32de2bb49171348bad5f566cce935df19212)",
"[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": "ed0263b348ce265d081aab1faa0d1370f2a9478410fadfc3b9cec8a810c1295f",
"receipt_sha256": "124a47e6bfbb43cbbbc398ad10bf42e3d448373a80affd297ed25c41d8b2b07a",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/124a47e6bfbb43cbbbc398ad10bf42e3d448373a80affd297ed25c41d8b2b07a.json"
}
```
-680
View File
@@ -1,680 +0,0 @@
# feature/ecs-deploy-dev8788-and-kb 任务卡
任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= cb36b59accd0418c5ca952912d7c181b83038a4c
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb(worktree);分支:feature/ecs-deploy-dev8788-and-kb
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=420311b8d5e0543749bb9743c05ae6757cc0244c7cfda204e6c28bc92eaf8b87;triage_receipt=/tmp/ecs-kb-deploy-triage.json;receipt_sha256=7a0bf8e0d2b9f1adcac5e58cbe70204473007a4dfd04a96f495dde3c0dd71ddb
- 授权与交付终点:owner 2026-10-22 指令“将当前的二进制服务部署到 imc-tx-ecs:先备份远端机器上的服务和数据库;将本机的知识库也部署在远端上;然后部署本机 8788 上的二进制服务;尽可能减少远端服务下线时间并保证服务可用;更换二进制前先告知 owner 确认”,并于 2026-10-23 追加“先把配置弄好,superadmin 账号使用 imc-tx-ecs 上的 sundehui,密码 12345678,第一次登录需要重置密码(把已有用户变成超级管理员)”。交付终点:ECS 8788 运行与本机 dev 同版本二进制(sha256 00ead2cf…),ECS 承载本机 kbreg 知识库且检索可用,远端用户数据零丢失,切换带自动回滚并留存冷备与异地副本。仓库写入仅本任务卡。未授权:修改产品代码、改 Caddy 路由/域名、删除远端既有备份或旧二进制、改动 sundehui 以外账号的凭据、owner 确认前执行二进制切换。
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进;切换二进制属人工决策点,先备齐再请确认)
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收
- 当前阶段:实施与自检(I1 备份 → I2 知识库 → I3 演练;I4 切换待 owner 确认);当前增量:I3(远端演练,零停机)
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
## 目标与非目标
预期结果:ECS(imc-tx-ecs,43.143.211.203,Ubuntu 22.04,2 vCPU/3.6GiB/49GiB 可用;Caddy `imcrd-agent.imcbot.work` → 127.0.0.1:8788)上:
1. 本机 8788 dev 二进制服务原样上线——`runtime/dev/current` 指向 `releases/rdm-ee8c2cd-20261022T055052Z-637762`,`bin/agent-system` sha256 `00ead2cfcb967441058d0413850467ba710738b6a9510d4f983894f00bc0b3de`(141509195 B),同版 htdocs;systemd 用户单元 `agent-system-dev.service` 的 ExecStart 参数不变。
2. 本机知识库供给到 ECS——本机 kbreg(PostgreSQL 16.15 + pgvector,127.0.0.1:5436,3.1 GB)在 ECS 以原生 PostgreSQL 16.15 + pgvector(回环 5436)承载,语料、已发布修订与索引逐表可核;服务侧经 `serve.env`(`KBREG_*`)+ systemd drop-in `EnvironmentFile` 启用。
3. 远端用户数据零丢失——`data/agent-system.db`(schema v35,24 MB + WAL)与 `rdm.db`(1.5 MB + WAL)经 SQLite 在线备份 API 一致落盘,切换前后以 `ecs-dbsnap.py manifest/compare` 逐行对账 `problems=[]`。
受影响:ECS `runtime/dev/{bin,htdocs,plugins,data,backups,serve.env,svc.env,history.jsonl}`、`~/.config/systemd/user/agent-system-dev.service.d/`、ECS 新增 PostgreSQL 16 集群。本仓:仅 `.task/**`。
非目标:不改产品代码与发布物;不改 Caddy 路由、域名与 TLS;不迁移本机业务数据到 ECS(仅知识库);不删除远端既有备份、旧二进制与 `agent-system-admin/cli`;不动账号、凭据与登录 Key(远端无 `super-admin.env`,切换后仍无,与现状一致);不引入 Docker(远端无容器运行时且不可达镜像仓库)。
## Agent 实施收据
路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;每次尝试一行,原始日志留在源码外收据路径。
| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|---|---|---|---|---|---|---|---|---|---|---|
| 无(运维任务,无仓库代码单元;仓库写入仅 `.task/**` host 豁免) | — | — | — | — | — | — | — | — | — | — |
## 关联方向与规划
- 适用状态:适用但不同阶段——`feature/container-storage-plan`(PR #697,open)是容器化部署形态 initiative,其章程把“生产迁移执行(O1)”列为非目标并要求另获窗口授权;本任务是该形态落地前的既有 SQLite 二进制运维部署,与 U1—U6 无依赖,分流 verdict=NEW(理由见分流收据)。
- 主记录路径/版本:`docs/superpowers/plans/2026-10-22-container-storage-plan.md`(位于 feature/container-storage-plan 分支,未合入 main;本卡引用其 O1 边界,不复制正文)
- 对应里程碑:不在其 M1—M3 内;本任务为独立运维增量 I1—I4
## 需求分析
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|---|---|---|---|---|---|---|
| R1 | 先备份远端服务与数据库 | 符合(owner 指令;沿用 `ecs-dbsnap.py` 在线备份 API) | P0 | 远端 `backups/<TS>-warm-pre-ee8c2cd/` 含 data/bin-old/htdocs-old/config + SHA256SUMS 校验通过 + 本机异地副本逐文件 sha 通过 | 无 | 达成(见证据) |
| R2 | 本机知识库部署到远端 | 符合(远端无容器运行时且镜像仓库不可达,按同主版本原生 PostgreSQL 16.15 + pgvector 供给) | P0 | ECS 127.0.0.1:5436/kbreg 可连;`schema_migrations` 0001—0005;关键表行数与本机一致;向量检索返回命中 | 无 | 达成(见证据) |
| R3 | 部署本机 8788 二进制服务且最小停机 | 符合(owner 指令) | P0 | 运行中 exe sha256=00ead2cf…;本地与域名 HTTP 200;停机窗口 ≤ 演练实测 + 人工确认 | R1、R2 | **未达成**:技术切换完成(停机 6.88 s、三处 200),但业务验收 `extension: project context unavailable: not-found`,已回退到旧版本 |
| R4 | 服务可用性保障(回滚) | 符合(切换脚本含健康门失败自动回滚) | P0 | 演练实例证明新版本可启动并迁移 v35→最新;回滚路径演练可用 | R3 | 达成(回退装备 sha 通过 + rollback-now.sh 已就位) |
| R5 | 用户数据零丢失 | 符合 | P0 | 冷备 manifest ↔ 切换后逐行对账 `problems=[]`;settings 明文密钥与 `secret_keys` 逐把一致 | R1、R3 | 达成(切换后与回退后两次对账均 `problems=[]`、零缺失;回退期间曾因脚本嵌套缺陷短暂运行空库,已从冷备真实路径恢复并核验) |
| R6 | 超级管理员 = 既有账号 sundehui(口令 12345678,首次登录强制改密) | 符合(owner 2026-10-23 追加指令) | P0 | 库中仅 sundehui 带超管标记且 `is_admin=1`、`must_change_password` 置位;服务以 `SUPER_ADMIN_OWNER=sundehui` 启动日志为 `already established`(无冲突);`POST /api/auth/login` 200 且 `isSuperAdmin=true`、`sessionScope=set_password_only` | 切换时达成并验证;回退后该标记随冷备数据撤回(远端现无超管标记,下次切换需重跑提升步骤) |
| R7 | 仓库侧交付(任务卡 + PR) | 符合 | P1 | Review 通过、CI 通过、PR 合入 main、任务卡闭合 | R1—R6 | 待收口 |
## 方案与计划
### 对象版本(本机发布物,运行中 /proc/652953/exe)
- 二进制:`/home/weichen/projects/Agent-System-Dev/runtime/dev/current/bin/agent-system`,sha256 `00ead2cfcb967441058d0413850467ba710738b6a9510d4f983894f00bc0b3de`,141509195 B;来源 `rdm@ee8c2cd`(`runtime/dev/svc.env`),started 2026-10-22T05:51:02Z,监听 127.0.0.1:8788。
- 前端:`current/htdocs`(148 KB,与本机 release 同源);`current/plugins` 为空(远端 plugins 亦为空)。
- 知识库:本机容器 `kbreg-postgres-1`(`pgvector/pgvector:pg16`,PostgreSQL 16.15),库 `kbreg` 3.1 GB;`schema_migrations` 0001—0005;regulations/editions 2897、legal_nodes 1701178、index_entries 191557、retrieval_chunks 64076、ingest_runs 2943。
- 远端现状:`agent-system.db` schema_max=35(58 表 / 28072 行)、`rdm.db`(28 表 / 3578 行,无 `schema_migrations`,由 `schema_version` 管理)、data 树 367 MB、bin 178 MB。
### 增量与步骤
- I1 备份(零停机):`~/ecs-ops/backup-warm.sh warm-pre-ee8c2cd`——SQLite 在线备份 API 取两个库的一致快照 + rsync 文件树(排除库本体)+ bin/htdocs/plugins/systemd 单元/Caddyfile/svc.env/svc.pid/history.jsonl + `manifest` 活库基线 + 相对路径 SHA256SUMS;随后 `~/ecs-ops/offsite-copy.sh` 流式拉回本机 `~/backups/imc-tx-ecs/<同名>/` 并逐文件校验。
- I2 知识库(零停机):远端加 PGDG apt 源 → 安装 `postgresql-16`(16.15)+ `postgresql-16-pgvector`(0.8.7)→ 集群改 `port=5436`、`listen_addresses=localhost` → 建 `kbreg` 角色与库 → 本机 `pg_dump -Fc`(容器内 16.15 客户端,与服务端同主版本)流式传输 → `pg_restore --no-owner --no-privileges` → 逐表行数与 `schema_migrations` 核对 → 向量检索冒烟 → 从本机 `serve.env` 只取 `KBREG_*` 行落远端 `runtime/dev/serve.env`(600,凭据值不出管道)→ systemd drop-in `20-serve-env.conf` + `daemon-reload`(不重启服务)。
- I3 演练(零停机):新二进制与 htdocs 预置到远端 `runtime/dev/stage/<版本>/`;`rsync -a` 复制 data 为 `rehearsal/data`;以 `127.0.0.1:8789` 启动演练实例(带 KBREG 环境)——实测启动+迁移耗时、HTTP 200、schema 版本、关键表行数、日志无 error、KB 检索可用;结束后清理演练目录。
- I4 切换(待 owner 确认后执行):停 recover timer → 停服务 → 库文件冷拷到新备份目录 → `install`+`mv` 原子换二进制、换 htdocs → 启动 → 健康门(本地 200、域名 200、pid/exe sha、schema、KB)→ 失败自动回滚(旧二进制 + 冷备 data + 旧 htdocs)→ 更新 svc.env/svc.pid/history.jsonl → 启 recover timer。
- I5 收口:切换后逐行对账 `compare`、密钥一致性、启动日志、证据写入、Review、PR/CI、合入。
### 停机时间控制
- I1—I3 全程不动线上进程;数据与文件预置在切换窗口之前完成。
- 切换窗口只做:停服务 → 拷两个库(24 MB + 1.5 MB + WAL)→ 原子换二进制 → 启动 → 健康检查。库迁移(v35→最新)在新进程启动时执行,耗时以 I3 演练实测为准;演练若显示迁移显著耗时,再评估预迁移 + 增量拷贝方案后才切换。
### 测试计划
- I1:远端 `sha256sum -c SHA256SUMS` 通过 + 本机异地副本同样通过 + `manifest` 生成成功(28,072 + 3,578 行)。
- I2:`psql` 连接成功;`schema_migrations` 5 条;`select count(*)` 关键表与本机一致(差异即 FAIL);`revision verify` 等价核验(staged/computed sha 一致);一条向量相似度查询返回非空命中;演练实例带 KBREG 启动无 KB 连接错误。
- I3:演练实例 `/` 200、`/healthz` 200;`schema_migrations` 升到目标版本;`users/conversations/runs` 行数与冷备一致(迁移不丢数据);日志 grep 无 `error|panic|fatal`。
- I4/I5:运行中 exe sha256 前缀 `00ead2cf`;`127.0.0.1:8788` 与 `https://imcrd-agent.imcbot.work/` 均 200;`compare MANIFEST-LIVE-PRE.json data` → `problems=[]`;`secret-check.py` 明文密钥与 `secret_keys` 逐把一致;停机窗口秒数由脚本记录。
## 阶段记录
| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 |
|---|---|---|---|---|---|
| 需求拆解 | R1—R6 | 已完成 | 主模型 + triage NEW | 2026-10-22 | /tmp/ecs-kb-deploy-triage.json |
| 需求评估 | 对象版本与远端现状核对 | 已完成 | 主模型 | 2026-10-22 | 本卡“方案与计划” |
| 需求表确认 | R1—R6 | 已完成 | owner 指令即验收边界 | 2026-10-22 | owner 2026-10-22 指令 |
| 方案设计 | I1—I5 | 已完成 | 主模型 | 2026-10-22 | 本卡“方案与计划” |
| 实施计划 | I1 备份 / I2 知识库 / I3 演练 / I4 切换 / I5 收口 | 已完成 | 主模型 | 2026-10-22 | 同上 |
| 实施与自检 | I1—I3b(rdm@ee8c2cd,备份/知识库/演练/超管配置) | 已完成 | 主模型 | 2026-10-23 | 证据表 |
| 正式测试与验收 | I4 切换后核验 | **失败并回退**(`project context unavailable: not-found`) | 主模型 + owner 指令回退 | 2026-10-23 | 证据表(回退后 `problems=[]`) |
| 代码 Review | 待收口 | 待执行 | — | — | — |
| 合入 | 待收口 | 待执行 | — | — | — |
| 合入核验与收口 | 待收口 | 待执行 | — | — | — |
## 探索试验与迭代
| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 |
|---|---|---|---|---|---|---|---|
| I3 远端演练实例(8789 + 复制 data + KBREG 环境) | 新版本能在 v35 旧库上完成迁移并在带知识库环境下健康启动 | 远端副本,不接流量、不写线上库 | 迁移失败或超 5 分钟 → 停下先修迁移/换方案,不进入切换窗口 | owner 指令“保证服务可用、最小停机” | 待执行 | 无(演练目录用后清理) | 是(切换前重放同一演练) |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | cb36b59accd0418c5ca952912d7c181b83038a4c | cb36b59accd0418c5ca952912d7c181b83038a4c | 2026-10-22T13:41:37Z |
## 证据(Evidence)
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 交付调度
- 调度状态:未入队;调度记录:无
- entry_id:无;attempt_id:无;窗口执行者:无
- 固定候选:无(base/head、组件与相关配置引用实际尝试)
- 调度依据:policy/delivery.md#合入调度
- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合
## 结果与验证
- 结果(I1 备份,零停机):远端 `backups/20261022T134400Z-warm-pre-ee8c2cd/`(539 MB、629 文件,SQLite 在线备份 API 取两个库的一致快照 + 文件树 rsync + bin/htdocs/plugins/systemd 单元/Caddyfile/svc 登记 + 活库基线 MANIFEST-LIVE-PRE.json)远端 `sha256sum -c` 通过;本机异地副本 `~/backups/imc-tx-ecs/20261022T134400Z-warm-pre-ee8c2cd/` 逐文件校验通过(629 文件)。线上进程全程未动(pid 4080336 自 2026-10-18 14:20:26 运行至今)。
- 结果(I2 知识库):远端装 PostgreSQL 16.15(PGDG,jammy)+ pgvector 0.8.7,回环 `127.0.0.1:5436`;角色/库 `kbreg`;本机容器 `pg_dump -Fc`(16.15 客户端,同主版本)→ `pg_restore --no-owner --no-privileges --no-comments`(3m09s);`serve.env`(600,仅 KBREG_*)与单元 drop-in `20-serve-env.conf` 已就位(daemon-reload,未重启服务)。
- 结果(I3 演练,零停机):`stage/rdm-ee8c2cd-20261022T055052Z-637762/bin/agent-system` sha256 与本机运行中制品一致(`00ead2cf…`);复制 data(367 MB)后以 8789 启动新二进制 + KBREG 环境:**1.02 s 健康(HTTP 200)、schema v35→v51 自动迁移完成、users 18→19(多出的就是本次演练的超级管理员账号)、conversations/runs/messages 不变、日志无 error/panic/fatal**;冷拷 data 实测 1.14 s、只拷库 0.05 s。
- 结果(I3b 超级管理员演练,零停机):因为产品只在启动时从 `SUPER_ADMIN_*` 建立超管,且**拒绝把已有账号直接指定为超管**(`configured owner belongs to an existing account without the super-admin marker`),采用与产品 bootstrap 同形的等价写入(`~/ecs-ops/seed-superadmin.py`:一个事务内更新 `users` 的 `is_admin/is_super_admin/password_hash(argon2id)/must_change_password/auth_version/credential_origin/initialization_state/state_version` + 按 v49 动作集写 `system_grants`(12 项)+ `account_events` + `audit_events`)。演练实测:阶段 A `--auth off` 迁移 v35→v51 用时 **0.27 s**;阶段 B 提升 sundehui(用户数仍为 18,未新增账号);阶段 C 以 `--auth on` + `SUPER_ADMIN_OWNER=sundehui` 启动,日志 `super admin: account "sundehui" already established`,健康 **0.27 s**;`POST /api/auth/login` 返回 `isSuperAdmin=true`、`sessionScope=set_password_only`(首次登录只能改密),`/api/me/profile` 403 `set-password session only`,日志无 error/panic/conflict。
- 配置就绪(远端,零停机):`runtime/dev/super-admin.env`(600,`SUPER_ADMIN_OWNER=sundehui`);drop-in `20-serve-env.conf` 同时引用 `serve.env` 与 `super-admin.env`(已 `daemon-reload`,未重启服务);待用口令哈希文件 `~/ecs-stage/seed.hash`(600)已就位;切换脚本 `~/ecs-ops/cutover-ee8c2cd.sh` 已按 A/B/C 三阶段重写并通过语法检查。
- 结果(I4 切换 → **验收失败 → 已回退**):08:43:27 CST 停服、08:43:30 CST 新版本健康,**停机 6.88 s**;新版本 pid 1617793、exe sha256 `00ead2cf…`、schema v51、本地/healthz/域名 200、sundehui 受限登录 200。**但业务验收出现 `extension: project context unavailable: not-found`(RDM 项目上下文解析为 not-found),owner 08:47 要求立即回退。** 回退分两步:首次执行 `rollback-now.sh` 因脚本缺陷(`mkdir -p $B/data` 后 `cp -a $R/data $B/data` 产生嵌套 `data/data/`,回退取到空目录)使服务短暂运行在空库;发现 users=0 后立即从冷备真实路径 `$B/data/data` 恢复,08:48:35 起服务运行旧版本,数据完整。两个脚本已修复并同步远端。
- 回退装备(保留可用):`runtime/dev/rollback/kit-20261023T004302Z`(旧二进制 sha `1f45f640…`、htdocs、单元、Caddyfile,12 文件 sha 通过)+ 修复后的 `rollback-now.sh`(嵌套目录探测、`rm -rf $R/data` 避免合并)+ 修复后的 `cutover-ee8c2cd.sh`(不再预建 `$B/data`)+ 冷备 `backups/20261023T004322Z-cold-pre-ee8c2cd`(真实数据在 `data/data`)+ 切换后的 v51 数据留存于 `runtime/dev/data.rolled-back` 供定位。
- 回退后核验(逐项,证据均为 SELF 收据):旧二进制 exe sha256 `1f45f640…`、pid 1619863、schema v35、`rdm.db` schema_version=6、本地/healthz/域名 200、recover timer 恢复;数据 users 18 / conversations 19 / runs 110 / messages 246,与热备基线逐行对账 **`problems=[]`、零缺失**(605→609 文件,新增 4 个为基线后新产生的用户文件);回退装备 SHA 通过;知识库仍在(legal_nodes 1701178、index_entries 191557)且 drop-in 已移除,旧二进制不读取它。热备/冷备/本机异地副本 SHA 全部通过;知识库指纹与端到端检索(隧道 + `kbreg migrate/search`)通过;演练迁移/健康/超管提升/受限登录通过;**浏览器端业务验收 FAIL**(`extension: project context unavailable: not-found`)。
- 影响与剩余项:
- 业务浏览器端验收(sundehui 首次登录改密、界面检索)NOT_RUN:需真人用真实账号密码登录,本任务不代持口令做交互验证。
- 回滚会丢失切换后新产生的数据(旧版本读不了 v51 库),执行前需 owner 确认。
- `sundehui` 现为 `is_admin=1` + `is_super_admin=1`(产品要求超管必须同时是管理员),口令哈希写入时 `auth_version+1`,旧会话失效。
- 知识库检索状态与本机一致为 degraded(本机向量索引同样为空);恢复向量索引属后续独立增量。
- 远端知识库为原生 PostgreSQL 16.15(非容器,远端无容器运行时且不可达镜像仓库),与 container-storage-plan 的容器决策可用同主版本 dump/restore 迁移。
- 结果(I6 修复合入,2026-10-23):owner 验收通过后按治理流程合入——所有代码变更都经 `scripts/agent-exec run` 的 worker transport 与 `apply` 进入分支(admit 门禁只放行 transport 或豁免路径),逐仓 CI 通过后合并:
- Agent-System-Backend PR #250「Carry project-scope defaults for roles with project management」→ main `1d04f07`;`go test ./internal/store ./internal/app ./internal/server` 通过,transport diff 与上线版本逐字一致。
- Agent-System-Frontend PR #129「Hide system actions from the role editor」→ main `9cc7113`;`npm run build` 与 717 个 vitest 通过。注意:PR #127(一次性密码改端点、恢复提案下线、侧栏无项目对话入口)在合入前发现上游 main 已自行实现同等能力,已关闭并以 #129 取代。
- Agent-System-RDM PR #482「Resolve host project ids in context and fall back to member defaults」→ main `d879839`;pin 同步到已合入的基座与前端 main,`go test ./...` 通过。
- Agent-System-Dev:main 的 `modules/backend` pin(`bd5c0f39`)已包含基座合并,无需再改;Dev 不跟踪 RDM 仓。
## 规划进展与下一步
- 本次推进/剩余:I1—I5 完成(首次切换因业务错误回退 → 三处修复并在 58788 验证 → 重建后二次切换停机 6.13 s 成功 → 角色/超管/模型配置就位 → 修复全部经 worker transport 合入 main)。剩余:合并后的产物(前端来自已合入 main)与线上二进制不同,需要你决定是否再切一次。
- 首选下一任务:本任务 I5(不新建任务)。
- 推荐依据:验收失败必须先消除阻塞项,不得以“技术切换成功”替代业务可用(`policy/delivery.md#目标优先范围控制与问题分流`)。
- 前置条件/待确认:定位材料——触发错误的入口(页面/会话)与其客户端响应;`data.rolled-back` 中已迁移的 v51 库可离线复现。
- 预期产物/验收条件:离线复现或代码级定位 `not-found` 成因;演练新增带项目上下文的真实请求并通过;再请 owner 确认切换窗口。
- 已知缺口:I3 演练只验证启动/迁移/登录/超管接口,**未发起带项目上下文的真实业务请求**,因此未能提前发现该错误。
- 重复任务核对:已核对 `.task/archive/feature-ecs-sync-local-dev.md`(2026-10-18 同机切换)与 feature/container-storage-plan(容器化形态,O1 生产迁移不在其范围);无重复在途任务。
- 启动授权:未授权(建议不自动启动新任务,仍须明确授权)
## 合入与清理
- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据)
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T13:28:34.782723+00:00",
"command": "make precheck",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-bootstrap-r0",
"before_head": "cb36b59accd0418c5ca952912d7c181b83038a4c",
"after_head": "cb36b59accd0418c5ca952912d7c181b83038a4c",
"branch": "HEAD",
"target_branch": "main",
"target_head": "cb36b59accd0418c5ca952912d7c181b83038a4c",
"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-wt-bootstrap-r0'",
"[repo] branch=HEAD head=cb36b59acc",
"[remote] origin/main=cb36b59acc ahead=0 behind=0",
"[tree] clean submodules_drift=0 uninit=0",
"[worktrees] linked=9",
"[recycle] merged un-recycled branches: fix/gitea-address-migration fix/svc-test-port-governance-batch1",
"[cards] 已合入未闭合任务卡:fix-svc-test-port-governance-batch1.md——make task-close 回填归档(主检出运行)",
"[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] cb36b59acc(完整:cb36b59accd0418c5ca952912d7c181b83038a4c)",
"[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-wt-bootstrap-r0'"
],
"output_sha256": "37096e9c91f721e54d19452b169e8558a0ddd1e0699512acf8d17d8331046441",
"receipt_sha256": "57bedc8bb759f306f6a2623b48f7117ef650a0a0c7951719ef6a25f5f51276eb",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/57bedc8bb759f306f6a2623b48f7117ef650a0a0c7951719ef6a25f5f51276eb.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T15:22:01.538077+00:00",
"command": "bash ~/ecs-ops/verify-local.sh",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"after_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "c3fc389b9f7060ee590439a7a26bffc45f454be8",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "a06b59bf58b89bcbe5ce8d8ca204d6d376fd61fd86af9b0ffbd7133a8d7c5b21",
"after_tracked_diff_sha256": "a06b59bf58b89bcbe5ce8d8ca204d6d376fd61fd86af9b0ffbd7133a8d7c5b21",
"output_tail": [
"=== 本机异地副本 ===",
"OFFSITE_SHA_OK files=629",
"539M\t/home/weichen/backups/imc-tx-ecs/20261022T134400Z-warm-pre-ee8c2cd",
"=== 知识库指纹比对:本机容器 vs ECS(经隧道) ===",
"KB_FINGERPRINT_IDENTICAL",
"md5_regulations|56bcaad63aab3f505f115d867faeb12b",
"md5_editions|a93c46f75144e3359ef8c78ca1b67b2d",
"md5_index_entries|7e599356f4aeb45631e074f6bbc99f6e",
"md5_revision_members|5322ba17aff3ffbd1d23ba48248522d0",
"legal_nodes_bytes|888742683"
],
"output_sha256": "5c4ca6b9861b52bd9dd704794bbfdbfac60ad80af5ce1daa77caf38766aea3c9",
"receipt_sha256": "7eb168bc415a5a89d4d30baea6989c77f7026b8b54aa72a689ab03a7808cb34d",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/7eb168bc415a5a89d4d30baea6989c77f7026b8b54aa72a689ab03a7808cb34d.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T15:23:11.116009+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/verify-remote.sh'",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"after_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "c3fc389b9f7060ee590439a7a26bffc45f454be8",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "bc02c637e5aa7caf698d242f47e2c4565469c1d97e695b71f8ac8c75a8d4c07f",
"after_tracked_diff_sha256": "bc02c637e5aa7caf698d242f47e2c4565469c1d97e695b71f8ac8c75a8d4c07f",
"output_tail": [
"version=agent-system 0.1.0-dev",
"started=2026-10-18T14:20:27+08:00",
"local8788=200",
"domain=200",
"=== 线上库 schema ===",
"agent-system.db schema_max 35",
" users 18",
" conversations 18",
" runs 109",
" messages 244",
"rdm.db schema_version [(6,)]",
" users ERR no such table: users",
" conversations ERR no such table: conversations",
" runs ERR no such table: runs",
" messages ERR no such table: messages",
"=== 远端热备校验 ===",
"backup=/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/backups/20261022T134400Z-warm-pre-ee8c2cd",
"REMOTE_BACKUP_SHA_OK files=629",
"539M\t/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/backups/20261022T134400Z-warm-pre-ee8c2cd",
"=== 待上线制品 ===",
"00ead2cfcb967441058d0413850467ba710738b6a9510d4f983894f00bc0b3de /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/stage/rdm-ee8c2cd-20261022T055052Z-637762/bin/agent-system",
"app.css",
"app.js",
"index.html",
"htdocs_files=6",
"=== 知识库供给 ===",
"active",
"Ver Cluster Port Status Owner Data directory Log file",
"16 main 5436 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log",
"LISTEN 0 200 127.0.0.1:5436 0.0.0.0:* users:((\"postgres\",pid=1462333,fd=6)) ",
"SET",
"regulations=2897",
"legal_nodes=1701178",
"index_entries=191557",
"retrieval_chunks=64076",
"/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/serve.env 600 ubuntu",
"[Service]",
"# 知识库供给环境(KBREG_*,来自受保护的 serve.env,权限 600)",
"EnvironmentFile=/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/serve.env",
"EnvironmentFiles=/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/serve.env (ignore_errors=no)"
],
"output_sha256": "b9d015bb32fed01af4e0490ca20745495bfa63fed1c4495d3dab9b3bc8d64682",
"receipt_sha256": "80f27d8775a7454eecc9279266ffdb8486c1fc9056770bd305f20fc5810b35fe",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/80f27d8775a7454eecc9279266ffdb8486c1fc9056770bd305f20fc5810b35fe.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T15:23:24.616557+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/rehearsal.sh'",
"exit_code": 1,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"after_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "c3fc389b9f7060ee590439a7a26bffc45f454be8",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "a4d29aaed46c125f0ea41b93903130a71287e2a36a10e457a5d6f54f94f05f28",
"after_tracked_diff_sha256": "a4d29aaed46c125f0ea41b93903130a71287e2a36a10e457a5d6f54f94f05f28",
"output_tail": [
"[rehearsal] copying data 2026-10-22T23:23:26+08:00",
"367M\t/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data",
"[rehearsal] pid=1475116 start=2026-10-22T23:23:27+08:00",
"[rehearsal] healthy=1 after=1.034965419s http=200",
"[rehearsal] schema:",
"agent-system schema_max = 51",
" users = 19",
" conversations = 18",
" runs = 109",
" messages = 244",
"Traceback (most recent call last):",
" File \"<stdin>\", line 9, in <module>",
"sqlite3.OperationalError: no such column: username"
],
"output_sha256": "633a16b70f361c207045e23bc573e0e8f9bd39eae69c648f1d4a4ee64c2e31ee",
"receipt_sha256": "86751433905579872d89a49231a54af8bf53abae1ba91b93c7c5e96b96e9ffa5",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/86751433905579872d89a49231a54af8bf53abae1ba91b93c7c5e96b96e9ffa5.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T15:23:34.788033+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/rehearsal.sh'",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"after_head": "c8d22d6ddb6e81b082c8c4eef513417000fd172c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "c3fc389b9f7060ee590439a7a26bffc45f454be8",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "ea88c0fc9ecb0d0c01399696327bda7c3c2f3eefd9c4249afc3c39ab53286428",
"after_tracked_diff_sha256": "ea88c0fc9ecb0d0c01399696327bda7c3c2f3eefd9c4249afc3c39ab53286428",
"output_tail": [
"[rehearsal] copying data 2026-10-22T23:23:36+08:00",
"367M\t/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data",
"[rehearsal] pid=1475273 start=2026-10-22T23:23:37+08:00",
"[rehearsal] healthy=1 after=1.016955899s http=200",
"[rehearsal] schema:",
"agent-system schema_max = 51",
" users = 19",
" conversations = 18",
" runs = 109",
" messages = 244",
" super_admins = [('rehearsal-admin',)]",
"[rehearsal] data tree files: 613",
"[rehearsal] log grep (error/panic/fatal):",
"[rehearsal] log tail:",
"2026/10/22 23:23:37 super admin: established account \"rehearsal-admin\"",
"2026/10/22 23:23:37 storage tier: auto (dialect=sqlite pool=0/0 idleSec=0 lifetimeSec=0 schema=v51)",
"2026/10/22 23:23:37 exec tools: backend=sandbox (bwrap: no network, ro root, session-only writes, proc limit)",
"2026/10/22 23:23:37 skills: wrote 0, upgraded 4 into /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data/skills",
"2026/10/22 23:23:37 auth: API key required (create with: agent-system apikey create --owner <name>)",
"2026/10/22 23:23:37 agent-system serving on http://127.0.0.1:8789 (data: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data, plugins: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/plugins)",
"[rehearsal] done 2026-10-22T23:23:38+08:00"
],
"output_sha256": "5e6268752d60e253de77c8979bfcfcac69ca8df0bd357dd3c2f1630d1965c7c0",
"receipt_sha256": "37a1f94a8b71c31ff766ac284ea2599dac2df25c12e065b33d2a91c7602e9632",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/37a1f94a8b71c31ff766ac284ea2599dac2df25c12e065b33d2a91c7602e9632.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T23:08:23.850986+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/rehearsal-superadmin.sh'",
"exit_code": 4,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "68ca3f5fb1b29ea18a70a30230ef9a2930241b1c",
"after_head": "68ca3f5fb1b29ea18a70a30230ef9a2930241b1c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"output_tail": [
"[rehearsal] copying data 2026-10-23T07:08:25+08:00",
"seed failed: no such column: is_super_admin"
],
"output_sha256": "6df9fb628078ad393282ce681f1fb69e6034f689237fe97e6a692da490a3f3ab",
"receipt_sha256": "4000943bcb84808a35cd147886eea4d6fd81a2982a9d8db37e6239d874d7789c",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/4000943bcb84808a35cd147886eea4d6fd81a2982a9d8db37e6239d874d7789c.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T23:08:52.895328+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/rehearsal-superadmin.sh'",
"exit_code": 1,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "68ca3f5fb1b29ea18a70a30230ef9a2930241b1c",
"after_head": "68ca3f5fb1b29ea18a70a30230ef9a2930241b1c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "04b1dba8da2af5a53de394e5e62fdf4a46ab168aa73346127d42bd67546e5c20",
"after_tracked_diff_sha256": "04b1dba8da2af5a53de394e5e62fdf4a46ab168aa73346127d42bd67546e5c20",
"output_tail": [
"[rehearsal] copying data 2026-10-23T07:08:54+08:00",
"[phase A] migrate-only start (auth off)",
"[phase A] healthy after .529100167s (pid=1593264)",
"2026/10/23 07:08:54 storage tier: auto (dialect=sqlite pool=0/0 idleSec=0 lifetimeSec=0 schema=v51)",
"2026/10/23 07:08:55 agent-system serving on http://127.0.0.1:8789 (data: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data, plugins: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/plugins)",
"[phase A] schema_max = 51",
"[phase B] elevate existing account",
"Traceback (most recent call last):",
" File \"/home/ubuntu/ecs-ops/seed-superadmin.py\", line 116, in <module>",
" sys.exit(main())",
" File \"/home/ubuntu/ecs-ops/seed-superadmin.py\", line 85, in main",
" instance = db.execute(\"SELECT instance_id FROM agent_system_identity\").fetchone()[0] or \"\"",
"TypeError: 'NoneType' object is not subscriptable"
],
"output_sha256": "bbeabf52756b45a9e3fc87670b7bdbbdcab5b5b35ab5682138d45462939306b5",
"receipt_sha256": "a88ff605df0338fb6cb529f9ab45900833c0120a528ee835b0b11628f9e66b50",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/a88ff605df0338fb6cb529f9ab45900833c0120a528ee835b0b11628f9e66b50.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-22T23:09:02.539134+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/rehearsal-superadmin.sh'",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "68ca3f5fb1b29ea18a70a30230ef9a2930241b1c",
"after_head": "68ca3f5fb1b29ea18a70a30230ef9a2930241b1c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "39b40acc28963d9413e554781ec8e65ed09774268e49cecb19496ac456e9a4d3",
"after_tracked_diff_sha256": "39b40acc28963d9413e554781ec8e65ed09774268e49cecb19496ac456e9a4d3",
"output_tail": [
"[rehearsal] copying data 2026-10-23T07:09:03+08:00",
"[phase A] migrate-only start (auth off)",
"[phase A] healthy after .267909096s (pid=1593451)",
"2026/10/23 07:09:04 storage tier: auto (dialect=sqlite pool=0/0 idleSec=0 lifetimeSec=0 schema=v51)",
"2026/10/23 07:09:04 agent-system serving on http://127.0.0.1:8789 (data: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data, plugins: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/plugins)",
"[phase A] schema_max = 51",
"[phase B] elevate existing account",
"seeded: {'owner': 'sundehui', 'is_admin': 1, 'is_super_admin': 1, 'must_change_password': <redacted>, 'auth_version': 2, 'credential_origin': '<redacted>', 'initialization_state': 'initialized', 'has_pw': 1} system_grants_allowed: 12",
"[phase C] start with auth on, super admin owner=sundehui",
"[phase C] healthy=1 after=.268576176s http=200",
"2026/10/23 07:09:06 super admin: account \"sundehui\" already established",
"2026/10/23 07:09:06 storage tier: auto (dialect=sqlite pool=0/0 idleSec=0 lifetimeSec=0 schema=v51)",
"2026/10/23 07:09:06 agent-system serving on http://127.0.0.1:8789 (data: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rehearsal/data, plugins: /home/ubuntu/weichen/Agent-System-Dev/runtime/dev/plugins)",
"[check] login as sundehui",
"{\"auth\":true,\"isAdmin\":true,\"isSuperAdmin\":true,\"mustChangePassword\":<redacted>,\"name\":\"\",\"owner\":\"sundehui\",\"passwordChangeMode\":\"<redacted>\",\"passwordSet\":<redacted>,\"sessionScope\":\"set_password_only\"}",
"",
"HTTP=200",
"[check] profile with session",
"forbidden: set-password session only",
"",
"HTTP=403",
"[check] super-admin gated endpoint",
"admin_users_http=403",
"[check] schema and account state",
"schema_max = 51",
"super admins = [('sundehui',)]",
"users total = 18",
"[check] log errors:",
"[rehearsal] done 2026-10-23T07:09:06+08:00"
],
"output_sha256": "690a87c97b30c36d517a47c90a6d951c307a094feefbf525ac6e7124c2f5c421",
"receipt_sha256": "47666f79ea5cbd315d5de3f89da4f90e19956fba77e4f880cb3f9bfca7c9df95",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/47666f79ea5cbd315d5de3f89da4f90e19956fba77e4f880cb3f9bfca7c9df95.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-23T00:44:11.563406+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/verify-post-cutover.sh'",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "f9a953831b63d78faf4e123068db2e5c68397db8",
"after_head": "f9a953831b63d78faf4e123068db2e5c68397db8",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"output_tail": [
"grep: SHA256SUMS: No such file or directory",
"COLD_META_SHA_SKIPPED(部分行需整体校验)",
"479M\t/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/backups/20261023T004322Z-cold-pre-ee8c2cd",
"=== 回滚装备 ===",
"ROLLBACK_KIT_SHA_OK files=12",
"kit created 20261023T004302Z",
"source_binary=1f45f64077b8dffd25d80513e3aaf5f06c71c8a09248c21af22f9d33eab516b9",
"data_source=/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/backups/20261022T134400Z-warm-pre-ee8c2cd",
"=== 服务 ===",
"active",
"active",
"pid=1617793 started=Fri Oct 23 08:43:29 2026 exe_sha256=00ead2cfcb967441058d0413850467ba710738b6a9510d4f983894f00bc0b3de",
"pid=1617793",
"addr=0.0.0.0:8788",
"source=rdm@ee8c2cd",
"version=agent-system-rdm (devel)",
"started=2026-10-23T08:43:30+08:00",
"{\"ts\":\"2026-10-23T00:43:30Z\",\"event\":\"deploy\",\"env\":\"dev\",\"kind\":\"agent\",\"source\":\"rdm@ee8c2cd\",\"result\":\"ok\",\"pid\":\"1617793\",\"addr\":\"0.0.0.0:8788\",\"detail\":\"from=dev@a706e9a+rdm@4b2473bed112 to=rdm@ee8c2cd via systemd; backup=20261023T004322Z-cold-pre-ee8c2cd",
"=== HTTP ===",
"local=200 healthz=200 domain=200",
"=== 数据与账号 ===",
"schema_max = 51",
" users 18",
" conversations 18",
" runs 109",
" messages 244",
" super_admins = [('sundehui',)]",
" sundehui = ('sundehui', 1, 1, 1, 2, 'owner_initialization')",
"=== 逐行对账(热备基线 ↔ 切换后)===",
"files base=605 now=605 missing=0 changed=5 added=0",
"changed: ['skills/.builtin-manifest.json', 'skills/docx-author/SKILL.md', 'skills/house-style/SKILL.md', 'skills/house-style/tests/test_housestyle.py', 'skills/house-style/housestyle/build.py']",
"tables with missing/changed rows: {\"agent-system.db.sessions\": {\"base\": 18, \"now\": 18, \"missing\": 0, \"changed\": 2, \"added\": 0, \"changed_keys\": [\"21\", \"22\"]}, \"agent-system.db.users\": {\"base\": 18, \"now\": 18, \"missing\": 0, \"changed\": 1, \"added\": 0, \"changed_keys\": [\"12\"]}}",
"problems: []",
"=== 密钥一致性 ===",
"owner='' scope=system models.rerank -> secret_keys match=True",
"owner='' scope=system tools.tavily -> secret_keys match=True",
"settings_snapshots=11 mismatches=0",
"=== 知识库 ===",
"active",
"SET",
"legal_nodes=1701178",
"index_entries=191557",
"=== 切换后错误扫描 ===",
"Failed to parse timestamp: 2026-10-23T00:24:14",
"(以上为空即无错误)"
],
"output_sha256": "52fc124a0e4e6e890949349d93afc89bd2228f460e780e14555160002da29282",
"receipt_sha256": "af18be0166d5bfe3abed4f5a433cb9d57e4dfdfa6f1eb60b828bc33496318e3b",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/af18be0166d5bfe3abed4f5a433cb9d57e4dfdfa6f1eb60b828bc33496318e3b.json"
}
```
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-23T00:44:39.306471+00:00",
"command": "bash ~/ecs-ops/kb-check-fast.sh",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "f9a953831b63d78faf4e123068db2e5c68397db8",
"after_head": "f9a953831b63d78faf4e123068db2e5c68397db8",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "1c4f20b9e2398f9fb813dfe305e789f20a8abad1c41f18ed847517dd35981cad",
"after_tracked_diff_sha256": "1c4f20b9e2398f9fb813dfe305e789f20a8abad1c41f18ed847517dd35981cad",
"output_tail": [
" \"version\": \"fts-v1\"",
" }",
" ],",
" \"hits\": [",
" {",
" \"regulation_id\": \"reg_3939376174cc5e1eb146d10b\",",
" \"edition_id\": \"ed_2e7d6d945848614ab386bdbc\",",
" \"node_id\": \"nd_79ccfca59982317806810fb3\",",
" \"kind\": \"guidance\",",
" \"title\": \"国家药监局关于发布医疗器械注册质量管理体系核查指南的通告(2022年第50号\",",
" \"edition_label\": \"src-5f55893f9096\",",
"KB_FAST_CHECK_OK"
],
"output_sha256": "cc655d17a77eb65ad064cfaaf4a903a3fe6adb05879dc8e2a70b16be36f93a09",
"receipt_sha256": "eda1eaba245f1c76e329747c66a6739435ddf8524ce0223fc86baac4b9bc92fd",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/eda1eaba245f1c76e329747c66a6739435ddf8524ce0223fc86baac4b9bc92fd.json"
}
```
| 同步(记录前对象) | 6ced938a126981ee2a4fab671a2da94f49cb56c5 | d436d6718ca997e575ad8be3d149efefca8cedd8 | 2026-10-23T00:46:40Z |
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
```json
{
"summary_schema": 1,
"source": "SELF",
"timestamp": "2026-10-23T00:49:39.764923+00:00",
"command": "ssh -o BatchMode=yes imc-tx-ecs 'bash ~/ecs-ops/verify-rollback.sh'",
"exit_code": 0,
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-ecs-deploy-dev8788-and-kb",
"before_head": "2487b561bb61103bd066d96f4be0135621daf66c",
"after_head": "2487b561bb61103bd066d96f4be0135621daf66c",
"branch": "feature/ecs-deploy-dev8788-and-kb",
"target_branch": "main",
"target_head": "6ced938a126981ee2a4fab671a2da94f49cb56c5",
"root_target": "main",
"immediate_target": "main",
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"output_tail": [
"=== 回退后运行态 ===",
"active",
"active",
"pid=1619863 exe_sha256=1f45f64077b8dffd25d80513e3aaf5f06c71c8a09248c21af22f9d33eab516b9 started=Fri Oct 23 08:48:35 2026",
"local=200 healthz=200 domain=200",
"=== 数据核对 ===",
"schema_max 35",
" users 18",
" conversations 19",
" runs 110",
" messages 246",
"rdm schema_version [(6,)]",
"files base=605 now=609 missing=0 changed=0 added=4",
"problems: []",
"tables missing/changed: {\"agent-system.db.sessions\": {\"base\": 18, \"now\": 19, \"missing\": 0, \"changed\": 2, \"added\": 1, \"changed_keys\": [\"21\", \"22\"]}}",
"=== 回退装备 ===",
"kit=/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/rollback/kit-20261023T004302Z",
"ROLLBACK_KIT_SHA_OK",
"/home/ubuntu/weichen/Agent-System-Dev/runtime/dev/data.rolled-back",
"=== 知识库仍在(旧二进制不读取,环境文件已移除)===",
"active",
"SET",
"legal_nodes=1701178",
"index_entries=191557"
],
"output_sha256": "d8f3dd94b561810a63a8515bc6cd1188a7f83e379a592c2106a88233258dbe51",
"receipt_sha256": "8c4cc241b55f8df83e9e6802d6bf15584553cfafe845bda647a2a8add611041c",
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/8c4cc241b55f8df83e9e6802d6bf15584553cfafe845bda647a2a8add611041c.json"
}
```
| 同步(记录前对象) | 0650e81133b853f27e8e53f516de8f6c1e73e4a4 | 1a3f9a1e5f08d25cedc82024ba1354b186932777 | 2026-10-23T11:13:15Z |
@@ -1,75 +0,0 @@
# 独立代码 Review:feature/intent-multi-turn-a1(A1 多轮意图闭环)
- Reviewer:独立 CLI 会话(pi,未参与本分支实施)
- 日期:2026-10-22(UTC)
- 对象:`git diff 955a32de...9126b09b`(Dev 超级仓);组件焦点 `modules/backend a75783fd..9ee752c3`
- 任务卡:`.task/feature-intent-multi-turn-a1.md`(设计决定:run-cancel 为第一业务动作、30min TTL、revision、cmdbus 确认不变)
- 性质:findings-only,未改任何代码;只新增本报告文件。
## 1. 范围与方法
- 超级仓 diff:`policy/architecture-baseline.json` +3 边、`modules/backend` gitlink a75783fd→9ee752c3、任务卡新增。
- backend 组件 diff(3 commits,+3267/−21):新包 `internal/intentsession`、`internal/store/intent_session.go`(表 v52)、`internal/route`(Cancel 裁决)、`internal/server`(`intent_session.go`、`intent_cancel.go`、`server.go` chat 接线)、`cmd/storage-migrate`(copyOrder 补 intent_session)。
- 方法:全量逐文件静态精读 + 调用链追踪(chat postMessage → fuseIntentSession → route.Decide → admitIntentCommand → Bus.Dispatch / handleIntentCancel → dispatchCancel;runmgr HITL 转轨判定的 route 契约),测试断言逐条对照审查镜头。
### 测试执行(诚实出口:NOT_RUN)
- 本会话**未能复跑**聚焦测试(`go test ./internal/intentsession/... ./internal/route/... ./internal/server/...`):当前工作区组件 checkout 只部分物化——`modules/backend/deps/agent-system-core、error-code-system、logging-system、agent-document-renderer` 为空目录,go.mod replace 闭包无法解析;无网络;本机模块缓存仅有 2026-10-13 伪版本(≠本次 pin 的提交),用它们编译出的结果不能作为本 PR 的证据,故弃用。
- 尝试过程可复核:为诊断曾临时在 `modules/backend/deps/` 建指向超级仓 `deps/` 的符号链接并随即删除(超级仓 deps 亦缺上述四个组件,`go build` 报缺 go.mod 为证);现场已恢复,`git status --porcelain` 干净(见 §5)。
- 任务卡记录的 verify-a1b/a1c exit=0、批二 CI 全绿:本会话无法重放,按 `[未经证实]` 处理;下述结论全部来自静态审查。按任务单要求未跑 `make test`。
## 2. 按镜头的结论(spec / security / regressions / tests)
**spec(多轮闭环)**:缺槽动作 → 澄清(`suspendIntentSession`,intent_session.go:273-310)→ 回答折叠(`fuseIntentSession` → `ApplyAnswer`,slot 齐则合成与完整识别同形的 MatchResult 走同一准入/派发路径,server.go:1252-1258 + intent_session.go:110-155)→ 完成;TTL 30min、写时续期、读侧惰性删除(intentsession.go:12-13、344-357);revision 单调、`ErrStale` 拒绝陈旧回答(intentsession.go:206-212、359-364);双用户/项目/会话键隔离(store 全查询带 owner 过滤)。主链路成立,测试覆盖到每一环。**例外见 B1/M1**。
**security**:会话不携带授权——补齐后仍走 `admitIntentCommand` + `Bus.Dispatch`,高危确认卡由 cmdbus 收口(TestIntentSessionStillRequiresHighRiskConfirmation 断言确认前处理器不执行);取消经 `run.cancel` 注册命令走同款 Dispatch,卡确认前不记「已取消」(intent_cancel.go:150-186,TestIntentCancelSurfacesHighDangerConfirmCard 覆盖确认→执行全链);跨用户运行 `GetRun(owner,…)` 不可见即澄清、不泄露存在性(intent_cancel.go:73-79,TestIntentCancelNeverResolvesAnotherOwnerRun);候选只列本人本会话未终态运行(intent_cancel.go:97-112)。**例外见 M2**。
**regressions**:无挂起预期时 fuse 段空转,`TestIntentSessionLegacyResponsesUnchanged` 做逐字节对照;route 契约——cancel 移出 unconsumedKinds 但 `StartFound` 刻意不置位(route.go:181-188),runmgr HITL 转轨判定(runmgr.go:2331-2334 只看 StartFound)行为与 main 一致;U6 准入——fused 结果同样过 `dispatchCommand`(res.Complete())与 `admitIntentCommand`(声明 schema 映射、必填不满足即 Blocked)两道闸,非法/缺失槽位不可能执行。**例外见 M1(累计槽位跨任务泄漏)。**
**tests**:覆盖面好——触发条件(有/无挂起、同意图/异意图、过期、隔离)、revision、高危确认、legacy 逐字节、显式/会话/歧义靶子、确认卡。缺口:无「同会话内换任务(异意图)再挂起」用例(正是 B1 场景);无真实并发写测试(revision 逻辑覆盖属逻辑等价,可接受);`answerFromRecognition` 显式落槽不看意图归属(intent_session.go:203)无专项用例。
## 3. Findings
### Blocker
**B1(spec/正确性)换任务再挂起时,上一个动作的累计槽位被继承,可把陈旧值派发给新动作。**
`Service.Suspend` 无条件合并旧行槽位:`slots = mergeSlots(cur.Slots, slots)`(internal/intentsession/intentsession.go:193-194)。宿主侧 `suspendIntentSession` 对「route 层槽位未就绪」的新动作一律走 Suspend(internal/server/intent_session.go:273-310),不区分新旧预期是否同一动作。可达序列(同会话、TTL 内):
1. T1 意图 A(如 `demo.run-cancel`)缺 reason → 挂起,累计 `{run_id: R-7}`;
2. T2 用户改做意图 B(如 `demo.purge`,缺 scope)→ fuse 判「异意图、无落槽」不碰会话(intent_session.go:124-127)→ route 澄清 → `Suspend(B)` 继承 `{run_id: R-7}`;
3. T3 用户补 scope → `ApplyAnswer` 合并全量槽位判齐(intentsession.go:227)→ `completedRecognition` 以累计槽位派发 B(intent_session.go:245-262);
4. `mapSchemaParams` 只丢弃**未声明**键,B 声明过的 `run_id` 会带陈旧值 R-7 进 params(internal/server/intent_admission.go:69-84)——若 run_id 是 B 的必填,准入反而被陈旧值满足。
这与包自己的不变量直接冲突(「绝不允许旧槽位漏进新任务」intentsession.go:25;「别的任务的输入绝不混进当前动作」intent_session.go:53、188)——该保护只做在了 fuse 的回答投影上(answerFromRecognition 异意图不并入,intent_session.go:180-208),Suspend 覆写预期时漏了。建议:Suspend 时若 `cur.Expectation` 存在且 `IntentID/Action` 与新预期不同,则丢弃旧累计槽位(或显式要求宿主先 Drop);并补「异意图换任务」回归用例。涉同一镜头的次生风险见 minor m4。
### Major
**M2(security/错误路径)`applyCancel` 吞掉存储错误后,可能把「放弃补槽」滑成「立即取消一个真实运行」。**
同意图取消的设计语义是只清挂起预期、不派发(intent_session.go:20-24)。但 `applyCancel` 对非 ErrStale 的存储错误同样返回 nil 且错误被丢弃(internal/server/intent_session.go:165-170),`fuseIntentSession` 整体返回无会话语义 → postMessage 继续走 `route.KindCancel` 规划(server.go:1273-1279)。此时挂起行的 `run_id` 槽位经 `sessionRunTarget`→`Binding` 仍解出靶子(intent_session.go:313-327),`planIntentCancel` 对发起人可见即放行;若部署未注册 `run.cancel`,走 `dispatchCancel` 本地路径**立即 `CancelRun`、无任何确认**(intent_cancel.go:164-176)。即:一次瞬时存储故障把「算了不取消」变成「未确认取消 R-7」。建议:applyCancel 对 `err != nil`(非 ErrStale)向上传播错误(writeStoreErr),fail-closed。
### Minor
- **m1(文档失实)** `internal/store/intent_session.go:11-12` 声称该表「不进 storage-migrate 的 copyOrder(同 admission_leases)」,但 `cmd/storage-migrate/main.go:311-313` 已把 `intent_session` 加入 copyOrder(且是清单夹具测试 `main_test.go:218` 的强制要求,9ee752c 补得对)。代码正确、注释过期,需同步删改;旧源库缺表已有 `sqliteHasTable` 跳过(main.go:400-402),无行为问题。
- **m2(回话如实性)** 总线路径派发被拒(StateRejected)时 `cancel.accepted=false` 但 `cancel.reason` 为空(internal/server/intent_cancel.go:142),与本地路径「失败带原因」不对称;receipt 虽随响应透出,建议把拒绝原因写入 `result.Reason`。
- **m3(过期边界 fail-open)** `expires_at` 解析失败得零值时 `live()` 视为「永不过期」(intentsession.go:366-373 + store `parseTime` 容错返回零值)。当前写入方恒写 TTL,风险仅来自自身写坏的数据,但与全文件 fail-closed 纪律不一致:解析失败宜按已过期处理。
- **m4(与 B1 联动)** 显式 `set_slot/change_slot` 命令「不看意图归属」直接写入当前挂起预期(intent_session.go:196-203,注释称有意)。B1 修复(换任务即弃旧槽位)落地后,此路径仍可把异动作的落槽命令并入当前预期;建议改为「命令名 ∈ 当前预期 Missing/声明键才采纳」,或补用例锁定现状语义。
### Notes
- **n1** 「放弃补槽」轮仍按 KindTurn 起普通回合(server.go:1273-1279 不改 routeKind;1340-1345 仅 KindCancel 拦截),响应同时带 `intent_session` note 与 run——与 main 上 cancel 落 turn 的旧行为一致,测试未固定该组合,建议在任务卡确认一次。
- **n2** `completedRecognition` 不搬运 `Risk/RequiresConfirm`(intent_session.go:245-262):全仓 grep 确认 `Decision.RequiresConfirm` 目前无消费者(route.go:114-116 仅透传),确认权在 cmdbus danger,今日无行为影响;将来若有人消费该字段,fused 路径会静默不同——建议留注释或补字段。
- **n3** `PutIntentSession` upsert 后读回非原子(store/intent_session.go:74-93),并发双写可能都读到同一新 revision;当前会话请求路径单写者,无实害,建议加注释固化「每会话单写者」前提。
- **n4** 未注册 `run.cancel` 的本地回落取消无确认卡——与既有 `POST /api/runs/{id}/cancel` 逐字同款、属设计决定(任务卡决定 1/4 与 intent_cancel.go:15-17 注释一致),非本 PR 引入;注册了命令的部署确认链有测试护栏。记录在案以回应「确认是否被绕过」镜头:**会话路径在命令注册时不绕过确认**。
- **n5** 架构基线 3 条新边与实际 import 逐一核对相符(intentsession→intentgate/store;server→intentsession);基线治理测试本会话未复跑(同 §1 NOT_RUN 原因)。
- **n6** 任务卡记载的 svc governance 偶发失败为已知预存问题、按任务单不属本次范围,未复跑 `make test`。
- **n7** 测试缺口清单:异意图换任务再挂起(B1 回归用例)、applyCancel 存储错误注入、显式落槽跨意图(m4)。
## 4. 结论
主链路(挂起→澄清→补齐→同路派发)、过期隔离、revision、双用户隔离、取消三态(显式/会话绑定/歧义→如实澄清)与 cmdbus 确认收口均实现且有测试护栏,route/runmgr 契约保持。但以下问题应先修再合:
- Blocker B1:跨动作挂起继承旧累计槽位,陈旧值可进新动作派发参数(intentsession.go:193-194;admission 只滤未声明键)。
- Major M2:applyCancel 吞存储错误 fail-open,可滑向无确认的本地取消(intent_session.go:165-170 → server.go:1273 → intent_cancel.go:164-176)。
- Minor m1–m4 建议随修复一并处理(m1 文档、m2 回话、m3 过期 fail-closed、m4 落槽归属)。
最终判定(理由如上三条):
REVIEW VERDICT: BLOCK
@@ -1,44 +0,0 @@
# Independent Code Review — feature/exec-recovery-diagnostics-a2 (A2 run diagnostics)
- Reviewer: independent CLI review session (`a2-independent-review` unit; fresh session, not the implementer)
- Date: 2026-10-22
- Review object: this checkout @ `d0b2bde9` (branch `feature/exec-recovery-diagnostics-a2`) versus authoritative main. No `origin/*` refs exist in this sandbox; task card R0 = `c9244328` (= `d0b2bde9~5`, the merged main tip `c92443280ef8e9ed29bf5bf5250df650c01b4478`), so `git diff c9244328...d0b2bde9` ≡ `git diff origin/main...HEAD`.
- Dev-level scope: task card `.task/feature-exec-recovery-diagnostics-a2.md` (new), `modules/backend` gitlink `9ee752c3` → `06b0c9f1`, `policy/architecture-baseline.json` +3 edges. Verified: dev diff is exactly these 3 files; the three edges (`backend/rundiag backend/cmdbus`, `backend/rundiag backend/store`, `backend/server backend/rundiag`) sit in correct sorted positions and match the actual import graph (see N5).
- Backend scope: `git -C modules/backend diff 9ee752c..06b0c9f` — 7 files, +1418/−0: `internal/rundiag/rundiag.go` (488), `internal/rundiag/rundiag_test.go` (440), `internal/server/run_diagnostics.go` (32), `internal/server/run_diagnostics_test.go` (226), `internal/server/server.go` (+2), `internal/store/run_diagnostics.go` (110), `internal/store/run_diagnostics_test.go` (120).
- Method: read-only review of the full diff plus the neighboring run-read/cancel routes, the pre-existing store methods the aggregator reuses, and the cmdbus pending-confirm write path. No code changed; the only write is this report.
## Findings
### Blockers
None.
### Major
None.
### Minor
- **MI1 — Evidence-driven wait states outrank terminal run statuses; a completed/failed run can be reported as `waiting_hitl`.** In the responsibility switch the HITL arm `case ev.Run.Status == core.RunWaitingHITL || card != nil || decision != nil` (`modules/backend/internal/rundiag/rundiag.go:252`) is evaluated before `failed` (:281) and `completed` (:296). A run that ended while a decision card scoped only by owner+conversation (human_decisions has no run column; attributed by the time-window inference at :432-448) remained `pending`, or with a leftover unresolved run-scoped HITL card, yields `state=waiting_hitl`, `role=human`, `next_step.action=confirm_card` for a terminal run. This is exactly the fixed precedence the task card approved and the documented bounded attribution (`rundiag.go:15-19`), and `run.status` stays visible in the report — but a follow-up that suppresses evidence-driven wait mapping (or adds a note) when the run is terminal would remove the confusing "confirm card" advice on finished runs. Not blocking: matches the approved design; purely advisory output.
- **MI2 — Silent truncation of side-effect evidence contradicts the package's own honesty contract.** `sideEffects` stops appending at `maxSideEffects = 50` with no note (`rundiag.go:342-366`, cap at :57), and `Gather` fetches only the 200 newest audit envelopes (`maxAuditScan`, `rundiag.go:175` → `ListAudits` orders `time DESC`, `internal/store/audit.go:147-152`), so a busy conversation can under-report a run's operations without any indication. Every other evidence gap in the package adds a note (even "absence of records is not proof of no effect", :313-315); a "side effects truncated" note would complete that contract. Diagnostic-only impact; recorded evidence is still correct as far as it goes.
- **MI3 — Precedence is tested only at the top of the chain.** Of the ordered arms at `rundiag.go:224-310`, only "unreleased recovery hold wins over everything" (`rundiag_test.go:81-89`) exercises interleaved evidence; combinations such as released-hold+interrupted+open-HITL-card (→ `reconciling`) or decision+pending-confirm (→ `waiting_hitl`) are untested and rest on the switch order alone. Low risk because the order is literal and table-covered per state; suggesting coverage only because precedence is the stated spec center of this unit.
### Notes
- **N1 — Per-state derivation and precedence are complete and match the task-card contract.** All eight states are derivable: `unknown` fail-closed on unreadable hold (:226-233), `recovery_hold` on an open hold row (:234-240), `reconciling` for released-hold+`RunInterrupted` residue (:241-251, predicate at :402-403), `waiting_hitl` from status/run-scoped card/time-windowed decision (:252-273), `confirmation_pending` from an owner-scoped pending confirm parsed out of the dispatch payload (:274-280, :453-471), `failed` with retry-safety gated on unknown receipts (:281-295), `completed` (:296-298), `running` for running+pending (:299-303), `unknown` default for unmapped statuses (cancelled; interrupted without hold; :304-310). Precedence order is exactly `recovery_hold > reconciling > waiting_hitl > confirmation_pending > failed > completed > running > unknown`, with the fail-closed `HoldUnknown` degradation deliberately first.
- **N2 — Security: scoping is byte-for-byte the neighboring run-read pattern; no cross-owner leakage path exists.** The handler resolves the run via `s.st.GetRun(s.owner(r), r.PathValue("id"))` (`internal/server/run_diagnostics.go:25`) — identical to `getRun` (`server.go:1592`) — where `owner` comes only from the auth context (`auth.go:220-225`), `GetRun` filters `WHERE id=? AND owner=?` with cross-tenant = `ErrNotFound` (`messages.go:307-316`), and `writeStoreErr` maps it to 404 indistinguishable from a missing run (`server.go:2617-2620`). Every follow-on query is keyed on the already-authorized run's own fields, never on request parameters: `GetRecoveryHold(run.ID)` (`recovery_holds.go:36`), `ActiveHITLCardsByRun(run.ID)` (`recovery.go:156`), `PendingHumanDecisionsByConversation(run.Owner, run.ConversationID)` with empty-owner defense (`store/run_diagnostics.go:51-53`), `ListOwnerPendingConfirms(run.Owner)` (:88-90), `LastRunCheckpoint(run.ConversationID, run.ID)` (:28-30), `ListAudits(run.Owner, run.ConversationID)` (`audit.go:134-140`). The `pending_confirms` payload is parsed only to extract command/owner/conversation identity; the raw payload is never echoed (`rundiag.go:453-471`).
- **N3 — Read-only holds end to end.** All six store entry points the aggregator uses are SELECT-only (verified by reading each); `Build` is a pure function; the handler path contains no store write and the mux registers only the GET pattern (`server.go:396`). The author's `TestRunDiagnosticsIsReadOnly` additionally asserts the run row and hold are unchanged after a GET and that POST/PUT/DELETE/PATCH are not served (`run_diagnostics_test.go:153-193`).
- **N4 — No route-registration conflicts.** `GET /api/runs/{id}/diagnostics` differs from `GET /api/runs/{id}` (:394) and `POST /api/runs/{id}/cancel` (:393) by a literal extra segment / method; ServeMux precedence is specificity-based, single registration, no duplicate pattern anywhere in the diff.
- **N5 — Architecture-baseline edges are correct and validated.** `internal/rundiag` imports exactly `cmdbus` and `store` within the backend module (`rundiag.go:21-24`); the `agent-command` import is intentionally untracked — the baseline records only intra-repo package edges and has no deps-module edges anywhere (same precedent as `backend/cmdbus`, which also imports agent-command without an edge). `backend/server backend/rundiag` matches the handler import. Sorted positions are correct (`route < rundiag`; `ratelimit < rundiag < runmgr`), and `scripts/lint-architecture.go` compares this file against the collected import graph, so the edges are machine-checked in CI.
- **N6 — Go tests NOT_RUN in this review workspace (environment, not code).** The backend's nested dependency checkouts (`modules/backend/deps/agent-command`, `deps/agent-system-core`, …) are empty gitlinks here and no `.git/modules` gitdirs exist; `git submodule init/update` is forbidden for this unit and network is off, so `go build ./internal/rundiag/ …` fails at module setup (`agent-command@v0.0.0 (replaced by ./deps/agent-command): reading deps/agent-command/go.mod: no such file or directory`), before any code compiles. Fallbacks applied: every changed file was fully line-read; all `core.Run*`/phase/event symbols referenced are used by pre-existing compiling backend code; `cmdbus.Request` carries `ID/Owner/ConversationID` (`cmdbus.go:163-177`) and the pending-confirm payload is the marshaled dispatch request (`cmdbus.go:522-523`), so `waitingConfirm`'s parsing is sound; the `events` table has `run_id`/`seq`/`ts`/`type` in both dialects (`migrate.go:47,71`) and the checkpoint query is served by `idx_events_conv(conversation_id, seq)`. Author-recorded receipts were reviewed and are internally consistent with the task card: `/tmp/agent-stack-b2/verify-a2b.json` (`go build ./... && go test -count=1 ./internal/server/... ./internal/rundiag/... ./internal/store/... && go vet …`, exit=0, `ok agent-system-backend/internal/{server,rundiag,store}`) and `/tmp/agent-stack-b2/verify-a2-combo2.json` (`make test`, exit=0) — author-claimed (SELF), not independently re-executed in this session.
- **N7 — Attribution boundaries are documented and bounded; one sibling-run overlap remains.** Audit envelopes carry no runId today (host bus doesn't write it), so `belongsToRun` (`rundiag.go:371-383`) attributes by conversation + "not issued before run start". Concurrent sibling runs (subagents) in the same conversation have overlapping windows, so until the bus persists `runId`, an envelope from a sibling can appear in both runs' `side_effects`. The limitation is stated in the package doc (:17-19) and the report surfaces `operation_id`/`command_id` so a human can disambiguate; when the bus starts writing `env.RunID` the strict branch (:372-373) takes over automatically. Acceptable for a diagnostic surface.
- **N8 — "No records ≠ no effect" honesty contract is consistently enforced** (unrecorded checkpoints → null + note at :311-312, tested at `rundiag_test.go:280-302`; empty side effects → note :313-315; unreadable hold → fail-closed unknown that can never yield `safe_retry` :226-233, tested :304-339). Only the truncation gap of MI2 falls short of it.
## Lens summaries
1. **Spec** — pass. State derivation and precedence are complete and per-state correct (N1); the JSON contract (snake_case, `last_checkpoint` explicit null, `side_effects` always an array, `next_step` never reason-less) is pinned by `TestReportJSONContract` (`rundiag_test.go:341-389`). Dev-level artifacts (task card, 3 baseline edges, gitlink `06b0c9f`) are exactly as claimed.
2. **Security** — pass. Auth-context-only owner, tenant-filtered run fetch, 404 on cross-owner/missing (tested `run_diagnostics_test.go:62-97`), all downstream reads anchored on the authorized run, strictly read-only (N2/N3), no payload echo, empty-owner fail-closed store helpers.
3. **Regressions** — pass. The diff is additive: 7 new files plus a 2-line route registration; no execution/recovery/runmgr path touched, no existing signature changed, no route conflicts (N4), no migration/schema/index change.
4. **Tests** — pass with minor gaps. Per-state table of 12 cases covering all eight states plus degradation (`rundiag_test.go:78-225`), scoping at both server and store layers including cross-owner/cross-conversation/empty-owner (`run_diagnostics_test.go:62-97`, `store/run_diagnostics_test.go:13-120`), unknown-degradation at three levels (unreadable source, unmapped status, fail-closed hold; `run_diagnostics_test.go:195-226`), read-only and write-method rejection (`:153-193`), pre-run evidence exclusion (`rundiag_test.go:391-406`). Gaps: MI3 (chain precedence below the top arm), MI2 (truncation without note). Focused tests could not be re-run here (N6); author receipts for `go test ./internal/rundiag/... ./internal/server/... ./internal/store/...` and full `make test` are on record and uncontradicted.
REVIEW VERDICT: APPROVE
@@ -1,48 +0,0 @@
# 独立代码 Review:feature/catalog-recall-eval-a3(A3 召回评测集与指标基线)
- Reviewer:独立 CLI session(未参与本单元实施;本报告为唯一产出变更)
- 日期:2026-10-22
- Review 对象:`git diff 6ced938a...HEAD`(6ced938a = R0 / origin-main 合入点,与任务卡 R0 一致)
- 组件重点:`deps/agent-intent` 765a7e7..6aa4925(单提交 "Add the sanitized recall evaluation set and metrics baseline",9 文件 +1354/-18);Dev 层 4 个 gitlink 前移 + 任务卡
- 方法:全量 diff 阅读被测文件;对语料/指标做独立的脚本化交叉复核(只读);静态追踪指标口径、生成回放路径与 JSON 兼容性;检查嵌套 pin 级联与越界改动。聚焦测试见下文 NOT_RUN 说明。
## 复核结论摘要(独立重算,非转抄)
- 语料 `intent/testdata/recall_corpus.json`:**37 例 / 46 轮**,五类齐全且恰好五类(expired_confirmation 7/14、generation_consistency 8/8、multi_intent 7/7、negation_reference 8/10、wrong_slot 7/7),`record_only: true`;`expect_top` 轮次 31、生成用例 8 —— 与 BASELINE.md 表头逐项一致。
- 语料 digest `sha256:53d26cfc…bc2885` 与 `evaluation/metrics.json` 的 `corpus_digest`、BASELINE.md 摘要三方一致(本机 `sha256sum` 独立重算)。
- `evaluation/metrics.json` 全部 10 组计数与比率逐项等于 BASELINE.md 表;比率均可用 `round(n/d*10000)/10000` 独立复算(如 30/31→0.9677、13/46→0.2826、6/46→0.1304、5/14→0.3571、5/8→0.625、5/7→0.7143、3/7→0.4286)。
- 总体口径自洽:top1_expected 3+3+7+11+7=31、hits 30(唯一 miss 即 multi_greet_then_report 的 1 次误执行)、mis_exec 5+1=6、clarify 0+0+5+5+3=13、fallback 2+0+0+3+0=5+1(gen)=6、pass 3+6+7+7+8=27。
- 嵌套 pin 级联一致:native@d1f72aa→intent@6aa4925;squad@a5afe9c→intent@6aa4925+native@d1f72aa;backend@8dc91c1→intent@6aa4925+native@d1f72aa+squad@a5afe9c(各自 `git ls-tree` 核对);三个组件 bump 均为 pins-only(`git diff --stat` 仅 gitlink),superrepo 层 diff 仅 4 gitlink + 任务卡,无评测范围外源码改动。
## Findings
### Blocker
无。
### Major
无。
### Minor
1. **基线数值漂移不设防(结构性盲区,刻意取舍但建议记录)** — `intent/recall_eval_test.go:119-162`(`checkSummaryJSON`/`checkMetricsBounds`):结构测试对已提交基线只校验 schema、digest、类别计数与「内部一致」(rate==ratio(n,d)),不把基线里的 hits/rates 与本次新鲜运行比对。若识别器行为改变而语料不变(digest 不变、计数不变),过期的基线数值不会被任何测试发现;BASELINE.md「重跑」一节只靠流程约束。这与「只记录不设阈值」的定位一致(比对数值即变相质量断言),属可辩护的设计取舍,但建议在 BASELINE.md 明示「引擎变更不会触发基线失效告警,须人工重跑」。
2. **missing-slots 断言用 `fmt.Sprint` 比较切片** — `intent/corpus.go:194-222`(`matchTurn` 中 `fmt.Sprint(got) != fmt.Sprint(turn.ExpectMissingSlots)`):`[]string(nil)` 与 `[]string{}` 都打印 `[]`,且比较对元素顺序敏感。当前引擎输出顺序确定、语料只断言单元素缺失(`["run_id"]`/`["topic"]`),不构成现实缺陷;但后续一旦断言多缺失槽位,顺序耦合会变成脆弱测试。建议改为逐元素显式比较。
3. **任务卡内部记录不一致(文档级)** — `.task/feature-catalog-recall-eval-a3.md`:「Agent 实施收据」表全部为 NOT_RUN/待定义 占位,而「合入与清理」节已记载 `a3-recall-eval-dataset(…intent 6aa4925,verify-a3 exit=0)`、嵌套级联与 `组合验证:全量 make test PASS`;且「证据(Evidence)」表为空,组合验证结论以带外文件名(verify-a3-combo3.json 等)引用。按仓库门禁「命令输出只经 make evidence 写入任务卡」,合入前应把收据表与证据表补齐一致。不阻塞本次代码评审结论。
### Notes
1. **指标口径正确** — `intent/evalmetrics.go:129-141`(`misExecuted`):仅当实际 `matched`/`expectation_hit` 出 Top 且与期望动作不同时计数;期望 matched/expectation_hit 而未写 expect_top 的轮次不约束动作,不计;期望 fallback/ambiguous/guard_hit 却执行了即计。与 BASELINE.md「指标口径」一节逐条一致。分母:top1=写了 expect_top 的轮次(`evalmetrics.go:103-108`)、clarify/fallback=全部轮次(`evalmetrics.go:111-118,153-159`)、gen_agree=写了 generator_commands 的用例数(用例级,`evalmetrics.go:120-124`)。`ratio` 定点 4 位舍入保证 JSON 字节稳定(`evalmetrics.go:161-166`)。
2. **汇总对账安全** — `intent/evalmetrics.go:61-92`(`SummarizeCorpus`):cases/results 按下标一一对应(越界即 break,契约已在注释声明),空 category 归入 `uncategorized`(旧语料兼容),类别名升序、overall 独立。`intent/eval_test.go:78-89` 在既有 `corpus.json`(无 category)上跑了一遍汇总,兼容路径有覆盖。
3. **生成回放路径离线、确定性、fail-closed** — `intent/corpus.go:156-186`:`replayGenerator.Generate` 忽略输入与目录、只回放脚本命令(无模型/无网络),并深拷贝 slots 防串染;`compareGeneratorPath` 复用识别器兜底同款 `generateCommands`(`intent/recognizer.go:352-385`:start 的 ActionRef 必须从同一快照唯一解析、命令种类受限、槽位键集校验,任一不过即无 Top)。三个「故意不一致」用例路径成立:`run_id=43` 槽位分歧(sameSlots 判否)、`taskrun.restart` 错动作(handler 反查到 restart spec)、`ghost.flow` 幻觉动作(resolveActionRef 失败→校验拒绝→无 Top)。两边都无 Top 也算一致,`gen_chitchat_agree` 覆盖该分支。Nil expectation 传入 `NewRecognizer` 在该路径安全(`generateCommands` 不触达链式预期)。
4. **eval 向后兼容** — `cmd/intentctl/main.go:254,260-262,365-390`:`-metrics-out` 缺省为空不写文件(唯一新增写文件入口,显式 opt-in;`-` 走 stdout);旧语料(无 `record_only`)失败语义不变,且 `TestEvalRecordOnlyWritesMetrics`(`cmd/intentctl/main_test.go:79-112`)同时断言 record_only 不失败+strict 语料仍失败。输出新增的指标表为追加行,不破坏既有两行指标输出。`evalCorpus` 结构仅增可选字段(`omitempty`),旧 JSON 解析不受影响。
5. **结构测试无质量阈值伪装** — `intent/recall_eval_test.go:64-117`:只断言 ≥30 例、恰好五类、每例可执行(无「识别错误」)、generation_consistency 用例形态、JSON 合法且计数与语料一致、基线 digest 与语料一致;`checkMetricsBounds`(:164-186)只做数学界校验(n≤d、rate==ratio)。无任何质量数值断言,符合「基线只记录」边界。测试与 CLI 同源建快照(:36-62,语料自声明 builtin_specs/guard/protected + 同款 specs 目录),指标同源声明成立。
6. **语料确为夹具派生、五类行为真实可辨** — 全部期望 top ∈ {demo.run.status_query/restart/cancel, demo.greet.hello, demo.misc.deploy, report.generate, guard.config_change},动作 id ∈ base.json 的 handler 集 + 刻意的幻觉 id;生成动作 id 与 `intent/testdata/specs/base.json` 的 handler/action 一一对应。各类用例确实 exercising 所标行为:语义否定(「不要/别/不用/先不要」无 excludes 声明,理想为 fallback)对照 `excludes_any` 声明否定(「配置」);multi_intent 覆盖 动作+动作→ambiguous、寒暄+动作→动作、动作+guard→guard_hit;wrong_slot 覆盖 缺失/中文数字/双候选/越字符集/仅符号/空主题;expired_confirmation 覆盖 链式命中、过期后裸回答回落、过期后完整重述走 matched、宿主声明 pending 再过期(`pending_expectation`+`expectation_expired` 组合,语义为「先声明后作废」,实现顺序 `intent/corpus.go:120-126` 与用例意图一致);wrong_slot 用单轮 `turns` 包装以使用本轮新增的 `expect_slots`/`expect_missing_slots` 能力。
7. **安全边界** — 语料全合成(无真实用户数据/PII);`record_only: true` 无阈值断言;生成路径为确定性回放,无网络/模型调用;未发现任何对外部输入的写路径(除显式 `-metrics-out`)。
8. **聚焦测试 NOT_RUN(环境受限,非变更缺陷)** — `cd deps/agent-intent && GOWORK=off go test ./intent/...` 无法编译:`agent-intent` 的嵌套 gitlink(deps/error-code-system@a3ee67b、logging-system@49b5d8b 等)在本工作区未物化,且目标提交不在任何本地对象库(`git cat-file` fatal)、无网络。按诚实出口如实标注 NOT_RUN;作为补偿,本文「复核结论摘要」的计数/digest/比率均为独立脚本重算,与基线三方对账一致。任务卡所记 `make test PASS(verify-a3-combo3.json exit=0)`本评审无法复现,标注 [未经证实](属 host 组合门禁范畴,不阻塞代码评审)。
## Verdict
未发现 blocker/major 级代码缺陷;三项 minor(基线漂移不设防的取舍说明、fmt.Sprint 切片比较的脆弱写法、任务卡收据/证据表未同步)均不阻塞合入,可在本次或后续增量处理。语料、指标、基线、CLI 兼容、结构守卫与嵌套 pin 级联均通过独立静态复核与数值对账。
REVIEW VERDICT: APPROVE
@@ -1,47 +0,0 @@
# 独立代码 Review:feature/recall-misexec-fixes(mis-exec 规则层修复)
- 日期:2026-10-22(评审执行于 2026-10-23 UTC 的分支对象上)
- 评审人:独立 CLI 会话(未参与实施;本文件为本次唯一变更)
- 评审对象:`git diff bccb88f2...HEAD`(= origin/main...HEAD;本 worktree 无 origin remote,bccb88f2 为 main 顶端合并提交,是 feature 分支的 merge-base)
- 组件范围:deps/agent-intent @6aa4925..7f3ff6a(单提交 "Make intent matching negation-aware and slots fail closed")+ gitlink 级联(native d1f72aa→be556f4、squad a5afe9c8→95b387d、backend 8dc91c17→8031855e→bd5c0f3)+ 任务卡 `.task/feature-recall-misexec-fixes.md`
- 方法:全文精读 9 个变更文件 + 逐条手工追踪语料用例 + 对否定判定函数做 stdlib-only 逐字提取探针验证(/tmp,脱离工作区);未修改任何代码。
## 验证(本工作区内的执行情况)
- `git status --porcelain`:评审开始时为空(clean);四个组件 checkout 均精确处于被 pin 的 SHA 且无脏文件(agent-intent 7f3ff6a、native be556f4、squad 95b387d、backend bd5c0f3)。
- `gofmt -l intent/ cmd/`:无输出(格式干净)。
- `go test`:**NOT_RUN(环境不可编译)**。agent-intent/go.mod 的 `replace error-code-system => ./deps/error-code-system` 所指嵌套子模块在本工作区任何层级都未物化(superrepo、agent-intent、backend 三处的 error-code-system/logging-system 目录均为空挂载点),且相应 commit 对象不存在于任何本地对象库;单元约束禁止 `git submodule init/update`、禁止网络。`GOWORK=off go test ./intent/...` 如预期在包 setup 阶段失败(reading deps/error-code-system/go.mod: no such file)。诚实出口:本报告的测试结论为读码+探针+追踪,非重放。
- 否定判定探针(negatedAt/inNonNegatingBie 逐字提取、21 组用例):全部 corpus 否定序列判否定;「不断」「无偿」不触发误否定;「不要马上重启」(间隔 2 字)判否定、「不要立刻马上重启」(间隔 3 字)不判(窗口语义);「特别的,重启任务」跨标点正确豁免;「任务别名重启」豁免成功;「判别任务进度」被误判否定(见 F1)。
## 评审发现
### Blocker
无。
### Major
无。
### Minor
- **F1(recall 损失,fail-safe 方向)**:「别」字白名单 `nonNegatingBie`(intent/registry.go:211-214)是封闭列表,「判别」「辨识」「作别」等含「别」的常用词不在列。探针实测「判别任务进度」→「进度」命中被 `negatedAt`(registry.go:263-275)判为否定 → status_query 以 `negated_match` 排除 → 回落 fallback。方向是安全的(少执行而非误执行),但对该输入是误回落。建议后续把「别」的否定判定改为右边界词表或按「别+动词」结构判断。
- **F2(残余否定词缺口,符合既定设计范围)**:token 表(registry.go:204)无「无须」「勿」「先不」「暂不」「不必」等变体,如「无须重启任务」仍会正向命中 restart。任务卡设计决定明确固定了这 7 个词,故不算实施偏差;作为残余风险记录:negation_reference 类的其他说法仍可能误执行。
### Notes
- **N1(tie-break 只认“有效”槽位)**:`resolveTie`(intent/recognizer.go:152-185)用 `len(slots)` 计数,invalid 捕获(如多候选值)不计。因此「你好,取消任务 42 和任务 43」这类输入中 cancel 的 run_id 无效 → filled=0,与寒暄同为 0 时按 priority(greet=10 > cancel=0)裁决到寒暄,动作侧的 clarify 丢失。仅在同分且优先级不同时可达,且寒暄无副作用、动作未启动,属可接受行为选择;若希望「带槽位信号」包含无效捕获信号,需另行设计。
- **N2(fail-closed 误伤面)**:`hasInnerSeparator`(slots.go:221)/`captureTruncated`(slots.go:201)对合法值也拦截——如主题「和平与发展」会因内部分隔词判多候选、值后紧跟 ASCII 词字符即判截断,一律 clarify 而非 start。与「无效一律 clarify」的设计一致,代价是 clarify 率上升(metrics 中 wrong_slot clarify 7/7=1.0),方向安全。
- **N3(测试夹具标签失真,表面问题)**:cmd/intentctl/main_test.go:82-85 把 record_only 的 miss 用例从「不用查进度了」换成「进度条的颜色真好看」是必要的(旧输入修复后正确回落,不再产生 miss 计数;新输入在 testSpecJSON 下确会命中 status_query,仍覆盖 miss 记账路径,断言 MisExecutions==1 成立),但 category 仍标 `negation_reference`,语义已不属否定类——建议下次触及时改名或换语料类别。
- **N4(验收证据等级)**:mis_exec=0 / 37/37 的验收来自 intent@7f3ff6a 内提交的 evaluation/metrics.json 与 BASELINE.md(作者证据,SELF 级)。本工作区无法重放(见上)。数字内部一致性已人工复核:各类 passed 7+8+7+8+7=37、clarify 5+0+5+0+7=17、fallback 3+1+0+7+0=11、top1 11+7+3+3+7=31/31、mis_exec 全 0,与 overall 行及 I2 段(TP=30/FP=0/FN=0,较基线同向 +1)全部吻合;gen_agree 5/8 未动(3 例 gen_agree 分歧按范围未处理,BASELINE.md 如实保留)。CI(`.gitea/workflows/ci.yaml` @ PR HEAD)是测试通过的权威源,合入门禁照常。
- **N5(单元包描述与实际 diff 的 pin 基线不符,仅描述性)**:包内写 squad b6d8763→95b387d、backend 330c1a3→bd5c0f3;实际 vs main 为 squad a5afe9c8→95b387d、backend 8dc91c17→8031855e→bd5c0f3(两个 backend tip 树完全相同,bd5c0f3 非 8031855e 的后代而是同树重提交)。终态与包目标一致,无内容影响。
- **N6(任务卡整理欠账)**:`.task/feature-recall-misexec-fixes.md` 的「Agent 实施收据」表仍是全 NOT_RUN/待定义,而「合入与清理」已记录实施收据;需求分析/阶段记录等模板段未填。卡片债务,非阻塞;Review 记录待本评审随 PR 流程回填。
- **N7(正面确认,范围)**:变更严格限于三个修复+pin 级联+任务卡:intent 单提交共 9 文件(boundary.go 为纯重构抽取 `boundedAt`,语义等价);squad@95b387d、native@be556f4 两提交均只改 gitlink;嵌套 pin 全链一致(native→intent@7f3ff6a;squad→intent@7f3ff6a+native@be556f4;backend→intent@7f3ff6a+native@be556f4+squad@95b387d,与 superrepo 四个 gitlink 完全对应)。语料文件 recall_corpus.json 未改动(验收基准未被移动)。
- **N8(正面确认,正确性要点)**:否定为逐次出现判断,同词条存在任一未否定出现即正向(TestNegationCountsUnnegatedOccurrence/TestNegatedPatternHit 覆盖了否定+未否定同词双情形);守卫域豁免否定(registry.go:236、254,宁可多拦,TestGuardIgnoresNegation);excludes_any/includes_all 语义未动;r2 回归测试改名并反转断言是对旧契约的有意修订且注释说明清楚;`resolveTie` 确定性(filled 降序→priority 降序→合并序,胜者须压过组内全体,平 priority 仍 ambiguous),仅在胜出时原位重排 hits,Explain/Recognize 双路径一致,I5 行为保留(TestRecognizeTieBreakPrefersPriority 未改);槽位新检查在 format/enum/validator 之前(slots.go:186-198),空白归一化后进 missing(slots.go:123),overlap 检测未动;misexec_test.go 逐条 pin 了 6 次误执行序列与 4 个错槽位序列(含 start+clarify 命令断言),非否定输入行为回归用例齐备。
- **N9(字节级安全性)**:`termHit` 以 `i+1+j` 推进的重叠出现扫描与 `negatedAt` 的 `DecodeLastRuneInString` 回退均为 rune 安全;否定判定在折叠文本上进行,否定词为 CJK,大小写折叠/NFKC 不影响。
## 结论
三个修复(否定感知匹配、specificity tie-break、槽位 fail-closed)实现与任务卡设计决定一致,验收序列全部有单元级 pin,pin 级联自洽,范围无外溢。发现集中在启发式边界的已记录取舍(F1/F2)与非阻塞整理项;无阻断合入的问题。测试在本评审工作区因嵌套依赖未物化而 NOT_RUN,以探针与读码代替,合入前按门禁以 CI 为准。
REVIEW VERDICT: APPROVE
+3 -1
View File
@@ -41,7 +41,9 @@ execution:
max_attempts_per_profile: 3
roles:
implementation: [cursor, antigravity, pi-glm, pi, codex]
# cursor 在 U1 单元已耗尽 4 次尝试:1 次传输中断、2 次终态 JSON 后附带尾文、1 次模型列表瞬时为空。
# 临时调整,交付前回退。
implementation: [antigravity, pi-glm, pi, cursor, codex]
repair: [cursor, antigravity, pi-glm, pi, codex]
test: [pi-glm, pi, antigravity, cursor, codex]
review: [pi-glm, pi, antigravity, cursor, codex]
+6 -1
View File
@@ -2,7 +2,10 @@
- 用途:开发集成父仓(gitlink 权威);非分发单元。
- 组合:`modules/`(产品组件)+ `deps/`(共享依赖);完整组件清单以 [`.gitmodules`](.gitmodules) 为准。
- 部署:非容器;Releases 仓从本仓固定提交导出版本。
- 部署:非容器为现行默认(`storage_mode=legacy`);容器化存储改造见
[容器化存储方案](docs/superpowers/plans/2026-10-22-container-storage-plan.md),
环境与数据归属合同(U1)见 [deploy/data-ownership.md](deploy/data-ownership.md)。
Releases 仓从本仓固定提交导出版本。
## 验证
@@ -56,6 +59,8 @@
[计划](modules/documents/docs/proposals/2026-10-12-work-companion-ui-plan.md) 覆盖任务摘要、对话/输入、窄屏和真实状态投影。
- [服务重启与全量数据恢复](modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md):svc 启停、异常退出后的依赖、配置/数据身份、任务/HITL/副作用、文件与组合备份恢复;
U8 规格见 [SQLite 实例守护](docs/recovery/u8-sqlite-instance-guard.md)。
- [单一应用二进制与容器化数据库、知识库存储](docs/superpowers/plans/2026-10-22-container-storage-plan.md):两容器三库、外置文件根、环境独立绑定;
U1 归属清单见 [deploy/data-ownership.md](deploy/data-ownership.md)。
- [svc 统一部署与一次性登录 Key](modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md):
二进制/源码统一部署、环境准备、无初始密码开户与账户恢复;规格见 `docs/svc-deploy/`,
账户与恢复续审见 [续审记录](docs/svc-deploy/2026-10-12-account-durability-plan-review.md)。
+40
View File
@@ -0,0 +1,40 @@
# 容器化存储:数据归属清单(U1)
权威设计:`docs/superpowers/plans/2026-10-22-container-storage-plan.md`(U1、KTD1—KTD4)。
实例身份与期望态唯一权威:`runtime/<name>/manifest.json`(`scripts/svc_manifest.py`);
本清单只登记**数据所有者、类型、引用形式与唯一写者**,不含秘密值与业务内容。
生产体量/源版本盘点需另行授权访问后补全;未核实项不得据此声明迁移完成。
## 归属总表
| 所有者 | 类型 | 引用形式(登记键) | 唯一写者 | 迁移范围 |
|---|---|---|---|---|
| 基座逻辑库 | PostgreSQL 逻辑库(账号/项目/消息/文档/设置等基座表族) | `base_dbref` 或 `base_dsn_ref`(绝对路径秘密文件)+ `base_role_ref` + `base_port`;身份锚 `identity.base_db_ref` | 本实例应用进程(经基座 DSN);引导/迁移角色与应用角色分离(U2) | 必需 |
| RDM 领域库 | PostgreSQL 逻辑库(RDM 域表/项目文档树等) | `rdm_dbref` 或 `rdm_dsn_ref` + `rdm_role_ref` + `rdm_port`;锚 `identity.rdm_db_ref` | 本实例应用内 RDM 域(独立 DSN);非基座连接串 | 必需 |
| 知识库 | PostgreSQL/pgvector 逻辑库(语料、索引投影) | `kb_dbref` 或 `kb_dsn_ref` + `kb_role_ref` + `kb_port`;锚 `identity.kb_db_ref` | 本实例知识库接入(应用内);索引可重建但语料库为权威 | 必需 |
| 持久文件根 | 目录树(工件、RDM uploads、KB 原件及必要解析产物) | `files_root`(**绝对路径**,不相对 checkout/worktree 解析);锚 `identity.files_root` | 本实例应用进程;禁止他实例共用同一根 | 必需 |
| 环境身份 | 枚举 | `env_class` ∈ `prod`\|`test`\|`verify`;锚 `identity.env_class` | 运维显式登记;克隆时换新(U5) | 必需(container) |
| 实例身份 | 稳定 ID + 数据 marker | `instance_id`;`<data_root>/.agentx/data-marker.json` | `svc` manifest 治理;启动只校验不重建 | 必需 |
| Compose 项目 | 容器项目名 | `compose_project`;同 runtime 内唯一 | 本环境容器驱动(U2);冲突在修改前拒绝 | container 必需 |
`storage_mode=legacy`(缺省):沿用既有 SQLite / 宿主 PG(`db`/`dbref`/`dsn`)路径,不要求三库与 `files_root`。
`storage_mode=container`:上表 container 行全部必需;缺任一绑定、错库、错文件根、foreign marker、同名 Compose 或端口冲突 → **拒绝**,不静默建空库、不回退默认。
凭据只允许以受保护文件路径引用(`*_dsn_ref` / `*_role_ref`),明文不得入 manifest、日志或本清单。
## 配置形态
见同目录 [`env-bindings.example.env`](env-bindings.example.env)。写入实例:
```bash
# 先写齐绑定键,最后打开 container 合同(缺一即拒)
scripts/svc.sh <name> … # 经 svc_set_key / manifest set;Compose·端口冲突在修改前失败
```
另一 checkout/worktree 只要指向同一绝对 `files_root` 与库引用,仍定位同一资源(路径不相对仓库根解析)。
## 与旧恢复清单关系
细粒度基座表/插件枚举仍见 [`docs/recovery/u1-data-ownership.md`](../docs/recovery/u1-data-ownership.md)。
本文件是容器化三库+文件根的**合同级**归属;实施迁移(U3)时两者对照,必需所有者未覆盖则阻塞切换。
+31
View File
@@ -0,0 +1,31 @@
# 容器化存储实例绑定示例(U1)——写入 runtime/<name> 经 manifest 权威登记。
# 秘密只引用受保护文件路径,不在此填写明文密码/DSN 值。
# 完整合同见 deploy/data-ownership.md;schema 键由 scripts/svc_manifest.py 校验。
# legacy(缺省)| container(三库+文件根+环境身份全部必需)
storage_mode=container
# 环境身份:prod | test | verify(三选一)
env_class=prod
# 持久文件根:必须是绝对路径(不相对代码 checkout/worktree 解析)
files_root=/var/lib/agent-system/state/prod/files
# Compose 项目名:同机各环境唯一;冲突在修改前拒绝
compose_project=agentx-prod
# 基座逻辑库:dbref=<pg实例>/<库名> 或 dsn_ref=<绝对路径秘密文件>(二选一)
# base_dbref=pg-business/agent_base
base_dsn_ref=/etc/agent-system/prod/base.dsn
base_role_ref=/etc/agent-system/prod/base.role
base_port=15432
# RDM 领域库
rdm_dsn_ref=/etc/agent-system/prod/rdm.dsn
rdm_role_ref=/etc/agent-system/prod/rdm.role
rdm_port=15432
# 知识库(pgvector 容器独立端口)
kb_dsn_ref=/etc/agent-system/prod/kb.dsn
kb_role_ref=/etc/agent-system/prod/kb.role
kb_port=15433
@@ -0,0 +1,266 @@
---
artifact_contract: ce-unified-plan/v1
product_contract_source: ce-plan-bootstrap
execution: code
date: 2026-10-22
title: 单一应用二进制与容器化数据库、知识库存储改造方案
---
> **落库状态(2026-10-22)**:方案已获审阅并授权实施。任务以 L1 initiative
> `feature/container-storage-plan` 重新登记(R0=955a32de2bb49171348bad5f566cce935df19212)。
> 原始规划工作区(基线 9a80897)因子模块物化失败已保留现场并对账后回收:
> 全部子模块 HEAD 均在远端分支上、无独有提交、无未跟踪文件,任务卡副本存于
> `/tmp/container-storage-plan-20261022/card-auto-generated.md`。本文正文为审阅时快照,
> 其中基线 9a80897 与工作区失败描述属历史记录;实施以本卡与各子任务卡为准。
# 单一应用二进制与容器化数据库、知识库存储改造方案
本文是待用户审阅的方案与实施计划,不代表业务实施、部署或迁移授权。应用二进制及数据库容器的总体方向来自本次会话;具体分工、步骤与验收为本方案建议。
计划归属 Agent-System-Dev;本文因新工作区子模块物化失败暂存仓外,尚未提交或合入。路径以各标明仓库的根目录为基准。后续正规落点建议为 Dev `docs/superpowers/plans/2026-10-22-container-storage-plan.md`。
## Goal Capsule
- **目标**:运维人员能重建生产、测试、验证环境,把一个完整环境迁到新机器或新目录,并通过恢复验证确认账号、项目、文档、文件和知识库引用仍可用。
- **手段**:一个应用服务二进制,两个 PostgreSQL 容器,三套逻辑数据库,外置持久文件;同一制品使用独立环境配置(KTD1—KTD4)。
- **当前授权**:制定方案和计划。产品代码、容器部署、生产数据读取/复制、迁移切换均未在本轮执行;后续按用户明确授权范围实施。
- **执行责任**:各仓遵循自身 AGENTS;Dev 范围的可独立实施单元按 AGENT-EXECUTION.md 分配全新外部 Agent 会话,host 管理范围、验证、独立 Review 和交付。任务进度放任务卡,不在本文伪装已完成。
- **停止条件**:需要新增数据库类型、改变业务权限、无法完整迁移现有数据、需要丢弃新写入或扩大业务范围时停止相关动作,提交最小决策;不为容器化引入高可用平台、通用运维平台或应用微服务化。
## Product Contract
### Summary
保留前后端、Agent、RDM、知识库接入在一个应用进程内;数据库依赖使用 Compose。业务 PostgreSQL 容器分别承载基座库和 RDM 领域库,知识库 PostgreSQL/pgvector 容器承载语料与检索索引。生产、测试、验证环境分别建立,不通过临时修改生产连接串来“切环境”。
### Problem Frame
现有宿主数据库运维、应用快照、RDM 领域备份和知识库恢复各有入口,但尚未形成可核验的完整环境恢复闭环。容器化只改变数据库运行方式,必须同时处理身份绑定、数据库与文件的一致恢复点,以及复制环境后的副作用隔离。
### Requirements
| ID | 要求与可核验结果 |
|---|---|
| R1 | 完整应用服务只需一个可执行文件,包含前端资源、后端、Agent、RDM、知识库接入;运行机无需源码、Go 或 Node 构建环境。 |
| R2 | 业务库和知识库存储容器化,明确持久卷、镜像版本、扩展版本及生命周期;容器重建后业务数据仍在。 |
| R3 | 每环境独立绑定基座库、RDM 库、知识库、文件根、实例身份、端口和凭据;错误绑定拒绝启动。 |
| R4 | 从备份恢复到新目录/主机后,账号、权限、业务记录、原文和历史引用可核对;成功备份必须是一个可解释的一致恢复点。 |
| R5 | 从当前实际 SQLite/宿主 PG 迁移时覆盖所有必需持久数据所有者,保留业务标识、引用与原始内容,缺失或未知数据阻止切换。 |
| R6 | 测试/验证克隆获得新实例身份及独立凭据,启动前隔离生产密钥、会话、自动任务和外部副作用;生产实例保持固定绑定。 |
| R7 | 普通启停、重建、升级不隐式删卷、不执行数据库大版本升级、不恢复旧备份、不在数据库失败时回退到空 SQLite。 |
| R8 | 形成可重复的组合验证和生产切换/回滚操作记录;实现完成、测试通过、生产迁移完成、业务验收通过分别记录。 |
### 边界与假设
- “单一二进制”按用户原话“最终的服务”解释为一个常驻应用服务。允许部署配置、脚本和离线迁移工具存在;不新增常驻 admin、KB HTTP 或前端服务。本期不为统一离线工具而重构全部 CLI。
- 一期面向单机部署,每套环境独立;可在同机不同端口运行,也可迁到不同主机。宿主应用通过回环地址连接本机容器。
- 知识库容器化指语料数据库及向量索引容器化;检索编排、权限与引用业务仍在应用二进制内。原始文档使用外置持久存储。
- 允许维护窗口,先实现可验证的停写备份与迁移,不承诺零停机、实时复制、PITR 或自动故障切换。
- 保留既有 SQLite/原生 PG 运行路径用于兼容与受控回退;本方案目标环境使用容器 PG,不顺带删除旧实现。
- 公有知识库共享、跨环境共用只读卷、Kubernetes、托管云数据库、全量对象存储替换均不进入首期。
- 生产停写窗口、源实例、机器容量、实际源版本与数据规模在 U1 核实;它们不妨碍方案审阅,但未核实前不能安排生产切换。规划不承诺未经测量的 RTO/RPO。
## Planning Contract
### 当前依据与可复用基础
本轮主仓基线 Dev `9a80897cb7e9108091d302e849f59662f21ce260`。以下是源码/文档核查,未探测实际生产实例,也未运行产品测试。
| 范围 | 已核对的依据 | 本次改造含义 |
|---|---|---|
| 单二进制 | Dev `Makefile`;Backend `web/embed.go`;RDM `main.go`、`scripts/build-product.sh` | 已有静态组合和前端嵌入,复用完整产品构建;不能用只有基座的二进制代替 RDM 产品。 |
| 宿主 PG | Dev `scripts/svc.sh` 的 pg_bindir/db_create/pg_do_start/pg_do_stop/pg_run/pg_dump_run/db_backup/db_restore/db_clone | 容器支持需要完整后端分派,不能只替换启动命令;备份恢复继续使用已有安全边界。 |
| 实例身份 | Dev `scripts/svc_manifest.py` 的 PROTECTED/db_anchor_of/reanchor;`tests/test_svc_identity.py` | manifest 已有权威身份;扩展三库和环境绑定,不另造第二份登记权威。 |
| 基座迁移 | Backend `cmd/storage-migrate/main.go` | 已有 SQLite→PG 工具,但只迁 Backend;artifacts 参数只盘点不复制,不能据此声称迁了整个产品。 |
| RDM 迁移 | RDM `main.go` 的 migrate-to-pg/migrate-check/backup-domain/restore-domain | 独立 RDM_DSN;RDM 领域库和 uploads 必须明确纳入。 |
| KB Compose | KB-Regulations `deploy/compose.yaml`、`deploy/README.md` | 已有本地 pgvector/pgvector:pg16、回环端口、卷与扩展初始化模板;固定项目名 kbreg 不适合作为所有环境的全局名称,已有标签不等于生产版本批准。 |
| 文件迁移 | Backend `internal/blobstore/blobstore.go`;KB-Regulations `migrations/0001_schema.sql`、`docs/operations/corpus-import.md` | 工件目录相关引用、source_assets.storage_uri 中 file 路径会影响异地恢复;不能只做 dump/restore。 |
| 组合恢复 | Dev `scripts/svc.sh` 的 agent_backup_snapshot;`tests/integration/regulations-kb/README.md` | 应用快照不含全套数据库和原始 manifest;KB 测试可复用,但不证明全套外部文件已恢复。 |
RDM 研究对象为兄弟检出 `b14c84c6dcc945bd4d0a9ad446cf1ec980396491`,KB 为 `393555bec8d964f294f932616aa69ab833a06de1`;它们与 Dev `tests/integration/regulations-kb/components.pin` 的固定组合不同。本方案引用它们用于定位能力,正式实施前必须选定并验证精确组件组合,不能把研究检出当作已采纳版本。
已有恢复设计 `modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md` 是设计约束来源,本文沿用其停写一致恢复点、恢复新目标、旧写者隔离思路,不把历史执行状态当现状。
### Key Technical Decisions
**KTD1.应用保持单一服务二进制。** 复用现有产品组合构建,配置和数据外置;容器承载持久数据库依赖,知识库插件不拆进程。落实 R1/R2。(session-settled: user-directed — chosen over containerizing the whole application: user requires the final service to remain a single binary)
**KTD2.两个 PG 容器、三个逻辑库。** 业务容器中分别设基座库与 RDM 库,知识库另用带 vector/pg_trgm 的 PG 容器。数据库使用独立应用角色;引导/迁移角色与应用权限分开,先核对现有启动迁移要求,再安排显式迁移阶段,不能直接收紧权限导致启动失败。落实 R2/R3/R5。
**KTD3.复用 svc 管理入口。** 新增窄的容器驱动和版本化环境配置,保留现有命令语义。manifest 是身份与期望态权威;Compose 文件是版本化模板,环境参数由同一配置解析生成。不得并存两份可独立修改的 DSN/卷权威。只管理明确归属本环境的容器,不启停外部数据库。落实 R3/R7。
**KTD4.稳定绝对数据根与环境独立资源。** 建议形态为 `state/<env>/` 与其外部 `backups/<env>/`,实际根由配置给出绝对路径;不相对代码 checkout/worktree 解析。Compose project、两套卷、各库、角色与端口都显式区分环境。凭据放受保护文件/secret 引用,日志和清单脱敏。文件树示意仅表达归属,不规定最终名称。
**KTD5.先备份完整恢复点,再追求不停机。** 关闭新写入口,排空或隔离在途任务,停止应用、定时任务、KB 导入/索引写者及迁移器;确认其他客户端不再写入后,对三库分别逻辑导出并复制受控文件。三次 dump 各自的快照不自动构成跨库一致性,靠整个窗口无写者保持一致。完成校验后才发布恢复包完成标记。落实 R4/R8。
**KTD6.恢复和克隆是不同操作。** 原实例恢复保留业务与实例身份,但须先停旧写者,绝不双开同一生产身份。测试克隆保留业务 ID 及引用关系,创建新的部署身份和守护绑定、数据库凭据;使生产登录会话/长期 API key 在副本失效,外部 provider 密钥不带入。后台任务与外发能力默认关闭,授权后逐项启用;不通过批量改业务 ID 实现隔离。落实 R3/R6。
**KTD7.迁移与升级分离。** 源 PG 采用同大版本兼容目标与匹配工具链,SQLite 迁移用已有逻辑迁移器;本次不顺带 PG 大版本升级或强制统一业务/KB PG 大版本。实施时固定经过验证的镜像 digest、平台与扩展版本。镜像官方文档说明不同主版本的 PGDATA/卷位置存在差异,配置按选定镜像逐项核对,不能把 KB pg16 的挂载路径盲用到另一主版本。初始化脚本仅负责新卷引导,既有库变更走显式迁移。落实 R2/R5/R7。
### High-Level Technical Design
以下是关系和流程草图,新增配置/命令的最终命名由实现单元在既有接口中确定。
```mermaid
flowchart LR
Client[浏览器] --> App[一个完整应用二进制]
Profile[环境配置与实例 manifest] --> App
Profile --> Compose[Compose 数据依赖]
App --> Base[业务容器 / 基座库]
App --> RDM[业务容器 / RDM 领域库]
App --> KB[知识库容器 / 语料库与索引]
App --> Files[持久文件根 / 工件 uploads 原件]
Compose --> Base
Compose --> RDM
Compose --> KB
```
```text
state/<env>/
instance/ manifest 与受保护配置、应用版本引用
files/ 工件、RDM uploads、KB 原件及必要解析产物
Docker 持久卷:<project>-business、<project>-knowledge(各环境独立)
backups/<env>/<backup-id>/ 三库导出、文件、配置恢复材料、版本与校验清单
```
```mermaid
flowchart LR
A[识别目标与锁定操作] --> B[预检配置/身份/镜像/资源]
B --> C[启动两库容器]
C --> D[认证/扩展/schema/三库身份检查]
D --> E[按需执行显式迁移并核验]
E --> F[启动应用]
F --> G[业务就绪验证]
D -->|失败| H[拒绝启动并保留现场]
```
```mermaid
stateDiagram-v2
[*] --> Running
Running --> Quiescing: 请求一致备份
Quiescing --> Frozen: 写者已停且在途状态已记录
Frozen --> Capturing: 三库与文件导出
Capturing --> Verified: 完整性和清单校验
Capturing --> Incomplete: 失败或中断
Verified --> Running: 发布完成标记后恢复原实例
Incomplete --> Running: 确认现场可安全恢复后解除停写
```
恢复流程:读取完成清单 → 校验兼容版本与文件 hash → 选择空的新目标 → 建库/恢复角色权限及三库 → 还原文件与受控路径映射 → 核对实例模式(原实例/克隆)→ 禁止外部副作用下启动 → 登录、权限、文档、KB 引用验证 → 单独决定是否开放写入。任一步失败保持目标隔离,不覆盖旧系统。
### 完整恢复包合同
- 基座、RDM、KB 三库的逻辑导出及 schema/扩展版本;必需角色/权限用受控恢复定义记录,不能盲目导入生产全局超级用户。
- 工件、uploads、KB 原件及必要解析产物;包含全部必需插件声明的数据。对对象存储记录不可变版本/校验值及可恢复获取方式;无法获取的必需对象阻止完整恢复声明。
- 原始实例 manifest、部署配置、应用制品及插件/前端组合的 SHA、容器镜像 digest、平台信息;配置清单不含明文秘密,恢复秘密通过独立受保护介质或重新注入。仅含引用时必须验证引用在目标可解析,不能称恢复包独立可恢复。
- 文件清单与 hash、数据所有者清单、停写时间和操作状态、恢复工具版本、校验结果及完成标记。备份与 runtime 卷分离,至少验证一份异机/独立存储副本可读取。
- 原件路径变更仅调整定位信息,不改变内容 hash、语料修订、节点 ID、历史 excerpt 和审计封印。受保护审计内容若包含原路径,必须通过解析映射兼容,禁止重写封印记录。
- 向量索引可重建,但 embedding 模型/维度和语料版本要记录;恢复测试不依赖付费重新嵌入,优先复用保存的向量重建索引。
## Implementation Units
### U1.环境与数据归属合同
- **需求**:R1/R3/R5/R6,KTD1—KTD4。
- **目标**:一份可解析配置确定完整实例资源,明确现有数据来源和迁移边界。
- **主要文件**:Dev `scripts/svc_manifest.py`、`scripts/svc.sh`、`docs/svc.md`、`PROJECT.md`;新增环境 schema/示例置于 Dev `deploy/`(建议路径)。
- **做法**:扩展现有 manifest 绑定基座/RDM/KB,版本化旧配置升级;保护环境命令不受调用者环境变量污染。列出各数据所有者、类型、源版本、目录、体量、表/对象清单、引用形式和唯一写者。不输出秘密或业务内容。生产盘点需先获得对应访问授权。
- **测试文件**:扩展 `tests/test_svc_identity.py`、`tests/test_svc_lifecycle_isolation.py`、`tests/test_svc_precheck.py`。
- **场景**:旧配置升级保留身份;错库/错根/foreign marker 拒绝;同名项目或冲突端口在修改前失败;在另一 checkout 调用仍定位同一资源;三库中任一必需绑定缺失不能静默新建空库。
- **完成条件**:配置、归属清单和迁移范围有唯一解释;明确需迁移的每个数据所有者。依赖:无。
### U2.容器数据库与 svc 接线
- **需求**:R2/R3/R7,KTD2—KTD4/KTD7。依赖 U1。
- **主要文件**:Dev `scripts/svc.sh`(后端分派)、新增 `scripts/svc_containers.sh` 和 `deploy/compose.storage.yaml`(建议路径);KB-Regulations `deploy/compose.yaml`、`deploy/initdb/01-extensions.sql`、`deploy/README.md` 作为复用来源,避免复制成两套互相漂移的模板。
- **做法**:start/stop/status/SQL/dump/restore 覆盖同一容器驱动;记录明确所有权;卷重建、普通停止和数据销毁分别处理。应用仅连 loopback 发布端口。引导创建基座/RDM 独立库与角色,KB 创建扩展;后续变更使用显式迁移。启动顺序验证真实认证、扩展和身份,不能只依赖 pg_isready。
- **测试文件**:新增 Dev `tests/test_svc_containers.py`;扩展 `tests/test_svc_precheck.py`、`tests/test_svc_realcombo.py`。
- **场景**:prod/test/verify 同时运行互不冲突;停止/重建一套不影响其他套且不丢数据;缺 Docker/镜像/磁盘/凭据时在破坏性动作前失败;密码变更不误以为改 env 就改变已初始化库;容器意外停止时应用不得写入 SQLite。
- **完成条件**:用固定制品及临时环境完成两容器三库启动、持久化、重建与失败路径验证。运行冒烟优先,不用大量模拟测试替代真实容器验证。
### U3.迁移与文件引用兼容
- **需求**:R4/R5/R7,KTD5—KTD7。依赖 U1/U2。
- **主要文件**:Backend `cmd/storage-migrate/`、`internal/blobstore/blobstore.go`;RDM `main.go` 与其现有领域迁移实现;KB-Regulations 的 source_assets 读取/原件定位实现及 `docs/operations/corpus-import.md`。函数名在实施启动时定点核对,不另建并行迁移框架。
- **做法**:基座和 RDM 分别复用现有迁移器;覆盖实际 schema 的新增表、外键、序列/标识和身份记录;未知表不能悄悄遗漏。已有 PG 走兼容逻辑恢复。文件迁移校验内容和所有权,新增受控资产根映射或兼容相对引用,维持旧引用可读取。禁止迁移时跨出允许根或追随外部软链复制未知文件。
- **测试文件**:Backend `cmd/storage-migrate/main_test.go`、`integration_test.go`、`seeded_singletons_test.go` 与 `internal/blobstore/` 对应测试;RDM 现有领域迁移测试(实施前定位);KB-Regulations `tests/integration/recovery_pg_test.go`。
- **场景**:SQLite 基座+RDM 迁入不同逻辑库;原 PG 同版本恢复;空库与真实结构 fixture;失败/重跑/非空目标拒绝;未知表拒绝;不同根目录恢复后读取旧工件与 file URI 原件;越界路径拒绝;历史 citation/审计 hash 不变。
- **完成条件**:逐所有者对账,不仅比较行数,还核对 ID、关键引用、内容 hash、权限和新写入;不能完成的必需所有者阻止继续生产准备。
### U4.组合备份与恢复新目标
- **需求**:R4/R5/R8,KTD5/KTD7。依赖 U2/U3。
- **主要文件**:Dev `scripts/svc.sh` 的 backup/restore 入口;新增组合恢复辅助实现(建议 `scripts/svc_bundle.py`);`docs/svc.md`、恢复操作说明;各组件只提供必要导出/校验接口。
- **做法**:按恢复包合同采集全部数据,锁定同环境操作,停写并确认所有写者;失败包无完成标记。完整备份默认只恢复到空的新目标;应用制品和 schema 兼容预检先于开放写入。秘密处理有明确备份/重新注入路径及权限校验。
- **测试文件**:扩展 `tests/test_svc_backup.py`;新增 `tests/test_svc_bundle.py`;扩展 `tests/ops/run_backup_drill.sh` 的不同目标根场景。
- **场景**:三库与文件可恢复;备份中断、空间不足、错包、hash 损坏、缺原件、缺必要 secret、镜像不兼容拒绝;目标非空拒绝;不得覆盖生产;重新启动源实例不把未完成包发布为成功;保留原生产身份恢复时,目标接管前必须隔离旧写者;独立克隆从已完成的一致备份恢复,源实例可在备份完成后继续运行,但目标首次启动前必须完成新身份、独立凭据及副作用隔离。
- **完成条件**:干净环境恢复完整产品并生成可复核清单;实测备份大小、停写时长和恢复用时。
### U5.测试/验证克隆与环境选择
- **需求**:R3/R6/R8,KTD4/KTD6。依赖 U4。
- **主要文件**:Dev `scripts/svc.sh` 的 clone/启动接线、`scripts/svc_manifest.py`;Backend/RDM 现有身份、会话和任务控制入口仅作必要接线;测试数据 fixture 与说明。
- **做法**:以明确目标环境创建新实例。业务 ID 不改,部署身份、守护与凭据换新;登录会话/API key 不在副本继续有效,账号密码处理按克隆用途明确。需要复制生产敏感数据时先落实访问范围和脱敏策略;常规自动验收优先合成数据。外部密钥、自动调度与恢复任务副作用默认禁用,并在首次启动前生效。
- **测试文件**:扩展 `tests/test_svc_identity.py`、`tests/test_svc_lifecycle_isolation.py`;新增 `tests/test_svc_environment_clone.py`。
- **场景**:克隆后生产仍正常;新身份不触发原守护冲突;旧 token 无法访问副本;待执行任务不自动发信/调用真实 provider/写生产目录;选择 test 不修改 prod 配置;操作摘要明确目标环境。
- **完成条件**:三套环境同机并存,隔离由实际读写与副作用拒绝验证,不靠名称或配置声明。
### U6.单二进制与组合验收、运维交付
- **需求**:R1—R8,KTD1—KTD7。依赖 U1—U5。
- **主要文件**:Dev `Makefile`、`tests/test_svc_realcombo.py`、`tests/integration/regulations-kb/run.sh`、`components.pin`、`PROJECT.md`、`docs/svc.md`;RDM `scripts/build-product.sh`;各仓必要 runbook/组合指针。
- **做法**:固定完整产品与镜像组合,在没有源码及 Go/Node 的机器/隔离环境用同一应用制品启动。把恢复、克隆、数据库容器重启加入组合验证。必要管理操作优先复用同一产品已有入口,离线工具不成为常驻依赖。受影响仓独立验证后再推进 Dev 组合,不重做发布体系。
- **场景**:UI/API、正常账号登录、角色边界、项目/会话、RDM 上传/文档原件读取、知识库检索、历史引用解析;两库容器依次重启;新目录恢复;test/verify 重建不影响 prod;客户端制品无 secret。
- **完成条件**:满足 Definition of Done;输出生产切换操作单与回滚分界,实测停写时间供用户安排窗口。生产执行属于下面 O1,不由代码测试替代。
## Verification Contract
- 本轮是规划:产品测试、真实容器试验、迁移演练、生产盘点/部署均为 **NOT_RUN**;源码核查属于 SELF,不能提升为 GATE 或 REPLAY。
- 实施每单元先运行其最小直接检查,容器与文件恢复必须有真实临时环境验证。禁止把缺 DSN 导致的跳过计作通过;真实测试仅指向显式临时环境。
- Backend 真 PG 迁移测试使用 `AGENT_PG_TEST_DSN`;法规组合门禁使用 `KBREG_TEST_DATABASE_URL`、精确 RDM/KB pin 及既有 `make test-regulations-kb`。
- Dev 治理脚本确有修改时运行 `make test-governance`;最终组合按项目要求执行 `make test`,并以当前 CI 要求核对 `test-pg`、`svc-realcombo` 等检查,不能凭旧清单声称全部通过。
- 推进 Dev gitlink 前完成所需组合验证;命令原始输出经 `make evidence` 写任务卡。正式 Review 前重放最小集合、同步权威目标;合入信任来自精确 HEAD 的必需 CI 或独立 REPLAY。
- 全量检查只在最终候选或有必要时运行;发现无关非阻塞问题不扩大本次任务。
## O1.生产迁移与回滚计划(另行授权执行)
1. **确认源与窗口**:记录实际源实例、三库/文件清单、schema、版本、容量、现有身份、备份位置及授权范围;确认恢复演练达标和可接受停写时间。
2. **提前准备目标**:拉取固定镜像、准备空库/角色/持久目录、验证磁盘与网络;用演练副本验证完整应用制品。此阶段不改生产连接。
3. **冻结生产写入**:暂停入口及所有后台/外部写者,排空或隔离在途工作,确认旧实例不会被 supervisor 自动拉起;记录已确认写入的对账点。
4. **最终备份与迁移**:按 U3/U4 生成最终完整包并恢复目标;核对所有必需数据与文件,运行只读业务验证;保持外部副作用关闭。
5. **受控切换**:以迁移操作更新目标绑定、身份和文件路径;先验证登录/权限/文档/知识库,再开放写入及经确认的后台任务。避免两个生产写者同时存在。
6. **观察与保留**:保留旧数据及最终备份,只读保护并记录新系统写入开始时间;到达业务验收及约定保留期后才单独安排旧资源回收。
回滚分界必须写入操作单:
- **新目标尚无业务写入**:停止新实例,确认无新数据后恢复旧配置/旧写者;不可让双方同时写。
- **新目标已有业务写入**:停止写入并保存新数据,禁止简单切回旧库;评估向前修复或经过验证的数据对账/回迁。未经确认不丢弃新写入。
- **schema 已改变**:旧应用制品不一定兼容。代码回滚、数据库恢复和业务数据回迁分别验证,不能用“换回旧二进制”代替恢复方案。
## Definition of Done
代码交付完成须同时满足:
- 一个完整应用服务二进制在目标运行环境启动,业务/RDM/KB 数据存储符合两容器三逻辑库边界。
- prod/test/verify 可独立启动、停止、重建与选择,配置/数据/身份/权限隔离已验证。
- 完整备份可以恢复到不同根目录;原文可读、业务引用和历史法规引用不变;缺失数据时明确失败。
- 真实迁移 fixture 对账通过,重启与容器重建不丢数据,失败不会回退空系统或启动外部副作用。
- 精确组件组合、测试证据、独立 Review、所需 CI 及规范文档齐全;没有遗留试验实现进入最终 diff。
- O1 的执行另记:代码完成不等于生产迁移完成,恢复演练也不等于业务验收。
## 顺序与建议里程碑
| 里程碑 | 工作 | 出口 |
|---|---|---|
| M1 最小可运行 | U1 → U2 | 同一应用连接两容器三库,test/verify 并存,持久化与错误绑定验证通过 |
| M2 可迁移可恢复 | U3 → U4 | 完整数据恢复到新目录,原文和历史引用可核对 |
| M3 可安全切环境 | U5 → U6 | 克隆隔离、完整产品组合验证、操作说明齐备 |
| M4 生产落地 | O1 | 获得窗口授权后迁移、业务验收,旧数据按约定保留 |
U1 完成后再按实际 schema 覆盖、文件规模及演练用时细化工期;当前不把未测量的数据量换算为承诺日期。每个里程碑只处理阻塞目标的问题。
## 外部依据与待核验项
本轮尝试读取 Docker Compose、PostgreSQL 备份与 pgvector 官方文档;网络读取不完整。成功获取 Docker Official Image 的 PostgreSQL 说明(docker-library/docs 的 postgres/README.md),其中确认 PGDATA/卷位置随主版本变化、初始化参数仅对新数据目录生效、密码支持文件注入。这些仅作为镜像适配约束,未确定最终镜像 digest,也未验证某个部署平台。
实施 U2 前需针对所选精确版本再次核对 Compose/镜像/pgvector 官方资料,并做真实冒烟;失败则阻止该镜像采用,不阻止本方案的源码设计工作。
@@ -0,0 +1,64 @@
# 独立 Review:容器化存储 U1(环境与数据归属合同)
- 日期:2026-10-22
- 审阅对象:`feature/container-storage-plan-u1` 分支 HEAD
- base:`955a32de2bb49171348bad5f566cce935df19212`
- head:`e1f08650a12656611f27438f23fddbfab6a96299`(分支尖端;实施内容经 agent-exec transport 提交 `2d75d101a04ce1d1c9fa1d66d9567894222399d5`,另有路由调整 transport 提交 `0c8bea37` 与任务卡记录提交)
- 送审 diff:`/tmp/container-storage-u1-review.diff`(已核对与 `git diff 955a32de..HEAD` 的代码部分逐字一致;仅 `.task/**` 任务卡不在 diff 内,属 host 直写豁免区)
- 方案依据:`docs/superpowers/plans/2026-10-22-container-storage-plan.md` U1 小节、KTD1—KTD4
- 审阅方式:全新会话独立审阅(非实施者);实际读代码核对绑定解析、拒绝路径与身份保留逻辑;未实施任何修复。
## 总体判定:REVIEW PASS
无阻塞项。U1 验收场景(旧配置升级保留身份、错库/错根/foreign marker 拒绝、同名项目或冲突端口修改前失败、另一 checkout 定位同一资源、缺必需绑定不静默新建空库)均有实现且被测试覆盖;KTD3 单一权威合同成立;无秘密入仓;测试 80/80 通过(1 项真实 PG 专项显式 NOT_RUN)。下列非阻塞项建议在 U2 及最终交付前处理。
## 阻塞项
无。
## 非阻塞项
| # | 级别 | 位置 | 发现 | 建议 |
|---|---|---|---|---|
| N1 | 建议 | `scripts/svc.sh` `svc_set_key`/`svc_del_key`(4674/4691 行起) | manifest `set` 因绑定校验或冲突被拒(exit 2/3/7)时,回退路径仍把该键写入派生视图 `svc.env`,整体操作"成功"返回。权威 manifest 未被污染(`resolve_storage_bindings` 只读 manifest),且两种漂移方向在启动时均 fail-closed(`svc.env=container`/`manifest=legacy` → `verify --require-storage` 触发 `完整存储合同要求 storage_mode=container` 拒启;反向漂移由 record_state 重建自愈),但"冲突在修改前失败"的语义被弱化为"权威拒绝+视图静默写入"。当前无任何调用方经 `svc_set_key` 写存储键(调用点仅 autostart/exec/dbref/dsn/conffile/addr/tags),风险处于潜伏状态。 | 存储键(STORAGE_KEYS 集合)在 manifest set 失败时硬失败,不走 legacy 单键回退。 |
| N2 | 建议 | `scripts/svc.sh` `svc_manifest_verify`(1302 行) | container 分支门控用 `svc_get storage_mode`(读派生 `svc.env`)而非 manifest identity。manifest=container 而 svc.env 过期(manifest 与 svc.env 两次原子写之间的崩溃窗口,或绕过 svc 手改 manifest)时,该次启动跳过存储校验;下次 record_state 经 bulk-set 重新派生后自愈。窗口窄且可自愈,但与"manifest 为权威"的门控意图不一致。 | 门控改为读 manifest identity 的 `storage_mode`(或在检测到两处不一致时拒绝并提示重建派生视图)。 |
| N3 | 建议 | `deploy/data-ownership.md`(持久文件根行) | 声明"禁止他实例共用同一根",但 `check_conflicts` 只扫描 compose 项目名与端口,不检测跨实例 files_root 重叠/嵌套;测试夹具(precheck `test_compose_project_conflict_refused`)中 b 的根位于 a 的根之下,同样未拦截。无 U1 验收场景要求此项,属文档强于实现。 | 收紧措辞为"登记层不拦截,容器挂载层(U2)负责隔离",或在 U2 增加跨实例根重叠预检。 |
| N4 | 记录 | `scripts/svc_manifest.py` `check_conflicts` | 冲突扫描范围限定同一 runtime 目录(`RUNTIME="$ROOT/runtime"`,`--runtime` 显式传入)。同机跨 checkout 的多个 runtime 目录之间的 compose 项目名/端口冲突在 U1 登记层不可见,实际碰撞推迟到 U2 容器启动时由端口占用/compose 报错暴露。实现注释与文档均按"同一 runtime"表述,范围自洽;U2 场景"prod/test/verify 同时运行互不冲突"已承接此项。 | U2 落地容器驱动时复核跨 runtime 同机冲突的最终防线。 |
| N5 | 建议 | `tests/` | 两个覆盖缺口:(a) 无 container 模式实例完整 start/stop 生命周期测试——`preserve_storage_keys`(record_state → bulk-set 写回时保住存储键)与 svc.sh 传 `--runtime` 的改动未被端到端 exercised,若该逻辑回归,绑定会在一次生命周期写回后被静默清掉;(b) `test_CS_U1_missing_binding_rejected` 只覆盖"全部缺失",未覆盖"三库中任一缺失"(如 base/rdm 齐备仅 kb 缺失)的逐槽拒绝。 | 各补一条测试:container 实例启停后锚点与 kv 绑定不变;逐槽缺失各断言一次。 |
| N6 | 记录 | `AGENT-EXECUTION.md`(implementation 角色顺序) | 不在 U1 单元 allowed_paths 内,属越界文件变更;但它是独立登记的路由单元(任务卡行 `container-storage-routing-promote-antigravity`,transport `0c8bea37`,收据 `/tmp/container-storage-routing-receipt2.json`),披露完整,且变更注释自述"临时调整,交付前回退"。 | 按其自述在 U1 合入/交付前回退;不阻塞 U1 评审结论。 |
| N7 | 记录 | `scripts/svc_manifest.py` | `slot_port_of` 只校验 `isdigit()`,接受 `0` 与超范围数值,未限 1—65535。 | 顺手项,U2 接真实容器端口发布前收紧即可。 |
| N8 | 记录 | `scripts/svc_manifest.py` `cmd_verify` | container 启动路径把 `resolve_storage_bindings`(源自 manifest)的值回传给 `verify` 比对,本质是 identity↔kv 自一致性检查(可拦手改 manifest 的不一致),并非运行时"连到外宾库"检测;后者按方案属 U2(启动顺序真实认证/身份检查)与 U3(库内身份)范围。U1 的"错库拒绝"在绑定锚点层面成立(`base 库绑定漂移` 路径有测试),范围声明准确。 | 无需改动;U2 接线时保留本检查作为快速失败层。 |
## 核对点(实际读代码/跑测试核对)
1. **旧配置升级保留身份**:`build()` 对已带 storage 键的 kv 只做形态校验(`require_complete=(mode==container)`),不重建 instance_id;`test_CS_U1_legacy_upgrade_keeps_identity` 断言升级后 `instance_id`/`data_root` 不变、锚点落 identity。✅
2. **身份保留/重锚**:`reanchor(write=False)` 预览仅用于冲突扫描、不落 marker;`mutate` 顺序为"校验 → 冲突扫描 → 重锚 → 落盘",冲突时不落盘;`slot_ref_of` 冲突(dbref 与 dsn_ref 同设)、相对路径 `*_dsn_ref`/`*_role_ref`/`files_root` 均拒绝;legacy→container 升级基座可回落既有 `dbref=` 专键,明文 `dsn=` 不进锚点(I5)。✅
3. **拒绝路径**:container 缺任一必需绑定在 set(`validate_storage_kv` exit 3)与 verify(`--require-storage` + `require_complete=True`)双端拒绝;错文件根/错库绑定漂移 exit 6(测试断言"文件根"/"base 库绑定漂移");foreign marker 沿用 base 逻辑(T5b);compose 项目名/端口冲突 exit 7 且发生在落盘前(`check_conflicts` 在 `save` 之前)。✅
4. **不静默新建空库**:U1 层无任何默认绑定生成;`resolve_storage_bindings` 缺绑定 exit 3 → svc.sh 拒绝;manifest 缺失时 verify exit 4、marker 缺失/损坏 exit 5,均阻塞启动。未发现静默回退空库路径。✅
5. **另一 checkout 定位同一资源**:`files_root` 强制绝对路径并 normpath,锚点/解析不依赖 cwd 与仓库根;`test_CS_U1_other_checkout_keeps_absolute_files_root` 与 T6 从外部 cwd 调用验证。✅
6. **调用方环境污染**:`env_config` 重置存储全局并 `unset AGENT_SYSTEM_FILES_ROOT/ENV_CLASS/COMPOSE_PROJECT/BASE_DB_REF/RDM_DB_REF/KB_DB_REF`;`test_caller_storage_env_does_not_reach_serve` 检查 serve 进程 environ 与 serve.env;三条棘轮测试守卫 env_config。✅
7. **KTD3 单一权威**:`resolve_storage_bindings` 内嵌 python 直读 `manifest.json`(不读 svc.env、不读调用方 env);U1 未引入 compose 文件/第二份卷或 DSN 权威;凭据仅存受保护文件路径引用。`svc.env` 仍是派生只读视图(record_state 不产出存储键,`preserve_storage_keys` 自 manifest 旧 kv 保留)。✅
8. **归属清单与契约文档**:`deploy/data-ownership.md` 覆盖基座/RDM/KB/文件根/环境身份/实例身份/Compose 项目七类所有者,含引用形式与唯一写者;体量/源版本/表清单明示"需另行授权访问后补全"(与方案"生产盘点需先获得对应访问授权"及诚实出口一致);无秘密值。`docs/svc.md` §10.1 键表与 `PROJECT.md` 指针已同步;链接目标 `docs/recovery/u1-data-ownership.md`、`docs/recovery/u1-identity-manifest.md` 均存在。✅
9. **测试充分性**:三测试文件合计覆盖全部五条 U1 验收场景 + 调用方隔离 + 原子写崩溃窗口;缺口仅 N5 两项(均非验收场景直接要求)。`PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_svc_identity tests.test_svc_lifecycle_isolation tests.test_svc_precheck` → **80 tests, OK (skipped=1)**,唯一 skip 为真实 PG 专项显式 NOT_RUN(诚实标注)。✅
10. **秘密与越界**:diff 无明文口令/DSN/token(示例 env 仅受保护路径,测试仅 fixture 值 `ref-only`/`x`);changed files 中除 `AGENT-EXECUTION.md`(见 N6,已另行登记)外全部落在 U1 主要文件/allowed_paths 内;`.task/**` 为 host 直写豁免。✅
## 复现命令
```bash
# 1) 送审 diff 范围与实际 HEAD diff 一致(代码部分逐字相同)
git diff 955a32de2bb49171348bad5f566cce935df19212 HEAD \
-- AGENT-EXECUTION.md PROJECT.md deploy docs/superpowers/plans docs/svc.md scripts tests \
| diff - /tmp/container-storage-u1-review.diff && echo IDENTICAL
# 2) 单元测试(本机实测:Ran 80 tests, OK (skipped=1),约 64s)
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest \
tests.test_svc_identity tests.test_svc_lifecycle_isolation tests.test_svc_precheck
# 3) 对象 SHA
git rev-parse HEAD # e1f08650a12656611f27438f23fddbfab6a96299
git rev-parse 955a32de # 955a32de2bb49171348bad5f566cce935df19212
```
## 结论
U1 单元按方案完成且质量合格:绑定/身份权威唯一、拒绝路径完整且 fail-closed、验收场景全部落地并有测试背书。上述 N1—N8 均不阻塞本单元交付;N1/N2/N5 建议随 U2 或合入前收口,N6 的路由临时调整须按其自述在交付前回退。
+12
View File
@@ -866,9 +866,21 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
| `dbref` | agent 托管数据库绑定 `<pg名>/<库名>`(启动时由实例登记重建 DSN;见 §4.5) |
| `dsn` | agent 外部直连 DSN(受保护;未命中本机实例登记的直连存放于此) |
| `conffile` | agent 配置文件绝对路径(`<name> config` 命令写入,跨重启保留;见 §4.6) |
| `storage_mode` | `legacy`(缺省,SQLite/宿主 PG)/ `container`(三库+文件根合同;见 [deploy/data-ownership.md](../deploy/data-ownership.md)) |
| `env_class` | 环境身份:`prod` / `test` / `verify`(container 必需) |
| `files_root` | 持久文件根**绝对路径**(不相对 checkout 解析;container 必需) |
| `compose_project` | Compose 项目名(同 runtime 唯一;冲突在修改前拒绝) |
| `base_dbref` / `base_dsn_ref` / `base_role_ref` / `base_port` | 基座逻辑库绑定(dsn/role 仅为受保护文件路径引用) |
| `rdm_dbref` / `rdm_dsn_ref` / `rdm_role_ref` / `rdm_port` | RDM 领域库绑定 |
| `kb_dbref` / `kb_dsn_ref` / `kb_role_ref` / `kb_port` | 知识库绑定 |
| `user` / `password` / `dbname` / `authmode` / `pgdata` | postgres 专属 |
| `htdocs` | static 专属 |
容器存储绑定由 `manifest.json` 冻结为身份锚点(与 `data_root`/`db_*` 同级);旧配置
升级保留 `instance_id`。`storage_mode=container` 时缺任一必需绑定、错库、错文件根、
foreign marker、同名 Compose 或端口冲突均拒绝启动/修改,不静默建空库、不回退默认。
示例键见 [`deploy/env-bindings.example.env`](../deploy/env-bindings.example.env)。
### 10.2 history.jsonl 事件
一行一个 JSON:`ts / event / env / kind / source / result / pid / addr / detail`。
+1 -1
View File
@@ -3,7 +3,7 @@
{
"id": "rdm",
"name": "研发记忆 RDM",
"repo": "ssh://git@gitea.naitangbaobao.cn:52222/weichen/Agent-System-RDM.git",
"repo": "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-RDM.git",
"repoEnv": "SVC_RDM_ORIGIN",
"defaultRef": "origin/main",
"goModule": "agent-system-rdm",
-6
View File
@@ -36,8 +36,6 @@
"backend/hostapi backend/store",
"backend/ingest backend/vision",
"backend/instanceguard backend/store",
"backend/intentsession backend/intentgate",
"backend/intentsession backend/store",
"backend/pgbus backend/bus",
"backend/pgbus backend/store",
"backend/plugins backend/settings",
@@ -51,8 +49,6 @@
"backend/rag backend/settings",
"backend/rag backend/store",
"backend/route backend/intentgate",
"backend/rundiag backend/cmdbus",
"backend/rundiag backend/store",
"backend/runmgr backend/bus",
"backend/runmgr backend/cardsvc",
"backend/runmgr backend/docgen",
@@ -80,14 +76,12 @@
"backend/server backend/execenv",
"backend/server backend/ingest",
"backend/server backend/intentgate",
"backend/server backend/intentsession",
"backend/server backend/plugins",
"backend/server backend/profiles",
"backend/server backend/projectscope",
"backend/server backend/prompts",
"backend/server backend/ratelimit",
"backend/server backend/route",
"backend/server backend/rundiag",
"backend/server backend/runmgr",
"backend/server backend/settings",
"backend/server backend/store",
+3 -3
View File
@@ -15,9 +15,9 @@
| ID | 事实 | 验证命令 | last_verified | TTL(天) |
|---|---|---|---|---|
| F1 | Gitea Web/API 可达(2026-10-22 迁移后新权威):https://gitea.naitangbaobao.cn(443,1.27.3);旧地址 git-home…:30319 过渡期仍可达,随时退役 | `curl -fsS -m 10 https://gitea.naitangbaobao.cn/api/v1/version` | 2026-10-22 | 30 |
| F2 | API token 存在且可用(值在 ~/.secrets/gitea,HOME/IMC 两段各一组 `- ACCESS_TOKEN=…`,本条取 HOME 段;不入仓不回显,不进子进程 argv);在新权威 URL 实测 200 | `bash scripts/facts-verify.sh api-token https://gitea.naitangbaobao.cn/api/v1/user` | 2026-10-22 | 30 |
| F3 | Git over SSH 可达(新地址,端口 52222):ssh://git@gitea.naitangbaobao.cn:52222/weichen/&lt;Repo&gt;.git(key ~/.ssh/home_gitea);旧地址 git-ssh…:31411 迁移期 push 间歇失败,不再作为权威 | `GIT_SSH_COMMAND="ssh -i ~/.ssh/home_gitea -p 52222 -o BatchMode=yes" git ls-remote ssh://git@gitea.naitangbaobao.cn:52222/weichen/Development-Guidelines.git HEAD` | 2026-10-22 | 30 |
| F1 | Gitea Web/API 可达:https://git-home.u1680832.nyat.app:30319(1.27.3) | `curl -fsS -m 10 https://git-home.u1680832.nyat.app:30319/api/v1/version` | 2026-10-14 | 30 |
| F2 | API token 存在且可用(值在 ~/.secrets/gitea,HOME/IMC 两段各一组 `- ACCESS_TOKEN=…`,本条取 HOME 段;不入仓不回显,不进子进程 argv) | `bash scripts/facts-verify.sh api-token https://git-home.u1680832.nyat.app:30319/api/v1/user` | 2026-10-14 | 30 |
| F3 | Git over SSH 可达:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/&lt;Repo&gt;.git(key ~/.ssh/home_gitea) | `GIT_SSH_COMMAND="ssh -i ~/.ssh/home_gitea -o BatchMode=yes" git ls-remote ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Development-Guidelines.git HEAD` | 2026-10-14 | 30 |
| F4 | 旧端点 git-panel.u1680832.nyat.app:30319 TCP 可连通但 TLS 握手失败(仅 TLS 层握手错误判 PASS) | `bash scripts/facts-verify.sh tls-handshake-fails git-panel.u1680832.nyat.app 30319` | 2026-10-14 | 180 |
| F5 | 基座 users 表存有姓名(name 列,CreateAccount/ListUsersAdmin 读写;EXT-19 目录自 P3 起向授权管理读面下发) | `cd modules/backend && go test ./internal/app/ -run TestInprocHostSearchAccounts -count=1` | 2026-10-14 | 180 |
| F6 | 基座账号无停用位(users 表无 disabled;EXT-19 Valid 恒为存在性)——停用机制未实现,不得当已具备 | `cd modules/backend && ! grep -rn -e disabled -e Disabled internal/store/auth.go pkg/extension/extension.go` | 2026-10-14 | 180 |
+3 -6
View File
@@ -60,9 +60,8 @@ done
# ── 1. 构建 ─────────────────────────────────────────────
# RDM 源码经 git archive 从本地对象导出(不建 worktree、不触远端——Gitea 抖动时
# submodule 克隆会挂死);pins/* 从 RDM 主检出的已物化 submodule 快照拷贝(tar 排除
# .git;含 go.mod replace 引用的全部 pin,不仅 backend),构建输入由此固定为
# RDM origin/main + 其 gitlink 指向的本地内容。
# submodule 克隆会挂死);pins/* 从 RDM 主检出的已物化 submodule 快照拷贝(cp 排除
# .git,39MB 级),构建输入由此固定为 RDM origin/main + 其 gitlink 指向的本地内容。
log "build RDM binary (archive origin/main + snapshot pins from local checkout)"
RDM_WT="$OUT/rdm-src"
RDM_SHA=$(git -C "$RDM_REPO" rev-parse origin/main) || die "no origin/main in RDM repo"
@@ -76,9 +75,7 @@ PIN_SHA_WANT=$(git -C "$RDM_REPO" ls-tree "$RDM_SHA" pins/agent-system-backend |
[ "$PIN_SHA_MAIN" = "$PIN_SHA_WANT" ] \
|| die "RDM checkout pins/backend ($PIN_SHA_MAIN) != origin/main pin ($PIN_SHA_WANT); git -C $RDM_REPO submodule update first"
mkdir -p "$RDM_WT/pins"
# 快照全部 pins/*(go.mod replace 依赖 agent-document-renderer、agent-squad 等,
# 仅拷贝 backend 会导致 RDM 构建缺 pin);--exclude=.git 保持不变。
tar -C "$RDM_REPO/pins" --exclude=.git -cf - . | tar -C "$RDM_WT/pins" -xf -
tar -C "$RDM_REPO/pins" --exclude=.git -cf - agent-system-backend | tar -C "$RDM_WT/pins" -xf -
cleanup() {
[ -n "${API_PID:-}" ] && { kill "$API_PID" 2>/dev/null || true; wait "$API_PID" 2>/dev/null || true; }
# Preview runs directly as one owned Vite process. Verify its command before
+2 -2
View File
@@ -4,7 +4,7 @@
# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回)
# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。
# 登记表引用的探针子命令(单独运行时在 stderr 说明判定,不输出凭据):
# api-token URL URL 须逐字等于登记端点 https://gitea.naitangbaobao.cn/api/v1/user,
# api-token URL URL 须逐字等于登记端点 https://git-home.u1680832.nyat.app:30319/api/v1/user,
# 校验通过后才进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET(不跟随重定向);
# 0=接受 3=缺失 4=格式不符 5=请求失败/被拒/重定向 6=非登记端点
# tls-handshake-fails HOST PORT 仅当 TCP 已连通且 TLS 层握手报错时 0;
@@ -20,7 +20,7 @@ import sys, urllib.request
sys.path.insert(0, sys.argv[1])
import task_triage
ENDPOINT = "https://gitea.naitangbaobao.cn/api/v1/user"
ENDPOINT = "https://git-home.u1680832.nyat.app:30319/api/v1/user"
if sys.argv[2] != ENDPOINT:
print("api-token: URL 不是 policy/facts.md F2 登记端点,拒绝发送凭据", file=sys.stderr)
sys.exit(6)
+104 -11
View File
@@ -144,7 +144,7 @@ SRC_DIR="${SVC_SRC_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-svc/src}"
# 独立 RDM 源仓(下游组合仓 Agent-System-RDM 的干净克隆;SVC_RDM_SRC_DIR 覆盖路径,
# SVC_RDM_ORIGIN 覆盖克隆来源)。rdm 源的解析/列举/物化全部基于它,语义与 SRC_DIR 一致。
RDM_SRC_DIR="${SVC_RDM_SRC_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-svc/rdm-src}"
RDM_ORIGIN_DEFAULT="ssh://git@gitea.naitangbaobao.cn:52222/weichen/Agent-System-RDM.git"
RDM_ORIGIN_DEFAULT="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-RDM.git"
# 提示信息走 stderr:stdout 只留给数据(提交表/服务表/DSN),便于 $( ) 捕获组合。
c_info() { [ "${CLI_QUIET:-0}" = 1 ] && return 0; printf '\033[1;34m[svc]\033[0m %s\n' "$*" >&2; }
@@ -1012,8 +1012,9 @@ record_state() {
chmod 600 "$tmp"
# U1 身份:manifest 治理下的整份提交(bulk-set 走 temp→fsync→rename 原子链,
# revision 递增,svc.env 由 manifest 派生);失败保留旧配置,不产生半写态。
# 存储绑定键(STORAGE_KEYS)由 manifest bulk-set 自旧 kv 保留,避免生命周期写回冲掉。
if manifest_ok; then
if python3 "$MANIFEST_PY" "$ENV_DIR" bulk-set < "$tmp"; then
if python3 "$MANIFEST_PY" "$ENV_DIR" bulk-set --runtime "$RUNTIME" < "$tmp"; then
rm -f "$tmp"
return 0
fi
@@ -1217,18 +1218,103 @@ svc_manifest_migrate() {
c_err "[$ENV_NAME] manifest 迁移失败(保持旧治理):$(printf '%s' "$out" | tail -1)"
return 0
}
# resolve_storage_bindings:从本实例 manifest 权威解析容器存储绑定(不读调用方环境)。
# 结果全局:STORAGE_MODE ENV_CLASS FILES_ROOT COMPOSE_PROJECT
# BASE_DB_REF RDM_DB_REF KB_DB_REF BASE_PORT RDM_PORT KB_PORT
# storage_mode=container 且缺任一必需绑定时返回 1(拒绝静默默认/建空库);
# legacy / 未配置返回 0 且 STORAGE_MODE=legacy。
resolve_storage_bindings() {
STORAGE_MODE=legacy; ENV_CLASS=""; FILES_ROOT=""; COMPOSE_PROJECT=""
BASE_DB_REF=""; RDM_DB_REF=""; KB_DB_REF=""
BASE_PORT=""; RDM_PORT=""; KB_PORT=""
manifest_ok || return 0
local out rc=0
out="$(python3 - "$ENV_DIR" <<'PY' 2>&1
import json, os, sys
env_dir = sys.argv[1]
path = os.path.join(env_dir, "manifest.json")
with open(path, encoding="utf-8") as f:
m = json.load(f)
ident = m.get("identity") or {}
kv = m.get("kv") or {}
mode = (kv.get("storage_mode") or ident.get("storage_mode") or "legacy").strip() or "legacy"
print("MODE=%s" % mode)
print("ENV_CLASS=%s" % (kv.get("env_class") or ident.get("env_class") or ""))
print("FILES_ROOT=%s" % (kv.get("files_root") or ident.get("files_root") or ""))
print("COMPOSE_PROJECT=%s" % (kv.get("compose_project") or ident.get("compose_project") or ""))
print("BASE_DB_REF=%s" % (ident.get("base_db_ref") or ""))
print("RDM_DB_REF=%s" % (ident.get("rdm_db_ref") or ""))
print("KB_DB_REF=%s" % (ident.get("kb_db_ref") or ""))
print("BASE_PORT=%s" % (kv.get("base_port") or ident.get("base_port") or ""))
print("RDM_PORT=%s" % (kv.get("rdm_port") or ident.get("rdm_port") or ""))
print("KB_PORT=%s" % (kv.get("kb_port") or ident.get("kb_port") or ""))
if mode == "container":
missing = []
if (kv.get("env_class") or ident.get("env_class") or "") not in ("prod", "test", "verify"):
missing.append("env_class")
fr = kv.get("files_root") or ident.get("files_root") or ""
if not fr or not os.path.isabs(fr):
missing.append("files_root")
if not (kv.get("compose_project") or ident.get("compose_project") or ""):
missing.append("compose_project")
for slot, key in (("base", "base_db_ref"), ("rdm", "rdm_db_ref"), ("kb", "kb_db_ref")):
if not (ident.get(key) or ""):
missing.append(slot + "_db")
if not (kv.get(slot + "_port") or ident.get(slot + "_port") or ""):
missing.append(slot + "_port")
if not (kv.get(slot + "_role_ref") or ""):
missing.append(slot + "_role_ref")
if missing:
print("MISSING=%s" % ",".join(missing), file=sys.stderr)
sys.exit(3)
PY
)" || rc=$?
if [ "$rc" -ne 0 ]; then
c_err "[$ENV_NAME] 存储绑定不完整或无效,拒绝(不静默新建空库/回退默认):$(printf '%s' "$out" | tail -1)"
return 1
fi
local line
while IFS= read -r line; do
case "$line" in
MODE=*) STORAGE_MODE="${line#MODE=}" ;;
ENV_CLASS=*) ENV_CLASS="${line#ENV_CLASS=}" ;;
FILES_ROOT=*) FILES_ROOT="${line#FILES_ROOT=}" ;;
COMPOSE_PROJECT=*) COMPOSE_PROJECT="${line#COMPOSE_PROJECT=}" ;;
BASE_DB_REF=*) BASE_DB_REF="${line#BASE_DB_REF=}" ;;
RDM_DB_REF=*) RDM_DB_REF="${line#RDM_DB_REF=}" ;;
KB_DB_REF=*) KB_DB_REF="${line#KB_DB_REF=}" ;;
BASE_PORT=*) BASE_PORT="${line#BASE_PORT=}" ;;
RDM_PORT=*) RDM_PORT="${line#RDM_PORT=}" ;;
KB_PORT=*) KB_PORT="${line#KB_PORT=}" ;;
esac
done <<<"$out"
return 0
}
# svc_manifest_verify:启动前身份校验——数据 marker 存在且 instance_id 匹配;
# 生效 --data / 数据库绑定与冻结锚点一致。失败即拒绝启动(KTD1/KTD2,I4)。
# 顺序:文件 marker → DB verify(缺/错即拦)。ensure 只在领养(svc_manifest_migrate)
# 与显式 init——普通启动不建身份,缺行即事件(修复路径:svc init,或换具备
# identity 能力的产物更新时由候选门禁补齐)。
# 顺序:文件 marker →(container)存储绑定完整/无漂移 → DB verify(缺/错即拦)。
# ensure 只在领养(svc_manifest_migrate)与显式 init——普通启动不建身份,缺行即事件。
svc_manifest_verify() {
manifest_ok || return 0
local out
if ! out="$(python3 "$MANIFEST_PY" "$ENV_DIR" verify \
--data "$ENV_DATA" --db-mode "${DB_MODE:-}" --db-ref "${DB_REF:-}" 2>&1)"; then
local out verify_args storage_mode
verify_args=(--data "$ENV_DATA" --db-mode "${DB_MODE:-}" --db-ref "${DB_REF:-}")
storage_mode="$(svc_get storage_mode)"
if [ "$storage_mode" = container ]; then
# 生效值只来自本实例登记(env_config 已剥离调用方污染);缺绑/错根/错库拒绝。
resolve_storage_bindings || return 1
verify_args+=(--require-storage
--files-root "$FILES_ROOT" --env-class "$ENV_CLASS"
--compose-project "$COMPOSE_PROJECT"
--base-db-ref "$BASE_DB_REF" --rdm-db-ref "$RDM_DB_REF" --kb-db-ref "$KB_DB_REF")
python3 "$MANIFEST_PY" "$ENV_DIR" check-conflicts --runtime "$RUNTIME" >/dev/null || {
c_err "[$ENV_NAME] 存储绑定冲突,拒绝启动(Compose 项目名或端口与他实例冲突)"
return 1
}
fi
if ! out="$(python3 "$MANIFEST_PY" "$ENV_DIR" verify "${verify_args[@]}" 2>&1)"; then
c_err "[$ENV_NAME] 身份校验失败,拒绝启动:$(printf '%s' "$out" | tail -1)"
c_err " 数据根/数据库绑定是冻结身份;换根/重绑须显式迁移或登记命令(docs/recovery/u1-identity-manifest.md §5)"
c_err " 数据根/数据库/文件根绑定是冻结身份;换根/重绑须显式迁移或登记命令(docs/recovery/u1-identity-manifest.md §5;容器存储见 deploy/data-ownership.md)"
return 1
fi
# 文件校验通过后再碰库。fresh 允许库尚未建立;established 只验证;
@@ -1704,12 +1790,18 @@ env_config() {
# update --all:dev 成功后 genui/rdm1/rdmcand 端口仍被孤儿占用)。
SVC_DB_MODE=""; SVC_DB_SOURCE=""; SVC_DB_REF=""; SVC_DB_DSN_SAVE=""
SVC_PIDCHECK=""; SVC_EXEC=""; SVC_VERSION=""; SVC_SOURCE=""
STORAGE_MODE=""; ENV_CLASS=""; FILES_ROOT=""; COMPOSE_PROJECT=""
BASE_DB_REF=""; RDM_DB_REF=""; KB_DB_REF=""
BASE_PORT=""; RDM_PORT=""; KB_PORT=""
# U1:身份环境同样按实例重置——上一服务 verify 成功后导出的
# AGENT_SYSTEM_INSTANCE_ID/DBREF 不得串给下一服务(批量 update 单进程逐服务解析;
# 2026-10-12 实况:dev 的身份漏给 rdm1/rdmcand,叠加 RDM 产物无 identity 子命令
# 跳过校验,serve 带外宾身份即被基座门禁拒绝)。serve 只允许拿到本服务 verify
# 写入的身份。
# 容器存储 U1:调用方/上一服务的文件根、环境身份、Compose 与三库引用不得污染本实例。
unset AGENT_SYSTEM_INSTANCE_ID AGENT_SYSTEM_DBREF
unset AGENT_SYSTEM_FILES_ROOT AGENT_SYSTEM_ENV_CLASS AGENT_SYSTEM_COMPOSE_PROJECT
unset AGENT_SYSTEM_BASE_DB_REF AGENT_SYSTEM_RDM_DB_REF AGENT_SYSTEM_KB_DB_REF
local registered=""
[ -f "$ENV_DIR/svc.env" ] && registered="$(svc_kind "$name")"
SVC_KIND="${registered:-agent}"
@@ -4578,10 +4670,11 @@ check_port_available() {
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键。manifest 治理下经 manifest
# 整份提交(原子 + revision);legacy 路径保持先读后写 + 临时文件原子替换。
# 存储绑定键变更经 --runtime 做 Compose/端口冲突预检(修改前失败)。
svc_set_key() {
local key="$1" val="$2" rest tmp
if manifest_ok; then
python3 "$MANIFEST_PY" "$ENV_DIR" set "$key" "$val" && return 0
python3 "$MANIFEST_PY" "$ENV_DIR" set --runtime "$RUNTIME" "$key" "$val" && return 0
c_warn "[$ENV_NAME] manifest set 失败,回退 legacy 单键写"
fi
[ -f "$ENV_DIR/svc.env" ] || { c_err "svc.env 不存在:$ENV_DIR"; return 1; }
@@ -4598,7 +4691,7 @@ svc_set_key() {
svc_del_key() {
local key="$1"
if manifest_ok; then
python3 "$MANIFEST_PY" "$ENV_DIR" del "$key" && return 0
python3 "$MANIFEST_PY" "$ENV_DIR" del --runtime "$RUNTIME" "$key" && return 0
c_warn "[$ENV_NAME] manifest del 失败,回退 legacy 单键写"
fi
[ -f "$ENV_DIR/svc.env" ] || return 0
+399 -36
View File
@@ -1,16 +1,19 @@
#!/usr/bin/env python3
"""svc 实例身份 manifest(U1):manifest.json 为启动权威,svc.env 为派生只读视图。
设计合同:docs/recovery/u1-identity-manifest.md(已批准 2026-10-12)。
设计合同:docs/recovery/u1-identity-manifest.md(已批准 2026-10-12);
容器化存储扩展:docs/superpowers/plans/2026-10-22-container-storage-plan.md U1/KTD1—KTD4。
落地说明:v1.0 以「identity 锚点(数据根/数据库身份,冻结)+ kv 权威键值映射 +
desired 期望态」承载 KTD1/KTD2 的全部不变量;完整 typed 分区随 U2/U6 演进。
desired 期望态」承载不变量;本文件在同一权威上扩展三库/文件根/环境身份绑定,
不另造第二份登记权威。完整 typed 分区与容器启停属后续单元。
用法: svc_manifest.py ENV_DIR <migrate|init|status|set|del|bulk-set|set-desired|verify> [参数]
用法: svc_manifest.py ENV_DIR <migrate|init|status|set|del|bulk-set|set-desired|verify|check-conflicts> [参数]
不变量(对应设计 §1):
不变量(对应设计 §1 + 容器存储 U1):
I3 原子性 —— 整份 temp→fsync→rename→dirsync,任意时刻只有完整旧版或新版。
I4 阻塞 —— manifest 存在时,数据 marker 缺失/身份不符即拒绝,不新建空系统。
I5 机密 —— 本文件不新增任何机密;kv 沿袭 svc.env 既有内容(DSN 迁出属 U6)。
I5 机密 —— 凭据只存受保护文件路径引用(*_dsn_ref / *_role_ref),不存明文。
"""
import json
import os
@@ -22,7 +25,27 @@ SCHEMA = 1
MANIFEST = "manifest.json"
DERIVED = "svc.env"
MARKER_REL = os.path.join(".agentx", "data-marker.json")
PROTECTED = ("db", "dbref", "dsn", "conffile") # 显式持久化写入可重锚;启动期漂移由 verify 拦截
# 显式持久化写入可重锚;启动期漂移由 verify 拦截。含容器存储绑定键。
PROTECTED = (
"db", "dbref", "dsn", "conffile",
"storage_mode", "env_class", "files_root", "compose_project",
"base_dbref", "base_dsn_ref", "base_role_ref", "base_port",
"rdm_dbref", "rdm_dsn_ref", "rdm_role_ref", "rdm_port",
"kb_dbref", "kb_dsn_ref", "kb_role_ref", "kb_port",
)
# record_state/bulk-set 重建时须保留的存储绑定键(避免被生命周期写回冲掉)。
STORAGE_KEYS = (
"storage_mode", "env_class", "files_root", "compose_project",
"base_dbref", "base_dsn_ref", "base_role_ref", "base_port",
"rdm_dbref", "rdm_dsn_ref", "rdm_role_ref", "rdm_port",
"kb_dbref", "kb_dsn_ref", "kb_role_ref", "kb_port",
)
ENV_CLASSES = ("prod", "test", "verify")
DB_SLOTS = ("base", "rdm", "kb")
STORAGE_MODES = ("legacy", "container")
def die(msg, code=1):
@@ -116,6 +139,144 @@ def db_anchor_of(kv):
return "", ""
def storage_mode_of(kv):
mode = (kv.get("storage_mode") or "").strip() or "legacy"
if mode not in STORAGE_MODES:
die("storage_mode 仅允许 legacy|container,收到:%s" % mode, 2)
return mode
def files_root_of(kv):
"""持久文件根:必须已是绝对路径;绝不相对 checkout/worktree 解析。"""
fr = (kv.get("files_root") or "").strip()
if not fr:
return ""
if not os.path.isabs(fr):
die("files_root 必须是绝对路径(不相对 checkout 解析):%s" % fr, 2)
return os.path.normpath(fr)
def slot_ref_of(kv, slot):
"""逻辑库引用锚点:优先 dbref,否则 dsn_ref(受保护文件路径);明文 dsn 禁止。"""
dbref = (kv.get("%s_dbref" % slot) or "").strip()
dsn_ref = (kv.get("%s_dsn_ref" % slot) or "").strip()
if dbref and dsn_ref:
die("%s 绑定冲突:不可同时设置 %s_dbref 与 %s_dsn_ref" % (slot, slot, slot), 2)
if dbref:
return "dbref:%s" % dbref
if dsn_ref:
if not os.path.isabs(dsn_ref):
die("%s_dsn_ref 必须是绝对路径(秘密文件引用):%s" % (slot, dsn_ref), 2)
return "dsn_ref:%s" % os.path.normpath(dsn_ref)
# 基座可回落既有 dbref/dsn 专键(legacy→container 升级兼容,dsn 明文不进锚点)
if slot == "base":
legacy = (kv.get("dbref") or "").strip()
if legacy:
return "dbref:%s" % legacy
# 明文 dsn= 不是引用;container 模式不得用其充当绑定
return ""
def slot_port_of(kv, slot):
return (kv.get("%s_port" % slot) or "").strip()
def slot_role_ref_of(kv, slot):
role = (kv.get("%s_role_ref" % slot) or "").strip()
if not role:
return ""
if not os.path.isabs(role):
die("%s_role_ref 必须是绝对路径(角色秘密文件引用):%s" % (slot, role), 2)
return os.path.normpath(role)
def validate_storage_kv(kv, require_complete=None):
"""校验存储绑定形态。container 模式缺任一必需绑定即拒绝(不静默默认/建空库)。"""
mode = storage_mode_of(kv)
if require_complete is None:
require_complete = mode == "container"
env_class = (kv.get("env_class") or "").strip()
if env_class and env_class not in ENV_CLASSES:
die("env_class 仅允许 prod|test|verify,收到:%s" % env_class, 2)
files_root = files_root_of(kv) # 相对路径在此失败
compose = (kv.get("compose_project") or "").strip()
if compose and (("/" in compose) or (".." in compose) or (" " in compose)):
die("compose_project 非法(禁止路径分隔/空格):%s" % compose, 2)
for slot in DB_SLOTS:
port = slot_port_of(kv, slot)
if port and not port.isdigit():
die("%s_port 必须是正整端口号,收到:%s" % (slot, port), 2)
slot_ref_of(kv, slot) # 冲突/相对路径检查
slot_role_ref_of(kv, slot)
if not require_complete:
return mode
# container 完整合同
if mode != "container":
die("完整存储合同要求 storage_mode=container", 2)
if env_class not in ENV_CLASSES:
die("container 模式缺少必需绑定 env_class(prod|test|verify)", 3)
if not files_root:
die("container 模式缺少必需绑定 files_root(绝对路径)", 3)
if not compose:
die("container 模式缺少必需绑定 compose_project", 3)
for slot in DB_SLOTS:
ref = slot_ref_of(kv, slot)
port = slot_port_of(kv, slot)
role = slot_role_ref_of(kv, slot)
if not ref:
die("container 模式缺少必需绑定 %s 库(%s_dbref 或 %s_dsn_ref)"
% (slot, slot, slot), 3)
if not port:
die("container 模式缺少必需绑定 %s_port" % slot, 3)
if not role:
die("container 模式缺少必需绑定 %s_role_ref(角色秘密文件路径)" % slot, 3)
return mode
def storage_anchor_of(kv):
"""从 kv 计算冻结的存储身份锚点(写入 identity;legacy 为空串)。"""
mode = storage_mode_of(kv)
if mode == "legacy" and not any(kv.get(k) for k in STORAGE_KEYS if k != "storage_mode"):
return {
"storage_mode": "legacy",
"env_class": "",
"files_root": "",
"compose_project": "",
"base_db_ref": "",
"base_port": "",
"rdm_db_ref": "",
"rdm_port": "",
"kb_db_ref": "",
"kb_port": "",
}
# 非完整 container 时仍记录已显式写入的锚点(供漂移检测);完整校验另走 validate
if mode == "container":
validate_storage_kv(kv, require_complete=True)
else:
validate_storage_kv(kv, require_complete=False)
return {
"storage_mode": mode,
"env_class": (kv.get("env_class") or "").strip(),
"files_root": files_root_of(kv),
"compose_project": (kv.get("compose_project") or "").strip(),
"base_db_ref": slot_ref_of(kv, "base"),
"base_port": slot_port_of(kv, "base"),
"rdm_db_ref": slot_ref_of(kv, "rdm"),
"rdm_port": slot_port_of(kv, "rdm"),
"kb_db_ref": slot_ref_of(kv, "kb"),
"kb_port": slot_port_of(kv, "kb"),
}
def preserve_storage_keys(new_kv, prev_kv):
"""bulk-set/生命周期写回时保留未出现在新写集中的存储绑定键。"""
out = dict(new_kv)
for k in STORAGE_KEYS:
if k not in out and k in prev_kv:
out[k] = prev_kv[k]
return out
def marker_path(data_root):
return os.path.join(data_root, MARKER_REL)
@@ -140,13 +301,18 @@ def read_marker(data_root):
return {"schema": -1}
def reanchor(env_dir, m):
"""按显式持久化写入重算身份锚点;数据根变化时在新根补写 marker(目录存在才写)。"""
def reanchor(env_dir, m, write=True):
"""按显式持久化写入重算身份锚点;数据根变化时在新根补写 marker(目录存在才写)。
write=False 仅预览身份(冲突扫描用),不落 marker。
"""
ident = dict(m["identity"])
ident["data_root"] = data_root_of(m["kv"], env_dir)
ident["db_mode"], ident["db_ref"] = db_anchor_of(m["kv"])
for k, v in storage_anchor_of(m["kv"]).items():
ident[k] = v
if ident != m["identity"]:
if os.path.isdir(ident["data_root"]):
if write and os.path.isdir(ident["data_root"]):
write_marker(ident["data_root"], m["instance_id"])
m["identity"] = ident
return m
@@ -160,6 +326,9 @@ def build(env_dir, kv, allow_create):
if not allow_create:
die("数据根不存在且非显式 init,拒绝建立身份(防新建空系统掩盖丢数据):%s" % data_root, 3)
os.makedirs(data_root, exist_ok=True)
# 旧配置升级:若已带 storage 键则校验;纯 legacy 不强制 container 合同
if storage_mode_of(kv) == "container" or any(kv.get(k) for k in STORAGE_KEYS):
validate_storage_kv(kv, require_complete=(storage_mode_of(kv) == "container"))
existing = read_marker(data_root)
if existing is not None:
if existing.get("instance_id") != kv.get("instance_id"):
@@ -170,6 +339,14 @@ def build(env_dir, kv, allow_create):
instance_id = kv.get("instance_id") or "svc-%s-%s" % (name, secrets.token_hex(6))
write_marker(data_root, instance_id)
db_mode, db_ref = db_anchor_of(kv)
storage = storage_anchor_of(kv)
identity = {
"data_root": data_root,
"db_mode": db_mode,
"db_ref": db_ref,
"db_marker": "fresh",
}
identity.update(storage)
m = {
"schema": SCHEMA,
"revision": 1,
@@ -177,18 +354,114 @@ def build(env_dir, kv, allow_create):
"service_group": name,
"created_at": now_iso(),
"desired": "running" if kv.get("autostart") == "1" else "stopped",
"identity": {
"data_root": data_root,
"db_mode": db_mode,
"db_ref": db_ref,
"db_marker": "fresh",
},
"identity": identity,
"kv": {k: v for k, v in kv.items() if k != "autostart"},
}
save(env_dir, m)
return m
def runtime_dir_of(env_dir):
return os.path.dirname(os.path.abspath(env_dir))
def iter_sibling_manifests(env_dir, runtime_dir=None):
rt = os.path.abspath(runtime_dir) if runtime_dir else runtime_dir_of(env_dir)
self_abs = os.path.abspath(env_dir)
if not os.path.isdir(rt):
return
for name in sorted(os.listdir(rt)):
d = os.path.join(rt, name)
if not os.path.isdir(d) or os.path.abspath(d) == self_abs:
continue
path = os.path.join(d, MANIFEST)
if not os.path.isfile(path):
# 兼容尚未 migrate 的 sibling:读 svc.env 中的存储键
env_path = os.path.join(d, DERIVED)
if not os.path.isfile(env_path):
continue
kv = {}
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.rstrip("\n")
if line and "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k] = v
yield name, {
"instance_id": kv.get("instance_id", ""),
"identity": storage_anchor_of(kv) if any(kv.get(k) for k in STORAGE_KEYS) else {},
"kv": kv,
}
continue
try:
with open(path, encoding="utf-8") as f:
yield name, json.load(f)
except Exception: # noqa: BLE001 —— 损坏 sibling 不阻断本实例冲突扫描语义以外的操作
continue
def binding_ports_from(m_or_kv, is_manifest=True):
if is_manifest:
kv = m_or_kv.get("kv") or {}
ident = m_or_kv.get("identity") or {}
else:
kv = m_or_kv
ident = {}
ports = set()
for slot in DB_SLOTS:
p = (kv.get("%s_port" % slot) or "").strip() or (ident.get("%s_port" % slot) or "").strip()
if p:
ports.add(p)
return ports
def compose_of(m_or_kv, is_manifest=True):
if is_manifest:
kv = m_or_kv.get("kv") or {}
ident = m_or_kv.get("identity") or {}
return (kv.get("compose_project") or ident.get("compose_project") or "").strip()
return (m_or_kv.get("compose_project") or "").strip()
def check_conflicts(env_dir, m, runtime_dir=None):
"""同一 runtime 下 Compose 项目名或存储端口冲突 → 修改前失败。"""
kv = m["kv"]
ident = m.get("identity") or {}
compose = (kv.get("compose_project") or ident.get("compose_project") or "").strip()
ports = binding_ports_from(m, is_manifest=True)
if not compose and not ports:
return
self_name = os.path.basename(os.path.abspath(env_dir))
for name, other in iter_sibling_manifests(env_dir, runtime_dir):
other_compose = compose_of(other, is_manifest=True)
if compose and other_compose and compose == other_compose:
die("Compose 项目名冲突:compose_project=%s 已被实例 %s 占用(修改前拒绝)"
% (compose, name), 7)
other_ports = binding_ports_from(other, is_manifest=True)
clash = ports & other_ports
if clash:
die("存储端口冲突:port=%s 与实例 %s 冲突(修改前拒绝)"
% (",".join(sorted(clash)), name), 7)
# 亦与 sibling 的应用监听 addr 端口冲突(避免与已登记服务抢口)
if ports:
rt = os.path.abspath(runtime_dir) if runtime_dir else runtime_dir_of(env_dir)
for name in sorted(os.listdir(rt)) if os.path.isdir(rt) else []:
if name == self_name:
continue
env_path = os.path.join(rt, name, DERIVED)
if not os.path.isfile(env_path):
continue
with open(env_path, encoding="utf-8") as f:
for line in f:
if line.startswith("addr="):
addr = line[5:].strip()
ap = addr.rsplit(":", 1)[-1] if addr else ""
if ap in ports:
die("存储端口冲突:port=%s 与服务 %s 的 addr=%s 冲突(修改前拒绝)"
% (ap, name, addr), 7)
break
def cmd_migrate(env_dir):
if load(env_dir) is not None:
print("manifest 已存在,幂等跳过")
@@ -221,32 +494,41 @@ def cmd_status(env_dir):
return 0
def mutate(env_dir, fn):
def mutate(env_dir, fn, runtime_dir=None):
m = load(env_dir)
if m is None:
die("manifest 不存在:先 migrate/init", 3)
before = json.dumps(m["identity"], sort_keys=True)
fn(m)
# 存储绑定变更:先形态校验,再冲突扫描,最后重锚——冲突时不落盘
if any(k in m["kv"] for k in STORAGE_KEYS) or storage_mode_of(m["kv"]) == "container":
validate_storage_kv(m["kv"], require_complete=(storage_mode_of(m["kv"]) == "container"))
# 预览重锚后的 identity 以做冲突检测(不落 marker / 不写盘)
preview = json.loads(json.dumps(m))
preview = reanchor(env_dir, preview, write=False)
check_conflicts(env_dir, preview, runtime_dir=runtime_dir)
m["revision"] = int(m["revision"]) + 1
m = reanchor(env_dir, m)
m = reanchor(env_dir, m, write=True)
save(env_dir, m)
if before != json.dumps(m["identity"], sort_keys=True):
print("[manifest] 身份锚点已随显式持久化写入更新(data=%s db=%s/%s)"
% (m["identity"]["data_root"], m["identity"]["db_mode"], m["identity"]["db_ref"]))
print("[manifest] 身份锚点已随显式持久化写入更新(data=%s db=%s/%s storage=%s files=%s)"
% (m["identity"]["data_root"], m["identity"]["db_mode"], m["identity"]["db_ref"],
m["identity"].get("storage_mode", "legacy"),
m["identity"].get("files_root") or "-"))
return 0
def cmd_set(env_dir, key, value):
return mutate(env_dir, lambda m: m["kv"].__setitem__(key, value))
def cmd_set(env_dir, key, value, runtime_dir=None):
return mutate(env_dir, lambda m: m["kv"].__setitem__(key, value), runtime_dir=runtime_dir)
def cmd_del(env_dir, key):
def cmd_del(env_dir, key, runtime_dir=None):
def drop(m):
m["kv"].pop(key, None)
return mutate(env_dir, drop)
return mutate(env_dir, drop, runtime_dir=runtime_dir)
def cmd_bulk_set(env_dir):
def cmd_bulk_set(env_dir, runtime_dir=None):
kv = {}
for line in sys.stdin.read().splitlines():
if line and "=" in line and not line.startswith("#"):
@@ -255,8 +537,8 @@ def cmd_bulk_set(env_dir):
kv.pop("autostart", None) # desired 独占期望态
def replace(m):
m["kv"] = kv
return mutate(env_dir, replace)
m["kv"] = preserve_storage_keys(kv, m["kv"])
return mutate(env_dir, replace, runtime_dir=runtime_dir)
def cmd_set_desired(env_dir, state):
@@ -265,7 +547,20 @@ def cmd_set_desired(env_dir, state):
return mutate(env_dir, lambda m: m.__setitem__("desired", state))
def cmd_verify(env_dir, data_root=None, db_mode=None, db_ref=None):
def cmd_check_conflicts(env_dir, runtime_dir=None):
m = load(env_dir)
if m is None:
die("manifest 不存在:先 migrate/init", 3)
check_conflicts(env_dir, m, runtime_dir=runtime_dir)
print("绑定冲突检查通过 instance_id=%s compose=%s"
% (m["instance_id"], m["identity"].get("compose_project") or "-"))
return 0
def cmd_verify(env_dir, data_root=None, db_mode=None, db_ref=None,
files_root=None, env_class=None, compose_project=None,
base_db_ref=None, rdm_db_ref=None, kb_db_ref=None,
require_storage=False):
m = load(env_dir)
if m is None:
die("manifest 缺失:本实例已进入 manifest 治理,缺失即事件(先 init/人工处置,禁止静默重建)", 4)
@@ -284,14 +579,51 @@ def cmd_verify(env_dir, data_root=None, db_mode=None, db_ref=None):
die("数据库模式漂移:生效 %s ≠ 锚定 %s" % (db_mode, ident["db_mode"]), 6)
if db_mode and ident["db_mode"] and db_ref is not None and (db_ref or "") != ident["db_ref"]:
die("数据库绑定漂移:生效 %s ≠ 锚定 %s(重绑须经显式登记命令)" % (db_ref or "<空>", ident["db_ref"]), 6)
print("身份校验通过 instance_id=%s data=%s db=%s/%s"
% (m["instance_id"], ident["data_root"], ident["db_mode"] or "-", ident["db_ref"] or "-"))
storage_mode = ident.get("storage_mode") or "legacy"
if storage_mode == "container" or require_storage:
# container:缺绑定/错根/错库一律拒绝,不回退默认、不建空库
validate_storage_kv(m["kv"], require_complete=True)
if files_root is not None:
got = os.path.normpath(files_root) if os.path.isabs(files_root) else files_root
if not os.path.isabs(files_root):
die("文件根拒绝相对路径(不相对 checkout 解析):%s" % files_root, 6)
if got != (ident.get("files_root") or ""):
die("文件根漂移:生效 %s ≠ 锚定 %s" % (got, ident.get("files_root")), 6)
if env_class is not None and env_class != (ident.get("env_class") or ""):
die("环境身份漂移:生效 %s ≠ 锚定 %s" % (env_class, ident.get("env_class")), 6)
if compose_project is not None and compose_project != (ident.get("compose_project") or ""):
die("Compose 项目漂移:生效 %s ≠ 锚定 %s" % (compose_project, ident.get("compose_project")), 6)
for slot, got in (("base", base_db_ref), ("rdm", rdm_db_ref), ("kb", kb_db_ref)):
if got is None:
continue
want = ident.get("%s_db_ref" % slot) or ""
if (got or "") != want:
die("%s 库绑定漂移:生效 %s ≠ 锚定 %s(错库拒绝,不静默新建)"
% (slot, got or "<空>", want or "<空>"), 6)
print("身份校验通过 instance_id=%s data=%s db=%s/%s storage=%s env=%s files=%s"
% (m["instance_id"], ident["data_root"], ident["db_mode"] or "-", ident["db_ref"] or "-",
storage_mode, ident.get("env_class") or "-", ident.get("files_root") or "-"))
return 0
def _parse_runtime_flag(args):
runtime_dir = None
out = []
it = iter(args)
for a in it:
if a == "--runtime":
runtime_dir = next(it, None)
else:
out.append(a)
return runtime_dir, out
def main(argv):
if len(argv) < 3:
die("用法: svc_manifest.py ENV_DIR <migrate|init|status|set|del|bulk-set|set-desired|verify> [参数]", 2)
die("用法: svc_manifest.py ENV_DIR <migrate|init|status|set|del|bulk-set|"
"set-desired|verify|check-conflicts> [参数]", 2)
env_dir, cmd = argv[1], argv[2]
args = argv[3:]
if cmd == "migrate":
@@ -300,16 +632,27 @@ def main(argv):
return cmd_init(env_dir)
if cmd == "status":
return cmd_status(env_dir)
if cmd == "set" and len(args) == 2:
return cmd_set(env_dir, args[0], args[1])
if cmd == "del" and len(args) == 1:
return cmd_del(env_dir, args[0])
if cmd == "set":
runtime_dir, args = _parse_runtime_flag(args)
if len(args) == 2:
return cmd_set(env_dir, args[0], args[1], runtime_dir=runtime_dir)
if cmd == "del":
runtime_dir, args = _parse_runtime_flag(args)
if len(args) == 1:
return cmd_del(env_dir, args[0], runtime_dir=runtime_dir)
if cmd == "bulk-set":
return cmd_bulk_set(env_dir)
runtime_dir, args = _parse_runtime_flag(args)
return cmd_bulk_set(env_dir, runtime_dir=runtime_dir)
if cmd == "set-desired" and len(args) == 1:
return cmd_set_desired(env_dir, args[0])
if cmd == "check-conflicts":
runtime_dir, _ = _parse_runtime_flag(args)
return cmd_check_conflicts(env_dir, runtime_dir=runtime_dir)
if cmd == "verify":
data_root = db_mode = db_ref = None
files_root = env_class = compose_project = None
base_db_ref = rdm_db_ref = kb_db_ref = None
require_storage = False
it = iter(args)
for a in it:
if a == "--data":
@@ -318,7 +661,27 @@ def main(argv):
db_mode = next(it, None)
elif a == "--db-ref":
db_ref = next(it, None)
return cmd_verify(env_dir, data_root, db_mode, db_ref)
elif a == "--files-root":
files_root = next(it, None)
elif a == "--env-class":
env_class = next(it, None)
elif a == "--compose-project":
compose_project = next(it, None)
elif a == "--base-db-ref":
base_db_ref = next(it, None)
elif a == "--rdm-db-ref":
rdm_db_ref = next(it, None)
elif a == "--kb-db-ref":
kb_db_ref = next(it, None)
elif a == "--require-storage":
require_storage = True
return cmd_verify(
env_dir, data_root, db_mode, db_ref,
files_root=files_root, env_class=env_class,
compose_project=compose_project,
base_db_ref=base_db_ref, rdm_db_ref=rdm_db_ref, kb_db_ref=kb_db_ref,
require_storage=require_storage,
)
die("未知命令或参数不全:%s %s" % (cmd, " ".join(args)), 2)
+3 -3
View File
@@ -813,7 +813,7 @@ card_merge_squash() { # $1=branch $2=ref → squash/rebase 落地没有 merge
# 仅查最近 5 页(250 条):card_merge 的 merge 提交路径仍是主路径,此兜底面向新近任务卡。
local token="${GITEA_TOKEN:-}" web_base url repo owner api payload pg found sha
[ -n "$token" ] || return 1
web_base="${TASK_CARD_WEB_BASE:-https://gitea.naitangbaobao.cn}"
web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
url=$(git remote get-url origin 2>/dev/null) || return 1
url="${url%.git}"
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
@@ -951,7 +951,7 @@ cmd_close_self() {
esac
pr="$nums"
fi
web_base="${TASK_CARD_WEB_BASE:-https://gitea.naitangbaobao.cn}"
web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
url=$(git remote get-url origin); url="${url%.git}"
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
link="${web_base}/${owner}/${repo}/pulls/${pr}"
@@ -981,7 +981,7 @@ cmd_close() {
fetch_target
# 默认值取远端通告的 Web 面(git push 输出的 PR 建议链接);可用 TASK_CARD_WEB_BASE 覆盖。
# git-panel 前缀已失效(TLS 握手失败),默认值改为实测可用的 git-home(AGENTS.md 访问事实节)。
local web_base="${TASK_CARD_WEB_BASE:-https://gitea.naitangbaobao.cn}"
local web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
local url owner repo
url=$(git remote get-url origin); url="${url%.git}"
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
+1 -1
View File
@@ -29,7 +29,7 @@ import urllib.parse
import urllib.request
SCHEMA = "agent-system/task-triage/v1"
DEFAULT_API = "https://gitea.naitangbaobao.cn/api/v1"
DEFAULT_API = "https://git-home.u1680832.nyat.app:30319/api/v1"
DEFAULT_TARGET = "main"
DEFAULT_MAX_AGE = 14400
PAGE_LIMIT = 50
+5 -6
View File
@@ -1,13 +1,12 @@
# RDM 制品钉扎。三行都是 40 位 hex。升级走 PR,不在 CI 里执行候选文件。
# sha:生命周期套件的目标版本(6-pin 布局);本次钉扎为增量 D(W11 DHF U2-U6)交付版本,
# 即 RDM main 上增量 D 合并 a3c721ae4 之后的 tip。
# sha:生命周期套件的目标版本(6-pin 布局)。
# prev_sha:来回部署身份稳定性测试的起点。选取受两条硬约束——与 sha 的 pins 布局一致,
# 且两者之间无跨 schema 迁移(scripts/svc.sh 的 schema 兼容闸会在停旧服务前拒绝不兼容切换)。
# 取上一个已钉住的 sha:同为 6-pin 布局,与 sha 之间无 store schema 迁移(W11/W8 变更走事件
# 重放而非 schema),往返部署覆盖的正是上一交付版本到增量 D 的真实升级路径。
# 取上一个已钉住的 sha:同为 6-pin 布局,与 sha 之间无 schema/迁移变更,在 RDM 主线上与 sha
# 相距 59 个提交,往返部署覆盖的版本跨度大于仅隔一次合并的相邻提交。
# legacy_sha:承载「无身份」语义(没有 version --json),必然早于 6-pin 布局。
# 三者的 pins 布局可能不同;CI 按每个 pin 自身的 gitlink 清单物化并逐个断言,不要求布局统一。
# 来回切换的 schema 兼容不由钉扎保证,由真实生命周期套件检验。
sha=9176476bf35d23e7886bf6ccb7c3a688067d357f
prev_sha=15fdd479a9fe60a59559139305b8d760931cec93
sha=ee8c2cdf4a0ed88cdc4037f4b43501e1eebc4c5b
prev_sha=bc8db886606e88201e7f7210a1357d22dc84124b
legacy_sha=72bb1fec79d64c2af5e49416640712320816c3b4
+15 -16
View File
@@ -256,7 +256,7 @@ class FactsVerifyTests(unittest.TestCase):
self.assert_status(result.stdout, "F2", "PASS")
requests = self.stub_events("http")
self.assertEqual(len(requests), 1, requests)
self.assertEqual(requests[0]["url"], "https://gitea.naitangbaobao.cn/api/v1/user")
self.assertEqual(requests[0]["url"], "https://git-home.u1680832.nyat.app:30319/api/v1/user")
self.assertEqual(requests[0]["authorization"], "token " + HOME_TOKEN)
self.assertEqual(self.dates(), {"F2": self.today, "F8": self.today, "F9": OLD_DATE})
self.assertTrue(receipt.exists(), result.stderr)
@@ -285,7 +285,7 @@ class FactsVerifyTests(unittest.TestCase):
self.assert_status(result.stdout, "F2", "FAIL")
self.assertEqual(self.stub_events("http"), [], "no request without a valid HOME token")
self.assertEqual(self.dates()["F2"], OLD_DATE)
direct = self.run_script("api-token", "https://gitea.naitangbaobao.cn/api/v1/user")
direct = self.run_script("api-token", "https://git-home.u1680832.nyat.app:30319/api/v1/user")
self.assertNotEqual(direct.returncode, 0)
self.assertIn("ACCESS_TOKEN", direct.stderr)
self.assert_no_secrets(result.stdout, result.stderr, direct.stdout, direct.stderr,
@@ -299,7 +299,7 @@ class FactsVerifyTests(unittest.TestCase):
result = self.run_script("--update", http=http)
self.assert_status(result.stdout, "F2", "FAIL")
self.assertEqual(self.dates(), {"F2": OLD_DATE, "F8": self.today, "F9": OLD_DATE})
direct = self.run_script("api-token", "https://gitea.naitangbaobao.cn/api/v1/user",
direct = self.run_script("api-token", "https://git-home.u1680832.nyat.app:30319/api/v1/user",
http=http)
self.assertNotEqual(direct.returncode, 0)
self.assertNotIn("Authorization", direct.stderr)
@@ -307,18 +307,17 @@ class FactsVerifyTests(unittest.TestCase):
def test_f2_rejects_unregistered_destination_before_reading_credentials(self):
bad_urls = [
"http://gitea.naitangbaobao.cn/api/v1/user",
"http://git-home.u1680832.nyat.app:30319/api/v1/user",
"https://evil.invalid/api/v1/user",
"https://git-home.u1680832.nyat.app:30319/api/v1/user",
"https://gitea.naitangbaobao.cn:30320/api/v1/user",
"https://gitea.naitangbaobao.cn:443/api/v1/user",
"https://user:pw@gitea.naitangbaobao.cn/api/v1/user",
"https://gitea.naitangbaobao.cn@evil.invalid/api/v1/user",
"https://gitea.naitangbaobao.cn/api/v1/user#frag",
"https://gitea.naitangbaobao.cn/api/v1/user?next=https://evil.invalid",
"https://gitea.naitangbaobao.cn/api/v1/users",
"https://gitea.naitangbaobao.cn/api/v1/user/../admin",
"https://gitea.naitangbaobao.cn/api/v1/user ",
"https://git-home.u1680832.nyat.app:30320/api/v1/user",
"https://git-home.u1680832.nyat.app/api/v1/user",
"https://user:pw@git-home.u1680832.nyat.app:30319/api/v1/user",
"https://git-home.u1680832.nyat.app:30319@evil.invalid/api/v1/user",
"https://git-home.u1680832.nyat.app:30319/api/v1/user#frag",
"https://git-home.u1680832.nyat.app:30319/api/v1/user?next=https://evil.invalid",
"https://git-home.u1680832.nyat.app:30319/api/v1/users",
"https://git-home.u1680832.nyat.app:30319/api/v1/user/../admin",
"https://git-home.u1680832.nyat.app:30319/api/v1/user ",
]
for secrets_present in (True, False):
if not secrets_present:
@@ -338,12 +337,12 @@ class FactsVerifyTests(unittest.TestCase):
for code in ("301", "302", "303", "307", "308"):
with self.subTest(code):
self.stub_log.unlink(missing_ok=True)
direct = self.run_script("api-token", "https://gitea.naitangbaobao.cn/api/v1/user",
direct = self.run_script("api-token", "https://git-home.u1680832.nyat.app:30319/api/v1/user",
http=code)
self.assertEqual(direct.returncode, 5, direct.stderr)
requests = self.stub_events("http")
self.assertEqual([r["url"] for r in requests],
["https://gitea.naitangbaobao.cn/api/v1/user"], "no follow-up request")
["https://git-home.u1680832.nyat.app:30319/api/v1/user"], "no follow-up request")
self.assertNotIn("redirect-target", direct.stdout + direct.stderr)
self.assert_no_secrets(direct.stdout, direct.stderr)
+25 -38
View File
@@ -296,14 +296,12 @@ class SvcDbRecoveryTests(unittest.TestCase):
# ---- 1. 存量 exec= DSN:连续启动两次均恢复,进程参数无秘密 ----
def test_legacy_exec_dsn_survives_consecutive_restarts(self):
pg_port = free_test_port()
self.register_pg("pg1", pg_port, password="legacy-pw-1")
dsn = "postgresql://agent:legacy-pw-1@127.0.0.1:%d/agent" % pg_port
self.register_pg("pg1", 18890, password="legacy-pw-1")
dsn = "postgresql://agent:legacy-pw-1@127.0.0.1:18890/agent"
d = self.runtime("dev")
port = free_test_port()
self.seed_agent("dev", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto --dsn %s"
% (port, d / "data", d / "plugins", dsn))
self.seed_agent("dev", 18791,
"--addr 127.0.0.1:18791 --data %s --plugins %s --auth auto --dsn %s"
% (d / "data", d / "plugins", dsn))
for _ in range(2):
self.clear_serve_record()
self.svc("dev", "restart")
@@ -324,14 +322,12 @@ class SvcDbRecoveryTests(unittest.TestCase):
# ---- 2. 仅 dbref:裸 restart 与重新部署均保留绑定 ----
def test_dbref_binding_survives_bare_restart_and_redeploy(self):
pg_port = free_test_port()
self.register_pg("pg1", pg_port, password="bind-pw-2")
dsn = "postgresql://agent:bind-pw-2@127.0.0.1:%d/agent" % pg_port
self.register_pg("pg1", 18891, password="bind-pw-2")
dsn = "postgresql://agent:bind-pw-2@127.0.0.1:18891/agent"
d = self.runtime("dev")
port = free_test_port()
self.seed_agent("dev", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
% (port, d / "data", d / "plugins"),
self.seed_agent("dev", 18792,
"--addr 127.0.0.1:18792 --data %s --plugins %s --auth auto"
% (d / "data", d / "plugins"),
extra_rows=[("dbref", "pg1/agent"), ("db", "pg")])
self.clear_serve_record()
self.svc("dev", "restart")
@@ -417,10 +413,9 @@ class SvcDbRecoveryTests(unittest.TestCase):
def test_refusals_keep_registration_without_sqlite_fallback(self):
d = self.runtime("a1")
port = free_test_port()
self.seed_agent("a1", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
% (port, d / "data", d / "plugins"),
self.seed_agent("a1", 18796,
"--addr 127.0.0.1:18796 --data %s --plugins %s --auth auto"
% (d / "data", d / "plugins"),
extra_rows=[("dbref", "pgmissing/agent"), ("db", "pg")])
self.clear_serve_record()
result = self.svc("a1", "restart", check=False)
@@ -430,41 +425,33 @@ class SvcDbRecoveryTests(unittest.TestCase):
self.assertEqual(self.kv("a1", "dbref"), "pgmissing/agent") # 可恢复配置保留
# 实例登记不全(缺 password)→ 拒启。
self.register_pg("pginc", free_test_port(), complete=False)
self.register_pg("pginc", 18895, complete=False)
d = self.runtime("a2")
port = free_test_port()
self.seed_agent("a2", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
% (port, d / "data", d / "plugins"),
self.seed_agent("a2", 18797,
"--addr 127.0.0.1:18797 --data %s --plugins %s --auth auto"
% (d / "data", d / "plugins"),
extra_rows=[("dbref", "pginc/agent"), ("db", "pg")])
result = self.svc("a2", "restart", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("拒绝", result.stdout)
# dbref 与 dsn= 指向不同库 → 配置冲突拒启。
pg_port = free_test_port()
self.register_pg("pg1", pg_port, password="clash-pw")
other_pg_port = free_test_port()
while other_pg_port == pg_port:
other_pg_port = free_test_port()
self.register_pg("pg1", 18896, password="clash-pw")
d = self.runtime("a3")
port = free_test_port()
self.seed_agent("a3", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
% (port, d / "data", d / "plugins"),
self.seed_agent("a3", 18798,
"--addr 127.0.0.1:18798 --data %s --plugins %s --auth auto"
% (d / "data", d / "plugins"),
extra_rows=[("dbref", "pg1/agent"), ("db", "pg"),
("dsn", "postgresql://u:p@127.0.0.1:%d/otherdb"
% other_pg_port)])
("dsn", "postgresql://u:p@127.0.0.1:18999/otherdb")])
result = self.svc("a3", "restart", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("拒绝", result.stdout)
# db=pg 但无可恢复 DSN → 拒启。
d = self.runtime("a4")
port = free_test_port()
self.seed_agent("a4", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
% (port, d / "data", d / "plugins"),
self.seed_agent("a4", 18799,
"--addr 127.0.0.1:18799 --data %s --plugins %s --auth auto"
% (d / "data", d / "plugins"),
extra_rows=[("db", "pg")])
result = self.svc("a4", "restart", check=False)
self.assertNotEqual(result.returncode, 0)
+2 -6
View File
@@ -2,13 +2,9 @@
import importlib.util
import json
import os
import sys
import unittest
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _port_alloc import free_test_port # noqa: E402
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location(
"tsi", SOURCE / "tests" / "test_svc_identity.py")
@@ -50,7 +46,7 @@ class DeployTxnTests(unittest.TestCase):
new.chmod(0o755)
fx.stub.write_text(old.read_text())
fx.stub.chmod(0o755)
fx.seed_agent("txn", free_test_port())
fx.seed_agent("txn", 9471)
fx.start_ok("txn")
pid = fx.pid_of("txn")
script = fx.root / "scripts" / "svc.sh"
@@ -72,7 +68,7 @@ class DeployTxnTests(unittest.TestCase):
def test_forward_only_requires_confirm_and_backup(self):
fx = self.fx
fx.seed_agent("fwd", free_test_port())
fx.seed_agent("fwd", 9472)
fx.start_ok("fwd")
iid = json.loads((fx.runtime("fwd") / "manifest.json").read_text())["instance_id"]
base = fx.stub.read_text()
+209 -36
View File
@@ -12,14 +12,10 @@ import os
import shutil
import sqlite3
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _port_alloc import free_test_port # noqa: E402
SOURCE = Path(__file__).resolve().parents[1]
SVC_SCRIPT = SOURCE / "scripts" / "svc.sh"
@@ -60,8 +56,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
# ---- T1 连续裸启停:同一身份与数据根 ----
def test_T1_bare_restart_stable_identity(self):
port = free_test_port()
self.seed_agent("id1", port)
self.seed_agent("id1", 9431)
self.start_ok("id1")
m1 = self.manifest("id1")
self.assertIsNotNone(m1, "start 应完成 manifest 迁移")
@@ -84,8 +79,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
# ---- T2 只改端口:数据/DB 锚点不动 ----
def test_T2_port_change_keeps_anchors(self):
port = free_test_port()
self.seed_agent("id2", port)
self.seed_agent("id2", 9432)
self.start_ok("id2")
anchor = self.manifest("id2")["identity"]
self.svc("id2", "stop")
@@ -101,8 +95,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
# ---- T3 数据根漂移:拒绝启动,不新建空系统 ----
def test_T3_data_drift_rejected(self):
port = free_test_port()
self.seed_agent("id3", port)
self.seed_agent("id3", 9434)
self.start_ok("id3")
self.svc("id3", "stop")
other = self.root / "other-data"
@@ -118,8 +111,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
# ---- T4 迁移窗口与损坏:不猜测 ----
def test_T4a_leftover_new_file_ignored(self):
port = free_test_port()
self.seed_agent("id4", port)
self.seed_agent("id4", 9435)
# 模拟 rename 前崩溃残留:.new 半成品必须被忽略/覆盖,不影响迁移结果
(self.runtime("id4") / "manifest.json.new").write_text("{ truncated")
self.start_ok("id4")
@@ -129,8 +121,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
self.svc("id4", "stop")
def test_T4b_corrupt_manifest_blocks(self):
port = free_test_port()
self.seed_agent("id5", port)
self.seed_agent("id5", 9436)
self.start_ok("id5")
self.svc("id5", "stop")
(self.runtime("id5") / "manifest.json").write_text("{ not json")
@@ -142,8 +133,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
# ---- T5 marker/manifest 事件:缺一即阻塞,不自动新建 ----
def test_T5a_missing_marker_blocks(self):
port = free_test_port()
self.seed_agent("id6", port)
self.seed_agent("id6", 9437)
self.start_ok("id6")
self.svc("id6", "stop")
m = self.manifest("id6")
@@ -155,8 +145,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
self.assertFalse(self.running("id6"))
def test_T5b_foreign_marker_blocks(self):
port = free_test_port()
self.seed_agent("id7", port)
self.seed_agent("id7", 9438)
self.start_ok("id7")
self.svc("id7", "stop")
m = self.manifest("id7")
@@ -169,8 +158,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
self.assertFalse(self.running("id7"))
def test_T5c_lost_manifest_blocks_not_rebuild(self):
port = free_test_port()
self.seed_agent("id8", port)
self.seed_agent("id8", 9439)
self.start_ok("id8")
m0 = self.manifest("id8")
iid = m0["instance_id"]
@@ -190,8 +178,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
# ---- T6 cwd 无关:从别处调用身份不变 ----
def test_T6_cwd_independent_identity(self):
port = free_test_port()
self.seed_agent("id9", port)
self.seed_agent("id9", 9440)
self.start_ok("id9")
iid = self.manifest("id9")["instance_id"]
self.svc("id9", "stop")
@@ -333,8 +320,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
旧服务继续运行、旧制品与登记不变(2026-10-12 rdm1/rdmcand 反例:停服→
覆盖→启动即拒,只剩混合状态)。"""
remote, old_sha, new_sha = self.make_rdm_repo(self.noid_stub())
port = free_test_port()
self.seed_agent("rdx", port)
self.seed_agent("rdx", 9451)
self.start_ok("rdx") # 已建立身份,有能力制品在运行
self.set_kv("rdx", "source", "rdm@%s" % old_sha)
pid = self.pid_of("rdx")
@@ -358,8 +344,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
"""已建立身份后删行:update 在停服前拒绝,不自动补行,旧进程保持。
补行只走 adopt --confirm。"""
old_sha, new_sha = self.make_src_repo()
port = free_test_port()
self.seed_agent("rowx", port)
self.seed_agent("rowx", 9452)
self.start_ok("rowx")
self.set_kv("rowx", "source", "dev@%s" % old_sha)
db = Path(self.manifest("rowx")["identity"]["data_root"]) / "agent-system.db"
@@ -379,8 +364,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
def test_U1_bare_start_missing_row_is_event_init_recovers(self):
"""普通启动只验证不补行:DB 行缺失 → 拒绝且不建空行(缺行即事件);
显式 init 补齐后恢复。"""
port = free_test_port()
self.seed_agent("rowm", port)
self.seed_agent("rowm", 9453)
self.start_ok("rowm")
iid = self.manifest("rowm")["instance_id"]
self.svc("rowm", "stop")
@@ -432,8 +416,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
def test_U1_adopt_identityless_artifact_reports_pending(self):
"""已装制品无 identity 能力:adopt 的 ensure 静默跳过,marker 留在 pending——
输出必须如实报 pending,不得宣称 db_marker=established。"""
port = free_test_port()
self.seed_agent("adx", port)
self.seed_agent("adx", 9454)
self.start_ok("adx")
iid = self.manifest("adx")["instance_id"]
self.svc("adx", "stop")
@@ -454,8 +437,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
cand.chmod(0o755)
remote, old_sha, _ = self._make_track_repo(cand, "agent-system")
self.git_ok(self.root, "clone", str(remote), str(self.root / "srccache"))
port = free_test_port()
self.seed_agent("scx", port)
self.seed_agent("scx", 9455)
self.start_ok("scx")
self.set_kv("scx", "source", "dev@%s" % old_sha)
mpath = self.runtime("scx") / "manifest.json"
@@ -477,8 +459,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
self.svc("scx", "stop")
def test_I3_start_ensures_sqlite_identity(self):
port = free_test_port()
self.seed_agent("idb1", port)
self.seed_agent("idb1", 9441)
self.start_ok("idb1")
iid = self.manifest("idb1")["instance_id"]
rows = self.db_identity_row("idb1")
@@ -489,8 +470,7 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
self.svc("idb1", "stop")
def test_I3_wrong_db_identity_blocks_start(self):
port = free_test_port()
self.seed_agent("idb2", port)
self.seed_agent("idb2", 9442)
self.start_ok("idb2")
iid = self.manifest("idb2")["instance_id"]
self.svc("idb2", "stop")
@@ -520,6 +500,199 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
self.skipTest("真 PG 专项应由隔离套件执行(本桩文件不连真实 PG)")
self.skipTest("真实 PG 身份专项 NOT_RUN:需真实实例(SVC_TEST_PG=1 + AGENT_PG_TEST_DSN)")
# ---- 容器存储 U1:环境与数据归属合同 ----
def _storage_secret_dir(self):
d = self.root / "storage-secrets"
d.mkdir(exist_ok=True)
for slot in ("base", "rdm", "kb"):
(d / ("%s.dsn" % slot)).write_text("ref-only\n", encoding="utf-8")
(d / ("%s.role" % slot)).write_text("ref-only\n", encoding="utf-8")
return d
def _apply_container_bindings(self, name, compose, ports, files_root, env_class="prod"):
"""先写齐绑定键,最后打开 storage_mode=container(缺一即拒)。
经 svc_manifest.py set(与 svc_set_key 同源权威)。"""
sec = self._storage_secret_dir()
items = [
("env_class", env_class),
("files_root", str(files_root)),
("compose_project", compose),
("base_dsn_ref", str(sec / "base.dsn")),
("base_role_ref", str(sec / "base.role")),
("base_port", ports[0]),
("rdm_dsn_ref", str(sec / "rdm.dsn")),
("rdm_role_ref", str(sec / "rdm.role")),
("rdm_port", ports[1]),
("kb_dsn_ref", str(sec / "kb.dsn")),
("kb_role_ref", str(sec / "kb.role")),
("kb_port", ports[2]),
("storage_mode", "container"),
]
man = self.root / "scripts" / "svc_manifest.py"
env_dir = self.runtime(name)
runtime = env_dir.parent
for k, v in items:
proc = subprocess.run(
["python3", str(man), str(env_dir), "set", "--runtime", str(runtime), k, str(v)],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertEqual(proc.returncode, 0, "%s=%s → %s" % (k, v, proc.stdout))
def test_CS_U1_legacy_upgrade_keeps_identity(self):
"""旧配置升级到 container 绑定:instance_id / data_root 保留。"""
self.seed_agent("cs1", 9471)
self.start_ok("cs1")
m0 = self.manifest("cs1")
iid, data = m0["instance_id"], m0["identity"]["data_root"]
self.svc("cs1", "stop")
files = self.root / "files-cs1"
files.mkdir()
self._apply_container_bindings("cs1", "svc-cs1", ("25432", "25433", "25434"), files)
m1 = self.manifest("cs1")
self.assertEqual(m1["instance_id"], iid)
self.assertEqual(m1["identity"]["data_root"], data)
self.assertEqual(m1["identity"]["storage_mode"], "container")
self.assertEqual(m1["identity"]["env_class"], "prod")
self.assertEqual(m1["identity"]["files_root"], str(files))
self.assertTrue(m1["identity"]["base_db_ref"].startswith("dsn_ref:"))
self.assertTrue(m1["identity"]["rdm_db_ref"].startswith("dsn_ref:"))
self.assertTrue(m1["identity"]["kb_db_ref"].startswith("dsn_ref:"))
def test_CS_U1_relative_files_root_rejected(self):
self.seed_agent("cs2", 9472)
self.start_ok("cs2")
self.svc("cs2", "stop")
man = self.root / "scripts" / "svc_manifest.py"
env_dir = self.runtime("cs2")
proc = subprocess.run(
["python3", str(man), str(env_dir), "set", "--runtime", str(env_dir.parent),
"files_root", "relative/not-abs"],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertNotEqual(proc.returncode, 0, proc.stdout)
self.assertIn("绝对路径", proc.stdout)
self.assertNotEqual(self.manifest("cs2")["identity"].get("files_root"),
"relative/not-abs")
def test_CS_U1_missing_binding_rejected(self):
"""container 模式缺任一必需绑定不得开启(不静默默认)。"""
self.seed_agent("cs3", 9473)
self.start_ok("cs3")
self.svc("cs3", "stop")
man = self.root / "scripts" / "svc_manifest.py"
env_dir = self.runtime("cs3")
rt = str(env_dir.parent)
proc = subprocess.run(
["python3", str(man), str(env_dir), "set", "--runtime", rt,
"storage_mode", "container"],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertNotEqual(proc.returncode, 0, proc.stdout)
self.assertIn("缺少必需绑定", proc.stdout)
self.assertNotEqual(self.manifest("cs3")["kv"].get("storage_mode"), "container")
def test_CS_U1_wrong_files_root_and_db_verify_rejected(self):
self.seed_agent("cs4", 9474)
self.start_ok("cs4")
self.svc("cs4", "stop")
files = self.root / "files-cs4"
files.mkdir()
self._apply_container_bindings("cs4", "svc-cs4", ("25532", "25533", "25534"), files)
man = self.root / "scripts" / "svc_manifest.py"
env_dir = self.runtime("cs4")
bad_files = subprocess.run(
["python3", str(man), str(env_dir), "verify", "--require-storage",
"--files-root", str(self.root / "other-files")],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertNotEqual(bad_files.returncode, 0, bad_files.stdout)
self.assertIn("文件根", bad_files.stdout)
bad_db = subprocess.run(
["python3", str(man), str(env_dir), "verify", "--require-storage",
"--files-root", str(files),
"--base-db-ref", "dbref:foreign/x"],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertNotEqual(bad_db.returncode, 0, bad_db.stdout)
self.assertIn("base 库绑定漂移", bad_db.stdout)
def test_CS_U1_compose_and_port_conflict_before_modify(self):
self.seed_agent("cs5a", 9475)
self.seed_agent("cs5b", 9476)
self.start_ok("cs5a")
self.start_ok("cs5b")
self.svc("cs5a", "stop")
self.svc("cs5b", "stop")
files_a = self.root / "files-cs5a"
files_b = self.root / "files-cs5b"
files_a.mkdir()
files_b.mkdir()
self._apply_container_bindings("cs5a", "svc-shared", ("25632", "25633", "25634"), files_a)
man = self.root / "scripts" / "svc_manifest.py"
env_b = self.runtime("cs5b")
rt = str(env_b.parent)
# 同 compose 名:修改前失败
sec = self._storage_secret_dir()
for k, v in (("env_class", "test"), ("files_root", str(files_b)),
("compose_project", "svc-shared")):
proc = subprocess.run(
["python3", str(man), str(env_b), "set", "--runtime", rt, k, v],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
if k == "compose_project":
self.assertNotEqual(proc.returncode, 0, proc.stdout)
self.assertIn("Compose 项目名冲突", proc.stdout)
else:
self.assertEqual(proc.returncode, 0, proc.stdout)
# 端口冲突
self.seed_agent("cs5c", 9477)
self.start_ok("cs5c")
self.svc("cs5c", "stop")
files_c = self.root / "files-cs5c"
files_c.mkdir()
env_c = self.runtime("cs5c")
for k, v in (
("env_class", "verify"),
("files_root", str(files_c)),
("compose_project", "svc-cs5c"),
("base_dsn_ref", str(sec / "base.dsn")),
("base_role_ref", str(sec / "base.role")),
("base_port", "25632"),
):
proc = subprocess.run(
["python3", str(man), str(env_c), "set", "--runtime", rt, k, str(v)],
cwd=str(self.root), env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
if k == "base_port":
self.assertNotEqual(proc.returncode, 0, proc.stdout)
self.assertIn("端口冲突", proc.stdout)
else:
self.assertEqual(proc.returncode, 0, proc.stdout)
def test_CS_U1_other_checkout_keeps_absolute_files_root(self):
"""另一 cwd/checkout 调用仍定位同一绝对 files_root。"""
self.seed_agent("cs6", 9478)
self.start_ok("cs6")
self.svc("cs6", "stop")
files = self.root / "files-cs6"
files.mkdir()
self._apply_container_bindings("cs6", "svc-cs6", ("25732", "25733", "25734"), files)
iid = self.manifest("cs6")["instance_id"]
fr = self.manifest("cs6")["identity"]["files_root"]
elsewhere = tempfile.mkdtemp(prefix="svc-cs-elsewhere-")
self.addCleanup(shutil.rmtree, elsewhere, True)
man = self.root / "scripts" / "svc_manifest.py"
proc = subprocess.run(
["python3", str(man), str(self.runtime("cs6")), "status"],
cwd=elsewhere, env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertEqual(proc.returncode, 0, proc.stdout)
payload = json.loads(proc.stdout.strip())
self.assertEqual(payload["instance_id"], iid)
self.assertEqual(payload["identity"]["files_root"], fr)
self.assertTrue(os.path.isabs(fr), fr)
class AtomicWriteTests(unittest.TestCase):
"""崩溃窗口单元级:os.replace 失败时原文件必须完好(I3 半写不可能)。"""
+40 -9
View File
@@ -1,14 +1,9 @@
"""生命周期隔离:调用方的 AGENT_SYSTEM_* 不得进入 serve,serve.env 为 0600。"""
import importlib.util
import json
import os
import shutil
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _port_alloc import free_test_port # noqa: E402
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
tsr = importlib.util.module_from_spec(_spec)
@@ -22,8 +17,7 @@ class LifecycleIsolationTests(tsr.SvcAutoRecoverTests):
self.root / "scripts" / "svc_manifest.py")
def test_caller_identity_env_does_not_reach_serve(self):
port = free_test_port()
self.seed_agent("iso", port)
self.seed_agent("iso", 9461)
env = dict(self.env)
env["AGENT_SYSTEM_DSN"] = "postgres://leak-user:leak-pass@127.0.0.1/leak"
env["AGENT_SYSTEM_DATA"] = "/tmp/leak-data"
@@ -44,8 +38,7 @@ class LifecycleIsolationTests(tsr.SvcAutoRecoverTests):
self.svc("iso", "stop")
def test_two_bare_restarts_keep_identity(self):
port = free_test_port()
self.seed_agent("iso2", port)
self.seed_agent("iso2", 9462)
self.svc("iso2", "start")
manifest = json.loads((self.runtime("iso2") / "manifest.json").read_text())
iid = manifest["instance_id"]
@@ -59,3 +52,41 @@ class LifecycleIsolationTests(tsr.SvcAutoRecoverTests):
self.assertEqual(again["identity"]["data_root"], data)
self.assertEqual(again["identity"].get("db_marker"), "established")
self.svc("iso2", "stop")
def test_caller_storage_env_does_not_reach_serve(self):
"""调用方 AGENT_SYSTEM_FILES_ROOT / ENV_CLASS / COMPOSE 不得进入 serve。"""
self.seed_agent("iso3", 9463)
env = dict(self.env)
env["AGENT_SYSTEM_FILES_ROOT"] = "/tmp/leak-files-root"
env["AGENT_SYSTEM_ENV_CLASS"] = "prod"
env["AGENT_SYSTEM_COMPOSE_PROJECT"] = "leak-compose"
env["AGENT_SYSTEM_BASE_DB_REF"] = "dbref:leak/base"
env["AGENT_SYSTEM_RDM_DB_REF"] = "dbref:leak/rdm"
env["AGENT_SYSTEM_KB_DB_REF"] = "dbref:leak/kb"
self.svc("iso3", "start", env=env)
pid = self.pid_of("iso3")
self.assertIsNotNone(pid)
raw = Path("/proc/%d/environ" % pid).read_bytes()
self.assertNotIn(b"/tmp/leak-files-root", raw)
self.assertNotIn(b"leak-compose", raw)
self.assertNotIn(b"dbref:leak/base", raw)
self.assertNotIn(b"dbref:leak/rdm", raw)
self.assertNotIn(b"dbref:leak/kb", raw)
serve_env = self.runtime("iso3") / "serve.env"
self.assertTrue(serve_env.is_file())
text = serve_env.read_text()
self.assertNotIn("AGENT_SYSTEM_FILES_ROOT", text)
self.assertNotIn("leak-files-root", text)
self.assertNotIn("leak-compose", text)
self.svc("iso3", "stop")
def test_env_config_resets_storage_globals(self):
"""棘轮:env_config 必须清空存储绑定全局与调用方身份环境。"""
import subprocess
out = subprocess.run(
["bash", "-c",
'source "$1"; declare -f env_config | grep -c \'AGENT_SYSTEM_FILES_ROOT\'',
"x", str(SOURCE / "scripts" / "svc.sh")],
capture_output=True, text=True, cwd=str(SOURCE), timeout=30)
self.assertEqual(out.returncode, 0, out.stderr)
self.assertGreaterEqual(int(out.stdout.strip()), 1)
+192
View File
@@ -246,6 +246,198 @@ class ResolverStateIsolation(unittest.TestCase):
)
self.assertEqual(out.strip(), "1")
def test_env_config_resets_storage_identity_env(self):
# 棘轮:env_config 必须剥离调用方 FILES_ROOT / ENV_CLASS / COMPOSE(容器存储 U1)。
out = _bash(
'source "$1"; declare -f env_config | '
'grep -c \'unset AGENT_SYSTEM_FILES_ROOT AGENT_SYSTEM_ENV_CLASS AGENT_SYSTEM_COMPOSE_PROJECT\''
)
self.assertEqual(out.strip(), "1")
class StorageBindingsPrecheck(unittest.TestCase):
"""容器存储 U1:resolve_storage_bindings 与冲突预检(不启真实服务)。"""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.rt = Path(self._tmp.name) / "runtime"
self.rt.mkdir()
self.sec = Path(self._tmp.name) / "secrets"
self.sec.mkdir()
self.files = Path(self._tmp.name) / "files"
self.files.mkdir()
for slot in ("base", "rdm", "kb"):
(self.sec / ("%s.dsn" % slot)).write_text("x\n")
(self.sec / ("%s.role" % slot)).write_text("y\n")
# 复制 manifest 脚本到可读路径(与仓内一致)
self.man = REPO / "scripts" / "svc_manifest.py"
def tearDown(self):
self._tmp.cleanup()
def _init_agent(self, name):
env = self.rt / name
data = env / "data"
data.mkdir(parents=True)
(env / "svc.env").write_text(
"kind=agent\nenv=%s\naddr=127.0.0.1:18080\n"
"exec=--addr 127.0.0.1:18080 --data %s\n" % (name, data),
encoding="utf-8",
)
proc = subprocess.run(
["python3", str(self.man), str(env), "migrate"],
capture_output=True, text=True, cwd=str(REPO), timeout=30)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
return env
def _set(self, env, key, value):
proc = subprocess.run(
["python3", str(self.man), str(env), "set",
"--runtime", str(self.rt), key, str(value)],
capture_output=True, text=True, cwd=str(REPO), timeout=30)
return proc
def test_resolve_storage_bindings_legacy_ok(self):
env = self._init_agent("leg")
out = _bash(
'source "$1"; shift; RUNTIME="$1"; ENV_NAME=leg; ENV_DIR="$2"; '
'if resolve_storage_bindings; then echo rc=ok; else echo rc=fail; fi; '
'printf "MODE=%s\\n" "$STORAGE_MODE"',
str(self.rt), str(env),
)
self.assertIn("rc=ok", out)
self.assertIn("MODE=legacy", out)
def test_resolve_incomplete_container_refuses(self):
env = self._init_agent("inc")
# 只写 storage_mode 会被 manifest 拒绝;直接改 kv 模拟半套后 resolve 拒绝
proc = self._set(env, "env_class", "prod")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
# 手工把 storage_mode 写成 container(绕过 mutate 完整校验)以测 resolve
import json
mp = env / "manifest.json"
m = json.loads(mp.read_text(encoding="utf-8"))
m["kv"]["storage_mode"] = "container"
m["identity"]["storage_mode"] = "container"
mp.write_text(json.dumps(m, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(env / "svc.env").write_text(
(env / "svc.env").read_text(encoding="utf-8") + "storage_mode=container\n",
encoding="utf-8",
)
try:
_bash(
'source "$1"; shift; RUNTIME="$1"; ENV_NAME=inc; ENV_DIR="$2"; '
'resolve_storage_bindings',
str(self.rt), str(env),
)
self.fail("incomplete container must refuse")
except AssertionError as e:
self.assertIn("存储绑定不完整", str(e))
def test_relative_files_root_refused_on_set(self):
env = self._init_agent("rel")
proc = self._set(env, "files_root", "not/absolute")
self.assertNotEqual(proc.returncode, 0)
self.assertIn("绝对路径", proc.stdout + proc.stderr)
def test_compose_project_conflict_refused(self):
a = self._init_agent("ca")
b = self._init_agent("cb")
for k, v in (
("env_class", "prod"),
("files_root", str(self.files)),
("compose_project", "same-proj"),
("base_dsn_ref", str(self.sec / "base.dsn")),
("base_role_ref", str(self.sec / "base.role")),
("base_port", "26001"),
("rdm_dsn_ref", str(self.sec / "rdm.dsn")),
("rdm_role_ref", str(self.sec / "rdm.role")),
("rdm_port", "26002"),
("kb_dsn_ref", str(self.sec / "kb.dsn")),
("kb_role_ref", str(self.sec / "kb.role")),
("kb_port", "26003"),
("storage_mode", "container"),
):
proc = self._set(a, k, v)
self.assertEqual(proc.returncode, 0, "%s: %s" % (k, proc.stdout + proc.stderr))
(self.files / "b").mkdir(exist_ok=True)
proc = self._set(b, "env_class", "test")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
proc = self._set(b, "files_root", str(self.files / "b"))
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
proc = self._set(b, "compose_project", "same-proj")
self.assertNotEqual(proc.returncode, 0)
self.assertIn("Compose 项目名冲突", proc.stdout + proc.stderr)
def test_storage_port_conflict_refused(self):
a = self._init_agent("pa")
b = self._init_agent("pb")
for k, v in (
("env_class", "prod"),
("files_root", str(self.files / "pa")),
("compose_project", "proj-pa"),
("base_dsn_ref", str(self.sec / "base.dsn")),
("base_role_ref", str(self.sec / "base.role")),
("base_port", "26101"),
("rdm_dsn_ref", str(self.sec / "rdm.dsn")),
("rdm_role_ref", str(self.sec / "rdm.role")),
("rdm_port", "26102"),
("kb_dsn_ref", str(self.sec / "kb.dsn")),
("kb_role_ref", str(self.sec / "kb.role")),
("kb_port", "26103"),
("storage_mode", "container"),
):
(self.files / "pa").mkdir(exist_ok=True)
proc = self._set(a, k, v)
self.assertEqual(proc.returncode, 0, "%s: %s" % (k, proc.stdout + proc.stderr))
(self.files / "pb").mkdir(exist_ok=True)
for k, v in (
("env_class", "test"),
("files_root", str(self.files / "pb")),
("compose_project", "proj-pb"),
("base_dsn_ref", str(self.sec / "base.dsn")),
("base_role_ref", str(self.sec / "base.role")),
("base_port", "26101"),
):
proc = self._set(b, k, v)
if k == "base_port":
self.assertNotEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("端口冲突", proc.stdout + proc.stderr)
else:
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
def test_resolve_complete_container_ok(self):
env = self._init_agent("okc")
(self.files / "okc").mkdir(exist_ok=True)
for k, v in (
("env_class", "verify"),
("files_root", str(self.files / "okc")),
("compose_project", "proj-okc"),
("base_dsn_ref", str(self.sec / "base.dsn")),
("base_role_ref", str(self.sec / "base.role")),
("base_port", "26201"),
("rdm_dsn_ref", str(self.sec / "rdm.dsn")),
("rdm_role_ref", str(self.sec / "rdm.role")),
("rdm_port", "26202"),
("kb_dsn_ref", str(self.sec / "kb.dsn")),
("kb_role_ref", str(self.sec / "kb.role")),
("kb_port", "26203"),
("storage_mode", "container"),
):
proc = self._set(env, k, v)
self.assertEqual(proc.returncode, 0, "%s: %s" % (k, proc.stdout + proc.stderr))
out = _bash(
'source "$1"; shift; RUNTIME="$1"; ENV_NAME=okc; ENV_DIR="$2"; '
'if resolve_storage_bindings; then echo rc=ok; else echo rc=fail; fi; '
'printf "MODE=%s\\nENV=%s\\nFILES=%s\\nCOMPOSE=%s\\n" '
'"$STORAGE_MODE" "$ENV_CLASS" "$FILES_ROOT" "$COMPOSE_PROJECT"',
str(self.rt), str(env),
)
self.assertIn("rc=ok", out)
self.assertIn("MODE=container", out)
self.assertIn("ENV=verify", out)
self.assertIn("COMPOSE=proj-okc", out)
if __name__ == "__main__":
unittest.main()
+12 -29
View File
@@ -11,17 +11,12 @@
夹具复用 tests/test_svc_recover.py(桩不需要运行——台账操作是 SQLite 直读直写)。
"""
import importlib.util
import os
import shutil
import sqlite3
import subprocess
import sys
import unittest
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _port_alloc import free_test_port # noqa: E402
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
@@ -69,24 +64,21 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
# ---- show ----
def test_show_prints_ledger_and_flags_unknown_owner(self):
port = free_test_port()
self.seed_agent("ra1", port)
self.seed_agent("ra1", 9461)
self.seed_dbs("ra1", ledger_owner="u7-admin", base_owners=("weichen",))
out = self.svc("ra1", "rdm-admin", "show").stdout
self.assertIn("RDM 域管理员 u7-admin", out)
self.assertIn("不在基座用户表", out) # u7-admin 不在 users → 残留警示
def test_show_known_owner_no_warning(self):
port = free_test_port()
self.seed_agent("ra2", port)
self.seed_agent("ra2", 9462)
self.seed_dbs("ra2", ledger_owner="weichen")
out = self.svc("ra2", "rdm-admin").stdout # 缺省子命令 = show
self.assertIn("RDM 域管理员 weichen", out)
self.assertNotIn("不在基座用户表", out)
def test_show_empty_ledger_hint(self):
port = free_test_port()
self.seed_agent("ra3", port)
self.seed_agent("ra3", 9463)
self.seed_dbs("ra3", ledger_owner=None)
out = self.svc("ra3", "rdm-admin").stdout
self.assertIn("台账为空", out)
@@ -95,8 +87,7 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
# ---- transfer ----
def test_transfer_rewrites_single_row_with_provenance(self):
port = free_test_port()
self.seed_agent("ra4", port)
self.seed_agent("ra4", 9464)
self.seed_dbs("ra4", ledger_owner="u7-admin")
out = self.svc("ra4", "rdm-admin", "transfer", "--to", "weichen").stdout
self.assertIn("受信部署通道", out)
@@ -106,15 +97,13 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
self.assertEqual(added_by, "svc:rdm-admin:u7-admin") # 前任可追溯
def test_transfer_idempotent_when_already_owner(self):
port = free_test_port()
self.seed_agent("ra5", port)
self.seed_agent("ra5", 9465)
self.seed_dbs("ra5", ledger_owner="weichen")
out = self.svc("ra5", "rdm-admin", "transfer", "--to", "weichen").stdout
self.assertIn("已是 weichen", out)
def test_transfer_rejects_owner_missing_from_base_users(self):
port = free_test_port()
self.seed_agent("ra6", port)
self.seed_agent("ra6", 9466)
self.seed_dbs("ra6", ledger_owner="weichen")
proc = self.svc("ra6", "rdm-admin", "transfer", "--to", "nobody", check=False)
self.assertEqual(proc.returncode, 1, proc.stdout)
@@ -122,8 +111,7 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
self.assertEqual(self.ledger_owner("ra6")[0], "weichen") # 台账未被动
def test_transfer_rejects_empty_ledger(self):
port = free_test_port()
self.seed_agent("ra7", port)
self.seed_agent("ra7", 9467)
self.seed_dbs("ra7", ledger_owner=None)
proc = self.svc("ra7", "rdm-admin", "transfer", "--to", "weichen", check=False)
self.assertEqual(proc.returncode, 1, proc.stdout)
@@ -132,8 +120,7 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
# ---- 绑定合同与用法 ----
def test_refuses_when_rdm_db_missing(self):
port = free_test_port()
self.seed_agent("ra8", port)
self.seed_agent("ra8", 9468)
self.seed_dbs("ra8") # 建基座库后删 rdm.db
(self.runtime("ra8") / "data" / "rdm.db").unlink()
proc = self.svc("ra8", "rdm-admin", check=False)
@@ -141,15 +128,13 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
self.assertIn("无 RDM 域库", proc.stdout)
def test_refuses_when_base_db_missing(self):
port = free_test_port()
self.seed_agent("ra9", port)
self.seed_agent("ra9", 9469)
proc = self.svc("ra9", "rdm-admin", check=False)
self.assertEqual(proc.returncode, 1, proc.stdout)
self.assertIn("既有库不存在", proc.stdout)
def test_unknown_subcommand_usage_exit_2(self):
port = free_test_port()
self.seed_agent("raa", port)
self.seed_agent("raa", 9470)
proc = self.svc("raa", "rdm-admin", "bogus", check=False)
self.assertEqual(proc.returncode, 2, proc.stdout)
self.assertIn("用法", proc.stdout)
@@ -157,16 +142,14 @@ class RdmAdminTests(tsr.SvcAutoRecoverTests):
# ---- status 巡检行 ----
def test_status_lists_rdm_admin_row_and_warning(self):
port = free_test_port()
self.seed_agent("rab", port)
self.seed_agent("rab", 9471)
self.seed_dbs("rab", ledger_owner="u7-admin", base_owners=("weichen",))
out = self.svc("rab", "status").stdout
self.assertIn("RDM 域管理员 u7-admin", out)
self.assertIn("不在基座用户表", out)
def test_status_silent_without_rdm_db(self):
port = free_test_port()
self.seed_agent("rac", port)
self.seed_agent("rac", 9472)
self.seed_dbs("rac")
(self.runtime("rac") / "data" / "rdm.db").unlink()
out = self.svc("rac", "status").stdout
+1 -2
View File
@@ -49,7 +49,6 @@ def ci_git_env():
if not (CI_SERVER and CI_TOKEN):
return {}
scheme = CI_SERVER.split("://", 1)[0]
# 子模块已改用权威 HTTPS 端点;这里只保留权威 SSH 端点的改写,供仍以 SSH 声明 pins 的仓库使用。
return {
"GIT_CONFIG_COUNT": "4",
"GIT_CONFIG_KEY_0": "protocol.%s.allow" % scheme,
@@ -57,7 +56,7 @@ def ci_git_env():
"GIT_CONFIG_KEY_1": "http.%s/.extraheader" % CI_SERVER,
"GIT_CONFIG_VALUE_1": "AUTHORIZATION: token " + CI_TOKEN,
"GIT_CONFIG_KEY_2": "url.%s/weichen/.insteadOf" % CI_SERVER,
"GIT_CONFIG_VALUE_2": "ssh://git@gitea.naitangbaobao.cn:52222/weichen/",
"GIT_CONFIG_VALUE_2": "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/",
"GIT_CONFIG_KEY_3": "core.hooksPath",
"GIT_CONFIG_VALUE_3": "/dev/null",
}
+7 -18
View File
@@ -12,13 +12,9 @@ import os
import shlex
import shutil
import subprocess
import sys
import unittest
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _port_alloc import free_test_port # noqa: E402
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
@@ -110,8 +106,7 @@ class U2ReleaseTests(tsi.IdentityTests):
clock.chmod(0o755)
self.env["PATH"] = str(clock_bin) + os.pathsep + self.env["PATH"]
self.make_src_repo() # 预克隆 srccache(本用例走 --from local,仅保环境一致)
port = free_test_port()
self.seed_agent("u2a", port)
self.seed_agent("u2a", 9461)
self.start_ok("u2a") # legacy 运行中(期望态=running)
self.assertTrue(self.running("u2a"))
@@ -152,8 +147,7 @@ class U2ReleaseTests(tsi.IdentityTests):
# ---- T2:构建失败 → 旧服务保持运行、current 不动 ----
def test_U2_build_failure_keeps_old_running(self):
port = free_test_port()
self.seed_agent("u2b", port)
self.seed_agent("u2b", 9462)
self.set_makefile(self.stub)
self.svc("u2b", "deploy", "--from", "local")
cur1 = self.cur_release("u2b")
@@ -170,8 +164,7 @@ class U2ReleaseTests(tsi.IdentityTests):
# ---- T3:中断切换(journal switched)→ start 入口回退并恢复 ----
def test_U2_journal_interrupt_resolved_on_start(self):
port = free_test_port()
self.seed_agent("u2c", port)
self.seed_agent("u2c", 9463)
self.set_makefile(self.stub)
self.svc("u2c", "deploy", "--from", "local")
v1 = self.cur_release("u2c")
@@ -197,8 +190,7 @@ class U2ReleaseTests(tsi.IdentityTests):
# ---- T4:schema 前进 → 回退阻断,不触碰业务库 ----
def test_U2_schema_advance_blocks_rollback(self):
port = free_test_port()
self.seed_agent("u2d", port)
self.seed_agent("u2d", 9464)
self.set_makefile(self.stub)
self.svc("u2d", "deploy", "--from", "local")
v1 = self.cur_release("u2d")
@@ -238,8 +230,7 @@ class U2ReleaseTests(tsi.IdentityTests):
# ---- T5:legacy 布局未迁移 → 裸 start/restart 仍走原路径(兼容) ----
def test_U2_fault_after_switch_rolls_back_on_next_start(self):
port = free_test_port()
self.seed_agent("u2f", port)
self.seed_agent("u2f", 9466)
self.set_makefile(self.stub)
self.svc("u2f", "deploy", "--from", "local")
v1 = self.cur_release("u2f")
@@ -257,8 +248,7 @@ class U2ReleaseTests(tsi.IdentityTests):
self.svc("u2f", "stop")
def test_U2_health_timeout_rolls_back(self):
port = free_test_port()
self.seed_agent("u2g", port)
self.seed_agent("u2g", 9467)
self.set_makefile(self.stub)
self.svc("u2g", "deploy", "--from", "local")
v1 = self.cur_release("u2g")
@@ -287,8 +277,7 @@ class U2ReleaseTests(tsi.IdentityTests):
return p
def test_U2_legacy_layout_bare_lifecycle_still_works(self):
port = free_test_port()
self.seed_agent("u2e", port)
self.seed_agent("u2e", 9465)
self.start_ok("u2e")
self.assertFalse((self.runtime("u2e") / "current").exists())
self.assertTrue((self.runtime("u2e") / "bin" / "agent-system").exists())
+5 -14
View File
@@ -9,13 +9,9 @@ import importlib.util
import json
import os
import shutil
import sys
import unittest
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _port_alloc import free_test_port # noqa: E402
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
@@ -61,8 +57,7 @@ class UserBindingTests(tsr.SvcAutoRecoverTests):
# ---- T1 合法 manifest:user list 绑定 data_root + --open-existing ----
def test_T1_user_list_binds_manifest_data(self):
port = free_test_port()
self.seed_agent("ub1", port)
self.seed_agent("ub1", 9441)
self.start_ok("ub1")
self.svc("ub1", "stop")
root = self.manifest("ub1")["identity"]["data_root"]
@@ -79,8 +74,7 @@ class UserBindingTests(tsr.SvcAutoRecoverTests):
# ---- T2 禁止覆盖 --data;未绑定默认目录拒绝 ----
def test_T2_reject_override_data_flag(self):
port = free_test_port()
self.seed_agent("ub2", port)
self.seed_agent("ub2", 9442)
self.start_ok("ub2")
self.svc("ub2", "stop")
self.touch_db("ub2")
@@ -112,8 +106,7 @@ class UserBindingTests(tsr.SvcAutoRecoverTests):
# ---- T3 删除 data 根:OpenExisting 失败,不建空库 ----
def test_T3_missing_data_root_rejected(self):
port = free_test_port()
self.seed_agent("ub3", port)
self.seed_agent("ub3", 9444)
self.start_ok("ub3")
self.svc("ub3", "stop")
root = Path(self.manifest("ub3")["identity"]["data_root"])
@@ -131,8 +124,7 @@ class UserBindingTests(tsr.SvcAutoRecoverTests):
# ---- T4 隐藏 flock:user/deploy 拒绝;已运行服务仍在 ----
def test_T4_no_flock_rejects_user_and_deploy(self):
port = free_test_port()
self.seed_agent("ub4", port)
self.seed_agent("ub4", 9445)
self.start_ok("ub4")
self.assertTrue(self.running("ub4"))
self.touch_db("ub4")
@@ -166,8 +158,7 @@ class UserBindingTests(tsr.SvcAutoRecoverTests):
# ---- T5 recovery U1 身份冒烟(完整套件见 test_svc_identity)----
def test_T5_identity_regression_smoke(self):
port = free_test_port()
self.seed_agent("ub5", port)
self.seed_agent("ub5", 9446)
self.start_ok("ub5")
iid = self.manifest("ub5")["instance_id"]
self.svc("ub5", "stop")