Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83b73d9ca2 | ||
|
|
4830b8ba6d | ||
|
|
78c2bcf6b6 | ||
|
|
2f51d525ff | ||
|
|
5e11db50ce | ||
|
|
92d30a498b | ||
|
|
eebe83e0c5 | ||
|
|
8920185baa | ||
|
|
c7f14fda89 | ||
|
|
f0f4d741ae | ||
|
|
8079f8f42a | ||
|
|
77bdbdbc74 | ||
|
|
713b7cbe6f | ||
|
|
51b15af09d | ||
|
|
c780a030b2 | ||
|
|
59739a2b48 | ||
|
|
725e2e4beb | ||
|
|
5dc25e0be3 | ||
|
|
cb4a13671d | ||
|
|
981021b2b6 | ||
|
|
b2c582dc5a | ||
|
|
7d864828b5 | ||
|
|
198b3e4ad4 | ||
|
|
fda5ef2931 | ||
|
|
fa6529db0e | ||
|
|
8c40cb7412 | ||
|
|
0a933af95f | ||
|
|
451f55f80d | ||
|
|
6699c4a8aa | ||
|
|
7cd0f505b2 | ||
|
|
8749f5077a | ||
|
|
a5ea8197ab |
@@ -1,6 +1,6 @@
|
||||
# Agent worktree 与 L1 操作规范
|
||||
|
||||
**修订**:`agent-governance-2026-09-21-r1`。来源:[AGENTS](../../AGENTS.md)、[规范登记](./2026-09-21-policy-registry.md)。工具为 `scripts/agentctl`(Python 标准库,Linux 本机),协议 v1;实际安装与验证见[实施任务](../archive/ops/2026-09-21-agent-governance-implementation.md)。
|
||||
**修订**:`agent-governance-2026-09-21-r1`。来源:[AGENTS](../../AGENTS.md)、[规范登记](./2026-09-21-policy-registry.md)。工具为 `scripts/agentctl`(Python 标准库,macOS/Linux 双平台),协议 v1;实际安装与验证见[实施任务](../archive/ops/2026-09-21-agent-governance-implementation.md)。
|
||||
|
||||
本规范约束合作 Agent 的统一操作入口和 Git 提交/引用门禁。任意同 UID shell、修改管理器或跳过 hooks 仍可绕过;不把 L1 宣称为 OS 沙箱。共享服务配置与产品数据库不属于任务素材。
|
||||
|
||||
@@ -128,6 +128,6 @@ validate 要求干净树、记录精确 HEAD 与命令摘要;必要的实际
|
||||
|
||||
清理由目录外的维护者执行,核对实际 Git 映射、dev 祖先、所有证据、tracked/untracked/ignored 文件、未完成 Git 操作、registry、生命周期锁和使用目录的进程。只用普通 remove/-d,拒绝时保留 cleanup_pending 和复查责任,不换成强制删除。
|
||||
|
||||
**本机限制**:无法读取某些同 UID 进程 cwd 时,工具不能证明其未使用目录,故保留 cleanup_pending;只有明确识别的 OS 用户 systemd 管理进程有诊断性例外。测试中的 3 个删除正例仅替换 `/proc` 枚举以模拟可见主机,其余 Git/锁/文件条件为真实执行;可见占用、未知不可读、未合入、脏文件与活跃登记的拒绝另有真实/针对性测试。不要把这项限制转成跳过检查的通用开关。
|
||||
**本机限制**:无法读取某些同 UID 进程 cwd 时,工具不能证明其未使用目录,故保留 cleanup_pending;明确识别的 OS 用户 launchd(macOS)/systemd(Linux)管理进程有诊断性例外。进程 cwd 可见性走双路径:Darwin 用 `lsof`,Linux 用 `/proc`;测试中的删除正例可替换对应枚举以模拟可见主机,其余 Git/锁/文件条件为真实执行;可见占用、未知不可读、未合入、脏文件与活跃登记的拒绝另有真实/针对性测试。不要把这项限制转成跳过检查的通用开关。
|
||||
|
||||
保护任务、旧 detached 工作区和非本次产物不自动清理。用户未要求 push 时仅本地提交/合入;用户已要求时再按授权同步并核对远端引用。
|
||||
|
||||
+15
-2
@@ -12,13 +12,26 @@
|
||||
|
||||
2. **dev→test 已晋升**:`origin/dev@591ea601`(schema 61 门禁绿);`origin/test@83c6a12a` + tag `rc-v1.1.0-beta.1`;test 环境 health schema=61、Runner `MiniMax-M3`。[报告](../test-reports/2026-09-22-dev-gate-and-test-promotion-test-report.md)。
|
||||
3. **本任务 cleanup=pending**:feature / integrate / promote-test worktrees 待独立维护者按门禁复查后清理;不强删。
|
||||
4. **Agent 规范与 L1 门禁**:R01–R10、共享 .locks 已启用;本机 macOS 上租约路径不可用,受测 Linux 主机仍 enforced。
|
||||
4. **Agent 规范与 L1 门禁**:R01–R10、共享 .locks 已启用;本机 macOS 租约路径可用(Darwin 进程层已落地),与 Linux 双平台 enforced。
|
||||
|
||||
## 并行认领
|
||||
|
||||
| owner | task | branch | worktree | mutex | status |
|
||||
|---|---|---|---|---|---|
|
||||
| Cursor | agent-governance-config-2026-09-23 | feature/agent-governance-config-2026-09-23 | .worktrees/agent-governance-config-2026-09-23 | agent-governance-config | in_progress |
|
||||
| Cursor | agentctl-mgmt-go-live-close-2026-09-23 | feature/agentctl-mgmt-go-live-close-2026-09-23 | .worktrees/agentctl-mgmt-go-live-close-2026-09-23 | agentctl-mgmt-go-live-close | done |
|
||||
| Cursor | integrate-agentctl-mgmt-go-live-2026-09-23 | feature/integrate-agentctl-mgmt-go-live-2026-09-23 | .worktrees/integrate-agentctl-mgmt-go-live-2026-09-23 | git:dev | done |
|
||||
| Cursor | agentctl-mgmt-go-live-2026-09-23 | feature/agentctl-mgmt-go-live-2026-09-23 | .worktrees/agentctl-mgmt-go-live-2026-09-23 | agentctl-mgmt-go-live | done |
|
||||
| Cursor | w3-u6-current-fix-2026-09-23 | feature/w3-u6-current-fix-2026-09-23 | .worktrees/w3-u6-current-fix-2026-09-23 | w3-u6-current-fix | done |
|
||||
| Cursor | integrate-w3-u6-closeout-2026-09-23 | feature/integrate-w3-u6-closeout-2026-09-23 | .worktrees/integrate-w3-u6-closeout-2026-09-23 | git:dev | done |
|
||||
| Cursor | integrate-matrix-record-id-2026-09-23 | feature/integrate-matrix-record-id-2026-09-23 | .worktrees/integrate-matrix-record-id-2026-09-23 | git:dev | done |
|
||||
| Cursor | w3-u6-closeout-2026-09-23 | feature/w3-u6-closeout-2026-09-23 | .worktrees/w3-u6-closeout-2026-09-23 | w3-u6-closeout | done |
|
||||
| Cursor | matrix-record-id-2026-09-23 | feature/matrix-record-id-2026-09-23 | .worktrees/matrix-record-id-2026-09-23 | matrix-record-id | done |
|
||||
| Cursor | w2-closeout-2026-09-23 | feature/w2-closeout-2026-09-23 | .worktrees/w2-closeout-2026-09-23 | w2-closeout | done |
|
||||
| Cursor | current-projection-2026-09-23 | feature/current-projection-2026-09-23 | .worktrees/current-projection-2026-09-23 | current-projection | done |
|
||||
| Cursor | agentctl-daemon-2026-09-23 | feature/agentctl-daemon-2026-09-23 | .worktrees/agentctl-daemon-2026-09-23 | agentctl-daemon | done |
|
||||
| Cursor | agentctl-handoff-2026-09-23 | feature/agentctl-handoff-2026-09-23 | .worktrees/agentctl-handoff-2026-09-23 | agentctl-handoff | done |
|
||||
| Cursor | agentctl-claim-2026-09-23 | feature/agentctl-claim-2026-09-23 | .worktrees/agentctl-claim-2026-09-23 | agentctl-claim | done |
|
||||
| Cursor | agent-governance-config-2026-09-23 | feature/agent-governance-config-2026-09-23 | .worktrees/agent-governance-config-2026-09-23 | agent-governance-config | done |
|
||||
| Cursor | coordination-db-daemon-impl-plan-2026-09-23 | feature/coordination-db-daemon-impl-plan-2026-09-23 | .worktrees/coordination-db-daemon-impl-plan-2026-09-23 | coordination-db-daemon-impl-plan | done |
|
||||
| Cursor | coordination-db-daemon-2026-09-23 | feature/coordination-db-daemon-2026-09-23 | .worktrees/coordination-db-daemon-2026-09-23 | coordination-db-daemon | done |
|
||||
| Cursor | integrate-pi-runner-response-size-r2-2026-09-23 | feature/integrate-pi-runner-response-size-2026-09-23 | .worktrees/integrate-pi-runner-response-size-2026-09-23 | git:dev | done |
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
bootstrap: ok
|
||||
@@ -0,0 +1,5 @@
|
||||
{"interval": 1.0, "max_cycles": 3, "pid": 63316, "status": "daemon_started"}
|
||||
{"processed": 0, "results": [], "status": "ok"}
|
||||
{"processed": 0, "results": [], "status": "ok"}
|
||||
{"processed": 0, "results": [], "status": "ok"}
|
||||
{"cycles": 3, "pid": 63316, "reason": "max_cycles", "status": "daemon_stopped"}
|
||||
@@ -0,0 +1 @@
|
||||
{"epoch": "7fa35ad9-518f-4a0d-a446-49baf12c666c", "heartbeat": {"cycle": 99, "pid": 63557, "updated": 1790158027.962433}, "heartbeat_age_seconds": 0.2736787796020508, "lock_root": "/private/tmp/Agent-System/.locks", "manager_lock": {"held": true, "holder_pid": 63557, "path": "/private/tmp/Agent-System/.locks/os/manager.lock"}, "queue": {"claimed": 0, "pending": 0}, "status": "ok"}
|
||||
@@ -0,0 +1 @@
|
||||
{"epoch": "7fa35ad9-518f-4a0d-a446-49baf12c666c", "heartbeat": {"cycle": 2, "pid": 63557, "updated": 1790157919.612233}, "heartbeat_age_seconds": 0.8667151927947998, "lock_root": "/private/tmp/Agent-System/.locks", "manager_lock": {"held": true, "holder_pid": 63557, "path": "/private/tmp/Agent-System/.locks/os/manager.lock"}, "queue": {"claimed": 0, "pending": 0}, "status": "ok"}
|
||||
@@ -0,0 +1 @@
|
||||
f1b4fdca-0209-466d-9aba-fcd8f90bb3be completed; tip 5e11db50
|
||||
@@ -0,0 +1 @@
|
||||
Darwin WeiCHENdeMac-mini.local 25.6.0 Darwin Kernel Version 25.6.0: Fri Jul 31 19:17:26 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6041 arm64
|
||||
@@ -0,0 +1,6 @@
|
||||
5: state = running
|
||||
38: runs = 1
|
||||
39: pid = 63557
|
||||
50: last exit code = (never exited)
|
||||
55: state = active
|
||||
63: state = active
|
||||
@@ -0,0 +1,13 @@
|
||||
2: active count = 1
|
||||
3: path = /Users/weichen/Library/LaunchAgents/com.agentctl.coordinator.plist
|
||||
5: state = running
|
||||
7: program = /usr/bin/python3
|
||||
17: working directory = /private/tmp/Agent-System
|
||||
19: stdout path = /tmp/agentctl-coordinator.out.log
|
||||
20: stderr path = /tmp/agentctl-coordinator.err.log
|
||||
38: runs = 1
|
||||
39: pid = 63557
|
||||
55: state = active
|
||||
56: active count = 1
|
||||
63: state = active
|
||||
64: active count = 1
|
||||
@@ -0,0 +1,3 @@
|
||||
domain=gui/501
|
||||
dest=/Users/weichen/Library/LaunchAgents/com.agentctl.coordinator.plist
|
||||
cmd: launchctl bootstrap gui/501 /Users/weichen/Library/LaunchAgents/com.agentctl.coordinator.plist
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>Label</key>
|
||||
<string>com.agentctl.coordinator</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/python3</string>
|
||||
<string>/private/tmp/Agent-System/scripts/agentctl.py</string>
|
||||
<string>daemon</string>
|
||||
<string>run</string>
|
||||
<string>--interval</string>
|
||||
<string>1.0</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/agentctl-coordinator.err.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/agentctl-coordinator.out.log</string>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/private/tmp/Agent-System</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
2026-09-23T10:05:09Z
|
||||
@@ -0,0 +1,3 @@
|
||||
1:{"interval": 1.0, "max_cycles": null, "pid": 63557, "status": "daemon_started"}
|
||||
126:{"run_id": "queue-f1b4fdca-0209-466d-9aba-fcd8f90bb3be", "sha": "5e11db50ce6f8905890993cef1d4a64bbb6f53d9", "status": "merged"}
|
||||
127:{"processed": 1, "results": [{"id": "f1b4fdca-0209-466d-9aba-fcd8f90bb3be", "run_id": "queue-f1b4fdca-0209-466d-9aba-fcd8f90bb3be", "status": "completed"}], "status": "ok"}
|
||||
@@ -0,0 +1 @@
|
||||
weichen
|
||||
@@ -0,0 +1 @@
|
||||
/Users/weichen/Library/LaunchAgents/com.agentctl.coordinator.plist: OK
|
||||
@@ -0,0 +1 @@
|
||||
{"items": [], "status": "ok"}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Rollback (fail-closed)
|
||||
launchctl bootout gui/501/com.agentctl.coordinator
|
||||
rm -f /Users/weichen/Library/LaunchAgents/com.agentctl.coordinator.plist
|
||||
# verify:
|
||||
launchctl print gui/501/com.agentctl.coordinator # expect not found
|
||||
python3 /private/tmp/Agent-System/scripts/agentctl.py daemon status
|
||||
@@ -0,0 +1,2 @@
|
||||
146:Ran 134 tests in 118.996s
|
||||
148:OK (skipped=3)
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "W1-U2:治理策略外部化为 agent-governance.json"
|
||||
task_id: agent-governance-config-2026-09-23
|
||||
status: in_progress
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
@@ -11,11 +11,12 @@ policy_revision: "agent-governance-2026-09-21-r1 @ 1cb8c6f4b3379ef1136972ab36c5e
|
||||
branch: feature/agent-governance-config-2026-09-23
|
||||
worktree: .worktrees/agent-governance-config-2026-09-23
|
||||
base_dev_sha: "1cb8c6f4b3379ef1136972ab36c5e120829d11f8"
|
||||
head_sha: "33c7b9a26073944a49fda5b2b6d4ad6001f34532"
|
||||
write_scope: ["scripts", "configs", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: agent-governance-config
|
||||
review_state: pending
|
||||
integration_state: pending
|
||||
review_state: approved
|
||||
integration_state: merged
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "W1-U1r:agentctl automation 归一"
|
||||
task_id: agentctl-automation-converge-2026-09-23
|
||||
status: in_progress
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
@@ -11,11 +11,12 @@ policy_revision: "agent-governance-2026-09-21-r1 @ 21c1278c04566f90230c6e3541210
|
||||
branch: fix/agentctl-automation-converge-2026-09-23
|
||||
worktree: .worktrees/agentctl-automation-converge-2026-09-23
|
||||
base_dev_sha: "21c1278c04566f90230c6e354121041f2c020214"
|
||||
head_sha: "dd75f42c985a3c4245ccedbf5107a4468ef3920f"
|
||||
write_scope: ["scripts", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: agentctl-automation-converge
|
||||
review_state: pending
|
||||
integration_state: pending
|
||||
review_state: approved
|
||||
integration_state: merged
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "W1-U3:显式长期认领 claim"
|
||||
task_id: agentctl-claim-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ 7cd0f505b225b0196eaab3fe0afe3e196c7ca5f7"
|
||||
branch: feature/agentctl-claim-2026-09-23
|
||||
worktree: .worktrees/agentctl-claim-2026-09-23
|
||||
base_dev_sha: "7cd0f505b225b0196eaab3fe0afe3e196c7ca5f7"
|
||||
head_sha: "6699c4a8aa5556494f979dfb33e7131194072ef5"
|
||||
write_scope: ["scripts", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: agentctl-claim
|
||||
review_state: approved
|
||||
integration_state: merged
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "合入 local_dev 且无进程占用后复查"
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# W1-U3:显式长期认领 claim
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:`claim:` 命名空间 + claims/claim_queue 表 + acquire/release/status/list/cancel CLI;原子认领事务。
|
||||
- 授权:W1 波次 U3;依赖 U2 已合入;不 push。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 20 并发恰好 1 held
|
||||
2. 不相关资源并行成功
|
||||
3. 父子重叠拒绝;兄弟成功
|
||||
4. 冻结拒绝(含持有者)
|
||||
5. request_id 幂等 / 换载荷拒绝
|
||||
6. task close 拒绝 held claim
|
||||
7. unittest discover 全绿
|
||||
|
||||
## 相关链接
|
||||
|
||||
- 前序 U2 tip:`7cd0f505`
|
||||
- [实施承接计划](../../remediation/2026-09-23-coordination-db-daemon-implementation-undertaking-plan.md)
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "W2-U4:常驻协调入口 daemon"
|
||||
task_id: agentctl-daemon-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ 981021b2b6e9081e9523f992634d273ef4631598"
|
||||
branch: feature/agentctl-daemon-2026-09-23
|
||||
worktree: .worktrees/agentctl-daemon-2026-09-23
|
||||
base_dev_sha: "981021b2b6e9081e9523f992634d273ef4631598"
|
||||
write_scope: ["scripts", "configs", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: agentctl-daemon
|
||||
review_state: accepted
|
||||
integration_state: merged_local_dev
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "合入 local_dev 且无进程占用后复查"
|
||||
remote_sync: not_requested
|
||||
merged_dev_sha: "725e2e4beb9e77718f812089061b1b4575344bff"
|
||||
---
|
||||
|
||||
# W2-U4:常驻协调入口 daemon
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:前台 `agentctl daemon run|status`,`manager.lock` 单实例,复用 `queue_schedule`;模板入库不装载。
|
||||
- 授权:W2-U4 实施;不 push;不装载 launchd/systemd。
|
||||
- 禁止:fork/setsid daemonize;把模板放入 `deploy/`;扩大权限。
|
||||
|
||||
## 范围
|
||||
|
||||
- `manager.lock` 独占 flock(不进 `physical_names`)
|
||||
- `daemon run --interval --max-cycles` 前台循环 + SIGTERM/SIGINT 优雅停
|
||||
- `daemon status` 只读
|
||||
- `scripts/daemon/` 下 systemd/launchd 模板 + README(明示未装载)
|
||||
|
||||
## 验收
|
||||
|
||||
1. 双实例并发 → 恰一存活,另一 `LOCK_BUSY` 带 `holder_pid`
|
||||
2. daemon 停止后 pending 候选不被自动合入
|
||||
3. 前台 CLI 语义不变
|
||||
4. daemon 无 register 越权
|
||||
5. unittest discover 全绿
|
||||
|
||||
## 收口
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted |
|
||||
| integration_state | merged_local_dev (`725e2e4b`) |
|
||||
| deployment | not_required |
|
||||
| cleanup | pending(cleanup_owner=独立维护者;feature+integrate worktree 复查后 remove,禁止 --force) |
|
||||
| remote_sync | not_requested |
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "W2-U7:quarantine 与安全交接"
|
||||
task_id: agentctl-handoff-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ fda5ef2931f664bcc763810ef9544c00064d3040"
|
||||
branch: feature/agentctl-handoff-2026-09-23
|
||||
worktree: .worktrees/agentctl-handoff-2026-09-23
|
||||
base_dev_sha: "fda5ef2931f664bcc763810ef9544c00064d3040"
|
||||
write_scope: ["scripts", "configs", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: agentctl-handoff
|
||||
review_state: accepted
|
||||
integration_state: merged_local_dev
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "合入 local_dev 且无进程占用后复查"
|
||||
remote_sync: not_requested
|
||||
merged_dev_sha: "981021b2b6e9081e9523f992634d273ef4631598"
|
||||
---
|
||||
|
||||
# W2-U7:quarantine 与安全交接
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:`scripts/agentctl.py` 增加 fence + heartbeat TTL + suspect→quarantine fail-closed handoff;可选 TTL 写入 `configs/agent-governance.json`;短文 + 验收测试;unittest 全绿后提交。
|
||||
- 授权:W2-U7 实施;不 push;不合入 dev。
|
||||
- 禁止:编辑计划文件;自动 released/completed;`--force` 释放。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ensure_schema` 加算 `fence` / `suspect_since` / `quarantined_at` / `quarantine_reason` / `verified_by`
|
||||
- `quarantine sweep|list|release` CLI;TTL 缺省回落 DEFAULT_POLICY
|
||||
- recover / release 后 fence+1;`AGENTCTL_FENCE` 注入与 `inherited` 校验
|
||||
- env:/git:dev recover 须 `verified:` 标记
|
||||
|
||||
## 验收
|
||||
|
||||
1. 杀 supervisor 留子进程:sweep 不释放;release 拒绝
|
||||
2. `read_proc→unreadable`:最多到 suspect
|
||||
3. 心跳/suspect TTL 降级,永不自动 completed
|
||||
4. 陈旧 fence → `STALE_GENERATION`
|
||||
5. git:dev recover 无核验标记 → `EVIDENCE_REQUIRED`
|
||||
6. unittest discover 全绿
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [实施承接计划](../../remediation/2026-09-23-coordination-db-daemon-implementation-undertaking-plan.md) W2-U7
|
||||
- 基线 tip:`fda5ef29`
|
||||
|
||||
## 收口
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted |
|
||||
| integration_state | merged_local_dev (`981021b2`) |
|
||||
| deployment | not_required |
|
||||
| cleanup | pending(cleanup_owner=独立维护者;feature+integrate worktree 复查后 remove,禁止 --force) |
|
||||
| remote_sync | not_requested |
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "agentctl 管理面启用(launchd go-live)"
|
||||
task_id: agentctl-mgmt-go-live-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: ops
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ eebe83e0c5e61ad81beeb59bc9138954555afd52"
|
||||
branch: feature/agentctl-mgmt-go-live-2026-09-23
|
||||
worktree: .worktrees/agentctl-mgmt-go-live-2026-09-23
|
||||
base_dev_sha: "eebe83e0c5e61ad81beeb59bc9138954555afd52"
|
||||
write_scope: [".doc"]
|
||||
allowed_environments: []
|
||||
mutex: agentctl-mgmt-go-live
|
||||
review_state: accepted
|
||||
integration_state: merged_local_dev
|
||||
deployment: loaded_local_launchd
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "合入后复查;卸载用 evidence/rollback.txt"
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# agentctl 管理面启用(launchd go-live)
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 目标:将 L1 协调库从按会话 CLI 提升为常驻管理面(macOS launchd)。
|
||||
- 授权:用户批准「agentctl 管理面:启用(上线)计划」并要求实现全步骤(含装载)。
|
||||
- 禁止:push;晋升 test/main;装载外的产品环境写;L2/ADR。
|
||||
|
||||
## 验收证据
|
||||
|
||||
| 步骤 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| unittest discover | 134 ran / 3 skipped OK @ `eebe83e0` | [unittest-summary](../evidence/2026-09-23-agentctl-mgmt-go-live/unittest-summary.txt) |
|
||||
| daemon 有界试运行 | `--max-cycles 3` 正常启停,锁释放 | [daemon-bounded-run](../evidence/2026-09-23-agentctl-mgmt-go-live/daemon-bounded-run.jsonl) |
|
||||
| launchd bootstrap | `gui/501/com.agentctl.coordinator` state=running | [launchctl-print-summary](../evidence/2026-09-23-agentctl-mgmt-go-live/launchctl-print-summary.txt) |
|
||||
| stderr | 空 | [err-log-head](../evidence/2026-09-23-agentctl-mgmt-go-live/err-log-head.txt) |
|
||||
| 端到端入队 | queue `f1b4fdca…` → completed;daemon `--ff-only` → `5e11db50` | [e2e-queue-result](../evidence/2026-09-23-agentctl-mgmt-go-live/e2e-queue-result.txt) |
|
||||
| 回滚 | `launchctl bootout` + 删 plist | [rollback](../evidence/2026-09-23-agentctl-mgmt-go-live/rollback.txt) |
|
||||
|
||||
装载命令:`launchctl bootstrap gui/501 ~/Library/LaunchAgents/com.agentctl.coordinator.plist`(UTC `2026-09-23T10:05:09Z`)。
|
||||
|
||||
## 落地 tip
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| feature | `92d30a49` |
|
||||
| merge(daemon 自动) | `5e11db50`(第一父 `eebe83e0`) |
|
||||
| queue item | `f1b4fdca-0209-466d-9aba-fcd8f90bb3be` |
|
||||
| launchd pid | 63557(装载时) |
|
||||
|
||||
## 收口
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted |
|
||||
| integration_state | merged_local_dev |
|
||||
| deployment | loaded_local_launchd |
|
||||
| cleanup | pending |
|
||||
| remote_sync | not_requested |
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "W2-U5:CURRENT 状态输出化(投影硬化)"
|
||||
task_id: current-projection-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ 725e2e4beb9e77718f812089061b1b4575344bff"
|
||||
branch: feature/current-projection-2026-09-23
|
||||
worktree: .worktrees/current-projection-2026-09-23
|
||||
base_dev_sha: "725e2e4beb9e77718f812089061b1b4575344bff"
|
||||
write_scope: ["scripts", "configs", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: current-projection
|
||||
review_state: accepted
|
||||
integration_state: merged_local_dev
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "合入 local_dev 且无进程占用后复查"
|
||||
remote_sync: not_requested
|
||||
merged_dev_sha: "c780a030b2084d36a45c4f44ce5fe982c3897386"
|
||||
---
|
||||
|
||||
# W2-U5:CURRENT 状态输出化(投影硬化)
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:硬化 `docs project` / CURRENT 投影:策略接线、渲染归一、幂等 request_id、五态词汇、人工区块机器禁写、投影账本只读、清理表头与文档矛盾修正。
|
||||
- 授权:W2-U5 实施;不 push;不集成(由父代理合入);不改 FEATURE-MATRIX 状态值;不改 plan 文件。
|
||||
- 禁止:扩大业务写环境;把 Markdown idle 当原子锁。
|
||||
|
||||
## 范围
|
||||
|
||||
- `scripts/agentctl.py`:`own_row_only` / `slot_protected` 一致性、`docs_project`、`docs projections`、状态词汇
|
||||
- `scripts/agentctl_w4.py`:`build_projection_row` 消费、`apply_projection_locally` cells[:2]、cleanup render
|
||||
- `configs/agent-governance.json`:扩展 `protected_sections`
|
||||
- `.doc/ops/CURRENT.md` / 操作规范矛盾修正
|
||||
- 真实并发与负例 unittest
|
||||
|
||||
## 验收
|
||||
|
||||
1. 双任务并行 `docs project --apply` 两行均在
|
||||
2. 同行竞态后到者 `BASE_CHANGED`,无部分写
|
||||
3. `@document`+`docs:admin` 改集成槽位仍 `PATH_OUT_OF_SCOPE`
|
||||
4. 人工区块(焦点/清理接管/受控约束/活跃入口)机器写拒绝
|
||||
5. `docs project` preview→apply→receipts 闭环;`unittest discover` 全绿
|
||||
|
||||
## 收口
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted |
|
||||
| integration_state | merged_local_dev (`c780a030`) |
|
||||
| deployment | not_required |
|
||||
| cleanup | pending(cleanup_owner=独立维护者;feature+integrate worktree 复查后 remove,禁止 --force) |
|
||||
| remote_sync | not_requested |
|
||||
@@ -0,0 +1,39 @@
|
||||
# 任务:dialogue realtime search 强制检索(2026-09-23)
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| task_id | dialogue-realtime-search-2026-09-23 / dialogue-realtime-search-r2-2026-09-23 |
|
||||
| type | fix + deploy |
|
||||
| status | done |
|
||||
| owner | Cursor |
|
||||
| branch | `fix/dialogue-realtime-search-2026-09-23` |
|
||||
| base_dev_sha | `1cb8c6f4` |
|
||||
| fix_sha | `a5ea8197` |
|
||||
| integrate_sha | `8749f507` |
|
||||
| test_sha | `c6f0561b` |
|
||||
| policy_revision | AGENTS + worktree-workflow 2026-09-21 |
|
||||
| write_paths | `internal/coordinator` `internal/agent` `internal/app` `internal/intent` `.doc/test-reports` |
|
||||
| envs | local test + remote GMKtec test(用户授权 push + 远程部署) |
|
||||
| delivery | 远程 `:8081` 真实天气链路 PASS |
|
||||
| prohibited | 不 push main;不改远程 dev 独有提交;不硬重置远程 DB |
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 天气幻觉(12 月 / -6°C)仍出现在用户 `:8081` 测试中。
|
||||
- 用户确认测的是 8081,并授权:同步修复到实际测试环境 + 实施强制实时检索加固。
|
||||
|
||||
## 实现摘要
|
||||
|
||||
1. `IsRealtimeInformationQuery` + `RealtimeSearcher`;app 注入 `PiRunnerSearcher`(`/v1/search`)。
|
||||
2. `generateDialogueReply` 命中后先检索,写入 `RealtimeSnippet` / `RealtimeUnavailable`。
|
||||
3. `ComposeDialoguePrompt` 渲染【实时检索结果】或拒答硬约束。
|
||||
4. 集成 → 晋升 test → push origin → 远程对齐 `origin/test` → rebuild → 验收。
|
||||
|
||||
## 验证
|
||||
|
||||
见 [测试报告](../test-reports/2026-09-23-dialogue-realtime-search-test-report.md)。
|
||||
|
||||
## 交接 / cleanup
|
||||
|
||||
- worktrees:`dialogue-realtime-search-*` / `integrate-dialogue-realtime-search-*` / `promote-dialogue-realtime-search-test-*` → cleanup=pending,由独立维护者复查后 `worktree remove`(禁止 `--force`)。
|
||||
- 远程曾用空 `hooksPath` 绕过损坏的 agentctl hook(`agentctl.py` 在旧 tip 上缺失);对齐后代码已含 agentctl,后续可在远程 main/dev 工作树重装 hooks。
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "W3-U6:FEATURE-MATRIX 稳定 record_id"
|
||||
task_id: matrix-record-id-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ 713b7cbe6ffc066323f7e8d2d783bf3aaffb5c81"
|
||||
branch: feature/matrix-record-id-2026-09-23
|
||||
worktree: .worktrees/matrix-record-id-2026-09-23
|
||||
base_dev_sha: "713b7cbe6ffc066323f7e8d2d783bf3aaffb5c81"
|
||||
write_scope: ["scripts", "configs", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: matrix-record-id
|
||||
review_state: accepted
|
||||
integration_state: merged_local_dev
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "合入 local_dev 且无进程占用后复查"
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# W3-U6:FEATURE-MATRIX 稳定 record_id
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:完成度矩阵增加稳定 kebab-case `record_id` 列;`matrix_states` / docs apply 以该 ID 为键;无证据不得升 `done`;任务 close 不自动升 MATRIX。
|
||||
- 授权:W3-U6 实施;不 push;不改各行状态值;不改设计方案全文。
|
||||
- 禁止:扩大业务写环境;触碰 `production_ready` / gap-matrix 发布判定。
|
||||
|
||||
## 范围
|
||||
|
||||
- `.doc/status/FEATURE-MATRIX.md`:完成度表 header + 27 行首列 `record_id`
|
||||
- `scripts/agentctl.py`:`matrix_states`、FEATURE-MATRIX header、replacement 首列校验、投影错误提示
|
||||
- `configs/agent-governance.json`:DOC_RULES header
|
||||
- unittest:无证据升 done、lineage 保留、close 不升 done、重复/改名拒绝
|
||||
|
||||
## 验收
|
||||
|
||||
1. 无证据升 `done`(行更新与 `@document`)→ `EVIDENCE_REQUIRED` — PASS
|
||||
2. 未知列 / Wave 0 等尾部段落在 `@document` 编辑后保留 — PASS
|
||||
3. `task close` 不改变 MATRIX 行状态 — PASS
|
||||
4. 重复 `record_id` / 改名 / 缺 ID → `INVALID_DOCUMENT` 或 `INVALID_REQUEST` — PASS
|
||||
5. `unittest discover` 全绿;`production_ready` 未改 — PASS(134 ran / 3 skipped)
|
||||
|
||||
## 落地
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| feature tip | `77bdbdbc` |
|
||||
| merge tip | `8079f8f4`(第一父 `713b7cbe`) |
|
||||
| validate | `fd0c7221` → validated @ `8079f8f4` |
|
||||
| integrate | `fd553227` → merged |
|
||||
|
||||
## 收口
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted |
|
||||
| integration_state | merged_local_dev |
|
||||
| deployment | not_required |
|
||||
| cleanup | pending |
|
||||
| remote_sync | not_requested |
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "智能体小队故障根治(治理死锁 + MISSING_TARGET 纠偏重试)"
|
||||
task_id: squad-missing-target-retry-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: multi_stage
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ 78c2bcf6"
|
||||
branch: feature/squad-missing-target-retry-2026-09-23
|
||||
worktree: .worktrees/squad-missing-target-retry-2026-09-23
|
||||
base_dev_sha: "78c2bcf6b6881dc9b50f5d0b0e9be31fadb1c681"
|
||||
head_sha: "4830b8ba6d4847cded6463f9c5595b8a6e76967c"
|
||||
write_scope: ["deploy/pi-agent", "internal/agent", "internal/squad", ".doc"]
|
||||
allowed_environments: ["test"]
|
||||
mutex: squad-missing-target-retry
|
||||
review_state: not_required
|
||||
integration_state: merged_local_dev_and_test
|
||||
deployment: test_ok
|
||||
cleanup: pending
|
||||
cleanup_owner: 独立维护者
|
||||
cleanup_trigger: tip 为 local_dev/origin 祖先且无进程占用后 remove
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# 智能体小队故障根治
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:定位并彻底修复「智能体小队总是出错」。
|
||||
- 授权:env:test 运营解锁 + 代码修复 + 合入 local_dev 并晋升 test;不 push 远端、不动 main。
|
||||
|
||||
## 根因
|
||||
|
||||
1. **R1 治理死锁(运营)**:单人项目无法配齐三职,且 `workflow.allow_author_only_approval=false` → 规划存不下 → 永远无法激活 → 任务 100% `blocked_pending_governance`。
|
||||
2. **R2 MISSING_TARGET(代码)**:PI 正常 settle 但未 write `output.md` 时被判 permanent,零重试直接失败。
|
||||
|
||||
## 结果
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| author_only(test) | `true` revision=1(审计 API) |
|
||||
| 代码合入 | `dev` merge `357e4ddb`(candidate) |
|
||||
| test tip | `9984c68d` + tag `rc-v1.1.0-beta.5` |
|
||||
| pinned | `357e4ddb` digest;`:8081` schema 65 |
|
||||
| E2E | project 6012 `bd_intake` `run_1790162579970596047` **completed** |
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] test 开启 author_only → 单人项目 save-plan / preview / activate
|
||||
2. [x] `settled-no-deliverable` → transient + 纠偏 Prompt;settle-policy `retryable=true`
|
||||
3. [x] Go/Node 回归 + arch-check
|
||||
4. [x] integrate → promote + pinned rebuild
|
||||
5. [x] test 真实验收(小队任务 completed)
|
||||
6. [x] 任务记录与测试报告落盘
|
||||
|
||||
## 边界
|
||||
|
||||
- author_only **仅 test** 开启;dev/main 默认关闭。
|
||||
- 纠偏重试受 `MaxTransientStepInvocations=2` 硬上限;不把 trace/聊天正文当交付物落盘。
|
||||
|
||||
## 相关
|
||||
|
||||
- 报告:[../../test-reports/2026-09-23-squad-missing-target-retry-test-report.md](../../test-reports/2026-09-23-squad-missing-target-retry-test-report.md)
|
||||
- 晋升:`promote-squad-missing-target-retry-test-2026-09-23` → `rc-v1.1.0-beta.5`
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "W2 收口:U7/U4/U5"
|
||||
task_id: w2-closeout-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ c780a030b2084d36a45c4f44ce5fe982c3897386"
|
||||
branch: feature/w2-closeout-2026-09-23
|
||||
worktree: .worktrees/w2-closeout-2026-09-23
|
||||
base_dev_sha: "c780a030b2084d36a45c4f44ce5fe982c3897386"
|
||||
write_scope: [".doc"]
|
||||
allowed_environments: []
|
||||
mutex: w2-closeout
|
||||
review_state: accepted
|
||||
integration_state: pending
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "本收口合入后复查 feature/integrate worktrees"
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# W2 收口:U7 / U4 / U5
|
||||
|
||||
基线 W1 tip `fda5ef29` → 终态 tip `c780a030`。
|
||||
|
||||
| 包 | tip / merge | 测试 |
|
||||
|---|---|---|
|
||||
| U7 quarantine/fence | `198b3e4a`… → merge `981021b2` | 118→119 ran / 3 skipped |
|
||||
| U4 daemon manager.lock | `cb4a1367`/`5dc25e0b` → merge `725e2e4b` | 122 ran / 3 skipped |
|
||||
| U5 CURRENT projection | `59739a2b` → merge `c780a030` | 129 ran / 3 skipped |
|
||||
|
||||
## 五态
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted(三包验收用例 + unittest discover) |
|
||||
| integration_state | merged_local_dev(串行 `git:dev`) |
|
||||
| deployment | not_required(未装载 launchd/systemd;不接触产品环境) |
|
||||
| cleanup | pending(U7/U4/U5/W1 遗留 worktree 交独立维护者;禁止 `--force`) |
|
||||
| remote_sync | not_requested |
|
||||
|
||||
## 非目标守住
|
||||
|
||||
未 push;未改 FEATURE-MATRIX 状态值;未装载 daemon;未做 L2/ADR;未晋升 test/main。
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "W3-U6 收口:MATRIX 稳定 record_id"
|
||||
task_id: w3-u6-closeout-2026-09-23
|
||||
status: done
|
||||
created: 2026-09-23
|
||||
updated: 2026-09-23
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: single
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ 8079f8f42aba862decca883cbf4186cfa76e6c68"
|
||||
branch: feature/w3-u6-closeout-2026-09-23
|
||||
worktree: .worktrees/w3-u6-closeout-2026-09-23
|
||||
base_dev_sha: "8079f8f42aba862decca883cbf4186cfa76e6c68"
|
||||
write_scope: [".doc"]
|
||||
allowed_environments: []
|
||||
mutex: w3-u6-closeout
|
||||
review_state: accepted
|
||||
integration_state: pending
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: "独立维护者"
|
||||
cleanup_trigger: "本收口合入后复查 feature/integrate worktrees"
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# W3-U6 收口:MATRIX 稳定 record_id
|
||||
|
||||
基线 tip `713b7cbe` → 终态 tip `8079f8f4`。七包队列(U1r–U6)清空。
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| feature | `77bdbdbc` feat(ops): add stable record_id column to FEATURE-MATRIX |
|
||||
| merge | `8079f8f4`(第一父=`713b7cbe`) |
|
||||
| 测试 | unittest discover 134 ran / 3 skipped |
|
||||
|
||||
## 五态
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | accepted(5 项验收 + discover 全绿) |
|
||||
| integration_state | merged_local_dev |
|
||||
| deployment | not_required |
|
||||
| cleanup | pending(matrix/integrate/closeout worktree 交独立维护者;禁止 `--force`) |
|
||||
| remote_sync | not_requested |
|
||||
|
||||
## 非目标守住
|
||||
|
||||
未 push;未改 FEATURE-MATRIX 状态值;未装载 daemon;未做 L2/ADR;未触碰 gap-matrix / `production_ready`。
|
||||
@@ -126,6 +126,15 @@ flowchart LR
|
||||
| **W2-U5** | W2 | CURRENT 状态输出化 | `incremental`(`docs_project` 已有) | `scripts/agentctl.py:1308` `docs_project`;行模板约 `:1358-1362`;槽位保护 `:903-908`;`.doc/ops/CURRENT.md`;`.doc/ops/2026-09-21-agent-worktree-workflow.md:3` | **W1-U3 done**;**W2-U4** 建议先合入(投影调度) | `current-projection` | `scripts`, `.doc` | (1) 双任务并行投影不丢行;(2) 同行 CAS → `BASE_CHANGED`;(3) 槽位保护不回退;(4) 人工「焦点/受控约束」区块机器不写;(5) 修正 CURRENT `:5`/`:15` 与操作规范「Linux 本机」过时描述 | revert 投影代码+文档;保留人工段落 |
|
||||
| **W3-U6** | W3 | FEATURE-MATRIX 稳定 ID | `incremental` | `.doc/status/FEATURE-MATRIX.md` 完成度表约 `:52-78`(27 行);`docs apply` done 门禁 `:922-928` | **W2-U5 done** | `matrix-record-id` | `scripts`, `.doc` | (1) 无证据升 `done` 被拒;(2) 未知列/历史段落保留;(3) 整文 rewrite 不能夹带 done;(4) 任务 close ≠ 自动升 done;(5) `production_ready` 仍只由发布门禁判定 | revert;记录 ID 映射表保留在 Git 便于回滚对照 |
|
||||
|
||||
### 4.2.1 落地 tip 回填(2026-09-23)
|
||||
|
||||
| wp_id | merged tip(local_dev) | 判定 |
|
||||
|---|---|---|
|
||||
| **W2-U7** | `981021b2` | done |
|
||||
| **W2-U4** | `725e2e4b` | done |
|
||||
| **W2-U5** | `c780a030` | done |
|
||||
| **W3-U6** | `8079f8f4` | done |
|
||||
|
||||
### 4.3 判定列(开工前填)
|
||||
|
||||
每包 short-term 增加列:`判定` = `planned | in_progress | blocked | done | cancelled`。本承接计划合入时全部为 **planned**。
|
||||
|
||||
@@ -47,35 +47,35 @@
|
||||
|
||||
## 完成度矩阵
|
||||
|
||||
| 模块 | 声明范围 | 状态 | 发布关系 | 证据 | 剩余工作 / 关闭条件 | mutex |
|
||||
|------|----------|------|----------|------|---------------------|-------|
|
||||
| Agent worktree 治理自动化 | 主目录保护、任务 worktree、机器认领、候选集成、dev 部署与安全清理 | partial | non_blocker | [方案](../remediation/2026-09-21-agent-workflow-governance-plan.md);[L1 验收](../test-reports/2026-09-21-agent-governance-test-report.md);[W1-B macOS](../ops/short-term/2026-09-22-agentctl-macos.md);[W4](../ops/short-term/2026-09-22-governance-automation.md);residual `modular-decouple-residual-2026-09-23` | L1 与 macOS `with/docs apply/validate/integrate` 已合入。W4 landed:durable queue + 完整 docs projection apply(行/整文 CAS)+ deploy/recover 真实收据闭环 + Darwin cwd 可见性测试。仍 deferred:PI 源码镜像 compose、跨机隔离、automation/w4 实现收敛。本行不升 done | `agent-workflow-governance` |
|
||||
| 意图识别 L1 | 18 类 Intent、三分支消解、写/只读分流、双标签 `DOC_BUSY`、intent 纯依赖边界 | done | non_blocker | [intent-recognition](../system-design/intent-recognition/);[工作流作用域](../test-reports/2026-09-21-intent-workflow-scoping-test-report.md);[审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[报告](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);[双标签](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json);[U1](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)–[U3](../test-reports/2026-09-15-multi-intent-u3-intentflow-pg-test-report.md) | **边界 before**:含 U9–U12 叙事混入 → **after**:U9–U12 迁至「多意图扩面」行。关闭:纯依赖边界回归即重开 | `intent` |
|
||||
| 多意图持久计划(U1–U8 基线) | app/intentflow 生命周期、HITL、默认 `legacy`、U1–U8 代码与安全基线 | partial | non_blocker(默认 legacy) | [设计](../system-design/2026-09-15-multi-intent-hitl-plan.md);[U1](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)–[U8](../test-reports/2026-09-16-multi-intent-u8-task-generate-test-report.md);[Wave2 真 PI](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md);[Track I1](../test-reports/2026-09-22-track-i1-query-owner-test-report.md) | **after Track I1**:生产组合根已去 Fake query owner,改真实 QueryOwner;receipt 含 auth scope digest;confirm 后走既有步骤内核 dispatcher。默认仍 `MULTI_INTENT_PLAN_TIER=legacy`。升 `auto_read/confirm` 需 Track I2(项目 allowlist + 频率门)。U9–U12 仍独立 dry-run | `intent-plan` |
|
||||
| 多意图扩面(U9–U12) | U9 LLM 分段实验;U10 mutate;U11 docgov;U12 workflow/run-control | partial | deferred_enhancement | [设计](../system-design/2026-09-15-multi-intent-hitl-plan.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[脚手架](../test-reports/2026-09-16-pu8-u9-u12-flags-test-report.md);[U9](../test-reports/2026-09-17-w4-u9-segment-test-report.md);[U10](../test-reports/2026-09-17-w4-u10-mutate-test-report.md);[U11](../test-reports/2026-09-17-w4-u11-docgov-test-report.md);[U12](../test-reports/2026-09-17-w4-u12-run-control-test-report.md);[2026-09-20 复核](../ops/research/2026-09-20-capability-enablement-impact-research.md) | U9 proposer 已实现和注入但未进入生产聊天分段调用;提议请求已带 reasoning profile。U10–U12 当前为 dry-run/validated-only adapter。历史直接调用/PI 测试继续有效,不代表 live 接线。逐域实现和验收,禁止以 `full` 一次放量 | `intent-plan` |
|
||||
| 对话 / ctxmgr baseline | 多轮上下文、刷新/重登/重启恢复、授权同集正文与计数、U10 端口化停止扩面 | done | non_blocker | [multi-turn](../system-design/multi-turn-dialogue/);[刷新审计](../audit/2026-09-13-chat-history-refresh-disappears.md);[U10](../test-reports/2026-09-15-chatlog-contextasm-u10-test-report.md);[门禁 B3](../test-reports/2026-09-16-chat-refresh-gate-test-report.md)(项目 6359) | **before**:语义摘要误标 D5 混入 → **after**:摘要迁「语义摘要 D3」;D7 数据治理独立。U10 停止扩面 | `chat` |
|
||||
| 语义摘要(D3) | 授权后可见集合上的语义摘要;shadow → 可选替换 deterministic | partial | deferred_enhancement | [chat-agent-p6-backlog](../backlog/chat-agent-p6-backlog.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[pu6](../ops/long-term/2026-09-16-pu6-semantic-summary-contract.md);[脚手架](../test-reports/2026-09-16-pu6-semantic-summary-test-report.md);[W2 真 PI](../test-reports/2026-09-17-w2-semantic-summary-shadow-test-report.md);[Track S1](../ops/short-term/2026-09-22-track-s1-summary-consolidation.md) | **after Track S1**:durable `chat_summary_shadow_jobs` + 后台租约/watermark/provenance 已合入;served Narrative 保持确定性。shadow 默认 off;`SEMANTIC_SUMMARY_REPLACE` **未接线**。**2026-09-23 实测** DOCREV 路径 schema **63**。授权可见集 digest 已进 shadow 路径。本行不升 done | `chat` |
|
||||
| 对话数据治理(D7) | 留存/删除口径、库·备份·日志控制、敏感字段加密边界 | deferred | deferred_enhancement(违规则升 blocker) | [计划](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md) Scope Boundaries;[D7 ADR 草案](../adr/2026-09-17-d7-chat-data-retention.md);[W7 盘点](../audit/2026-09-17-d7-data-inventory.md);[红线证据](../test-reports/2026-09-22-redline-evidence-d7-test-report.md) | 只读盘点 + Track E ID-only 符合性已补。口径仍 `pending-compliance-signoff`;**禁止物理删除**。本行不升 done | `chat` |
|
||||
| 智能体小队 | 角色-任务-工作区-黑板 | done | non_blocker | [agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6)属 backlog | `squad` |
|
||||
| 工作流治理 | 三正式工作流、设置页/DutyMode、动作级鉴权、真实 PI 三工作流 E2E、full/none 混合长链 | done | non_blocker | [三工作流整改](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[409/职责](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[PI E2E](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2 full/none](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md) | full 跨用户审批 + none 免审批不阻塞已签。纯软件自举仍独立 planned | `workflow` |
|
||||
| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[收口](../ops/short-term/2026-09-22-rdev-closeout.md);[报告](../test-reports/2026-09-22-rdev-closeout-test-report.md);[P3 历史](../test-reports/2026-09-19-rdev-p3-real-pi-test-report.md) | **after 2026-09-22 收口**:`rd_proposal` 标题门禁 + 法规表首表截止 dry PASS;nightly 脚本已扩四工作流(FullChain×3+FullNone)。P3 真 PI 三样本重跑因缺 `env:dev` **blocked**(不以 stub 冒充)。剩余:获环境后重跑 P3。本行保持 partial | `workflow-def` / `rdev-templates` |
|
||||
| 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | partial | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[ADR](../adr/2026-09-16-software-project-bootstrap-keys.md);[L1 报告](../test-reports/2026-09-16-software-project-bootstrap-test-report.md);[W5a](../test-reports/2026-09-17-w5a-software-bootstrap-ui-pi-test-report.md);[红线证据](../test-reports/2026-09-22-redline-evidence-swboot-test-report.md) | **after W5a + 红线证据**:catalog 驱动创建、软件不挂医疗 key。**SWBOOT-10 仍 stub**(L1–L4 证据已补;L5/L6 待授权)。本行不升 done | `workflow-design` |
|
||||
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | done | 加固 | [v3 full inventory](../audit/authorization-matrix-v3-full.md);[session 中断](../test-reports/2026-09-16-iam-route-inventory-test-report.md);[fail-closed](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md);[410 策略](../audit/2026-09-16-bug11-410-retire-decision.md);[R13](../audit/2026-09-16-r13-data-safety-checklist.md) | 全路由 inventory + session 中断负例 + 410 冻结 + R13 基线已签 | `authz` |
|
||||
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | non_blocker | [docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[正文 UI](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[canonical hash](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md);[键统一](../test-reports/2026-09-21-workflow-doc-key-unification-test-report.md) | 项目 143 只读;不终止。`doc_proj*` 同一发布键不能再并存 | `docgov` |
|
||||
| 文档版本 baseline | 账本、人工修订、确定性 diff+lineage、运行时 UI、负责人通知 | done | non_blocker | [管理](../test-reports/2026-09-13-document-revision-management-test-report.md);[Round20 UI](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[ADR](../adr/2026-09-13-document-version-boundary.md);[proposed/authoritative](../ops/short-term/2026-09-20-proposed-authoritative-reports.md) | **before**:完整 PI narrative 混入 → **after**:迁「PI change-report 叙事」。fallback 报告是本行内显式行为,不是缺口。lineage GET 已按 URL 项目和文档可见性裁剪。test_manager 血缘的下游/上游版本改读文档版本,不再写死 1。Agent 候选报告为 `proposed`,正式提交才写 authoritative 后继并挂到版本;拒绝、替代后旧报告不能再出建议。人工修订、导入和聊天卡仍未分开预估与正式报告,见 [T1](../ops/short-term/2026-09-20-impact-loop-t1.md)。项目追踪索引见下一行 | `docgov` |
|
||||
| 阶段回溯与影响闭环 | 五表追踪索引、影响报告、L1 文档修订、L2 阶段再打开、待复核软标记、对话修订提案 | done | deferred_enhancement(不挡首发) | [设计](../system-design/2026-09-20-stage-backtrack-and-impact-loop-design.md);[Track T](../remediation/2026-09-20-capability-enablement-impact-plan.md);[对话化报告](../test-reports/2026-09-21-trace-panel-to-chat-test-report.md);[T 补强](../ops/short-term/2026-09-22-track-t-strengthen.md) | 已合入 dev。常驻「需求与影响」面板已退役。**2026-09-23 实测** DOCREV 路径 schema **63**;test=`rc-v1.1.0-beta.1` schema 61;main 本轮未晋升。阶段推进确认门禁仍在。不签 D7 对话 ADR | `trace-index` |
|
||||
| PI change-report 叙事 | 只读消费 diff+lineage 的 PI 叙事;失败全量降级 fallback | partial | deferred_enhancement | [Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[pu7](../ops/long-term/2026-09-16-pu7-pi-narrative-contract.md);[W3 真 PI](../test-reports/2026-09-17-w3-pi-change-report-narrative-test-report.md);[红线负例](../test-reports/2026-09-22-redline-evidence-pi-narrative-test-report.md) | flag 默认 **off**(红线不放量)。fallback 全量负例单测已补。pi-agent reasoning 路径已带 `--no-tools`。不打开开关 | `docgov` |
|
||||
| 前端工作台 | 三栏 + 块文档 + SSE;错误卡/`TaskRunCard` 竖向进度/busy;版本冲突信封;demo seed 默认关;终止 preview | done | non_blocker | [workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[双标签](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png);[demo](../test-reports/2026-09-16-demo-seed-pollution-test-report.md);[B4](../test-reports/2026-09-16-frontend-gate-b4-test-report.md)(项目 6363);[TaskRunCard](../test-reports/2026-09-21-taskrun-status-card-persistence-test-report.md) | **before**:空「缺口」却 partial → **after**:本行 done。其他 409 消费方信封复用另列 hardening backlog | `web` |
|
||||
| PI Runner / 配置链 | pi-agent + MiniMax + typed-result-v1 | done | non_blocker | [piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md);[typed-result](../test-reports/2026-09-15-pi-typed-result-v1-test-report.md) | 文档生成仍使用读写和搜索工具。`tool_profile=reasoning` argv 为 `--no-tools`。**2026-09-22 实测** pi-agent 已 bind-mount `pi-args.mjs` | `pi-agent` |
|
||||
| 三环境运维 | dev/test/main + 共享 KB;对话/业务库隔离 | done | non_blocker | [PROJECT.md](../../PROJECT.md) §4;[晋升报告](../test-reports/2026-09-22-dev-gate-and-test-promotion-test-report.md);[DOCREV](../test-reports/2026-09-22-docrev-ui-rerun-test-report.md) | **2026-09-23 实测**:本地/共享 dev DOCREV 路径 schema **63**;test health schema **61**(`rc-v1.1.0-beta.1`);main 本轮未晋升 | `infra` |
|
||||
| CI / E2E | PR CI、PG 集成、治理 PG、已签真实 PI 门禁;**nightly** schedule/`workflow_dispatch` + infra/product 分类 receipt | done | 加固(阻止**下一次**晋升,不自动否定已发布) | [BUG-08](../test-reports/2026-09-16-ci-governance-pg-bug08-test-report.md);[PI 三工作流](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md);[W1 green](../test-reports/2026-09-17-w1-ci-e2e-green-nightly-test-report.md) + [receipt `6e2476f0…`](../assets/promotion-receipts/6e2476f0f2e408e111fe06ca7c31a38294a340b3.json) | **before**:partial(FullNone product_fail / secrets 文案过时)→ **after**:secrets 已配;duplicate goose 00048 已清;FullNone+U8 nightly `verdict=pass` | `ci` |
|
||||
| Artifact 同 digest 晋升 | 候选 commit 一次构建、按 digest 晋升、promotion receipt | done | 加固 | [计划 PU5B](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md) | **关闭**:same-image pin(`candidate-images-*` + `PINNED_IMAGE_MODE` + promote `--require-digests`)。**声明**:单机 retag/digest pin;远程 registry 推送另立任务 | `infra` |
|
||||
| 可靠事件核心 | PG dispatcher、notify/advance/projection live、生产闭包 hook 退出、Phase2 选择性解耦已合入 | done | non_blocker | [E0A–E2](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md) 等;[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[shared live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[phase2 closeout](../test-reports/2026-09-15-phase2-closeout-test-report.md) | **before**:与 FAV off 混成 partial → **after**:FAV 独立行。选择性后续解耦按收益触发 | `architecture-docs` / `events` |
|
||||
| 模块开发规范 | 窄端口、领域事件、wire DTO 三种契约;故障隔离红线;新增模块/表/事件流程。不含批次 A–G 的代码整改 | done | non_blocker | [规范](../system-design/2026-09-20-module-development-standards.md);[审计](../audit/2026-09-20-module-coupling-audit.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md) | 文档入口已立。代码整改不在本行 | `module-decoupling-spec` |
|
||||
| 模块解耦批次 A–G | server 端口装配、chat 解环、docgov 词汇、工作流单向依赖、巨型文件、前端 api client、21 条例外清零 | done | non_blocker | [分批计划](../remediation/2026-09-20-module-decoupling-batch-plan.md);[审计](../audit/2026-09-20-module-coupling-audit.md);[A](../ops/short-term/2026-09-22-server-assembly-cleanup.md);[B](../ops/short-term/2026-09-22-chat-app-decouple.md);[C](../ops/short-term/2026-09-22-docgov-vocabulary-convergence.md);[D](../ops/short-term/2026-09-22-workflow-def-unidirect.md);[E](../ops/short-term/2026-09-22-engine-split.md);[F](../ops/short-term/2026-09-22-web-api-client.md);[G1](../ops/short-term/2026-09-22-g1-exception-clearance.md);[G2/G3](../ops/short-term/2026-09-22-g2-g3-exception-clearance.md) | **after 2026-09-22**:A–F 与 E 已合入;G1/G2/G3 例外清零后 `arch-check --mode block` PASS 且 baseline import/sql_write/path/event **全 0**。关闭本伞行 | `module-decoupling-spec` |
|
||||
| Final Artifact Vault(FAV) | 不可变终态制品库;`FAV_DEGRADED_READ=false` | partial | deferred_enhancement | [Track E 复盘](../ops/research/2026-09-15-track-e-retrospective.md);[U7 报告](../test-reports/2026-09-16-fav-u7-incremental-test-report.md);[W6 shadow](../test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md);[红线证据](../test-reports/2026-09-22-redline-evidence-fav-test-report.md) | subscriber shadow 只计算 intended outcome。**live 未放量**;观察窗起点已登记。`FAV_DEGRADED_READ` 默认 false;禁止整支 merge | `events` |
|
||||
| 系统管理台 | 全屏系统级管理视图(总览/KB/特性开关/系统设置/工作流/角色/用户/文档 8 分组);`/api/v1/admin/system/*`;`system_settings` 白名单可写;吸收下线 AdminSettingsModal;细粒度 `system:*` + 设置审计日志(goose 00065) | done | non_blocker | [short-term](../ops/short-term/2026-09-17-system-admin-console.md);[测试报告](../test-reports/2026-09-17-system-admin-console-test-report.md);residual `modular-decouple-residual-2026-09-23` | 剩余 deferred:Playwright 实跑、KB 运维动作、白名单项接消费方 | `web` + `sysadmin` |
|
||||
| 个人工程中控台(已移出) | 原本地只读看板与后来的 Control Center 都不再属于本仓库 | removed | 进度管理改由独立项目承担 | [历史设计](../archive/design/2026-09-17-personal-engineering-control-center-plan.md);[历史报告](../test-reports/2026-09-17-engineering-dashboard-test-report.md) | 2026-09-19 删除 `cmd/control-center`、`internal/controlcenter`、`control-center/web`、`deploy/control-center`。不恢复 `engineering-dashboard` | — |
|
||||
| record_id | 模块 | 声明范围 | 状态 | 发布关系 | 证据 | 剩余工作 / 关闭条件 | mutex |
|
||||
|------|----------|------|----------|------|---------------------|-------|-------|
|
||||
| gov-agent-worktree | Agent worktree 治理自动化 | 主目录保护、任务 worktree、机器认领、候选集成、dev 部署与安全清理 | partial | non_blocker | [方案](../remediation/2026-09-21-agent-workflow-governance-plan.md);[L1 验收](../test-reports/2026-09-21-agent-governance-test-report.md);[W1-B macOS](../ops/short-term/2026-09-22-agentctl-macos.md);[W4](../ops/short-term/2026-09-22-governance-automation.md);residual `modular-decouple-residual-2026-09-23` | L1 与 macOS `with/docs apply/validate/integrate` 已合入。W4 landed:durable queue + 完整 docs projection apply(行/整文 CAS)+ deploy/recover 真实收据闭环 + Darwin cwd 可见性测试。仍 deferred:PI 源码镜像 compose、跨机隔离、automation/w4 实现收敛。本行不升 done | `agent-workflow-governance` |
|
||||
| intent-l1 | 意图识别 L1 | 18 类 Intent、三分支消解、写/只读分流、双标签 `DOC_BUSY`、intent 纯依赖边界 | done | non_blocker | [intent-recognition](../system-design/intent-recognition/);[工作流作用域](../test-reports/2026-09-21-intent-workflow-scoping-test-report.md);[审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[报告](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);[双标签](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json);[U1](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)–[U3](../test-reports/2026-09-15-multi-intent-u3-intentflow-pg-test-report.md) | **边界 before**:含 U9–U12 叙事混入 → **after**:U9–U12 迁至「多意图扩面」行。关闭:纯依赖边界回归即重开 | `intent` |
|
||||
| intent-plan-u1u8 | 多意图持久计划(U1–U8 基线) | app/intentflow 生命周期、HITL、默认 `legacy`、U1–U8 代码与安全基线 | partial | non_blocker(默认 legacy) | [设计](../system-design/2026-09-15-multi-intent-hitl-plan.md);[U1](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)–[U8](../test-reports/2026-09-16-multi-intent-u8-task-generate-test-report.md);[Wave2 真 PI](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md);[Track I1](../test-reports/2026-09-22-track-i1-query-owner-test-report.md) | **after Track I1**:生产组合根已去 Fake query owner,改真实 QueryOwner;receipt 含 auth scope digest;confirm 后走既有步骤内核 dispatcher。默认仍 `MULTI_INTENT_PLAN_TIER=legacy`。升 `auto_read/confirm` 需 Track I2(项目 allowlist + 频率门)。U9–U12 仍独立 dry-run | `intent-plan` |
|
||||
| intent-plan-u9u12 | 多意图扩面(U9–U12) | U9 LLM 分段实验;U10 mutate;U11 docgov;U12 workflow/run-control | partial | deferred_enhancement | [设计](../system-design/2026-09-15-multi-intent-hitl-plan.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[脚手架](../test-reports/2026-09-16-pu8-u9-u12-flags-test-report.md);[U9](../test-reports/2026-09-17-w4-u9-segment-test-report.md);[U10](../test-reports/2026-09-17-w4-u10-mutate-test-report.md);[U11](../test-reports/2026-09-17-w4-u11-docgov-test-report.md);[U12](../test-reports/2026-09-17-w4-u12-run-control-test-report.md);[2026-09-20 复核](../ops/research/2026-09-20-capability-enablement-impact-research.md) | U9 proposer 已实现和注入但未进入生产聊天分段调用;提议请求已带 reasoning profile。U10–U12 当前为 dry-run/validated-only adapter。历史直接调用/PI 测试继续有效,不代表 live 接线。逐域实现和验收,禁止以 `full` 一次放量 | `intent-plan` |
|
||||
| chat-ctxmgr | 对话 / ctxmgr baseline | 多轮上下文、刷新/重登/重启恢复、授权同集正文与计数、U10 端口化停止扩面 | done | non_blocker | [multi-turn](../system-design/multi-turn-dialogue/);[刷新审计](../audit/2026-09-13-chat-history-refresh-disappears.md);[U10](../test-reports/2026-09-15-chatlog-contextasm-u10-test-report.md);[门禁 B3](../test-reports/2026-09-16-chat-refresh-gate-test-report.md)(项目 6359) | **before**:语义摘要误标 D5 混入 → **after**:摘要迁「语义摘要 D3」;D7 数据治理独立。U10 停止扩面 | `chat` |
|
||||
| chat-semantic-d3 | 语义摘要(D3) | 授权后可见集合上的语义摘要;shadow → 可选替换 deterministic | partial | deferred_enhancement | [chat-agent-p6-backlog](../backlog/chat-agent-p6-backlog.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[pu6](../ops/long-term/2026-09-16-pu6-semantic-summary-contract.md);[脚手架](../test-reports/2026-09-16-pu6-semantic-summary-test-report.md);[W2 真 PI](../test-reports/2026-09-17-w2-semantic-summary-shadow-test-report.md);[Track S1](../ops/short-term/2026-09-22-track-s1-summary-consolidation.md) | **after Track S1**:durable `chat_summary_shadow_jobs` + 后台租约/watermark/provenance 已合入;served Narrative 保持确定性。shadow 默认 off;`SEMANTIC_SUMMARY_REPLACE` **未接线**。**2026-09-23 实测** DOCREV 路径 schema **63**。授权可见集 digest 已进 shadow 路径。本行不升 done | `chat` |
|
||||
| chat-data-d7 | 对话数据治理(D7) | 留存/删除口径、库·备份·日志控制、敏感字段加密边界 | deferred | deferred_enhancement(违规则升 blocker) | [计划](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md) Scope Boundaries;[D7 ADR 草案](../adr/2026-09-17-d7-chat-data-retention.md);[W7 盘点](../audit/2026-09-17-d7-data-inventory.md);[红线证据](../test-reports/2026-09-22-redline-evidence-d7-test-report.md) | 只读盘点 + Track E ID-only 符合性已补。口径仍 `pending-compliance-signoff`;**禁止物理删除**。本行不升 done | `chat` |
|
||||
| squad | 智能体小队 | 角色-任务-工作区-黑板 | done | non_blocker | [agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6)属 backlog | `squad` |
|
||||
| workflow-gov | 工作流治理 | 三正式工作流、设置页/DutyMode、动作级鉴权、真实 PI 三工作流 E2E、full/none 混合长链 | done | non_blocker | [三工作流整改](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[409/职责](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[PI E2E](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2 full/none](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md) | full 跨用户审批 + none 免审批不阻塞已签。纯软件自举仍独立 planned | `workflow` |
|
||||
| workflow-rdev | 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[收口](../ops/short-term/2026-09-22-rdev-closeout.md);[报告](../test-reports/2026-09-22-rdev-closeout-test-report.md);[P3 历史](../test-reports/2026-09-19-rdev-p3-real-pi-test-report.md) | **after 2026-09-22 收口**:`rd_proposal` 标题门禁 + 法规表首表截止 dry PASS;nightly 脚本已扩四工作流(FullChain×3+FullNone)。P3 真 PI 三样本重跑因缺 `env:dev` **blocked**(不以 stub 冒充)。剩余:获环境后重跑 P3。本行保持 partial | `workflow-def` / `rdev-templates` |
|
||||
| workflow-swboot | 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | partial | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[ADR](../adr/2026-09-16-software-project-bootstrap-keys.md);[L1 报告](../test-reports/2026-09-16-software-project-bootstrap-test-report.md);[W5a](../test-reports/2026-09-17-w5a-software-bootstrap-ui-pi-test-report.md);[红线证据](../test-reports/2026-09-22-redline-evidence-swboot-test-report.md) | **after W5a + 红线证据**:catalog 驱动创建、软件不挂医疗 key。**SWBOOT-10 仍 stub**(L1–L4 证据已补;L5/L6 待授权)。本行不升 done | `workflow-design` |
|
||||
| iam-authz | IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | done | 加固 | [v3 full inventory](../audit/authorization-matrix-v3-full.md);[session 中断](../test-reports/2026-09-16-iam-route-inventory-test-report.md);[fail-closed](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md);[410 策略](../audit/2026-09-16-bug11-410-retire-decision.md);[R13](../audit/2026-09-16-r13-data-safety-checklist.md) | 全路由 inventory + session 中断负例 + 410 冻结 + R13 基线已签 | `authz` |
|
||||
| docgov | 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | non_blocker | [docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[正文 UI](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[canonical hash](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md);[键统一](../test-reports/2026-09-21-workflow-doc-key-unification-test-report.md) | 项目 143 只读;不终止。`doc_proj*` 同一发布键不能再并存 | `docgov` |
|
||||
| docgov-revision | 文档版本 baseline | 账本、人工修订、确定性 diff+lineage、运行时 UI、负责人通知 | done | non_blocker | [管理](../test-reports/2026-09-13-document-revision-management-test-report.md);[Round20 UI](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[ADR](../adr/2026-09-13-document-version-boundary.md);[proposed/authoritative](../ops/short-term/2026-09-20-proposed-authoritative-reports.md) | **before**:完整 PI narrative 混入 → **after**:迁「PI change-report 叙事」。fallback 报告是本行内显式行为,不是缺口。lineage GET 已按 URL 项目和文档可见性裁剪。test_manager 血缘的下游/上游版本改读文档版本,不再写死 1。Agent 候选报告为 `proposed`,正式提交才写 authoritative 后继并挂到版本;拒绝、替代后旧报告不能再出建议。人工修订、导入和聊天卡仍未分开预估与正式报告,见 [T1](../ops/short-term/2026-09-20-impact-loop-t1.md)。项目追踪索引见下一行 | `docgov` |
|
||||
| trace-impact | 阶段回溯与影响闭环 | 五表追踪索引、影响报告、L1 文档修订、L2 阶段再打开、待复核软标记、对话修订提案 | done | deferred_enhancement(不挡首发) | [设计](../system-design/2026-09-20-stage-backtrack-and-impact-loop-design.md);[Track T](../remediation/2026-09-20-capability-enablement-impact-plan.md);[对话化报告](../test-reports/2026-09-21-trace-panel-to-chat-test-report.md);[T 补强](../ops/short-term/2026-09-22-track-t-strengthen.md) | 已合入 dev。常驻「需求与影响」面板已退役。**2026-09-23 实测** DOCREV 路径 schema **63**;test=`rc-v1.1.0-beta.1` schema 61;main 本轮未晋升。阶段推进确认门禁仍在。不签 D7 对话 ADR | `trace-index` |
|
||||
| docgov-pi-narrative | PI change-report 叙事 | 只读消费 diff+lineage 的 PI 叙事;失败全量降级 fallback | partial | deferred_enhancement | [Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[pu7](../ops/long-term/2026-09-16-pu7-pi-narrative-contract.md);[W3 真 PI](../test-reports/2026-09-17-w3-pi-change-report-narrative-test-report.md);[红线负例](../test-reports/2026-09-22-redline-evidence-pi-narrative-test-report.md) | flag 默认 **off**(红线不放量)。fallback 全量负例单测已补。pi-agent reasoning 路径已带 `--no-tools`。不打开开关 | `docgov` |
|
||||
| web-workbench | 前端工作台 | 三栏 + 块文档 + SSE;错误卡/`TaskRunCard` 竖向进度/busy;版本冲突信封;demo seed 默认关;终止 preview | done | non_blocker | [workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[双标签](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png);[demo](../test-reports/2026-09-16-demo-seed-pollution-test-report.md);[B4](../test-reports/2026-09-16-frontend-gate-b4-test-report.md)(项目 6363);[TaskRunCard](../test-reports/2026-09-21-taskrun-status-card-persistence-test-report.md) | **before**:空「缺口」却 partial → **after**:本行 done。其他 409 消费方信封复用另列 hardening backlog | `web` |
|
||||
| pi-runner | PI Runner / 配置链 | pi-agent + MiniMax + typed-result-v1 | done | non_blocker | [piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md);[typed-result](../test-reports/2026-09-15-pi-typed-result-v1-test-report.md) | 文档生成仍使用读写和搜索工具。`tool_profile=reasoning` argv 为 `--no-tools`。**2026-09-22 实测** pi-agent 已 bind-mount `pi-args.mjs` | `pi-agent` |
|
||||
| infra-envs | 三环境运维 | dev/test/main + 共享 KB;对话/业务库隔离 | done | non_blocker | [PROJECT.md](../../PROJECT.md) §4;[晋升报告](../test-reports/2026-09-22-dev-gate-and-test-promotion-test-report.md);[DOCREV](../test-reports/2026-09-22-docrev-ui-rerun-test-report.md) | **2026-09-23 实测**:本地/共享 dev DOCREV 路径 schema **63**;test health schema **61**(`rc-v1.1.0-beta.1`);main 本轮未晋升 | `infra` |
|
||||
| ci-e2e | CI / E2E | PR CI、PG 集成、治理 PG、已签真实 PI 门禁;**nightly** schedule/`workflow_dispatch` + infra/product 分类 receipt | done | 加固(阻止**下一次**晋升,不自动否定已发布) | [BUG-08](../test-reports/2026-09-16-ci-governance-pg-bug08-test-report.md);[PI 三工作流](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md);[W1 green](../test-reports/2026-09-17-w1-ci-e2e-green-nightly-test-report.md) + [receipt `6e2476f0…`](../assets/promotion-receipts/6e2476f0f2e408e111fe06ca7c31a38294a340b3.json) | **before**:partial(FullNone product_fail / secrets 文案过时)→ **after**:secrets 已配;duplicate goose 00048 已清;FullNone+U8 nightly `verdict=pass` | `ci` |
|
||||
| infra-artifact-digest | Artifact 同 digest 晋升 | 候选 commit 一次构建、按 digest 晋升、promotion receipt | done | 加固 | [计划 PU5B](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md) | **关闭**:same-image pin(`candidate-images-*` + `PINNED_IMAGE_MODE` + promote `--require-digests`)。**声明**:单机 retag/digest pin;远程 registry 推送另立任务 | `infra` |
|
||||
| events-core | 可靠事件核心 | PG dispatcher、notify/advance/projection live、生产闭包 hook 退出、Phase2 选择性解耦已合入 | done | non_blocker | [E0A–E2](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md) 等;[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[shared live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[phase2 closeout](../test-reports/2026-09-15-phase2-closeout-test-report.md) | **before**:与 FAV off 混成 partial → **after**:FAV 独立行。选择性后续解耦按收益触发 | `architecture-docs` / `events` |
|
||||
| module-spec | 模块开发规范 | 窄端口、领域事件、wire DTO 三种契约;故障隔离红线;新增模块/表/事件流程。不含批次 A–G 的代码整改 | done | non_blocker | [规范](../system-design/2026-09-20-module-development-standards.md);[审计](../audit/2026-09-20-module-coupling-audit.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md) | 文档入口已立。代码整改不在本行 | `module-decoupling-spec` |
|
||||
| module-batches-ag | 模块解耦批次 A–G | server 端口装配、chat 解环、docgov 词汇、工作流单向依赖、巨型文件、前端 api client、21 条例外清零 | done | non_blocker | [分批计划](../remediation/2026-09-20-module-decoupling-batch-plan.md);[审计](../audit/2026-09-20-module-coupling-audit.md);[A](../ops/short-term/2026-09-22-server-assembly-cleanup.md);[B](../ops/short-term/2026-09-22-chat-app-decouple.md);[C](../ops/short-term/2026-09-22-docgov-vocabulary-convergence.md);[D](../ops/short-term/2026-09-22-workflow-def-unidirect.md);[E](../ops/short-term/2026-09-22-engine-split.md);[F](../ops/short-term/2026-09-22-web-api-client.md);[G1](../ops/short-term/2026-09-22-g1-exception-clearance.md);[G2/G3](../ops/short-term/2026-09-22-g2-g3-exception-clearance.md) | **after 2026-09-22**:A–F 与 E 已合入;G1/G2/G3 例外清零后 `arch-check --mode block` PASS 且 baseline import/sql_write/path/event **全 0**。关闭本伞行 | `module-decoupling-spec` |
|
||||
| events-fav | Final Artifact Vault(FAV) | 不可变终态制品库;`FAV_DEGRADED_READ=false` | partial | deferred_enhancement | [Track E 复盘](../ops/research/2026-09-15-track-e-retrospective.md);[U7 报告](../test-reports/2026-09-16-fav-u7-incremental-test-report.md);[W6 shadow](../test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md);[红线证据](../test-reports/2026-09-22-redline-evidence-fav-test-report.md) | subscriber shadow 只计算 intended outcome。**live 未放量**;观察窗起点已登记。`FAV_DEGRADED_READ` 默认 false;禁止整支 merge | `events` |
|
||||
| web-sysadmin | 系统管理台 | 全屏系统级管理视图(总览/KB/特性开关/系统设置/工作流/角色/用户/文档 8 分组);`/api/v1/admin/system/*`;`system_settings` 白名单可写;吸收下线 AdminSettingsModal;细粒度 `system:*` + 设置审计日志(goose 00065) | done | non_blocker | [short-term](../ops/short-term/2026-09-17-system-admin-console.md);[测试报告](../test-reports/2026-09-17-system-admin-console-test-report.md);residual `modular-decouple-residual-2026-09-23` | 剩余 deferred:Playwright 实跑、KB 运维动作、白名单项接消费方 | `web` + `sysadmin` |
|
||||
| removed-control-center | 个人工程中控台(已移出) | 原本地只读看板与后来的 Control Center 都不再属于本仓库 | removed | 进度管理改由独立项目承担 | [历史设计](../archive/design/2026-09-17-personal-engineering-control-center-plan.md);[历史报告](../test-reports/2026-09-17-engineering-dashboard-test-report.md) | 2026-09-19 删除 `cmd/control-center`、`internal/controlcenter`、`control-center/web`、`deploy/control-center`。不恢复 `engineering-dashboard` | — |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "dialogue realtime search 强制检索 → 远程 test 验收"
|
||||
date: 2026-09-23
|
||||
task_id: dialogue-realtime-search-2026-09-23
|
||||
verdict: PASS
|
||||
---
|
||||
|
||||
# dialogue realtime search 强制检索 → 远程 test 验收
|
||||
|
||||
## 结论
|
||||
|
||||
天气/实时问句在对话路径强制调用 pi-agent runner `/v1/search`(Tavily),结果注入 Prompt【实时检索结果】;失败则注入拒答硬约束。已合入本地 `dev`/`test`、推送 `origin`,并部署到用户实际使用的 **远程 GMKtec `:8081` test**。真实链路回复气温 **18~27°C**,不再出现「12 月 / 深冬 / -6°C」幻觉。
|
||||
|
||||
## 根因回顾
|
||||
|
||||
| 层 | 事实 |
|
||||
|---|---|
|
||||
| 环境错位 | 用户测的是远程 GMKtec `8081`,不是本机 test;远程库记录了 10:18/11:03/11:42 三次天气问答,本机库为零 |
|
||||
| 制品过旧 | 远程镜像原为 2026-09-20、schema 58;无时钟锚定、无 smalltalk、无实时检索 |
|
||||
| 指令不够硬 | 仅靠 instructions 第 9 条无法阻止模型编造温度 |
|
||||
|
||||
## 版本矩阵
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| fix commit | `a5ea8197` |
|
||||
| local_dev tip(合入后) | `8749f507`(其后另有 governance 合入,现 tip 含本修复) |
|
||||
| local/origin test | `c6f0561b` |
|
||||
| origin/dev | 含 `a5ea8197` |
|
||||
| 远程 test HEAD | `c6f0561b` |
|
||||
| 远程 health schema | **65**(由 58 前向 goose 迁移) |
|
||||
| 远程二进制 | `当前时间`/`时间锚定`/`实时检索结果`/`本轮实时检索失败` = True |
|
||||
|
||||
## 验证
|
||||
|
||||
| 项 | 证据 | 结果 |
|
||||
|---|---|---|
|
||||
| unit | `go test ./internal/coordinator/ ./internal/agent/` | PASS |
|
||||
| vet/arch | `go vet` + `arch-check --mode block` | PASS |
|
||||
| 本地 test rebuild | schema 65 + 二进制符号 | PASS |
|
||||
| 远程 DB backup | `~/backup-test-20260923-065802.sql`(925K) | PASS |
|
||||
| 远程对齐 | local test 与 origin/test 曾分歧(远程独有 `700a1abf`);`reset --hard origin/test` 对齐部署目标 | 已记录 |
|
||||
| 远程 rebuild | `goproxy.cn` 重试后 app/events build + compose up | PASS |
|
||||
| 真实链路 | admin → `POST /api/v1/chat/stream`「今天北京天气怎么样…」 | PASS:18~27°C;无 12 月/深冬/-6 |
|
||||
| Prompt 注入 | `TASK.md` 含 `当前时间: 2026-09-23` + `【实时检索结果】` + Tavily | PASS |
|
||||
| DB 落盘 | remote `chat_messages` 07:06 UTC 用户/助手各一条 | PASS |
|
||||
|
||||
资产:[assets/2026-09-23-dialogue-realtime-search/](./assets/2026-09-23-dialogue-realtime-search/)(`reply.txt`、`TASK.hits.txt`)。
|
||||
|
||||
## 状态分列
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | code+tests reviewed in-task |
|
||||
| integration_state | local_dev merged; local test promoted `c6f0561b` |
|
||||
| deployment | remote GMKtec test deployed (schema 65) |
|
||||
| remote_sync | origin/dev + origin/test pushed |
|
||||
| cleanup | pending(feature/integrate/promote worktrees) |
|
||||
|
||||
## 禁止项遵守
|
||||
|
||||
- 未 push main;未打 rc tag(本轮未要求)
|
||||
- 未动远程 `dev` 分支上的本地 docs 提交
|
||||
- 远程 DB 迁移前已 pg_dump;失败未硬重置
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "智能体小队故障根治 — 测试报告"
|
||||
date: 2026-09-23
|
||||
task_id: squad-missing-target-retry-2026-09-23
|
||||
verdict: PASS
|
||||
---
|
||||
|
||||
# 智能体小队故障根治 — 测试报告
|
||||
|
||||
## 结论
|
||||
|
||||
R1(test author_only 解锁)+ R2(settled-no-deliverable 纠偏重试)已落地;local `test@9984c68d` + `rc-v1.1.0-beta.5` pinned 重建后,项目 6012 触发「编写用户初始需求收集表」小队任务 **completed**(不再 `blocked_pending_governance`)。未 push 远端。
|
||||
|
||||
## 版本矩阵
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| feature head | `4830b8ba` |
|
||||
| integrate candidate / local_dev tip(合入时) | `357e4ddb` |
|
||||
| local test | `9984c68d` |
|
||||
| rc tag | `rc-v1.1.0-beta.5` → `9984c68d` |
|
||||
| pinned commit | `357e4ddb` |
|
||||
| app digest | `shuimu-agent-app@sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef` |
|
||||
| pi-agent digest | `shuimu-pi-agent@sha256:03e6249ab0f5a44128975f63de48557214498a01a8009ab289bc041082826114` |
|
||||
| test health schema | 65 |
|
||||
| author_only | `true` rev=1 |
|
||||
|
||||
## 命令与判定
|
||||
|
||||
| 步骤 | 判定 | 说明 |
|
||||
|---|---|---|
|
||||
| PUT `workflow.allow_author_only_approval` | PASS | CAS revision 0→1 |
|
||||
| save-plan author_only + activate 6012 | PASS | 7 文档兼任;instance active |
|
||||
| `go test -short ./internal/agent ./internal/squad ./internal/stream` | PASS | 含 settled-no-deliverable 单测 |
|
||||
| `node --test deploy/pi-agent/*.test.mjs` | PASS | clean settle+missing → retryable |
|
||||
| `arch-check --mode block` | PASS | |
|
||||
| agentctl validate + integrate | PASS | ff `78c2bcf6`→`357e4ddb` |
|
||||
| promote merge + update-ref + tag | PASS | P1=`c6f0561b` P2=`357e4ddb` |
|
||||
| candidate-images-build/apply + rebuild | PASS | pinned recreate |
|
||||
| E2E `bd_intake` | PASS | `run_1790162579970596047` completed;三步均有 `output.md` |
|
||||
|
||||
## E2E 要点
|
||||
|
||||
- 创建 HTTP 202,`status=running`(非 blocked)
|
||||
- ~6 分钟后 `completed`;workspace `bd_intake_1790162580039623588`
|
||||
- 交付物:`01_organize` / `02_draft` / `03_review` / 根 `output.md` 均非空
|
||||
|
||||
## 资产
|
||||
|
||||
- [assets/2026-09-23-squad-missing-target-retry/](./assets/2026-09-23-squad-missing-target-retry/)
|
||||
- [promotion-receipts/357e4ddb…](./assets/promotion-receipts/357e4ddb0fe62e12cf6d1321382376257b0fe674.json)
|
||||
|
||||
## 状态
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| review_state | not_required |
|
||||
| integration_state | merged_local_dev_and_test |
|
||||
| deployment | test_ok |
|
||||
| remote_sync | not_requested |
|
||||
| cleanup | pending |
|
||||
@@ -0,0 +1,17 @@
|
||||
- 主题锚点: "今天北京天气怎么样,出去见客户要准备什么吗"
|
||||
- 当前时间: 2026-09-23 (周三) 15:06 (UTC+8)
|
||||
【实时检索结果】(权威实时数据,回答天气/新闻等只准以此为准)
|
||||
## 🌐 Tavily 实时网络搜索结果: "今天北京天气怎么样,出去见客户要准备什么吗"
|
||||
1. **北京天气预报,北京7天天气预报,北京15天天气预报,北京天气查询**
|
||||
- **摘要**: 应减少外出,外出需采取防护措施。 舒适 穿衣指数 建议穿长袖衬衫单裤等服装。) 不宜 洗车指数 有雨,雨水和泥水会弄脏爱车。 最弱 紫外线指数 辐射弱,涂擦SPF8-12防晒护肤品。 少发 感冒指数 无明显降温,感冒机率较低。 适宜 运动指数 天气较好,尽情感受运动的快乐吧。 易发 过敏指数 应减少外出,外出需采取防护措施。 热 穿衣指数 适合穿T恤、短薄外套等夏季服装。) 较适宜 洗车指数 无雨且风力较小,易保持清洁度。 弱 紫外线指数 辐射较弱,涂擦SPF12-15、PA+护肤品。 较易发 感冒指数 天凉,湿度大,较易感冒。 适宜 运动指数 天气较好,尽情感受运动的快乐吧。 易发 过敏指数
|
||||
2. **北京天气|四季气候、最佳旅游时间与穿搭指南【2026 更新】**
|
||||
- **摘要**: # 北京天气|四季气候、最佳旅游时间与穿搭指南【2026 更新】 Gambar Profil Penulis Oleh Amal 20 Ogos 2026 我去过几次北京,有一件事情非常重要——出发前一定要查天气! 🌦️北京的天气真是充满惊喜,尤其是当你来自常年温暖潮湿的马来西亚 🌴☀️。有一次,我在七月被36°C的烈日晒得满身大汗 🥵🍧,另一回却在一月的干冷空气中冻到手指都快失去知觉 ❄️🧣。北京的四季分明,变化剧烈——冬天是雪花飘落的屋檐和红墙宫殿 ☃️🏯,秋天是金黄的落叶铺满寺院小径 🍁📸。每一次来都像是到了一个全新的城市。 相信我,带对衣服和鞋子🧳👟,真的能让旅程舒
|
||||
3. **实时Beijing US Embassy天气状况:气温| 30天预报**
|
||||
- **摘要**: 从 01:45 PM 22 八月 2026 起,Beijing US Embassy 的10天预报显示以下趋势: 今天 (八月 22) : 气温 28°C,湿度 64%,Smoky haze 天气状况。 星期天 (八月 23) : 气温 28°C,湿度 78%,Thundery outbreaks in nearby 天气状况。 星期一 (八月 24) : 气温 29°C,湿度 75%,Smoky haze 天气状况。 星期二 (八月 25) : 气温 26°C,湿度 89%,Rain Shower 天气状况。 星期三 (八月 26) : 气温 25°C,湿度 62%,Overcast 天气状
|
||||
4. **北京, 北京市, 中國每日天氣**
|
||||
5. **北京-天气预报 - 中央气象台**
|
||||
【本轮用户】
|
||||
今天北京天气怎么样,出去见客户要准备什么吗
|
||||
【答复要求】
|
||||
1. 优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。
|
||||
5. 实时数据:涉及天气/新闻等时效问题,必须严格依据【实时检索结果】作答,不得使用训练记忆中的过期数值。
|
||||
@@ -0,0 +1,53 @@
|
||||
【工作记忆】
|
||||
【本轮任务状态 (Working Memory)】
|
||||
- 主题锚点: "今天北京天气怎么样,出去见客户要准备什么吗"
|
||||
- 待补充槽位: device_type、target_market、budget
|
||||
|
||||
- 上一轮挂起建议摘要: 项目「医疗器械研发项目」当前处于初始摸底期,基准产品画像已梳理完成。
|
||||
|
||||
【项目基准与协同上下文】
|
||||
- 当前时间: 2026-09-23 (周三) 15:06 (UTC+8)
|
||||
- 当前项目: 医疗器械研发项目 (ID: )
|
||||
- 阶段: V0.1_triage | 当前工作流: intake
|
||||
- 当前协同用户: 用户 (角色: engineer, 项目角色: composer)
|
||||
|
||||
【实时检索结果】(权威实时数据,回答天气/新闻等只准以此为准)
|
||||
## 🌐 Tavily 实时网络搜索结果: "今天北京天气怎么样,出去见客户要准备什么吗"
|
||||
|
||||
> **智能总结**: Today in Beijing, expect a hot, humid day with a high of 28°C; bring sunscreen and light clothing. The air quality is moderate, so normal outdoor activities are fine. Be prepared for possible rain showers.
|
||||
|
||||
### 检索到的参考条目 (5 条):
|
||||
|
||||
1. **北京天气预报,北京7天天气预报,北京15天天气预报,北京天气查询**
|
||||
- **摘要**: 应减少外出,外出需采取防护措施。 舒适 穿衣指数 建议穿长袖衬衫单裤等服装。) 不宜 洗车指数 有雨,雨水和泥水会弄脏爱车。 最弱 紫外线指数 辐射弱,涂擦SPF8-12防晒护肤品。 少发 感冒指数 无明显降温,感冒机率较低。 适宜 运动指数 天气较好,尽情感受运动的快乐吧。 易发 过敏指数 应减少外出,外出需采取防护措施。 热 穿衣指数 适合穿T恤、短薄外套等夏季服装。) 较适宜 洗车指数 无雨且风力较小,易保持清洁度。 弱 紫外线指数 辐射较弱,涂擦SPF12-15、PA+护肤品。 较易发 感冒指数 天凉,湿度大,较易感冒。 适宜 运动指数 天气较好,尽情感受运动的快乐吧。 易发 过敏指数
|
||||
- **相关度评分**: 0.597
|
||||
|
||||
2. **北京天气|四季气候、最佳旅游时间与穿搭指南【2026 更新】**
|
||||
- **摘要**: # 北京天气|四季气候、最佳旅游时间与穿搭指南【2026 更新】 Gambar Profil Penulis Oleh Amal 20 Ogos 2026 我去过几次北京,有一件事情非常重要——出发前一定要查天气! 🌦️北京的天气真是充满惊喜,尤其是当你来自常年温暖潮湿的马来西亚 🌴☀️。有一次,我在七月被36°C的烈日晒得满身大汗 🥵🍧,另一回却在一月的干冷空气中冻到手指都快失去知觉 ❄️🧣。北京的四季分明,变化剧烈——冬天是雪花飘落的屋檐和红墙宫殿 ☃️🏯,秋天是金黄的落叶铺满寺院小径 🍁📸。每一次来都像是到了一个全新的城市。 相信我,带对衣服和鞋子🧳👟,真的能让旅程舒
|
||||
- **相关度评分**: 0.518
|
||||
|
||||
3. **实时Beijing US Embassy天气状况:气温| 30天预报**
|
||||
- **摘要**: 从 01:45 PM 22 八月 2026 起,Beijing US Embassy 的10天预报显示以下趋势: 今天 (八月 22) : 气温 28°C,湿度 64%,Smoky haze 天气状况。 星期天 (八月 23) : 气温 28°C,湿度 78%,Thundery outbreaks in nearby 天气状况。 星期一 (八月 24) : 气温 29°C,湿度 75%,Smoky haze 天气状况。 星期二 (八月 25) : 气温 26°C,湿度 89%,Rain Shower 天气状况。 星期三 (八月 26) : 气温 25°C,湿度 62%,Overcast 天气状
|
||||
- **相关度评分**: 0.450
|
||||
|
||||
4. **北京, 北京市, 中國每日天氣**
|
||||
- **摘要**: RealFeel®30° RealFeel Shade™29° 最大紫外線指數6.0 (不健康-敏感人士) 風東北偏北 每小时11公里 ## 周一 9/14 RealFeel®31° RealFeel Shade™28° 最大紫外線指數6.0 (不健康-敏感人士) 風東南 每小时9公里 ## 周二 9/15 RealFeel®31° RealFeel Shade™29° 最大紫外線指數6.0 (不健康-敏感人士) 風東南 每小时6公里 ## 周三 9/16 RealFeel®32° RealFeel Shade™30° 最大紫外線指數5.0 (溫和) 風東北偏北 每小时6公里 ## 周四 9/
|
||||
- **相关度评分**: 0.418
|
||||
|
||||
5. **北京-天气预报 - 中央气象台**
|
||||
- **摘要**: 05:00 - 18.7℃ 2.7m/s 东北风 1017.1hPa 86.3% 10.1% 08:00 - 21.1℃ 3.3m/s 北风 1017.8hPa 81.6% 10.1% 11:00 - 24.8℃ 2.5m/s 东南风 1017.1hPa 69.2% 10.1% 14:00 - 26.4℃ 3.3m/s 南风 1014.8hPa 65% 79.9% 17:00 - 27.5℃ 3.2m/s 南风 1013.9hPa 64.9% 79.9% 20:00 - 25℃ 2.3m/s 南风 1014.5hPa 58.9% 79.9% 23:00 - 22.5℃ 2.4m/s 南风 10
|
||||
- **相关度评分**: 0.416
|
||||
|
||||
【用户个性化工作习惯与偏好指导 (Memory Injection)】
|
||||
• 沟通风格:该用户偏好【结构精炼、结论先行】;答复宜短而清,优先摘要与下一步,避免冗长铺垫。
|
||||
• 重点关注:该用户较关心【法规标准合规与审评审批风险、关键研发排期、型检周期与备料冗余】,结论中请点到并做轻量避险提示。
|
||||
|
||||
【本轮用户】
|
||||
今天北京天气怎么样,出去见客户要准备什么吗
|
||||
|
||||
【答复要求】
|
||||
1. 优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。
|
||||
2. 事实与引用:涉及法规条款与标准时,优先依据【参考知识】与【项目基准与协同上下文】并沿用〔引用键〕格式;无把握时明确提示依据不足,严禁生造标准号。
|
||||
3. 隐私与跨用户边界:其他成员姓名与私人习惯仅限理解语境,禁止外泄复述。
|
||||
4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。
|
||||
5. 实时数据:涉及天气/新闻等时效问题,必须严格依据【实时检索结果】作答,不得使用训练记忆中的过期数值。
|
||||
@@ -0,0 +1,22 @@
|
||||
好的,帮你查了一下今天北京的天气情况:
|
||||
|
||||
**🌤️ 今天北京天气预报**
|
||||
- 气温:18~27°C(午后较热,早晚偏凉)
|
||||
- 风力:南风 2~3 m/s
|
||||
- 湿度:65%~80%
|
||||
- 空气质量:目前良好,适合正常户外活动
|
||||
|
||||
---
|
||||
|
||||
**📋 出见客户准备建议**
|
||||
|
||||
1. **衣物**:建议"洋葱式"穿搭——轻便长袖/薄外套 + 易穿脱,方便应对早晚温差和室内外冷热切换。
|
||||
2. **防雨防晒**:午后可能转阴,建议备一把折叠伞或轻便雨衣,有备无患。
|
||||
3. **口罩**:秋季干燥,户外可备一个口罩,尤其是长距离步行或待在空调房时。
|
||||
4. **商务物品**:名片、纸质资料(防水袋装好)、笔记本、充电宝等。
|
||||
|
||||
---
|
||||
|
||||
今天是工作日(周三),下午这个时间出门见客户时间上没问题,注意返程高峰就行。
|
||||
|
||||
需要我帮您整理一下待会儿见客户要用的产品资料或项目简介吗?
|
||||
@@ -0,0 +1,5 @@
|
||||
key | value | revision
|
||||
-------------------------------------+-------+----------
|
||||
workflow.allow_author_only_approval | true | 1
|
||||
(1 row)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"create_http": 200,
|
||||
"run_id": "run_1790162579970596047",
|
||||
"final": {
|
||||
"run_id": "run_1790162579970596047",
|
||||
"session_id": "sess-p6012-u4-squad-e2e",
|
||||
"project_id": 6012,
|
||||
"user_id": 4,
|
||||
"client_turn_id": "turn-d7affa45251e",
|
||||
"idempotency_key": "turn:sess-p6012-u4-squad-e2e:turn-d7affa45251e",
|
||||
"task_key": "bd_intake",
|
||||
"stage": "",
|
||||
"workflow_instance_id": 1,
|
||||
"stage_instance_id": 1,
|
||||
"workflow_stage_key": "V0.1_triage",
|
||||
"status": "completed",
|
||||
"workspace_id": "/app/data/workspaces/bizdev/bd_intake_1790162580039623588",
|
||||
"assistant_message_id": 246,
|
||||
"worker_id": "",
|
||||
"lease_expires_at": null,
|
||||
"created_at": "2026-09-23T11:22:59.971662338Z",
|
||||
"updated_at": "2026-09-23T11:28:59.203425671Z",
|
||||
"error_message": "",
|
||||
"attempt": 1,
|
||||
"resumable": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"status":"ok","env":"test","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432,"kb_ready":true,"database_ready":true,"schema_version":65,"schema_ready":true,"task_store_ready":true}
|
||||
@@ -0,0 +1,4 @@
|
||||
/app/data/workspaces/bizdev/bd_intake_1790162580039623588/tasks/02_draft/output.md
|
||||
/app/data/workspaces/bizdev/bd_intake_1790162580039623588/tasks/03_review/output.md
|
||||
/app/data/workspaces/bizdev/bd_intake_1790162580039623588/tasks/01_organize/output.md
|
||||
/app/data/workspaces/bizdev/bd_intake_1790162580039623588/output.md
|
||||
@@ -0,0 +1,5 @@
|
||||
14218 /app/data/workspaces/bizdev/bd_intake_1790162580039623588/tasks/02_draft/output.md
|
||||
19238 /app/data/workspaces/bizdev/bd_intake_1790162580039623588/tasks/03_review/output.md
|
||||
13821 /app/data/workspaces/bizdev/bd_intake_1790162580039623588/tasks/01_organize/output.md
|
||||
19239 /app/data/workspaces/bizdev/bd_intake_1790162580039623588/output.md
|
||||
66516 total
|
||||
@@ -0,0 +1,5 @@
|
||||
run_id | status | task_key | workspace_id
|
||||
-------------------------+-----------+-----------+-----------------------------------------------------------
|
||||
run_1790162579970596047 | completed | bd_intake | /app/data/workspaces/bizdev/bd_intake_1790162580039623588
|
||||
(1 row)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
dev=5cdcee26
|
||||
test=9984c68d
|
||||
tag=9984c68d
|
||||
code_sha=357e4ddb
|
||||
promote_sha=9984c68d
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"workflow_id": "wf_bd_intake_1790162580043670880",
|
||||
"task_key": "bd_intake",
|
||||
"title": "用户初始需求收集表",
|
||||
"planning_mode": "fixed_adaptive",
|
||||
"status": "completed",
|
||||
"current_step_index": 0,
|
||||
"steps": [
|
||||
{
|
||||
"id": "step_01_organize",
|
||||
"role": "organizer",
|
||||
"title": "抽取材料事实并回填字段",
|
||||
"sub_workspace": "tasks/01_organize",
|
||||
"mode": "execute",
|
||||
"parallel": false,
|
||||
"accept": [
|
||||
"抽取材料中的客户身份与产品定义",
|
||||
"回填 fields.json 结构化数据",
|
||||
"向 blackboard 写入 facts 与 gaps"
|
||||
],
|
||||
"status": "completed",
|
||||
"frozen": true,
|
||||
"archived_at": "2026-09-23T11:25:52.244130335Z"
|
||||
},
|
||||
{
|
||||
"id": "step_02_draft",
|
||||
"role": "writer",
|
||||
"title": "撰写用户初始需求收集表初稿",
|
||||
"sub_workspace": "tasks/02_draft",
|
||||
"mode": "execute",
|
||||
"parallel": false,
|
||||
"needs": [
|
||||
"tasks/01_organize/output.md"
|
||||
],
|
||||
"accept": [
|
||||
"按 template.md 各章节严格四段式展开",
|
||||
"正文翔实,各节须实质展开",
|
||||
"文末集中附录字段快照与待确认清单"
|
||||
],
|
||||
"status": "completed",
|
||||
"frozen": true,
|
||||
"archived_at": "2026-09-23T11:27:44.140019845Z"
|
||||
},
|
||||
{
|
||||
"id": "step_03_review",
|
||||
"role": "leader",
|
||||
"title": "审核并定稿交付",
|
||||
"sub_workspace": "tasks/03_review",
|
||||
"mode": "execute",
|
||||
"parallel": false,
|
||||
"needs": [
|
||||
"tasks/02_draft/output.md"
|
||||
],
|
||||
"accept": [
|
||||
"核验全文逻辑一致性与出处标注",
|
||||
"汇编待确认清单并给出下一步建议",
|
||||
"交付终稿 output.md"
|
||||
],
|
||||
"status": "completed",
|
||||
"frozen": true,
|
||||
"archived_at": "2026-09-23T11:28:59.149263421Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"commit": "357e4ddb0fe62e12cf6d1321382376257b0fe674",
|
||||
"source_tree": "c09b36c2f8fbfde8ccd35f3fdf0caa6f32f253ea",
|
||||
"source_context": "git-archive",
|
||||
"recorded_at": "2026-09-23T11:20:50Z",
|
||||
"tags": {
|
||||
"app": "shuimu-agent-app:candidate-357e4ddb0fe6",
|
||||
"events": "shuimu-agent-events:candidate-357e4ddb0fe6",
|
||||
"pi-agent": "shuimu-pi-agent:candidate-357e4ddb0fe6"
|
||||
},
|
||||
"image_ids": {
|
||||
"app": "sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef",
|
||||
"events": "sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef",
|
||||
"pi-agent": "sha256:03e6249ab0f5a44128975f63de48557214498a01a8009ab289bc041082826114"
|
||||
},
|
||||
"image_digests": {
|
||||
"app": "shuimu-agent-app@sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef",
|
||||
"events": "shuimu-agent-app@sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef",
|
||||
"pi-agent": "shuimu-pi-agent@sha256:03e6249ab0f5a44128975f63de48557214498a01a8009ab289bc041082826114"
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"verdict": "pass",
|
||||
"commit": "357e4ddb0fe62e12cf6d1321382376257b0fe674",
|
||||
"recorded_at": "2026-09-23T11:21:06Z",
|
||||
"runner_url": "http://127.0.0.1:18865",
|
||||
"model_id": "MiniMax-M2.7-highspeed",
|
||||
"protocol": "typed-result-v1",
|
||||
"image_digests": {
|
||||
"app": "shuimu-agent-app@sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef",
|
||||
"events": "shuimu-agent-app@sha256:038c301cb68f68d2346fe1e8ad9ed67e844f0338556b8fbffbd35dcf1f02d3ef",
|
||||
"pi-agent": "shuimu-pi-agent@sha256:03e6249ab0f5a44128975f63de48557214498a01a8009ab289bc041082826114"
|
||||
},
|
||||
"suite": "squad-settled-no-deliverable-unit",
|
||||
"failure_class": "none",
|
||||
"evidence_refs": [
|
||||
"/tmp/squad-r2-images-build.txt"
|
||||
]
|
||||
}
|
||||
@@ -24,11 +24,19 @@
|
||||
},
|
||||
".doc/status/FEATURE-MATRIX.md": {
|
||||
"evidence_for_done": true,
|
||||
"header": "| 模块 |"
|
||||
"header": "| record_id | 模块 |"
|
||||
}
|
||||
},
|
||||
"reserved_record_ids": ["holder", "idle", "Cursor", "Codex", "-"],
|
||||
"protected_sections": {
|
||||
".doc/ops/CURRENT.md": ["## 集成槽位"]
|
||||
}
|
||||
".doc/ops/CURRENT.md": [
|
||||
"## 集成槽位",
|
||||
"## 焦点",
|
||||
"## 清理接管与遗留现场",
|
||||
"## 受控约束",
|
||||
"## 活跃入口"
|
||||
]
|
||||
},
|
||||
"heartbeat_ttl_seconds": 300,
|
||||
"suspect_ttl_seconds": 600
|
||||
}
|
||||
|
||||
@@ -112,9 +112,12 @@ export function classifyRunOutcome({
|
||||
return { ok: false, retryable, fallback_reason: reason };
|
||||
}
|
||||
if (miss.length > 0) {
|
||||
// Clean settle without writing the target is still retryable once at the
|
||||
// product layer (Go AssessStepFailure → settled-no-deliverable). Runner
|
||||
// signals retryable; Go remains the final adjudicator and bound.
|
||||
return {
|
||||
ok: false,
|
||||
retryable: Boolean(retryFailed || providerTransient),
|
||||
retryable: true,
|
||||
fallback_reason: `missing target deliverable ${miss.join(",")}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const cleanMissing = classifyRunOutcome({
|
||||
missing: ["output.md"],
|
||||
});
|
||||
assert.equal(cleanMissing.ok, false);
|
||||
assert.equal(cleanMissing.retryable, false, "clean settle + missing target is permanent");
|
||||
assert.equal(cleanMissing.retryable, true, "clean settle + missing target is retryable at product layer");
|
||||
assert.match(cleanMissing.fallback_reason, /missing target deliverable/);
|
||||
|
||||
const promptEmpty = classifyRunOutcome({
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const piRunnerSearchTimeout = 20 * time.Second
|
||||
|
||||
// PiRunnerSearcher 通过 pi-agent runner 的 /v1/search 做 Tavily 检索。
|
||||
type PiRunnerSearcher struct {
|
||||
URL string
|
||||
Token string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Search 调用 runner /v1/search,返回 markdown 结果。
|
||||
func (s *PiRunnerSearcher) Search(ctx context.Context, query string) (string, error) {
|
||||
if s == nil {
|
||||
return "", fmt.Errorf("pi runner searcher is nil")
|
||||
}
|
||||
base := strings.TrimRight(strings.TrimSpace(s.URL), "/")
|
||||
q := strings.TrimSpace(query)
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("pi runner url empty")
|
||||
}
|
||||
if q == "" {
|
||||
return "", fmt.Errorf("search query empty")
|
||||
}
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"query": q,
|
||||
"max_results": 5,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, piRunnerSearchTimeout)
|
||||
defer cancel()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodPost, base+"/v1/search", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
ApplyRunnerAuth(httpReq, s.Token)
|
||||
|
||||
client := s.Client
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: piRunnerSearchTimeout}
|
||||
}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
limited, err := io.ReadAll(io.LimitReader(resp.Body, MaxResultBodyBytes+1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(limited) > MaxResultBodyBytes {
|
||||
return "", fmt.Errorf("search response too large")
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("search http %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
OK bool `json:"ok"`
|
||||
Markdown string `json:"markdown"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(limited, &parsed); err != nil {
|
||||
return "", fmt.Errorf("decode search response: %w", err)
|
||||
}
|
||||
if !parsed.OK {
|
||||
msg := strings.TrimSpace(parsed.Error)
|
||||
if msg == "" {
|
||||
msg = "search failed"
|
||||
}
|
||||
return "", fmt.Errorf("%s", msg)
|
||||
}
|
||||
md := strings.TrimSpace(parsed.Markdown)
|
||||
if md == "" {
|
||||
return "", fmt.Errorf("empty search markdown")
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPiRunnerSearcher_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/search" {
|
||||
t.Fatalf("path=%s", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("Authorization") != "Bearer tok-1" {
|
||||
t.Fatalf("missing auth: %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if body["query"] != "今天北京天气" {
|
||||
t.Fatalf("query=%v", body["query"])
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"markdown": "## 北京天气\n气温 21~33°C",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PiRunnerSearcher{URL: srv.URL, Token: "tok-1", Client: srv.Client()}
|
||||
md, err := s.Search(context.Background(), "今天北京天气")
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if !strings.Contains(md, "21~33") {
|
||||
t.Fatalf("markdown=%q", md)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiRunnerSearcher_HTTPFailure(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = w.Write([]byte(`{"ok":false,"error":"upstream"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PiRunnerSearcher{URL: srv.URL, Client: srv.Client()}
|
||||
_, err := s.Search(context.Background(), "今天天气")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiRunnerSearcher_EmptyMarkdown(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "markdown": " "})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PiRunnerSearcher{URL: srv.URL, Client: srv.Client()}
|
||||
_, err := s.Search(context.Background(), "新闻")
|
||||
if err == nil {
|
||||
t.Fatal("expected empty markdown error")
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,12 @@ func AssessStepFailure(sig StepFailureSignals) FailureAssessment {
|
||||
}
|
||||
return FailureAssessment{Class: FailureTransient, Reason: kind + ": " + compactReason(reason)}
|
||||
}
|
||||
// Model settled cleanly but never wrote the target (e.g. text-only reply,
|
||||
// no write tool). One product-layer re-invocation with a corrective prompt
|
||||
// is worth trying; still bounded by MaxTransientStepInvocations.
|
||||
if missing && trace.Settled && !provider {
|
||||
return FailureAssessment{Class: FailureTransient, Reason: "settled-no-deliverable: " + compactReason(reason)}
|
||||
}
|
||||
if missing {
|
||||
return FailureAssessment{Class: FailurePermanent, Reason: "permanent-missing: " + compactReason(reason)}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,20 @@ func TestAssessStepFailure_TransientVsPermanent(t *testing.T) {
|
||||
want: FailureTransient,
|
||||
},
|
||||
{
|
||||
name: "missing without provider signal is permanent",
|
||||
name: "settled clean missing is transient settled-no-deliverable",
|
||||
sig: StepFailureSignals{
|
||||
Result: &Result{Success: false, ErrorReason: "missing target deliverable output.md"},
|
||||
ExtraText: `{"type":"agent_settled"}`,
|
||||
TargetMissing: true,
|
||||
},
|
||||
want: FailureTransient,
|
||||
},
|
||||
{
|
||||
name: "missing without settle remains permanent",
|
||||
sig: StepFailureSignals{
|
||||
Result: &Result{Success: false, ErrorReason: "missing target deliverable output.md"},
|
||||
TargetMissing: true,
|
||||
},
|
||||
want: FailurePermanent,
|
||||
},
|
||||
{
|
||||
@@ -149,6 +157,20 @@ func TestAssessStepFailure_ReadsWorkDirExecutionLog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssessStepFailure_SettledNoDeliverableReason(t *testing.T) {
|
||||
got := AssessStepFailure(StepFailureSignals{
|
||||
Result: &Result{Success: false, ErrorReason: "missing target deliverable output.md"},
|
||||
ExtraText: `{"type":"agent_settled"}`,
|
||||
TargetMissing: true,
|
||||
})
|
||||
if got.Class != FailureTransient {
|
||||
t.Fatalf("class=%s want transient", got.Class)
|
||||
}
|
||||
if !strings.Contains(got.Reason, "settled-no-deliverable") {
|
||||
t.Fatalf("reason=%q want settled-no-deliverable", got.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxTransientStepInvocations_IsBounded(t *testing.T) {
|
||||
if MaxTransientStepInvocations < 2 {
|
||||
t.Fatal("need at least one fresh re-invocation after the first")
|
||||
|
||||
@@ -116,6 +116,12 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
|
||||
}
|
||||
dialogueBackend := agent.NewBackend(backendName, cfg)
|
||||
coordEngine := coordinator.NewCoordinatorEngineWithDialogue(nil, dialogueBackend, cfg.DataDir)
|
||||
if cfg != nil && strings.TrimSpace(cfg.PiAgentRunnerURL) != "" {
|
||||
coordEngine.SetRealtimeSearcher(&agent.PiRunnerSearcher{
|
||||
URL: cfg.PiAgentRunnerURL,
|
||||
Token: cfg.PiAgentRunnerAuthToken,
|
||||
})
|
||||
}
|
||||
parts := bizdev.Snapshot(svc)
|
||||
if ctxmgr.SemanticSummaryShadowEnabled() && parts.Ctx != nil && backendName == "pi-agent" {
|
||||
model := ""
|
||||
|
||||
@@ -222,6 +222,10 @@ type UserProjectContext struct {
|
||||
UpgradeOffered bool `json:"-"`
|
||||
// DocumentSummaryReader 仅用于服务端鉴权后的只读正文访问,不序列化。
|
||||
DocumentSummaryReader DocumentSummaryReader `json:"-"`
|
||||
// RealtimeSnippet 本轮实时检索(天气/新闻等)成功结果,供 Prompt 注入;不序列化。
|
||||
RealtimeSnippet string `json:"-"`
|
||||
// RealtimeUnavailable 本轮命中实时查询但检索失败/空结果,强制拒答约束;不序列化。
|
||||
RealtimeUnavailable bool `json:"-"`
|
||||
}
|
||||
|
||||
// ContextAssembler 上下文组装服务
|
||||
|
||||
@@ -58,6 +58,22 @@ func (e *CoordinatorEngine) generateDialogueReply(
|
||||
return "", ReplySourceTemplate
|
||||
}
|
||||
|
||||
if userCtx != nil {
|
||||
userCtx.RealtimeSnippet = ""
|
||||
userCtx.RealtimeUnavailable = false
|
||||
if e.realtimeSearcher != nil && IsRealtimeInformationQuery(userInput) {
|
||||
if st := stream.FromContext(ctx); st != nil {
|
||||
st.EmitPhase("dialogue_realtime_search", "正在检索实时信息...", "assistant", 0, 0, 40)
|
||||
}
|
||||
md, searchErr := e.realtimeSearcher.Search(ctx, userInput)
|
||||
if searchErr != nil || strings.TrimSpace(md) == "" {
|
||||
userCtx.RealtimeUnavailable = true
|
||||
} else {
|
||||
userCtx.RealtimeSnippet = strings.TrimSpace(md)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt := buildDialoguePrompt(userInput, userCtx)
|
||||
instructions := `你是「水木东方」医疗器械「项目助手」——温和、靠谱、会替用户着想的项目助理。
|
||||
定位:以助理身份与用户友好协作。能力覆盖商务研判、工程方案与法规合规,但对外只以「项目助手」自称,不要分饰多角色,也不要自称「小队/参谋/总工/法规专家」。
|
||||
|
||||
@@ -393,3 +393,98 @@ func TestDialogueLLM_AmbiguousSmalltalkDoesNotUseStageSalesTemplate(t *testing.T
|
||||
t.Fatalf("expected casual-chat template fallback, got: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type stubRealtimeSearcher struct {
|
||||
markdown string
|
||||
err error
|
||||
calls int
|
||||
lastQ string
|
||||
}
|
||||
|
||||
func (s *stubRealtimeSearcher) Search(ctx context.Context, query string) (string, error) {
|
||||
s.calls++
|
||||
s.lastQ = query
|
||||
return s.markdown, s.err
|
||||
}
|
||||
|
||||
func TestDialogueLLM_RealtimeSearchInjectsSnippet(t *testing.T) {
|
||||
backend := &scriptedDialogueBackend{reply: "依据检索:北京今日约 25°C。"}
|
||||
tmp := t.TempDir()
|
||||
engine := NewCoordinatorEngineWithDialogue(nil, backend, tmp)
|
||||
searcher := &stubRealtimeSearcher{markdown: "## 北京天气\n气温 21~33°C"}
|
||||
engine.SetRealtimeSearcher(searcher)
|
||||
|
||||
userCtx := &UserProjectContext{
|
||||
Project: ProjectInfo{
|
||||
ID: "p-rt-1",
|
||||
Name: "全自动冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
rec := &intent.IntentRecognitionResult{
|
||||
RawInput: "今天北京天气怎么样,出去见客户要准备什么吗",
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentCasualChat,
|
||||
Confidence: 0.85,
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
},
|
||||
}
|
||||
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if searcher.calls != 1 {
|
||||
t.Fatalf("expected 1 search call, got %d", searcher.calls)
|
||||
}
|
||||
if !strings.Contains(backend.lastPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("prompt missing realtime section: %s", backend.lastPrompt)
|
||||
}
|
||||
if !strings.Contains(backend.lastPrompt, "21~33") {
|
||||
t.Fatalf("prompt missing snippet: %s", backend.lastPrompt)
|
||||
}
|
||||
if res.ReplySource != ReplySourcePiAgent {
|
||||
t.Fatalf("reply_source=%q", res.ReplySource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogueLLM_RealtimeSearchFailureInjectsRefuseConstraint(t *testing.T) {
|
||||
backend := &scriptedDialogueBackend{reply: "抱歉,暂时无法获取实时天气数据,建议您查看天气 App。"}
|
||||
tmp := t.TempDir()
|
||||
engine := NewCoordinatorEngineWithDialogue(nil, backend, tmp)
|
||||
searcher := &stubRealtimeSearcher{err: context.DeadlineExceeded}
|
||||
engine.SetRealtimeSearcher(searcher)
|
||||
|
||||
userCtx := &UserProjectContext{
|
||||
Project: ProjectInfo{
|
||||
ID: "p-rt-2",
|
||||
Name: "全自动冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
rec := &intent.IntentRecognitionResult{
|
||||
RawInput: "今天北京天气怎么样",
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentCasualChat,
|
||||
Confidence: 0.9,
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
},
|
||||
}
|
||||
_, err := engine.ProcessIntent(context.Background(), rec, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if searcher.calls != 1 {
|
||||
t.Fatalf("expected 1 search call, got %d", searcher.calls)
|
||||
}
|
||||
if strings.Contains(backend.lastPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("should not inject empty realtime section: %s", backend.lastPrompt)
|
||||
}
|
||||
if !strings.Contains(backend.lastPrompt, "本轮实时检索失败") {
|
||||
t.Fatalf("missing refuse constraint: %s", backend.lastPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +65,16 @@ type CoordinationResult struct {
|
||||
|
||||
// CoordinatorEngine L2 协调引擎
|
||||
type CoordinatorEngine struct {
|
||||
stageTasks StageTaskRunner
|
||||
instSvc *projectinstance.Service
|
||||
assembler *ContextAssembler
|
||||
dialogueBackend agent.Backend
|
||||
dataDir string
|
||||
ctxMgr *ctxmgr.ContextManager
|
||||
kbClient *kb.Client
|
||||
impact ImpactActions
|
||||
projectAccess ProjectAccess
|
||||
stageTasks StageTaskRunner
|
||||
instSvc *projectinstance.Service
|
||||
assembler *ContextAssembler
|
||||
dialogueBackend agent.Backend
|
||||
realtimeSearcher RealtimeSearcher
|
||||
dataDir string
|
||||
ctxMgr *ctxmgr.ContextManager
|
||||
kbClient *kb.Client
|
||||
impact ImpactActions
|
||||
projectAccess ProjectAccess
|
||||
}
|
||||
|
||||
// StageTaskRunner is the application owner for ExecuteTask / ExecuteStage.
|
||||
@@ -109,6 +110,14 @@ func (e *CoordinatorEngine) SetInstanceService(svc *projectinstance.Service) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// SetRealtimeSearcher 注入对话路径的实时检索(天气/新闻等);nil 时跳过强制检索。
|
||||
func (e *CoordinatorEngine) SetRealtimeSearcher(s RealtimeSearcher) {
|
||||
if e != nil {
|
||||
e.realtimeSearcher = s
|
||||
}
|
||||
}
|
||||
|
||||
// SetImpactActions wires read-only impact analysis and confirmed revision proposals.
|
||||
func (e *CoordinatorEngine) SetImpactActions(a ImpactActions) {
|
||||
if e != nil {
|
||||
|
||||
@@ -230,6 +230,18 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 6b. 【实时检索结果】 天气/新闻等权威实时数据(强制锚定)
|
||||
// -------------------------------------------------------------
|
||||
if snip := strings.TrimSpace(userCtx.RealtimeSnippet); snip != "" {
|
||||
sb.WriteString("【实时检索结果】(权威实时数据,回答天气/新闻等只准以此为准)\n")
|
||||
sb.WriteString(snip)
|
||||
if !strings.HasSuffix(snip, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 7. 【用户偏好画像】 PersonalizedGuidance (~300 字)
|
||||
// -------------------------------------------------------------
|
||||
@@ -259,7 +271,12 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
sb.WriteString("1. 优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。\n")
|
||||
sb.WriteString("2. 事实与引用:涉及法规条款与标准时,优先依据【参考知识】与【项目基准与协同上下文】并沿用〔引用键〕格式;无把握时明确提示依据不足,严禁生造标准号。\n")
|
||||
sb.WriteString("3. 隐私与跨用户边界:其他成员姓名与私人习惯仅限理解语境,禁止外泄复述。\n")
|
||||
sb.WriteString("4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。")
|
||||
sb.WriteString("4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。\n")
|
||||
if userCtx.RealtimeUnavailable {
|
||||
sb.WriteString("5. 本轮实时检索失败:必须明确告知无法获取实时数据(建议查看天气 App),严禁编造任何具体天气/新闻数值。\n")
|
||||
} else if strings.TrimSpace(userCtx.RealtimeSnippet) != "" {
|
||||
sb.WriteString("5. 实时数据:涉及天气/新闻等时效问题,必须严格依据【实时检索结果】作答,不得使用训练记忆中的过期数值。\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -133,3 +133,47 @@ func TestMemoryComposer_HierarchyAndPriority(t *testing.T) {
|
||||
idxWorkMem, idxProject, idxSummary, idxRecent, idxRecalled, idxReference, idxGuidance, idxUser, idxReq)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestMemoryComposer_RealtimeSnippetAndUnavailable(t *testing.T) {
|
||||
composer := coordinator.NewMemoryComposer()
|
||||
fixed := time.Date(2026, 9, 23, 10, 41, 0, 0, time.FixedZone("CST", 8*3600))
|
||||
composer.NowFn = func() time.Time { return fixed }
|
||||
|
||||
okCtx := &coordinator.UserProjectContext{
|
||||
Project: coordinator.ProjectInfo{
|
||||
ID: "proj_rt",
|
||||
Name: "冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
RealtimeSnippet: "北京今日气温约 21~33°C,晴。",
|
||||
}
|
||||
okPrompt := composer.ComposeDialoguePrompt("今天北京天气怎么样", okCtx)
|
||||
if !strings.Contains(okPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("missing realtime section: %s", okPrompt)
|
||||
}
|
||||
if !strings.Contains(okPrompt, "21~33") {
|
||||
t.Fatalf("missing snippet body: %s", okPrompt)
|
||||
}
|
||||
if !strings.Contains(okPrompt, "必须严格依据【实时检索结果】作答") {
|
||||
t.Fatalf("missing success constraint: %s", okPrompt)
|
||||
}
|
||||
|
||||
failCtx := &coordinator.UserProjectContext{
|
||||
Project: coordinator.ProjectInfo{
|
||||
ID: "proj_rt",
|
||||
Name: "冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
RealtimeUnavailable: true,
|
||||
}
|
||||
failPrompt := composer.ComposeDialoguePrompt("今天北京天气怎么样", failCtx)
|
||||
if strings.Contains(failPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("should not render empty realtime section: %s", failPrompt)
|
||||
}
|
||||
if !strings.Contains(failPrompt, "本轮实时检索失败") {
|
||||
t.Fatalf("missing failure constraint: %s", failPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
// RealtimeSearcher 对话路径的实时信息检索(天气/新闻等);由 app 注入 Pi Runner /v1/search。
|
||||
type RealtimeSearcher interface {
|
||||
Search(ctx context.Context, query string) (markdown string, err error)
|
||||
}
|
||||
|
||||
var realtimeInfoMarkers = []string{
|
||||
"天气", "气温", "温度", "下雨", "下雪", "降雨", "降雪", "台风", "雾霾",
|
||||
"天气预报", "气象", "风力", "体感",
|
||||
"新闻", "头条", "股价", "汇率", "实时", "今日新闻", "热点新闻",
|
||||
}
|
||||
|
||||
// IsRealtimeInformationQuery 判定是否为需要实时检索的时效性问句。
|
||||
// 含强业务词典时不触发(避免「型检温度」「项目进度」等误伤)。
|
||||
func IsRealtimeInformationQuery(input string) bool {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if trimmed == "" || utf8.RuneCountInString(trimmed) < 2 {
|
||||
return false
|
||||
}
|
||||
if intent.HasStrongBusinessLexicon(trimmed) {
|
||||
return false
|
||||
}
|
||||
for _, m := range realtimeInfoMarkers {
|
||||
if strings.Contains(trimmed, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package coordinator
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsRealtimeInformationQuery(t *testing.T) {
|
||||
positives := []string{
|
||||
"今天北京天气怎么样",
|
||||
"明天会下雨吗",
|
||||
"外面气温多少",
|
||||
"今天有什么新闻",
|
||||
"美元汇率多少",
|
||||
"查一下实时股价",
|
||||
}
|
||||
for _, in := range positives {
|
||||
if !IsRealtimeInformationQuery(in) {
|
||||
t.Fatalf("expected realtime query: %q", in)
|
||||
}
|
||||
}
|
||||
|
||||
negatives := []string{
|
||||
"",
|
||||
"a",
|
||||
"项目进度怎么样",
|
||||
"型检温度指标怎么定",
|
||||
"注册路径需要注意什么",
|
||||
"帮我推进 V0.5 方案",
|
||||
"你好",
|
||||
}
|
||||
for _, in := range negatives {
|
||||
if IsRealtimeInformationQuery(in) {
|
||||
t.Fatalf("did not expect realtime query: %q", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,11 @@ func IsSmallTalkOrGeneralQuestion(input string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasStrongBusinessLexicon 是否含强业务词典(项目/法规/文档治理等),供闲聊与实时检索判定复用。
|
||||
func HasStrongBusinessLexicon(input string) bool {
|
||||
return hasStrongBusinessLexicon(input)
|
||||
}
|
||||
|
||||
func hasStrongBusinessLexicon(input string) bool {
|
||||
markers := []string{
|
||||
"项目", "阶段", "推进", "法规", "注册", "文档", "审核", "批准", "驳回",
|
||||
|
||||
@@ -63,6 +63,22 @@ func (b *countingBackend) Execute(ctx context.Context, req agent.Request) (*agen
|
||||
case "permanent-missing":
|
||||
return &agent.Result{Success: false, ErrorReason: "missing target deliverable output.md"}, nil
|
||||
|
||||
case "settled-no-deliverable-then-ok":
|
||||
if n == 1 {
|
||||
_ = os.WriteFile(filepath.Join(req.WorkDir, "execution.log"), []byte(
|
||||
`{"type":"agent_settled"}`+"\n",
|
||||
), 0o644)
|
||||
return &agent.Result{Success: true, Patches: map[string][]byte{
|
||||
"execution.log": []byte(`{"type":"agent_settled"}` + "\n"),
|
||||
}}, nil
|
||||
}
|
||||
if !strings.Contains(req.Prompt, "纠偏重试") || !strings.Contains(req.Prompt, "write") {
|
||||
return &agent.Result{Success: false, ErrorReason: "corrective prompt missing on retry"}, nil
|
||||
}
|
||||
body := []byte("# recovered after settled-no-deliverable\n")
|
||||
_ = os.WriteFile(filepath.Join(req.WorkDir, "output.md"), body, 0o644)
|
||||
return &agent.Result{Success: true, Patches: map[string][]byte{"output.md": body}}, nil
|
||||
|
||||
case "always-transient":
|
||||
return &agent.Result{Success: false, ErrorReason: failText, Retryable: true}, nil
|
||||
|
||||
@@ -175,6 +191,28 @@ func TestWorkflowDriver_DoesNotRetryPermanentMissing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowDriver_RetriesSettledNoDeliverableWithCorrectivePrompt(t *testing.T) {
|
||||
wsDir, driver := prepareSingleWriterWS(t)
|
||||
backend := &countingBackend{mode: "settled-no-deliverable-then-ok"}
|
||||
res, err := driver.Execute(context.Background(), wsDir, backend, squad.ExecuteOptions{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
t.Fatalf("settled-no-deliverable must retry with corrective prompt: %v", err)
|
||||
}
|
||||
if !res.Success {
|
||||
t.Fatalf("want success after corrective retry, got %s", res.ErrorReason)
|
||||
}
|
||||
if backend.calls != 2 {
|
||||
t.Fatalf("want one corrective re-invocation, calls=%d", backend.calls)
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(wsDir, "tasks", "01_draft", "output.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(body), "settled-no-deliverable") {
|
||||
t.Fatalf("recovered body missing: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowDriver_DoesNotOverwriteExistingTarget(t *testing.T) {
|
||||
wsDir, driver := prepareSingleWriterWS(t)
|
||||
backend := &countingBackend{mode: "fail-after-writing-target"}
|
||||
|
||||
@@ -514,11 +514,13 @@ func (d *WorkflowDriver) runOneStep(
|
||||
}
|
||||
|
||||
var (
|
||||
res *agent.Result
|
||||
execErr error
|
||||
matErr error
|
||||
duration int64
|
||||
ok bool
|
||||
res *agent.Result
|
||||
execErr error
|
||||
matErr error
|
||||
duration int64
|
||||
ok bool
|
||||
lastAssess agent.FailureAssessment
|
||||
basePrompt = string(taskData)
|
||||
)
|
||||
for attempt := 1; attempt <= agent.MaxTransientStepInvocations; attempt++ {
|
||||
onDisk, _ := os.ReadFile(prevOutPath)
|
||||
@@ -527,6 +529,11 @@ func (d *WorkflowDriver) runOneStep(
|
||||
// Preserve a this-invocation file (may be truncated). Do not call again.
|
||||
break
|
||||
}
|
||||
req.Prompt = basePrompt
|
||||
if attempt > 1 && strings.Contains(lastAssess.Reason, "settled-no-deliverable") {
|
||||
req.Prompt = basePrompt + "\n\n【纠偏重试】你上一轮没有调用 write 工具写 " + deliverableName +
|
||||
",本轮必须先用 write 工具把完整交付物写入当前目录 " + deliverableName + ",再结束。"
|
||||
}
|
||||
if attempt > 1 && streamer != nil {
|
||||
streamer.EmitPhase(
|
||||
"step_retrying",
|
||||
@@ -608,14 +615,14 @@ func (d *WorkflowDriver) runOneStep(
|
||||
}
|
||||
break
|
||||
}
|
||||
assess := agent.AssessStepFailure(agent.StepFailureSignals{
|
||||
lastAssess = agent.AssessStepFailure(agent.StepFailureSignals{
|
||||
Err: execErr,
|
||||
Result: res,
|
||||
WorkDir: subDir,
|
||||
TargetMissing: !wroteThisInvocation && (matErr != nil || (res != nil && strings.Contains(res.ErrorReason, "missing target deliverable"))),
|
||||
TargetExists: wroteThisInvocation,
|
||||
})
|
||||
if assess.Class != agent.FailureTransient || attempt >= agent.MaxTransientStepInvocations {
|
||||
if lastAssess.Class != agent.FailureTransient || attempt >= agent.MaxTransientStepInvocations {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+823
-66
File diff suppressed because it is too large
Load Diff
+17
-12
@@ -267,8 +267,14 @@ def apply_projection_locally(path: Path, change: dict) -> dict:
|
||||
after = replacement
|
||||
else:
|
||||
lines = before.splitlines()
|
||||
matches = [i for i, line in enumerate(lines)
|
||||
if record_id and record_id in [c.strip() for c in line.split('|') if c.strip()]]
|
||||
matches = []
|
||||
for i, line in enumerate(lines):
|
||||
if line.lstrip(' ').startswith('|'):
|
||||
cells = [cell.strip() for cell in line.strip().strip('|').split('|')]
|
||||
if record_id and record_id in cells[:2]:
|
||||
matches.append(i)
|
||||
if len(matches) > 1:
|
||||
raise RuntimeError('AMBIGUOUS_RECORD: record id is not unique in this document')
|
||||
current_row = lines[matches[0]] if matches else None
|
||||
live_hash = content_digest(current_row) if current_row else None
|
||||
if expected != live_hash:
|
||||
@@ -555,28 +561,27 @@ def cleanup_visibility_report(
|
||||
|
||||
|
||||
def project_cleanup_markdown(entries):
|
||||
"""Render cleanup visibility projection from structured entries.
|
||||
"""Render cleanup visibility markdown from structured entries (report only).
|
||||
|
||||
Ported for W2-U5 projection reuse; structured report remains authoritative.
|
||||
This is a readout helper for operators; it is not a docs-apply write path.
|
||||
"""
|
||||
lines = [
|
||||
'# Cleanup visibility projection',
|
||||
'',
|
||||
'> Generated by `agentctl docs project --kind cleanup`. '
|
||||
'Not a lock; machine `.locks` is authoritative.',
|
||||
'> Generated cleanup report render. Not a lock; machine `.locks` is authoritative.',
|
||||
'',
|
||||
'| path | branch | reason | cleanup_owner |',
|
||||
'| 路径/事项 | 分类与保留原因 | cleanup_owner | 复查触发 |',
|
||||
'| --- | --- | --- | --- |',
|
||||
]
|
||||
if not entries:
|
||||
lines.append('| — | — | none_pending | — |')
|
||||
lines.append('| — | none_pending | — | — |')
|
||||
else:
|
||||
for item in entries:
|
||||
path = str(item.get('path') or '').replace('|', '\\|')
|
||||
branch = str(item.get('branch') or '').replace('|', '\\|')
|
||||
reason = str(item.get('reason') or '').replace('|', '\\|')
|
||||
path = str(item.get('path') or item.get('item') or '').replace('|', '\\|')
|
||||
reason = str(item.get('reason') or item.get('category') or '').replace('|', '\\|')
|
||||
owner = str(item.get('cleanup_owner') or 'independent-maintainer').replace('|', '\\|')
|
||||
lines.append(f'| {path} | {branch} | {reason} | {owner} |')
|
||||
trigger = str(item.get('trigger') or item.get('复查触发') or '').replace('|', '\\|')
|
||||
lines.append(f'| {path} | {reason} | {owner} | {trigger} |')
|
||||
lines.append('')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# agentctl coordinator unit templates
|
||||
|
||||
Templates only. **Not loaded** on this machine.
|
||||
|
||||
- `agentctl-coordinator.service` — systemd `Type=simple` foreground unit
|
||||
- `com.agentctl.coordinator.plist` — launchd `KeepAlive` foreground job
|
||||
|
||||
Loading requires **explicit authorization** plus a fail-closed verification record
|
||||
(owner, host, command evidence, rollback). Do not `launchctl load` / `systemctl enable`
|
||||
from a task worktree without that gate.
|
||||
|
||||
Daemon runs as the caller; it only drives already-validated integrate candidates via
|
||||
`queue schedule` under exclusive `manager.lock`. It does not touch `env:*` and does
|
||||
not grant new capabilities.
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=agentctl foreground coordinator (queue schedule under manager.lock)
|
||||
Documentation=file://%h/Agent-System/scripts/daemon/README.md
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# WorkingDirectory must be the main worktree checked out on branch `dev`.
|
||||
WorkingDirectory=/path/to/Agent-System
|
||||
ExecStart=/usr/bin/python3 /path/to/Agent-System/scripts/agentctl.py daemon run --interval 1.0
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
# Runs as the unit user; no CapabilityBoundingSet escalation.
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.agentctl.coordinator</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/python3</string>
|
||||
<string>/path/to/Agent-System/scripts/agentctl.py</string>
|
||||
<string>daemon</string>
|
||||
<string>run</string>
|
||||
<string>--interval</string>
|
||||
<string>1.0</string>
|
||||
</array>
|
||||
<!-- WorkingDirectory must be the main worktree checked out on branch `dev`. -->
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/path/to/Agent-System</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<!-- Foreground process; launchd supervises without setsid daemonization. -->
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/agentctl-coordinator.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/agentctl-coordinator.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -49,7 +49,8 @@ class AgentctlTests(unittest.TestCase):
|
||||
"# Coordination\n\n| Task | Owner | Branch | State |\n| --- | --- | --- | --- |\n"
|
||||
+ "\n".join(self.current_rows.values()) + "\n", encoding="utf-8")
|
||||
(self.root / ".doc/status/FEATURE-MATRIX.md").write_text(
|
||||
"# Features\n\n| Feature | State |\n| --- | --- |\n| core | planned |\n", encoding="utf-8")
|
||||
"# Features\n\n| record_id | 模块 | 状态 |\n| --- | --- | --- |\n| core | core | planned |\n",
|
||||
encoding="utf-8")
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-m", "test fixture")
|
||||
|
||||
@@ -137,6 +138,11 @@ class AgentctlTests(unittest.TestCase):
|
||||
out, err = process.communicate(timeout=10)
|
||||
self.assertEqual(process.returncode, 0, out + err)
|
||||
|
||||
def _last_json(self, text):
|
||||
lines = [line for line in text.splitlines() if line.strip()]
|
||||
self.assertTrue(lines, text)
|
||||
return json.loads(lines[-1])
|
||||
|
||||
def docs_request(self, changes, request_id="request-alpha"):
|
||||
path = pathlib.Path(self.temp.name) / (request_id + ".json")
|
||||
path.write_text(json.dumps({"request_id": request_id, "changes": changes}), encoding="utf-8")
|
||||
@@ -368,7 +374,7 @@ class AgentctlTests(unittest.TestCase):
|
||||
before = {name: (task / name).read_bytes() for name in (current, features)}
|
||||
request = self.docs_request([
|
||||
self.doc_change(current, "alpha", self.current_rows["alpha"], self.current_rows["alpha"].replace("active", "done")),
|
||||
self.doc_change(features, "core", "| core | stale |", "| core | done |"),
|
||||
self.doc_change(features, "core", "| core | core | stale |", "| core | core | done |"),
|
||||
])
|
||||
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
@@ -523,11 +529,159 @@ class AgentctlTests(unittest.TestCase):
|
||||
for indent in range(4):
|
||||
with self.subTest(indent=indent):
|
||||
request = self.docs_request([{"file":filename,"record_id":"@document","expected_sha256":snapshot["sha256"],
|
||||
"replacement":original.replace("| core | planned |", " "*indent+"| core |done|")}], "compact-done-"+str(indent))
|
||||
"replacement":original.replace("| core | core | planned |", " "*indent+"| core | core |done|")}], "compact-done-"+str(indent))
|
||||
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual((task / filename).read_text(), original)
|
||||
|
||||
def test_matrix_done_without_evidence_refused(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish", "docs:approve", "docs:admin"))
|
||||
filename = ".doc/status/FEATURE-MATRIX.md"
|
||||
old = "| core | core | planned |"
|
||||
# Row update to done without evidence list → EVIDENCE_REQUIRED
|
||||
request = self.docs_request([
|
||||
self.doc_change(filename, "core", old, "| core | core | done |"),
|
||||
], "matrix-done-no-evidence")
|
||||
denied = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("EVIDENCE_REQUIRED", denied.stderr)
|
||||
self.assertIn("| core | core | planned |", (task / filename).read_text())
|
||||
# @document rewrite that bumps partial/planned→done without per-row evidence
|
||||
original = (task / filename).read_text()
|
||||
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "@document", cwd=task).stdout)
|
||||
bumped = original.replace("| core | core | planned |", "| core | core | done |")
|
||||
request = self.docs_request([{
|
||||
"file": filename, "record_id": "@document",
|
||||
"expected_sha256": snapshot["sha256"], "replacement": bumped,
|
||||
}], "matrix-doc-done-bypass")
|
||||
denied = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("EVIDENCE_REQUIRED", denied.stderr)
|
||||
self.assertEqual((task / filename).read_text(), original)
|
||||
|
||||
def test_matrix_record_id_layout_migration_without_new_dones(self):
|
||||
"""Additive record_id column must not trip EVIDENCE_REQUIRED when statuses unchanged."""
|
||||
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
|
||||
filename = ".doc/status/FEATURE-MATRIX.md"
|
||||
legacy = (
|
||||
"# Features\n\n"
|
||||
"| 模块 | 状态 |\n"
|
||||
"| --- | --- |\n"
|
||||
"| core | planned |\n"
|
||||
"| intent | done |\n"
|
||||
)
|
||||
(task / filename).write_text(legacy, encoding="utf-8")
|
||||
self.git("add", filename, cwd=task)
|
||||
self.git("commit", "-m", "legacy matrix", cwd=task)
|
||||
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "@document", cwd=task).stdout)
|
||||
migrated = (
|
||||
"# Features\n\n"
|
||||
"| record_id | 模块 | 状态 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| core | core | planned |\n"
|
||||
"| intent-l1 | intent | done |\n"
|
||||
)
|
||||
request = self.docs_request([{
|
||||
"file": filename, "record_id": "@document",
|
||||
"expected_sha256": snapshot["sha256"], "replacement": migrated,
|
||||
}], "matrix-layout-migrate")
|
||||
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
text = (task / filename).read_text()
|
||||
self.assertIn("| record_id | 模块 | 状态 |", text)
|
||||
self.assertIn("| intent-l1 | intent | done |", text)
|
||||
self.assertIn("| core | core | planned |", text)
|
||||
|
||||
def test_matrix_unknown_columns_and_lineage_preserved(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
|
||||
filename = ".doc/status/FEATURE-MATRIX.md"
|
||||
body = (
|
||||
"# Features\n\n"
|
||||
"| record_id | 模块 | 状态 | extra |\n"
|
||||
"| --- | --- | --- | --- |\n"
|
||||
"| core | core | planned | keep-me |\n\n"
|
||||
"## Wave 0 lineage\n\n"
|
||||
"trailing history must survive layout edits.\n"
|
||||
)
|
||||
(task / filename).write_text(body, encoding="utf-8")
|
||||
self.git("add", filename, cwd=task)
|
||||
self.git("commit", "-m", "seed matrix lineage", cwd=task)
|
||||
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "@document", cwd=task).stdout)
|
||||
updated = body.replace("| core | core | planned | keep-me |",
|
||||
"| core | core | planned | keep-me-too |")
|
||||
request = self.docs_request([{
|
||||
"file": filename, "record_id": "@document",
|
||||
"expected_sha256": snapshot["sha256"], "replacement": updated,
|
||||
}], "matrix-preserve-wave0")
|
||||
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
text = (task / filename).read_text()
|
||||
self.assertIn("## Wave 0 lineage", text)
|
||||
self.assertIn("trailing history must survive layout edits.", text)
|
||||
self.assertIn("keep-me-too", text)
|
||||
self.assertIn("| record_id | 模块 | 状态 | extra |", text)
|
||||
|
||||
def test_task_close_does_not_auto_promote_matrix_done(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish",))
|
||||
filename = ".doc/status/FEATURE-MATRIX.md"
|
||||
before = (task / filename).read_text()
|
||||
self.assertIn("| core | core | planned |", before)
|
||||
closed = json.loads(self.cli("task", "close", "--id", "alpha", "--status", "done").stdout)
|
||||
self.assertEqual(closed["status"], "done")
|
||||
after = (task / filename).read_text()
|
||||
self.assertEqual(after, before)
|
||||
self.assertIn("| core | core | planned |", after)
|
||||
self.assertFalse((task / ".doc/remediation/gap-matrix.md").exists())
|
||||
|
||||
def test_matrix_duplicate_or_rename_record_id_rejected(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
|
||||
filename = ".doc/status/FEATURE-MATRIX.md"
|
||||
# Duplicate record_id in document → INVALID_DOCUMENT / AMBIGUOUS
|
||||
dup = (
|
||||
"# Features\n\n"
|
||||
"| record_id | 模块 | 状态 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| core | core | planned |\n"
|
||||
"| core | other | partial |\n"
|
||||
)
|
||||
(task / filename).write_text(dup, encoding="utf-8")
|
||||
self.git("add", filename, cwd=task)
|
||||
self.git("commit", "-m", "dup matrix", cwd=task)
|
||||
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "@document", cwd=task).stdout)
|
||||
request = self.docs_request([{
|
||||
"file": filename, "record_id": "@document",
|
||||
"expected_sha256": snapshot["sha256"], "replacement": dup,
|
||||
}], "matrix-dup-apply")
|
||||
denied = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertTrue(
|
||||
"INVALID_DOCUMENT" in denied.stderr or "AMBIGUOUS_RECORD" in denied.stderr,
|
||||
denied.stderr)
|
||||
# Replacement missing / renaming record_id → INVALID_REQUEST
|
||||
good = (
|
||||
"# Features\n\n"
|
||||
"| record_id | 模块 | 状态 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| core | core | planned |\n"
|
||||
)
|
||||
(task / filename).write_text(good, encoding="utf-8")
|
||||
self.git("add", filename, cwd=task)
|
||||
self.git("commit", "-m", "reset matrix", cwd=task)
|
||||
old = "| core | core | planned |"
|
||||
# Renaming record_id while editing under the old id
|
||||
request = self.docs_request([
|
||||
self.doc_change(filename, "core", old, "| core-renamed | core | planned |"),
|
||||
], "matrix-rename-id")
|
||||
denied = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("INVALID_REQUEST", denied.stderr)
|
||||
request = self.docs_request([
|
||||
self.doc_change(filename, "core", old, "| enigmamodule | planned |"),
|
||||
], "matrix-missing-id")
|
||||
denied = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("INVALID_REQUEST", denied.stderr)
|
||||
|
||||
def test_validated_linear_commit_cannot_bypass_candidate_entrypoint(self):
|
||||
task = self.initialized_task(scopes=("src",), capabilities=("git:dev",))
|
||||
self.cli("hooks", "install")
|
||||
@@ -619,7 +773,7 @@ class AgentctlTests(unittest.TestCase):
|
||||
self.assertEqual(self.git('rev-parse','dev'),baseline)
|
||||
blocked=self.run_lease('candidate',['git:index'],cwd=candidate)
|
||||
self.assertNotEqual(blocked.returncode,0)
|
||||
self.cli('recover','--run',run_id,'--reason','test confirmed Git proxy stopped before ref update')
|
||||
self.cli('recover','--run',run_id,'--reason','verified: test confirmed Git proxy stopped before ref update')
|
||||
finally:
|
||||
listener.close()
|
||||
if process.poll() is None:
|
||||
@@ -789,6 +943,543 @@ class AgentctlTests(unittest.TestCase):
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("POLICY_MISMATCH", denied.stderr)
|
||||
|
||||
def _register_claim_task(self, name, scopes=("src",)):
|
||||
path = self.root / ".worktrees" / name
|
||||
if not path.exists():
|
||||
self.git("worktree", "add", "-b", "feature/" + name, str(path), "dev")
|
||||
args = ["task", "register", "--id", name, "--owner", "ClaimAgent"]
|
||||
for scope in scopes:
|
||||
args.extend(("--scope", scope))
|
||||
self.cli(*args, cwd=path)
|
||||
return path
|
||||
|
||||
def test_claim_concurrent_twenty_exactly_one_held(self):
|
||||
import concurrent.futures
|
||||
self.cli("init")
|
||||
(self.root / "src").mkdir(exist_ok=True)
|
||||
tasks = []
|
||||
for idx in range(20):
|
||||
name = f"c{idx:02d}"
|
||||
self._register_claim_task(name, scopes=("src",))
|
||||
tasks.append(name)
|
||||
|
||||
def attempt(name):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(self.root / "scripts/agentctl.py"),
|
||||
"claim", "acquire", "--task", name, "--resource", "claim:src/shared",
|
||||
"--request-id", f"req-{name}"],
|
||||
cwd=self.root / ".worktrees" / name, text=True, capture_output=True, timeout=30)
|
||||
return name, result.returncode, result.stdout, result.stderr
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as pool:
|
||||
outcomes = list(pool.map(attempt, tasks))
|
||||
held = [item for item in outcomes if item[1] == 0]
|
||||
busy = [item for item in outcomes if item[1] != 0]
|
||||
self.assertEqual(len(held), 1, outcomes)
|
||||
self.assertEqual(len(busy), 19)
|
||||
for _, code, _out, err in busy:
|
||||
self.assertEqual(code, 75)
|
||||
payload = json.loads(err)
|
||||
self.assertEqual(payload["code"], "LOCK_BUSY")
|
||||
self.assertIn("holder", payload)
|
||||
self.assertIn("ticket", payload)
|
||||
self.assertIn("retry_after", payload)
|
||||
listed = json.loads(self.cli("claim", "list").stdout)
|
||||
held_rows = [row for row in listed["claims"] if row["state"] == "held"]
|
||||
self.assertEqual(len(held_rows), 1)
|
||||
self.assertEqual(held_rows[0]["resource"], "claim:src/shared")
|
||||
|
||||
def test_claim_unrelated_resources_parallel_ok(self):
|
||||
alpha = self.initialized_task("alpha", scopes=("src/a", "src/b"))
|
||||
beta = self.initialized_task("beta", scopes=("src/a", "src/b"))
|
||||
a = json.loads(self.cli(
|
||||
"claim", "acquire", "--task", "alpha", "--resource", "claim:src/a",
|
||||
"--request-id", "par-a", cwd=alpha).stdout)
|
||||
b = json.loads(self.cli(
|
||||
"claim", "acquire", "--task", "beta", "--resource", "claim:src/b",
|
||||
"--request-id", "par-b", cwd=beta).stdout)
|
||||
self.assertEqual(a["status"], "held")
|
||||
self.assertEqual(b["status"], "held")
|
||||
|
||||
def test_claim_parent_child_overlap_and_sibling_ok(self):
|
||||
alpha = self.initialized_task("alpha", scopes=("src", "src2"))
|
||||
beta = self.initialized_task("beta", scopes=("src", "src2"))
|
||||
self.cli("claim", "acquire", "--task", "alpha", "--resource", "claim:src",
|
||||
"--request-id", "parent-1", cwd=alpha)
|
||||
child = self.cli(
|
||||
"claim", "acquire", "--task", "beta", "--resource", "claim:src/a.py",
|
||||
"--request-id", "child-1", cwd=beta, check=False)
|
||||
self.assertEqual(child.returncode, 75)
|
||||
self.assertIn("LOCK_BUSY", child.stderr)
|
||||
sibling = json.loads(self.cli(
|
||||
"claim", "acquire", "--task", "beta", "--resource", "claim:src2",
|
||||
"--request-id", "sib-1", cwd=beta).stdout)
|
||||
self.assertEqual(sibling["status"], "held")
|
||||
|
||||
def test_claim_frozen_rejects_even_holder(self):
|
||||
alpha = self.initialized_task("alpha", scopes=("src",))
|
||||
self.cli("claim", "acquire", "--task", "alpha", "--resource", "claim:src/locked",
|
||||
"--request-id", "fz-1", cwd=alpha)
|
||||
self.cli("freeze", "--task", "alpha", "--resource", "claim:src/locked",
|
||||
"--reason", "maintenance", cwd=alpha)
|
||||
denied = self.cli(
|
||||
"claim", "acquire", "--task", "alpha", "--resource", "claim:src/locked",
|
||||
"--request-id", "fz-2", cwd=alpha, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("FROZEN", denied.stderr)
|
||||
|
||||
def test_claim_request_id_idempotent_and_reuse_guard(self):
|
||||
alpha = self.initialized_task("alpha", scopes=("src",))
|
||||
first = json.loads(self.cli(
|
||||
"claim", "acquire", "--task", "alpha", "--resource", "claim:src/idem",
|
||||
"--request-id", "same-req", cwd=alpha).stdout)
|
||||
self.assertEqual(first["status"], "held")
|
||||
replay = json.loads(self.cli(
|
||||
"claim", "acquire", "--task", "alpha", "--resource", "claim:src/idem",
|
||||
"--request-id", "same-req", cwd=alpha).stdout)
|
||||
self.assertTrue(replay.get("replayed"))
|
||||
reused = self.cli(
|
||||
"claim", "acquire", "--task", "alpha", "--resource", "claim:src/other",
|
||||
"--request-id", "same-req", cwd=alpha, check=False)
|
||||
self.assertNotEqual(reused.returncode, 0)
|
||||
self.assertIn("REQUEST_ID_REUSED", reused.stderr)
|
||||
|
||||
def test_claim_blocks_task_close_until_released(self):
|
||||
alpha = self.initialized_task("alpha", scopes=("src",))
|
||||
self.cli("claim", "acquire", "--task", "alpha", "--resource", "claim:src/close",
|
||||
"--request-id", "close-1", cwd=alpha)
|
||||
denied = self.cli("task", "close", "--id", "alpha", check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("LOCK_BUSY", denied.stderr)
|
||||
self.cli("claim", "release", "--task", "alpha", "--resource", "claim:src/close", cwd=alpha)
|
||||
closed = json.loads(self.cli("task", "close", "--id", "alpha").stdout)
|
||||
self.assertEqual(closed["status"], "done")
|
||||
|
||||
def _seed_run(self, run_id, task, resources, state, *, heartbeat_age=0, suspect_age=None,
|
||||
pid=None, start="start-token", boot=None, pgid=None, fence=0):
|
||||
import sqlite3
|
||||
import time as time_mod
|
||||
now = time_mod.time()
|
||||
boot = boot or "boot-token"
|
||||
pid = 1 if pid is None else pid
|
||||
conn = sqlite3.connect(self.root / ".locks" / "state.sqlite")
|
||||
try:
|
||||
cols = {row[1] for row in conn.execute("PRAGMA table_info(runs)")}
|
||||
for name, decl in (
|
||||
("fence", "INTEGER NOT NULL DEFAULT 0"),
|
||||
("suspect_since", "REAL"),
|
||||
("quarantined_at", "REAL"),
|
||||
("quarantine_reason", "TEXT"),
|
||||
("verified_by", "TEXT"),
|
||||
):
|
||||
if name not in cols:
|
||||
conn.execute(f"ALTER TABLE runs ADD COLUMN {name} {decl}")
|
||||
suspect_since = None if suspect_age is None else now - suspect_age
|
||||
conn.execute(
|
||||
"""INSERT INTO runs(id,task,fingerprint,resources,mode,state,pid,start,boot,
|
||||
created,heartbeat,kind,pgid,fence,suspect_since)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(run_id, task, "fp", json.dumps(resources), "X", state, pid, start, boot,
|
||||
now, now - heartbeat_age, "command", pgid or pid, fence, suspect_since))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_quarantine_sweep_keeps_live_child_and_release_refuses(self):
|
||||
if not hasattr(os, "pidfd_open"):
|
||||
self.skipTest("pidfd_open is Linux-only")
|
||||
task = self.initialized_task()
|
||||
process, connection, payload = self.live_lease("alpha", ["path:src/a"], task)
|
||||
child_fd = os.pidfd_open(payload["pid"])
|
||||
self.addCleanup(os.close, child_fd)
|
||||
process.kill()
|
||||
process.wait(timeout=5)
|
||||
self.assertFalse(select.select([child_fd], [], [], 0)[0], "fixture child must still be alive")
|
||||
# Force heartbeat stale so sweep considers demotion; live child still blocks release.
|
||||
import sqlite3
|
||||
import time as time_mod
|
||||
conn = sqlite3.connect(self.root / ".locks" / "state.sqlite")
|
||||
conn.execute("UPDATE runs SET heartbeat=? WHERE id=?", (time_mod.time() - 10_000, payload["run"]))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
swept = json.loads(self.cli("quarantine", "sweep").stdout)
|
||||
states = {item["to"] for item in swept.get("demoted", []) if item["run_id"] == payload["run"]}
|
||||
self.assertTrue(not states or states <= {"suspect"}, swept)
|
||||
release = self.cli(
|
||||
"quarantine", "release", "--run", payload["run"],
|
||||
"--reason", "verified: operator checked", "--verified-by", "tester", check=False)
|
||||
self.assertNotEqual(release.returncode, 0, release.stdout + release.stderr)
|
||||
self.assertIn("LOCK_BUSY", release.stderr)
|
||||
connection.sendall(b"x")
|
||||
self.assertTrue(select.select([child_fd], [], [], 10)[0], "fixture child must finish")
|
||||
process.communicate(timeout=5)
|
||||
|
||||
def test_quarantine_unreadable_proc_stops_at_suspect(self):
|
||||
from unittest import mock
|
||||
import importlib.util
|
||||
self.initialized_task()
|
||||
self._seed_run("run-unknown", "alpha", ["path:src/a"], "running", heartbeat_age=10_000)
|
||||
self._seed_run("run-suspect", "alpha", ["path:src/a"], "suspect",
|
||||
heartbeat_age=10_000, suspect_age=10_000)
|
||||
spec = importlib.util.spec_from_file_location("agentctl_q", self.root / "scripts/agentctl.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
repo = mod.Repo(self.root)
|
||||
mod.require_policy(repo)
|
||||
with mock.patch.object(mod, "alive_status", return_value="unknown"), \
|
||||
mock.patch.object(mod, "group_status", return_value="unknown"):
|
||||
mod.quarantine_sweep(repo)
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(self.root / ".locks" / "state.sqlite")
|
||||
rows = {r[0]: r[1] for r in conn.execute(
|
||||
"SELECT id,state FROM runs WHERE id IN ('run-unknown','run-suspect')")}
|
||||
conn.close()
|
||||
self.assertEqual(rows["run-unknown"], "suspect")
|
||||
self.assertEqual(rows["run-suspect"], "suspect")
|
||||
|
||||
def test_quarantine_ttl_demotes_never_auto_completes(self):
|
||||
from unittest import mock
|
||||
import importlib.util
|
||||
self.initialized_task()
|
||||
self._seed_run("hb-stale", "alpha", ["path:src/a"], "running", heartbeat_age=10_000)
|
||||
self._seed_run("sus-stale", "alpha", ["path:src/b"], "suspect",
|
||||
heartbeat_age=10_000, suspect_age=10_000)
|
||||
spec = importlib.util.spec_from_file_location("agentctl_ttl", self.root / "scripts/agentctl.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
repo = mod.Repo(self.root)
|
||||
mod.require_policy(repo)
|
||||
with mock.patch.object(mod, "alive_status", return_value="dead"), \
|
||||
mock.patch.object(mod, "group_status", return_value="dead"):
|
||||
mod.quarantine_sweep(repo)
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(self.root / ".locks" / "state.sqlite")
|
||||
rows = {r[0]: r[1] for r in conn.execute(
|
||||
"SELECT id,state FROM runs WHERE id IN ('hb-stale','sus-stale')")}
|
||||
conn.close()
|
||||
self.assertEqual(rows["hb-stale"], "suspect")
|
||||
self.assertEqual(rows["sus-stale"], "quarantine")
|
||||
listed = json.loads(self.cli("quarantine", "list").stdout)
|
||||
ids = {r["id"] for r in listed["runs"]}
|
||||
self.assertIn("hb-stale", ids)
|
||||
self.assertIn("sus-stale", ids)
|
||||
self.assertNotIn("completed", rows.values())
|
||||
self.assertNotIn("recovered", rows.values())
|
||||
|
||||
def test_stale_fence_env_is_stale_generation(self):
|
||||
task = self.initialized_task()
|
||||
code = (
|
||||
"import os,subprocess,sys;"
|
||||
"os.environ['AGENTCTL_FENCE']='999';"
|
||||
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
|
||||
"'guard','resource','path:src/a']);sys.exit(p.returncode)"
|
||||
)
|
||||
result = self.run_lease("alpha", ["path:src/a"], code, cwd=task)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("STALE_GENERATION", result.stderr)
|
||||
|
||||
def test_unset_fence_env_matches_default_zero(self):
|
||||
"""Self-bootstrap: hooks re-exec updated agentctl without AGENTCTL_FENCE set."""
|
||||
task = self.initialized_task()
|
||||
code = (
|
||||
"import os,subprocess,sys;"
|
||||
"os.environ.pop('AGENTCTL_FENCE', None);"
|
||||
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
|
||||
"'guard','resource','path:src/a']);sys.exit(p.returncode)"
|
||||
)
|
||||
result = self.run_lease("alpha", ["path:src/a"], code, cwd=task)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_recover_git_dev_requires_verification_marker(self):
|
||||
task = self.initialized_task(capabilities=("git:dev",))
|
||||
self._seed_run("git-dev-run", "alpha", ["git:dev"], "recovery_required",
|
||||
heartbeat_age=10_000, pid=2**30 - 3, boot="other-boot")
|
||||
denied = self.cli("recover", "--run", "git-dev-run", "--reason", "looks dead", check=False)
|
||||
self.assertNotEqual(denied.returncode, 0, denied.stdout + denied.stderr)
|
||||
self.assertIn("EVIDENCE_REQUIRED", denied.stderr)
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(self.root / ".locks" / "state.sqlite")
|
||||
state = conn.execute("SELECT state FROM runs WHERE id='git-dev-run'").fetchone()[0]
|
||||
conn.close()
|
||||
self.assertEqual(state, "recovery_required")
|
||||
|
||||
def test_daemon_single_instance_lock_busy_with_holder_pid(self):
|
||||
self.cli("init")
|
||||
import concurrent.futures
|
||||
cmd = [sys.executable, str(self.root / "scripts/agentctl.py"),
|
||||
"daemon", "run", "--interval", "0", "--max-cycles", "3"]
|
||||
|
||||
def attempt():
|
||||
return subprocess.run(cmd, cwd=self.root, text=True, capture_output=True, timeout=20)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
||||
first = pool.submit(attempt)
|
||||
second = pool.submit(attempt)
|
||||
results = [first.result(), second.result()]
|
||||
codes = sorted(r.returncode for r in results)
|
||||
self.assertEqual(codes, [0, 75], [(r.returncode, r.stdout, r.stderr) for r in results])
|
||||
busy = next(r for r in results if r.returncode == 75)
|
||||
payload = json.loads(busy.stderr)
|
||||
self.assertEqual(payload["code"], "LOCK_BUSY")
|
||||
self.assertIn("holder_pid", payload)
|
||||
self.assertIsInstance(payload["holder_pid"], int)
|
||||
|
||||
def test_daemon_stop_leaves_pending_queue_untouched(self):
|
||||
task = self.initialized_task(scopes=(".",), capabilities=("git:dev",))
|
||||
tip_before = self.git("rev-parse", "dev")
|
||||
# Seed a pending item that is NOT validated — daemon must not promote it.
|
||||
import sqlite3
|
||||
import time as time_mod
|
||||
import uuid
|
||||
item_id = str(uuid.uuid4())
|
||||
now = time_mod.time()
|
||||
with sqlite3.connect(self.root / ".locks" / "state.sqlite") as db:
|
||||
db.execute(
|
||||
"INSERT INTO queue_items(id,task,head_sha,expected_dev,priority,state,created,updated)"
|
||||
" VALUES(?,?,?,?,?,?,?,?)",
|
||||
(item_id, "alpha", tip_before, tip_before, 0, "pending", now, now))
|
||||
db.commit()
|
||||
self.cli("daemon", "run", "--interval", "0", "--max-cycles", "2", cwd=self.root)
|
||||
self.assertEqual(self.git("rev-parse", "dev"), tip_before)
|
||||
with sqlite3.connect(self.root / ".locks" / "state.sqlite") as db:
|
||||
state = db.execute("SELECT state FROM queue_items WHERE id=?", (item_id,)).fetchone()[0]
|
||||
# Failed admit is ok; must not become completed/merged into tip.
|
||||
self.assertNotEqual(state, "completed")
|
||||
self.assertEqual(self.git("rev-parse", "dev"), tip_before)
|
||||
|
||||
def test_daemon_does_not_grant_register_or_docs_apply(self):
|
||||
self.cli("init")
|
||||
self.cli("daemon", "run", "--interval", "0", "--max-cycles", "1", cwd=self.root)
|
||||
denied = self.cli("task", "register", "--id", "rogue", "--owner", "x", "--scope", "src",
|
||||
cwd=self.root, check=False)
|
||||
self.assertNotEqual(denied.returncode, 0)
|
||||
self.assertIn("WORKTREE_REQUIRED", denied.stderr)
|
||||
status = json.loads(self.cli("daemon", "status", cwd=self.root).stdout)
|
||||
self.assertEqual(status["status"], "ok")
|
||||
self.assertIn("manager_lock", status)
|
||||
self.assertIn("queue", status)
|
||||
|
||||
def _write_current_with_sections(self, task_path, *, claim_rows=None, slot_body=None):
|
||||
"""CURRENT fixture with human sections + integration slot for protection tests."""
|
||||
rows = claim_rows or [
|
||||
"| owner | task | branch | worktree | mutex | status |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
"| Test Agent | alpha | feature/alpha | .worktrees/alpha | - | planned |",
|
||||
"| Test Agent | beta | feature/beta | .worktrees/beta | - | planned |",
|
||||
]
|
||||
slot = slot_body or (
|
||||
"| holder | phase | since | scope | queue |\n"
|
||||
"|--------|-------|-------|-------|-------|\n"
|
||||
"| - | idle | 2026-09-23 | test | - |\n"
|
||||
)
|
||||
body = (
|
||||
"# Coordination\n\n"
|
||||
"## 焦点\n\nfocus line\n\n"
|
||||
"## 并行认领\n\n"
|
||||
+ "\n".join(rows) + "\n\n"
|
||||
"## 清理接管与遗留现场\n\n"
|
||||
"| 路径/事项 | 分类与保留原因 | cleanup_owner | 复查触发 |\n"
|
||||
"| --- | --- | --- | --- |\n"
|
||||
"| none | none | - | - |\n\n"
|
||||
"## 集成槽位\n\n"
|
||||
+ slot + "\n"
|
||||
"## 受控约束\n\n- constraint\n\n"
|
||||
"## 活跃入口\n\n- entry\n"
|
||||
)
|
||||
path = task_path / ".doc/ops/CURRENT.md"
|
||||
path.write_text(body, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def _project_apply_with_retry(self, task_id, cwd, *extra, attempts=40):
|
||||
import time
|
||||
last = None
|
||||
for _ in range(attempts):
|
||||
last = self.cli("docs", "project", "--task", task_id, "--apply", *extra,
|
||||
cwd=cwd, check=False)
|
||||
if last.returncode == 0:
|
||||
return last
|
||||
err = last.stderr + last.stdout
|
||||
if "LOCK_BUSY" in err:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
return last
|
||||
return last
|
||||
|
||||
def test_docs_project_parallel_apply_keeps_both_rows(self):
|
||||
import concurrent.futures
|
||||
alpha = self.initialized_task(capabilities=("docs:publish",))
|
||||
beta = self.initialized_task(task="beta", scopes=("src/b", ".doc"),
|
||||
capabilities=("docs:publish",))
|
||||
self._write_current_with_sections(alpha)
|
||||
self.git("add", ".doc/ops/CURRENT.md", cwd=alpha)
|
||||
self.git("commit", "-m", "sectioned current", cwd=alpha)
|
||||
# Mirror committed baseline into beta worktree.
|
||||
self.git("checkout", "feature/alpha", "--", ".doc/ops/CURRENT.md", cwd=beta)
|
||||
self.git("add", ".doc/ops/CURRENT.md", cwd=beta)
|
||||
self.git("commit", "-m", "sync current", cwd=beta)
|
||||
|
||||
def run(task_id, cwd, status):
|
||||
return self._project_apply_with_retry(
|
||||
task_id, cwd, "--mutex", task_id, "--status", status)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
||||
fut_a = pool.submit(run, "alpha", alpha, "in_progress")
|
||||
fut_b = pool.submit(run, "beta", beta, "blocked")
|
||||
results = [fut_a.result(), fut_b.result()]
|
||||
for result in results:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
payload = self._last_json(result.stdout)
|
||||
self.assertEqual(payload["status"], "projected_and_applied")
|
||||
text_a = (alpha / ".doc/ops/CURRENT.md").read_text(encoding="utf-8")
|
||||
text_b = (beta / ".doc/ops/CURRENT.md").read_text(encoding="utf-8")
|
||||
self.assertRegex(text_a, r"\| Test Agent \| alpha \|.*\| in_progress \|")
|
||||
self.assertRegex(text_a, r"\| Test Agent \| beta \|")
|
||||
self.assertRegex(text_b, r"\| Test Agent \| beta \|.*\| blocked \|")
|
||||
self.assertRegex(text_b, r"\| Test Agent \| alpha \|")
|
||||
|
||||
def test_docs_project_same_row_race_base_changed_no_partial(self):
|
||||
import concurrent.futures
|
||||
task = self.initialized_task(capabilities=("docs:publish",))
|
||||
self._write_current_with_sections(task)
|
||||
self.git("add", ".doc/ops/CURRENT.md", cwd=task)
|
||||
self.git("commit", "-m", "sectioned current", cwd=task)
|
||||
snap = json.loads(self.cli(
|
||||
"docs", "snapshot", "--file", ".doc/ops/CURRENT.md",
|
||||
"--record", "alpha", cwd=task).stdout)
|
||||
row = snap["row"]
|
||||
self.assertIsNotNone(row)
|
||||
req_a = self.docs_request(
|
||||
[self.doc_change(
|
||||
".doc/ops/CURRENT.md", "alpha", row,
|
||||
"| Test Agent | alpha | feature/alpha | .worktrees/alpha | - | in_progress |")],
|
||||
"same-row-a")
|
||||
req_b = self.docs_request(
|
||||
[self.doc_change(
|
||||
".doc/ops/CURRENT.md", "alpha", row,
|
||||
"| Test Agent | alpha | feature/alpha | .worktrees/alpha | - | done |")],
|
||||
"same-row-b")
|
||||
baseline = (task / ".doc/ops/CURRENT.md").read_bytes()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
||||
r1 = pool.submit(lambda: self.cli(
|
||||
"docs", "apply", "--task", "alpha", "--request", req_a,
|
||||
cwd=task, check=False))
|
||||
r2 = pool.submit(lambda: self.cli(
|
||||
"docs", "apply", "--task", "alpha", "--request", req_b,
|
||||
cwd=task, check=False))
|
||||
results = [r1.result(), r2.result()]
|
||||
codes = sorted(r.returncode for r in results)
|
||||
self.assertEqual(codes[0], 0, [(r.returncode, r.stderr) for r in results])
|
||||
self.assertNotEqual(codes[1], 0)
|
||||
loser = next(r for r in results if r.returncode != 0)
|
||||
if "LOCK_BUSY" in loser.stderr:
|
||||
detail = json.loads(loser.stderr)
|
||||
rid = detail.get("request_id")
|
||||
retry_req = req_a if rid == "same-row-a" else req_b
|
||||
loser = self.cli("docs", "apply", "--task", "alpha", "--request", retry_req,
|
||||
cwd=task, check=False)
|
||||
self.assertNotEqual(loser.returncode, 0, loser.stderr)
|
||||
self.assertIn("BASE_CHANGED", loser.stderr)
|
||||
after = (task / ".doc/ops/CURRENT.md").read_text(encoding="utf-8")
|
||||
self.assertEqual(after.count("| alpha |"), 1)
|
||||
self.assertIn("## 焦点", after)
|
||||
self.assertIn("focus line", after)
|
||||
self.assertNotEqual(after.encode(), baseline)
|
||||
# Exactly one of the two statuses landed.
|
||||
self.assertTrue(("| in_progress |" in after) ^ ("| done |" in after))
|
||||
|
||||
def test_admin_document_cannot_overwrite_integration_slot(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
|
||||
path = self._write_current_with_sections(task)
|
||||
self.git("add", ".doc/ops/CURRENT.md", cwd=task)
|
||||
self.git("commit", "-m", "sectioned current", cwd=task)
|
||||
original = path.read_text(encoding="utf-8")
|
||||
snap = json.loads(self.cli(
|
||||
"docs", "snapshot", "--file", ".doc/ops/CURRENT.md",
|
||||
"--record", "@document", cwd=task).stdout)
|
||||
mutated = original.replace("| - | idle | 2026-09-23 | test | - |",
|
||||
"| alice | merging | 2026-09-23 | stolen | - |")
|
||||
self.assertNotEqual(mutated, original)
|
||||
request = self.docs_request([{
|
||||
"file": ".doc/ops/CURRENT.md",
|
||||
"record_id": "@document",
|
||||
"expected_sha256": snap["sha256"],
|
||||
"replacement": mutated,
|
||||
}], "slot-steal")
|
||||
result = self.cli("docs", "apply", "--task", "alpha", "--request", request,
|
||||
cwd=task, check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("PATH_OUT_OF_SCOPE", result.stderr)
|
||||
self.assertEqual(path.read_text(encoding="utf-8"), original)
|
||||
|
||||
def test_admin_document_cannot_overwrite_human_sections(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
|
||||
path = self._write_current_with_sections(task)
|
||||
self.git("add", ".doc/ops/CURRENT.md", cwd=task)
|
||||
self.git("commit", "-m", "sectioned current", cwd=task)
|
||||
original = path.read_text(encoding="utf-8")
|
||||
snap = json.loads(self.cli(
|
||||
"docs", "snapshot", "--file", ".doc/ops/CURRENT.md",
|
||||
"--record", "@document", cwd=task).stdout)
|
||||
for old, new, label in (
|
||||
("focus line", "machine rewrote focus", "focus"),
|
||||
("| none | none | - | - |", "| hacked | x | y | z |", "cleanup"),
|
||||
("- constraint", "- machine constraint", "constraints"),
|
||||
("- entry", "- machine entry", "entries"),
|
||||
):
|
||||
with self.subTest(section=label):
|
||||
mutated = original.replace(old, new)
|
||||
self.assertNotEqual(mutated, original)
|
||||
request = self.docs_request([{
|
||||
"file": ".doc/ops/CURRENT.md",
|
||||
"record_id": "@document",
|
||||
"expected_sha256": snap["sha256"],
|
||||
"replacement": mutated,
|
||||
}], "human-" + label)
|
||||
result = self.cli("docs", "apply", "--task", "alpha", "--request", request,
|
||||
cwd=task, check=False)
|
||||
self.assertNotEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("PATH_OUT_OF_SCOPE", result.stderr)
|
||||
self.assertEqual(path.read_text(encoding="utf-8"), original)
|
||||
|
||||
def test_docs_project_preview_apply_receipts_and_projections(self):
|
||||
task = self.initialized_task(capabilities=("docs:publish",))
|
||||
self._write_current_with_sections(task)
|
||||
self.git("add", ".doc/ops/CURRENT.md", cwd=task)
|
||||
self.git("commit", "-m", "sectioned current", cwd=task)
|
||||
preview = self._last_json(self.cli(
|
||||
"docs", "project", "--task", "alpha", "--mutex", "m1",
|
||||
"--status", "in_progress", cwd=task).stdout)
|
||||
self.assertEqual(preview["status"], "projected")
|
||||
self.assertIn("request_id", preview["request"])
|
||||
self.assertTrue(preview["request"]["request_id"].startswith("alpha-project-"))
|
||||
replay = self._last_json(self.cli(
|
||||
"docs", "project", "--task", "alpha", "--mutex", "m1",
|
||||
"--status", "in_progress", cwd=task).stdout)
|
||||
self.assertEqual(replay["request"]["request_id"], preview["request"]["request_id"])
|
||||
bad = self.cli("docs", "project", "--task", "alpha", "--status", "queued",
|
||||
cwd=task, check=False)
|
||||
self.assertNotEqual(bad.returncode, 0)
|
||||
self.assertIn("INVALID_REQUEST", bad.stderr)
|
||||
applied = self._last_json(self.cli(
|
||||
"docs", "project", "--task", "alpha", "--apply", "--mutex", "m1",
|
||||
"--status", "in_progress", cwd=task).stdout)
|
||||
self.assertEqual(applied["status"], "projected_and_applied")
|
||||
text = (task / ".doc/ops/CURRENT.md").read_text(encoding="utf-8")
|
||||
self.assertRegex(text, r"\| Test Agent \| alpha \|.*\| in_progress \|")
|
||||
import sqlite3
|
||||
with sqlite3.connect(self.root / ".locks" / "state.sqlite") as db:
|
||||
receipt = db.execute(
|
||||
"SELECT hash FROM receipts WHERE task=? AND file=?",
|
||||
("alpha", ".doc/ops/CURRENT.md")).fetchone()
|
||||
self.assertIsNotNone(receipt)
|
||||
self.assertEqual(receipt[0], hashlib.sha256(text.encode()).hexdigest())
|
||||
ledger = self._last_json(self.cli(
|
||||
"docs", "projections", "--file", ".doc/ops/CURRENT.md", cwd=task).stdout)
|
||||
self.assertEqual(ledger["status"], "ok")
|
||||
self.assertTrue(any(p["record_id"] == "alpha" and p["task"] == "alpha"
|
||||
for p in ledger["projections"]))
|
||||
|
||||
|
||||
class ProcessPlatformTests(unittest.TestCase):
|
||||
@classmethod
|
||||
|
||||
@@ -154,20 +154,45 @@ class AgentctlW4Tests(unittest.TestCase):
|
||||
self.assertEqual(receipt.get("recover_reason"),
|
||||
"operator cleared after fake failure")
|
||||
|
||||
def test_deploy_phase_transition_rejects_illegal_jump(self):
|
||||
deploy = self.w4.deployment_create(
|
||||
self.db, deploy_id="dep-phase", task="phase-task",
|
||||
env="dev", expected_commit="abcdef4", manifest=None, dry_run=True)
|
||||
self.assertEqual(deploy["phase"], "prepared")
|
||||
self.w4.deployment_set_phase(self.db, "dep-phase", "deploying")
|
||||
self.w4.deployment_set_phase(self.db, "dep-phase", "accepting")
|
||||
passed = self.w4.deployment_set_phase(self.db, "dep-phase", "passed")
|
||||
self.assertEqual(passed["phase"], "passed")
|
||||
def test_row_projection_ambiguous_record_refuses(self):
|
||||
doc = (
|
||||
"# Coordination\n\n"
|
||||
"| owner | task | branch | state |\n"
|
||||
"| --- | --- | --- | --- |\n"
|
||||
"| Alice | alpha | feature/alpha | active |\n"
|
||||
"| Bob | alpha | feature/alpha-2 | active |\n"
|
||||
)
|
||||
self._write_current(doc)
|
||||
change = self.w4.build_row_projection_change(
|
||||
file=".doc/ops/CURRENT.md",
|
||||
record_id="alpha",
|
||||
current_row="| Alice | alpha | feature/alpha | active |",
|
||||
replacement="| Alice | alpha | feature/alpha | done |",
|
||||
)
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
self.w4.deployment_set_phase(self.db, "dep-phase", "deploying")
|
||||
self.assertIn("illegal transition passed -> deploying", str(ctx.exception))
|
||||
still = self.w4.deployment_get(self.db, "dep-phase")
|
||||
self.assertEqual(still["phase"], "passed")
|
||||
self.w4.apply_projection_locally(self.root, change)
|
||||
self.assertIn("AMBIGUOUS_RECORD", str(ctx.exception))
|
||||
|
||||
def test_project_cleanup_markdown_header_is_report_only(self):
|
||||
rendered = self.w4.project_cleanup_markdown([{
|
||||
"path": ".worktrees/example",
|
||||
"reason": "cleanup=pending",
|
||||
"cleanup_owner": "maintainer",
|
||||
"trigger": "tip ancestor",
|
||||
}])
|
||||
self.assertIn("| 路径/事项 | 分类与保留原因 | cleanup_owner | 复查触发 |", rendered)
|
||||
self.assertNotIn("--kind cleanup", rendered)
|
||||
self.assertIn(".worktrees/example", rendered)
|
||||
self.assertIn("tip ancestor", rendered)
|
||||
|
||||
def test_build_projection_row_canonical(self):
|
||||
row = self.w4.build_projection_row(
|
||||
task_id="alpha", owner="Test Agent", branch="feature/alpha",
|
||||
worktree=".worktrees/alpha", mutex="m1", status="in_progress")
|
||||
self.assertEqual(
|
||||
row,
|
||||
"| Test Agent | alpha | feature/alpha | .worktrees/alpha | m1 | in_progress |")
|
||||
|
||||
|
||||
|
||||
class DarwinCwdVisibilityTests(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user