@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 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,3 @@
1
+ # PROMPT_START_30 · S6 fixture
2
+
3
+ Static invoke path for compliance bench S6.
@@ -0,0 +1,3 @@
1
+ # 任务审核 · cyning-harness A6 · R1 fixture
2
+
3
+ S6 compliance bench · static review path.
@@ -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
- 机械辅助(推荐):`npx @cyning/harness verify --target <meta> --task docs/tasks/active/task_*.md`
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
- rest = rest.filter((a) => a !== '--graph');
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 summary = taskFile
411
- ? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${path.basename(taskFile)}`
412
- : `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
413
- console.log(summary);
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyning/harness",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "cyning-harness discipline package · init / upgrade / check CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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–S5
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–S5(默认输出含公理解释与摘要表)
17
+ --all 运行 S1–S6(默认输出含公理解释与摘要表)
18
18
  --quiet CI/脚本模式:stdout 仅打印合规率数字;摘要说明走 stderr
19
19
 
20
20
  说明:
21
21
  合规率 = PASS 场景数 ÷ 总场景数 × 100。
22
- 100 表示 S1–S5 全部通过,即 gate-check / sync 行为符合 SDD 公理;
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–S5 ║"
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
- index.index[slug] = {
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}`);