@cyning/harness 2.0.0 → 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,46 @@
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
+
28
+ ## [2.0.1] - 2026-06-17
29
+
30
+ ### Added
31
+
32
+ - **D4-a 公理**:`checkAxioms()` 检测 in_progress + `HG-GRAPH-MODULES` pending/rejected · MUST_READ 缺 Inform 可选 warn
33
+ - **compliance-bench S5**:`S5_rejected_draft/` · rejected→draft 静态夹具 · `--all` 含 S5(5/5 = 100%)
34
+ - **optional pre-commit**:`.cyning-harness/hooks/pre-commit.sample` · USER_GUIDE §13.3 · 默认 init 不安装
35
+
36
+ ### Changed
37
+
38
+ - **rejected→draft**:事件流精确匹配 `TaskStatusChanged(draft)` · 有回退则 pass · 缺则 error
39
+ - **版本**:`package.json` → **2.0.1**
40
+ - **USER_GUIDE**:§13 公理集 · optional Git hooks
41
+
42
+ ### Notes
43
+
44
+ - **HG-RELEASE**:待维护者 publish `@cyning/harness@2.0.1`
45
+ - bench S5 语义向 HGM axioms 靠拢 · drift 时改夹具不改 axioms
46
+
7
47
  ## [2.0.0] - 2026-06-17
8
48
 
9
49
  ### Added
@@ -23,7 +63,7 @@
23
63
 
24
64
  ### Notes
25
65
 
26
- - **HG-RELEASE**(tag v2.0.0 · npm publish)仍留维护者人闸
66
+ - **HG-RELEASE**:tag v2.0.0 · npm `@cyning/harness@2.0.0` 已发布
27
67
  - `gate-check --graph` 语义不变;HGM 独立子命令
28
68
  - HGM v2.0 默认本地 JSONL + snapshot,不含 Neo4j / SQLite / 远端同步
29
69
 
@@ -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` |
@@ -276,12 +300,12 @@ npx @cyning/harness graph yaml check --all --input docs/_tech_graph
276
300
  | 项 | 说明 |
277
301
  | --- | --- |
278
302
  | 不是胜率工具 | [`README` 试点证据 B2](../README.md) · 完整表 [`PILOT_EVIDENCE_B2_v1_zh.md`](./methodology/execution/PILOT_EVIDENCE_B2_v1_zh.md) · **小样本机制证据**,不可外推 |
279
- | bench `100` | [SDD-Compliance](../examples/compliance_bench/README.md) 四场景合规率 · 见上文 §6.1 |
303
+ | bench `100` | [SDD-Compliance](../examples/compliance_bench/README.md) 五场景合规率 · 见上文 §6.1 |
280
304
  | Extended 帽 | 00/50/链式 PROMPT 不在 Starter 默认包 · 见 [`harness/prompts/README.md`](../harness/prompts/README.md) |
281
305
  | Inform-YAML | **v1.1+** · 可选编辑源 · 须 `graph yaml check` 与 `graph.json` 一致 |
282
306
  | HGM / 图数据库 | **v2.0+ 已实现** `graph ingest|snapshot|axioms` · 本地 JSONL + snapshot;Neo4j / 远端同步 **仍提案** |
283
307
  | Agent-shell | 研究轨 #9,非 npm 功能 |
284
- | rejected→draft | bench S5 场景 v1 未纳入;gate-check 对非 approved 拒 30 |
308
+ | rejected→draft | **v2.0.1+** bench S5 + HGM axioms 事件流精确匹配 |
285
309
 
286
310
  ---
287
311
 
@@ -327,18 +351,31 @@ npx @cyning/harness graph ingest --target /path/to/your-repo
327
351
  # 事件重放 → .cyning-harness/graph/snapshot.json
328
352
  npx @cyning/harness graph snapshot --target /path/to/your-repo
329
353
 
330
- # 公理检查(D2 · D3 · S2 · rejected→draft)
354
+ # 公理检查(D2 · D3 · D4-a · S2 · rejected→draft)
331
355
  npx @cyning/harness graph axioms check --target /path/to/your-repo
332
356
  npx @cyning/harness graph axioms check --target /path/to/your-repo --json
333
357
  ```
334
358
 
