@cyning/harness 2.0.1 → 2.0.3
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 +21 -0
- package/docs/USER_GUIDE_v1.0_zh.md +25 -1
- package/examples/compliance_bench/S6_agent_handoff/.cyning-harness/manifest.json +1 -0
- package/examples/compliance_bench/S6_agent_handoff/docs/harness/invokes/by-task/s6/PROMPT_START_30_v1.md +3 -0
- package/examples/compliance_bench/S6_agent_handoff/docs/harness/reviews/task_s6_agent_handoff_audit_R1_20260618.md +3 -0
- package/examples/compliance_bench/S6_agent_handoff/task_s6_agent_handoff.md +31 -0
- package/harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md +1 -1
- package/harness/templates/QUICKREF_v1_zh.md +1 -1
- package/lib/cli.js +39 -10
- package/lib/task-meta.js +223 -0
- package/lib/verify.js +69 -0
- package/package.json +1 -1
- package/schema/verify_result.v1.schema.json +65 -0
- package/wizard/compliance-bench.sh +34 -7
- package/wizard/lib/generate-invoke-index.js +63 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.2] - 2026-06-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A6 Agent handoff**:`verify --json` · `--agent-hint` · `--workspace-root`
|
|
12
|
+
- **lib/task-meta.js**:解析 task Harness 表 `entry_invoke_*` · 人工闸 · `may_start_30`
|
|
13
|
+
- **schema/verify_result.v1.schema.json**:JSON 输出文档化
|
|
14
|
+
- **sync index**:索引 task `entry_points.{10,20,30}`
|
|
15
|
+
- **compliance-bench S6**:`S6_agent_handoff/` · `--all` 含 S6(6/6 = 100%)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **FRAGMENT_30**:verify 从「推荐」→ **「30 改码前必须」**
|
|
20
|
+
- **USER_GUIDE** §5.1 Agent handoff · **QUICKREF** 新 flags
|
|
21
|
+
- **版本**:`package.json` → **2.0.2**
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
- **HG-RELEASE**:待维护者 publish `@cyning/harness@2.0.2`
|
|
26
|
+
- dogfood:`kimi-code-meta` · `task_meta_graph_issue_sync_gate_v1.md`
|
|
27
|
+
|
|
7
28
|
## [2.0.1] - 2026-06-17
|
|
8
29
|
|
|
9
30
|
### Added
|
|
@@ -114,6 +114,12 @@ npx @cyning/harness@1.0.1 init --preset harness-only --ide cursor,agents --yes
|
|
|
114
114
|
# 30 前聚合验证(gate-check + audit D5 + S5 warn + 可选 --graph)
|
|
115
115
|
npx @cyning/harness verify --target . --task docs/tasks/active/task_xxx.md
|
|
116
116
|
|
|
117
|
+
# Agent handoff(v2.0.2+):JSON 路由 + 下一帽提示
|
|
118
|
+
npx @cyning/harness verify --target . \
|
|
119
|
+
--task docs/tasks/active/task_xxx.md \
|
|
120
|
+
[--json] [--agent-hint] \
|
|
121
|
+
[--workspace-root /path/to/Projects]
|
|
122
|
+
|
|
117
123
|
# 仅人工闸
|
|
118
124
|
npx @cyning/harness gate-check --target . --task docs/tasks/active/task_xxx.md
|
|
119
125
|
npx @cyning/harness gate-check --graph --target . # Inform 图谱闸
|
|
@@ -131,6 +137,24 @@ npx @cyning/harness gate-check --graph --target . # Inform 图谱闸
|
|
|
131
137
|
| `HG-GRAPH-MODULES` | 架构模块表人签 | pending → 拒改码 30 |
|
|
132
138
|
| `HG-RELEASE` | 发版闸(产品仓) | 一般业务仓不涉及 |
|
|
133
139
|
|
|
140
|
+
### 5.1 Agent handoff(v2.0.2+)
|
|
141
|
+
|
|
142
|
+
`verify --json` 从 task Harness 表解析 `entry_invoke_30` 等字段,输出 Agent 可消费的机械路由(不读取 Projects 文件正文):
|
|
143
|
+
|
|
144
|
+
| JSON 字段 | 含义 |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `may_start_30` | 等价 gate-check「可 30」 |
|
|
147
|
+
| `blocked_reason` | 阻塞时 D2/D3 文案 |
|
|
148
|
+
| `review_path` | 最新 `*_audit_R1_*.md`(相对 target) |
|
|
149
|
+
| `entry_invoke_30` | task 表原始路径 |
|
|
150
|
+
| `entry_invoke_30_resolved` | 绝对路径(`Projects/` 前缀须 `--workspace-root`) |
|
|
151
|
+
| `next_hat` | `"30"` 或 `null` |
|
|
152
|
+
| `agent_preamble` | 短句提醒首输出 GATE_VERIFY |
|
|
153
|
+
|
|
154
|
+
Schema:[`schema/verify_result.v1.schema.json`](../schema/verify_result.v1.schema.json)
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
134
158
|
**Inform 图谱闸(v1.0)**:改码类 task 前,确保 `docs/_tech_graph/` 模块表已维护者签 `HG-GRAPH-MODULES = approved`。存量大仓可按 [`ONBOARDING.md`](./ONBOARDING.md) §3 选 S0–S3 档位,**不必一次画完所有 Mermaid**。
|
|
135
159
|
|
|
136
160
|
---
|
|
@@ -142,7 +166,7 @@ npx @cyning/harness gate-check --graph --target . # Inform 图谱闸
|
|
|
142
166
|
| `npx @cyning/harness init` | 首次安装模板与 manifest(可选 `--with-scripts`) |
|
|
143
167
|
| `npx @cyning/harness upgrade` | 同步产品包更新(可加 `--gate-check` 先 audit) |
|
|
144
168
|
| `npx @cyning/harness check` | 检查是否有新版本 |
|
|
145
|
-
| `npx @cyning/harness verify` | 30 前聚合:gate-check + audit D5 + S5 warn + 可选 `--graph` |
|
|
169
|
+
| `npx @cyning/harness verify` | 30 前聚合:gate-check + audit D5 + S5 warn + 可选 `--graph` · v2.0.2+ `--json` / `--agent-hint` / `--workspace-root` |
|
|
146
170
|
| `npx @cyning/harness gate-check` | 仅人工闸(`--graph` / `--json`) |
|
|
147
171
|
| `npx @cyning/harness audit` | ICVO 机械审计(D3/D5/S5) |
|
|
148
172
|
| `npx @cyning/harness sync index` | 生成 `.cyning-harness/invoke_index.json` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"2.0.2","preset":"harness-only"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Task fixture · S6 · Agent handoff · verify --json
|
|
2
|
+
|
|
3
|
+
> 用途:SDD-Compliance micro-bench S6 · approved + entry_invoke_30 → may_start_30 true
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Harness 元信息
|
|
8
|
+
|
|
9
|
+
| 字段 | 值 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **task_slug** | `s6_agent_handoff` |
|
|
12
|
+
| **test_strategy** | `not_applicable` |
|
|
13
|
+
| **test_strategy_note** | compliance bench 静态夹具 |
|
|
14
|
+
| **entry_invoke_30** | `docs/harness/invokes/by-task/s6/PROMPT_START_30_v1.md` |
|
|
15
|
+
|
|
16
|
+
### 人工闸
|
|
17
|
+
|
|
18
|
+
| human_gate_id | status | blocks_hats | 说明 |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| HG-TASK-DRAFT | approved | 20, 30 | ok |
|
|
21
|
+
| HG-AUDIT-R1 | approved | 30 | R1 已签 |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 背景与目标
|
|
26
|
+
|
|
27
|
+
验证 A6 Agent handoff:`verify --json` 输出 `may_start_30: true` 且 `entry_invoke_30` 非空。
|
|
28
|
+
|
|
29
|
+
## 验收标准
|
|
30
|
+
|
|
31
|
+
- `verify --json --task task_s6_agent_handoff.md` · `may_start_30` = true · `entry_invoke_30` 非空
|
|
@@ -20,7 +20,7 @@ reviews:task_*_audit_R1_*.md 存在且 R1 通过? {是/否}
|
|
|
20
20
|
|
|
21
21
|
结论:{STOP · 签闸指引 | 可进入读码/改码}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
机械辅助(30 改码前必须):`npx @cyning/harness verify --target <meta> --task docs/tasks/active/task_*.md [--json] [--agent-hint] [--workspace-root <Projects>]`
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Agent 纪律
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
# 30 前聚合验证(gate-check + audit D5 + S5 warn + 可选 --graph)
|
|
11
|
-
npx @cyning/harness verify [--target .] [--task docs/tasks/active/task_xxx.md] [--graph]
|
|
11
|
+
npx @cyning/harness verify [--target .] [--task docs/tasks/active/task_xxx.md] [--graph] [--json] [--agent-hint] [--workspace-root PATH]
|
|
12
12
|
|
|
13
13
|
# 仅人工闸 npx @cyning/harness gate-check [--target .] [--task ...] [--graph] [--json]
|
|
14
14
|
|
package/lib/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ function usage(version = 'unknown') {
|
|
|
38
38
|
npx @cyning/harness check [--target PATH]
|
|
39
39
|
npx @cyning/harness audit [--target PATH] [--task FILE]
|
|
40
40
|
npx @cyning/harness gate-check [--target PATH] [--task FILE] [--graph] [--json]
|
|
41
|
-
npx @cyning/harness verify [--target PATH] [--task FILE] [--graph]
|
|
41
|
+
npx @cyning/harness verify [--target PATH] [--task FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH]
|
|
42
42
|
npx @cyning/harness sync index [--target PATH]
|
|
43
43
|
npx @cyning/harness task check --file PATH [--no-circular] [--registry DIR]...
|
|
44
44
|
npx @cyning/harness graph yaml compile --graph-id ID [--input DIR] [--output FILE]
|
|
@@ -375,12 +375,15 @@ async function cmdGateCheck(args) {
|
|
|
375
375
|
|
|
376
376
|
async function cmdVerify(args) {
|
|
377
377
|
// D3 · implemented in lib/verify.js
|
|
378
|
-
const { verifyTarget } = await import('./verify.js');
|
|
378
|
+
const { verifyTarget, buildVerifyHandoff, formatAgentHint } = await import('./verify.js');
|
|
379
379
|
if (args.includes('--help') || args.includes('-h')) {
|
|
380
|
-
console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE] [--graph]
|
|
380
|
+
console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH]
|
|
381
381
|
|
|
382
382
|
无 --task 时:扫描 docs/tasks/active/task_*.md 并逐项列出闸表(同 gate-check)。
|
|
383
383
|
任一 task 阻塞则 VERIFY: BLOCKED · exit 2;全部可 30 则 VERIFY: PASS。
|
|
384
|
+
--json:输出 Agent handoff JSON(may_start_30 · entry_invoke_30 · review_path 等)。
|
|
385
|
+
--agent-hint:人类可读 handoff 摘要(可与 --json 同用)。
|
|
386
|
+
--workspace-root:解析 task 中 Projects/ 前缀的 entry_invoke 路径。
|
|
384
387
|
`);
|
|
385
388
|
return;
|
|
386
389
|
}
|
|
@@ -390,8 +393,12 @@ async function cmdVerify(args) {
|
|
|
390
393
|
rest = r1;
|
|
391
394
|
const { value: taskFile, rest: r2 } = takeOption(rest, '--task');
|
|
392
395
|
rest = r2;
|
|
396
|
+
const { value: workspaceRoot, rest: r3 } = takeOption(rest, '--workspace-root');
|
|
397
|
+
rest = r3;
|
|
393
398
|
const graph = rest.includes('--graph');
|
|
394
|
-
|
|
399
|
+
const json = rest.includes('--json');
|
|
400
|
+
const agentHint = rest.includes('--agent-hint');
|
|
401
|
+
rest = rest.filter((a) => a !== '--graph' && a !== '--json' && a !== '--agent-hint');
|
|
395
402
|
|
|
396
403
|
if (rest.length > 0) {
|
|
397
404
|
const err = new Error(`verify 未知参数: ${rest.join(' ')}`);
|
|
@@ -402,15 +409,37 @@ async function cmdVerify(args) {
|
|
|
402
409
|
const target = resolveTarget(process.cwd(), targetArg);
|
|
403
410
|
const result = verifyTarget(target, { taskFile, graph });
|
|
404
411
|
|
|
405
|
-
// forward gate-check/audit/graph stdout if any
|
|
406
|
-
if (result.stdout) {
|
|
412
|
+
// forward gate-check/audit/graph stdout if any(--json 时仍透传机械闸表)
|
|
413
|
+
if (result.stdout && !json) {
|
|
407
414
|
process.stdout.write(result.stdout);
|
|
415
|
+
} else if (result.stdout && json) {
|
|
416
|
+
// JSON 模式:闸表走 stderr 避免污染 stdout JSON
|
|
417
|
+
process.stderr.write(result.stdout);
|
|
408
418
|
}
|
|
409
419
|
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
:
|
|
413
|
-
|
|
420
|
+
const handoff = buildVerifyHandoff(target, {
|
|
421
|
+
taskFile,
|
|
422
|
+
workspaceRoot: workspaceRoot ? path.resolve(workspaceRoot) : undefined,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
if (json) {
|
|
426
|
+
console.log(JSON.stringify(handoff, null, 2));
|
|
427
|
+
}
|
|
428
|
+
if (agentHint) {
|
|
429
|
+
console.log(formatAgentHint(handoff));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!json && !agentHint) {
|
|
433
|
+
const summary = taskFile
|
|
434
|
+
? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${path.basename(taskFile)}`
|
|
435
|
+
: `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
|
|
436
|
+
console.log(summary);
|
|
437
|
+
} else if (json || agentHint) {
|
|
438
|
+
const summary = taskFile
|
|
439
|
+
? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${path.basename(taskFile)}`
|
|
440
|
+
: `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
|
|
441
|
+
console.error(summary);
|
|
442
|
+
}
|
|
414
443
|
|
|
415
444
|
if (!result.ok) {
|
|
416
445
|
process.exitCode = result.exitCode ?? 2;
|
package/lib/task-meta.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const META_FIELD_RE = /\|\s*\*\*([^*]+)\*\*\s*\|\s*`([^`]+)`/;
|
|
5
|
+
const GATE_ROW_RE = /^\|\s*(?:\*\*)?([^*|]+?)(?:\*\*)?\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]*)\|/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从 task Markdown 解析 Harness 元信息表(反引号包裹的值)。
|
|
9
|
+
*/
|
|
10
|
+
export function parseHarnessMeta(content) {
|
|
11
|
+
const meta = {};
|
|
12
|
+
const section = extractSection(content, '## Harness 元信息', '###');
|
|
13
|
+
if (!section) return meta;
|
|
14
|
+
|
|
15
|
+
for (const line of section.split('\n')) {
|
|
16
|
+
const match = line.match(META_FIELD_RE);
|
|
17
|
+
if (!match) continue;
|
|
18
|
+
const key = match[1].trim();
|
|
19
|
+
meta[key] = match[2].trim();
|
|
20
|
+
}
|
|
21
|
+
return meta;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析 ### 人工闸 表。
|
|
26
|
+
*/
|
|
27
|
+
export function parseHumanGates(content) {
|
|
28
|
+
const section = extractSection(content, '### 人工闸', '##');
|
|
29
|
+
if (!section) return [];
|
|
30
|
+
|
|
31
|
+
const gates = [];
|
|
32
|
+
for (const line of section.split('\n')) {
|
|
33
|
+
const match = line.match(GATE_ROW_RE);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
const id = match[1].trim();
|
|
36
|
+
if (!id.startsWith('HG-') || id.includes('human_gate')) continue;
|
|
37
|
+
gates.push({
|
|
38
|
+
id,
|
|
39
|
+
status: normalizeCell(match[2]),
|
|
40
|
+
blocks_hats: normalizeCell(match[3]),
|
|
41
|
+
note: normalizeCell(match[4]),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return gates;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 机械判定 may_start_30(与 gate-check.sh 一致)。
|
|
49
|
+
*/
|
|
50
|
+
export function evaluateMayStart30(gates) {
|
|
51
|
+
const byId = Object.fromEntries(gates.map((g) => [g.id, g]));
|
|
52
|
+
const draft = byId['HG-TASK-DRAFT'];
|
|
53
|
+
const audit = byId['HG-AUDIT-R1'];
|
|
54
|
+
const graph = byId['HG-GRAPH-MODULES'];
|
|
55
|
+
|
|
56
|
+
if (audit?.status !== 'approved') {
|
|
57
|
+
return {
|
|
58
|
+
may_start_30: false,
|
|
59
|
+
blocked_reason: 'HG-AUDIT-R1 非 approved(须维护者签 task 表)',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
draft?.status !== 'approved' &&
|
|
65
|
+
draft?.blocks_hats?.includes('30')
|
|
66
|
+
) {
|
|
67
|
+
return {
|
|
68
|
+
may_start_30: false,
|
|
69
|
+
blocked_reason: 'HG-TASK-DRAFT pending 且 blocks 30',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (graph?.status === 'pending') {
|
|
74
|
+
return {
|
|
75
|
+
may_start_30: false,
|
|
76
|
+
blocked_reason: 'HG-GRAPH-MODULES pending',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { may_start_30: true, blocked_reason: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 解析 Projects/ 前缀路径;需 workspaceRoot 才 resolve。
|
|
85
|
+
*/
|
|
86
|
+
export function resolveInvokePath(rawPath, { target, workspaceRoot } = {}) {
|
|
87
|
+
if (!rawPath) {
|
|
88
|
+
return { resolved: null, warnings: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const warnings = [];
|
|
92
|
+
|
|
93
|
+
if (rawPath.startsWith('Projects/')) {
|
|
94
|
+
if (!workspaceRoot) {
|
|
95
|
+
warnings.push(
|
|
96
|
+
`entry_invoke 路径以 Projects/ 开头但未提供 --workspace-root:${rawPath}`,
|
|
97
|
+
);
|
|
98
|
+
return { resolved: null, warnings };
|
|
99
|
+
}
|
|
100
|
+
const abs = path.join(workspaceRoot, rawPath.replace(/^Projects\//, ''));
|
|
101
|
+
if (!fs.existsSync(abs)) {
|
|
102
|
+
warnings.push(`entry_invoke 路径不存在:${rawPath}`);
|
|
103
|
+
}
|
|
104
|
+
return { resolved: abs, warnings };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (path.isAbsolute(rawPath)) {
|
|
108
|
+
if (!fs.existsSync(rawPath)) {
|
|
109
|
+
warnings.push(`entry_invoke 路径不存在:${rawPath}`);
|
|
110
|
+
}
|
|
111
|
+
return { resolved: rawPath, warnings };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const abs = path.resolve(target ?? process.cwd(), rawPath);
|
|
115
|
+
if (!fs.existsSync(abs)) {
|
|
116
|
+
warnings.push(`entry_invoke 路径不存在(相对 target):${rawPath}`);
|
|
117
|
+
}
|
|
118
|
+
return { resolved: abs, warnings };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 查找最新 R1 审查文(相对 target)。
|
|
123
|
+
*/
|
|
124
|
+
export function findReviewPath(target, taskFile) {
|
|
125
|
+
const reviewsDir = path.join(target, 'docs/harness/reviews');
|
|
126
|
+
if (!fs.existsSync(reviewsDir)) return null;
|
|
127
|
+
|
|
128
|
+
const taskBase = path.basename(taskFile, '.md');
|
|
129
|
+
const prefix = `${taskBase}_audit_R1_`;
|
|
130
|
+
|
|
131
|
+
let best = null;
|
|
132
|
+
let bestMtime = 0;
|
|
133
|
+
|
|
134
|
+
for (const name of fs.readdirSync(reviewsDir)) {
|
|
135
|
+
if (!name.startsWith(prefix) || !name.endsWith('.md')) continue;
|
|
136
|
+
const full = path.join(reviewsDir, name);
|
|
137
|
+
const mtime = fs.statSync(full).mtimeMs;
|
|
138
|
+
if (mtime > bestMtime) {
|
|
139
|
+
bestMtime = mtime;
|
|
140
|
+
best = path.relative(target, full).replace(/\\/g, '/');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return best;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 聚合单 task 的 Agent handoff 结果。
|
|
149
|
+
*/
|
|
150
|
+
export function buildTaskHandoff(target, taskFile, options = {}) {
|
|
151
|
+
const { workspaceRoot } = options;
|
|
152
|
+
const absTask = path.isAbsolute(taskFile)
|
|
153
|
+
? taskFile
|
|
154
|
+
: path.join(target, taskFile);
|
|
155
|
+
|
|
156
|
+
const content = fs.readFileSync(absTask, 'utf8');
|
|
157
|
+
const meta = parseHarnessMeta(content);
|
|
158
|
+
const gates = parseHumanGates(content);
|
|
159
|
+
const gateEval = evaluateMayStart30(gates);
|
|
160
|
+
|
|
161
|
+
const warnings = [];
|
|
162
|
+
const entry30Raw = meta.entry_invoke_30 ?? null;
|
|
163
|
+
const entry20Raw = meta.entry_invoke_20 ?? null;
|
|
164
|
+
|
|
165
|
+
const entry30 = resolveInvokePath(entry30Raw, { target, workspaceRoot });
|
|
166
|
+
const entry20 = resolveInvokePath(entry20Raw, { target, workspaceRoot });
|
|
167
|
+
warnings.push(...entry30.warnings, ...entry20.warnings);
|
|
168
|
+
|
|
169
|
+
const reviewPath = findReviewPath(target, taskFile);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
task: path.basename(taskFile),
|
|
173
|
+
task_path: taskFile.replace(/\\/g, '/'),
|
|
174
|
+
task_slug: meta.task_slug ?? null,
|
|
175
|
+
may_start_30: gateEval.may_start_30,
|
|
176
|
+
blocked_reason: gateEval.blocked_reason,
|
|
177
|
+
review_path: reviewPath,
|
|
178
|
+
entry_invoke_20: entry20Raw,
|
|
179
|
+
entry_invoke_20_resolved: entry20.resolved,
|
|
180
|
+
entry_invoke_30: entry30Raw,
|
|
181
|
+
entry_invoke_30_resolved: entry30.resolved,
|
|
182
|
+
next_hat: gateEval.may_start_30 ? '30' : null,
|
|
183
|
+
agent_preamble: gateEval.may_start_30
|
|
184
|
+
? '首输出 GATE_VERIFY 闸扫描表,再读 entry_invoke_30 开 30 帽。'
|
|
185
|
+
: null,
|
|
186
|
+
warnings,
|
|
187
|
+
gates: gates.map((g) => ({
|
|
188
|
+
id: g.id,
|
|
189
|
+
status: g.status,
|
|
190
|
+
blocks_hats: g.blocks_hats,
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 扫描 target 下 active task 列表。
|
|
197
|
+
*/
|
|
198
|
+
export function listActiveTasks(target) {
|
|
199
|
+
const activeDir = path.join(target, 'docs/tasks/active');
|
|
200
|
+
if (!fs.existsSync(activeDir)) return [];
|
|
201
|
+
|
|
202
|
+
return fs
|
|
203
|
+
.readdirSync(activeDir)
|
|
204
|
+
.filter((f) => f.startsWith('task_') && f.endsWith('.md'))
|
|
205
|
+
.map((f) => path.join('docs/tasks/active', f).replace(/\\/g, '/'))
|
|
206
|
+
.sort();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractSection(content, startMarker, endMarker) {
|
|
210
|
+
const start = content.indexOf(startMarker);
|
|
211
|
+
if (start === -1) return null;
|
|
212
|
+
|
|
213
|
+
let end = content.length;
|
|
214
|
+
if (endMarker) {
|
|
215
|
+
const next = content.indexOf(endMarker, start + startMarker.length);
|
|
216
|
+
if (next !== -1) end = next;
|
|
217
|
+
}
|
|
218
|
+
return content.slice(start, end);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeCell(cell) {
|
|
222
|
+
return cell.replace(/\*/g, '').trim();
|
|
223
|
+
}
|
package/lib/verify.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveHarnessRootForTarget, wizardScript } from './paths.js';
|
|
5
5
|
import { auditTarget } from './audit.js';
|
|
6
|
+
import { buildTaskHandoff, listActiveTasks } from './task-meta.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 30 前聚合验证:gate-check + audit D5(仅 --task)+ S5 git-clean warn + 可选 --graph
|
|
@@ -150,3 +151,71 @@ function runGitCleanCheck(target) {
|
|
|
150
151
|
const dirty = result.status === 0 && result.stdout.trim().length > 0;
|
|
151
152
|
return { dirty };
|
|
152
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 构建 verify --json / --agent-hint 的 Agent handoff 载荷。
|
|
157
|
+
*/
|
|
158
|
+
export function buildVerifyHandoff(target, options = {}) {
|
|
159
|
+
const { taskFile, workspaceRoot } = options;
|
|
160
|
+
const handoffOpts = { workspaceRoot };
|
|
161
|
+
|
|
162
|
+
if (taskFile) {
|
|
163
|
+
const handoff = buildTaskHandoff(target, taskFile, handoffOpts);
|
|
164
|
+
return {
|
|
165
|
+
schema_version: '1',
|
|
166
|
+
verify_ok: handoff.may_start_30,
|
|
167
|
+
...handoff,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const taskFiles = listActiveTasks(target);
|
|
172
|
+
const tasks = taskFiles.map((tf) => {
|
|
173
|
+
const handoff = buildTaskHandoff(target, tf, handoffOpts);
|
|
174
|
+
return {
|
|
175
|
+
schema_version: '1',
|
|
176
|
+
verify_ok: handoff.may_start_30,
|
|
177
|
+
...handoff,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const verifyOk = tasks.length > 0 && tasks.every((t) => t.may_start_30);
|
|
182
|
+
return {
|
|
183
|
+
schema_version: '1',
|
|
184
|
+
verify_ok: verifyOk,
|
|
185
|
+
tasks,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 人类可读的 --agent-hint(5–10 行)。
|
|
191
|
+
*/
|
|
192
|
+
export function formatAgentHint(payload) {
|
|
193
|
+
const lines = ['=== Harness verify · agent-hint ==='];
|
|
194
|
+
|
|
195
|
+
const items = payload.tasks ?? [payload];
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
lines.push(`task: ${item.task}`);
|
|
198
|
+
lines.push(` may_start_30: ${item.may_start_30}`);
|
|
199
|
+
if (item.blocked_reason) {
|
|
200
|
+
lines.push(` blocked: ${item.blocked_reason}`);
|
|
201
|
+
}
|
|
202
|
+
if (item.review_path) {
|
|
203
|
+
lines.push(` review: ${item.review_path}`);
|
|
204
|
+
}
|
|
205
|
+
if (item.entry_invoke_30) {
|
|
206
|
+
lines.push(` entry_invoke_30: ${item.entry_invoke_30}`);
|
|
207
|
+
if (item.entry_invoke_30_resolved) {
|
|
208
|
+
lines.push(` resolved: ${item.entry_invoke_30_resolved}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (item.next_hat) {
|
|
212
|
+
lines.push(` next_hat: ${item.next_hat}`);
|
|
213
|
+
}
|
|
214
|
+
for (const w of item.warnings ?? []) {
|
|
215
|
+
lines.push(` warn: ${w}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return lines.join('\n').trimEnd();
|
|
221
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://cyning.me/harness/schemas/verify_result.v1.schema.json",
|
|
4
|
+
"title": "Harness Verify Agent Handoff Result v1",
|
|
5
|
+
"description": "npx @cyning/harness verify --json 输出 · A6 v2.0.2+",
|
|
6
|
+
"oneOf": [
|
|
7
|
+
{ "$ref": "#/definitions/single" },
|
|
8
|
+
{ "$ref": "#/definitions/multi" }
|
|
9
|
+
],
|
|
10
|
+
"definitions": {
|
|
11
|
+
"single": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": [
|
|
14
|
+
"schema_version",
|
|
15
|
+
"verify_ok",
|
|
16
|
+
"task",
|
|
17
|
+
"may_start_30"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"schema_version": { "type": "string", "const": "1" },
|
|
21
|
+
"verify_ok": { "type": "boolean" },
|
|
22
|
+
"task": { "type": "string" },
|
|
23
|
+
"task_path": { "type": "string" },
|
|
24
|
+
"task_slug": { "type": ["string", "null"] },
|
|
25
|
+
"may_start_30": { "type": "boolean" },
|
|
26
|
+
"blocked_reason": { "type": ["string", "null"] },
|
|
27
|
+
"review_path": { "type": ["string", "null"] },
|
|
28
|
+
"entry_invoke_20": { "type": ["string", "null"] },
|
|
29
|
+
"entry_invoke_20_resolved": { "type": ["string", "null"] },
|
|
30
|
+
"entry_invoke_30": { "type": ["string", "null"] },
|
|
31
|
+
"entry_invoke_30_resolved": { "type": ["string", "null"] },
|
|
32
|
+
"next_hat": { "type": ["string", "null"], "enum": ["30", null] },
|
|
33
|
+
"agent_preamble": { "type": ["string", "null"] },
|
|
34
|
+
"warnings": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string" }
|
|
37
|
+
},
|
|
38
|
+
"gates": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["id", "status"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"id": { "type": "string" },
|
|
45
|
+
"status": { "type": "string" },
|
|
46
|
+
"blocks_hats": { "type": "string" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"multi": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"required": ["schema_version", "verify_ok", "tasks"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"schema_version": { "type": "string", "const": "1" },
|
|
57
|
+
"verify_ok": { "type": "boolean" },
|
|
58
|
+
"tasks": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": { "$ref": "#/definitions/single" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SDD-Compliance micro-bench · v1 · S1–
|
|
2
|
+
# SDD-Compliance micro-bench · v1 · S1–S6
|
|
3
3
|
# 输出合规率 %;不测 LLM 解题,只测 Orchestrate/Verify 可机械部分。
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
@@ -12,14 +12,14 @@ QUIET=0
|
|
|
12
12
|
|
|
13
13
|
usage() {
|
|
14
14
|
cat <<'EOF'
|
|
15
|
-
用法:compliance-bench.sh [--all | S1 S2 S3 S4 S5] [--quiet]
|
|
15
|
+
用法:compliance-bench.sh [--all | S1 S2 S3 S4 S5 S6] [--quiet]
|
|
16
16
|
|
|
17
|
-
--all 运行 S1–
|
|
17
|
+
--all 运行 S1–S6(默认输出含公理解释与摘要表)
|
|
18
18
|
--quiet CI/脚本模式:stdout 仅打印合规率数字;摘要说明走 stderr
|
|
19
19
|
|
|
20
20
|
说明:
|
|
21
21
|
合规率 = PASS 场景数 ÷ 总场景数 × 100。
|
|
22
|
-
100 表示 S1–
|
|
22
|
+
100 表示 S1–S6 全部通过,即 gate-check / sync / verify handoff 行为符合 SDD 公理;
|
|
23
23
|
不是 LLM 解题正确率,也不是业务项目胜率。
|
|
24
24
|
|
|
25
25
|
示例:
|
|
@@ -30,9 +30,9 @@ EOF
|
|
|
30
30
|
|
|
31
31
|
while [[ $# -gt 0 ]]; do
|
|
32
32
|
case "$1" in
|
|
33
|
-
--all) SCENARIOS=(S1 S2 S3 S4 S5); shift ;;
|
|
33
|
+
--all) SCENARIOS=(S1 S2 S3 S4 S5 S6); shift ;;
|
|
34
34
|
--quiet) QUIET=1; shift ;;
|
|
35
|
-
S1|S2|S3|S4|S5) SCENARIOS+=("$1"); shift ;;
|
|
35
|
+
S1|S2|S3|S4|S5|S6) SCENARIOS+=("$1"); shift ;;
|
|
36
36
|
-h|--help) usage; exit 0 ;;
|
|
37
37
|
*) echo "未知参数: $1" >&2; usage; exit 1 ;;
|
|
38
38
|
esac
|
|
@@ -68,7 +68,7 @@ gate_status() {
|
|
|
68
68
|
print_banner() {
|
|
69
69
|
[[ "$QUIET" == "1" ]] && return 0
|
|
70
70
|
log_scenario "╔══════════════════════════════════════════════════════════════╗"
|
|
71
|
-
log_scenario "║ SDD-Compliance micro-bench · v1 · S1–
|
|
71
|
+
log_scenario "║ SDD-Compliance micro-bench · v1 · S1–S6 ║"
|
|
72
72
|
log_scenario "╚══════════════════════════════════════════════════════════════╝"
|
|
73
73
|
log_scenario ""
|
|
74
74
|
log_scenario "性质 机械合规测试 · 合成夹具 · 不调用 LLM"
|
|
@@ -241,6 +241,32 @@ scenario_S5() {
|
|
|
241
241
|
fi
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
scenario_S6() {
|
|
245
|
+
local dir="$BENCH_DIR/S6_agent_handoff"
|
|
246
|
+
local tf="task_s6_agent_handoff.md"
|
|
247
|
+
scenario_header "S6" "Agent handoff · verify --json" \
|
|
248
|
+
"A6 · 30 前 Agent 可发现路由" \
|
|
249
|
+
"examples/compliance_bench/S6_agent_handoff/" \
|
|
250
|
+
"HG-AUDIT-R1=approved + entry_invoke_30 → may_start_30 true"
|
|
251
|
+
|
|
252
|
+
local json may entry
|
|
253
|
+
json="$(node "$HARNESS_ROOT/bin/harness.js" verify --target "$dir" --task "$tf" --json 2>/dev/null || true)"
|
|
254
|
+
may="$(printf '%s' "$json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);console.log(j.may_start_30)}catch{console.log('')}})")"
|
|
255
|
+
entry="$(printf '%s' "$json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);console.log(j.entry_invoke_30||'')}catch{console.log('')}})")"
|
|
256
|
+
|
|
257
|
+
if [[ "$may" == "true" && -n "$entry" ]]; then
|
|
258
|
+
scenario_footer "S6" "PASS" "may_start_30=true · entry_invoke_30 非空" \
|
|
259
|
+
"verify --json 输出 Agent handoff 字段 · A6 路由可机械发现"
|
|
260
|
+
PASS=$((PASS+1))
|
|
261
|
+
RESULTS+=("S6|PASS|may_start_30=true")
|
|
262
|
+
else
|
|
263
|
+
scenario_footer "S6" "FAIL" "may=$may entry=$entry" \
|
|
264
|
+
"verify --json 缺 handoff 字段 · 须修 lib/task-meta 或夹具"
|
|
265
|
+
FAIL=$((FAIL+1))
|
|
266
|
+
RESULTS+=("S6|FAIL|may=$may")
|
|
267
|
+
fi
|
|
268
|
+
}
|
|
269
|
+
|
|
244
270
|
print_summary() {
|
|
245
271
|
local total=$((PASS+FAIL))
|
|
246
272
|
local rate=0
|
|
@@ -286,6 +312,7 @@ for s in "${SCENARIOS[@]}"; do
|
|
|
286
312
|
S3) scenario_S3 ;;
|
|
287
313
|
S4) scenario_S4 ;;
|
|
288
314
|
S5) scenario_S5 ;;
|
|
315
|
+
S6) scenario_S6 ;;
|
|
289
316
|
esac
|
|
290
317
|
done
|
|
291
318
|
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
// 生成 .cyning-harness/invoke_index.json(harness-sync --index 的 JSON 引擎)
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { parseHarnessMeta } from '../../lib/task-meta.js';
|
|
5
6
|
|
|
6
7
|
const target = process.argv[2];
|
|
8
|
+
const workspaceRoot = process.argv[3] || undefined;
|
|
9
|
+
|
|
7
10
|
if (!target) {
|
|
8
|
-
console.error('用法: node generate-invoke-index.js /path/to/target-repo');
|
|
11
|
+
console.error('用法: node generate-invoke-index.js /path/to/target-repo [/path/to/workspace-root]');
|
|
9
12
|
process.exit(1);
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -13,7 +16,6 @@ const byTaskDir = path.join(target, 'docs/harness/invokes/by-task');
|
|
|
13
16
|
const indexFile = path.join(target, '.cyning-harness/invoke_index.json');
|
|
14
17
|
|
|
15
18
|
function extractHatId(filename) {
|
|
16
|
-
// invoke_YYYYMMDD_<hat_id>_*.md 或 invoke_YYYYMMDD_<hat_id>.md
|
|
17
19
|
const base = path.basename(filename, '.md');
|
|
18
20
|
const parts = base.split('_');
|
|
19
21
|
if (parts.length >= 3 && parts[0] === 'invoke') {
|
|
@@ -22,12 +24,47 @@ function extractHatId(filename) {
|
|
|
22
24
|
return 'unknown';
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
function collectTaskEntryPoints() {
|
|
28
|
+
const entryBySlug = {};
|
|
29
|
+
const taskDirs = [
|
|
30
|
+
path.join(target, 'docs/tasks/active'),
|
|
31
|
+
path.join(target, 'docs/tasks/done'),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const dir of taskDirs) {
|
|
35
|
+
if (!fs.existsSync(dir)) continue;
|
|
36
|
+
for (const name of fs.readdirSync(dir)) {
|
|
37
|
+
if (!name.startsWith('task_') || !name.endsWith('.md')) continue;
|
|
38
|
+
const content = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
39
|
+
const meta = parseHarnessMeta(content);
|
|
40
|
+
const slug = meta.task_slug;
|
|
41
|
+
if (!slug) continue;
|
|
42
|
+
|
|
43
|
+
const points = {};
|
|
44
|
+
if (meta.entry_invoke_10_task) points['10'] = meta.entry_invoke_10_task;
|
|
45
|
+
if (meta.entry_invoke_20) points['20'] = meta.entry_invoke_20;
|
|
46
|
+
if (meta.entry_invoke_30) points['30'] = meta.entry_invoke_30;
|
|
47
|
+
|
|
48
|
+
if (Object.keys(points).length === 0) continue;
|
|
49
|
+
|
|
50
|
+
entryBySlug[slug] = {
|
|
51
|
+
task_markdown: path.relative(target, path.join(dir, name)).replace(/\\/g, '/'),
|
|
52
|
+
entry_points: points,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entryBySlug;
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
const index = {
|
|
26
61
|
schema_version: '1',
|
|
27
62
|
generated_at: new Date().toISOString(),
|
|
28
63
|
index: {},
|
|
29
64
|
};
|
|
30
65
|
|
|
66
|
+
const taskEntries = collectTaskEntryPoints();
|
|
67
|
+
|
|
31
68
|
if (fs.existsSync(byTaskDir)) {
|
|
32
69
|
for (const entry of fs.readdirSync(byTaskDir, { withFileTypes: true })) {
|
|
33
70
|
if (!entry.isDirectory()) continue;
|
|
@@ -48,13 +85,36 @@ if (fs.existsSync(byTaskDir)) {
|
|
|
48
85
|
|
|
49
86
|
invokes.sort((a, b) => a.path.localeCompare(b.path));
|
|
50
87
|
|
|
51
|
-
|
|
88
|
+
const record = {
|
|
52
89
|
task_slug: slug,
|
|
53
90
|
invokes,
|
|
54
91
|
};
|
|
92
|
+
|
|
93
|
+
if (taskEntries[slug]) {
|
|
94
|
+
record.task_markdown = taskEntries[slug].task_markdown;
|
|
95
|
+
record.entry_points = taskEntries[slug].entry_points;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
index.index[slug] = record;
|
|
55
99
|
}
|
|
56
100
|
}
|
|
57
101
|
|
|
102
|
+
// task 有 entry_invoke 但尚无 invokes/by-task 目录时也索引
|
|
103
|
+
for (const [slug, data] of Object.entries(taskEntries)) {
|
|
104
|
+
if (!index.index[slug]) {
|
|
105
|
+
index.index[slug] = {
|
|
106
|
+
task_slug: slug,
|
|
107
|
+
invokes: [],
|
|
108
|
+
task_markdown: data.task_markdown,
|
|
109
|
+
entry_points: data.entry_points,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (workspaceRoot) {
|
|
115
|
+
index.workspace_root = path.resolve(workspaceRoot);
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
fs.mkdirSync(path.dirname(indexFile), { recursive: true });
|
|
59
119
|
fs.writeFileSync(indexFile, JSON.stringify(index, null, 2) + '\n');
|
|
60
120
|
console.log(`invoke_index: ${indexFile}`);
|