@hunter-harness/workflow-harness 0.2.69 → 0.2.70
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +31 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +20 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/claude-code/harness-test/reference.md +9 -0
- package/harness/bundles/general/claude-code/scripts/harness_context.py +27 -7
- package/harness/bundles/general/claude-code/scripts/harness_events.py +12 -3
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/claude-code/scripts/harness_service.py +69 -15
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +31 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +20 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/codebuddy/harness-test/reference.md +9 -0
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +27 -7
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +12 -3
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +69 -15
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +31 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-run/reference.md +20 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/codex/harness-test/reference.md +9 -0
- package/harness/bundles/general/codex/scripts/harness_context.py +27 -7
- package/harness/bundles/general/codex/scripts/harness_events.py +12 -3
- package/harness/bundles/general/codex/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/codex/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/codex/scripts/harness_service.py +69 -15
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +31 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +20 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-test/pitfalls.md +34 -3
- package/harness/bundles/general/cursor/harness-test/reference.md +9 -0
- package/harness/bundles/general/cursor/scripts/harness_context.py +27 -7
- package/harness/bundles/general/cursor/scripts/harness_events.py +12 -3
- package/harness/bundles/general/cursor/scripts/harness_gate.py +127 -11
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +244 -5
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/general/cursor/scripts/harness_service.py +69 -15
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +31 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/claude-code/scripts/harness_context.py +27 -7
- package/harness/bundles/java/claude-code/scripts/harness_events.py +12 -3
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/claude-code/scripts/harness_service.py +69 -15
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +31 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +27 -7
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +12 -3
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +69 -15
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +31 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/codex/scripts/harness_context.py +27 -7
- package/harness/bundles/java/codex/scripts/harness_events.py +12 -3
- package/harness/bundles/java/codex/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/codex/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/codex/scripts/harness_service.py +69 -15
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +48 -1
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +31 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-test/pitfalls.md +34 -3
- package/harness/bundles/java/cursor/scripts/harness_context.py +27 -7
- package/harness/bundles/java/cursor/scripts/harness_events.py +12 -3
- package/harness/bundles/java/cursor/scripts/harness_gate.py +127 -11
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +244 -5
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +254 -19
- package/harness/bundles/java/cursor/scripts/harness_service.py +69 -15
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +48 -1
- package/harness/manifests/general/claude-code.json +26 -26
- package/harness/manifests/general/codebuddy.json +26 -26
- package/harness/manifests/general/codex.json +26 -26
- package/harness/manifests/general/cursor.json +26 -26
- package/harness/manifests/java/claude-code.json +26 -26
- package/harness/manifests/java/codebuddy.json +26 -26
- package/harness/manifests/java/codex.json +26 -26
- package/harness/manifests/java/cursor.json +26 -26
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -357,7 +357,14 @@ npx hunter-harness plan evidence-pack --input .harness/changes/<cn>/meta/plan-ev
|
|
|
357
357
|
npx hunter-harness plan finalize --input .harness/changes/<cn>/meta/plan-evidence.json
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
-
**自然输入文件**(`meta/plan-evidence-input.json`,权威定义 `packages/cli/src/commands/plan-evidence-pack.ts` 的 `EvidencePackInputFile
|
|
360
|
+
**自然输入文件**(`meta/plan-evidence-input.json`,权威定义 `packages/cli/src/commands/plan-evidence-pack.ts` 的 `EvidencePackInputFile`)由规划阶段逐步沉淀,各字段定稿时点不得倒置。
|
|
361
|
+
|
|
362
|
+
> 📋 **先取骨架,别猜结构**:CLI 自带模板输出,不需要去找 TS 接口或翻 npx 缓存——
|
|
363
|
+
> ```bash
|
|
364
|
+
> npx hunter-harness plan evidence-pack --print-template > .harness/changes/<cn>/meta/plan-evidence-input.json
|
|
365
|
+
> ```
|
|
366
|
+
> 输出是带 `<...>` 占位符的完整骨架,逐项替换即可;替换完 grep 一次 `<` 自检有无遗漏。
|
|
367
|
+
> `--print-template` 不读写任何文件,只打到 stdout。
|
|
361
368
|
|
|
362
369
|
| 字段 | 内容 | 定稿阶段 |
|
|
363
370
|
|------|------|:---:|
|
|
@@ -379,6 +386,29 @@ npx hunter-harness plan finalize --input .harness/changes/<cn>/meta/plan-evidenc
|
|
|
379
386
|
- **legacy 收据**:过渡期(阶段 14 前)v2 路径不写 `plan-finalization.json`;消费方若仍读 legacy receipt,由兼容投影单独提供,不得反向要求 v2 双写。
|
|
380
387
|
- **回退**:v2 自然输入不完整(如缺真实审批记录)时才允许走 Python finalizer(legacy 路径);同一次发布不得两条路径混用。
|
|
381
388
|
|
|
389
|
+
### 发布后修订计划(republish)
|
|
390
|
+
|
|
391
|
+
计划发布后又要改产物,是**正常且高频**的情况——用户看完计划补一个回归场景、修正一条任务、调整验收标准。这时不要与哈希守卫搏斗:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
python <skills-root>/scripts/harness_plan_finalize.py republish \
|
|
395
|
+
--change-dir ".harness/changes/<cn>" --staging-dir ".harness/changes/<cn>/runtime/plan-staging" \
|
|
396
|
+
--change <cn> --run-id "plan_$(uuidgen)" --reason "<为什么要改>" --json
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
它一次性完成整套动作:校验 staging → 分配新 attempt(自动取已用最大值 +1)→ 追加 `phase.start` → 替换收据 → 重新派生 `scenario-manifest.json` 与 `implementation-checkpoints.json` → 写 `phase.end`。收据里保留 `supersedes`(被取代的 hash/runId/attempt)与 `amendReason`,修订全程可审计。
|
|
400
|
+
|
|
401
|
+
| 约束 | 说明 |
|
|
402
|
+
|------|------|
|
|
403
|
+
| `--reason` 必填 | 修订已发布计划必须留下理由,否则 `PLAN_AMEND_REASON_REQUIRED` |
|
|
404
|
+
| `--run-id` 必须全新 | 复用旧 run-id 报 `PLAN_AMEND_RUN_ID_IN_USE`(attempt 与 run-id 一一绑定) |
|
|
405
|
+
| 首次发布不能用它 | 无收据时报 `PLAN_NOT_FINALIZED`,首次发布走 `finalize` |
|
|
406
|
+
| 内容没变则空操作 | 返回 `idempotent:true`,不写事件、不消耗 attempt |
|
|
407
|
+
|
|
408
|
+
⚠️ **绝对不要手改 `meta/scenario-manifest.json`**。它是 finalizer 从 `test-scenarios.md` 派生的产物,手改会造成真实漂移:`verify` 报 `ARTIFACT_HASH_DRIFT`,run 阶段 `validate_plan_handoff` 也会记 WARN。`republish` 会重新派生它,这才是唯一正确入口。
|
|
409
|
+
|
|
410
|
+
> 直接重跑 `finalize` 会报 `PLAN_FINALIZATION_HASH_CONFLICT`——这是守卫在防止发布后产物被悄悄改动,不是 bug。报错信息里已经给出 `republish` 命令行。
|
|
411
|
+
|
|
382
412
|
### Plan 结束行为规则
|
|
383
413
|
|
|
384
414
|
- **禁止询问执行模式**:Subagent-Driven / Inline Execution 属于 /harness-run 阶段
|
|
@@ -3,7 +3,7 @@ name: harness-pull
|
|
|
3
3
|
description: 从 Hunter Platform 下拉配置/规则/架构/指令(及显式来源分支的分支文件恢复)。仅当用户显式调用
|
|
4
4
|
/harness-pull 或明确说'从平台拉取/恢复'时使用;不得自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-pull — 从 Hunter Platform 下拉与恢复
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-push
|
|
|
3
3
|
description: 上传本地配置/规则/架构/指令(及显式归档)到 Hunter Platform。仅当用户显式调用 /harness-push
|
|
4
4
|
或明确说'上传到平台'时使用;不得因存在本地修改就自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-push — 上传到 Hunter Platform
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -4,7 +4,7 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
|
|
|
4
4
|
.harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在
|
|
5
5
|
test 结束后自动接续执行。
|
|
6
6
|
---
|
|
7
|
-
<!-- generated by harness_deploy.py; core=
|
|
7
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
8
8
|
# harness-review — 代码审查
|
|
9
9
|
|
|
10
10
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-run
|
|
|
3
3
|
description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。仅当用户显式调用
|
|
4
4
|
/harness-run 时使用;不得因用户提到编码/实现就自动触发,也不得被其他阶段 skill 自动接续。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-run — 需求编码
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -68,6 +68,8 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
|
|
|
68
68
|
|
|
69
69
|
**阶段归属规则**:只用 `ownerPhase=run` 的任务和场景判定编码阶段结果。`ownerPhase=test` 的任务或场景按计划留给测试阶段属于正常移交,必须记录为“待测试阶段执行”,不得将编码阶段降级为 WARN;只有 run 自身负责的工作未完成、验证降级或证据异常时才使用 WARN。
|
|
70
70
|
|
|
71
|
+
> 关门脚本与本规则一致:`harness_gate.py close --phase run` 的 C9 场景覆盖只要求 `ownerPhase` 为 `plan`/`run` 的必需场景有通过 receipt,`ownerPhase=test` 的场景出现在返回值的 `deferred` 里,不阻断 run。若 run 关门报 `REQUIRED_SCENARIO_NOT_EXECUTED` 且缺的是接口/端到端场景,那是 `meta/scenario-manifest.json` 里 `ownerPhase` 标错了(或老清单没声明),应当修清单——**不要**为了过门在 run 阶段起服务补跑本属 test 的验证。
|
|
72
|
+
|
|
71
73
|
**Fixback**:入口只用 `launch-review`,后续问题处理通过 `resolve-issue/close` 驱动,不得把修复说明当成新的普通 Run。只读取返回的受影响问题和文件;验证仅失效与 `changedFiles` 相交的目标,其他 Test/Review 证据继续复用。RED 优先;`manual`、`workflow` 或未选用的建议不进入代码批次,使用中文记录处理结论。
|
|
72
74
|
|
|
73
75
|
**执行器边界**:优先使用项目 build profile 和已有测试入口。禁止为了绕过 ESM、路径或参数问题临时生成 `.js`、`require` 脚本;需要文件式 runner 时使用项目已有入口,确需新增时遵循项目模块类型(例如 ESM 使用 `.mjs`)。runner 包装说明写入 `runnerCommand` 元数据,不得拼进账本的规范 `command`。
|
|
@@ -3,7 +3,7 @@ name: harness-submit
|
|
|
3
3
|
description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。仅当用户显式调用
|
|
4
4
|
/harness-submit(或 /harness-merge 重入合并段)时使用;用户口头说'提交/commit/push'时必须先确认,不得自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -4,7 +4,7 @@ description: Use when the user asks to synchronize, refresh, or validate Harness
|
|
|
4
4
|
metadata, adapters, remote knowledge ownership, instruction entrypoints,
|
|
5
5
|
config origins, or CodeGraph status.
|
|
6
6
|
---
|
|
7
|
-
<!-- generated by harness_deploy.py; core=
|
|
7
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
8
8
|
# harness-sync
|
|
9
9
|
|
|
10
10
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-test
|
|
|
3
3
|
description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test
|
|
4
4
|
时使用;不得在 run 结束后自动接续执行。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=9a77548e8651f505; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-test — 测试执行
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -195,6 +195,8 @@ python <skills-root>/scripts/harness_test_guard.py record --project . --change-d
|
|
|
195
195
|
|
|
196
196
|
先读 `.harness/changes/<change-name>/runtime/credential-cache.json`(认证凭证缓存,按项目认证机制;token/SSO 为常见实现),本地轻量接口验证通过则复用,失败才走远程认证。接口测试执行器用 request context / 原生 HTTP 客户端直连本地 baseURL,**不得依赖浏览器当前页面 origin**。同一次流程内凭证刷新计数 > 1 → 🟡 WARN。**不得在报告/日志/对话总结中输出明文凭证**。详见 `reference.md`「认证凭证缓存与复用」。
|
|
197
197
|
|
|
198
|
+
> ⛔ **验证码 = 硬停,不是待解的技术问题。** 登录响应出现 `验证码` / `captcha` / `blockPuzzle` / `slider` / `geetest` 等特征时,**禁止**编写或运行任何求解代码(图像匹配、OCR、打码平台、反编译服务端找容差均在禁止之列)。立即记 `apiTest=BLOCKED`,请用户手工把凭证写入 `runtime/credential-cache.json` 或临时关闭测试环境验证码,并提示 `.harness/config/harness-test-config.md` 的认证方式已过期。详见 `pitfalls.md` 规则 31。
|
|
199
|
+
|
|
198
200
|
### 九、测试报告状态规则
|
|
199
201
|
|
|
200
202
|
整体 ✅OK / 🟡WARN / ❌FAIL 三态;API 维度使用 `OK` / `PARTIAL` / `BLOCKED` / `NOT_RUN` / `FAIL` 五态。**不得把「5 PASS + 9 BLOCKED + 1 FAIL」写成 `apiTest=NOT_RUN`**,正确为 `apiTest=PARTIAL`。P0 场景 BLOCKED 不得仍 OK。详见 `reference.md`「结果分级规则」。
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
description: harness-test 的踩坑规则(来自真实对话日志)。执行测试前必须通读,遇到测试失败时重新参考。
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
# 避坑规则(
|
|
5
|
+
# 避坑规则(32 条)
|
|
6
6
|
|
|
7
|
-
> 以下规则来自真实对话日志(2026-06-12 ~
|
|
7
|
+
> 以下规则来自真实对话日志(2026-06-12 ~ 08-17),每个都有明确的现象和根因。
|
|
8
8
|
> 执行 `harness-test` 时必须通读,避免重蹈覆辙。
|
|
9
9
|
|
|
10
10
|
| # | 规则 | 现象 | 根因 | 解法 |
|
|
11
11
|
|:--:|------|------|------|------|
|
|
12
12
|
| 1 | 凭证可展示但注意安全 | 对话日志记录了凭证 | Bearer 凭证被粘贴到聊天中 | 凭证可以在对话中展示用于调试,但不要写入持久化文件(测试报告、执行日志等) |
|
|
13
13
|
| 2 | 不用 curl 发中文 | `Invalid UTF-8 start byte 0xb2` | Windows curl 默认 GBK 编码 | 用 PowerShell Invoke-WebRequest 或 Playwright fetch |
|
|
14
|
-
| 3 | 必须带租户标识 | 所有接口 500 |
|
|
14
|
+
| 3 | 必须带租户标识 | 所有接口 500,或 200 但 `data:null` | 租户上下文为空,或请求头租户值与测试数据行的租户不一致 | 请求头加租户标识(见 build-profile.httpHeaders);**取值必须等于测试数据实际所属租户**,先查数据行的租户列再填,不要默认写 0 |
|
|
15
15
|
| 4 | 认证降级 | 接口 401 "账号未登录" | 本地无认证服务 | 应用配置放行(如 permit-all-urls)或权限降级 |
|
|
16
16
|
| 5 | 错误码双格式兼容 | 测试断言失败 | 预期 `1_003_002_009`,实际 `1003002009` | 自动兼容下划线和数字两种格式 |
|
|
17
17
|
| 6 | 编译产物确认 | 修改代码后测试结果不变 | IDE 热重载未触发 | 测试前编译(如 `mvn compile -o`) |
|
|
@@ -39,6 +39,8 @@ description: harness-test 的踩坑规则(来自真实对话日志)。执行
|
|
|
39
39
|
| 28 | **已有服务未先决策就跑业务接口** | 旧服务不含新代码,接口 500 后才发现版本不匹配 | 检测到已有应用服务(端口被占)后,未先展示服务决策门就跑业务接口 | 先展示服务决策门,询问复用/重启/跳过/停止;询问前只允许 health/meta 检查 |
|
|
40
40
|
| 29 | **启动命令反复试相对配置叠加 / 中文路径** | 相对路径或中文路径导致应用读不到运行时配置叠加,启动失败 | 默认只用 `C:/temp/harness-test-overlay/<change>/application-harness-test.yml` ASCII 绝对路径,并固化已知良好测试配置 |
|
|
41
41
|
| 30 | **伪 diffHash 导致错误复用** | `3files-84plus-5minus` 不能证明代码未变 | 用 `git diff --binary` 生成 patch 并计算 SHA-256,ledger 只认 `sha256:<hash>` |
|
|
42
|
+
| 31 | **尝试破解验证码 / 人机验证** | 认证服务返回"验证码不能为空",AI 写图像识别脚本反复试坐标(实测 28 次 / 4 种算法全失败) | 测试配置里的账号密码登录流程被加了滑块/图形验证码,配置未同步 | **硬停**。禁止编写或运行任何验证码求解代码;立即标 `apiTest=BLOCKED`,请用户手工提供凭证写入 `runtime/credential-cache.json`,或临时关闭测试环境验证码 |
|
|
43
|
+
| 32 | **预存环境缺陷被当成本次变更的阻塞** | `unitTestFull` 144 个测试挂 1 个,根因是别人提交引入的缺省配置缺失 | 全量验证把预存缺陷和本次 diff 混在一起判定 | 先用 `git log -S` + diff 触点证明与本次变更无关,再按「预存缺陷」上报并请用户决策(修根因 / 记 🟡WARN / 停止),**不得**擅自改 tracked 配置或排除测试 |
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
## 详细说明
|
|
@@ -257,6 +259,35 @@ $cred = $resp.data.accessToken
|
|
|
257
259
|
**后果**:代码已变却复用旧测试结果,掩盖回归问题
|
|
258
260
|
**正确做法**:用 `git diff --binary` 生成 patch 并计算 SHA-256,ledger 只认 `sha256:<hash>` 格式。diffHash 不一致时必须重跑相关测试。
|
|
259
261
|
|
|
262
|
+
### 规则 31:尝试破解验证码 / 人机验证
|
|
263
|
+
**严重度**:❌FAIL(硬性禁止)
|
|
264
|
+
**场景**:本地服务已起来,但业务接口需要真实登录上下文;认证服务登录接口返回 `验证码不能为空` / 返回滑块拼图(如 AJ-Captcha `blockPuzzle`)/ 图形验证码 / 短信验证码
|
|
265
|
+
**后果**:真实日志里 AI 连续写了 4 版求解算法(边缘求和 → NCC 模板匹配 → 二维边缘匹配 → 一维归一化互相关)、反编译服务端 jar 找容差、共 28 次尝试全部失败,烧掉整段会话,最终仍然 BLOCKED
|
|
266
|
+
**正确做法**:**验证码是安全控制,不是待调试的技术问题。** 一旦识别到验证码/人机验证:
|
|
267
|
+
|
|
268
|
+
1. **立即停止**,不得编写、下载或运行任何验证码求解代码(图像匹配、OCR、打码平台、反编译校验逻辑找容差都在禁止之列)
|
|
269
|
+
2. 该维度记 `apiTest=BLOCKED`,受影响场景标 🟡 BLOCKED,**不得**伪造 receipt 或把未执行写成通过
|
|
270
|
+
3. 向用户输出原文,并给出三条可选出路:
|
|
271
|
+
- 用户手工登录后把凭证写入 `.harness/changes/<change-name>/runtime/credential-cache.json`(推荐,执行器本来就先读 cache)
|
|
272
|
+
- 用户在测试环境临时关闭验证码开关,AI 重跑
|
|
273
|
+
- 本轮跳过接口验证,按 BLOCKED 归档,留待环境就绪
|
|
274
|
+
4. 顺手提示更新 `.harness/config/harness-test-config.md`——认证方式已变更,配置已过期
|
|
275
|
+
|
|
276
|
+
> 判据:只要登录响应里出现 `验证码` / `captcha` / `blockPuzzle` / `slider` / `geetest` / `secretKey + originalImageBase64` 任一特征,即触发本规则。
|
|
277
|
+
|
|
278
|
+
### 规则 32:预存环境缺陷被当成本次变更的阻塞
|
|
279
|
+
**严重度**:🟡WARN
|
|
280
|
+
**场景**:模块全量验证(如 `unitTestFull`)失败,但失败点在本次 diff 之外——例如别人提交的类新增了无默认值的配置占位符,导致某个全上下文测试无法加载
|
|
281
|
+
**后果**:要么误判成自己的回归浪费时间排查,要么擅自改 tracked 配置 / 排除测试把问题掩盖掉,污染 diff
|
|
282
|
+
**正确做法**:
|
|
283
|
+
|
|
284
|
+
1. **先证明无关**:`git log -S '<符号>' --oneline` 定位引入提交 + `git diff` 证明本次触点不涉及该文件/目录,两条证据都要有
|
|
285
|
+
2. **再上报**:按「预存缺陷」向用户说明——受影响模块、失败文件、引入提交、不阻断依据
|
|
286
|
+
3. **由用户决策**:修根因(会扩大 diff,需明确点头)/ 记 🟡WARN 继续 / 停止本轮
|
|
287
|
+
4. **禁止自作主张**:不得改 tracked 配置文件、不得 `-Dtest=` 排除失败测试、不得偏离 build-profile 的规范命令——这些都会让证据与 ledger 命令不一致
|
|
288
|
+
|
|
289
|
+
> 用户批准修复后,该修复文件即进入本次 diff:必须在提交前复核它仍在工作区(真实日志里用户中途手工还原了该修复,AI 直到 staging 才发现)。
|
|
290
|
+
|
|
260
291
|
> diffHash/ledger 复用规则遵循 `../protocols/ledger-protocol.md`;结果证据要求遵循 `../protocols/evidence-based-reporting-protocol.md`。
|
|
261
292
|
|
|
262
293
|
## BOM-safe JSON / identifier sanitizer / 凭据扫描 (runtime-helpers.mjs)
|
|
@@ -219,10 +219,30 @@ def _same_repository(project: Path, candidate: Path) -> bool:
|
|
|
219
219
|
return candidate.is_relative_to(project)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
def _contract_error_code(exc: BaseException) -> str:
|
|
223
|
+
"""Keep PROJECT_ROOT_INVALID distinct from a genuinely missing change."""
|
|
224
|
+
if str(exc).startswith("PROJECT_ROOT_INVALID"):
|
|
225
|
+
return "PROJECT_ROOT_INVALID"
|
|
226
|
+
return "CHANGE_NOT_FOUND"
|
|
227
|
+
|
|
228
|
+
|
|
222
229
|
def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
|
|
223
230
|
root = project.resolve()
|
|
224
|
-
|
|
231
|
+
# A bare project *name* (--project udp) resolves to <cwd>/udp and then
|
|
232
|
+
# reports CHANGE_NOT_FOUND, which sends the caller hunting for the change
|
|
233
|
+
# instead of fixing the argument. Separate the two failures.
|
|
234
|
+
if not root.is_dir():
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"PROJECT_ROOT_INVALID: {root} is not a directory — --project takes "
|
|
237
|
+
"a path to the project root (use '.'), not the project name"
|
|
238
|
+
)
|
|
225
239
|
changes_root = (root / ".harness" / "changes").resolve()
|
|
240
|
+
if not changes_root.is_dir():
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"PROJECT_ROOT_INVALID: {changes_root} does not exist — "
|
|
243
|
+
f"{root} is not a harness project root"
|
|
244
|
+
)
|
|
245
|
+
contract_root = (root / ".harness" / "changes" / change).resolve()
|
|
226
246
|
if not contract_root.is_relative_to(changes_root) or not contract_root.is_dir():
|
|
227
247
|
raise ValueError(f"CHANGE_NOT_FOUND: {change}")
|
|
228
248
|
context_path = contract_root / "meta" / "change-context.json"
|
|
@@ -296,7 +316,7 @@ def configure_phase_plan(
|
|
|
296
316
|
try:
|
|
297
317
|
contract_root, _contract_data, _state_root = _contract(project, change)
|
|
298
318
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
299
|
-
return {"ok": False, "code":
|
|
319
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
300
320
|
normalized = [str(item).strip() for item in phases if str(item).strip()]
|
|
301
321
|
if (
|
|
302
322
|
not normalized
|
|
@@ -802,7 +822,7 @@ def close_transition(
|
|
|
802
822
|
try:
|
|
803
823
|
contract_root, _contract_data, state_root = _contract(project, change)
|
|
804
824
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
805
|
-
return {"ok": False, "code":
|
|
825
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
806
826
|
allowed_next = _allowed_next_phases(contract_root, from_phase)
|
|
807
827
|
if to_phase not in allowed_next:
|
|
808
828
|
planned_phases, source = _phase_plan(contract_root)
|
|
@@ -962,7 +982,7 @@ def _begin_transition_unlocked(
|
|
|
962
982
|
try:
|
|
963
983
|
contract_root, _contract_data, state_root = _contract(project, change)
|
|
964
984
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
965
|
-
return {"ok": False, "code":
|
|
985
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
966
986
|
paths = _paths(state_root)
|
|
967
987
|
try:
|
|
968
988
|
execution_root = _execution_root(project, contract_root, state_root)
|
|
@@ -1074,7 +1094,7 @@ def begin_transition(
|
|
|
1074
1094
|
try:
|
|
1075
1095
|
_contract_root, _contract_data, state_root = _contract(project, change)
|
|
1076
1096
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1077
|
-
return {"ok": False, "code":
|
|
1097
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
1078
1098
|
paths = _paths(state_root)
|
|
1079
1099
|
with _exclusive_state_lock(paths["runtime"] / "branch-selection.lock"):
|
|
1080
1100
|
return _begin_transition_unlocked(
|
|
@@ -1100,7 +1120,7 @@ def cancel_prepared_context(
|
|
|
1100
1120
|
try:
|
|
1101
1121
|
_contract_root, _contract_data, state_root = _contract(project, change)
|
|
1102
1122
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1103
|
-
return {"ok": False, "code":
|
|
1123
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
1104
1124
|
paths = _paths(state_root)
|
|
1105
1125
|
removed: list[str] = []
|
|
1106
1126
|
receipt_hash: str | None = None
|
|
@@ -1186,7 +1206,7 @@ def context_view(project: Path, change: str) -> dict[str, Any]:
|
|
|
1186
1206
|
try:
|
|
1187
1207
|
contract_root, contract, state_root = _contract(project, change)
|
|
1188
1208
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1189
|
-
return {"ok": False, "code":
|
|
1209
|
+
return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
|
|
1190
1210
|
paths = _paths(state_root)
|
|
1191
1211
|
transitions = _read_ndjson(paths["transitions"])
|
|
1192
1212
|
begins = _read_ndjson(paths["begins"])
|
|
@@ -704,7 +704,11 @@ def append_event(
|
|
|
704
704
|
return {
|
|
705
705
|
"ok": False,
|
|
706
706
|
"code": "PHASE_ALREADY_CLOSED",
|
|
707
|
-
"message":
|
|
707
|
+
"message": (
|
|
708
|
+
"PHASE_ALREADY_CLOSED: refusing a second phase.end for the "
|
|
709
|
+
"same attempt. A retry needs a fresh run id AND the next "
|
|
710
|
+
"attempt number (attempts are global per phase, not per run id)."
|
|
711
|
+
),
|
|
708
712
|
"event": event,
|
|
709
713
|
"autoSealed": result.get("autoSealed") or [],
|
|
710
714
|
}
|
|
@@ -1931,7 +1935,9 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1931
1935
|
):
|
|
1932
1936
|
attempt_error = (
|
|
1933
1937
|
"EVENT_ATTEMPT_CONFLICT: run_id is already bound to "
|
|
1934
|
-
f"attempt {expected_attempt}, received
|
|
1938
|
+
f"attempt {expected_attempt}, received "
|
|
1939
|
+
f"{explicit_attempt}. One run id maps to exactly one "
|
|
1940
|
+
"attempt — allocate a fresh run id for the next attempt."
|
|
1935
1941
|
)
|
|
1936
1942
|
else:
|
|
1937
1943
|
event["attempt"] = expected_attempt
|
|
@@ -1972,7 +1978,10 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1972
1978
|
return emit_error(f"append failed: {exc}", as_json=as_json)
|
|
1973
1979
|
if phase_closed:
|
|
1974
1980
|
return emit_error(
|
|
1975
|
-
"PHASE_ALREADY_CLOSED: refusing a second phase.end for the same
|
|
1981
|
+
"PHASE_ALREADY_CLOSED: refusing a second phase.end for the same "
|
|
1982
|
+
"attempt. A retry needs a fresh run id AND the next attempt number "
|
|
1983
|
+
"(attempts are global per phase, not per run id). For a published "
|
|
1984
|
+
"plan use harness_plan_finalize.py republish, which allocates both.",
|
|
1976
1985
|
as_json=as_json,
|
|
1977
1986
|
error_code="PHASE_ALREADY_CLOSED",
|
|
1978
1987
|
)
|
|
@@ -151,6 +151,11 @@ SOFT_GATE_SITES = frozenset({
|
|
|
151
151
|
})
|
|
152
152
|
GATE_WARNINGS_REL = Path("evidence") / "gate-warnings.ndjson"
|
|
153
153
|
|
|
154
|
+
# Lifecycle order used to scope C9 scenario coverage by scenario ownerPhase.
|
|
155
|
+
# A scenario owned by a later phase is deferred, not missing, at an earlier
|
|
156
|
+
# phase close. Must stay a superset-ordering of hpf.VALID_OWNER_PHASES.
|
|
157
|
+
SCENARIO_OWNER_PHASE_ORDER = ("plan", "run", "test", "review", "submit")
|
|
158
|
+
|
|
154
159
|
|
|
155
160
|
def gate_severity_mode(project: Path, change_dir: Path | None = None) -> str:
|
|
156
161
|
"""Resolve gate severity mode: env > change gate-policy > project config.
|
|
@@ -367,7 +372,11 @@ def evaluate_projection_gate(
|
|
|
367
372
|
def resolve_execution_root(main_project: Path, raw: str | None) -> Path:
|
|
368
373
|
candidate = Path(raw).expanduser().resolve() if raw else main_project.resolve()
|
|
369
374
|
if not candidate.is_dir():
|
|
370
|
-
raise ValueError(
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"execution root not found: {candidate} — --project takes a "
|
|
377
|
+
"filesystem path to the execution root (use '.' when running from "
|
|
378
|
+
"it), not the project name"
|
|
379
|
+
)
|
|
371
380
|
top = _git_text(candidate, "rev-parse", "--show-toplevel")
|
|
372
381
|
if not top:
|
|
373
382
|
raise ValueError(f"execution root is not a git worktree: {candidate}")
|
|
@@ -1957,12 +1966,78 @@ def _sync_after_phase_close(project: Path, change_dir: Path) -> dict[str, Any]:
|
|
|
1957
1966
|
}
|
|
1958
1967
|
|
|
1959
1968
|
|
|
1960
|
-
def
|
|
1969
|
+
def _scenario_owner_phase_rank(owner_phase: str | None) -> int | None:
|
|
1970
|
+
"""Rank a scenario ownerPhase against the lifecycle order, None when unknown."""
|
|
1971
|
+
if not owner_phase:
|
|
1972
|
+
return None
|
|
1973
|
+
normalized = str(owner_phase).strip().lower()
|
|
1974
|
+
if normalized not in hpf.VALID_OWNER_PHASES:
|
|
1975
|
+
return None
|
|
1976
|
+
try:
|
|
1977
|
+
return SCENARIO_OWNER_PHASE_ORDER.index(normalized)
|
|
1978
|
+
except ValueError:
|
|
1979
|
+
return None
|
|
1980
|
+
|
|
1981
|
+
|
|
1982
|
+
def _partition_scenarios_by_owner_phase(
|
|
1983
|
+
scenarios: list[Any],
|
|
1984
|
+
required_ids: set[str],
|
|
1985
|
+
phase: str | None,
|
|
1986
|
+
) -> tuple[set[str], list[str]]:
|
|
1987
|
+
"""Split required scenarios into (due now, deferred to a later phase).
|
|
1988
|
+
|
|
1989
|
+
A scenario is deferred only when it declares an `ownerPhase` that ranks
|
|
1990
|
+
strictly after the phase being closed. Scenarios without a usable
|
|
1991
|
+
`ownerPhase` (legacy/v1 manifests) stay due, preserving old behaviour.
|
|
1992
|
+
"""
|
|
1993
|
+
closing_rank = _scenario_owner_phase_rank(phase)
|
|
1994
|
+
if closing_rank is None:
|
|
1995
|
+
return set(required_ids), []
|
|
1996
|
+
due: set[str] = set()
|
|
1997
|
+
deferred: set[str] = set()
|
|
1998
|
+
for scenario in scenarios:
|
|
1999
|
+
if not isinstance(scenario, dict):
|
|
2000
|
+
continue
|
|
2001
|
+
scenario_id = str(scenario.get("id") or "").strip()
|
|
2002
|
+
if scenario_id not in required_ids:
|
|
2003
|
+
continue
|
|
2004
|
+
owner_rank = _scenario_owner_phase_rank(scenario.get("ownerPhase"))
|
|
2005
|
+
if owner_rank is not None and owner_rank > closing_rank:
|
|
2006
|
+
deferred.add(scenario_id)
|
|
2007
|
+
else:
|
|
2008
|
+
due.add(scenario_id)
|
|
2009
|
+
# IDs present in required_ids but absent from the scan stay due (defensive).
|
|
2010
|
+
due |= required_ids - due - deferred
|
|
2011
|
+
# A duplicated ID declaring two ownerPhases must fail closed: if any
|
|
2012
|
+
# occurrence is due now, the scenario is due now.
|
|
2013
|
+
deferred -= due
|
|
2014
|
+
return due, sorted(deferred)
|
|
2015
|
+
|
|
2016
|
+
|
|
2017
|
+
def _deferred_hint(deferred_ids: list[str], phase: str | None) -> str:
|
|
2018
|
+
"""Append a note so callers never mistake a deferred scenario for a blocker."""
|
|
2019
|
+
if not deferred_ids:
|
|
2020
|
+
return ""
|
|
2021
|
+
return (
|
|
2022
|
+
f"(另有 {len(deferred_ids)} 个场景按 ownerPhase 移交后续阶段,"
|
|
2023
|
+
f"未计入 {phase or '本'} 阶段要求: " + ", ".join(deferred_ids) + ")"
|
|
2024
|
+
)
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
def _validate_scenario_coverage(
|
|
2028
|
+
change_dir: Path, phase: str | None = None
|
|
2029
|
+
) -> dict[str, Any]:
|
|
1961
2030
|
"""C9: validate all ledger-required scenarios are covered by ledger entries.
|
|
1962
2031
|
|
|
1963
2032
|
Reads meta/scenario-manifest.json and evidence/verification-ledger.json.
|
|
1964
2033
|
Returns ok=True when a legacy manifest is missing or all required scenarios
|
|
1965
2034
|
are covered. A present but empty manifest is always invalid.
|
|
2035
|
+
|
|
2036
|
+
Phase scoping: when `phase` is given, only scenarios whose `ownerPhase`
|
|
2037
|
+
is due by that phase are required to carry passing receipts. Scenarios
|
|
2038
|
+
owned by a later phase (e.g. `ownerPhase=test` at `run` close) are
|
|
2039
|
+
reported under `deferred` instead of blocking the close — this matches the
|
|
2040
|
+
documented hand-off rule in harness-run/SKILL.md ("ownerPhase=test 按计划移交").
|
|
1966
2041
|
"""
|
|
1967
2042
|
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
1968
2043
|
if not manifest_path.is_file():
|
|
@@ -2010,6 +2085,9 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2010
2085
|
}
|
|
2011
2086
|
if not required_ids:
|
|
2012
2087
|
return {"ok": True, "code": "NO_LEDGER_REQUIRED_SCENARIOS"}
|
|
2088
|
+
due_ids, deferred_ids = _partition_scenarios_by_owner_phase(
|
|
2089
|
+
scenarios, required_ids, phase
|
|
2090
|
+
)
|
|
2013
2091
|
raw_schema_version = (
|
|
2014
2092
|
manifest.get("schemaVersion") if isinstance(manifest, dict) else None
|
|
2015
2093
|
)
|
|
@@ -2043,6 +2121,18 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2043
2121
|
"missingMappings": missing_mappings,
|
|
2044
2122
|
}
|
|
2045
2123
|
|
|
2124
|
+
if not due_ids:
|
|
2125
|
+
# Every required scenario is owned by a later phase — nothing is due
|
|
2126
|
+
# at this close. Report the hand-off instead of blocking.
|
|
2127
|
+
return {
|
|
2128
|
+
"ok": True,
|
|
2129
|
+
"code": "SCENARIO_COVERAGE_DEFERRED",
|
|
2130
|
+
"covered": [],
|
|
2131
|
+
"deferred": deferred_ids,
|
|
2132
|
+
"ownerPhaseScope": phase,
|
|
2133
|
+
"schemaVersion": schema_version,
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2046
2136
|
try:
|
|
2047
2137
|
ledger, ledger_path = hl.load_ledger(change_dir)
|
|
2048
2138
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
@@ -2050,14 +2140,16 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2050
2140
|
"ok": False,
|
|
2051
2141
|
"code": "SCENARIO_COVERAGE_FAILED",
|
|
2052
2142
|
"message": f"ledger unreadable: {exc}",
|
|
2053
|
-
"missing": sorted(
|
|
2143
|
+
"missing": sorted(due_ids),
|
|
2144
|
+
"deferred": deferred_ids,
|
|
2054
2145
|
}
|
|
2055
2146
|
if ledger is None or ledger_path is None:
|
|
2056
2147
|
return {
|
|
2057
2148
|
"ok": False,
|
|
2058
2149
|
"code": "SCENARIO_COVERAGE_FAILED",
|
|
2059
2150
|
"message": "ledger missing; cannot verify required scenario coverage",
|
|
2060
|
-
"missing": sorted(
|
|
2151
|
+
"missing": sorted(due_ids),
|
|
2152
|
+
"deferred": deferred_ids,
|
|
2061
2153
|
}
|
|
2062
2154
|
if schema_version >= 2:
|
|
2063
2155
|
coverage_sets: dict[str, set[str]] = {
|
|
@@ -2137,8 +2229,8 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2137
2229
|
)
|
|
2138
2230
|
|
|
2139
2231
|
passed = coverage_sets["passed"]
|
|
2140
|
-
missing = sorted(
|
|
2141
|
-
unexecuted = sorted(
|
|
2232
|
+
missing = sorted(due_ids - bound)
|
|
2233
|
+
unexecuted = sorted(due_ids - passed)
|
|
2142
2234
|
detail = {
|
|
2143
2235
|
key: sorted(values & required_ids)
|
|
2144
2236
|
for key, values in coverage_sets.items()
|
|
@@ -2147,6 +2239,8 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2147
2239
|
{
|
|
2148
2240
|
"missing": missing,
|
|
2149
2241
|
"unexecuted": unexecuted,
|
|
2242
|
+
"deferred": deferred_ids,
|
|
2243
|
+
"ownerPhaseScope": phase,
|
|
2150
2244
|
"attempts": {
|
|
2151
2245
|
scenario_id: sorted(values)
|
|
2152
2246
|
for scenario_id, values in sorted(attempts.items())
|
|
@@ -2162,6 +2256,7 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2162
2256
|
"message": (
|
|
2163
2257
|
"required scenarios without exact passed execution receipts: "
|
|
2164
2258
|
+ ", ".join(unexecuted)
|
|
2259
|
+
+ _deferred_hint(deferred_ids, phase)
|
|
2165
2260
|
),
|
|
2166
2261
|
**detail,
|
|
2167
2262
|
}
|
|
@@ -2180,7 +2275,7 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2180
2275
|
ids = entry.get("scenarioIds")
|
|
2181
2276
|
if isinstance(ids, list):
|
|
2182
2277
|
covered.update(str(i) for i in ids)
|
|
2183
|
-
missing = sorted(
|
|
2278
|
+
missing = sorted(due_ids - covered)
|
|
2184
2279
|
if missing:
|
|
2185
2280
|
return {
|
|
2186
2281
|
"ok": False,
|
|
@@ -2188,13 +2283,18 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
2188
2283
|
"message": (
|
|
2189
2284
|
"ledger-required scenarios without ledger entry: "
|
|
2190
2285
|
+ ", ".join(missing)
|
|
2286
|
+
+ _deferred_hint(deferred_ids, phase)
|
|
2191
2287
|
),
|
|
2192
2288
|
"missing": missing,
|
|
2289
|
+
"deferred": deferred_ids,
|
|
2290
|
+
"ownerPhaseScope": phase,
|
|
2193
2291
|
}
|
|
2194
2292
|
return {
|
|
2195
2293
|
"ok": True,
|
|
2196
2294
|
"code": "SCENARIO_COVERAGE_OK",
|
|
2197
2295
|
"covered": sorted(covered & required_ids),
|
|
2296
|
+
"deferred": deferred_ids,
|
|
2297
|
+
"ownerPhaseScope": phase,
|
|
2198
2298
|
}
|
|
2199
2299
|
|
|
2200
2300
|
|
|
@@ -2888,9 +2988,11 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
2888
2988
|
},
|
|
2889
2989
|
)
|
|
2890
2990
|
|
|
2891
|
-
# C9: scenario coverage check —
|
|
2991
|
+
# C9: scenario coverage check — every P0/ledger scenario *due by this phase*
|
|
2992
|
+
# must have a ledger entry. Scenarios with a later ownerPhase are deferred,
|
|
2993
|
+
# not missing (see _validate_scenario_coverage).
|
|
2892
2994
|
if args.phase in {"run", "test"}:
|
|
2893
|
-
coverage = _validate_scenario_coverage(change_dir)
|
|
2995
|
+
coverage = _validate_scenario_coverage(change_dir, args.phase)
|
|
2894
2996
|
if not coverage.get("ok"):
|
|
2895
2997
|
if gate_soft_allowed(severity_mode, args.phase, "scenario-coverage"):
|
|
2896
2998
|
gate_warnings.append(record_gate_warning(
|
|
@@ -3284,7 +3386,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3284
3386
|
p_begin = sub.add_parser("begin", parents=[shared])
|
|
3285
3387
|
p_begin.add_argument("--phase", required=True)
|
|
3286
3388
|
p_begin.add_argument("--change", default=None)
|
|
3287
|
-
p_begin.add_argument(
|
|
3389
|
+
p_begin.add_argument(
|
|
3390
|
+
"--project",
|
|
3391
|
+
default=None,
|
|
3392
|
+
help=(
|
|
3393
|
+
"execution root PATH (worktree) for this phase; defaults to the "
|
|
3394
|
+
"change's main project. Takes a path, not a project name."
|
|
3395
|
+
),
|
|
3396
|
+
)
|
|
3288
3397
|
p_begin.add_argument("--skills-root", default=None)
|
|
3289
3398
|
p_begin.add_argument("--run-id", default=None)
|
|
3290
3399
|
p_begin.add_argument("--ttl-seconds", type=int, default=3600)
|
|
@@ -3298,7 +3407,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3298
3407
|
p_close = sub.add_parser("close", parents=[shared])
|
|
3299
3408
|
p_close.add_argument("--phase", required=True)
|
|
3300
3409
|
p_close.add_argument("--change", default=None)
|
|
3301
|
-
p_close.add_argument(
|
|
3410
|
+
p_close.add_argument(
|
|
3411
|
+
"--project",
|
|
3412
|
+
default=None,
|
|
3413
|
+
help=(
|
|
3414
|
+
"execution root PATH (worktree) for this phase; defaults to the "
|
|
3415
|
+
"change's main project. Takes a path, not a project name."
|
|
3416
|
+
),
|
|
3417
|
+
)
|
|
3302
3418
|
p_close.add_argument("--status", required=True)
|
|
3303
3419
|
p_close.add_argument("--run-id", default=None)
|
|
3304
3420
|
p_close.add_argument("--task", type=int, default=None)
|