335
- ### 13.3 Inform-YAML 的接口
359
+ **公理最小集(v2.0.1)**:D2 · D3 · **D4-a**(`HG-GRAPH-MODULES` pending/rejected + in_progress)· rejected→draft(事件流精确匹配)· S2。
360
+
361
+ ### 13.3 可选 · Git hooks
362
+
363
+ `init` **默认不安装** pre-commit。维护者可手动复制:
364
+
365
+ ```bash
366
+ cp .cyning-harness/hooks/pre-commit.sample .git/hooks/pre-commit
367
+ chmod +x .git/hooks/pre-commit
368
+ ```
369
+
370
+ 示例 hook 调用 `graph ingest --target`(幂等增量)· 失败默认 **warn** 不阻断 commit(可改 `exit 1`)。
371
+
372
+ ### 13.4 与 Inform-YAML 的接口
336
373
 
337
374
  - `InformArtifact` 节点 ID:`inform:{repo_rel_path}`
338
375
  - Task → InformArtifact 边:`MUST_READ`
339
376
  - HGM **不存储** Inform 正文,只存路径指针与 schema 版本
340
377
 
341
- ### 13.4 局限
378
+ ### 13.5 局限
342
379
 
343
380
  - v2.0 默认本地 JSONL + snapshot;**不含** Neo4j / SQLite / 远端同步
344
381
  - 多仓聚合、时光机重建任意时点图:v2.1+ 提案
@@ -370,4 +407,5 @@ npx @cyning/harness graph axioms check --target /path/to/your-repo --json
370
407
  | 2026-06-16 | 补全相对链接 · §6.1 compliance-bench · §12 阅读索引 |
371
408
  | 2026-06-16 | v1.0.1:verify / gate-check / sync index CLI · `--with-scripts` · QUICKREF |
372
409
  | 2026-06-17 | v1.1.0:新增 §10 Inform-YAML · `graph yaml compile|check` · 三轨边界说明 |
410
+ | 2026-06-17 | v2.0.1:D4-a axioms · rejected→draft 精确化 · S5 bench · optional pre-commit hook |
373
411
  | 2026-06-17 | v2.0.0:新增 §13 HGM 过程轨 · `graph ingest|snapshot|axioms` · InformArtifact 与 MUST_READ 边 |
@@ -58,7 +58,7 @@
58
58
 
59
59
  | 能力标签 | 代号 | 须交付(实现) | 文档 |
60
60
  | --- | --- | --- | --- |
61
- | **G1 · v2.0+** | HGM 起步 | `events/*.jsonl` · graph ingest · snapshot · axioms check 子集(**均未实现**) | HGM design v0.1→v0.2 |
61
+ | **G1 · v2.0+** | HGM 起步 | `events/*.jsonl` · graph ingest · snapshot · axioms check 子集(**v2.0.0 已实现**) | HGM design v0.1→v0.2 · [`task_g1`](../../../../docs/harness/tasks/done/cyning-harness/task_cyning_harness_g1_hgm_v2_v1.md) |
62
62
  | **G2 · v2.1+** | HGM 查询 | timeline · patterns · SQLite 投影 | 可选 Neo4j 导出 |
63
63
 
64
64
  **刻意不做(至 v1.0)**:GrowingReasoningAgent · OWL 推理机 · 热插拔 Track(Q5)· Neo4j 默认后端 · **Track G ingest(v1.0 前)**。
@@ -118,8 +118,8 @@ Epic 与子 task 真值:`Projects/docs/harness/tasks/active/`
118
118
  | #7 A4 v1.0 | v1.0.0 · 引用 #8 |
119
119
  | **#8 B2 + bench** | Track B · **1+3 主证据** · P0 绿后优先 |
120
120
  | **#9 Agent-shell** | Track B · **后置** · HGM+agent 后 | deferred · 见 §8 |
121
- | **I-YAML** | Track A · v1.1+ Inform | [`task_y1`](../../../../docs/harness/tasks/active/task_cyning_harness_y1_yaml_inform_v19_v1.md) · YAML P0 后 |
122
- | **Track G G1** | HGM v2.0 | [`task_g1`](../../../../docs/harness/tasks/active/task_cyning_harness_g1_hgm_v2_v1.md) · I-YAML + G0 后 |
121
+ | **I-YAML** | Track A · v1.1+ Inform | [`task_y1`](../../../../docs/harness/tasks/done/cyning-harness/task_cyning_harness_y1_yaml_inform_v19_v1.md) · YAML P0 后 |
122
+ | **Track G G1** | HGM v2.0 | [`task_g1`](../../../../docs/harness/tasks/done/cyning-harness/task_cyning_harness_g1_hgm_v2_v1.md) · I-YAML + G0 后 |
123
123
 
