@cr1992/agentkit 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/bin/cli.mjs +229 -2
- package/core/contract-scaffold.mjs +37 -0
- package/core/contract-substance.mjs +134 -0
- package/core/ledger-pointer.mjs +210 -0
- package/docs/orchestrate/contract-interview.md +175 -0
- package/docs/orchestrate/orchestration-runtime.md +123 -5
- package/docs/verify/input-preparation.md +32 -1
- package/docs/verify/verification-protocol.md +3 -0
- package/docs/worktree/review-lifecycle.md +7 -0
- package/docs/worktree/spawn-and-stack.md +20 -0
- package/domains/loop/loop-runtime.mjs +11 -3
- package/domains/orchestrate/contract-interview.mjs +448 -0
- package/domains/orchestrate/contract-tool.mjs +78 -6
- package/domains/orchestrate/orchestration-ledger.mjs +216 -24
- package/domains/orchestrate/orchestration-metadata.mjs +17 -1
- package/domains/verify/verification-runtime.mjs +37 -18
- package/domains/worktree/worktree-doctor.mjs +8 -2
- package/domains/worktree/worktree-lifecycle.mjs +23 -1
- package/domains/worktree/worktree-mgr.mjs +8 -2
- package/manage-worktrees/SKILL.md +10 -10
- package/orchestrate-subagents/SKILL.md +21 -17
- package/package.json +3 -3
- package/run-agent-verify-loop/SKILL.md +4 -4
- package/schemas/ledger-pointer-v1.schema.json +16 -0
- package/schemas/orchestration-ledger-v1.schema.json +18 -1
- package/shell-manifest.json +1 -1
- package/verify-agent-output/SKILL.md +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,130 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.2.0 - 2026-09-20
|
|
8
|
+
|
|
9
|
+
- 升级影响:本批改动多数触及 `core/`、`schemas/`、各域 `domains/<域>/` 或 `docs/<域>/`,升级后
|
|
10
|
+
所有升级前 init 的在途 ledger、loop、verify run 都会判定为 `skill_drift`,需要先收尾在途任务
|
|
11
|
+
再升级。升级后 drift 的 ledger 可用 `agentkit orchestrate ledger close --abandon --reason <text>`
|
|
12
|
+
记为放弃;drift 但未放弃、未进入终态的 ledger 指针会一直保留,等待 `close --abandon` 或
|
|
13
|
+
re-contract。升级前 init 的 ledger 没有仓级指针,`agentkit status` 找不到它们,仍需手传
|
|
14
|
+
`--ledger <ledger 目录>`。
|
|
15
|
+
- 新增 `agentkit contract interview-ask / interview-answer / interview-freeze`:一台由实质性判据的
|
|
16
|
+
拒绝清单驱动提问、由自己的三条完成判据决定何时冻结的状态机。命令本身不调用任何模型,只出题、
|
|
17
|
+
校验回填、冻结;选项由调用它的模型填,选哪个由用户定。提问顺序固定为
|
|
18
|
+
`permissions → objective → acceptance → scope.include → scope.exclude → stop_conditions`,
|
|
19
|
+
`permissions` 必问且最先问——`scope.exclude` / `stop_conditions` 的判据只在 write 模式下生效,
|
|
20
|
+
而 scaffold 默认 `read_only`,不先问权限,写任务会一路走完却从没被问到边界和刹车。
|
|
21
|
+
轮次与作答记录写在契约草稿自己的 `extensions.interview` 里,并进入 `contract_digest`;上限 3 轮。
|
|
22
|
+
`permissions` / `objective` / `acceptance` 必须有 `source: "user"` 的作答记录,不接受 assumption——
|
|
23
|
+
否则模型可以先把 `objective` 写进草稿,再记一条"用户说都行",在没有任何用户选择的情况下把契约冻掉。
|
|
24
|
+
只有 `scope.include` / `scope.exclude` / `stop_conditions` 可以 `deferred`,`assumed` 会原样写进对应字段
|
|
25
|
+
(执行方读的是契约字段而不是 `extensions`),`assumed` 允许是空数组表示"没有"。
|
|
26
|
+
用法见 `agentkit docs orchestrate contract-interview`。
|
|
27
|
+
- 新增 `agentkit contract scaffold` 别名。契约骨架下沉到 `core/contract-scaffold.mjs`,
|
|
28
|
+
与 `agentkit verify scaffold --kind contract` 同源,两边只在 `skill_set` 上分叉
|
|
29
|
+
(各自冻结自己域的 content digest)。
|
|
30
|
+
- 契约、profile 的创建入口(`contract validate`、`ledger init`、`verify preflight / init / prepare-run`、
|
|
31
|
+
`loop init`)新增实质性检查,原样照抄 `verify scaffold` 生成的占位契约或 profile 一律拒绝,
|
|
32
|
+
报错给出字段路径与当前值但不给可以照抄的合规值;`prepare-run` 遇到该拒绝时紧凑输出也会带上
|
|
33
|
+
`errors`。续跑与恢复入口(`ledger add-node`、`verify record-review` / `validate`、
|
|
34
|
+
`loop adopt-root` / `record-embedded-review` / `validate`)不重判实质性,行为不变。
|
|
35
|
+
同一批判据里,每条 `acceptance[].contract_item_id` 必须至少被一条 `l1_review` 引用,不分权限
|
|
36
|
+
模式,只在同时拿到契约与 profile 的创建入口执行;`permissions.mode` 为 `write` 且
|
|
37
|
+
`scope.exclude` / `stop_conditions` 为空只降级为 warning,不拒绝、退出码不变。`warnings` 键仅
|
|
38
|
+
非空时出现:见 `contract validate`、`ledger init`、`verify init`、`loop init` 的返回值,以及
|
|
39
|
+
`preflight` 报告(该键恒在,可能是空数组);`prepare-run` 把它放进完整报告的
|
|
40
|
+
`preflight.warnings`,默认紧凑输出不带,取证需加 `--verbose`。`ledger / verify / loop doctor`
|
|
41
|
+
新增 `substance_warnings`:恒在,把手上能执行的判据整体降级为 warning,不进 `findings`,
|
|
42
|
+
不改变 `healthy`。
|
|
43
|
+
- 声明 `extensions.verification.provider === 'verify-agent-output'` 时,`completion_ready` 要求每个
|
|
44
|
+
required 的实现节点(`verification.requirement !== 'not_applicable'`)要么自身是已通过的
|
|
45
|
+
`independent_evidence` 节点,要么沿 `dependency` / `barrier` 边可达一个已通过、
|
|
46
|
+
`artifact_scope: integration_candidate` 的 `independent_evidence` 节点;不满足则拒绝视为完成。
|
|
47
|
+
没有任何 required 节点的空 ledger,`completion_ready` 恒为 false。`status` / `inspect` 的
|
|
48
|
+
`summary` 新增三份名单:`uncovered_implementation_nodes`(声明 provider 但未被覆盖的实现节点)、
|
|
49
|
+
`non_required_implementation_nodes`、`nodes_without_independent_evidence`(未声明 provider 时,
|
|
50
|
+
列出尚未经独立验证的实现节点)。
|
|
51
|
+
- `orchestrate ledger close` 要求 `status` 判定的 `completion_ready` 为 true,不满足时非零退出,
|
|
52
|
+
逐条列出未满足的条件(同一份文本同时出现在 `status.summary.unmet_completion_conditions`);
|
|
53
|
+
`close --abandon --reason <text>` 记为放弃,`reason` 必填且非空,不带 `--abandon` 的 `--reason`
|
|
54
|
+
会被拒绝。事件链新增终态事件 `closed` / `abandoned`,快照新增可选字段 `lifecycle`
|
|
55
|
+
(终态种类、时间、reason、写入时的 runtime 摘要与 drift 状态),旧快照不含该字段仍然合法。
|
|
56
|
+
`close` 返回体新增 `pointer` 字段(仓级指针的写入/删除结果)。ledger 进入终态后任务图冻结,
|
|
57
|
+
`add-node / add-edge / dispatch-record / update / attach / batch-init / batch-record / close`
|
|
58
|
+
等修改命令一律拒绝,只读命令与 `record-reflection / propose-improvement / rebuild` 仍可用;
|
|
59
|
+
`status` / `inspect` 新增输出键 `skill_drift`,drift 下 `close --abandon` 是唯一仍能写入的路径,
|
|
60
|
+
其余修改命令照常拒绝。
|
|
61
|
+
- ledger 状态发现。`orchestrate ledger init` 在 `contract.environment.repository` 所属仓库的
|
|
62
|
+
git common dir 下写仓级指针 `<git-common-dir>/agentkit/ledgers/<ledger_id>.json`(第 18 份
|
|
63
|
+
canonical schema `ledger-pointer-v1`),`close`(含 `--abandon`)成功后删除它。**指针不是真源**:
|
|
64
|
+
只回答"ledger 在哪",一切判定仍回读 state root 的事件链;指针只写在 `.git/` 下,不进版本控制。
|
|
65
|
+
写/删失败一律降级为 warning,不让 `init` 失败后留下半个 ledger。
|
|
66
|
+
- 新增顶层 `agentkit status [--json]`:从 cwd 找 git common dir,读全部指针、回读各 state root,
|
|
67
|
+
筛出未终态的 ledger,单屏给出当前阶段、活跃 worktree、阻塞项、未覆盖节点与下一步命令。多个时
|
|
68
|
+
全部列出不猜测;受管 worktree 里用 record 的 `ledger` 字段收窄;`skill_drift` 的单独成组,
|
|
69
|
+
下一步只给 `close --abandon` 与 re-contract。
|
|
70
|
+
- `orchestrate ledger doctor` 新增 `--repository <path>` 档位(返回体新增 `mode: "repository"`,
|
|
71
|
+
单 ledger 档位为 `mode: "ledger"`),扫描并分类该仓的全部指针;回收是显式的
|
|
72
|
+
`orchestrate ledger reclaim-pointers --repository <path>`,不藏在只读的 `doctor` 里。
|
|
73
|
+
**drift 但未进入终态的 ledger 指针一律保留**:它还需要有人来 `close --abandon` 或 re-contract。
|
|
74
|
+
- `worktree spawn` 新增可选 `--ledger <id>`,写进 record 的 `ledger` 字段供 `agentkit status` 收窄;
|
|
75
|
+
worktree 域只校验 id 格式,格式规则下沉到 `core/ledger-pointer.mjs`,两个域之间不互相 import。
|
|
76
|
+
- 编排拦截点(缺 `verification_ref`、`verification_ref` 类型不符、`completion_ready` 为 false 时
|
|
77
|
+
`close`、drift 下执行修改命令)的报错文案改写为"违规原因 + 合规做法":点出字段路径或节点 id、
|
|
78
|
+
当前值、要求的性质与可执行的命令/flag 名,取值一律用占位符,不给可以照抄的合规值;不新增拦截点,
|
|
79
|
+
不改变任何命令的放行/拒绝行为、`error` 码或退出码。
|
|
80
|
+
- `verify-agent-output` 的证伪任务提示词新增一条:已成立的 finding 涉及结构调整时,`expected`
|
|
81
|
+
必须写出具名重构手法且只针对该 finding 的 `contract_item_id`,给不出具名手法的结构评价不写入
|
|
82
|
+
findings;不扩大取证范围,不新增 schema 字段。
|
|
83
|
+
- 架构文档 `docs/architecture/skill-system-architecture.md` 只保留与真源不重复的内容,其余段落
|
|
84
|
+
改写为指向 `domains/<域>/`、`docs/<域>/`、四份 `SKILL.md`、`schemas/`、`core/digest.mjs` 等真源的
|
|
85
|
+
指针(累计从 114,315 字节精简到 49,877 字节);`tests/architecture-consistency.test.mjs` 的判据
|
|
86
|
+
从"固定字符串存在"改为"指针能实际解析且覆盖当前 `domains/*` 与 `docs/<域>/` 目录"。四份
|
|
87
|
+
`SKILL.md` 补齐指向 `agentkit status`、`orchestrate ledger close`、`contract interview-*`、
|
|
88
|
+
`worktree spawn --ledger` 的指针;`tests/documentation.test.mjs` 新增孤儿文档检查,
|
|
89
|
+
`docs/<域>/` 下每个主题必须被至少一个 `SKILL.md` 以 `agentkit docs <域> <主题>` 的形式指到。
|
|
90
|
+
- 四个 Skill 的正文字符预算按当前实测值重新标定并去掉总量卡口里不再起约束作用的重复上限;
|
|
91
|
+
`description` 合计上限改为按 Skill 数量推导。删除 `approximate_tokens`
|
|
92
|
+
(原按英文字符数估算 token,在中英混排文本上系统性偏差较大)。`tests/skill-budgets.mjs`
|
|
93
|
+
作为预算数字的共享真源,供架构文档反查比对。
|
|
94
|
+
- 新增手动触发的发版流水线(`.github/workflows/release.yml`):`verify`(限定从 `main` 发布、
|
|
95
|
+
版本号须与 `package.json` 一致、该版本不得已在 registry 上、`npm test` 与 tarball 干净安装验证,
|
|
96
|
+
Node 22/24 双版本矩阵)→ `publish`(`npm publish --provenance`)→ `attest`(从 registry 反装并跑
|
|
97
|
+
`agentkit doctor` 确认版本一致,同样跑 Node 22/24 双版本矩阵)→ `tag-and-release`(同一 commit
|
|
98
|
+
对同一 semver 打 tag、建 GitHub Release,正文从本文件对应小节程序化提取,缺小节即失败)。
|
|
99
|
+
`verify` 与 `attest` 两段的发布证据(`npm pack --dry-run` 文件清单、`agentkit doctor` 输出,
|
|
100
|
+
均带上 commit SHA 与版本号)按矩阵各存为一份 GitHub Actions 产物。需要仓库 secret `NPM_TOKEN`。
|
|
101
|
+
- 新增 `evals/protocol-routing/`:协议路由评测 harness 与 11 条用例,用于衡量 agent 在给定提示下
|
|
102
|
+
是否按预期路由到只读、`agentkit <域> <动词>` 或写操作。支持预录 JSONL 回放(接入 `npm test`)与
|
|
103
|
+
两种真实评测运行方式——GitHub Actions + `ANTHROPIC_API_KEY`,或本机一次性容器
|
|
104
|
+
(`evals/protocol-routing/container/`)+ `claude setup-token` 生成的订阅 token;容器以只读挂载
|
|
105
|
+
仓库、非 root 用户、`--cap-drop ALL` 等收紧运行。均不进发布包,不在任何 Skill 内容摘要范围内。
|
|
106
|
+
- 本批各域新增用户可见能力,同步各升一个 minor:`orchestrate-subagents` 的
|
|
107
|
+
`ORCHESTRATION_RUNTIME_VERSION` 1.7.0 → 1.8.0(`ledger close`、`reclaim-pointers`、`status` 新键);
|
|
108
|
+
`contract-tool` 的 `capabilities().runtime_version` 1.1.0 → 1.2.0(`scaffold`、`interview-*`);
|
|
109
|
+
`verify-agent-output` 的 `RUNTIME_VERSION` 1.3.0 → 1.4.0(实质性检查、`warnings` /
|
|
110
|
+
`substance_warnings`);`run-agent-verify-loop` 的 `RUNTIME_VERSION` 1.0.0 → 1.1.0(同上);
|
|
111
|
+
`manage-worktrees` 的 `runtime_version` 1.4.0 → 1.5.0(`spawn --ledger`)。
|
|
112
|
+
`orchestration-reflection`、`worker-capability-preflight`、`review-budget` 本批未改行为,
|
|
113
|
+
runtime version 不动。
|
|
114
|
+
|
|
115
|
+
## 1.1.1 - 2026-09-08
|
|
116
|
+
|
|
117
|
+
- 修复 `doctor` 对已回收 record 仍生成需要活树才能收敛的 metadata finding。`stack_parent`、
|
|
118
|
+
`history_operation` 和 `review_refresh` 三个分支缺少 `worktree_state` 过滤,对已删除目录报
|
|
119
|
+
`STACK_PARENT_ADVANCED` 等提示,其补救动作(managed rebase/retarget、`rebase --continue`、
|
|
120
|
+
`refresh-review`)在回收后都无法执行,噪声永远清不掉;其中 error 级的还会按「任何 error 都暂停
|
|
121
|
+
`spawn/adopt`」把后续派工钉死。生命周期与 watcher 两个收集器早已是这个口径,本次补齐 metadata。
|
|
122
|
+
- `manage-worktrees` 补齐 `--no-watch` 的使用判据:只用于确定不会合入,或武装失败原因为永久性
|
|
123
|
+
(否则 pending 会积一条清不掉的 `AUTO_RECLAIM_NOT_ARMED`)。明确禁止因 watcher 活不过会话或
|
|
124
|
+
为压后台进程数而关闭——前者是 `watch-service` 的职责,后者不成立(`disabled` 持久且不重试)。
|
|
125
|
+
- `watch-service status` 进入强制流程表的「恢复/盘点」行,不再只以表外散文形式存在。
|
|
126
|
+
- 四个 SKILL.md 对 `docs/<域>/` 的引用从相对链接改为 `agentkit docs <域> <主题>` 命令。宿主把 Skill
|
|
127
|
+
基目录报成安装路径(常为软链),Read 工具按词法折叠 `..`,原 `../docs/...` 链接在安装态全部不可达,
|
|
128
|
+
agent 只剩摘要表。`validate-skills` 新增安装态检查:SKILL.md 相对链接越出 Skill 目录即报错,
|
|
129
|
+
`agentkit docs` 引用的主题必须真实存在。
|
|
130
|
+
|
|
7
131
|
## 1.1.0 - 2026-09-07
|
|
8
132
|
|
|
9
133
|
- 修复从待回收 worktree 自身执行 `reclaim` 时,目录删除后误判本地分支已不存在的问题;未完成的
|
package/README.en.md
CHANGED
|
@@ -12,7 +12,7 @@ the `orchestrate-subagents` control plane, which then chooses lightweight or ful
|
|
|
12
12
|
rerouting from effective capabilities, task scale, and local model policy. Providers remain independently usable and
|
|
13
13
|
compose through frozen Artifact, Binding, and Evidence envelopes.
|
|
14
14
|
|
|
15
|
-
[Detailed collaboration contracts and safety boundaries (Chinese, v1.
|
|
15
|
+
[Detailed collaboration contracts and safety boundaries (Chinese, v1.2.0)](./docs/architecture/skill-system-architecture.md)
|
|
16
16
|
|
|
17
17
|
## Included skills
|
|
18
18
|
|
|
@@ -75,6 +75,7 @@ script entry points remain as compatibility forwarders; new integrations should
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
agentkit capabilities --json
|
|
78
|
+
agentkit status --json
|
|
78
79
|
agentkit doctor --json
|
|
79
80
|
agentkit worktree --help
|
|
80
81
|
agentkit contract --help
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
其内部再按有效能力、任务规模、本地模型配置和验收证据选择轻量或完整运行方式及后续重路由。
|
|
12
12
|
各 provider 仍可独立使用,并通过冻结的 Artifact、Binding 和 Evidence envelope 按需组合。
|
|
13
13
|
|
|
14
|
-
[Agent Skills 协作契约与安全边界(v1.
|
|
14
|
+
[Agent Skills 协作契约与安全边界(v1.2.0)](./docs/architecture/skill-system-architecture.md)
|
|
15
15
|
|
|
16
16
|
## 包含的 Skill
|
|
17
17
|
|
|
@@ -72,6 +72,7 @@ npx skills add https://github.com/cr1992/agentkit.git -g --agent '*' --skill man
|
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
agentkit capabilities --json
|
|
75
|
+
agentkit status --json
|
|
75
76
|
agentkit doctor --json
|
|
76
77
|
agentkit worktree --help
|
|
77
78
|
agentkit contract --help
|
package/bin/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// CLI 自身的输出(help、version、capabilities 聚合、docs、doctor 汇总)不受该约束,
|
|
5
5
|
// 但任何迁移提示只写 stderr,避免污染可被管道消费的 stdout。
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
8
8
|
import { basename, dirname, join } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
|
|
@@ -109,7 +109,7 @@ function helpText() {
|
|
|
109
109
|
'',
|
|
110
110
|
'域:',
|
|
111
111
|
' worktree <命令> Git worktree 隔离与生命周期(含 worktree scan)',
|
|
112
|
-
' contract <命令>
|
|
112
|
+
' contract <命令> 任务契约 scaffold、访谈、规范化、校验、摘要与投影',
|
|
113
113
|
' orchestrate <工具> <命令> ledger | preflight | review-budget | reflection',
|
|
114
114
|
' host <工具> <命令> cache | model-policy',
|
|
115
115
|
' verify <命令> 冻结 Artifact 的一次性独立验收',
|
|
@@ -117,6 +117,7 @@ function helpText() {
|
|
|
117
117
|
'',
|
|
118
118
|
'跨域命令:',
|
|
119
119
|
' capabilities [--json] 汇总四个 Skill 的能力发现结果',
|
|
120
|
+
' status [--json] 从当前仓库的 ledger 指针发现未终态 ledger 并给出下一步',
|
|
120
121
|
' doctor [--json] 检查 Node、Git、安装完整性与各域能力发现',
|
|
121
122
|
' docs [<域>] [<主题>] 输出参考文档原文;缺主题时只列索引',
|
|
122
123
|
' --version 打印版本',
|
|
@@ -203,6 +204,231 @@ function runDoctor(args) {
|
|
|
203
204
|
return healthy ? 0 : 1;
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
// ── agentkit status ───────────────────────────────────────────────────────────
|
|
208
|
+
// 顶层状态发现:从 cwd 找 git common dir,读仓级指针,回读各 state root,筛出未终态的 ledger。
|
|
209
|
+
// 这是 bin 层同时读 orchestrate(ledger 状态)与 worktree(record 里的 ledger 绑定)两个域的地方——
|
|
210
|
+
// 域与域之间不互相 import,跨域聚合只发生在这一层。
|
|
211
|
+
const LEDGER_TARGET = GROUPS.orchestrate.ledger;
|
|
212
|
+
const PHASE_LABELS = {
|
|
213
|
+
empty: '空 ledger(还没有任何节点)',
|
|
214
|
+
not_started: '未开始',
|
|
215
|
+
in_progress: '进行中',
|
|
216
|
+
blocked: '阻塞',
|
|
217
|
+
ready_to_close: '待收口',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
function ledgerPhase(status) {
|
|
221
|
+
const nodes = Object.values(status.nodes ?? {});
|
|
222
|
+
if (!nodes.length) return 'empty';
|
|
223
|
+
if (status.summary?.completion_ready) return 'ready_to_close';
|
|
224
|
+
if (nodes.some((node) => node.state === 'blocked')) return 'blocked';
|
|
225
|
+
if (nodes.some((node) => ['running', 'awaiting_verification'].includes(node.state))) return 'in_progress';
|
|
226
|
+
if (nodes.every((node) => node.state === 'pending')) return 'not_started';
|
|
227
|
+
return 'in_progress';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// drift 的 ledger 不给续跑命令:冻结的协议已经不成立,只剩记为放弃或换当前 runtime 重签。
|
|
231
|
+
function nextCommands(entry) {
|
|
232
|
+
if (entry.skill_drift) {
|
|
233
|
+
return [
|
|
234
|
+
`agentkit orchestrate ledger close --ledger ${entry.ledger_dir} --abandon --reason <text>`,
|
|
235
|
+
're-contract:用当前 runtime 重签 Task Contract,再 agentkit orchestrate ledger init --contract <contract.json> --state-root <仓外路径>',
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
if (entry.phase === 'ready_to_close') return [`agentkit orchestrate ledger close --ledger ${entry.ledger_dir}`];
|
|
239
|
+
if (entry.phase === 'empty') return [`agentkit orchestrate ledger add-node --ledger ${entry.ledger_dir} --input <node.json>`];
|
|
240
|
+
if (entry.phase === 'blocked') {
|
|
241
|
+
return [
|
|
242
|
+
`agentkit orchestrate ledger status --ledger ${entry.ledger_dir}`,
|
|
243
|
+
`agentkit orchestrate ledger update --ledger ${entry.ledger_dir} --node <node> --input <state.json>`,
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
return [`agentkit orchestrate ledger status --ledger ${entry.ledger_dir}`];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function worktreeRecords(commonDir) {
|
|
250
|
+
try {
|
|
251
|
+
const { listRecordCacheEntries } = await import('../domains/worktree/worktree-trace.mjs');
|
|
252
|
+
return listRecordCacheEntries(commonDir)
|
|
253
|
+
.map((entry) => entry.record)
|
|
254
|
+
.filter((record) => record && record.worktree_state !== 'reclaimed' && record.worktree_state !== 'archived');
|
|
255
|
+
} catch {
|
|
256
|
+
// worktree 域没装或 trace store 不可读时,status 仍然要能靠仓级指针工作。
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function currentWorktreePath() {
|
|
262
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
263
|
+
if (result.status !== 0) return null;
|
|
264
|
+
const top = result.stdout.trim();
|
|
265
|
+
if (!top) return null;
|
|
266
|
+
try { return realpathSync(top); } catch { return top; }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function worktreeSummary(record) {
|
|
270
|
+
return {
|
|
271
|
+
worktree_id: record.worktree_id,
|
|
272
|
+
task: record.task,
|
|
273
|
+
branch: record.branch ?? null,
|
|
274
|
+
path: record.path,
|
|
275
|
+
agent: record.agent ? `${record.agent.host}/${record.agent.id}` : null,
|
|
276
|
+
task_status: record.task_status ?? null,
|
|
277
|
+
worktree_state: record.worktree_state ?? null,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ledger 目录不在这里重新推导:它由 core 的 ledgerDirectory 算出后传进来,避免第二份推导规则。
|
|
282
|
+
function describeLedger(pointer, ledgerDir, status, records) {
|
|
283
|
+
const nodes = Object.values(status.nodes ?? {});
|
|
284
|
+
const entry = {
|
|
285
|
+
ledger_id: status.ledger_id,
|
|
286
|
+
ledger_dir: ledgerDir,
|
|
287
|
+
state_root: pointer.state_root,
|
|
288
|
+
revision: status.revision,
|
|
289
|
+
contract_digest: status.contract_digest,
|
|
290
|
+
// 指针里的摘要只用于交叉核对与展示:不一致说明指针陈旧,状态仍以 state root 为准。
|
|
291
|
+
pointer_contract_digest_matches: status.contract_digest === pointer.contract_digest,
|
|
292
|
+
skill_drift: Boolean(status.skill_drift),
|
|
293
|
+
skill_drift_remediation: status.skill_drift_remediation ?? null,
|
|
294
|
+
recovery_needed: Boolean(status.recovery_needed),
|
|
295
|
+
summary: {
|
|
296
|
+
pending: status.summary?.pending ?? 0,
|
|
297
|
+
active: status.summary?.active ?? 0,
|
|
298
|
+
terminal: status.summary?.terminal ?? 0,
|
|
299
|
+
completion_ready: Boolean(status.summary?.completion_ready),
|
|
300
|
+
},
|
|
301
|
+
blockers: [
|
|
302
|
+
...nodes.filter((node) => node.state === 'blocked').map((node) => `${node.node_id} blocked:${node.reason ?? '未记录原因'}`),
|
|
303
|
+
...nodes.filter((node) => node.state === 'failed' && node.required !== false).map((node) => `${node.node_id} failed:${node.reason ?? '未记录原因'}`),
|
|
304
|
+
],
|
|
305
|
+
uncovered_implementation_nodes: status.summary?.uncovered_implementation_nodes ?? [],
|
|
306
|
+
unmet_completion_conditions: status.summary?.unmet_completion_conditions ?? [],
|
|
307
|
+
worktrees: records.filter((record) => record.ledger === status.ledger_id).map(worktreeSummary),
|
|
308
|
+
};
|
|
309
|
+
entry.phase = ledgerPhase(status);
|
|
310
|
+
entry.next_commands = nextCommands(entry);
|
|
311
|
+
return entry;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function collectLedgers(pointers, pointerApi, records) {
|
|
315
|
+
const active = [];
|
|
316
|
+
const drifted = [];
|
|
317
|
+
const terminal = [];
|
|
318
|
+
const dangling = [];
|
|
319
|
+
for (const item of pointers) {
|
|
320
|
+
if (item.error) {
|
|
321
|
+
dangling.push({ pointer_path: item.path, ledger_id: item.ledger_id, state: 'malformed', detail: item.error });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const pointer = item.pointer;
|
|
325
|
+
const dir = pointerApi.ledgerDirectory(pointer);
|
|
326
|
+
if (!existsSync(join(dir, 'events.ndjson'))) {
|
|
327
|
+
dangling.push({ pointer_path: item.path, ledger_id: pointer.ledger_id, state: 'dangling_state_root', detail: `state_root ${pointer.state_root} 下找不到 ledger 事件链 ${join(dir, 'events.ndjson')}` });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const status = capture(LEDGER_TARGET, ['status', '--ledger', dir]);
|
|
331
|
+
if (!status) {
|
|
332
|
+
dangling.push({ pointer_path: item.path, ledger_id: pointer.ledger_id, state: 'unreadable', detail: `ledger 目录 ${dir} 无法读取状态;用 agentkit orchestrate ledger doctor --ledger ${dir} 查看` });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (status.lifecycle) {
|
|
336
|
+
terminal.push({ pointer_path: item.path, ledger_id: pointer.ledger_id, ledger_dir: dir, lifecycle_state: status.lifecycle.state, closed_at: status.lifecycle.closed_at });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const entry = { ...describeLedger(pointer, dir, status, records), pointer_path: item.path };
|
|
340
|
+
(entry.skill_drift ? drifted : active).push(entry);
|
|
341
|
+
}
|
|
342
|
+
return { active, drifted, terminal, dangling };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function renderStatusText(report) {
|
|
346
|
+
const lines = [`agentkit ${report.cli_version} status`];
|
|
347
|
+
lines.push(`当前工作树: ${report.worktree_root ?? '(未知)'} git common dir: ${report.git_common_dir}`);
|
|
348
|
+
lines.push(`指针目录: ${report.pointer_dir}`);
|
|
349
|
+
if (report.scope === 'worktree') {
|
|
350
|
+
lines.push(`当前 worktree 绑定 ledger=${report.worktree_binding.ledger_id}(record ${report.worktree_binding.worktree_id.slice(0, 8)}),已收窄到该 ledger。`);
|
|
351
|
+
} else if (report.worktree_binding) {
|
|
352
|
+
lines.push(`当前 worktree 的 record 绑定 ledger=${report.worktree_binding.ledger_id},但该 ledger 没有可用指针;下面列出全部未终态 ledger。`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const groups = [['未终态 ledger', report.ledgers], ['skill_drift ledger(不能续跑)', report.drifted_ledgers]];
|
|
356
|
+
if (!report.ledgers.length && !report.drifted_ledgers.length) {
|
|
357
|
+
lines.push('');
|
|
358
|
+
lines.push('未发现 ledger。');
|
|
359
|
+
lines.push(` 在本仓 init 过的 ledger 会在 ${report.pointer_dir} 留下指针;升级前 init 的 ledger 没有指针,需要手传 --ledger <ledger 目录>。`);
|
|
360
|
+
lines.push(' 新建:agentkit orchestrate ledger init --contract <contract.json> --state-root <仓外路径>');
|
|
361
|
+
}
|
|
362
|
+
for (const [title, entries] of groups) {
|
|
363
|
+
if (!entries.length) continue;
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push(`${title}:${entries.length} 个`);
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
lines.push(` [${entry.ledger_id}] 阶段=${PHASE_LABELS[entry.phase] ?? entry.phase} revision=${entry.revision} 节点 pending/active/terminal=${entry.summary.pending}/${entry.summary.active}/${entry.summary.terminal}`);
|
|
368
|
+
lines.push(` ledger: ${entry.ledger_dir}`);
|
|
369
|
+
lines.push(` 活跃 worktree: ${entry.worktrees.length ? entry.worktrees.map((item) => `${item.task}@${item.path}`).join('、') : '无'}`);
|
|
370
|
+
lines.push(` 阻塞项: ${entry.blockers.length ? entry.blockers.join(';') : '无'}`);
|
|
371
|
+
lines.push(` 未覆盖节点: ${entry.uncovered_implementation_nodes.length ? entry.uncovered_implementation_nodes.join('、') : '无'}`);
|
|
372
|
+
if (entry.skill_drift) lines.push(` skill_drift: ${entry.skill_drift_remediation ?? '冻结的 runtime 与当前不一致'}`);
|
|
373
|
+
lines.push(' 下一步:');
|
|
374
|
+
for (const command of entry.next_commands) lines.push(` ${command}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (report.dangling_pointers.length) {
|
|
378
|
+
lines.push('');
|
|
379
|
+
lines.push(`悬空指针:${report.dangling_pointers.length} 个(state root 已不存在或指针损坏)`);
|
|
380
|
+
for (const item of report.dangling_pointers) lines.push(` [${item.ledger_id}] ${item.state}:${item.detail}`);
|
|
381
|
+
lines.push(` 回收:agentkit orchestrate ledger reclaim-pointers --repository ${report.worktree_root ?? report.git_common_dir}`);
|
|
382
|
+
}
|
|
383
|
+
if (report.terminal_pointers.length) {
|
|
384
|
+
lines.push('');
|
|
385
|
+
lines.push(`已终态但指针仍在:${report.terminal_pointers.length} 个`);
|
|
386
|
+
for (const item of report.terminal_pointers) lines.push(` [${item.ledger_id}] ${item.lifecycle_state} @ ${item.closed_at}`);
|
|
387
|
+
lines.push(` 回收:agentkit orchestrate ledger reclaim-pointers --repository ${report.worktree_root ?? report.git_common_dir}`);
|
|
388
|
+
}
|
|
389
|
+
return `${lines.join('\n')}\n`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function runStatus(args) {
|
|
393
|
+
const unknown = args.filter((arg) => arg !== '--json');
|
|
394
|
+
if (unknown.length) {
|
|
395
|
+
process.stderr.write(`agentkit status: 未知选项「${unknown[0]}」;本命令只接受 --json,作用域由当前工作目录所属的 git 仓库决定\n`);
|
|
396
|
+
return 2;
|
|
397
|
+
}
|
|
398
|
+
const pointerApi = await import('../core/ledger-pointer.mjs');
|
|
399
|
+
const found = pointerApi.resolveGitCommonDir(process.cwd());
|
|
400
|
+
if (!found.common_dir) {
|
|
401
|
+
process.stderr.write(`agentkit status: ${found.reason};status 从当前工作目录所属的 git 仓库读取仓级 ledger 指针,请在仓库内运行\n`);
|
|
402
|
+
return 2;
|
|
403
|
+
}
|
|
404
|
+
const commonDir = found.common_dir;
|
|
405
|
+
const records = await worktreeRecords(commonDir);
|
|
406
|
+
const here = currentWorktreePath();
|
|
407
|
+
const bound = here ? records.find((record) => record.path === here && record.ledger) ?? null : null;
|
|
408
|
+
const collected = collectLedgers(pointerApi.listLedgerPointers(commonDir), pointerApi, records);
|
|
409
|
+
|
|
410
|
+
// 受管 worktree 里优先用 record 的 ledger 字段收窄;收窄不到就退回全量,并说明原因,不做猜测。
|
|
411
|
+
const narrowed = bound ? [...collected.active, ...collected.drifted].filter((entry) => entry.ledger_id === bound.ledger) : [];
|
|
412
|
+
const scope = narrowed.length ? 'worktree' : 'repository';
|
|
413
|
+
const report = {
|
|
414
|
+
cli: 'agentkit',
|
|
415
|
+
cli_version: packageVersion(),
|
|
416
|
+
// 当前工作树(可能是 linked worktree);reclaim-pointers 的 --repository 接受该仓的任意工作树。
|
|
417
|
+
worktree_root: here,
|
|
418
|
+
git_common_dir: commonDir,
|
|
419
|
+
pointer_dir: pointerApi.pointerDirectory(commonDir),
|
|
420
|
+
scope,
|
|
421
|
+
worktree_binding: bound ? { ledger_id: bound.ledger, worktree_id: bound.worktree_id, task: bound.task, path: bound.path } : null,
|
|
422
|
+
ledgers: scope === 'worktree' ? narrowed.filter((entry) => !entry.skill_drift) : collected.active,
|
|
423
|
+
drifted_ledgers: scope === 'worktree' ? narrowed.filter((entry) => entry.skill_drift) : collected.drifted,
|
|
424
|
+
dangling_pointers: collected.dangling,
|
|
425
|
+
terminal_pointers: collected.terminal,
|
|
426
|
+
};
|
|
427
|
+
if (args.includes('--json')) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
428
|
+
else process.stdout.write(renderStatusText(report));
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
206
432
|
function docsIndex(domain) {
|
|
207
433
|
const dir = join(PACKAGE_ROOT, 'docs', DOC_DOMAINS[domain]);
|
|
208
434
|
if (!existsSync(dir)) return [];
|
|
@@ -247,6 +473,7 @@ export async function main(argv) {
|
|
|
247
473
|
return 0;
|
|
248
474
|
}
|
|
249
475
|
if (head === 'capabilities') return runCapabilities(rest);
|
|
476
|
+
if (head === 'status') return runStatus(rest);
|
|
250
477
|
if (head === 'doctor') return runDoctor(rest);
|
|
251
478
|
if (head === 'docs') return runDocs(rest);
|
|
252
479
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Task Contract 骨架的唯一构造出处。
|
|
3
|
+
//
|
|
4
|
+
// 骨架此前只长在 verify 域的 scaffold 里。orchestrate 域要提供 `contract scaffold` 别名,
|
|
5
|
+
// 而 domains/ 之间禁止互相 import,照抄一份骨架就会出现两个各自漂移的真源:
|
|
6
|
+
// 一边补了字段、另一边没补,interview 出的题和 verify 的占位判据就会对不上。
|
|
7
|
+
// 因此把骨架本身下沉到 core,两个域各自注入自己的 skill 绑定与签名口径。
|
|
8
|
+
//
|
|
9
|
+
// 这里只负责"形状",不负责"内容":占位字面量仍以 core/contract-substance.mjs 为唯一出处,
|
|
10
|
+
// 本模块只引用,不重新声明——否则 #12 的占位判据会静默失效。
|
|
11
|
+
// 摘要也不在这里算:verify 与 orchestrate 的 canonicalJson 严格度不同(见 core/digest.mjs),
|
|
12
|
+
// 由调用方用自己的 kit 签名,本模块返回未签名的对象。
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { SCAFFOLD_OBJECTIVE, SCAFFOLD_REQUIREMENT, SCAFFOLD_SCOPE_ITEM } from './contract-substance.mjs';
|
|
16
|
+
|
|
17
|
+
/** scaffold 默认 acceptance 条目的 ID。interview 回填第一条要求时复用它,不另起编号。 */
|
|
18
|
+
export const SCAFFOLD_ACCEPTANCE_ID = 'acceptance-1';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{ workdir: string, contractId: string, skillSet: { name: string, version: string, content_digest: string, provider_mode: string }[] }} options
|
|
22
|
+
* @returns {Record<string, any>} 未签名的契约骨架;字段顺序即两个域 scaffold 输出的字段顺序。
|
|
23
|
+
*/
|
|
24
|
+
export function buildScaffoldContract({ workdir, contractId, skillSet }) {
|
|
25
|
+
return {
|
|
26
|
+
schema_version: 1,
|
|
27
|
+
contract_id: contractId,
|
|
28
|
+
objective: SCAFFOLD_OBJECTIVE,
|
|
29
|
+
scope: { include: [SCAFFOLD_SCOPE_ITEM], exclude: [] },
|
|
30
|
+
acceptance: [{ contract_item_id: SCAFFOLD_ACCEPTANCE_ID, requirement: SCAFFOLD_REQUIREMENT }],
|
|
31
|
+
permissions: { mode: 'read_only', writable_paths: [] },
|
|
32
|
+
environment: { repository: resolve(workdir), isolation: 'caller_supplied' },
|
|
33
|
+
skill_set: skillSet.map((skill) => ({ ...skill })),
|
|
34
|
+
stop_conditions: [],
|
|
35
|
+
extensions: {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Task Contract 与 Verification Profile 的实质性检查。
|
|
3
|
+
//
|
|
4
|
+
// 形状校验只能说明结构合法。scaffold 原样生成的契约和 profile 结构完全合法,却什么也没断言,
|
|
5
|
+
// 这样的输入一路走到 Evidence 签发,整条 fail-closed 链就退化成了 fail-open。本模块负责在冻结的
|
|
6
|
+
// 那一刻把这类输入拦下来。
|
|
7
|
+
//
|
|
8
|
+
// 调用约定:
|
|
9
|
+
// - 只在创建入口调用:contract validate、ledger init、verify preflight/init/prepare-run、loop init;
|
|
10
|
+
// - 续跑与恢复入口只做形状校验,不重判实质性,理由有两条:
|
|
11
|
+
// (a) 契约冻结后不可变,实质性只在冻结那一刻判定一次,再判一次不会得到新信息;
|
|
12
|
+
// (b) verify validate、loop validate、adopt-root、各域 doctor 这些只读回看路径不经过 mutate,
|
|
13
|
+
// 会读到本判据出现之前冻结的状态;在这些路径上拒绝,等于让历史 Evidence 的审计结论随
|
|
14
|
+
// runtime 版本变化;
|
|
15
|
+
// - mutate 路径不需要这层保护:三个域的 mutate 都先比对 skill_provenance.content_digest,
|
|
16
|
+
// 摘要范围含 core/ 与 schemas/。跨版本的在途状态在到达校验器之前就已经以 skill_drift 终止,
|
|
17
|
+
// 同版本内的在途状态则已经过了创建入口;
|
|
18
|
+
// - 三个域的 doctor 用 substanceWarnings 把同一批判据整体降级成 warning,不改变 healthy;
|
|
19
|
+
// - 各入口直接使用这里给出的原因字符串,只套各自的错误类型,不改写措辞。
|
|
20
|
+
//
|
|
21
|
+
// error 与 warning 的分界:只做存在性检查的判据一律只给 warning。"write 模式没写 exclude" 说明
|
|
22
|
+
// 边界没划出来,但划不划得对本模块判断不了,拿它拒绝创建就是把没有信息量的检查当门禁。
|
|
23
|
+
//
|
|
24
|
+
// scaffold 的占位字面量以这里为唯一出处,verify scaffold 直接引用。两边一旦各自漂移,判据就会静默失效。
|
|
25
|
+
|
|
26
|
+
export const SCAFFOLD_OBJECTIVE = 'TODO: describe the frozen artifact objective';
|
|
27
|
+
export const SCAFFOLD_REQUIREMENT = 'TODO: replace with an observable requirement';
|
|
28
|
+
export const SCAFFOLD_SCOPE_ITEM = 'TODO';
|
|
29
|
+
export const SCAFFOLD_CHECK_ID = 'replace-with-real-check';
|
|
30
|
+
export const SCAFFOLD_ARGV = Object.freeze(['node', '--version']);
|
|
31
|
+
|
|
32
|
+
/** @typedef {{ errors: string[], warnings: string[] }} SubstanceReport */
|
|
33
|
+
|
|
34
|
+
const quote = (/** @type {unknown} */ value) => JSON.stringify(value);
|
|
35
|
+
|
|
36
|
+
/** @param {unknown} argv */
|
|
37
|
+
function isScaffoldArgv(argv) {
|
|
38
|
+
return Array.isArray(argv) && argv.length === SCAFFOLD_ARGV.length && argv.every((item, index) => item === SCAFFOLD_ARGV[index]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 契约层判据。只依赖契约本身,任何创建入口都能执行。
|
|
43
|
+
* 输入可能尚未通过形状校验,所以每个字段都按不可信数据读取。
|
|
44
|
+
* @param {any} contract
|
|
45
|
+
* @returns {SubstanceReport}
|
|
46
|
+
*/
|
|
47
|
+
export function contractSubstance(contract) {
|
|
48
|
+
const errors = [];
|
|
49
|
+
if (contract?.objective === SCAFFOLD_OBJECTIVE) {
|
|
50
|
+
errors.push(`objective = ${quote(SCAFFOLD_OBJECTIVE)}:仍是 scaffold 占位文本,需写明本次任务的真实目标`);
|
|
51
|
+
}
|
|
52
|
+
const acceptance = Array.isArray(contract?.acceptance) ? contract.acceptance : [];
|
|
53
|
+
acceptance.forEach((item, index) => {
|
|
54
|
+
if (item?.requirement === SCAFFOLD_REQUIREMENT) {
|
|
55
|
+
errors.push(`acceptance[${index}].requirement = ${quote(SCAFFOLD_REQUIREMENT)}:仍是 scaffold 占位文本,需写明可观察的验收要求`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const include = Array.isArray(contract?.scope?.include) ? contract.scope.include : [];
|
|
59
|
+
include.forEach((item, index) => {
|
|
60
|
+
if (item === SCAFFOLD_SCOPE_ITEM) errors.push(`scope.include[${index}] = ${quote(SCAFFOLD_SCOPE_ITEM)}:仍是 scaffold 占位,需列出本次任务的真实范围`);
|
|
61
|
+
});
|
|
62
|
+
const warnings = [];
|
|
63
|
+
// 只读合同越界由 permissions 本身兜住;写入合同则全靠 scope.exclude 与 stop_conditions 划边界,
|
|
64
|
+
// 两处都空等于把"改哪里、什么时候停"完全交给执行方判断。
|
|
65
|
+
if (contract?.permissions?.mode === 'write') {
|
|
66
|
+
if (!(Array.isArray(contract?.scope?.exclude) ? contract.scope.exclude : []).length) {
|
|
67
|
+
warnings.push('permissions.mode = "write" 且 scope.exclude 为空:写入型合同没有划出任何不可触碰的面,改动跑偏时没有范围边界可对照');
|
|
68
|
+
}
|
|
69
|
+
if (!(Array.isArray(contract?.stop_conditions) ? contract.stop_conditions : []).length) {
|
|
70
|
+
warnings.push('permissions.mode = "write" 且 stop_conditions 为空:写入型合同没有声明任何终止条件,执行失控时没有机械停机点');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { errors, warnings };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* profile 层判据。只有同时拿到 profile 的入口才能执行。
|
|
78
|
+
* 两个分支各自独立触发:只改 check_id、或只换 argv,都绕不过去。
|
|
79
|
+
* @param {any} profile
|
|
80
|
+
* @returns {SubstanceReport}
|
|
81
|
+
*/
|
|
82
|
+
export function profileSubstance(profile) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
const checks = Array.isArray(profile?.l0_checks) ? profile.l0_checks : [];
|
|
85
|
+
checks.forEach((check, index) => {
|
|
86
|
+
if (check?.check_id === SCAFFOLD_CHECK_ID) errors.push(`l0_checks[${index}].check_id = ${quote(SCAFFOLD_CHECK_ID)}:这是 scaffold 占位检查的标识,需替换为真实检查`);
|
|
87
|
+
});
|
|
88
|
+
if (checks.length > 0 && checks.every((check) => isScaffoldArgv(check?.argv))) {
|
|
89
|
+
errors.push(`l0_checks[*].argv 全部为 ${quote(SCAFFOLD_ARGV)}:只证明运行环境存在,没有检查本次 Artifact`);
|
|
90
|
+
}
|
|
91
|
+
return { errors, warnings: [] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 契约 × profile 的覆盖判据。只有同时拿到两份文件的入口才能执行。
|
|
96
|
+
* 现有 profile 校验只做单向绑定(拒绝 L1 引用不存在的 acceptance),反向不成立:
|
|
97
|
+
* 一条 acceptance 可以一次都不被审,签出来的 Evidence 照样是"全部通过"。
|
|
98
|
+
*
|
|
99
|
+
* 这里只保证每条 acceptance 都被 L1 审过,不保证被 L0 测到:l0_checks 条目没有
|
|
100
|
+
* contract_item_id 字段,schema 又是 additionalProperties: false,无从建立对应关系。
|
|
101
|
+
* @param {any} contract @param {any} profile
|
|
102
|
+
* @returns {SubstanceReport}
|
|
103
|
+
*/
|
|
104
|
+
export function coverageSubstance(contract, profile) {
|
|
105
|
+
const errors = [];
|
|
106
|
+
const reviewed = new Set((Array.isArray(profile?.l1_review) ? profile.l1_review : []).map((item) => item?.contract_item_id).filter((id) => typeof id === 'string' && id));
|
|
107
|
+
const acceptance = Array.isArray(contract?.acceptance) ? contract.acceptance : [];
|
|
108
|
+
acceptance.forEach((item, index) => {
|
|
109
|
+
const id = item?.contract_item_id;
|
|
110
|
+
// ID 缺失或非字符串是形状问题,交给形状校验报,这里不重复报一遍。
|
|
111
|
+
if (typeof id !== 'string' || !id || reviewed.has(id)) return;
|
|
112
|
+
errors.push(`acceptance[${index}].contract_item_id = ${quote(id)}:未被任何 l1_review 条目引用`);
|
|
113
|
+
});
|
|
114
|
+
return { errors, warnings: [] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* doctor 口径:把手上能执行的全部判据整体降级成 warning。
|
|
119
|
+
* doctor 是只读回看路径,会读到本判据出现之前冻结的状态;在这里判 unhealthy,
|
|
120
|
+
* 等于让同一份 Evidence 的审计结论随 runtime 版本变化。profile 传 null 时只出契约层判据。
|
|
121
|
+
* @param {any} contract @param {any} [profile]
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
export function substanceWarnings(contract, profile = null) {
|
|
125
|
+
const report = contractSubstance(contract);
|
|
126
|
+
const findings = [...report.errors];
|
|
127
|
+
if (profile) findings.push(...profileSubstance(profile).errors, ...coverageSubstance(contract, profile).errors);
|
|
128
|
+
return [...findings, ...report.warnings];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {string[]} errors */
|
|
132
|
+
export function formatSubstanceErrors(errors) {
|
|
133
|
+
return `实质性检查失败(${errors.length} 项):\n- ${errors.join('\n- ')}`;
|
|
134
|
+
}
|