diff --git a/.task/feature-evidence-driven-gates.md b/.task/feature-evidence-driven-gates.md index 11f33c1c..a4c1e583 100644 --- a/.task/feature-evidence-driven-gates.md +++ b/.task/feature-evidence-driven-gates.md @@ -6,11 +6,23 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 602d2ce9c6900343155f2a9c7ccb4f1c83aa4a59 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-evidence-driven-gates(worktree);分支:feature/evidence-driven-gates -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:用户 2026-10-05 授权「按证据驱动方案完善规范」——guidelines 子模块(PR)+ + Dev 仓(AGENTS.md/policy/工具/Makefile/task.sh + gitlink 推进)经 `make test` 组合验证后 + PR 合入 main 并回收工作区。未授权:改前端/后端业务代码、解除分支保护、改写历史。 ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +目标:落地「声明≠能力」证据门禁—— +1) guidelines draft.10:证据来源三分 SELF/REPLAY/GATE、工具写入、审阅者重放、外部事实 + 零信任、诚实出口(verification/collaboration/delivery/baseline/glossary/两模板/CHANGELOG)。 +2) Dev 仓:AGENTS.md「证据与验证门禁」节;policy/facts.md 事实登记表(F1–F4 实测后登记); + scripts/evidence.sh(make evidence:命令×HEAD×sha256×尾部写入任务卡); + scripts/facts-verify.sh(make facts-verify [--update]);task.sh:precheck 增 [hooks]/[facts], + sync 增 [evidence] 旧 HEAD 告警,task-start 卡片模板带证据表;Makefile 增两目标。 +3) gitlink 推进至 guidelines 合入 SHA(af199ee),组合验证 make test。 + +非目标:task-close 核对 CI 状态(PR3 权威迁移)、CI 工作流改动、UI/业务代码、 +review 技能文本改造(后续任务)。 ## 同步记录 diff --git a/AGENTS.md b/AGENTS.md index 72972bfd..2c5a7086 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,29 @@ `feature/card-close-*` 分支+PR,任务分支内加 `FOLD=--fold` 随当前 PR 合入。 precheck 的 `[cards]` 行报告欠账,验收前应清零;闭合 PR 由所有者合入。 +## 证据与验证门禁(强制) + +原则:**「声明已就位」≠「能力已就位」**。任何来源(用户输入、方案、报告、他人或自己的 +叙述)的结果声明都不是证据;证据 = 命令 × 被测对象 SHA × 可复核输出,三者齐备才算事实。 +规范依据:guidelines `standards/verification.md`「声明与证据来源」(draft.10)。 + +- **来源三分**:产出物(任务卡、PR、报告、review 结论)中的通过类断言须标注来源—— + `GATE`(CI/分支保护/precheck 实测)/ `REPLAY`(审阅者独立重跑)/ `SELF`(作者粘贴)。 + 合入与验收判定只认 GATE/REPLAY;SELF 仅为线索,与重验不一致时以重验为准。 +- **证据由工具写入**:命令输出进入任务卡/PR 一律经 `make evidence CMD="…"` 写入 + (自动记录命令、被测 HEAD、退出码、输出 sha256 与尾部摘录并脱敏 token)。手抄、转写、 + 凭记忆重构的输出视为无效证据;补写输出与伪造测试同级红线。 +- **完成声明三段输出**(precheck / task-sync / 测试)必须由 evidence 工具写入任务卡, + PR 描述引用任务卡段落,不另手抄。测试通过的权威源是 CI(`.gitea/workflows/ci.yaml`, + 在 PR HEAD 上 `make test`+`make build`),本地输出降级为辅助线索。 +- **验收即重放**:请求审核后,审阅者先在评审环境重放最小重放集(关键门禁命令 + 变更 + 直接相关检查)再读作者证据;重放与声明不一致直接阻塞,并作为失实事件登记。 +- **外部事实零信任**:用户/文档/旧报告里的「可用/已就位」必须先执行 `policy/facts.md` + 登记表中对应验证命令(或当场实测)才能写进产出物;无法当场复核的结论标 `[未经证实]`, + 禁止裸写。登记表每条事实带 `last_verified` 与 TTL,precheck `[facts]` 报告过期项, + `make facts-verify` 逐条重验。 +- **诚实出口**:未执行/未验证是合法状态,如实标注(NOT_RUN/未验证)优于补写。 + ## 提交与 PR - 主题行:英文祈使句一行(≤100 列,不以句点结尾);body 写 `Why: / Evidence: / Impact: / Refs:` @@ -44,10 +67,14 @@ - **高后果操作双钥匙**:改写历史、解除/变更分支保护、删除归档引用、发布版本——Agent 只准备 方案与命令,由用户确认并亲手执行;Agent 不得自行实施(baseline 硬边界 1 的授权范围要求)。 - 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。 -- 验证命令见 PROJECT.md「验证」节;完成声明须附实际命令输出证据。 +- 验证命令见 PROJECT.md「验证」节;完成声明的三段输出按「证据与验证门禁」节由 + `make evidence` 写入任务卡,不手抄。 - 首次克隆本仓后运行 `make setup` 启用本地 pre-commit 钩子。 -## Gitea 与 Git 访问事实(2026-09-30 实测) +## Gitea 与 Git 访问事实(2026-09-30 实测;登记表 `policy/facts.md`) + +机器可核的登记表在 `policy/facts.md`(每条带验证命令、last_verified 与 TTL); +引用任一事实前先跑其验证命令。以下为叙述版: 历史任务卡与旧脚本里的 `git-panel.u1680832.nyat.app:30319` 已失效(TLS 握手失败), 见到一律按过期处理,不要再试。当前权威通道: diff --git a/Makefile b/Makefile index 33f3a064..844ab48f 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PLUGINS := plugins COMBO := core=$(shell git -C $(CORE) rev-parse --short HEAD 2>/dev/null)+backend=$(shell git -C $(BACKEND) rev-parse --short HEAD 2>/dev/null)+frontend=$(shell git -C $(FRONTEND) rev-parse --short HEAD 2>/dev/null)+ui=$(shell git -C $(UI) rev-parse --short HEAD 2>/dev/null)+agentcmd=$(shell git -C deps/agent-command rev-parse --short HEAD 2>/dev/null)+documents=$(shell git -C modules/documents rev-parse --short HEAD 2>/dev/null)+native=$(shell git -C deps/agent-native rev-parse --short HEAD 2>/dev/null)+squad=$(shell git -C deps/agent-squad rev-parse --short HEAD 2>/dev/null)+intent=$(shell git -C deps/agent-intent rev-parse --short HEAD 2>/dev/null) VERSION := $(shell git describe --tags --always --dirty 2>/dev/null)+$(COMBO) -.PHONY: help sync status build frontend svc svc-list svc-info svc-commits svc-history svc-update test-backend test-frontend test-ui test-core test-agentcmd test-pins test-libs dev pin clean setup precheck task-start task-sync task-close task-finish version +.PHONY: help sync status build frontend svc svc-list svc-info svc-commits svc-history svc-update test-backend test-frontend test-ui test-core test-agentcmd test-pins test-libs dev pin clean setup precheck task-start task-sync task-close task-finish facts-verify evidence version version: @echo "agent-system $(VERSION)" @@ -22,7 +22,7 @@ TASK := scripts/task.sh setup: git config core.hooksPath scripts/hooks - chmod +x scripts/hooks/pre-commit scripts/hooks/post-merge scripts/task.sh + chmod +x scripts/hooks/pre-commit scripts/hooks/post-merge scripts/task.sh scripts/evidence.sh scripts/facts-verify.sh @echo "OK: pre-commit/post-merge 钩子已启用(core.hooksPath=scripts/hooks)" precheck: @@ -41,6 +41,16 @@ task-close: task-finish: @bash $(TASK) finish $(ABANDON) +# 证据写入:执行命令并把结果行追加进当前任务卡(AGENTS.md「证据与验证门禁」)。 +# 用法:make evidence CMD="<单行命令>" [SRC=SELF|REPLAY|GATE] [TAIL=15];退出码随被执行命令。 +evidence: + @test -n "$(CMD)" || (echo 'usage: make evidence CMD="" [SRC=SELF|REPLAY|GATE] [TAIL=15]'; exit 2) + @CMD='$(CMD)' SRC='$(SRC)' TAIL='$(TAIL)' bash scripts/evidence.sh + +# 外部事实登记表逐条重验(--update 刷新 PASS 项 last_verified)。 +facts-verify: + @bash scripts/facts-verify.sh $(ARGS) + help: @echo "make sync 物化全部 submodule(递归)" @echo "make build 构建单二进制 bin/agent-system(契约同步→前端构建→embed→静态链接)" diff --git a/deps/development-guidelines b/deps/development-guidelines index ed2af3e6..af199eea 160000 --- a/deps/development-guidelines +++ b/deps/development-guidelines @@ -1 +1 @@ -Subproject commit ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 +Subproject commit af199eeaa23c5b39e08b4649f2513d7572dc5274 diff --git a/policy/facts.md b/policy/facts.md new file mode 100644 index 00000000..e6be6d33 --- /dev/null +++ b/policy/facts.md @@ -0,0 +1,16 @@ +# 项目外部事实登记表 + +规则见 AGENTS.md「证据与验证门禁」与 deps/development-guidelines/standards/verification.md「声明与证据来源」。 + +- 每条事实 = 陈述 + 验证命令 + last_verified + TTL;**引用前必须先执行其验证命令**(外部事实零信任)。 +- 过期(>TTL)必须重验后更新:`make facts-verify` 逐条重验,`make facts-verify ARGS=--update` + 把 PASS 项的 last_verified 刷新为当天并写回本表(随 PR 合入)。 +- 表内验证命令不得含竖线 `|`(保持表格与解析可核);新增事实先实测再登记,不得凭记忆填日期。 +- precheck 的 `[facts]` 行报告过期项;AGENTS.md「Gitea 与 Git 访问事实」节为本表的叙述版。 + +| ID | 事实 | 验证命令 | last_verified | TTL(天) | +|---|---|---|---|---| +| F1 | Gitea Web/API 可达:https://git-home.u1680832.nyat.app:30319(1.27.3) | `curl -fsS -m 10 https://git-home.u1680832.nyat.app:30319/api/v1/version` | 2026-10-05 | 30 | +| F2 | API token 存在且可用(值在 ~/.secrets/gitea,不入仓不回显) | `t=$(sed -n "s/^ACCESS_TOKEN=//p" ~/.secrets/gitea); test -n "$t" && curl -fsS -m 10 -o /dev/null -H "Authorization: token $t" https://git-home.u1680832.nyat.app:30319/api/v1/user` | 2026-10-05 | 30 | +| F3 | Git over SSH 可达:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/<Repo>.git(key ~/.ssh/home_gitea) | `GIT_SSH_COMMAND="ssh -i ~/.ssh/home_gitea -o BatchMode=yes" git ls-remote ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Development-Guidelines.git HEAD` | 2026-10-05 | 30 | +| F4 | 旧端点 git-panel.u1680832.nyat.app:30319 已失效(TLS 握手失败),不得使用 | `! curl -fsS -m 8 https://git-panel.u1680832.nyat.app:30319/api/v1/version` | 2026-10-05 | 180 | diff --git a/scripts/evidence.sh b/scripts/evidence.sh new file mode 100755 index 00000000..7b3e7163 --- /dev/null +++ b/scripts/evidence.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# 证据写入工具:执行命令,把(命令、被测 HEAD、退出码、输出 sha256、尾部摘录、时间、来源) +# 追加到当前分支任务卡的「## 证据(Evidence)」表。规范依据:AGENTS.md「证据与验证门禁」、 +# guidelines standards/verification.md「声明与证据来源」——转写/手抄的输出视为无效证据。 +# 用法:make evidence CMD="<单行命令>" [SRC=SELF|REPLAY|GATE] [TAIL=15] +# 退出码 = 被执行命令的退出码(失败可见),证据行无论成败都会写入。 +set -uo pipefail + +CMD="${CMD:-}"; SRC="${SRC:-SELF}"; TAILN="${TAIL:-15}" +[ -n "$CMD" ] || { echo 'usage: make evidence CMD="" [SRC=SELF|REPLAY|GATE] [TAIL=15]' >&2; exit 2; } +case "$SRC" in SELF|REPLAY|GATE) ;; *) echo "ERROR: SRC 须为 SELF|REPLAY|GATE(当前:$SRC)" >&2; exit 2 ;; esac +case "$TAILN" in ''|*[!0-9]*) TAILN=15 ;; esac +case "$CMD" in *$'\n'*) echo 'ERROR: CMD 须为单行命令' >&2; exit 2 ;; esac + +branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) +[ -n "$branch" ] || { echo "ERROR: 不在 git 仓库内" >&2; exit 1; } +slug=$(echo "$branch" | tr '/' '-') +card=".task/${slug}.md" +if [ ! -f "$card" ]; then + # 回退:按卡片「分支:」字段匹配(与 task.sh cmd_sync 同款口径) + for pc in .task/*.md; do + [ -f "$pc" ] || continue + if [ "$(sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$pc" | head -1)" = "$branch" ]; then + card=$pc; break + fi + done +fi +[ -f "$card" ] || { echo "ERROR: 找不到分支 ${branch} 的任务卡(.task/${slug}.md)" >&2; exit 1; } + +head=$(git rev-parse HEAD) +short_head=$(echo "$head" | cut -c1-10) +ts=$(date -u +%FT%TZ) + +tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT +bash -c "$CMD" >"$tmp" 2>&1; rc=$? +digest=$(sha256sum "$tmp" | cut -c1-16) + +# 脱敏(baseline 硬边界 2):token 风格的 40 位 hex / Authorization 头不打进任务卡。 +sanitize() { + sed -E -e 's/(token[ =:]+)[0-9a-fA-F]{40}/\1/g' \ + -e 's/(Authorization: *token +)[^ ]+/\1/Ig' \ + -e 's/(access_token[=:]+)[0-9a-fA-F]{40}/\1/Ig' +} + +if ! grep -q '^## 证据(Evidence)' "$card"; then + printf '\n## 证据(Evidence)\n\n\n| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |\n|---|---|---|---|---|---|\n' >> "$card" +fi +cmd_cell=$(echo "$CMD" | sed -e 's/|/\\|/g' -e 's/`/‘/g') +printf '| %s | `%s` | %s | %d | %s | %s |\n' "$ts" "$cmd_cell" "$short_head" "$rc" "$SRC" "$digest" >> "$card" +printf '\n\n```\n$ %s\n[exit %d | HEAD %s | sha256:%s] 输出尾部 %s 行:\n%s\n```\n' \ + "$ts" "$CMD" "$rc" "$short_head" "$digest" "$TAILN" "$(tail -n "$TAILN" "$tmp" | sanitize)" >> "$card" + +echo "[evidence] card=$card cmd=\"${CMD}\" exit=$rc src=$SRC head=$short_head sha256=$digest" +exit "$rc" diff --git a/scripts/facts-verify.sh b/scripts/facts-verify.sh new file mode 100755 index 00000000..490ed92d --- /dev/null +++ b/scripts/facts-verify.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# 外部事实登记表逐条重验:对 policy/facts.md 每条事实执行其验证命令并报告 PASS/FAIL。 +# 规则依据:AGENTS.md「证据与验证门禁」(外部事实零信任);verification.md「声明与证据来源」。 +# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回) +# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。 +set -uo pipefail +F="${FACTS:-policy/facts.md}" +[ -f "$F" ] || { echo "ERROR: 事实登记表 $F 不存在" >&2; exit 1; } +python3 - "$F" "${1:-}" <<'PY' +import re, subprocess, sys, datetime +path, update = sys.argv[1], sys.argv[2] == "--update" +lines = open(path, encoding="utf-8").read().splitlines(keepends=True) +today = datetime.date.today().isoformat() +total = fails = 0 +for i, line in enumerate(lines): + if not re.match(r"^\|\s*F\d+\s*\|", line): + continue + raw = line.strip().strip("|").replace("\\|", "\x00") + cells = [c.strip().replace("\x00", "|") for c in raw.split("|")] + if len(cells) < 4: + print(f"[row {i+1}] SKIP 格式不符(列数<4)", file=sys.stderr) + continue + fid, cmd = cells[0], cells[2].strip("`") + total += 1 + try: + r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=90) + ok = r.returncode == 0 + except subprocess.TimeoutExpired: + ok = False + print(f"[{fid}] TIMEOUT >90s", file=sys.stderr) + print(f"[{fid}] {'PASS' if ok else 'FAIL'} :: {cmd[:80]}") + if not ok: + fails += 1 + elif update and len(cells) >= 4 and re.match(r"\d{4}-\d{2}-\d{2}", cells[3]): + cells[3] = today + lines[i] = "| " + " | ".join(c.replace("|", "\\|") for c in cells) + " |\n" +if update: + open(path, "w", encoding="utf-8").writelines(lines) +print(f"[facts-verify] total={total} pass={total - fails} fail={fails}" + + (";last_verified 已刷新为 " + today if update and total else "")) +sys.exit(1 if fails else 0) +PY diff --git a/scripts/task.sh b/scripts/task.sh index f9814795..12217377 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -59,6 +59,34 @@ cmd_precheck() { fi done < <(git for-each-ref --format='%(refname:short) %(committerdate:unix)' "refs/remotes/origin/feature/card-close-*") [ -n "$stale" ] && echo "[cards] 闭合分支超过 24 小时未合入:$(echo $stale)" + # [hooks] 声明≠能力:core.hooksPath 未指向 scripts/hooks 时,"钩子已启用"只是声明。 + local hp; hp=$(git config core.hooksPath 2>/dev/null || true) + if [ "$hp" = "scripts/hooks" ] && [ -x scripts/hooks/pre-commit ]; then + echo "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)" + else + echo "[hooks] WARN core.hooksPath='${hp:-未设置}'——本地钩子未生效,先 make setup;本检出提交将绕过本地闸(远端准入仍在)" + fi + # [facts] 外部事实登记表过期项(policy/facts.md;引用前先跑其验证命令,AGENTS.md 证据与验证门禁)。 + if [ -f policy/facts.md ]; then + local fid lv ttl expired="" ftotal=0 + while IFS='|' read -r _ fid _ _ lv ttl _; do + fid=$(echo "${fid:-}" | tr -d ' '); case "$fid" in F[0-9]*) ;; *) continue ;; esac + ftotal=$((ftotal+1)) + lv=$(echo "$lv" | tr -d ' '); ttl=$(echo "$ttl" | tr -d ' ') + case "$lv" in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; *) expired="$expired ${fid}(日期格式)"; continue ;; esac + case "$ttl" in ''|*[!0-9]*) ttl=30 ;; esac + if [ $(( $(date +%s) - $(date -d "$lv" +%s) )) -gt $(( ttl*86400 )) ]; then + expired="$expired ${fid}(last=${lv},ttl=${ttl}d)" + fi + done < <(grep -E '^\| *F[0-9]+ ' policy/facts.md || true) + if [ -n "$expired" ]; then + echo "[facts] WARN 过期事实:$(echo $expired)——引用前先 make facts-verify 重验并更新登记表" + else + echo "[facts] policy/facts.md ${ftotal} 条事实均在有效期内" + fi + else + echo "[facts] WARN policy/facts.md 不存在——外部事实登记表缺失(AGENTS.md 证据与验证门禁)" + fi 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 @@ -120,10 +148,16 @@ cmd_start() { |---|---|---|---| | 启动 | ${rmain} | $(git -C "$wt" rev-parse HEAD) | $(date -u +%FT%TZ) | +## 证据(Evidence) + + +| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | +|---|---|---|---|---|---| + ## 结果与验证(完成后填写) - 结果: -- 验证: +- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> - 影响与剩余项: EOF git -C "$wt" add .task && git -C "$wt" commit -q -m "Record the task card for ${slug}" && echo "[card] $card(已随分支提交)" @@ -155,6 +189,14 @@ cmd_sync() { printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card" git add "$card" git commit -q -m "Record the sync checkpoint" || true + # [evidence] 同步改变了被测对象:卡片证据表中旧 HEAD 的行自动失效,关键检查须重跑。 + local eh stale_ev="" short_head + short_head=$(echo "$head" | cut -c1-10) + while IFS='|' read -r _ _ _ eh _ _ _; do + eh=$(echo "$eh" | tr -d ' `') + [ -n "$eh" ] && [ "$eh" != "$short_head" ] && stale_ev="$stale_ev $eh" + done < <(grep -E '^\| [0-9]{4}-[0-9]{2}-[0-9]{2}T[^|]*\|' "$card" 2>/dev/null || true) + [ -n "$stale_ev" ] && echo "[evidence] WARN 证据表含旧被测 HEAD:$(echo $stale_ev | tr ' ' '\n' | sort -u | tr '\n' ' ')(≠ 当前 $short_head)——同步后关键检查须重跑并以 make evidence 重新写入" else echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2 fi