124
124
  **证据策略(2026-06-15 锁定)**:
125
125
 
@@ -9,7 +9,7 @@
9
9
  | **设计真值(已有)** | [`HARNESS_GRAPH_MODEL_design_v0_zh.md`](./HARNESS_GRAPH_MODEL_design_v0_zh.md) |
10
10
  | **语义真值** | [`../product/DESIGN_ONTOLOGY_v1_zh.md`](../product/DESIGN_ONTOLOGY_v1_zh.md) v1.2 |
11
11
  | **L2 路线** | [`../ROADMAP_v1_zh.md`](../ROADMAP_v1_zh.md) §2.2 · §8 |
12
- | **编排 task** | 工作区 [`task_cyning_harness_g1_hgm_v2_v1.md`](../../../../docs/harness/tasks/active/task_cyning_harness_g1_hgm_v2_v1.md) |
12
+ | **编排 task** | 工作区 [`task_cyning_harness_g1_hgm_v2_v1.md`](../../../../docs/harness/tasks/done/cyning-harness/task_cyning_harness_g1_hgm_v2_v1.md) |
13
13
  | **G1 维护者决策** | [`DECISIONS_hgm_g1_maintainer_v1_zh.md`](./DECISIONS_hgm_g1_maintainer_v1_zh.md) |
14
14
  | **Post-G1 序列表** | [`POST_G1_SEQUENCE_v1_zh.md`](../../../../docs/harness/guides/POST_G1_SEQUENCE_v1_zh.md) |
15
15
 
@@ -227,8 +227,8 @@ SEM-02 维持:**主轨 v1.x · HGM 从 v2.0 主版本起** · 不写「v0.5 =
227
227
  | --- | --- |
228
228
  | Post-v1.0 序列表 | [`POST_V1_0_SEQUENCE_v1_zh.md`](../../../../docs/harness/guides/POST_V1_0_SEQUENCE_v1_zh.md) |
229
229
  | 后端 YAML P0 | [`task_engineering_graph_yaml_p0_00_main_v1.md`](../../../../ai-ink-brain-api-python/docs/tasks/active/task_engineering_graph_yaml_p0_00_main_v1.md) |
230
- | 产品 I-YAML | [`task_cyning_harness_y1_yaml_inform_v19_v1.md`](../../../../docs/harness/tasks/active/task_cyning_harness_y1_yaml_inform_v19_v1.md) |
231
- | HGM G1 task | [`task_cyning_harness_g1_hgm_v2_v1.md`](../../../../docs/harness/tasks/active/task_cyning_harness_g1_hgm_v2_v1.md) |
230
+ | 产品 I-YAML | [`task_cyning_harness_y1_yaml_inform_v19_v1.md`](../../../../docs/harness/tasks/done/cyning-harness/task_cyning_harness_y1_yaml_inform_v19_v1.md) |
231
+ | HGM G1 task | [`task_cyning_harness_g1_hgm_v2_v1.md`](../../../../docs/harness/tasks/done/cyning-harness/task_cyning_harness_g1_hgm_v2_v1.md) |
232
232
  | B9(后置) | [`task_cyning_harness_b9_agent_shell_v1.md`](../../../../docs/harness/tasks/active/task_cyning_harness_b9_agent_shell_v1.md) |
233
233
  | A5 v1.0.1 | [`task_cyning_harness_a5_cli_verify_v101_v1.md`](../../../../docs/harness/tasks/active/task_cyning_harness_a5_cli_verify_v101_v1.md) |
234
234
 
@@ -239,9 +239,11 @@ SEM-02 维持:**主轨 v1.x · HGM 从 v2.0 主版本起** · 不写「v0.5 =
239
239
  | 节 | 状态 | 签收人 | 日期 |
240
240
  | --- | --- | --- | --- |
241
241
  | §0 决策摘要 | `confirmed` | (维护者 2026-06-16 口头) | |
