@catheadowl/dsh-eval 0.2.0 → 0.3.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 +129 -0
- package/README.i18n.yaml +2 -2
- package/README.md +2 -5
- package/README.zh.md +2 -5
- package/bin/dsh-eval.mjs +344 -335
- package/bin/dsh-review.mjs +179 -154
- package/docs/README.md +3 -2
- package/docs/cross-turn.md +66 -0
- package/docs/experimental.md +4 -3
- package/docs/host-wiring.md +27 -2
- package/docs/known-issues.md +9 -1
- package/docs/matchers.md +34 -1
- package/docs/review.md +15 -10
- package/docs/runner-api.md +1 -2
- package/package.json +8 -2
- package/src/adapters/dsh/review.mjs +99 -52
- package/src/assertions.mjs +499 -389
- package/src/discovery.mjs +190 -166
- package/src/driver/multi-turn-driver.mjs +166 -0
- package/src/experiment/review.mjs +118 -118
- package/src/experimental.mjs +6 -1
- package/src/index.mjs +4 -0
- package/src/mock/mock-adapter.mjs +87 -73
- package/src/mock/script.mjs +49 -49
- package/src/overlay.mjs +13 -0
- package/src/report.mjs +5 -0
- package/src/review-report.mjs +45 -1
- package/src/runner.mjs +35 -30
- package/src/sandbox.mjs +62 -1
- package/src/tool-validation.mjs +85 -77
- package/src/trace.mjs +590 -218
package/docs/review.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: comprehension review 指南——defineReviewExperiment
|
|
2
|
+
description: comprehension review 指南——defineReviewExperiment 实验定义、空白环境(默认禁树外插件行)盲评运行、.runs 产物与 review-report 判读模板、六条评审规则
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Comprehension review
|
|
@@ -34,7 +34,7 @@ export default defineReviewExperiment({
|
|
|
34
34
|
})
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
抽象层公开 `defineReviewExperiment`(稳定根入口)。实验执行与渲染原语——`materializeReviewExperiment`、`executeReviewExperiment`、标准 observation renderer(`renderObservationSections`;prompt 里的观测占位符常量是 `OBSERVATIONS_PLACEHOLDER`,必须恰好出现一次)——在 `@catheadowl/dsh-eval/experimental` 逃生面(见 [experimental.md](experimental.md),无兼容承诺)。自定义 executor 只需实现 `(task, context) => result`;接入其他 agent runtime
|
|
37
|
+
抽象层公开 `defineReviewExperiment`(稳定根入口)。实验执行与渲染原语——`materializeReviewExperiment`、`executeReviewExperiment`、标准 observation renderer(`renderObservationSections`;prompt 里的观测占位符常量是 `OBSERVATIONS_PLACEHOLDER`,必须恰好出现一次)——在 `@catheadowl/dsh-eval/experimental` 逃生面(见 [experimental.md](experimental.md),无兼容承诺)。自定义 executor 只需实现 `(task, context) => result`;接入其他 agent runtime 不需要改试验定义。结果契约上,报告消费 `result.answer`(对任务的回答;未提供时回落 `result.stdout`),`result.stderr` 落盘为每轮 transcript——自定义 executor 提供这两者即可获得同等的产物与报告行为。
|
|
38
38
|
|
|
39
39
|
## CLI
|
|
40
40
|
|
|
@@ -43,19 +43,23 @@ export default defineReviewExperiment({
|
|
|
43
43
|
dsh-review --dry-run <experiment file or directory>
|
|
44
44
|
|
|
45
45
|
dsh-review \
|
|
46
|
-
--profile <
|
|
46
|
+
--profile <profile> \
|
|
47
47
|
--repo <deepseek-harness checkout> \
|
|
48
|
-
[--runs 5] [--timeout 300000] \
|
|
48
|
+
[--runs 5] [--timeout 300000] [--keep-plugin-rows] \
|
|
49
49
|
<experiment file or directory>
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
`--profile`/`--repo` 可来自 `dsh-eval.config.mjs`(见 README),flags 覆盖 config。
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## 空白环境(默认)与工具边界
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
真实运行的 reviewer 会话默认在**空白环境**启动:适配器先照常暂存所选 profile,再枚举它组合出的**全部树外插件行**(`package.json` 的 `dsh.profile.bundles` 中非 `@deepseek-ai/*` 的 bundle 各自 patch 文件里的行,加上 profile 自有 `cordis.patch.yml` 的行),在 `--patch` overlay 里逐行禁用——宿主 profile 装了什么 gates/插件都与 reviewer 无关,可复现性不再依赖「本机 profile 恰好干净」。白名单保留 reviewer 起不来就无测可言的接线行(`agent-default-model`、`session-title-llm`、`system-prompt`、`session-persistence-jsonl`);宿主模板工具行(`tool-fs`、shell、web、subagent 等)由静态清单继续禁用,cwd 指向空临时目录——reviewer 只能从物化的观测文本推理。运行后解析 session trace 的 `request/header` 事件做**工具边界校验**:发现任何非预期工具即视为 adapter failure(证据写入 `.runs/<id>/run-N.tool-boundary-evidence.json`)。
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
**校验没跑成也要记账**:收不到 session artifact(宿主 artifact 命名或会话格式换代是最常见的成因)时 `validateToolBoundary` 返回 `status: 'not-executed'` 而**不是**通过,适配器把该状态与 seam 诊断挂上执行结果;报告头写明 `tool boundary: NOT EXECUTED on run(s) N`,对应轮次的 `run-N.txt` 顶部与报告条目也各写一行原因,`run.json` 的 `toolBoundaries` 给机器读。这是有意的取向:**「没验证」必须看起来像没验证**——静默 fail-open 会让一份「工具边界从未检查过」的 review 产物读起来完全正常。
|
|
59
|
+
|
|
60
|
+
**刻意复用宿主插件面**(例如要评审某插件自己的 gate 行为):加 `--keep-plugin-rows`——跳过树外行枚举,仅保留静态工具禁用,宿主 gates 恢复运行。
|
|
61
|
+
|
|
62
|
+
> 注意:无 `id` 的组合条目对 id 定位的禁用天然不可见(宿主 loader 语义),本包的树外 bundle 生态均为带 id 行形态;发现无 id 树外行时以工具边界校验 fail-loud 兜底。白名单是**按行名**无条件保留——若某树外 bundle 刻意以白名单名(如 `system-prompt`)insert 自己的行,该行不会被禁(威胁模型是本机自己的 profile,非对抗面);此类泄漏同样由工具边界校验兜底。
|
|
59
63
|
|
|
60
64
|
## 产物
|
|
61
65
|
|
|
@@ -63,9 +67,10 @@ dsh-review \
|
|
|
63
67
|
|
|
64
68
|
- `observations.md`:本次实时物化的可见证据;
|
|
65
69
|
- `task.txt`:实际发给每位 reviewer 的完整任务;
|
|
66
|
-
- `run-N.txt`
|
|
67
|
-
- `run.
|
|
68
|
-
- `
|
|
70
|
+
- `run-N.txt`:该轮 reviewer 的**答案**——trace 推导(首条插件注入消息之前的最后一条 assistant 文本,抗回合尾部劫持),无 trace 时回落到 stdout 的最终消息;劫持发生时原始最终消息另存 `run-N.stdout.txt`;工具边界校验未执行时,本条文件顶部先写一行 `[tool-boundary: NOT EXECUTED — <原因>]`(答案本身退回 stdout 的最终消息);
|
|
71
|
+
- `run-N.stderr.txt` / error:完整会话转录(判读回溯的 transcript 指针,报告每轮引用)/ 失败原因;
|
|
72
|
+
- `run.json`:experiment、rubric、adapter、profile、run 数与每轮 `toolBoundaries` 记账(`checked` / `not-executed` + 原因);
|
|
73
|
+
- `review-report.md`:判读报告骨架——机器字段自动填(experiment/adapter/ profile/runs、observations 指纹、rubric 位置、每轮 reviewer **答案**与 transcript 指针),三个 **人工判读栏目**留白待填:intentional design 命中项、新 red flag、下一步(改输出 / 改 rubric / 改 behavior case / 不处理)。刻意不做自动评分——review 层的价值在人工判断,报告只把判断物化成可归档、可对比的工程证据(dry-run 也会生成,runs 记 0)。
|
|
69
74
|
|
|
70
75
|
## 六条评审规则
|
|
71
76
|
|
package/docs/runner-api.md
CHANGED
|
@@ -17,10 +17,9 @@ const result = await runEvalCase(evalCase, { profile: 'headless', cliPath, mode:
|
|
|
17
17
|
| 键 | 类型 | 语义 |
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| `profile` | `string` | 必填。承载被测插件的 dsh profile(沙箱会暂存其 store,不污染真实 home)。 |
|
|
20
|
-
| `cliPath` | `string` |
|
|
20
|
+
| `cliPath` | `string` | 必填。编译好的 dsh CLI 入口(`apps/cli/lib/bin.js`)绝对路径(`resolveDshCliChain` 结果)。程序化取值见下节。 |
|
|
21
21
|
| `mode` | `'real' \| 'mock'` | 覆写 case 自带的 mode;mock 需 `script.steps`。 |
|
|
22
22
|
| `artifactsDir` | `string` | 提供则把 stdout/stderr/trace/session 日志拷贝到该目录(自动创建)。 |
|
|
23
|
-
| `dshRepoDir` | `string` | **已弃用**:宿主 checkout 目录(从中拼出 CLI 路径)。下个 minor 删除——迁移到 `cliPath`。 |
|
|
24
23
|
|
|
25
24
|
## `cliPath` 从哪来:跨档关系(读我)
|
|
26
25
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catheadowl/dsh-eval",
|
|
3
3
|
"description": "Agent eval framework over dsh headless runs: case runner, session-trace assertions, and a scripted mock-LLM layer for plugin intent tests.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/CatheadOwl/dsh-eval.git"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
11
|
+
"packageManager": "pnpm@11.7.0",
|
|
7
12
|
"main": "src/index.mjs",
|
|
8
13
|
"exports": {
|
|
9
14
|
".": "./src/index.mjs",
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
"docs/",
|
|
29
34
|
"README.md",
|
|
30
35
|
"README.zh.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
31
37
|
"LICENSE"
|
|
32
38
|
],
|
|
33
39
|
"scripts": {
|
|
@@ -36,6 +42,6 @@
|
|
|
36
42
|
"verify:experimental": "node scripts/verify-experimental-face.mjs",
|
|
37
43
|
"verify:readme-i18n": "node scripts/verify-readme-i18n.mjs",
|
|
38
44
|
"prepack": "node scripts/verify-manifest-face.mjs && node scripts/verify-publish-readiness.mjs && node scripts/verify-readme-i18n.mjs && node scripts/verify-experimental-face.mjs",
|
|
39
|
-
"test": "node --test --test-isolation=none tests/trace.test.mjs tests/assertions.test.mjs tests/overlay.test.mjs tests/staging.test.mjs tests/runner.test.mjs tests/cli.test.mjs tests/discovery.test.mjs tests/review.test.mjs tests/report.test.mjs tests/config.test.mjs tests/review-report.test.mjs && node scripts/verify-publish-readiness.mjs && node scripts/verify-manifest-face.mjs && node scripts/verify-readme-i18n.mjs && node scripts/verify-experimental-face.mjs"
|
|
45
|
+
"test": "node --test --test-isolation=none tests/trace.test.mjs tests/assertions.test.mjs tests/overlay.test.mjs tests/staging.test.mjs tests/runner.test.mjs tests/cli.test.mjs tests/cli-report.test.mjs tests/discovery.test.mjs tests/review.test.mjs tests/tool-validation.test.mjs tests/report.test.mjs tests/config.test.mjs tests/review-report.test.mjs && node scripts/verify-publish-readiness.mjs && node scripts/verify-manifest-face.mjs && node scripts/verify-readme-i18n.mjs && node scripts/verify-experimental-face.mjs"
|
|
40
46
|
}
|
|
41
47
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/** dsh-headless execution adapter for model-independent review experiments. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join, resolve } from 'node:path'
|
|
6
6
|
import { executeReviewExperiment } from '../../experiment/review.mjs'
|
|
7
|
-
import { CLI_RELATIVE_PATH } from '../../cli.mjs'
|
|
8
7
|
import { overlayDisableRows } from '../../overlay.mjs'
|
|
9
8
|
import {
|
|
10
|
-
resolveRealDshHome, stageSandboxHome, teardownSandbox, spawnHeadlessDsh,
|
|
9
|
+
resolveRealDshHome, stageSandboxHome, stagedPluginRows, teardownSandbox, spawnHeadlessDsh,
|
|
11
10
|
} from '../../sandbox.mjs'
|
|
12
|
-
import {
|
|
11
|
+
import { collectSessionTrace } from '../../trace.mjs'
|
|
13
12
|
import { validateToolBoundary, renderToolBoundaryEvidence } from '../../tool-validation.mjs'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Model-facing tool rows every shipped dsh profile mounts from `dsh-base`.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* validation (see `validateToolBoundary`) detects any residual
|
|
16
|
+
* A static supplementary guard on top of the blank-environment enumeration
|
|
17
|
+
* (host template tool rows never appear in `stagedPluginRows` — they are the
|
|
18
|
+
* sterile baseline, so their ids are enumerated here instead). Post-run
|
|
19
|
+
* tool boundary validation (see `validateToolBoundary`) detects any residual
|
|
20
|
+
* drift.
|
|
21
21
|
*/
|
|
22
22
|
const REVIEW_DISABLED_TOOL_ROWS = [
|
|
23
23
|
'tool-bash',
|
|
@@ -39,43 +39,65 @@ const REVIEW_DISABLED_TOOL_ROWS = [
|
|
|
39
39
|
'tool-workflow',
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Rows the reviewer session must KEEP even when an out-of-tree bundle or the
|
|
44
|
+
* profile patch touches them: model wiring and the session log — without
|
|
45
|
+
* these the reviewer cannot answer at all and the trace (tool boundary
|
|
46
|
+
* check, run artifacts) never materializes. These ids are host-template
|
|
47
|
+
* rows; an out-of-tree patch row targeting one of them only overrides its
|
|
48
|
+
* config, so keeping it enabled stays safe.
|
|
49
|
+
*/
|
|
50
|
+
const REVIEW_REQUIRED_ROWS = new Set([
|
|
51
|
+
'agent-default-model',
|
|
52
|
+
'session-title-llm',
|
|
53
|
+
'system-prompt',
|
|
54
|
+
'session-persistence-jsonl',
|
|
55
|
+
])
|
|
46
56
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Serialize the blank-environment overlay: disable every plugin row the
|
|
59
|
+
* staged profile composes beyond the host templates (`stagedPluginRows` —
|
|
60
|
+
* out-of-tree bundle rows + profile patch rows), UNION the static host tool
|
|
61
|
+
* rows, MINUS the model/session wiring the reviewer needs. With
|
|
62
|
+
* `keepPluginRows` (explicit opt-in to study the host plugin face itself)
|
|
63
|
+
* the enumeration is skipped and only the static tool lockdown remains.
|
|
64
|
+
*/
|
|
65
|
+
function buildReviewOverlayYaml(pluginRows, { keepPluginRows }) {
|
|
66
|
+
const disabled = new Set(keepPluginRows ? [] : pluginRows)
|
|
67
|
+
for (const row of REVIEW_DISABLED_TOOL_ROWS) disabled.add(row)
|
|
68
|
+
for (const row of REVIEW_REQUIRED_ROWS) disabled.delete(row)
|
|
69
|
+
return overlayDisableRows([...disabled])
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
/** The CLI entry for an executor: explicit cliPath (
|
|
58
|
-
*
|
|
59
|
-
*
|
|
72
|
+
/** The CLI entry for an executor: the explicit cliPath (a `resolveDshCliChain`
|
|
73
|
+
* result). Its absence is a caller bug the CLI bins already catch — this
|
|
74
|
+
* guard serves direct API consumers. */
|
|
60
75
|
function executorCli(options) {
|
|
61
|
-
if (options.cliPath
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
if (options.cliPath === undefined) {
|
|
77
|
+
throw new Error('review adapter needs options.cliPath (a resolveDshCliChain result)')
|
|
78
|
+
}
|
|
79
|
+
return resolve(options.cliPath)
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
/**
|
|
67
83
|
* Create an executor compatible with executeReviewExperiment.
|
|
68
84
|
*
|
|
69
|
-
* The executor boots a
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
85
|
+
* The executor boots a blank review environment in an isolated DSH_HOME
|
|
86
|
+
* (staging/teardown mechanics shared with the behavior runner via
|
|
87
|
+
* sandbox.mjs): the staged profile's every out-of-tree plugin row and every
|
|
88
|
+
* host model-facing tool row is disabled via overlay (blank =
|
|
89
|
+
* dsh-base/dsh-headless templates + model wiring, nothing else — regardless
|
|
90
|
+
* of what the host profile carries), and the tool boundary is validated
|
|
91
|
+
* after the run.
|
|
75
92
|
*
|
|
76
93
|
* @param {object} options
|
|
77
|
-
* @param {string} [options.profile='headless'] - the
|
|
94
|
+
* @param {string} [options.profile='headless'] - the review profile (host
|
|
95
|
+
* templates + whatever the host machine carries; plugin rows are disabled
|
|
96
|
+
* by the overlay anyway).
|
|
78
97
|
* @param {Set<string>} [options.allowedTools] - tool names permitted in the reviewer's session (default: empty).
|
|
98
|
+
* @param {boolean} [options.keepPluginRows=false] - opt back into the host
|
|
99
|
+
* profile's plugin face (e.g. to review a plugin's own gate behavior);
|
|
100
|
+
* only the static tool lockdown remains.
|
|
79
101
|
*/
|
|
80
102
|
export function createDshHeadlessReviewExecutor(options) {
|
|
81
103
|
const cli = executorCli(options)
|
|
@@ -85,6 +107,7 @@ export function createDshHeadlessReviewExecutor(options) {
|
|
|
85
107
|
}
|
|
86
108
|
const timeoutMs = options.timeoutMs ?? 300_000
|
|
87
109
|
const allowedTools = options.allowedTools ?? new Set()
|
|
110
|
+
const keepPluginRows = options.keepPluginRows ?? false
|
|
88
111
|
|
|
89
112
|
return async function executeWithDsh(task) {
|
|
90
113
|
// A fresh process alone is not enough: dsh also stores settings, titles,
|
|
@@ -93,9 +116,18 @@ export function createDshHeadlessReviewExecutor(options) {
|
|
|
93
116
|
// while retaining the selected profile's model config and plugin links.
|
|
94
117
|
const runDir = mkdtempSync(join(tmpdir(), 'dsh-review-'))
|
|
95
118
|
const dshHome = join(runDir, 'dsh-home')
|
|
96
|
-
const overlayPath = join(runDir, 'review-overlay.yml')
|
|
97
|
-
writeFileSync(overlayPath, buildReviewOverlayYaml(), 'utf8')
|
|
98
119
|
stageSandboxHome(options.dshHome ?? resolveRealDshHome(), dshHome, profile)
|
|
120
|
+
// Blank environment by DEFAULT (review-blank-environment TODO): disable
|
|
121
|
+
// every row the staged profile composes beyond the host templates, so a
|
|
122
|
+
// host profile carrying out-of-tree plugins (gates included) can no
|
|
123
|
+
// longer steer or crash the reviewer. `keepPluginRows` opts back into
|
|
124
|
+
// the host plugin face deliberately. Enumeration happens on the STAGED
|
|
125
|
+
// copy, after staging — the staged home is what actually boots.
|
|
126
|
+
const overlayPath = join(runDir, 'review-overlay.yml')
|
|
127
|
+
writeFileSync(overlayPath, buildReviewOverlayYaml(
|
|
128
|
+
stagedPluginRows(dshHome, profile),
|
|
129
|
+
{ keepPluginRows },
|
|
130
|
+
), 'utf8')
|
|
99
131
|
|
|
100
132
|
try {
|
|
101
133
|
const { stdout, stderr, exitCode, timedOut } = await spawnHeadlessDsh({
|
|
@@ -120,28 +152,43 @@ export function createDshHeadlessReviewExecutor(options) {
|
|
|
120
152
|
|
|
121
153
|
// Post-run tool boundary check: parse the session
|
|
122
154
|
// trace, verify no unexpected tools were mounted in the reviewer's
|
|
123
|
-
// session, fail the run on violation.
|
|
124
|
-
// the check
|
|
155
|
+
// session, fail the run on violation. A missing session log does NOT
|
|
156
|
+
// skip the check silently: `validateToolBoundary` reports
|
|
157
|
+
// `status: 'not-executed'`, and the adapter carries that fact (plus the
|
|
158
|
+
// seam diagnosis) on the result so the report and run artifacts state
|
|
159
|
+
// that the boundary guarantee was not verified (EVAL-021).
|
|
125
160
|
// Validation inspects the main session only (buildTrace selects
|
|
126
161
|
// non-subagent logs); plugin tools leaking in a subagent session
|
|
127
162
|
// would not be caught — irrelevant in review where the overlay
|
|
128
163
|
// disables every subagent tool row.
|
|
129
|
-
const trace =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
const { trace, gap } = collectSessionTrace(join(dshHome, 'sessions'))
|
|
165
|
+
const validation = validateToolBoundary(trace, { allowedTools })
|
|
166
|
+
result.toolValidation = validation
|
|
167
|
+
if (validation.status === 'not-executed') {
|
|
168
|
+
// `traceGap` mirrors the behavior runner's field name for the same
|
|
169
|
+
// seam diagnosis; the bin writes both into run-N.txt and the report.
|
|
170
|
+
result.traceGap = gap
|
|
171
|
+
}
|
|
172
|
+
if (validation.status === 'checked' && !validation.ok) {
|
|
173
|
+
// Attach evidence for the caller to persist (the adapter's
|
|
174
|
+
// runDir is ephemeral — removed by the finally block). The
|
|
175
|
+
// bin writes this to `.runs/<id>/tool-boundary-evidence.json`.
|
|
176
|
+
result.toolBoundaryEvidence = renderToolBoundaryEvidence(validation, { runDir, profile })
|
|
177
|
+
const boundaryError = new Error(
|
|
178
|
+
`tool boundary violation: unexpected tools [${validation.unexpected.join(', ')}]`,
|
|
179
|
+
)
|
|
180
|
+
boundaryError.result = result
|
|
181
|
+
throw boundaryError
|
|
144
182
|
}
|
|
183
|
+
// The ANSWER to the task, not the last message: stdout carries the
|
|
184
|
+
// headless CLI's final assistant message — whatever the reviewer
|
|
185
|
+
// said LAST. If any tail interaction intervened (a turn-close gate
|
|
186
|
+
// splice that slipped past the blank environment, an infra
|
|
187
|
+
// complaint), stdout holds that instead of the analysis. The
|
|
188
|
+
// trace's answerText (last assistant text before the first
|
|
189
|
+
// plugin-sourced injection) IS the analysis; stdout remains the
|
|
190
|
+
// fallback for trace-less runs (recorded by `traceGap` above).
|
|
191
|
+
result.answer = trace !== undefined && trace.answerText !== '' ? trace.answerText : stdout
|
|
145
192
|
|
|
146
193
|
return result
|
|
147
194
|
} finally {
|