diff --git a/.task/feature-agent-stack-r0-r1.md b/.task/feature-agent-stack-r0-r1.md index 33bec37d..1e184e36 100644 --- a/.task/feature-agent-stack-r0-r1.md +++ b/.task/feature-agent-stack-r0-r1.md @@ -7,6 +7,7 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= f4aa0348d7f60e4dd5b6b222205a2c561465db8e - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-agent-stack-r0-r1(worktree);分支:feature/agent-stack-r0-r1 - 授权与交付终点:按提案第 5 节实施 R0(组合一致性)与 R1(三库 P0 与安全向 P1)。允许在 Agent-Intent、Agent-Native、Agent-Squad、Agent-System-Backend、Agent-System-Dev 五个仓各从 origin/main 切 feature/agent-stack-r0-r1,提交、推送并建 PR,推进 Dev 与 Backend 的 gitlink。终点 = 五个 PR 已开且均未合入。未授权:合入、发版、改写历史、变更分支保护、改 development-guidelines 嵌套指针。 +- 追加授权(2026-10-03,主检出 `make precheck` verdict READY,head=`f4aa0348d7`,linked worktrees=2):在同一批 `feature/agent-stack-r0-r1` 上补收尾加固与 CI。允许改 Squad 的并行审批与挂起、Dev 的指针检查、`make pin`、导入棘轮和 `task.sh`,五个仓的 CI workflow,以及任务卡欠账。R2 与 D1 等五个 PR 由所有者合入后再从各仓 main 开独立任务。未授权:合入、发版、改分支保护、配置 `SUBMODULE_TOKEN`。 ## 目标与非目标 @@ -36,11 +37,11 @@ - N14 `TestFetchHostCheckRedirectStillRejectsPrivate` - Q1 `TestStepApprovalApproveExecutes`、`TestStepApprovalSkipDoesNotCancel` - Q2 `TestApprovalSurvivesRestart` - - Q3 `AtomicWrite`(`writeJSON`、黑板、store、leader inbox;既有落盘测试覆盖) + - Q3 `TestAtomicWriteLeavesNoTemp`、`TestAtomicWriteRenameFailureKeepsOriginal` - Q4 `TestInterruptedRunningStepStaysHeld` - Q5 `TestTwoEnginesOneRunIsBusy` - Q8/Q9 `TestWakeupDoesNotApprovePermission`(原有 `TestWakeupRunDone`、`TestWakeupGuardMaxFires` 仍通过) - - Q14 `TestMissingTokenIsUnauthorized` + - Q14 `TestListenGuard`、`TestMissingTokenIsUnauthorized` - Q15 `TestPathTraversalRejected` - Q16 `TestOversizedBodyIs413` - 验证: @@ -50,3 +51,8 @@ - `make build` 退出码 0。版本串 `314f798+core=7c66075+backend=898fdd1+frontend=794c3e1+ui=b4a4fc1+agentcmd=cc1ad77+documents=acd6f01+native=694b08c+squad=8e987bf+intent=3130190`。 - 负例:Backend 的 squad gitlink 临时改为 `5cbac00b994223b6aa76afd9bc70277b8b49e985` 后 `scripts/check-pins.sh` 退出码 1(`pin: modules/backend deps/agent-squad is 5cbac00b9942, Dev deps/agent-squad is 8e987bf535ef`)。已恢复到 `898fdd1`,再次运行 `pins ok`。该临时提交未推送。 - 影响与剩余项:非 unix 平台没有 flock,只有进程内互斥(见 agent-squad PROJECT.md)。合入、发版、改写历史均未做。 +- 收尾加固(2026-10-03): + - Squad `73b8d09545a8c479344aded2227de5fc520ef769`。并行组第二步 `TestParallelGroupSecondStepApprovalHolds`、`TestParallelGroupSecondStepPermissionHolds`;自由文本 `TestFreeTextKeepsChoiceHold`;失败后挂起仍在 `TestFailedDecisionRestoresPending`。`GOWORK=off go vet ./...`、`go test -count=1 ./squad/ ./cli/`、`go test -race -count=1 ./squad/` 均通过。 + - 同 tree 正例:Backend 所记 squad `8e987bf535efbbbf317147542e858217f2bb3504` 的 tree `0fe06ce3ceea5cfa32476e2f0eed7f86cebc44a0`,`commit-tree` 得本地 `6c0f1e8bf1be85f10163eb0c6eebb5a6aa8f8798` 并临时写入 Dev 索引。`check-pins` 输出 `ok modules/backend agent-squad (same tree)`,退出码 0。 + - 负例:空 tree 的本地提交 `8db069d7352d` 写入同一索引后,`check-pins` 退出码 1,`pin: modules/backend deps/agent-squad is 8e987bf535ef, Dev deps/agent-squad is 8db069d7352d`。索引已 `git reset` 恢复,再次 `pins ok`。两个临时对象都没有推送。 + - 棘轮:`scripts/lint-imports.go` 在空 `GOMODCACHE` 下的配对与有缓存的 `go list` 相同,14 对。`GOMODCACHE=$(mktemp -d) bash scripts/lint-boundaries.sh` 与默认缓存的输出都是 `boundaries ok`。 diff --git a/Makefile b/Makefile index 4ea73660..9178bb7d 100644 --- a/Makefile +++ b/Makefile @@ -149,9 +149,8 @@ pin: @for m in modules/core modules/backend modules/frontend modules/ui modules/documents deps/agent-native deps/agent-squad deps/agent-intent deps/error-code-system deps/logging-system deps/development-guidelines deps/agent-command; do \ (git -C $$m fetch -q origin && git -C $$m checkout -q origin/main) || echo "skip $$m"; \ done - @cd $(BACKEND)/deps/agent-system-core && git fetch -q origin && git checkout -q origin/main || true - @cd $(BACKEND)/deps/agent-command && git fetch -q origin && git checkout -q origin/main || true @echo "submodule 指针已推进;请 make test 后 git add .gitmodules modules deps && git commit" + @scripts/check-pins.sh || echo "pin: check-pins 未通过,上面列出的容器需要重钉;pin 本身不失败" clean: rm -rf $(BIN) $(FRONTEND)/dist $(FRONTEND)/node_modules $(UI)/node_modules data diff --git a/scripts/check-pins.sh b/scripts/check-pins.sh index 715272b0..219a017e 100755 --- a/scripts/check-pins.sh +++ b/scripts/check-pins.sh @@ -20,6 +20,21 @@ check_link() { return fi if [ "$sha" != "$expect" ]; then + local sha_tree expect_tree + if ! sha_tree=$(git -C "$expect_path" rev-parse "${sha}^{tree}" 2>/dev/null); then + echo "pin: $container deps/$name ${sha:0:12} is missing locally; fetch $expect_path first" >&2 + fail=1 + return + fi + if ! expect_tree=$(git -C "$expect_path" rev-parse "${expect}^{tree}" 2>/dev/null); then + echo "pin: Dev $expect_path ${expect:0:12} is missing locally; fetch $expect_path first" >&2 + fail=1 + return + fi + if [ "$sha_tree" = "$expect_tree" ]; then + echo "ok $container $name (same tree)" + return + fi echo "pin: $container deps/$name is ${sha:0:12}, Dev $expect_path is ${expect:0:12}" >&2 fail=1 return diff --git a/scripts/lint-boundaries.sh b/scripts/lint-boundaries.sh index 00842346..ba35e59c 100755 --- a/scripts/lint-boundaries.sh +++ b/scripts/lint-boundaries.sh @@ -34,8 +34,8 @@ siblings=(agent-intent agent-native agent-squad) for dir in deps/agent-intent deps/agent-native deps/agent-squad; do mod=$(awk '/^module /{print $2; exit}' "$root/$dir/go.mod") ( - cd "$root/$dir" - GOWORK=off go list -f '{{.ImportPath}}{{range .Imports}} {{.}}{{end}}' ./... + cd "$root/scripts" + go run lint-imports.go "$root/$dir" ) | while read -r importer rest; do for imp in $rest; do for sib in "${siblings[@]}"; do diff --git a/scripts/lint-imports.go b/scripts/lint-imports.go new file mode 100644 index 00000000..488340eb --- /dev/null +++ b/scripts/lint-imports.go @@ -0,0 +1,99 @@ +// 离线列出模块内非测试 Go 文件的导入。 +// 只使用标准库,跳过 _test.go 和带 go.mod 的子目录,避免 go list 去下载模块图。 +package main + +import ( + "bufio" + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" +) + +func main() { + for _, root := range os.Args[1:] { + mod := modulePath(filepath.Join(root, "go.mod")) + if mod == "" { + fmt.Fprintf(os.Stderr, "lint-imports: %s has no module path\n", root) + os.Exit(1) + } + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + name := d.Name() + if name == "vendor" || (name != "." && strings.HasPrefix(name, ".")) { + return filepath.SkipDir + } + if path != root { + if _, statErr := os.Stat(filepath.Join(path, "go.mod")); statErr == nil { + return filepath.SkipDir + } + } + return printImports(root, mod, path) + }) + if err != nil { + fmt.Fprintf(os.Stderr, "lint-imports: %v\n", err) + os.Exit(1) + } + } +} + +func modulePath(goMod string) string { + f, err := os.Open(goMod) + if err != nil { + return "" + } + defer f.Close() + sc := bufio.NewScanner(f) + for sc.Scan() { + fields := strings.Fields(sc.Text()) + if len(fields) >= 2 && fields[0] == "module" { + return fields[1] + } + } + return "" +} + +func printImports(root, mod, dir string) error { + rel, err := filepath.Rel(root, dir) + if err != nil { + return err + } + importer := mod + if rel != "." { + importer = mod + "/" + filepath.ToSlash(rel) + } + entries, err := os.ReadDir(dir) + if err != nil { + return err + } + fset := token.NewFileSet() + seen := map[string]struct{}{} + var imports []string + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + file, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.ImportsOnly) + if err != nil { + continue + } + for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if _, ok := seen[path]; ok { + continue + } + seen[path] = struct{}{} + imports = append(imports, path) + } + } + if len(imports) == 0 { + return nil + } + fmt.Printf("%s %s\n", importer, strings.Join(imports, " ")) + return nil +} diff --git a/scripts/task.sh b/scripts/task.sh index 84c1617a..f9814795 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -42,6 +42,23 @@ cmd_precheck() { fi done [ -n "$open_cards" ] && echo "[cards] 已合入未闭合任务卡:$(echo $open_cards)——make task-close 回填归档(主检出运行)" + local done_open="" stale="" now ts ref + for pc in .task/*.md; do + [ -f "$pc" ] || continue + case "$(card_status "$pc")" in + 已完成*) done_open="${done_open} ${pc##*/}" ;; + esac + done + [ -n "$done_open" ] && echo "[cards] 已完成但未归档:$(echo $done_open)" + now=$(date +%s) + while read -r ref ts; do + [ -n "$ref" ] || continue + git merge-base --is-ancestor "$ref" "origin/${TARGET_BRANCH}" 2>/dev/null && continue + if [ $((now - ts)) -gt 86400 ]; then + stale="${stale} ${ref#origin/}" + fi + done < <(git for-each-ref --format='%(refname:short) %(committerdate:unix)' "refs/remotes/origin/feature/card-close-*") + [ -n "$stale" ] && echo "[cards] 闭合分支超过 24 小时未合入:$(echo $stale)" echo "[required-reading] core/baseline.md, workflows/delivery.md §1$([ "$dirty" -gt 0 ] && echo ", standards/collaboration.md(树脏=他人或未收尾现场,不得写入)")" echo "[R0] $(echo "$rmain" | cut -c1-10)(完整:$rmain)" if [ "$dirty" -gt 0 ] || [ "${sub_drift:-0}" -gt 0 ]; then @@ -119,13 +136,27 @@ cmd_sync() { [ "$(git status --porcelain | grep -v '^.. \.zcode/' | wc -l)" -eq 0 ] || die "dirty tree — 先显式提交;不隐式 stash(collaboration.md:11)" git merge --no-edit "origin/${TARGET_BRANCH}" git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "sync incomplete" - local base head card + local base head card branch slug pc base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD) - card=$(ls .task/*.md 2>/dev/null | head -1 || true) - if [ -n "$card" ]; then + branch=$(git rev-parse --abbrev-ref HEAD) + slug=$(echo "$branch" | tr '/' '-') + card=".task/${slug}.md" + if [ ! -f "$card" ]; then + card="" + for pc in .task/*.md; do + [ -f "$pc" ] || continue + if [ "$(card_of_branch "$pc")" = "$branch" ]; then + card=$pc + break + fi + done + fi + if [ -n "$card" ] && [ -f "$card" ]; then printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card" git add "$card" git commit -q -m "Record the sync checkpoint" || true + else + echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2 fi echo "[synced] sync_base=$(echo "$base" | cut -c1-10) tested_head=$(echo "$head" | cut -c1-10) checked_at=$(date -u +%FT%TZ)" echo "[next] 跑正式验收(make test 等),证据写入任务卡" @@ -189,13 +220,18 @@ cmd_close() { for c in .task/*.md; do [ -f "$c" ] || continue b=$(card_of_branch "$c"); [ -n "$b" ] || continue - st=$(card_status "$c"); case "$st" in 已完成*) continue ;; esac + st=$(card_status "$c") + done_unarchived=0 + case "$st" in 已完成*) done_unarchived=1 ;; esac if card_pending "${c##*/}"; then echo "[close] ${c##*/} 已有待合入的闭合分支,跳过(等其 PR 合入)" continue fi m=$(card_merge "$b") || m="" - [ -n "$m" ] || continue + if [ -z "$m" ]; then + [ "$done_unarchived" = 1 ] || continue + m="ARCHIVE" + fi cards="$cards $c|$b|$m" done @@ -213,6 +249,13 @@ $c|$b|$m" local n=0 sha subject pr closed_list="" while IFS='|' read -r c b m; do [ -n "$c" ] || continue + if [ "$m" = "ARCHIVE" ]; then + mkdir -p .task/archive + git mv "$c" ".task/archive/${c##*/}" + closed_list="$closed_list ${c##*/}(already-complete)" + n=$((n+1)) + continue + fi sha="${m%%|*}"; subject="${m#*|}" pr=$(printf '%s' "$subject" | sed -n 's/.*(#\([0-9]\+\)) from.*/\1/p') [ -n "$pr" ] || pr=unknown