242
- | §3 G1 范围 | `pending` | | |
243
- | §5 事件 schema | `pending` | | |
244
- | §6 I-YAML 接口 | `pending` | | |
242
+ | §3 G1 范围 | `confirmed` | 维护者 | 2026-06-17 |
243
+ | §5 事件 schema | `confirmed` | 维护者 | 2026-06-17 |
244
+ | §6 I-YAML 接口 | `confirmed` | 维护者 | 2026-06-17 |
245
+ | §8 agent/B9 | `confirmed` | 维护者 | 2026-06-17 |
246
+ | **G0 整体** | `signed` | 维护者 | 2026-06-17 |
245
247
  | §8 agent/B9 | `pending` | | |
246
248
  | **G0 整体** | `pending` | 全表勾选后开 G1 30 帽 | |
247
249
 
@@ -1,8 +1,8 @@
1
1
  # SDD-Compliance micro-bench · v1
2
2
 
3
3
  > **标题**:SDD-Compliance · **非** Agent Score / pass@1 类模型 benchmark。
4
- > **范围**:S1–S4 · 公理 D2/D3/S2 可机械检查部分 · 不覆盖 D4/D5 与 Terminal-Bench。
5
- > **状态**:v1.0 · B8 冻结 · 4/4 期望合规率 **100%**。
4
+ > **范围**:S1–S5 · 公理 D2/D3/S2/rejected→draft 可机械检查部分 · 不覆盖 D5 与 Terminal-Bench。
5
+ > **状态**:v1.1 · G1.1 · 5/5 期望合规率 **100%**。
6
6
 
7
7
  ---
8
8
 
@@ -12,7 +12,7 @@
12
12
  | --- | --- |
13
13
  | **测什么** | `gate-check.sh` 与 `harness-sync.sh` 是否符合 SDD 公理(合成夹具) |
14
14
  | **不测什么** | LLM 会不会写代码 · 业务测试绿不绿 · 真实项目胜率 |
15
- | **100 是什么** | 合规率 % = PASS 场景 ÷ 总场景 × 100;4/4 全过即 **100** |
15
+ | **100 是什么** | 合规率 % = PASS 场景 ÷ 总场景 × 100;5/5 全过即 **100** |
16
16
  | **和 B2 关系** | B2 Part A 是真实试点 retro;bench 是可机械复现的 **机制回归** |
17
17
 
18
18
  ---
@@ -48,8 +48,7 @@ cd /path/to/cyning-harness
48
48
  | **S2** | R1 approved 无 review | D2/D3 | `S2_r1_no_review/` | 无 `*_audit_R1_*` 文件 | 只改 task 表不算真审核 |
49
49
  | **S3** | R1 approved + review | D2/D3 | `S3_r1_with_review/` | exit=0 且 review 存在 | 审核闭环 Happy Path |
50
50
  | **S4** | sync 域边界 | S2 | `S4_sync_domain/` + 临时仓 | plan 无 `docs/tasks/`、`reviews/` | upgrade 不覆盖用户 task |
51
-
52
- **v1 未纳入**:**S5** rejected→draft(待 gate-check 行为稳定后加夹具)。
51
+ | **S5** | rejected→draft | rejected→draft | `S5_rejected_draft/` | gate rejected + task status=draft | 否决后须回 draft(与 HGM axioms 同语义) |
53
52
 
54
53
  ---
55
54
 
@@ -66,9 +65,9 @@ cd /path/to/cyning-harness
66
65
  场景 结果 详情
67
66
  S1 ✅ PASS gate-check exit=1 · 30 被拒
68
67
  ...
69
- 合规率 4/4 = 100%
68
+ 合规率 5/5 = 100%
70
69
 
71
- 解读 100 = 全部 4 个场景行为符合 SDD 公理
70
+ 解读 100 = 全部 5 个场景行为符合 SDD 公理
72
71
  不是 LLM 解题分数 · 不可外推为「AI 胜率」
