Compare commits

..
Author SHA1 Message Date
weichen 0ca2a86b77 Bump the development-manager pin for the endpoint migration
governance / governance (pull_request) Successful in 1m3s
Why: governance checks validate candidate .gitmodules URLs against the
pinned development-manager scaffold, which now carries the new Gitea
endpoint base after its own migration.

Evidence: host-verified; see Dev PR #706

Impact: pin update only, part of the coordinated endpoint migration.

Refs: Agent-System-Dev PR #706
2026-10-22 14:25:40 +00:00
weichen b2ef004313 Update Gitea endpoints after server migration
governance / governance (pull_request) Failing after 36s
Why: the Gitea server moved to https://gitea.naitangbaobao.cn with SSH
on port 52222; submodule URLs and workflow references must follow or
superproject CI checkout refuses the old URLs.

Evidence: host-verified F1-equivalent API reach and SSH auth before
this change (see Dev task card fix/gitea-endpoint-migration)

Impact: configuration only; no runtime behavior change.

Refs: Dev PR #706
2026-10-22 14:16:13 +00:00
weichen 6fe40b0342 Merge pull request 'Add bounded evidence to work checkpoint candidates' (#31) from feature/fast-slow-u2-u3 into main
governance / governance (push) Successful in 16s
2026-10-19 09:20:24 +00:00
weichen 0cf03446c4 Merge the fast-slow U2/U3 branch history after syncing with main
governance / governance (pull_request) Successful in 16s
2026-10-19 02:59:18 +00:00
agent-exec-transport cbd3d3e3d0 agent-exec transport 68889ca8d4b0eec4cfe89d0cc80b6516/5ca3b7798211 2026-10-19 02:59:03 +00:00
weichen 5cae3a6e30 Merge pull request 'Adopt the shared governance kit (development-governance-v2)' (#35) from feature/governance-adoption into main
governance / governance (push) Successful in 17s
2026-10-17 08:31:36 +00:00
weichen b240077300 Close and archive the governance adoption card
governance / governance (pull_request) Successful in 16s
Why: the adoption PR is reviewed and its candidate HEAD passed CI; the card closure is folded into the PR so no separate close PR is needed.
Evidence: CI GATE on 18f617d4e8cd63d172a5ab9519271831beb546db; independent Review REPLAY (cursor-agent-stable --mode ask 全新只读会话(fresh_session=true,session id not-observable;codex/pi 因 Token Plan 429 不可用)VERDICT: APPROVE,无阻塞项).
Impact: task card only (moved to .task/archive/).
Refs: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/35
2026-10-17 08:25:12 +00:00
weichen 18f617d4e8 Record the adoption unit run and positive/negative evidence
governance / governance (pull_request) Successful in 23s
Why: Phase 6 requires one positive executor run and the negative cases (missing config, bad receipt, out-of-scope path, unknown side effect) before adoption becomes active.
Evidence: .task/evidence/governance-adoption/ (SELF): governance check, agent-exec validate and adoption cases; this commit passed the generated pre-commit hook (admit).
Impact: task card and evidence logs only.
Refs: .task/feature-governance-adoption.md
2026-10-17 07:15:37 +00:00
agent-exec-transport 1ada9955d5 agent-exec transport e64b5afee9fabb0925523057eb540b68/8399a44af60d 2026-10-17 06:55:22 +00:00
weichenandCursor a160b03c7b Record the adoption unit run and positive/negative evidence
Why: Phase 6 requires one positive executor run and the negative cases (missing config, bad receipt, out-of-scope path, unknown side effect) before adoption becomes active.
Evidence: .task/evidence/governance-adoption/ (SELF): governance check all ok; agent-exec validate ok; adoption cases ALL PASS. The generated pre-commit hook refused this commit because scripts/dm resolves the DM checkout through the hook's GIT_DIR; fixed in Development-Manager before this repository re-pins.
Impact: task card and evidence logs only.
Refs: .task/feature-governance-adoption.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 06:19:48 +00:00
agent-exec-transport 98cfc6408d agent-exec transport 9a7ec13541404ada96c0f8bfc8d77058/a09c2ed0bb4b 2026-10-17 05:55:46 +00:00
weichen dde4f359a8 Bootstrap AGENT-EXECUTION.md and the governance adoption card
Why: development-governance-v2 Phase 6 adopts the shared governance kit in every formal repository; the executor needs a valid AGENT-EXECUTION.md before any implementation unit can run.
Evidence: generated by dm-server governance init at Development-Manager 42e675fec8e0069646319fcd298e61d03d8cbfd5; dm-server agent-exec validate passes.
Impact: configuration and task card only; no code or CI change yet.
Refs: Agent-System-Dev docs/superpowers/plans/2026-10-16-development-governance-v2-master.md
2026-10-17 05:51:28 +00:00
weichen 35560aa5a3 Merge pull request 'Keep complete background results and optional decisions on work cards' (#34) from feature/workbench-flow-completion into main 2026-10-15 08:18:43 +00:00
weichen 1a5ef6b75c Anchor work decisions to a reserved follow-up run identity
Why: Reconcile interrupted dispatch without spending another model request.

Evidence: Core package tests pass locally; integration exercises acceptance and recovery.

Impact: Add one optional persisted run identifier.

Refs: feature/workbench-flow-completion
2026-10-15 06:53:28 +00:00
weichen fcfe8149bd Verify durable work results and legacy card compatibility
Why: Retain full results and exact answers across persistence and reload.

Evidence: go test ./... passed locally, including full-tail and legacy JSON coverage.

Impact: Add contract roundtrip tests and a guarded dispatching state.

Refs: feature/workbench-flow-completion
2026-10-15 06:49:33 +00:00
weichen 00f182611c Define durable nonblocking work card snapshots
Why: Keep background results and optional decisions outside the foreground conversation lock.

Evidence: Core package tests and combination checks are recorded in the Dev task.

Impact: Add an optional backward-compatible work card payload without a database migration.

Refs: feature/workbench-flow-completion
2026-10-15 06:46:23 +00:00
weichen b4743949e5 Keep U3 TypeScript evidence helpers local
Why: Generated mirrors gained unused named exports and tripped the frontend knip ratchet.

Evidence: Exact Dev combination failed at 53 unused exports over 52 baseline; Core Go tests passed after narrowing the TS export surface.

Impact: Preserve WorkFindingCandidate.evidence shape while avoiding unused exported helper names.

Refs: Dev task feature/fast-slow-u2-u3-plan; failed combo receipt 275bebe2
2026-10-13 16:48:01 +00:00
weichen c722c16626 Validate optional evidence in work checkpoint candidates
Why: U3 needs bounded, source-indexed premises before the host can judge numeric and membership claims.

Evidence: TestWorkCheckpointEvidenceModesAndBounds failed on missing types, then go test ./... -count=1 and git diff --check passed.

Impact: Extend only the candidate wire contract; persisted task snapshots and display kinds stay unchanged.

Refs: Dev task feature/fast-slow-u2-u3-plan; r0.3 evidence implementation plan Task 1
2026-10-13 16:03:33 +00:00
weichen c55ebbcb96 Merge remote-tracking branch 'origin/main' into feature/fast-slow-u2-u3 2026-10-13 10:19:06 +00:00
weichen 5773e74086 Merge pull request 'Add managed scope to authoritative SettingDesc TS mirror' (#32) from fix/settingdesc-managed-scope into main 2026-10-13 06:54:12 +00:00
weichen d6f62af0ed Add managed scope to authoritative SettingDesc TS mirror
Why: Dev 组合构建经 make contracts 从 Core ts/types.ts 重新生成 frontend/ui 的 contracts.ts——受管 scope 字段此前只改了 frontend 镜像,构建时被权威源覆盖丢失(TS2339)。
Evidence: go test ./... 全绿;生成链路验证随 Dev 组合 CI。
Impact: 权威 TS 类型加可选字段;Go 契约(SettingDesc.Scope)此前已随 Core #30 合入,两侧对齐。
2026-10-13 06:53:42 +00:00
weichen beafd7537a Merge scoped settings contracts from current main
Why: Preserve upstream settings authority and current component admission.

Evidence: Exact authoritative main object fetched before merge; composition replay follows.

Impact: Retain Task contracts alongside scoped settings.

Refs: Agent-System-Dev U2/U3 delivery
2026-10-13 05:28:02 +00:00
weichen 0f94e4b062 Merge pull request 'Add system-managed setting scope contract to SettingDesc' (#30) from feature/rdm-increment-settings-duties into main 2026-10-13 04:19:40 +00:00
weichen aced665ba6 Merge origin/main into feature/rdm-increment-settings-duties 2026-10-13 04:19:24 +00:00
weichen 9150e6f90b Add system-managed setting scope contract to SettingDesc
Why: P2 受管设置机制需要 schema 声明写入归属——system 受管项仅管理员可写,user/project 层拒绝写入且生效链忽略存量覆盖。
Evidence: go test ./... 全绿(ManagedPaths/Touches/Strip 含祖先标量覆盖与整棵子树剥离用例)。
Impact: 新增 SettingScope 常量、SettingDesc.Scope 字段与三个纯函数;空 Scope 保持既有行为,向后兼容。
Refs: modules/documents/docs/proposals/2026-10-13-rdm-settings-members-duties-perms-increment.md P2
2026-10-13 03:40:25 +00:00
weichen 6e385743e4 Retain coverage limits in legacy task summaries
Why: Older clients must see the same coverage limits as structured task cards.

Evidence: Core suite and composition checks follow this projection update.

Impact: Projects optional limitations into existing list nodes.

Refs: fast-slow-u2-u3 Task 6
2026-10-13 02:08:00 +00:00
weichen dbe1f544d7 Keep material coverage limits separate from outcome claims
Why: A completed run can still lack readable or fully provided source material.

Evidence: Composition validation follows the backend and UI consumers.

Impact: Adds optional host-owned limitations without changing candidate authority.

Refs: fast-slow-u2-u3 Tasks 4 through 6
2026-10-13 01:59:40 +00:00
weichen 124309b985 Retain trusted input bindings for task result reuse
Why: Completed tasks require current source authorization and content validation.

Evidence: Optional fields preserve existing contracts; composition verification follows.

Impact: Adds host-owned input source references with no new model authority.

Refs: fast-slow-u2-u3 Tasks 2 and 4
2026-10-13 01:35:53 +00:00
weichen 06f2723161 Add persistent work snapshots and checkpoint contracts
Why: preserve scoped work findings and truthful activity across reloads.

Evidence: task JSON loss and validation/state regressions RED to GREEN; complete Core suite.

Impact: optional card/document wire fields and bounded candidate validation; old Body remains supported.

Refs: fast-slow U2/U3 Task1, R03 R05 R09 R17
2026-10-13 01:04:56 +00:00
weichen 8e4f4e0382 Merge pull request 'Add U1 task summary body contract helpers' (#29) from feature/fast-slow-display-u1 into main
Reviewed-on: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/29
2026-10-12 12:16:33 +00:00
weichenandCursor ba289e7b17 Add U1 task summary body contract helpers
Why: Freeze L1 skeleton KV keys and honest no-findings placeholder
so Backend can stop stuffing raw task text into progress cards.
Evidence: go test ./... -count=1
Impact: New TaskSummaryView helpers; findings[] still deferred to U2/U3.
Refs: fast-slow-channel-display-u1-plan W1

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 07:41:05 +00:00
weichen 89f3906c39 Merge pull request 'Add surface.updated to the StreamEventType union in the TS authority' (#28) from feature/generative-ui-u4-eventtype into main 2026-10-11 11:37:16 +00:00
weichen bbebd153f4 Merge remote-tracking branch 'origin/main' into feature/generative-ui-u4-eventtype 2026-10-11 11:37:11 +00:00
weichen a563d31b1e Add surface.updated to the StreamEventType union in the TS authority
Why: U4 frontend store switches on the event type; the TS union mirrors
the Go EvSurfaceUpdated constant (Core #25) and must include it or the
frontend typecheck rejects the case.

Evidence: go test unchanged (TS-only); frontend tsc in the Dev
workspace after mirror sync.

Impact: Type-only additive.

Refs: Core PR #25; Dev task feature/generative-ui-u4
2026-10-11 11:36:33 +00:00
weichen 39632c2e7a Merge pull request 'Add the Surface snapshot types to the TS contract authority' (#27) from feature/generative-ui-u4-ts into main 2026-10-11 11:32:55 +00:00
weichen 37b96fd94f Add the Surface snapshot types to the TS contract authority
Why: U4 needs the frontend to render saved surfaces; the Go contract
(Core #25) gains its TypeScript mirror in the single authority so make
contracts regenerates the frontend and UI copies.

Evidence: Mirror regeneration and combined make test in the Dev
workspace after re-pinning.

Impact: Type-only additive change.

Refs: Core PR #25; Dev task feature/generative-ui-u4
2026-10-11 11:32:43 +00:00
weichen 7a98a7e7fe Merge pull request 'Add the optional node id/source fields to the TS contract authority' (#26) from feature/generative-ui-u2-ts-nodeids into main 2026-10-11 08:44:13 +00:00
weichen 360a8a8946 Add the optional node id/source fields to the TS contract authority
Why: PR #25 added the fields to the Go UINode; ts/types.ts is the
single authority that make contracts mirrors into the frontend and UI
repos, so the fields must originate here or the sync reverts manual
mirror edits (CI build caught exactly that).

Evidence: Regenerated mirrors and combined make test run in the Dev
workspace after re-pinning.

Impact: Type-only additive change.

Refs: Core PR #25; Dev task feature/generative-ui-u2
2026-10-11 08:44:00 +00:00
weichen 452daf3ffa Merge pull request 'Add the Surface contract types, node ids, and spec validation' (#25) from feature/generative-ui-u2-surface into main 2026-10-11 08:15:26 +00:00
weichen 626f8e1062 Add the Surface contract types, node ids, and spec validation
Why: The Generative UI proposal U2 needs a versioned, host-validated
envelope for agent-authored pages before any backend surface exists:
identity/compat/version envelopes, stable per-node ids with source
annotations, catalog-driven spec validation, and the surface.updated
projection event.

Evidence: go vet + go test in this repo (table tests cover a realistic
valid spec from the U1 trial shape, violations, limits, and old-payload
backward compatibility).

Impact: Additive only - UINode gains optional id/source (omitempty),
old card/form paths unaffected; no existing symbol changes.

Refs: Documents proposal 2026-10-10-generative-ui-plugin.md (r2, U2/U1
sections); Dev task feature/generative-ui-u2
2026-10-11 08:15:10 +00:00
34 changed files with 2040 additions and 67 deletions
+150
View File
@@ -0,0 +1,150 @@
name: governance
on:
pull_request:
push:
branches: [main]
jobs:
governance:
runs-on: ubuntu-latest
env:
GOPROXY: https://goproxy.cn,direct
steps:
- name: Checkout
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# 只物化两个治理 gitlink(Guidelines 与 deps/development-manager),非递归;
# 其它 gitlink 不物化、不取回。凭据只以 URL 限定的 extraheader 存在于本进程树内。
python3 - <<'PY_CHECKOUT'
import os
from pathlib import Path
import re
import subprocess
import sys
from urllib.parse import urlsplit
# 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed_server = urlsplit(SERVER)
if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc:
sys.exit("Checkout server must be the platform-injected http(s) URL")
SSH = "ssh://git@gitea.naitangbaobao.cn:52222/weichen/"
# 审查过的白名单,独立于候选 .gitmodules:仅列出两个治理 gitlink。
REPOSITORIES = {
"deps/development-guidelines": "Development-Guidelines",
"deps/development-manager": "Development-Manager",
}
TARGET = "weichen/Agent-System-Core"
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
if not root_token or not component_token:
sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN")
if os.environ.get("GITHUB_REPOSITORY") != TARGET:
sys.exit("Checkout repository is outside the reviewed allowlist")
sha = os.environ.get("GITHUB_SHA", "")
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
sys.exit("Checkout requires a fixed commit SHA")
# 丢弃继承的 Git 追踪/配置/SSH 钩子;凭据只存在于单个 Git 进程树的精确 URL 头中。
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null",
GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0")
config = {
"core.hooksPath": "/dev/null", "core.fsmonitor": "false",
"init.templateDir": "", "credential.helper": "",
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
}
def git(repo, *args, extra=None):
values = config | (extra or {})
env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return subprocess.run(["git", "-C", str(repo), *args], env=env,
check=True, stdout=subprocess.PIPE).stdout
def materialize_governance(root):
# -r 列出任意深度的 gitlink,但不进入 submodule 内部;其它 gitlink 只记录、从不取回。
links = {}
for entry in git(root, "ls-tree", "-rz", "HEAD").split(b"\0"):
if not entry:
continue
metadata, path = entry.split(b"\t", 1)
mode, kind, commit = metadata.decode().split()
if mode == "160000":
links[path.decode()] = commit
for name in REPOSITORIES:
if name not in links:
raise ValueError("Governance gitlink is missing from the fixed commit")
raw = git(root, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list")
entries = {}
for item in raw.split(b"\0"):
if not item:
continue
key, value = item.decode().split("\n", 1)
match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key)
if not match:
continue
name, field = match.groups()
if name not in REPOSITORIES:
continue
if field in entries.setdefault(name, {}):
raise ValueError("Duplicate candidate .gitmodules setting")
entries[name][field] = value
# 先整体校验两个治理条目,再进行任何带凭据的取回。
for name, repository in REPOSITORIES.items():
fields = entries.get(name, {})
if fields.get("path") != name:
raise ValueError("Governance submodule path is outside the reviewed allowlist")
trusted_url = f"{SERVER}/weichen/{repository}.git"
if fields.get("url") not in {SSH + repository + ".git", trusted_url}:
raise ValueError("Governance submodule URL is outside the reviewed allowlist")
for name, repository in REPOSITORIES.items():
url = f"{SERVER}/weichen/{repository}.git"
# 仅持久化审查过的公开 URL,使 Git 在进程级配置消失后仍识别该 submodule。
git(root, "config", f"submodule.{name}.url", url)
git(root, "submodule", "update", "--init", "--checkout", "--", name,
extra={f"submodule.{name}.url": url,
f"submodule.{name}.update": "checkout",
f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token})
child = root / name
if git(child, "rev-parse", "HEAD").decode().strip() != links[name]:
raise ValueError("Materialized governance gitlink does not match the fixed SHA")
try:
root = Path.cwd()
# 必须是干净的临时工作区;不复用既有 Git 状态。
if (root / ".git").exists():
raise ValueError("Checkout requires a fresh workspace")
git(root, "init", "-q")
url = f"{SERVER}/{TARGET}.git"
git(root, "remote", "add", "origin", url)
git(root, "fetch", "--depth=1", "origin", sha,
extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token})
git(root, "checkout", "-q", "--detach", "FETCH_HEAD")
materialize_governance(root)
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit(f"Checkout refused: {error}")
print("Checkout complete; credentials were not persisted in Git configuration")
PY_CHECKOUT
unset TOKEN SUBMODULE_TOKEN
- name: Install Go 1.27.1
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Validate agent execution configuration
run: scripts/agent-exec validate --repo .
- name: Governance check
run: scripts/dm governance check --repo .
+6
View File
@@ -0,0 +1,6 @@
[submodule "deps/development-guidelines"]
path = deps/development-guidelines
url = ssh://git@gitea.naitangbaobao.cn:52222/weichen/Development-Guidelines.git
[submodule "deps/development-manager"]
path = deps/development-manager
url = ssh://git@gitea.naitangbaobao.cn:52222/weichen/Development-Manager.git
@@ -0,0 +1,53 @@
# Agent-System-Core 采纳 development-governance-v2 治理套件 任务卡
任务主记录:[Agent-System-Core PR #35](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/35)(本文件随 PR 描述维护)。
状态:已完成(闭合随本 PR 合入;合入以平台记录为准);owner:weichen。
## 基线与授权
- 权威仓:weichen/Agent-System-Core;root_target:main;immediate_target:main
- branch_kind:initiative;initiative_id:governance-adoption-Agent-System-Core
- R0(initial_main_sha):35560aa5a3fdc9d8f92bb4b2e9eece5254b94c41;工作区:/home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption(worktree);分支:feature/governance-adoption
- 父任务信息(仅子任务填写):无
- accepting_requirements:false(单仓采纳 initiative,范围固定为下列十步,不接收新需求)
- 需求分流:verdict=NEW;selected_initiative=无;候选=无;理由=development-governance-v2 Phase 6 每个正式仓库独立 initiative;triage 收据=无(本仓采纳前尚无 triage 入口)
- 授权与交付终点:owner 已授权 development-governance-v2 全部实施(Agent-System-Dev 主记录 docs/superpowers/plans/2026-10-16-development-governance-v2-master.md);终点为本 PR 合入 main 且采纳状态 active。分支保护与 tag 由用户执行。
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md;外部 Agent 一单元一新进程/会话/工作区)
## 目标与非目标
目标(Phase 6 十步):固定 Guidelines SHA(deps/development-guidelines @ 2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb);增加 AGENT-EXECUTION.md;增加共享执行器薄入口(deps/development-manager @ 42e675fec8e0069646319fcd298e61d03d8cbfd5 + scripts/dm、scripts/agent-exec、scripts/task);四条路由(implementation/repair/test/review);任务卡模板与分支元数据;CI 中启用 schema 校验(.gitea/workflows/governance.yml:agent-exec validate + governance check);一次正例运行;负例(缺配置、坏收据、越界路径、未知副作用);独立 Review + CI;采纳状态 active。
非目标:不改业务代码与既有测试;不改分支保护(由用户执行);不发布 tag。
引导说明:AGENT-EXECUTION.md 由 host 以固定版本治理套件(dm-server governance init @ 42e675fec8e0069646319fcd298e61d03d8cbfd5)生成并作为引导配置提交(执行器运行前必须存在有效配置;沿用 DM 采纳先例);其余文件由实施单元生成。
## Agent 实施收据
| 单元 ID | 预期范围 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|---|---|---|---|---|---|---|---|---|---|
| governance-adoption-Agent-System-Core-r1 | 13 个允许路径(治理文件 + 两个 gitlink) | dde4f359 | implementation | cursor→antigravity→pi | run 9a7ec135(dm-server 42e675f,attempts=1) | cursor / not-observable | success | 98cfc6408d49(apply 快进) | 否 | /tmp/gov-receipt-Agent-System-Core-r1.json |
| governance-adoption-Agent-System-Core-repin-r1 | 9 个允许路径(治理文件 + 两个 gitlink) | a160b03c | implementation | cursor→antigravity→pi | run e64b5afe(dm-server pinned,attempts=1) | cursor / not-observable | success | 1ada9955d57a(apply 快进) | 否 | /tmp/gov-receipt-Agent-System-Core-repin-r1.json |
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at | 结果 |
|---|---|---|---|---|
| 启动 | 35560aa5a3fdc9d8f92bb4b2e9eece5254b94c41 | 35560aa5a3fdc9d8f92bb4b2e9eece5254b94c41 | 2026-10-17T05:51:28Z | 一致 |
## 结果与验证(完成后填写)
- 结果:治理套件已采纳——Guidelines 与 Development-Manager gitlink 固定、AGENT-EXECUTION.md 四条路由、共享执行器薄入口、pre-commit admit、governance CI;采纳状态 active(随合入生效)。
- 验证:见下表(命令输出由 /tmp/gov-neg/ev.sh 写入 .task/evidence/governance-adoption/,SELF 除非另注)。
- 影响与剩余项:不改业务代码;本地启用钩子需由人执行 `git config core.hooksPath scripts/hooks`;将 governance 检查设为分支保护必需项属用户双钥匙事项(命令见 Agent-System-Dev 治理主记录)。
| 时间(UTC) | 命令 | 被测 HEAD | exit | 来源 | 摘要/位置 |
|---|---|---|---|---|---|
| 20261017T061900Z | `scripts/dm governance check --repo .` | 98cfc6408d49 | 0 | SELF | ok fixtures;[输出](../evidence/governance-adoption/00-20261017T061900Z.log) sha256:ae4439e39cad7e8d |
| 20261017T061900Z | `scripts/agent-exec validate --repo .` | 98cfc6408d49 | 0 | SELF | validate: ok;[输出](../evidence/governance-adoption/01-20261017T061900Z.log) sha256:89ea92d2c3d1f9e4 |
| 20261017T061900Z | `python3 /tmp/gov-neg/adopt_cases.py .` | 98cfc6408d49 | 0 | SELF | [verdict] ALL PASS cases=5;[输出](../evidence/governance-adoption/02-20261017T061900Z.log) sha256:64f13535108476f9 |
| 20261017T071536Z | `scripts/dm governance check --repo .` | 1ada9955d57a | 0 | SELF | ok fixtures;[输出](../evidence/governance-adoption/03-20261017T071536Z.log) sha256:f46b6a39dd69f693 |
| 20261017T071536Z | `scripts/agent-exec validate --repo .` | 1ada9955d57a | 0 | SELF | validate: ok;[输出](../evidence/governance-adoption/04-20261017T071536Z.log) sha256:4a0c76e79c70e3bb |
| 20261017T071536Z | `python3 /tmp/gov-neg/adopt_cases.py .` | 1ada9955d57a | 0 | SELF | [verdict] ALL PASS cases=5;[输出](../evidence/governance-adoption/05-20261017T071536Z.log) sha256:3eb026a392f8336b |
| — | 候选 HEAD CI | 18f617d4e8cd | 0 | GATE | success governance / governance (pull_request)=success(/weichen/Agent-System-Core/actions/runs/3535/jobs/5611) |
| — | 独立 Review | 18f617d4e8cd | 0 | REPLAY | cursor-agent-stable --mode ask 全新只读会话(fresh_session=true,session id not-observable;codex/pi 因 Token Plan 429 不可用)VERDICT: APPROVE,无阻塞项 |
@@ -0,0 +1,19 @@
command: scripts/dm governance check --repo .
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
head: 98cfc6408d49dd88d957bb2dd13ca37bebe9facf
exit: 0
utc: 20261017T061900Z
---
ok gitlink deps/development-guidelines
ok gitlink deps/development-manager
ok agent-execution
ok file AGENTS.md
ok file PROJECT.md
ok file policy/guidelines-adoption.md
ok file .task/templates/task-card.md
ok executable scripts/agent-exec
ok executable scripts/hooks/pre-commit
ok executable scripts/dm
ok executable scripts/task
ok adoption status
ok fixtures
@@ -0,0 +1,7 @@
command: scripts/agent-exec validate --repo .
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
head: 98cfc6408d49dd88d957bb2dd13ca37bebe9facf
exit: 0
utc: 20261017T061900Z
---
validate: ok
@@ -0,0 +1,13 @@
command: python3 /tmp/gov-neg/adopt_cases.py .
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
head: 98cfc6408d49dd88d957bb2dd13ca37bebe9facf
exit: 0
utc: 20261017T061900Z
---
[object] Agent-System-Core-wt-feature-governance-adoption HEAD=98cfc6408d49dd88d957bb2dd13ca37bebe9facf
PASS positive-run-apply: run exit=0 terminal=success apply exit=0 head=b38a8a0da1df
PASS missing-config: run exit=2 terminal=invalid-config validate exit=2
PASS bad-receipt: apply exit=4 head_unchanged=True msg=apply: no local integrity record: changed paths do not match the transport diff
PASS out-of-scope-path: run exit=5 terminal=scope-expansion transport=False
PASS unknown-side-effect: run exit=4 terminal=failed-unknown transport=False
[verdict] ALL PASS cases=5
@@ -0,0 +1,19 @@
command: scripts/dm governance check --repo .
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
head: 1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
exit: 0
utc: 20261017T071536Z
---
ok gitlink deps/development-guidelines
ok gitlink deps/development-manager
ok agent-execution
ok file AGENTS.md
ok file PROJECT.md
ok file policy/guidelines-adoption.md
ok file .task/templates/task-card.md
ok executable scripts/agent-exec
ok executable scripts/hooks/pre-commit
ok executable scripts/dm
ok executable scripts/task
ok adoption status
ok fixtures
@@ -0,0 +1,7 @@
command: scripts/agent-exec validate --repo .
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
head: 1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
exit: 0
utc: 20261017T071536Z
---
validate: ok
@@ -0,0 +1,13 @@
command: python3 /tmp/gov-neg/adopt_cases.py .
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
head: 1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
exit: 0
utc: 20261017T071536Z
---
[object] Agent-System-Core-wt-feature-governance-adoption HEAD=1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
PASS positive-run-apply: run exit=0 terminal=success apply exit=0 head=f6303ea75059
PASS missing-config: run exit=2 terminal=invalid-config validate exit=2
PASS bad-receipt: apply exit=4 head_unchanged=True msg=apply: no local integrity record: changed paths do not match the transport diff
PASS out-of-scope-path: run exit=5 terminal=scope-expansion transport=False
PASS unknown-side-effect: run exit=4 terminal=failed-unknown transport=False
[verdict] ALL PASS cases=5
+66
View File
@@ -0,0 +1,66 @@
# <任务标题> 任务卡
任务主记录:<Issue/PR 链接,或注明"本文件随 PR 描述维护">。
状态:<进行中 / 待审核 / 已合入 / 已放弃>;owner:<填写>。
## 基线与授权
- 权威仓:<仓 URL 或逻辑 ID>;root_target:main;immediate_target:<main 或 feature/initiative>
- branch_kind:<initiative | child | standalone>;initiative_id:<稳定 ID,普通任务为空>
- R0(initial_main_sha):<完整 SHA>;工作区:<路径>(worktree / clone);分支:<feature|fix|hotfix/...>
- 父任务信息(仅子任务填写):parent_branch=<父分支>;parent_head_sha=<SHA>;parent_pr=<父 PR>
- accepting_requirements:<true | false>(仅 initiative 可为 true;冻结后为 false)
- 需求分流:verdict=<JOIN | NEW>;selected_initiative=<分支或无>;候选=<分支@HEAD 列表或无>;理由=<不适配/适配依据>;triage 收据=<源码外路径与摘要>
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md;外部 Agent 一单元一新进程/会话/工作区)
## 目标与非目标
<预期结果、受影响入口/模块/契约;明确不做什么。>
## Agent 实施收据
按根目录 AGENT-EXECUTION.md 路由;外部 Agent 一单元一新进程/会话/隔离工作区。实施、修复与测试必须通过登记的候选执行;未配置或候选安全耗尽停在 unavailable/blocked,禁止 host 隐式代写。
| 单元 ID | 预期范围 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|---|---|---|---|---|---|---|---|---|---|
| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |
## 复用材料(task-start 自动检索结果摘录)
- 相关近期提交:<git log -- <paths> 命中;无则写无>。
- 相关经验条目:<检索命中;无则写无>。
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at | 结果 |
|---|---|---|---|---|
| 启动 | <R0> | <HEAD> | <日期时间> | <一致/已同步> |
| 验收前 | <填写> | <填写> | <填写> | <填写> |
字段没有发生的写"无",不省略。本卡与 PR 描述同源:开 PR 时将"目标与非目标、结果与验证、影响与剩余项"复制进 PR 描述,不另写第二份。
## 交付调度(有多个就绪任务时)
- 调度记录 / entry_id / 当前 attempt_id:<平台或调度服务链接及标识;未入队写无>。
- 入队源 HEAD / owner / 依赖:<完整 SHA、身份、依赖或无>。
- 固定候选引用:<调度尝试中的 base/head、组件与配置;未固定写无>。
- 窗口、最终 CI、Review 与真实合入结果:<引用调度记录及原始证据,不复制全队列>。
定义见 [合入调度与 CI 分配](../standards/merge-queue.md)。本段在候选固定前整理;固定后先使用源码之外的记录保存运行结果,再按项目闭合流程回填。
## 结果与验证(完成后填写)
- 结果:<交付了什么;未发生写无>。
- 验证:<结论性摘要:PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据见下表,来源标注与写入规则见 [按影响选择验证与证据](../standards/verification.md)>。
- 影响与剩余项:<影响面、遗留风险、后续任务>。
| 时间(UTC) | 命令 | 被测 HEAD | exit | 来源 | 摘要/位置 |
|---|---|---|---|---|---|
| <时间> | <命令;由项目 evidence 工具写入,无工具时附命令+对象 SHA+输出 digest 并标 SELF> | <HEAD> | <exit> | GATE/REPLAY/SELF | <摘要/位置> |
## 回收(task-finish)
- 合入核对:<分支已包含于目标分支的核对输出>。
- 未跟踪/ignored 文件处置:<归档/可再生/存疑 逐项;无则写无>。
- 回收动作:<worktree remove、分支删除、prune;放弃路径注明原因>。
+67
View File
@@ -0,0 +1,67 @@
# Project Agent Execution Configuration
# 项目工程 Agent 执行路由配置
适用范围:定义本项目中由 AI / Agent 主导的代码实施、修复、测试与审阅所调用的 Agent CLI 路由。
解析器识别正文中唯一一个 ```agent-execution 代码块。正文外的说明文字可根据项目需要调整。
通用硬规则见 Development-Guidelines 的 [Agent 执行与协作契约](deps/development-guidelines/standards/agent-execution.md)。
本配置为项目本地路由权威,声明具体的候选 profile、尝试次数上限、超时、集成入口与豁免路径;
代码块须通过 `deps/development-guidelines/templates/schemas/agent-execution.schema.json` 校验后才能登记为 active。
`fallback_to_host` 只能为 `never`;旧值 `all-unavailable` 仅供迁移工具读取,不能通过准入。
下例的 CLI 名称、命令与路径均为占位,按项目实际执行器替换。
```agent-execution
schema: agent-execution/v1
execution:
fresh_process_per_unit: true
permission: run-everything
fallback_to_host: never
max_parallel: 3
timeout_seconds: 7200
idle_timeout_seconds: 600
summary_bytes: 16384
raw_output_bytes: 10485760
max_attempts_per_profile: 3
emergency_profile: codex
roles:
implementation: [cursor, antigravity, pi]
repair: [cursor, antigravity, pi]
test: [pi, antigravity, cursor]
review: [pi, antigravity, cursor, codex]
profiles:
cursor:
adapter: cursor
executable: "cursor-agent-stable"
model: auto
enabled: true
antigravity:
adapter: antigravity
executable: agy
model: auto
enabled: true
pi:
adapter: pi
executable: pi
model: auto
enabled: true
codex:
adapter: codex
executable: codex
model: auto
enabled: true
integration:
exempt_paths: [".task/**", "docs/superpowers/specs/**", "docs/superpowers/plans/**"]
transport:
command: "scripts/agent-exec apply"
admission:
command: "scripts/agent-exec admit"
cleanup:
command: "scripts/agent-exec cleanup"
```
+21 -3
View File
@@ -23,9 +23,9 @@
历史文档与旧链接里的 `git-panel.u1680832.nyat.app:30319` 已失效(TLS 握手失败),
见到一律按过期处理,不要再试。当前权威通道:
- **Web / API(HTTPS)**:`https://git-home.u1680832.nyat.app:30319`(Gitea 1.27.3,
- **Web / API(HTTPS)**:`https://gitea.naitangbaobao.cn`(Gitea 1.27.3,
API 前缀 `/api/v1`;偶发 broken pipe / 超时,重试即可)。
- **Git over SSH**:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/<Repo>.git`
- **Git over SSH**:`ssh://git@gitea.naitangbaobao.cn:52222/weichen/<Repo>.git`
(各仓 origin 与 gitlink 均为此格式)。
- **API token**:`~/.secrets/gitea`,两行键值 `URL_PREFIX=…` 与
`ACCESS_TOKEN=<40 位 hex>`;取值 `grep ACCESS_TOKEN ~/.secrets/gitea | cut -d= -f2-`,
@@ -33,4 +33,22 @@
(baseline 硬边界 2:只记录来源、字段与可用状态)。
- **SSH key**:`~/.ssh/home_gitea`(ED25519,公钥指纹
`SHA256:GYXaOXIMeloQ1fKSfdJ/ndgPQzs/Ix7Hk62ekfkfQ20`,Gitea 侧密钥名 GMKtec);
`~/.ssh/config` 已有 `git-ssh.u1680832.nyat.app` 主机条目,克隆/推送直接可用。
`~/.ssh/config` 已有 `gitea.naitangbaobao.cn` 主机条目(端口 52222),克隆/推送直接可用。
## Agent 执行与新项目准入(强制)
1. 缺 Guidelines 固定版本(`deps/development-guidelines` gitlink 未就绪或 HEAD 不符)时,Agent 只能只读分析,不得实施。
2. 缺有效 `AGENT-EXECUTION.md`(`scripts/dm governance check` 未通过)时禁止 AI 实施。
3. 缺共享执行器(`scripts/agent-exec` 或 `deps/development-manager` 不可用)时,状态为 `blocked/unavailable`,不得改由 host 替代。
4. 不允许 host 代写实施单元;实施、修复与测试必须经 `AGENT-EXECUTION.md` 登记的候选执行。
5. 人类直接开发按其自身授权与项目规则进行,不受本条款约束。
6. 分支保护与规范 tag 由用户负责设置与维护,Agent 不得代为设置。
AI 实施单元只能通过 `scripts/agent-exec run` 运行(配置与路由见 AGENT-EXECUTION.md),并由 `scripts/agent-exec apply` 集成。
入口:
- 需求分流:`scripts/task triage discover|decide|verify`
- 任务启动:`scripts/task start …`
- PR 检查:`scripts/task pr-check …`
- Agent 执行:`scripts/agent-exec run|apply|admit|validate`
+3
View File
@@ -8,6 +8,9 @@
在本仓根目录执行 `go test ./...` 验证 Go 契约与不变量;纯文档修改执行
`git diff --check` 并核对受影响链接、命令和来源。本仓没有 Makefile。
治理门禁(与 `.gitea/workflows/governance.yml` 相同):在本仓根目录依次执行
`scripts/agent-exec validate --repo .` 与 `scripts/dm governance check --repo .`,两者均须通过。
修改 `ts/types.ts` 后在 Dev 仓运行 `make contracts` 同步消费者,再以 `make test`
验证固定组件组合;单独运行 Core 测试不能代替提供方与消费者的兼容验证。
本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。
+47 -13
View File
@@ -12,19 +12,53 @@ import "time"
type ActivityCard struct {
ID ID `json:"id"`
// ProjectID/ConversationID 定位归属;ConversationID 为空表示项目级卡片(远期跨会话收件箱)。
ProjectID ID `json:"projectId"`
ConversationID ID `json:"conversationId,omitempty"`
SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run
AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话)
Kind CardKind `json:"kind"`
Title string `json:"title"`
Status CardStatus `json:"status"`
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
Body []UINode `json:"body,omitempty"`
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
Pinned bool `json:"pinned,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ProjectID ID `json:"projectId"`
ConversationID ID `json:"conversationId,omitempty"`
SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run
AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话)
Kind CardKind `json:"kind"`
Title string `json:"title"`
Status CardStatus `json:"status"`
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
Task *TaskSnapshot `json:"task,omitempty"`
Body []UINode `json:"body,omitempty"`
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
Pinned bool `json:"pinned,omitempty"`
Work *WorkCard `json:"work,omitempty"` // optional background-work snapshot; never a foreground approval
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// WorkCard keeps the complete saved result and an optional, non-blocking next step
// on the original task. Reading and answering do not change the result version.
type WorkCard struct {
Result string `json:"result,omitempty"`
ResultVersion int `json:"resultVersion"`
ReadVersion int `json:"readVersion"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
Decision *WorkDecision `json:"decision,omitempty"`
}
type WorkDecision struct {
RunID ID `json:"runId,omitempty"` // reserved child identity; reconciles acceptance after restart without rerunning
Mode string `json:"mode"` // single | multiple
State string `json:"state"` // open | deferred | dispatching | submitted
Options []WorkOption `json:"options,omitempty"`
SelectedIDs []string `json:"selectedIds,omitempty"`
FreeText string `json:"freeText,omitempty"`
RespondedAt *time.Time `json:"respondedAt,omitempty"`
}
type WorkOption struct {
ID string `json:"id"`
Label string `json:"label"`
}
// WorkCardAction is version-fenced. Presentation-only opens never mark a result read.
type WorkCardAction struct {
ResultVersion int `json:"resultVersion"`
SelectedIDs []string `json:"selectedIds,omitempty"`
FreeText string `json:"freeText,omitempty"`
}
type CardKind string
+46
View File
@@ -0,0 +1,46 @@
package core
import (
"encoding/json"
"strings"
"testing"
"time"
)
func TestWorkCardSurvivesPersistedSnapshot(t *testing.T) {
completed := time.Date(2026, 10, 15, 6, 0, 0, 0, time.UTC)
card := ActivityCard{ID: "card_work", Status: CardDone, Work: &WorkCard{
Result: strings.Repeat("完整结果\n", 200) + "最后一项依据",
ResultVersion: 2, ReadVersion: 1, CompletedAt: &completed,
Decision: &WorkDecision{Mode: "multiple", State: "submitted", Options: []WorkOption{{ID: "summary", Label: "生成摘要"}},
SelectedIDs: []string{"summary"}, FreeText: "请突出关键限制", RespondedAt: &completed},
}}
data, err := json.Marshal(card)
if err != nil {
t.Fatal(err)
}
var restored ActivityCard
if err := json.Unmarshal(data, &restored); err != nil {
t.Fatal(err)
}
if restored.Work == nil || restored.Work.Result != card.Work.Result || restored.Work.ReadVersion != 1 || restored.Work.ResultVersion != 2 {
t.Fatalf("complete result or unread version lost: %+v", restored.Work)
}
if d := restored.Work.Decision; d.State != "submitted" || d.SelectedIDs[0] != "summary" || d.FreeText != "请突出关键限制" || !d.RespondedAt.Equal(completed) {
t.Fatalf("decision history lost: %+v", d)
}
}
func TestLegacyCardOmitsOptionalWork(t *testing.T) {
var legacy ActivityCard
if err := json.Unmarshal([]byte(`{"id":"old","status":"resolved","body":[{"type":"markdown","markdown":"保留历史"}]}`), &legacy); err != nil {
t.Fatal(err)
}
data, err := json.Marshal(legacy)
if err != nil {
t.Fatal(err)
}
if legacy.Work != nil || strings.Contains(string(data), `"work"`) || legacy.Body[0].Markdown != "保留历史" {
t.Fatalf("legacy payload changed: %s", data)
}
}
+47 -46
View File
@@ -13,18 +13,19 @@ type ParsedDocument struct {
}
type DocMeta struct {
Filename string `json:"filename"`
MIME string `json:"mime,omitempty"`
Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent
Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低
CrossCheck *CrossCheck `json:"crossCheck,omitempty"`
ParsedAt time.Time `json:"parsedAt"`
Origin *DocumentOrigin `json:"origin,omitempty"`
Filename string `json:"filename"`
MIME string `json:"mime,omitempty"`
Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent
Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低
CrossCheck *CrossCheck `json:"crossCheck,omitempty"`
ParsedAt time.Time `json:"parsedAt"`
}
type CrossCheck struct {
With string `json:"with"` // 交叉验证的另一解析器
DiffBlocks int `json:"diffBlocks"`
Verdict string `json:"verdict"` // agree | differ
With string `json:"with"` // 交叉验证的另一解析器
DiffBlocks int `json:"diffBlocks"`
Verdict string `json:"verdict"` // agree | differ
}
type DocBlockType string
@@ -39,13 +40,13 @@ const (
)
type DocBlock struct {
Type DocBlockType `json:"type"`
Level int `json:"level,omitempty"` // heading
Text string `json:"text,omitempty"` // para/heading/list 项拼接/code
Items []string `json:"items,omitempty"` // list
Rows [][]string `json:"rows,omitempty"` // table(首行为表头)
Ref string `json:"ref,omitempty"` // image-ref 的对象键
Confidence float64 `json:"confidence,omitempty"`
Type DocBlockType `json:"type"`
Level int `json:"level,omitempty"` // heading
Text string `json:"text,omitempty"` // para/heading/list 项拼接/code
Items []string `json:"items,omitempty"` // list
Rows [][]string `json:"rows,omitempty"` // table(首行为表头)
Ref string `json:"ref,omitempty"` // image-ref 的对象键
Confidence float64 `json:"confidence,omitempty"`
}
// PlainText 把文档压成纯文本(附件注入与全文索引用);表格按行拼接。
@@ -88,60 +89,60 @@ func (d ParsedDocument) PlainText() string {
// ParserDesc 注册表条目(来源标记:builtin/plugin/...;一切可禁用)。
type ParserDesc struct {
ID string `json:"id"`
Formats []string `json:"formats"` // 支持的扩展名
ID string `json:"id"`
Formats []string `json:"formats"` // 支持的扩展名
Source SourceKind `json:"source"`
Note string `json:"note,omitempty"`
Note string `json:"note,omitempty"`
}
// ---- 检索(4.12)----
// KnowledgeDoc 是检索语料的一条文档(来自 ingest 或插件知识库,带来源标记)。
type KnowledgeDoc struct {
ID ID `json:"id"`
Owner ID `json:"owner,omitempty"` // 空 = 共享(插件级)知识
ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库
Source string `json:"source"` // upload | artifact | plugin:<id>
Title string `json:"title"`
Ref string `json:"ref,omitempty"` // 工件/文件引用
Chunk string `json:"chunk"`
ID ID `json:"id"`
Owner ID `json:"owner,omitempty"` // 空 = 共享(插件级)知识
ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库
Source string `json:"source"` // upload | artifact | plugin:<id>
Title string `json:"title"`
Ref string `json:"ref,omitempty"` // 工件/文件引用
Chunk string `json:"chunk"`
CreatedAt time.Time `json:"createdAt"`
}
type RetrievalHit struct {
Doc KnowledgeDoc `json:"doc"`
Score float64 `json:"score"`
Kind string `json:"kind"` // fulltext | vector | plugin
Doc KnowledgeDoc `json:"doc"`
Score float64 `json:"score"`
Kind string `json:"kind"` // fulltext | vector | plugin
}
type RetrievalQuery struct {
ProjectID ID `json:"projectId,omitempty"`
Text string `json:"text"`
TopK int `json:"topK,omitempty"`
ProjectID ID `json:"projectId,omitempty"`
Text string `json:"text"`
TopK int `json:"topK,omitempty"`
Sources []string `json:"sources,omitempty"` // 过滤来源
}
// ---- 记忆(4.13 契约;实现远期)----
type MemoryEntry struct {
ID string `json:"id"`
Scope string `json:"scope"` // user | project
ProjectID ID `json:"projectId,omitempty"`
Type string `json:"type"` // fact | preference | decision | episode | procedure
Content string `json:"content"`
Source string `json:"source"` // run:<id> | manual
Confidence float64 `json:"confidence,omitempty"`
CreatedAt time.Time `json:"createdAt"`
LastAccessed time.Time `json:"lastAccessed"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
ID string `json:"id"`
Scope string `json:"scope"` // user | project
ProjectID ID `json:"projectId,omitempty"`
Type string `json:"type"` // fact | preference | decision | episode | procedure
Content string `json:"content"`
Source string `json:"source"` // run:<id> | manual
Confidence float64 `json:"confidence,omitempty"`
CreatedAt time.Time `json:"createdAt"`
LastAccessed time.Time `json:"lastAccessed"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
// ---- 上传文件 ----
type UploadInfo struct {
ArtifactID ID `json:"artifactId"`
Filename string `json:"filename"`
MIME string `json:"mime,omitempty"`
Size int64 `json:"size"`
ArtifactID ID `json:"artifactId"`
Filename string `json:"filename"`
MIME string `json:"mime,omitempty"`
Size int64 `json:"size"`
Parsed *ParsedDocument `json:"parsed,omitempty"`
}
Vendored Submodule
+1
+3
View File
@@ -61,6 +61,9 @@ const (
EvCardUpdated StreamEventType = "card.updated"
EvCardStatus StreamEventType = "card.status"
EvCardResolved StreamEventType = "card.resolved"
// EvSurfaceUpdated:Generative UI Surface 快照保存后的投影事件(提案 U2;
// 载荷 core.SurfaceSnapshot,按会话流发布,前端消费在 U4)。
EvSurfaceUpdated StreamEventType = "surface.updated"
)
// ProtocolVersion 是事件信封与 payload 词汇的当前版本。
+37
View File
@@ -0,0 +1,37 @@
# 通用规范采纳记录
- 项目/负责人:Agent-System-Core / weichen
- 状态:active
- 规范位置:`deps/development-guidelines`
- 规范性文档目录:`policy/`(本仓现有 `policy/README.md`;项目规则与制度放此处)
- 过程文档位置:仓内固定目录 `.task/`(任务卡与任务主记录;任务主记录同时随 PR 描述维护,豁免理由见「项目差异」)
- 版本权威:本仓 gitlink `deps/development-guidelines` @ `2688fd2be6a5`(完整 SHA `2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb`);可读标签:无
- 批准/生效依据:feature/governance-adoption PR(Agent-System-Core)
- 替代范围和迁移说明:无
| 规范/工作流 | 适用条件与范围 | 采纳或不适用理由 | 执行方式/证据 |
|---|---|---|---|
| core/baseline.md | 日常工作 | 采纳:所有任务的硬边界、默认做法与完成声明基线 | manual |
| workflows/delivery.md、debugging.md、handoff.md | 交付、排障与交接 | 采纳:本仓以 PR 交付,Bug 排障与会话交接走统一流程 | manual |
| standards/collaboration.md | 并行共同起点、两级分支、节点同步与陈旧候选拒绝 | 采纳:PR 合入前须同步最新目标分支并附证据(见 AGENTS.md) | manual;暂无真实平台负例,未登记 enforced |
| standards/merge-queue.md | 多个就绪任务的最终同步、CI、Review 与合入调度 | 不适用:本仓由单一 owner 串行合入,当前无多个就绪任务竞争交付窗口;出现时另行登记 | 不适用 |
| standards/agent-execution.md | 工程 Agent CLI 执行路由、会话隔离与受控集成 | 采纳:允许 AI 协助代码实施;配置见 AGENT-EXECUTION.md | AGENT-EXECUTION.md + scripts/agent-exec(validate / admit);证据:governance check 与正负例(.task/feature-governance-adoption.md) |
| standards/verification.md | 验证与结论 | 采纳:完成声明须附实际命令输出 | 命令见 PROJECT.md「验证」节(`go test ./...`、`git diff --check`、`scripts/agent-exec validate --repo .`、`scripts/dm governance check --repo .`);CI:`.gitea/workflows/governance.yml` 运行治理门禁 |
| standards/module-contracts.md、standards/multi-repo.md | 公共契约(Go 类型、黄金样本、TS 镜像)的边界,以及依赖仓以 gitlink 固定版本 | 采纳:本仓是公共契约库,治理依赖以 gitlink 固定 | manual;gitlink 与 `.gitmodules` 固定,CI 物化白名单见 `.gitea/workflows/governance.yml`;契约一致性由 `conformance_test.go` 与 `testdata/golden` 覆盖 |
| standards/security.md | CI 凭据、submodule 取回与执行工具边界 | 采纳:CI 仅以限定 URL 的凭据取回两个治理 gitlink,凭据不落盘 | manual;`.gitea/workflows/governance.yml` 白名单与凭据约束 |
| workflows/design-verification.md、standards/testability.md | 契约增量的设计与验证、纯函数的可独立测试性 | 采纳:契约变更按增量设计验收,并以黄金样本与一致性测试验证 | manual;`conformance_test.go`、`testdata/golden` |
| standards/failure-containment.md、data-reliability.md、observability.md、configuration.md、release-operations.md、automation.md | 运行时服务、数据库、部署发布、定时自动化与故障控制 | 不适用:本仓只含契约与纯函数,不含 I/O、数据库或运行服务,不独立部署(见 PROJECT.md) | 不适用 |
## 项目差异
| 原规则/章节 | 项目采用方式 | 原因与风险 | 负责人/复查条件 |
|---|---|---|---|
| 过程文档默认放独立过程仓 | 任务卡与任务主记录放仓内 `.task/`,AGENT-EXECUTION.md 的 `exempt_paths` 已豁免 `.task/**` | 单一 owner 的小仓库,过程记录体量小,独立过程仓成本不划算;风险是过程记录与代码同仓,禁止写入凭据或 token | weichen;协作人增加或过程记录需跨仓共享时复查 |
## 本次升级或首次接入验证
- 入口:`AGENTS.md` 保留原内容并追加「Agent 执行与新项目准入(强制)」;`PROJECT.md`「验证」节补充 `scripts/agent-exec validate --repo .` 与 `scripts/dm governance check --repo .`;`AGENT-EXECUTION.md` 原样保留(`governance init --adopt` 跳过已存在文件)。
- 版本获取:`deps/development-guidelines` @ `2688fd2be6a5`,`deps/development-manager` @ `bd3a7de57a3a`;`.gitmodules` 使用规范 SSH 地址,不含凭证或本机路径。
- 占位符:模板占位符已全部替换;`policy/guidelines-adoption.md` 与追加的准入段中不再有模板尖括号文字。
- 冲突检查:仓内原无其他采纳记录(无 `policy/adoption.md`),原无旧版 Agent 执行入口(无 `scripts/agent_exec.py`),原无 `.gitea/workflows`,治理 workflow 为新增,无需修改既有 CI。
- 正负例与 Review/CI 证据:见 `.task/feature-governance-adoption.md`。
+125 -1
View File
@@ -1,6 +1,9 @@
package core
import "encoding/json"
import (
"encoding/json"
"strings"
)
// 智能体画像(4.7):核心定制机制,全部数据化、可导入导出。
type AgentProfile struct {
@@ -107,6 +110,16 @@ const (
EffectRestart SettingEffect = "restart" // 需重启
)
// SettingScope 声明设置项的写入归属(提案 2026-10-13-rdm-settings-members-duties-perms-increment P2)。
// 空串 = 默认:user 与 project 层可写(向后兼容既有 schema)。
type SettingScope string
const (
// SettingScopeSystem 受管项:仅系统管理面(admin)可写;user/project 写入被拒,
// 生效链忽略这两层的存量覆盖值。
SettingScopeSystem SettingScope = "system"
)
// SettingDesc 描述一个设置项;设置页按此渲染并标注生效时机(schema 驱动)。
type SettingDesc struct {
Key string `json:"key"`
@@ -118,6 +131,117 @@ type SettingDesc struct {
Options []string `json:"options,omitempty"` // enum
Effect SettingEffect `json:"effect"`
Secret bool `json:"secret,omitempty"`
Scope SettingScope `json:"scope,omitempty"` // 空 = user/project 可写;system = 受管
}
// ManagedPaths 返回 schema 中 scope=system 的键路径(点路径形式)。
func ManagedPaths(descs []SettingDesc) []string {
out := make([]string, 0, len(descs))
for _, d := range descs {
if d.Scope == SettingScopeSystem {
out = append(out, d.Key)
}
}
return out
}
// FlattenSettingLeafPaths 列出 values 的全部叶子点路径。子树为标量或数组时该子树
// 根即叶子。用于受管触碰检测:深合并语义下叶子决定最终覆盖面。
func FlattenSettingLeafPaths(values map[string]any) []string {
var out []string
var walk func(prefix string, m map[string]any)
walk = func(prefix string, m map[string]any) {
for k, v := range m {
path := k
if prefix != "" {
path = prefix + "." + k
}
if vm, ok := v.(map[string]any); ok && len(vm) > 0 {
walk(path, vm)
continue
}
out = append(out, path)
}
}
walk("", values)
return out
}
// pathTouchesManaged 判断 path 是否触碰 managed:等于受管路径、位于受管子树内
// (受管路径是 path 的前缀),或整体覆盖受管子树(path 是受管路径的前缀——
// 标量覆盖整棵受管 map 同样破坏受管值)。
func pathTouchesManaged(path string, managed []string) bool {
for _, mp := range managed {
if path == mp ||
strings.HasPrefix(path, mp+".") ||
strings.HasPrefix(mp, path+".") {
return true
}
}
return false
}
// TouchesManagedSettings 返回 values 中触碰受管路径的叶子;空切片 = 无触碰。
// 调用方须传入已展开点路径的嵌套 map(normalize 后)。
func TouchesManagedSettings(values map[string]any, managed []string) []string {
var hits []string
for _, p := range FlattenSettingLeafPaths(values) {
if pathTouchesManaged(p, managed) {
hits = append(hits, p)
}
}
return hits
}
// StripManagedSettings 返回删除受管路径后的浅拷贝(不改入参)。受管路径的祖先
// 若因此只剩空 map 一并删除。入参须为嵌套形式(点路径已展开)。
func StripManagedSettings(values map[string]any, managed []string) map[string]any {
if len(values) == 0 || len(managed) == 0 {
return values
}
// 叶子命中或祖先整体命中都删;逐叶子重建最简单可靠。
leaves := FlattenSettingLeafPaths(values)
keep := map[string]any{}
for _, leaf := range leaves {
if pathTouchesManaged(leaf, managed) {
continue
}
setPath(keep, leaf, lookupAny(values, leaf))
}
return keep
}
func setPath(m map[string]any, path string, val any) {
parts := strings.Split(path, ".")
cur := m
for i, p := range parts {
if i == len(parts)-1 {
cur[p] = val
return
}
next, ok := cur[p].(map[string]any)
if !ok {
next = map[string]any{}
cur[p] = next
}
cur = next
}
}
func lookupAny(m map[string]any, path string) any {
parts := strings.Split(path, ".")
var cur any = m
for _, p := range parts {
cm, ok := cur.(map[string]any)
if !ok {
return nil
}
cur, ok = cm[p]
if !ok {
return nil
}
}
return cur
}
// EffectiveSettings 是覆盖链(system → user → project → conversation)深合并结果。
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 共享 Agent 执行器薄入口:转调 scripts/dm 的 agent-exec 子命令(validate / run / apply / admit / cleanup)。
set -euo pipefail
exec "$(dirname "$0")/dm" agent-exec "$@"
Executable
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# 共享 dm-server 薄入口:读取 deps/development-manager 的 gitlink 钉住提交,
# 按该提交构建并缓存 dm-server,再 exec 其子命令(agent-exec、task、governance)。执行语义与退出码以该二进制为准。
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
dm="$root/deps/development-manager"
die() { echo "dm: $*" >&2; exit 2; }
# git 钩子会导出仓库本地变量(GIT_DIR、GIT_INDEX_FILE 等),它们会让 `git -C "$dm"` 解析到外层仓库。
# 所有针对 DM 检出的 git 与 go 调用都在清除这些变量后执行;读取外层 gitlink 与最终 exec 仍保留继承环境。
dm_local_env="GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_GRAFT_FILE GIT_NAMESPACE GIT_CONFIG GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT GIT_SHALLOW_FILE GIT_IMPLICIT_WORK_TREE $(git rev-parse --local-env-vars 2>/dev/null || true)"
dm_clean_env() { for v in $dm_local_env; do unset "$v"; done; }
dmgit() ( dm_clean_env; git -C "$dm" "$@" )
# 1. 钉住版本只来自索引中的 gitlink(mode 160000),不读取工作区文件。
entry=$(git -C "$root" ls-files -s -- deps/development-manager 2>/dev/null || true)
[ -n "$entry" ] || die "索引中缺少 deps/development-manager 的 gitlink,无法确定钉住的 DM 版本"
read -r mode pin _stage _path <<<"$entry"
[ "$mode" = 160000 ] || die "deps/development-manager 不是 gitlink(mode=$mode)"
[[ "$pin" =~ ^[0-9a-f]{40}$ ]] || die "gitlink 提交格式无效:$pin"
# 2. 检出必须存在且确为 DM 独立仓库(避免 git -C 误解析到外层仓库),HEAD 必须等于钉住提交。
[ -e "$dm/.git" ] || die "deps/development-manager 未检出;请按钉住提交 $pin 检出该 gitlink"
top=$(dmgit rev-parse --show-toplevel 2>/dev/null || true)
[ -n "$top" ] && [ "$top" = "$(cd "$dm" && pwd -P)" ] \
|| die "deps/development-manager 不是独立检出;请按钉住提交 $pin 检出该 gitlink"
head=$(dmgit rev-parse HEAD 2>/dev/null || true)
[ "$head" = "$pin" ] \
|| die "deps/development-manager HEAD(${head:-未知})与钉住提交 $pin 不一致;请按钉住提交检出"
# 3. 检出工作树必须干净(忽略嵌套 submodule 状态)。
dirty=$(dmgit status --porcelain --ignore-submodules=all) \
|| die "无法读取 deps/development-manager 的工作树状态"
[ -z "$dirty" ] || die "deps/development-manager 存在未提交改动,拒绝运行(请先清理该检出)"
# 4. 二进制按钉住提交缓存;缺失时在缓存目录内临时构建,完成后原子改名,避免留下半成品。
cache="${DM_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/development-manager/dm-server}"
bin="$cache/dm-server-$pin"
if [ ! -x "$bin" ]; then
command -v go >/dev/null 2>&1 || die "缺少 go,无法构建 dm-server($pin)"
mkdir -p "$cache"
cache=$(cd "$cache" && pwd -P)
bin="$cache/dm-server-$pin"
work=$(mktemp -d "$cache/build-$pin.XXXXXX")
if ! (dm_clean_env; cd "$dm" && GOWORK=off go build -trimpath -o "$work/dm-server" ./cmd/dm-server); then
rm -rf "$work"
die "构建 dm-server 失败($pin)"
fi
mv -f "$work/dm-server" "$bin"
rm -rf "$work"
fi
exec "$bin" "$@"
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# 本地检查(pre-commit):集成分支直提拒绝 + 分支命名 + 本地完整性核对。权威准入在 PR(baseline.md:26)。
branch=$(git rev-parse --abbrev-ref HEAD)
case "$branch" in
main|master)
echo "pre-commit: 拒绝——集成分支 ${branch} 不得直接提交,走 feature 分支 + PR(collaboration.md)" >&2
exit 1 ;;
esac
echo "$branch" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || {
echo "pre-commit: 拒绝——分支名须为 feature|fix|hotfix|release/<slug>,当前:${branch}" >&2
exit 1
}
root=$(git rev-parse --show-toplevel)
# admit 只是本地完整性/篡改证据(SELF):同一 OS 用户可读取本地 HMAC 密钥并绕过本钩子,
# 它不是 GATE,也不是可信身份。缺失或无效的本地记录仍拒绝;交付信任来自精确 HEAD 的 CI 或独立 REPLAY。
"$root/scripts/agent-exec" admit --repo "$root"
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 通用任务入口:转调 scripts/dm 的 task 子命令(triage / start / pr-check)。
set -euo pipefail
exec "$(dirname "$0")/dm" task "$@"
+85
View File
@@ -0,0 +1,85 @@
package core
import (
"reflect"
"sort"
"testing"
)
func TestManagedPathsFiltersSystemScope(t *testing.T) {
descs := []SettingDesc{
{Key: "models.chat.model", Scope: SettingScopeSystem},
{Key: "ui.theme"},
{Key: "tools.tavily.apiKeys", Scope: SettingScopeSystem},
}
got := ManagedPaths(descs)
sort.Strings(got)
want := []string{"models.chat.model", "tools.tavily.apiKeys"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ManagedPaths = %v, want %v", got, want)
}
}
func TestTouchesManagedSettings(t *testing.T) {
managed := []string{"models.chat.model", "tools.tavily.apiKeys", "telemetry.otlpEndpoint"}
cases := []struct {
name string
in map[string]any
want []string
}{
{"exact leaf", map[string]any{"models": map[string]any{"chat": map[string]any{"model": "x"}}}, []string{"models.chat.model"}},
{"sibling untouched", map[string]any{"models": map[string]any{"chat": map[string]any{"baseUrl": "x"}}}, nil},
{"ancestor scalar overwrite", map[string]any{"models": "oops"}, []string{"models"}},
{"subtree inside managed", map[string]any{"telemetry": map[string]any{"otlpEndpoint": "http://x"}}, []string{"telemetry.otlpEndpoint"}},
{"other group untouched", map[string]any{"ui": map[string]any{"theme": "light"}}, nil},
{"empty map", map[string]any{}, nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := TouchesManagedSettings(tc.in, managed)
sort.Strings(got)
if tc.want == nil && len(got) != 0 {
t.Fatalf("TouchesManagedSettings = %v, want none", got)
}
if tc.want != nil && !reflect.DeepEqual(got, tc.want) {
t.Fatalf("TouchesManagedSettings = %v, want %v", got, tc.want)
}
})
}
}
func TestStripManagedSettings(t *testing.T) {
managed := []string{"models.chat.model", "models.chat.apiKey"}
in := map[string]any{
"models": map[string]any{
"chat": map[string]any{"model": "x", "baseUrl": "https://b"},
"lightweight": map[string]any{"model": "y"},
},
"ui": map[string]any{"theme": "dark"},
}
got := StripManagedSettings(in, managed)
want := map[string]any{
"models": map[string]any{
"chat": map[string]any{"baseUrl": "https://b"},
"lightweight": map[string]any{"model": "y"},
},
"ui": map[string]any{"theme": "dark"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("StripManagedSettings = %v, want %v", got, want)
}
if in["models"].(map[string]any)["chat"].(map[string]any)["model"] != "x" {
t.Fatal("StripManagedSettings 不应修改入参")
}
}
func TestStripManagedSettingsWholeSubtree(t *testing.T) {
managed := []string{"models.chat.model", "models.chat.baseUrl", "models.chat.apiKey", "models.chat.fallbacks"}
in := map[string]any{
"models": map[string]any{"chat": map[string]any{"model": "x", "baseUrl": "b", "apiKey": "k"}},
}
got := StripManagedSettings(in, managed)
if len(got) != 0 {
t.Fatalf("整棵受管子树应被剥除(含空父),got %v", got)
}
}
+224
View File
@@ -0,0 +1,224 @@
package core
import "fmt"
// Surface 契约(Generative UI 提案 U2;依据 r2「U1 结果与 r2 修订」):
// 有版本的呈现产物信封 + 页面规格校验。信封与版本字段由服务端分配/校验,
// 模型只产出 SurfaceSpec;节点级稳定 ID 与来源标注由 UINode 的可选字段承载
// (ui.go),旧卡片/表单路径零影响。
const (
// SurfaceSchemaVersion 是 Surface 信封/规格的结构版本。
SurfaceSchemaVersion = 1
// SurfaceCatalogVersion 是当前认可的组件目录版本(现有 16 类 UINode 词表)。
SurfaceCatalogVersion = "ui-v1"
// SurfaceMaxNodes / SurfaceMaxDepth / SurfaceMaxBytes 是 U1 试验采纳的资源
// 预算(提案 KTD5):超限整页拒绝,不静默裁剪。
SurfaceMaxNodes = 200
SurfaceMaxDepth = 8
SurfaceMaxBytes = 256 * 1024
)
// SurfaceCompat 兼容标识(KTD3):由宿主与已安装组件协商记录,不采信自报值。
type SurfaceCompat struct {
SchemaVersion int `json:"schemaVersion"`
CatalogVersion string `json:"catalogVersion"`
PluginVersion string `json:"pluginVersion,omitempty"`
}
// SurfaceIdentity 身份信封:全部由服务端分配或从当前请求绑定;提交方不能自报。
type SurfaceIdentity struct {
SurfaceID ID `json:"surfaceId"`
Owner string `json:"owner"`
ProjectID ID `json:"projectId,omitempty"`
ConversationID ID `json:"conversationId,omitempty"`
SourceRunID ID `json:"sourceRunId,omitempty"`
PluginID string `json:"pluginId"`
}
// SurfaceSpec 是模型可产出的页面规格(KTD3「页面」层)。
type SurfaceSpec struct {
Title string `json:"title"`
Nodes []UINode `json:"nodes"`
}
// SurfaceSnapshot 是已保存的呈现产物:信封+兼容+版本+页面+回放摘要。
// Revision 单调递增;更新必须携带 BaseRevision,冲突由服务端拒绝(不自动覆盖)。
type SurfaceSnapshot struct {
Identity SurfaceIdentity `json:"identity"`
Compat SurfaceCompat `json:"compat"`
Revision int `json:"revision"`
BaseRevision int `json:"baseRevision,omitempty"`
ContextVersion string `json:"contextVersion,omitempty"`
ActivationEpoch int64 `json:"activationEpoch"`
Spec SurfaceSpec `json:"spec"`
SemanticText string `json:"semanticText,omitempty"`
}
// SurfaceMeta 是列表投影(不携带整页规格)。
type SurfaceMeta struct {
SurfaceID ID `json:"surfaceId"`
Title string `json:"title"`
PluginID string `json:"pluginId"`
Revision int `json:"revision"`
ConversationID ID `json:"conversationId,omitempty"`
UpdatedAt string `json:"updatedAt"`
}
// ValidateSurfaceSpec 按目录纪律校验页面规格,返回违规清单(空即合法)。
// knownSources 为 nil 表示本层不校验来源授权(U3 数据绑定层职责),仅做格式
// 检查;非空时引用/doc-ref 未知来源即为违规。未知类型、非法 props、重复 ID、
// 超限均拒绝(U1 试验校验器的 Go 版本:目录=提示=校验三同源)。
func ValidateSurfaceSpec(spec SurfaceSpec, knownSources map[string]bool) []string {
var errs []string
seen := map[string]bool{}
nodes := 0
var walk func(n UINode, path string, depth int)
walk = func(n UINode, path string, depth int) {
nodes++
if nodes > SurfaceMaxNodes {
errs = append(errs, fmt.Sprintf("%s: 节点数超过 %d", path, SurfaceMaxNodes))
return
}
if depth > SurfaceMaxDepth {
errs = append(errs, fmt.Sprintf("%s: 嵌套深度超过 %d", path, SurfaceMaxDepth))
return
}
if n.ID != "" {
if seen[n.ID] {
errs = append(errs, fmt.Sprintf("%s: 重复节点 id %q", path, n.ID))
}
seen[n.ID] = true
}
for _, s := range n.Source {
if s == "" {
errs = append(errs, path+": source 含空标识")
continue
}
if knownSources != nil && !knownSources[s] {
errs = append(errs, fmt.Sprintf("%s: 引用未知来源 %q", path, s))
}
}
switch n.Type {
case UISection:
for i, c := range n.Children {
walk(c, fmt.Sprintf("%s/child[%d]", path, i), depth+1)
}
case UIList:
if n.Items == nil {
errs = append(errs, path+": list 需要 items")
}
case UIKV:
if n.KV == nil {
errs = append(errs, path+": kv 需要键值对数组")
}
case UIForm:
if len(n.Fields) == 0 {
errs = append(errs, path+": form.fields 需非空")
}
for _, f := range n.Fields {
switch f.Type {
case "text", "textarea", "number", "boolean", "select":
default:
errs = append(errs, fmt.Sprintf("%s: 字段 %q 非法 type %q", path, f.Key, f.Type))
}
if f.Type == "select" && len(f.Options) == 0 {
errs = append(errs, fmt.Sprintf("%s: select 字段 %q 缺 options", path, f.Key))
}
}
case UIMarkdown:
if n.Markdown == "" {
errs = append(errs, path+": markdown 需要正文")
}
case UIDocRef:
if n.DocID == "" {
errs = append(errs, path+": doc-ref 需要 docId")
} else if knownSources != nil && !knownSources[string(n.DocID)] {
errs = append(errs, fmt.Sprintf("%s: doc-ref 引用未知工件 %q", path, n.DocID))
}
case UIActions:
if len(n.Actions) == 0 {
errs = append(errs, path+": actions 需非空")
}
for _, a := range n.Actions {
if a.ID == "" || a.Label == "" {
errs = append(errs, fmt.Sprintf("%s: action 缺 id/label", path))
}
switch a.Style {
case "", "primary", "default", "danger":
default:
errs = append(errs, fmt.Sprintf("%s: action %q 非法 style %q", path, a.ID, a.Style))
}
}
case UIChartLite:
if n.Chart == nil || (n.Chart.Kind != "bar" && n.Chart.Kind != "line") ||
len(n.Chart.Labels) != len(n.Chart.Series) {
errs = append(errs, path+": chart 需要 kind∈{bar,line} 且 labels/series 等长")
}
case UIAvatar:
if n.Avatar == nil || n.Avatar.Name == "" {
errs = append(errs, path+": avatar 需要 name")
}
case UIProgress:
if n.Progress == nil || n.Progress.Value < 0 || n.Progress.Value > 1 {
errs = append(errs, path+": progress.value 需在 0~1")
}
case UICompare:
if n.Compare == nil {
errs = append(errs, path+": compare 需要 left/right")
} else {
for side, sd := range map[string]UICompareSide{"left": n.Compare.Left, "right": n.Compare.Right} {
for i, c := range sd.Nodes {
walk(c, fmt.Sprintf("%s/%s[%d]", path, side, i), depth+1)
}
}
}
case UITree:
if n.Tree == nil || len(n.Tree.Nodes) == 0 {
errs = append(errs, path+": tree.nodes 需非空")
}
case UITable:
if n.Table == nil || len(n.Table.Columns) == 0 {
errs = append(errs, path+": table.columns 需非空")
} else {
for i, r := range n.Table.Rows {
if len(r.Cells) != len(n.Table.Columns) {
errs = append(errs, fmt.Sprintf("%s: 行[%d] cells 与 columns 不等长", path, i))
}
}
}
case UISteps:
if n.Steps == nil || len(n.Steps.Steps) == 0 || n.Steps.Current < 0 || n.Steps.Current >= len(n.Steps.Steps) {
errs = append(errs, path+": steps 需非空且 current 在范围内")
}
case UITabs:
if n.Tabs == nil || len(n.Tabs.Tabs) == 0 {
errs = append(errs, path+": tabs.tabs 需非空")
} else {
for i, tb := range n.Tabs.Tabs {
for j, c := range tb.Nodes {
walk(c, fmt.Sprintf("%s/tab[%d][%d]", path, i, j), depth+1)
}
}
}
case UITimeline:
if n.Timeline == nil || len(n.Timeline.Items) == 0 {
errs = append(errs, path+": timeline.items 需非空")
}
default:
errs = append(errs, fmt.Sprintf("%s: 未知节点类型 %q", path, n.Type))
}
}
if spec.Title == "" {
errs = append(errs, "$: title 必填")
}
if len(spec.Nodes) == 0 {
errs = append(errs, "$: nodes 需非空")
}
for i, n := range spec.Nodes {
walk(n, fmt.Sprintf("$[%d]", i), 1)
}
return errs
}
+137
View File
@@ -0,0 +1,137 @@
package core
import (
"encoding/json"
"strings"
"testing"
)
func validSpec() SurfaceSpec {
return SurfaceSpec{
Title: "三家候选工具选型对比",
Nodes: []UINode{
{Type: UISection, ID: "n1", Title: "一、概览", Children: []UINode{
{Type: UIMarkdown, ID: "n1a", Source: []string{"doc:eval/rc-01/vendor1"}, Markdown: "本期评估三家候选。"},
}},
{Type: UITable, ID: "n2", Source: []string{"doc:eval/rc-01/vendor1", "doc:eval/rc-01/vendor2"}, Table: &UITableSpec{
Columns: []UITableColumn{{Key: "k", Label: "维度"}, {Key: "v", Label: "值"}},
Rows: []UITableRow{{ID: "r1", Cells: [][2]string{{"k", "价格"}, {"v", "¥8/人/天"}}}},
}},
{Type: UISteps, ID: "n3", Steps: &UIStepsSpec{Current: 1, Steps: []UIStep{
{Label: "需求冻结", Status: "done"}, {Label: "契约定义", Status: "active"},
}}},
{Type: UIForm, ID: "n4", Fields: []UIField{
{Key: "years", Label: "归档年限", Type: "select", Options: []string{"3", "5"}, Required: true},
}},
},
}
}
func TestValidateSurfaceSpecValid(t *testing.T) {
if errs := ValidateSurfaceSpec(validSpec(), nil); len(errs) != 0 {
t.Fatalf("合法规格不应有违规: %v", errs)
}
// JSON 往返后仍合法(信封序列化面)。
b, err := json.Marshal(validSpec())
if err != nil {
t.Fatal(err)
}
var back SurfaceSpec
if err := json.Unmarshal(b, &back); err != nil {
t.Fatal(err)
}
if errs := ValidateSurfaceSpec(back, nil); len(errs) != 0 {
t.Fatalf("JSON 往返后不应有违规: %v", errs)
}
}
func TestValidateSurfaceSpecViolations(t *testing.T) {
cases := []struct {
name string
mut func(*SurfaceSpec)
want string
}{
{"未知类型", func(s *SurfaceSpec) { s.Nodes[0].Type = " hologram" }, "未知节点类型"},
{"重复节点 id", func(s *SurfaceSpec) { s.Nodes[1].ID = "n1" }, "重复节点 id"},
{"表格行列不齐", func(s *SurfaceSpec) {
s.Nodes[1].Table.Rows = []UITableRow{{Cells: [][2]string{{"k", "只有一列"}}}}
}, "不等长"},
{"select 缺 options", func(s *SurfaceSpec) {
s.Nodes[3].Fields[0].Options = nil
}, "缺 options"},
{"steps 越界", func(s *SurfaceSpec) { s.Nodes[2].Steps.Current = 5 }, "current 在范围内"},
{"空 title", func(s *SurfaceSpec) { s.Title = "" }, "title 必填"},
{"空 nodes", func(s *SurfaceSpec) { s.Nodes = nil }, "nodes 需非空"},
{"markdown 空", func(s *SurfaceSpec) {
s.Nodes[0].Children[0].Markdown = ""
}, "markdown 需要正文"},
{"progress 越界", func(s *SurfaceSpec) {
s.Nodes = append(s.Nodes, UINode{Type: UIProgress, Progress: &UIProgressSpec{Value: 1.5}})
}, "progress.value"},
{"未知来源", func(s *SurfaceSpec) {}, ""},
}
for _, c := range cases {
s := validSpec()
c.mut(&s)
errs := ValidateSurfaceSpec(s, nil)
if c.want == "" {
continue
}
found := false
for _, e := range errs {
if strings.Contains(e, c.want) {
found = true
break
}
}
if !found {
t.Errorf("%s: 期望包含 %q,得到 %v", c.name, c.want, errs)
}
}
// knownSources 非空时:未知来源拒绝、已知来源放行。
s := validSpec()
errs := ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true, "doc:eval/rc-01/vendor2": true})
if len(errs) != 0 {
t.Fatalf("已知来源全引用不应违规: %v", errs)
}
s.Nodes[0].Children[0].Source = append(s.Nodes[0].Children[0].Source, "doc:eval/rc-99/ghost")
errs = ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true})
if len(errs) == 0 {
t.Fatal("引用未知来源应拒绝")
}
}
func TestValidateSurfaceSpecLimits(t *testing.T) {
deep := UINode{Type: UISection, ID: "d0"}
cur := &deep
for i := 1; i <= SurfaceMaxDepth+1; i++ {
cur.Children = []UINode{{Type: UISection, ID: "d" + string(rune('0'+i%10))}}
cur = &cur.Children[0]
}
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: []UINode{deep}}, nil); len(errs) == 0 {
t.Fatal("超过嵌套深度应拒绝")
}
many := make([]UINode, SurfaceMaxNodes+1)
for i := range many {
many[i] = UINode{Type: UIMarkdown, Markdown: "x"}
}
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: many}, nil); len(errs) == 0 {
t.Fatal("超过节点数应拒绝")
}
}
func TestUINodeBackwardCompat(t *testing.T) {
// 旧卡片路径的 UINode JSON(无 id/source)解出后字段为零值,不受影响。
var n UINode
if err := json.Unmarshal([]byte(`{"type":"list","items":["a"]}`), &n); err != nil {
t.Fatal(err)
}
if n.ID != "" || n.Source != nil || n.Type != UIList || len(n.Items) != 1 {
t.Fatalf("旧负载解引用异常: %+v", n)
}
// 新字段双向序列化。
b, _ := json.Marshal(UINode{Type: UIMarkdown, ID: "m1", Source: []string{"s1"}, Markdown: "x"})
if !strings.Contains(string(b), `"id":"m1"`) || !strings.Contains(string(b), `"source":["s1"]`) {
t.Fatalf("新字段序列化缺失: %s", b)
}
}
+330
View File
@@ -0,0 +1,330 @@
package core
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type DocumentOrigin struct {
ArtifactID ID `json:"artifactId"`
ContentHash string `json:"contentHash"`
ParserRevision string `json:"parserRevision"`
}
type TaskPhase struct {
ID string `json:"id"`
Label string `json:"label"`
ObservedAt time.Time `json:"observedAt,omitempty"`
}
type TaskSourceRef struct {
ID string `json:"id"`
ArtifactID string `json:"artifactId"`
ContentHash string `json:"contentHash"`
ParserRevision string `json:"parserRevision"`
BlockIndex int `json:"blockIndex"`
Label string `json:"label"`
Quote string `json:"quote,omitempty"`
Availability string `json:"availability,omitempty"`
}
type TaskResultRef struct {
ArtifactID ID `json:"artifactId,omitempty"`
SurfaceID ID `json:"surfaceId,omitempty"`
Version string `json:"version,omitempty"`
ContentHash string `json:"contentHash,omitempty"`
Availability string `json:"availability"`
Name string `json:"name,omitempty"`
}
type TaskFinding struct {
ID string `json:"id"`
Text string `json:"text"`
Impact string `json:"impact,omitempty"`
Kind string `json:"kind"`
Status string `json:"status"`
Revision int64 `json:"revision"`
Sources []TaskSourceRef `json:"sources"`
Assumptions string `json:"assumptions,omitempty"`
Corrects string `json:"corrects,omitempty"`
}
type TaskSnapshot struct {
SchemaVersion int `json:"schemaVersion"`
TaskID ID `json:"taskId"`
AttemptID ID `json:"attemptId"`
CardID ID `json:"cardId"`
ProjectID ID `json:"projectId"`
ConversationID ID `json:"conversationId"`
SourceMessageID ID `json:"sourceMessageId,omitempty"`
ParentRunID ID `json:"parentRunId,omitempty"`
RunID ID `json:"runId,omitempty"`
OwnerInstanceID string `json:"ownerInstanceId"`
ExecutionEpoch int64 `json:"executionEpoch,omitempty"`
Revision int64 `json:"revision"`
SourceSeq int64 `json:"sourceSeq"`
StreamScope string `json:"streamScope"`
InputRevision string `json:"inputRevision,omitempty"`
Title string `json:"title"`
Goal string `json:"goal,omitempty"`
ExecutionState string `json:"executionState"`
OutcomeCompleteness string `json:"outcomeCompleteness"`
DeliveryState string `json:"deliveryState"`
Phase TaskPhase `json:"phase"`
Focus string `json:"focus,omitempty"`
Coverage string `json:"coverage,omitempty"`
Remaining string `json:"remaining,omitempty"`
NextStep string `json:"nextStep,omitempty"`
Conclusion string `json:"conclusion,omitempty"`
Findings []TaskFinding `json:"findings,omitempty"`
Limitations []string `json:"limitations,omitempty"`
Inputs []TaskSourceRef `json:"inputs,omitempty"`
Actions []UIAction `json:"actions,omitempty"`
ResultRefs []TaskResultRef `json:"resultRefs,omitempty"`
LastActivityAt time.Time `json:"lastActivityAt,omitempty"`
LastMeaningfulUpdateAt time.Time `json:"lastMeaningfulUpdateAt,omitempty"`
CheckpointKeys map[string]string `json:"checkpointKeys,omitempty"`
CheckpointCount int `json:"checkpointCount,omitempty"`
CheckpointSeq int64 `json:"checkpointSeq,omitempty"`
}
type TaskSourceQuote struct {
SourceID string `json:"sourceId"`
BlockIndex int `json:"blockIndex"`
Quote string `json:"quote"`
}
type WorkCheckpointCandidate struct {
CheckpointID string `json:"checkpointId"`
PhaseID string `json:"phaseId"`
Focus string `json:"focus,omitempty"`
Remaining string `json:"remaining,omitempty"`
NextStep string `json:"nextStep,omitempty"`
Findings []WorkFindingCandidate `json:"findings"`
}
type WorkFindingCandidate struct {
FindingKey string `json:"findingKey"`
Text string `json:"text"`
Impact string `json:"impact,omitempty"`
Kind string `json:"kind"`
Assumptions string `json:"assumptions,omitempty"`
Corrects string `json:"corrects,omitempty"`
Sources []TaskSourceQuote `json:"sources"`
Evidence *WorkFindingEvidence `json:"evidence,omitempty"`
}
// WorkFindingEvidence is a model-supplied candidate, not a persisted proof.
// Backend checks the quoted source text and computes any resulting statement.
type WorkEvidencePremise struct {
Role string `json:"role"`
SourceIndex int `json:"sourceIndex"`
Value *int `json:"value,omitempty"`
}
type WorkFindingEvidence struct {
Mode string `json:"mode"`
Premises []WorkEvidencePremise `json:"premises"`
Result *int `json:"result,omitempty"`
}
func validateWorkFindingEvidence(e *WorkFindingEvidence, sourceCount int) error {
if e == nil {
return nil
}
var required []string
switch e.Mode {
case "direct_relation":
required = []string{"direct_relation"}
if e.Result != nil {
return fmt.Errorf("checkpoint.evidence: direct relation cannot have result")
}
case "difference":
required = []string{"total", "phase"}
if e.Result == nil || *e.Result < 0 || *e.Result > 1000000 {
return fmt.Errorf("checkpoint.evidence: difference requires bounded result")
}
case "unresolved_relation":
required = []string{"total", "phase", "dependent"}
if e.Result != nil {
return fmt.Errorf("checkpoint.evidence: unresolved relation cannot have result")
}
default:
return fmt.Errorf("checkpoint.evidence: unknown mode")
}
if len(e.Premises) != len(required) {
return fmt.Errorf("checkpoint.evidence: wrong premise count")
}
roles := make(map[string]bool, len(required))
for _, premise := range e.Premises {
if roles[premise.Role] || premise.SourceIndex < 0 || premise.SourceIndex >= sourceCount {
return fmt.Errorf("checkpoint.evidence: duplicate role or invalid source index")
}
roles[premise.Role] = true
if e.Mode == "direct_relation" {
if premise.Value != nil {
return fmt.Errorf("checkpoint.evidence: direct relation cannot have value")
}
} else if premise.Value == nil || *premise.Value < 0 || *premise.Value > 1000000 {
return fmt.Errorf("checkpoint.evidence: count premise requires bounded value")
}
}
for _, role := range required {
if !roles[role] {
return fmt.Errorf("checkpoint.evidence: missing premise role")
}
}
return nil
}
// Validate enforces wire limits; owner/source authorization belongs to Backend.
func (s TaskSnapshot) Validate() error {
if s.SchemaVersion != 1 {
return fmt.Errorf("task.schema: unsupported")
}
if s.TaskID == "" || s.AttemptID == "" || s.CardID == "" || s.ProjectID == "" || s.ConversationID == "" || s.OwnerInstanceID == "" {
return fmt.Errorf("task.scope: missing identity")
}
if s.StreamScope != "conversation:"+s.ConversationID {
return fmt.Errorf("task.scope: inconsistent stream")
}
if s.Revision < 1 || s.Revision > 9007199254740991 || s.SourceSeq < 0 || s.SourceSeq > 9007199254740991 || s.ExecutionEpoch < 0 {
return fmt.Errorf("task.revision: unsafe cursor")
}
switch s.ExecutionState {
case "queued", "running", "awaiting_user", "completed", "failed", "interrupted":
default:
return fmt.Errorf("task.state: unknown")
}
if (s.ExecutionState == "running" || s.ExecutionState == "awaiting_user") && s.RunID == "" {
return fmt.Errorf("task.run: active task requires run")
}
switch s.OutcomeCompleteness {
case "none", "partial", "complete":
default:
return fmt.Errorf("task.outcome: unknown")
}
switch s.DeliveryState {
case "pending", "available", "reported":
default:
return fmt.Errorf("task.delivery: unknown")
}
active := 0
for _, f := range s.Findings {
if f.ID == "" || f.Revision < 1 {
return fmt.Errorf("task.finding: missing identity")
}
switch f.Kind {
case "material_record", "analysis", "provisional", "needs_user", "verified":
default:
return fmt.Errorf("task.finding.kind: unknown")
}
switch f.Status {
case "active":
active++
case "corrected", "withdrawn", "superseded":
default:
return fmt.Errorf("task.finding.status: unknown")
}
}
if active > 12 {
return fmt.Errorf("task.findings: too many active")
}
b, err := json.Marshal(s)
if err != nil {
return err
}
if len(b) > 64*1024 {
return fmt.Errorf("task.size: exceeds 64KiB")
}
return nil
}
func (s TaskSnapshot) Terminal() bool {
return s.ExecutionState == "completed" || s.ExecutionState == "failed" || s.ExecutionState == "interrupted"
}
func ValidateWorkCheckpoint(c WorkCheckpointCandidate) error {
if strings.TrimSpace(c.CheckpointID) == "" || len(c.CheckpointID) > 128 {
return fmt.Errorf("checkpoint.id: required or too long")
}
switch c.PhaseID {
case "reading", "crosscheck", "summarizing", "draft-ready":
default:
return fmt.Errorf("checkpoint.phase: unknown")
}
if len(c.Findings) == 0 || len(c.Findings) > 6 {
return fmt.Errorf("checkpoint.findings: require 1..6")
}
keys := map[string]bool{}
for _, f := range c.Findings {
switch f.Kind {
case "material_record", "analysis", "provisional", "needs_user":
default:
return fmt.Errorf("checkpoint.kind: model cannot grant verified status")
}
if f.FindingKey == "" || keys[f.FindingKey] || strings.TrimSpace(f.Text) == "" {
return fmt.Errorf("checkpoint.finding: empty or duplicate key")
}
keys[f.FindingKey] = true
if len(f.Sources) == 0 {
return fmt.Errorf("checkpoint.sources: evidence required")
}
for _, src := range f.Sources {
if src.SourceID == "" || src.BlockIndex < 0 || strings.TrimSpace(src.Quote) == "" {
return fmt.Errorf("checkpoint.source: invalid")
}
}
if err := validateWorkFindingEvidence(f.Evidence, len(f.Sources)); err != nil {
return err
}
if (f.Kind == "analysis" || f.Kind == "provisional") && strings.TrimSpace(f.Assumptions) == "" {
return fmt.Errorf("checkpoint.assumptions: required")
}
}
b, err := json.Marshal(c)
if err != nil {
return err
}
if len(b) > 16*1024 {
return fmt.Errorf("checkpoint.size: exceeds 16KiB")
}
return nil
}
func (s TaskSnapshot) SummaryBody() []UINode {
labels := map[string]string{"queued": "排队中", "running": "进行中", "awaiting_user": "需要你处理", "completed": "已完成", "failed": "执行失败", "interrupted": "已中断"}
var lines []string
for _, f := range s.Findings {
if f.Status == "active" {
line := f.Text
if f.Impact != "" {
line += ";" + f.Impact
}
lines = append(lines, line)
}
}
note := strings.Join(lines, "\n")
if note == "" {
note = TaskSummaryNoFindingsYet
if s.ExecutionState == "queued" {
note = "尚未开始处理,等待执行资源"
}
if s.Terminal() {
note = s.Conclusion
if note == "" {
note = "没有可用阶段成果"
}
}
}
v := TaskSummaryView{Focus: s.Focus, FindingsNote: note, Remaining: s.Remaining, NextStep: s.NextStep, OutcomeLine: s.Conclusion}
var body []UINode
if s.Terminal() {
body = v.FinishedBody(labels[s.ExecutionState], nil)
} else {
body = v.RunningBody(labels[s.ExecutionState], nil)
}
if s.Coverage != "" {
body = append(body, UINode{Type: UIKV, KV: [][2]string{{"提供范围", s.Coverage}}})
}
if len(s.Limitations) > 0 {
body = append(body, UINode{Type: UIList, Title: "范围与限制", Items: s.Limitations})
}
for _, r := range s.ResultRefs {
if r.ArtifactID != "" && r.Availability == "available" {
body = append(body, UINode{Type: UIDocRef, DocID: r.ArtifactID, Title: r.Name})
}
}
return body
}
+147
View File
@@ -0,0 +1,147 @@
package core
import (
"encoding/json"
"strings"
"testing"
)
// Loss of the optional task object makes refresh discard stage findings.
func TestTaskSnapshotCardRoundTrip(t *testing.T) {
raw := []byte(`{"id":"card1","projectId":"p1","conversationId":"c1","kind":"progress","status":"active","task":{"schemaVersion":1,"taskId":"t1","attemptId":"a1","cardId":"card1","revision":4,"executionState":"running","focus":"核对材料","findings":[{"id":"f1","text":"日期不同","kind":"material_record","status":"active","revision":1}]}}`)
var card ActivityCard
if err := json.Unmarshal(raw, &card); err != nil {
t.Fatal(err)
}
encoded, err := json.Marshal(card)
if err != nil {
t.Fatal(err)
}
var restored map[string]json.RawMessage
if err := json.Unmarshal(encoded, &restored); err != nil {
t.Fatal(err)
}
if _, ok := restored["task"]; !ok {
t.Fatal("task findings were lost in card persistence")
}
var task map[string]json.RawMessage
if err := json.Unmarshal(restored["task"], &task); err != nil {
t.Fatal(err)
}
if string(task["revision"]) != "4" || string(task["focus"]) != `"核对材料"` {
t.Fatal("revision/focus did not survive")
}
}
func validTaskSnapshot() TaskSnapshot {
return TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: "card1", ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", StreamScope: "conversation:c1", Title: "整理材料"}
}
func TestTaskSnapshotRejectsUnsafeAndUnboundState(t *testing.T) {
for _, mutate := range []func(*TaskSnapshot){
func(s *TaskSnapshot) { s.TaskID = "" }, func(s *TaskSnapshot) { s.AttemptID = "" },
func(s *TaskSnapshot) { s.Revision = 9007199254740992 }, func(s *TaskSnapshot) { s.SourceSeq = -1 },
func(s *TaskSnapshot) { s.ExecutionState = "imagined" }, func(s *TaskSnapshot) { s.StreamScope = "conversation:other" },
func(s *TaskSnapshot) { s.ExecutionState = "running" },
} {
s := validTaskSnapshot()
mutate(&s)
if s.Validate() == nil {
t.Errorf("invalid snapshot accepted: %+v", s)
}
}
if err := validTaskSnapshot().Validate(); err != nil {
t.Fatal(err)
}
}
func TestWorkCheckpointRejectsVerifiedAndEmptyEvidence(t *testing.T) {
c := WorkCheckpointCandidate{CheckpointID: "cp1", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{FindingKey: "date", Text: "日期不同", Kind: "verified", Sources: []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}}}}
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "kind") {
t.Fatal("model granted verified status", err)
}
c.Findings[0].Kind = "material_record"
c.Findings[0].Sources = nil
if ValidateWorkCheckpoint(c) == nil {
t.Fatal("source-free fact accepted")
}
c.Findings[0].Sources = []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}
if err := ValidateWorkCheckpoint(c); err != nil {
t.Fatal(err)
}
c.Findings[0].Text = strings.Repeat("x", 17000)
if ValidateWorkCheckpoint(c) == nil {
t.Fatal("unbounded candidate accepted")
}
}
func TestWorkCheckpointEvidenceModesAndBounds(t *testing.T) {
base := func() WorkCheckpointCandidate {
return WorkCheckpointCandidate{CheckpointID: "cp-evidence", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{
FindingKey: "phase", Text: "待核对", Kind: "needs_user",
Sources: []TaskSourceQuote{{SourceID: "total", Quote: "总12项"}, {SourceID: "phase", Quote: "一期8项"}, {SourceID: "dependent", Quote: "其中3项依赖接口"}},
}}}
}
n := func(v int) *int { return &v }
total := WorkEvidencePremise{Role: "total", SourceIndex: 0, Value: n(12)}
phase := WorkEvidencePremise{Role: "phase", SourceIndex: 1, Value: n(8)}
dependent := WorkEvidencePremise{Role: "dependent", SourceIndex: 2, Value: n(3)}
for _, tc := range []struct {
name string
evidence WorkFindingEvidence
valid bool
}{
{"direct", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}, true},
{"difference", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}, true},
{"unresolved", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}}, true},
{"unknown mode", WorkFindingEvidence{Mode: "prediction", Premises: []WorkEvidencePremise{total, phase}}, false},
{"missing result", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}}, false},
{"extra result", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}, Result: n(3)}, false},
{"duplicate role", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, total}, Result: n(4)}, false},
{"missing premise", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase}}, false},
{"negative index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: -1}}}, false},
{"foreign index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 3}}}, false},
{"negative value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(-1)}, phase}, Result: n(4)}, false},
{"large value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(1000001)}, phase}, Result: n(4)}, false},
{"direct value", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0, Value: n(1)}}}, false},
{"missing value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0}, phase}, Result: n(4)}, false},
} {
t.Run(tc.name, func(t *testing.T) {
c := base()
c.Findings[0].Evidence = &tc.evidence
err := ValidateWorkCheckpoint(c)
if (err == nil) != tc.valid {
t.Fatalf("valid=%v err=%v", tc.valid, err)
}
})
}
c := base()
c.Findings[0].Evidence = &WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}
c.Findings[0].Text = strings.Repeat("字", 5500)
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "16KiB") {
t.Fatal("evidence bypassed checkpoint size cap", err)
}
}
func TestTaskSnapshotBodyPreservesLimitsAndHonestStates(t *testing.T) {
s := validTaskSnapshot()
s.Remaining = "图片内容未纳入"
s.ExecutionState = "failed"
s.Conclusion = "处理失败,已有发现可查看"
s.Findings = []TaskFinding{{ID: "f1", Text: "金额缺失", Kind: "material_record", Status: "active", Revision: 1}}
raw, _ := json.Marshal(s.SummaryBody())
for _, want := range []string{"执行失败", "图片内容未纳入", "金额缺失", "处理失败"} {
if !strings.Contains(string(raw), want) {
t.Errorf("lost critical summary %s", want)
}
}
}
func TestTaskSnapshotUnstartedAndFinishedDoNotClaimReading(t *testing.T) {
for _, state := range []string{"queued", "completed", "interrupted"} {
s := validTaskSnapshot()
s.ExecutionState = state
s.Conclusion = "处理已结束"
b, _ := json.Marshal(s.SummaryBody())
if strings.Contains(string(b), "正在读取") {
t.Errorf("%s falsely claims active reading", state)
}
}
}
+100
View File
@@ -0,0 +1,100 @@
package core
// TaskSummaryView 是 U1 写入 ActivityCard.Body 的权威语义草图。
// 完整 TaskSnapshot(findings[]、revision、streamScope)留待 U2;本类型只冻结
// 运行中/终态的精炼骨架字段与 Body KV 键名,供 Backend 生成、旧客户端回退阅读。
type TaskSummaryView struct {
Title string // 业务动宾短标题(通常已在 ActivityCard.Title)
Focus string // 正在做什么,≤2 行
FindingsNote string // U1:无成果时诚实占位;结构化 findings 属 U3
Remaining string
NextStep string
OutcomeLine string // 完成后的结论一行;运行中可空
}
// Body KV 键名(U1 冻结;用户默认卡身可见,勿改成内部材料键)。
const (
TaskSummaryKVStatus = "状态"
TaskSummaryKVFocus = "正在做"
TaskSummaryKVFindings = "当前发现"
TaskSummaryKVRemaining = "剩余"
TaskSummaryKVNextStep = "下一步"
)
// 状态行用户文案(与执行态映射;排队/进行中/完成/失败)。
const (
TaskSummaryStatusQueued = "排队中"
TaskSummaryStatusRunning = "进行中"
TaskSummaryStatusDone = "已完成"
TaskSummaryStatusFailed = "执行失败"
)
// 无阶段成果时的诚实占位(R03-a);不得编造业务发现。
const TaskSummaryNoFindingsYet = "尚无阶段结论,正在读取材料"
// RunningBody 生成运行中(含排队)进度卡 Body:Markdown 占位 + 固定骨架 KV。
// extraKV(材料键等)保留在同一 UIKV 节点,供系统去重,不充当用户进度正文。
func (v TaskSummaryView) RunningBody(statusLabel string, extraKV [][2]string) []UINode {
focus := stringsOr(v.Focus, "处理已登记工作")
findings := stringsOr(v.FindingsNote, TaskSummaryNoFindingsYet)
remaining := stringsOr(v.Remaining, "待任务进展后更新")
next := stringsOr(v.NextStep, "继续处理已登记工作")
if statusLabel == "" {
statusLabel = TaskSummaryStatusRunning
}
kv := append(append([][2]string(nil), extraKV...),
[2]string{TaskSummaryKVStatus, statusLabel},
[2]string{TaskSummaryKVFocus, focus},
[2]string{TaskSummaryKVFindings, findings},
[2]string{TaskSummaryKVRemaining, remaining},
[2]string{TaskSummaryKVNextStep, next},
)
return []UINode{
{Type: UIMarkdown, Markdown: findings},
{Type: UIKV, KV: kv},
}
}
// FinishedBody 生成终态卡 Body:结论 Markdown + 骨架 KV(状态为已完成或执行失败)。
func (v TaskSummaryView) FinishedBody(statusLabel string, extraKV [][2]string) []UINode {
outcome := stringsOr(v.OutcomeLine, "(无结果摘要)")
findings := stringsOr(v.FindingsNote, outcome)
if statusLabel == "" {
statusLabel = TaskSummaryStatusDone
}
kv := append(append([][2]string(nil), extraKV...),
[2]string{TaskSummaryKVStatus, statusLabel},
[2]string{TaskSummaryKVFocus, stringsOr(v.Focus, "")},
[2]string{TaskSummaryKVFindings, findings},
[2]string{TaskSummaryKVRemaining, stringsOr(v.Remaining, "")},
[2]string{TaskSummaryKVNextStep, stringsOr(v.NextStep, "")},
)
// 去掉空值骨架行,避免完成卡出现空白「正在做」。
compact := make([][2]string, 0, len(kv))
for _, pair := range kv {
if pair[1] == "" && (pair[0] == TaskSummaryKVFocus || pair[0] == TaskSummaryKVRemaining || pair[0] == TaskSummaryKVNextStep) {
continue
}
compact = append(compact, pair)
}
return []UINode{
{Type: UIMarkdown, Markdown: outcome},
{Type: UIKV, KV: compact},
}
}
// CardDisplaysFailed 判断卡是否应按失败展示:含 CardFailed,以及旧客户端
// done + danger 兼容映射(U1 出口白名单)。
func CardDisplaysFailed(status CardStatus, tone string) bool {
if status == CardFailed {
return true
}
return status == CardDone && tone == "danger"
}
func stringsOr(s, fallback string) string {
if s == "" {
return fallback
}
return s
}
+90
View File
@@ -0,0 +1,90 @@
package core
import (
"encoding/json"
"strings"
"testing"
)
func TestTaskSummaryRunningBodyOmitsRawTaskAndUsesHonestPlaceholder(t *testing.T) {
v := TaskSummaryView{Focus: "核对需求与会议决定"}
body := v.RunningBody(TaskSummaryStatusQueued, [][2]string{{"材料键", "mk_x"}})
if len(body) != 2 || body[0].Type != UIMarkdown || body[1].Type != UIKV {
t.Fatalf("body shape = %+v", body)
}
if body[0].Markdown != TaskSummaryNoFindingsYet {
t.Fatalf("markdown = %q, want honest placeholder", body[0].Markdown)
}
rawTask := "逐页解析上传材料并对照验收条件" + strings.Repeat("x", 200)
if strings.Contains(body[0].Markdown, rawTask) {
t.Fatal("running body must not embed raw task text")
}
got := map[string]string{}
for _, kv := range body[1].KV {
got[kv[0]] = kv[1]
}
for _, key := range []string{TaskSummaryKVStatus, TaskSummaryKVFocus, TaskSummaryKVFindings, TaskSummaryKVRemaining, TaskSummaryKVNextStep, "材料键"} {
if _, ok := got[key]; !ok {
t.Fatalf("missing kv %q in %v", key, got)
}
}
if got[TaskSummaryKVStatus] != TaskSummaryStatusQueued {
t.Fatalf("status = %q", got[TaskSummaryKVStatus])
}
if got[TaskSummaryKVFindings] != TaskSummaryNoFindingsYet {
t.Fatalf("findings = %q", got[TaskSummaryKVFindings])
}
}
func TestTaskSummaryFinishedBodyFailureLabel(t *testing.T) {
v := TaskSummaryView{OutcomeLine: "执行失败:boom"}
body := v.FinishedBody(TaskSummaryStatusFailed, nil)
if body[0].Markdown != "执行失败:boom" {
t.Fatalf("outcome = %q", body[0].Markdown)
}
found := false
for _, kv := range body[1].KV {
if kv[0] == TaskSummaryKVStatus {
found = true
if kv[1] != TaskSummaryStatusFailed {
t.Fatalf("status = %q, want %q", kv[1], TaskSummaryStatusFailed)
}
}
}
if !found {
t.Fatal("missing status kv")
}
}
func TestTaskSummaryViewJSONRoundTrip(t *testing.T) {
in := TaskSummaryView{
Title: "核对三份材料", Focus: "核对需求", FindingsNote: TaskSummaryNoFindingsYet,
Remaining: "评审意见", NextStep: "整理冲突", OutcomeLine: "",
}
b, err := json.Marshal(in)
if err != nil {
t.Fatal(err)
}
var out TaskSummaryView
if err := json.Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
if out != in {
t.Fatalf("round-trip = %+v, want %+v", out, in)
}
}
func TestCardDisplaysFailed(t *testing.T) {
if !CardDisplaysFailed(CardFailed, "danger") {
t.Fatal("CardFailed should display failed")
}
if !CardDisplaysFailed(CardDone, "danger") {
t.Fatal("legacy done+danger should display failed")
}
if CardDisplaysFailed(CardDone, "ok") {
t.Fatal("done+ok must not display failed")
}
if CardDisplaysFailed(CardActive, "info") {
t.Fatal("active must not display failed")
}
}
+91 -3
View File
@@ -98,7 +98,8 @@ export type StreamEventType =
| 'agent.spawn' | 'agent.progress' | 'agent.result'
| 'artifact.created' | 'artifact.updated'
| 'run.completed' | 'run.failed' | 'error'
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved';
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved'
| 'surface.updated';
export interface StreamEvent<P = unknown> {
seq: number; runId?: ID; conversationId: ID; ts: string; v: number;
@@ -148,8 +149,23 @@ export interface ActivityCard {
id: ID; projectId: ID; conversationId?: ID;
sourceRunId?: ID; anchorMessageId?: ID;
kind: CardKind; title: string; status: CardStatus; tone?: string;
task?: TaskSnapshot;
body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[];
pinned?: boolean; createdAt: string; updatedAt: string;
pinned?: boolean; createdAt: string; updatedAt: string; work?: WorkCard;
}
export interface WorkCard {
result?: string; resultVersion: number; readVersion: number; completedAt?: string;
decision?: WorkDecision;
}
export interface WorkDecision {
runId?: ID;
mode: 'single' | 'multiple'; state: 'open' | 'deferred' | 'dispatching' | 'submitted';
options?: { id: string; label: string }[];
selectedIds?: string[]; freeText?: string; respondedAt?: string;
}
export interface WorkCardAction {
resultVersion: number; selectedIds?: string[]; freeText?: string;
}
export interface CardPayload { card: ActivityCard }
@@ -184,6 +200,9 @@ export type UINodeType =
export interface UINode {
type: UINodeType;
/** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */
id?: string;
source?: string[];
title?: string;
items?: string[];
kv?: [string, string][];
@@ -265,6 +284,8 @@ export interface SettingDesc {
default?: unknown; options?: string[];
effect: 'hot' | 'new-run' | 'restart';
secret?: boolean;
/** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */
scope?: 'system';
}
// ---- 插件 ----
@@ -330,7 +351,7 @@ export interface PluginPageDecl {
export interface ParsedDocument {
meta: {
filename: string; mime?: string; parser: string; confidence?: number;
filename: string; mime?: string; parser: string; confidence?: number; origin?: DocumentOrigin;
crossCheck?: { with: string; diffBlocks: number; verdict: 'agree' | 'differ' };
parsedAt: string;
};
@@ -351,3 +372,70 @@ export interface UploadInfo {
artifactId: ID; filename: string; mime?: string; size: number;
parsed?: ParsedDocument;
}
// ---- Generative UI Surface(提案 U2/U4;与 Go surface.go 同步)----
export interface SurfaceCompat {
schemaVersion: number;
catalogVersion: string;
pluginVersion?: string;
}
export interface SurfaceIdentity {
surfaceId: ID;
owner: string;
projectId?: ID;
conversationId?: ID;
sourceRunId?: ID;
pluginId: string;
}
export interface SurfaceSpec {
title: string;
nodes: UINode[];
}
export interface SurfaceSnapshot {
identity: SurfaceIdentity;
compat: SurfaceCompat;
revision: number;
baseRevision?: number;
contextVersion?: string;
activationEpoch: number;
spec: SurfaceSpec;
semanticText?: string;
}
export interface SurfaceMeta {
surfaceId: ID;
title: string;
pluginId: string;
revision: number;
conversationId?: ID;
updatedAt: string;
}
// Persistent work projection; host-owned identities and revisions.
export interface DocumentOrigin { artifactId: ID; contentHash: string; parserRevision: string }
export interface TaskPhase { id: string; label: string; observedAt?: string }
export interface TaskSourceRef { id: string; artifactId: ID; contentHash: string; parserRevision: string; blockIndex: number; label: string; quote?: string; availability?: string }
export interface TaskResultRef { artifactId?: ID; surfaceId?: ID; version?: string; contentHash?: string; availability: string; name?: string }
export interface TaskFinding { id: string; text: string; impact?: string; kind: string; status: string; revision: number; sources: TaskSourceRef[]; assumptions?: string; corrects?: string }
export interface TaskSnapshot {
inputs?: TaskSourceRef[]
limitations?: string[]
schemaVersion: number; taskId: ID; attemptId: ID; cardId: ID; projectId: ID; conversationId: ID;
sourceMessageId?: ID; parentRunId?: ID; runId?: ID; ownerInstanceId: string; executionEpoch?: number;
revision: number; sourceSeq: number; streamScope: string; inputRevision?: string; title: string; goal?: string;
executionState: 'queued'|'running'|'awaiting_user'|'completed'|'failed'|'interrupted'; outcomeCompleteness: 'none'|'partial'|'complete'; deliveryState: 'pending'|'available'|'reported'; phase: TaskPhase;
focus?: string; coverage?: string; remaining?: string; nextStep?: string; conclusion?: string;
findings?: TaskFinding[]; actions?: UIAction[]; resultRefs?: TaskResultRef[]; lastActivityAt?: string; lastMeaningfulUpdateAt?: string;
checkpointKeys?: Record<string,string>; checkpointCount?: number; checkpointSeq?: number;
}
export interface TaskSourceQuote { sourceId: string; blockIndex: number; quote: string }
interface WorkEvidencePremise { role: "total" | "phase" | "dependent" | "direct_relation"; sourceIndex: number; value?: number }
interface WorkFindingEvidence { mode: "direct_relation" | "difference" | "unresolved_relation"; premises: WorkEvidencePremise[]; result?: number }
export interface WorkFindingCandidate { findingKey: string; text: string; impact?: string; kind: string; assumptions?: string; corrects?: string; sources: TaskSourceQuote[]; evidence?: WorkFindingEvidence }
export interface WorkCheckpointCandidate { checkpointId: string; phaseId: string; focus?: string; remaining?: string; nextStep?: string; findings: WorkFindingCandidate[] }
export interface UIAction { id: string; label: string; style?: string; command?: string }
+6 -1
View File
@@ -28,7 +28,12 @@ const (
)
type UINode struct {
Type UINodeType `json:"type"`
Type UINodeType `json:"type"`
// ID / Source 是 Generative UI Surface 层的节点级标注(提案 U2):ID 整页
// 唯一且稳定(交互与修订对位);Source 为支撑该节点事实内容的来源标识,
// 授权校验在数据绑定层(U3)。均为可选:旧卡片/表单路径不填不受影响。
ID string `json:"id,omitempty"`
Source []string `json:"source,omitempty"`
Title string `json:"title,omitempty"`
Items []string `json:"items,omitempty"` // list
KV [][2]string `json:"kv,omitempty"` // kv