73
72
  ```
74
73
 
@@ -78,4 +77,5 @@ S1 ✅ PASS gate-check exit=1 · 30 被拒
78
77
 
79
78
  | 日期 | 说明 |
80
79
  | --- | --- |
80
+ | 2026-06-17 | G1.1:新增 S5 rejected→draft · 5/5 合规率 |
81
81
  | 2026-06-16 | 丰富脚本输出 · 公理/夹具/解读 · quiet 摘要走 stderr |
@@ -0,0 +1,33 @@
1
+ # Task fixture · S5 · rejected→draft 合规态
2
+
3
+ > 用途:SDD-Compliance micro-bench S5 夹具 · gate rejected 后 task 须回 draft(与 HGM axioms 同语义)
4
+
5
+ ---
6
+
7
+ ## Harness 元信息
8
+
9
+ | 字段 | 值 |
10
+ | --- | --- |
11
+ | **task_slug** | `s5_rejected_draft` |
12
+ | **test_strategy** | `not_applicable` |
13
+ | **audit_profile** | `full` |
14
+
15
+ > **状态**:`draft`
16
+
17
+ ### 人工闸
18
+
19
+ | human_gate_id | status | blocks_hats | 说明 |
20
+ | --- | --- | --- | --- |
21
+ | HG-TASK-DRAFT | approved | 22, 30 | 10 完成 |
22
+ | HG-AUDIT-R1 | rejected | 30 | 22 R1 否决 · 已回 draft |
23
+
24
+ ---
25
+
26
+ ## 背景与目标
27
+
28
+ 验证 **rejected→draft** 公理:维护者否决 R1 后,task 须回到 **draft** 状态(非 in_progress)。
29
+
30
+ ## 验收标准
31
+
32
+ - task 表 **status = draft** · **HG-AUDIT-R1 = rejected**
33
+ - bench 判 **合规**(与 `graph axioms check` rejected→draft 语义一致)
@@ -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;
@@ -750,7 +779,7 @@ async function cmdGraphAxioms(args) {
750
779
  const target = resolveTarget(process.cwd(), targetArg);
751
780
  const events = loadEvents(target);
752
781
  const snapshot = buildSnapshot(events);
753
- const result = checkAxioms(snapshot);
782
+ const result = checkAxioms(snapshot, events);
754
783
 
755
784
  if (json) {
756
785
  console.log(JSON.stringify(result, null, 2));
package/lib/graph-hgm.js CHANGED
@@ -226,6 +226,19 @@ export function ingestRepo(target, options = {}) {
226
226
  }
227
227
  }
228
228
 
229
+ const hasRejectedGate = parsed.gates.some((g) => g.status === 'rejected');
230
+ if (hasRejectedGate && parsed.status === 'draft') {
231
+ events.push({
232
+ event_id: eventId(new Date(occurredAt), seq++),
233
+ type: 'TaskStatusChanged',
234
+ occurred_at: occurredAt,
235
+ actor,
236
+ subject: taskSubject,
237
+ data: { task_slug: parsed.task_slug, new_status: 'draft', reason: 'rejected→draft from task table' },
238
+ source,
239
+ });
240
+ }
241
+
229
242
  for (const mr of parsed.must_read) {
230
243
  events.push({
231
244
  event_id: eventId(new Date(occurredAt), seq++),
@@ -454,9 +467,63 @@ export function buildSnapshot(events) {
454
467
  }
455
468
 
456
469
  /**
457
- * 公理检查:D2 · D3 · rejected→draft · S2 事件审计。
470
+ * 从事件流提取 rejected 事件(GateStatusChanged rejected HumanGateRejected)。
471
+ */
472
+ function collectRejectedEvents(events) {
473
+ if (!events || events.length === 0) return [];
474
+ return events.filter(
475
+ (e) =>
476
+ e.type === 'HumanGateRejected' ||
477
+ (e.type === 'GateStatusChanged' && e.data?.new_status === 'rejected'),
478
+ );
479
+ }
480
+
481
+ /**
482
+ * rejected→draft:重放事件流,rejected 后须同 task 的 TaskStatusChanged(draft)。
483
+ */
484
+ function checkRejectedToDraft(events) {
485
+ const violations = [];
486
+ if (!events || events.length === 0) return violations;
487
+
488
+ const sorted = [...events].sort(
489
+ (a, b) => a.occurred_at.localeCompare(b.occurred_at) || a.event_id.localeCompare(b.event_id),
490
+ );
491
+
492
+ for (let i = 0; i < sorted.length; i += 1) {
493
+ const rej = sorted[i];
494
+ if (
495
+ rej.type !== 'HumanGateRejected' &&
496
+ !(rej.type === 'GateStatusChanged' && rej.data?.new_status === 'rejected')
497
+ ) {
498
+ continue;
499
+ }
500
+ const taskSlug = rej.data?.task_slug;
501
+ if (!taskSlug) continue;
502
+ const taskSubject = `task:${taskSlug}`;
503
+ const hasDraftFollowUp = sorted.slice(i + 1).some(
504
+ (e) =>
505
+ e.type === 'TaskStatusChanged' &&
506
+ e.subject === taskSubject &&
507
+ e.data?.new_status === 'draft',
508
+ );
509
+ if (!hasDraftFollowUp) {
510
+ violations.push({
511
+ axiom: 'rejected→draft',
512
+ severity: 'error',
513
+ message: `gate rejected(${rej.event_id})后缺少 TaskStatusChanged(draft) 回退`,
514
+ node: rej.subject,
515
+ });
516
+ }
517
+ }
518
+ return violations;
519
+ }
520
+
521
+ /**
522
+ * 公理检查:D2 · D3 · D4-a · rejected→draft · S2 事件审计。
523
+ * @param {object} snapshot - buildSnapshot 产物
524
+ * @param {object[]|null} events - 原始事件流(rejected→draft 精确匹配须传入)
458
525
  */
459
- export function checkAxioms(snapshot) {
526
+ export function checkAxioms(snapshot, events = null) {
460
527
  const violations = [];
461
528
  const { nodes, edges, projections } = snapshot;
462
529
  const nodeMap = new Map(Object.entries(nodes));
@@ -465,10 +532,6 @@ export function checkAxioms(snapshot) {
465
532
  return edges.filter((e) => e.from === id && e.type === type);
466
533
  }
467
534
 
468
- function hasEdge(from, to, type) {
469
- return edges.some((e) => e.from === from && e.to === to && e.type === type);
470
- }
471
-
472
535
  // D2: pending 的 HG-AUDIT-R1 BLOCKS 30 帽
473
536
  for (const [id, node] of nodeMap) {
474
537
  if (node.kind === 'HumanGate' && node.status === 'pending') {
@@ -501,17 +564,39 @@ export function checkAxioms(snapshot) {
501
564
  }
502
565
  }
503
566
 
504
- // rejected→draft: rejected 后必须有 TaskStatusChanged(draft)
505
- for (const eventId of projections.rejected_events || []) {
506
- // snapshot 保留 event_id,但需事件流重放才能精确检查;这里只标记缺失 draft 回退
507
- violations.push({
508
- axiom: 'rejected→draft',
509
- severity: 'warn',
510
- message: `gate rejected 事件 ${eventId} 须后续 TaskStatusChanged(draft)`,
511
- node: eventId,
512
- });
567
+ // D4-a: in_progress 改码 task 须 HG-GRAPH-MODULES approved
568
+ for (const [taskId, status] of Object.entries(projections.task_status || {})) {
569
+ if (status !== 'in_progress') continue;
570
+ const taskSlug = taskId.replace(/^task:/, '');
571
+ const graphGateId = `gate:${taskSlug}:HG-GRAPH-MODULES`;
572
+ const graphGate = nodeMap.get(graphGateId);
573
+ if (graphGate && (graphGate.status === 'pending' || graphGate.status === 'rejected')) {
574
+ violations.push({
575
+ axiom: 'D4-a',
576
+ severity: 'error',
577
+ message: `task ${taskId} in_progress 但 HG-GRAPH-MODULES=${graphGate.status}`,
578
+ node: graphGateId,
579
+ });
580
+ }
581
+ const mustReadTargets = edges
582
+ .filter((e) => e.from === taskId && e.type === 'MUST_READ')
583
+ .map((e) => e.to.replace(/^inform:/, ''));
584
+ const hasInformGraph = mustReadTargets.some(
585
+ (p) => p.includes('01_struct') || p.includes('_tech_graph'),
586
+ );
587
+ if (mustReadTargets.length > 0 && !hasInformGraph) {
588
+ violations.push({
589
+ axiom: 'D4-a',
590
+ severity: 'warn',
591
+ message: `task ${taskId} MUST_READ 缺 01_struct / _tech_graph InformArtifact`,
592
+ node: taskId,
593
+ });
594
+ }
513
595
  }
514
596
 
597
+ // rejected→draft: 事件流精确匹配
598
+ violations.push(...checkRejectedToDraft(events));
599
+
515
600
  // S2: SyncOperation 不得 touch S2 路径
516
601
  const s2Prefixes = ['docs/tasks/', 'docs/harness/reviews/', 'docs/harness/invokes/by-task/'];
517
602
  for (const edge of edges) {
@@ -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.0",
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–S4
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] [--quiet]
15
+ 用法:compliance-bench.sh [--all | S1 S2 S3 S4 S5 S6] [--quiet]
16
16
 
17
- --all 运行 S1–S4(默认输出含公理解释与摘要表)
17
+ --all 运行 S1–S6(默认输出含公理解释与摘要表)
18
18
  --quiet CI/脚本模式:stdout 仅打印合规率数字;摘要说明走 stderr
19
19
 
20
20
  说明:
21
21
  合规率 = PASS 场景数 ÷ 总场景数 × 100。
22
- 100 表示 S1–S4 全部通过,即 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); shift ;;
33
+ --all) SCENARIOS=(S1 S2 S3 S4 S5 S6); shift ;;
34
34
  --quiet) QUIET=1; shift ;;
35
- S1|S2|S3|S4) 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
@@ -53,14 +53,26 @@ log_scenario() {
53
53
  [[ "$QUIET" == "1" ]] || echo "$@"
54
54
  }
55
55
 
56
+ gate_status() {
57
+ local file="$1" gate="$2"
58
+ awk -F'|' -v g="$gate" '
59
+ $0 ~ /^[[:space:]]*\|/ && index($0, g) > 0 {
60
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", $3)
61
+ gsub(/\*/, "", $3)
62
+ print $3
63
+ exit
64
+ }
65
+ ' "$file"
66
+ }
67
+
56
68
  print_banner() {
57
69
  [[ "$QUIET" == "1" ]] && return 0
58
70
  log_scenario "╔══════════════════════════════════════════════════════════════╗"
59
- log_scenario "║ SDD-Compliance micro-bench · v1 · S1–S4 ║"
71
+ log_scenario "║ SDD-Compliance micro-bench · v1 · S1–S6 ║"
60
72
  log_scenario "╚══════════════════════════════════════════════════════════════╝"
61
73
  log_scenario ""
62
74
  log_scenario "性质 机械合规测试 · 合成夹具 · 不调用 LLM"
63
- log_scenario "公理 D2 HumanGate · D3 30 前置闸 · S2 sync 域边界"
75
+ log_scenario "公理 D2 HumanGate · D3 30 前置闸 · S2 sync 域 · rejected→draft"
64
76
  log_scenario "夹具 $BENCH_DIR/"
65
77
  log_scenario "场景 ${SCENARIOS[*]}"
66
78
  log_scenario ""
@@ -199,6 +211,62 @@ scenario_S4() {
199
211
  fi
200
212
  }
201
213
 
214
+ task_status() {
215
+ local file="$1"
216
+ sed -n 's/.*\*\*状态\*\*[^`]*`\([^`]*\)`.*/\1/p' "$file" | head -1
217
+ }
218
+
219
+ scenario_S5() {
220
+ local dir="$BENCH_DIR/S5_rejected_draft"
221
+ local tf="$dir/task.md"
222
+ scenario_header "S5" "rejected→draft · task 须回 draft" \
223
+ "rejected→draft(与 HGM axioms 同语义)" \
224
+ "examples/compliance_bench/S5_rejected_draft/" \
225
+ "HG-AUDIT-R1=rejected 且 task status=draft → 合规"
226
+
227
+ local audit status
228
+ audit="$(gate_status "$tf" HG-AUDIT-R1)"
229
+ status="$(task_status "$tf")"
230
+
231
+ if [[ "$audit" == "rejected" && "$status" == "draft" ]]; then
232
+ scenario_footer "S5" "PASS" "gate rejected · task draft" \
233
+ "否决后 task 已回 draft · 符合 rejected→draft 公理(bench 静态态 · axioms 为事件流真值)"
234
+ PASS=$((PASS+1))
235
+ RESULTS+=("S5|PASS|rejected gate + draft status")
236
+ else
237
+ scenario_footer "S5" "FAIL" "audit=$audit status=$status" \
238
+ "rejected 后 task 须 draft · 检查夹具或 gate-check 行为"
239
+ FAIL=$((FAIL+1))
240
+ RESULTS+=("S5|FAIL|audit=$audit status=$status")
241
+ fi
242
+ }
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
+
202
270
  print_summary() {
203
271
  local total=$((PASS+FAIL))
204
272
  local rate=0
@@ -243,6 +311,8 @@ for s in "${SCENARIOS[@]}"; do
243
311
  S2) scenario_S2 ;;
244
312
  S3) scenario_S3 ;;
245
313
  S4) scenario_S4 ;;
314
+ S5) scenario_S5 ;;
315
+ S6) scenario_S6 ;;
246
316
  esac
247
317
  done
248
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}`);