@hunter-harness/workflow-harness 0.4.3 → 0.4.8
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 +4 -3
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/general/claude-code/harness-plan/reference.md +30 -3
- 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 +3 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +28 -1
- package/harness/bundles/general/claude-code/scripts/harness_change.py +74 -0
- package/harness/bundles/general/claude-code/scripts/harness_context.py +271 -10
- package/harness/bundles/general/claude-code/scripts/harness_events.py +4 -1
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +280 -76
- package/harness/bundles/general/claude-code/scripts/harness_review.py +120 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +13 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +4 -3
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +30 -3
- 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 +3 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +28 -1
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +74 -0
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +271 -10
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +4 -1
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +280 -76
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +120 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +13 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +4 -3
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/general/codex/harness-plan/reference.md +30 -3
- 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 +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +28 -1
- package/harness/bundles/general/codex/scripts/harness_change.py +74 -0
- package/harness/bundles/general/codex/scripts/harness_context.py +271 -10
- package/harness/bundles/general/codex/scripts/harness_events.py +4 -1
- package/harness/bundles/general/codex/scripts/harness_gate.py +280 -76
- package/harness/bundles/general/codex/scripts/harness_review.py +120 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +13 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +4 -3
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/general/cursor/harness-plan/reference.md +30 -3
- 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 +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +28 -1
- package/harness/bundles/general/cursor/scripts/harness_change.py +74 -0
- package/harness/bundles/general/cursor/scripts/harness_context.py +271 -10
- package/harness/bundles/general/cursor/scripts/harness_events.py +4 -1
- package/harness/bundles/general/cursor/scripts/harness_gate.py +280 -76
- package/harness/bundles/general/cursor/scripts/harness_review.py +120 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +13 -0
- package/harness/bundles/general/pi/.harness-build.json +1 -1
- package/harness/bundles/general/pi/harness-archive/SKILL.md +4 -3
- package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-execute/SKILL.md +4 -2
- package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-plan/SKILL.md +2 -2
- package/harness/bundles/general/pi/harness-plan/reference.md +30 -3
- package/harness/bundles/general/pi/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/pi/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/pi/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/pi/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/pi/scripts/harness_archive.py +28 -1
- package/harness/bundles/general/pi/scripts/harness_change.py +74 -0
- package/harness/bundles/general/pi/scripts/harness_context.py +271 -10
- package/harness/bundles/general/pi/scripts/harness_events.py +4 -1
- package/harness/bundles/general/pi/scripts/harness_gate.py +280 -76
- package/harness/bundles/general/pi/scripts/harness_review.py +120 -0
- package/harness/bundles/general/pi/scripts/harness_runtime.py +13 -0
- 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 +4 -3
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/java/claude-code/harness-plan/reference.md +30 -3
- 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 +3 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +28 -1
- package/harness/bundles/java/claude-code/scripts/harness_change.py +74 -0
- package/harness/bundles/java/claude-code/scripts/harness_context.py +271 -10
- package/harness/bundles/java/claude-code/scripts/harness_events.py +4 -1
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +280 -76
- package/harness/bundles/java/claude-code/scripts/harness_review.py +120 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +13 -0
- 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 +4 -3
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +30 -3
- 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 +3 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +28 -1
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +74 -0
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +271 -10
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +4 -1
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +280 -76
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +120 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +13 -0
- 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 +4 -3
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/java/codex/harness-plan/reference.md +30 -3
- 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 +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +28 -1
- package/harness/bundles/java/codex/scripts/harness_change.py +74 -0
- package/harness/bundles/java/codex/scripts/harness_context.py +271 -10
- package/harness/bundles/java/codex/scripts/harness_events.py +4 -1
- package/harness/bundles/java/codex/scripts/harness_gate.py +280 -76
- package/harness/bundles/java/codex/scripts/harness_review.py +120 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +13 -0
- 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 +4 -3
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-execute/SKILL.md +4 -2
- 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 +2 -2
- package/harness/bundles/java/cursor/harness-plan/reference.md +30 -3
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +28 -1
- package/harness/bundles/java/cursor/scripts/harness_change.py +74 -0
- package/harness/bundles/java/cursor/scripts/harness_context.py +271 -10
- package/harness/bundles/java/cursor/scripts/harness_events.py +4 -1
- package/harness/bundles/java/cursor/scripts/harness_gate.py +280 -76
- package/harness/bundles/java/cursor/scripts/harness_review.py +120 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +13 -0
- package/harness/bundles/java/pi/.harness-build.json +1 -1
- package/harness/bundles/java/pi/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-archive/SKILL.md +4 -3
- package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-execute/SKILL.md +4 -2
- package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-plan/SKILL.md +2 -2
- package/harness/bundles/java/pi/harness-plan/reference.md +30 -3
- package/harness/bundles/java/pi/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/pi/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/pi/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/pi/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/pi/scripts/harness_archive.py +28 -1
- package/harness/bundles/java/pi/scripts/harness_change.py +74 -0
- package/harness/bundles/java/pi/scripts/harness_context.py +271 -10
- package/harness/bundles/java/pi/scripts/harness_events.py +4 -1
- package/harness/bundles/java/pi/scripts/harness_gate.py +280 -76
- package/harness/bundles/java/pi/scripts/harness_review.py +120 -0
- package/harness/bundles/java/pi/scripts/harness_runtime.py +13 -0
- package/harness/manifests/general/claude-code.json +22 -22
- package/harness/manifests/general/codebuddy.json +22 -22
- package/harness/manifests/general/codex.json +22 -22
- package/harness/manifests/general/cursor.json +22 -22
- package/harness/manifests/general/pi.json +22 -22
- package/harness/manifests/java/claude-code.json +24 -24
- package/harness/manifests/java/codebuddy.json +24 -24
- package/harness/manifests/java/codex.json +24 -24
- package/harness/manifests/java/cursor.json +24 -24
- package/harness/manifests/java/pi.json +24 -24
- package/hunter-workflow-family.json +2 -2
- package/package.json +15 -15
|
@@ -121,6 +121,11 @@ def emit_error(
|
|
|
121
121
|
sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
122
122
|
else:
|
|
123
123
|
sys.stderr.write(f"error: {message} ({code})\n")
|
|
124
|
+
# 文本模式同样给出恢复指引——--json 的 recoveryAction 一直有,
|
|
125
|
+
# 但人类读的恰恰是文本模式(2026-08-30 实测:用户翻文档才找到 claim 命令)
|
|
126
|
+
recovery = payload.get("recoveryAction")
|
|
127
|
+
if isinstance(recovery, str) and recovery:
|
|
128
|
+
sys.stderr.write(f"recovery: {recovery}\n")
|
|
124
129
|
return 1
|
|
125
130
|
|
|
126
131
|
|
|
@@ -1967,12 +1972,20 @@ def validate_context_for_gate_begin(
|
|
|
1967
1972
|
return {"ok": True, "code": "CONTEXT_LEGACY_COMPATIBLE"}
|
|
1968
1973
|
current_phase = str(current.get("phase") or view.get("currentPhase") or "")
|
|
1969
1974
|
if hp.resolve_phase_name(current_phase) != phase:
|
|
1975
|
+
candidates = hctx.allowed_next_phases(project, change_id, current_phase)
|
|
1970
1976
|
return {
|
|
1971
1977
|
"ok": False,
|
|
1972
1978
|
"code": "CONTEXT_HANDOFF_REQUIRED",
|
|
1973
1979
|
"message": "阶段交接尚未完成,门禁未启动;请先完成上下文交接。",
|
|
1974
1980
|
"expectedPhase": current_phase or None,
|
|
1975
1981
|
"requestedPhase": phase,
|
|
1982
|
+
"recoveryAction": (
|
|
1983
|
+
f"当前上下文停留在 {current_phase} 阶段。一条命令补齐交接"
|
|
1984
|
+
"(自动补租约→写收据→begin 确认,无需 change claim/release):"
|
|
1985
|
+
f"harness_context.py handoff --project . --change {change_id} "
|
|
1986
|
+
f"--to-phase {phase} --executor <tool> --json"
|
|
1987
|
+
+ (f"(计划后继阶段:{'/'.join(candidates)})" if candidates else "")
|
|
1988
|
+
),
|
|
1976
1989
|
}
|
|
1977
1990
|
if isinstance(transitions, list) and transitions:
|
|
1978
1991
|
latest = transitions[-1] if isinstance(transitions[-1], dict) else {}
|
|
@@ -1990,6 +2003,12 @@ def validate_context_for_gate_begin(
|
|
|
1990
2003
|
"code": "CONTEXT_BEGIN_REQUIRED",
|
|
1991
2004
|
"message": "阶段交接收据尚未确认,门禁未启动;请先执行上下文 begin。",
|
|
1992
2005
|
"requestedPhase": phase,
|
|
2006
|
+
"recoveryAction": (
|
|
2007
|
+
"交接收据已写入但缺 begin 确认。一条命令补齐:"
|
|
2008
|
+
f"harness_context.py handoff --project . --change {change_id} "
|
|
2009
|
+
f"--to-phase {phase} --executor <tool> --json"
|
|
2010
|
+
"(检测到收据已存在时只补 begin 确认,不写重复 receipt)"
|
|
2011
|
+
),
|
|
1993
2012
|
}
|
|
1994
2013
|
return {"ok": True, "code": "CONTEXT_READY"}
|
|
1995
2014
|
|
|
@@ -2503,7 +2522,11 @@ def validate_review_outputs_for_close(
|
|
|
2503
2522
|
"code": "REVIEW_OUTPUTS_INCOMPLETE",
|
|
2504
2523
|
"message": "评审的结构化发现或处置记录缺失,尚不能结束评审阶段。",
|
|
2505
2524
|
"missing": missing,
|
|
2506
|
-
"recoveryAction":
|
|
2525
|
+
"recoveryAction": (
|
|
2526
|
+
"用骨架生成器补写:harness_review.py scaffold --change-dir <change-dir>"
|
|
2527
|
+
"(无 findings 时先生成发现骨架→write-findings 落地→重跑 scaffold "
|
|
2528
|
+
"生成处置骨架→write-dispositions 落地),再重试关门"
|
|
2529
|
+
),
|
|
2507
2530
|
}
|
|
2508
2531
|
try:
|
|
2509
2532
|
findings = json.loads(findings_path.read_text(encoding="utf-8-sig"))
|
|
@@ -2930,6 +2953,169 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
2930
2953
|
return 0
|
|
2931
2954
|
|
|
2932
2955
|
|
|
2956
|
+
def _emit_close_summary(
|
|
2957
|
+
code: str,
|
|
2958
|
+
phase: str,
|
|
2959
|
+
status: str,
|
|
2960
|
+
to_phase: str | None,
|
|
2961
|
+
*,
|
|
2962
|
+
derived: bool = False,
|
|
2963
|
+
) -> None:
|
|
2964
|
+
"""关门成功的一行摘要(stderr,不污染 stdout 的 JSON 契约)。"""
|
|
2965
|
+
suffix = "(auto)" if derived and to_phase else ""
|
|
2966
|
+
sys.stderr.write(
|
|
2967
|
+
f"{code} · phase={phase} · status={status} · next={to_phase or '-'}{suffix}\n"
|
|
2968
|
+
)
|
|
2969
|
+
|
|
2970
|
+
|
|
2971
|
+
def _finalize_close_journal(
|
|
2972
|
+
change_dir: Path,
|
|
2973
|
+
phase: str,
|
|
2974
|
+
run_id: str,
|
|
2975
|
+
close_status: str,
|
|
2976
|
+
) -> None:
|
|
2977
|
+
"""续跑成功后把 capsule 的 closeTransaction 收口为 CLOSED(best-effort)。"""
|
|
2978
|
+
if not run_id:
|
|
2979
|
+
return
|
|
2980
|
+
try:
|
|
2981
|
+
capsule = load_phase_capsule(change_dir, phase, run_id)
|
|
2982
|
+
except (OSError, ValueError):
|
|
2983
|
+
capsule = None
|
|
2984
|
+
if not isinstance(capsule, dict):
|
|
2985
|
+
return
|
|
2986
|
+
transaction = capsule.get("closeTransaction")
|
|
2987
|
+
if not isinstance(transaction, dict):
|
|
2988
|
+
return
|
|
2989
|
+
transaction.update({
|
|
2990
|
+
"status": "CLOSED",
|
|
2991
|
+
"retryable": False,
|
|
2992
|
+
"leaseReleased": True,
|
|
2993
|
+
"updatedAt": he.now_iso(),
|
|
2994
|
+
})
|
|
2995
|
+
capsule["closedAt"] = he.now_iso()
|
|
2996
|
+
capsule["closeStatus"] = close_status
|
|
2997
|
+
try:
|
|
2998
|
+
write_phase_capsule(change_dir, phase, run_id, capsule)
|
|
2999
|
+
except OSError:
|
|
3000
|
+
pass
|
|
3001
|
+
|
|
3002
|
+
|
|
3003
|
+
def _resume_closed_phase(
|
|
3004
|
+
project: Path,
|
|
3005
|
+
resolved: dict[str, Any],
|
|
3006
|
+
args: argparse.Namespace,
|
|
3007
|
+
terminal: dict[str, Any],
|
|
3008
|
+
*,
|
|
3009
|
+
as_json: bool,
|
|
3010
|
+
) -> int:
|
|
3011
|
+
"""phase.end 已写、租约已释放、收尾未走完的幂等续跑。
|
|
3012
|
+
|
|
3013
|
+
这个中间态是"租约释放早于后续步骤"(或输出中断)留下的:本地关门事实
|
|
3014
|
+
已经存在,剩下的 handoff/monitor/recovery/scratch 全部幂等。识别并补跑,
|
|
3015
|
+
而不是报 LEASE_ABSENT 逼调用方手工 claim 重取租约(2026-08-30 实测路径)。
|
|
3016
|
+
"""
|
|
3017
|
+
change_id = str(resolved["changeId"])
|
|
3018
|
+
change_dir = Path(resolved["changeDir"])
|
|
3019
|
+
run_id = str(terminal.get("run_id") or getattr(args, "run_id", None) or "")
|
|
3020
|
+
close_status = str(terminal.get("status") or args.status)
|
|
3021
|
+
to_phase = getattr(args, "to_phase", None)
|
|
3022
|
+
|
|
3023
|
+
view = hctx.context_view(project, change_id)
|
|
3024
|
+
transitions = view.get("transitions") if isinstance(view, dict) else None
|
|
3025
|
+
handoff_recorded = isinstance(transitions, list) and any(
|
|
3026
|
+
isinstance(item, dict)
|
|
3027
|
+
and hp.resolve_phase_name(item.get("fromPhase")) == args.phase
|
|
3028
|
+
for item in transitions
|
|
3029
|
+
)
|
|
3030
|
+
|
|
3031
|
+
handoff: dict[str, Any] | None = None
|
|
3032
|
+
derived_to_phase: str | None = None
|
|
3033
|
+
if handoff_recorded:
|
|
3034
|
+
# 交接收据已在 transitions.ndjson——重放 close_transition 也只会幂等命中
|
|
3035
|
+
handoff = {"ok": True, "code": "TRANSITION_ALREADY_CLOSED", "resumed": True}
|
|
3036
|
+
else:
|
|
3037
|
+
candidates = [
|
|
3038
|
+
candidate
|
|
3039
|
+
for candidate in hctx.allowed_next_phases(project, change_id, args.phase)
|
|
3040
|
+
if hp.resolve_phase_name(candidate) != args.phase
|
|
3041
|
+
]
|
|
3042
|
+
if to_phase is None and len(candidates) == 1:
|
|
3043
|
+
# 计划后继唯一(排除 fixback 自环):close 的意图没有歧义,直接补跑 handoff
|
|
3044
|
+
to_phase = candidates[0]
|
|
3045
|
+
args.to_phase = to_phase
|
|
3046
|
+
derived_to_phase = to_phase
|
|
3047
|
+
if to_phase is None and candidates:
|
|
3048
|
+
return emit_error(
|
|
3049
|
+
"PHASE_HANDOFF_PENDING",
|
|
3050
|
+
"phase gate already closed locally (phase.end recorded), but the context "
|
|
3051
|
+
"handoff is still pending and the successor is ambiguous; re-run with --to-phase",
|
|
3052
|
+
as_json=as_json,
|
|
3053
|
+
extra={
|
|
3054
|
+
"localCloseComplete": True,
|
|
3055
|
+
"retryable": True,
|
|
3056
|
+
"phase": args.phase,
|
|
3057
|
+
"status": close_status,
|
|
3058
|
+
"changeId": change_id,
|
|
3059
|
+
"candidateNextPhases": candidates,
|
|
3060
|
+
"recoveryAction": (
|
|
3061
|
+
"本地关门已完成(phase.end 已写、租约已释放),只剩上下文交接。"
|
|
3062
|
+
"不需要重新 claim 租约——用原命令补 --to-phase 重跑即幂等续跑:"
|
|
3063
|
+
"python <skills-root>/scripts/harness_gate.py close "
|
|
3064
|
+
f"--phase {args.phase} --change {change_id} --status {close_status} "
|
|
3065
|
+
f"--to-phase <{'|'.join(candidates)}> --json"
|
|
3066
|
+
),
|
|
3067
|
+
},
|
|
3068
|
+
)
|
|
3069
|
+
if to_phase is not None:
|
|
3070
|
+
handoff = _close_context_handoff(
|
|
3071
|
+
project,
|
|
3072
|
+
change_id,
|
|
3073
|
+
args,
|
|
3074
|
+
status=close_status,
|
|
3075
|
+
)
|
|
3076
|
+
monitor = _sync_after_phase_close(project, change_dir)
|
|
3077
|
+
if isinstance(handoff, dict) and not handoff.get("ok"):
|
|
3078
|
+
return emit_error(
|
|
3079
|
+
"PHASE_HANDOFF_PENDING",
|
|
3080
|
+
"phase gate is closed, but context handoff still needs attention",
|
|
3081
|
+
as_json=as_json,
|
|
3082
|
+
extra={
|
|
3083
|
+
"localCloseComplete": True,
|
|
3084
|
+
"retryable": True,
|
|
3085
|
+
"contextHandoff": handoff,
|
|
3086
|
+
"platformMonitor": monitor,
|
|
3087
|
+
},
|
|
3088
|
+
)
|
|
3089
|
+
recovery_result = record_gate_recovered(
|
|
3090
|
+
change_dir,
|
|
3091
|
+
phase=args.phase,
|
|
3092
|
+
run_id=run_id,
|
|
3093
|
+
)
|
|
3094
|
+
try:
|
|
3095
|
+
scratch_swept = hruntime.sweep_scratch(change_dir)
|
|
3096
|
+
except Exception as exc: # noqa: BLE001 — 收尾步骤失败只记录,绝不阻断续跑
|
|
3097
|
+
scratch_swept = {"ok": False, "code": "SCRATCH_SWEEP_FAILED", "error": str(exc)}
|
|
3098
|
+
_finalize_close_journal(change_dir, args.phase, run_id, close_status)
|
|
3099
|
+
emit(
|
|
3100
|
+
{
|
|
3101
|
+
"ok": True,
|
|
3102
|
+
"code": "PHASE_CLOSE_RESUMED",
|
|
3103
|
+
"phase": args.phase,
|
|
3104
|
+
"status": close_status,
|
|
3105
|
+
"changeId": change_id,
|
|
3106
|
+
"localCloseComplete": True,
|
|
3107
|
+
"contextHandoff": handoff,
|
|
3108
|
+
"platformMonitor": monitor,
|
|
3109
|
+
"gateRecovery": recovery_result,
|
|
3110
|
+
"scratchSwept": scratch_swept,
|
|
3111
|
+
**({"derivedToPhase": derived_to_phase} if derived_to_phase else {}),
|
|
3112
|
+
},
|
|
3113
|
+
as_json=as_json,
|
|
3114
|
+
)
|
|
3115
|
+
_emit_close_summary("PHASE_CLOSE_RESUMED", args.phase, close_status, to_phase, derived=derived_to_phase is not None)
|
|
3116
|
+
return 0
|
|
3117
|
+
|
|
3118
|
+
|
|
2933
3119
|
def cmd_close(args: argparse.Namespace) -> int:
|
|
2934
3120
|
as_json = bool(args.json)
|
|
2935
3121
|
normalized = _normalize_gate_phase(args, as_json)
|
|
@@ -3043,47 +3229,13 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3043
3229
|
|
|
3044
3230
|
if current_lease is None:
|
|
3045
3231
|
terminal = _latest_phase_end(change_dir, args.phase, explicit_run_id)
|
|
3046
|
-
if (
|
|
3047
|
-
|
|
3048
|
-
and terminal is not None
|
|
3049
|
-
and _terminal_matches_context_session(
|
|
3050
|
-
project, str(resolved["changeId"]), args.phase, terminal
|
|
3051
|
-
)
|
|
3232
|
+
if terminal is not None and _terminal_matches_context_session(
|
|
3233
|
+
project, str(resolved["changeId"]), args.phase, terminal
|
|
3052
3234
|
):
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
args,
|
|
3058
|
-
status=terminal_status,
|
|
3059
|
-
)
|
|
3060
|
-
monitor = _sync_after_phase_close(project, change_dir)
|
|
3061
|
-
if not isinstance(handoff, dict) or not handoff.get("ok"):
|
|
3062
|
-
return emit_error(
|
|
3063
|
-
"PHASE_HANDOFF_PENDING",
|
|
3064
|
-
"phase gate is closed, but context handoff still needs attention",
|
|
3065
|
-
as_json=as_json,
|
|
3066
|
-
extra={
|
|
3067
|
-
"localCloseComplete": True,
|
|
3068
|
-
"retryable": True,
|
|
3069
|
-
"contextHandoff": handoff,
|
|
3070
|
-
"platformMonitor": monitor,
|
|
3071
|
-
},
|
|
3072
|
-
)
|
|
3073
|
-
emit(
|
|
3074
|
-
{
|
|
3075
|
-
"ok": True,
|
|
3076
|
-
"code": "PHASE_CLOSE_RESUMED",
|
|
3077
|
-
"phase": args.phase,
|
|
3078
|
-
"status": terminal_status,
|
|
3079
|
-
"changeId": resolved["changeId"],
|
|
3080
|
-
"localCloseComplete": True,
|
|
3081
|
-
"contextHandoff": handoff,
|
|
3082
|
-
"platformMonitor": monitor,
|
|
3083
|
-
},
|
|
3084
|
-
as_json=as_json,
|
|
3085
|
-
)
|
|
3086
|
-
return 0
|
|
3235
|
+
# phase.end 已写且属于当前会话:本地关门已完成,租约缺失是历史运行的
|
|
3236
|
+
# 释放顺序(或输出中断)留下的中间态——幂等续跑剩余步骤,与是否携带
|
|
3237
|
+
# --to-phase 无关;后继未显式给出时从计划派生(唯一后继自动补跑)。
|
|
3238
|
+
return _resume_closed_phase(project, resolved, args, terminal, as_json=as_json)
|
|
3087
3239
|
resume_run_id = explicit_run_id or _latest_open_run_id(change_dir, args.phase)
|
|
3088
3240
|
return emit_error(
|
|
3089
3241
|
"LEASE_ABSENT",
|
|
@@ -3457,39 +3609,32 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3457
3609
|
close_transaction["updatedAt"] = he.now_iso()
|
|
3458
3610
|
write_phase_capsule(change_dir, args.phase, run_id, capsule)
|
|
3459
3611
|
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
"leaseReleased": True,
|
|
3487
|
-
"updatedAt": he.now_iso(),
|
|
3488
|
-
})
|
|
3489
|
-
capsule["closedAt"] = he.now_iso()
|
|
3490
|
-
capsule["closeStatus"] = close_status
|
|
3491
|
-
write_phase_capsule(change_dir, args.phase, run_id, capsule)
|
|
3492
|
-
|
|
3612
|
+
# 租约释放必须是关门的最后一个可失败步骤。历史上它排在 handoff/monitor
|
|
3613
|
+
# 之前,一旦中途失败就留下“phase.end 已写 + 租约已放 + 交接没写”的三不管
|
|
3614
|
+
# 状态,重试直接死在 LEASE_ABSENT(2026-08-30 sales-insight-agent 实测)。
|
|
3615
|
+
# 现在顺序为:handoff → monitor → recovery → scratch → release → emit;
|
|
3616
|
+
# 任何中途失败时租约仍持有,重试同一命令按 closeTransaction journal 幂等续跑。
|
|
3617
|
+
# P0-1:plain close 不得静默跳过交接。上下文状态存在且计划后继唯一
|
|
3618
|
+
# (排除 fixback 自环)时自动派生后继并执行交接——断链从根上消失;
|
|
3619
|
+
# 多后继仍要求显式 --to-phase。
|
|
3620
|
+
derived_to_phase: str | None = None
|
|
3621
|
+
if not getattr(args, "to_phase", None):
|
|
3622
|
+
context_view = hctx.context_view(project, str(resolved["changeId"]))
|
|
3623
|
+
if (
|
|
3624
|
+
isinstance(context_view, dict)
|
|
3625
|
+
and context_view.get("ok")
|
|
3626
|
+
and isinstance(context_view.get("current"), dict)
|
|
3627
|
+
):
|
|
3628
|
+
forward = [
|
|
3629
|
+
candidate
|
|
3630
|
+
for candidate in hctx.allowed_next_phases(
|
|
3631
|
+
project, str(resolved["changeId"]), args.phase
|
|
3632
|
+
)
|
|
3633
|
+
if hp.resolve_phase_name(candidate) != args.phase
|
|
3634
|
+
]
|
|
3635
|
+
if len(forward) == 1:
|
|
3636
|
+
derived_to_phase = forward[0]
|
|
3637
|
+
args.to_phase = derived_to_phase
|
|
3493
3638
|
context_handoff = _close_context_handoff(
|
|
3494
3639
|
project,
|
|
3495
3640
|
str(resolved["changeId"]),
|
|
@@ -3517,6 +3662,10 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3517
3662
|
"changeId": resolved["changeId"],
|
|
3518
3663
|
"contextHandoff": context_handoff,
|
|
3519
3664
|
"platformMonitor": platform_monitor,
|
|
3665
|
+
"recoveryAction": (
|
|
3666
|
+
"租约仍持有,无需重新 claim——原样重跑同一 close 命令即可幂等续跑"
|
|
3667
|
+
"(phase.end 已落会跳过,handoff 会重试)"
|
|
3668
|
+
),
|
|
3520
3669
|
},
|
|
3521
3670
|
)
|
|
3522
3671
|
|
|
@@ -3530,9 +3679,50 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3530
3679
|
# 直接把归档挡住了。白名单式删除,证据/报告/运行态一律不碰;失败只记不阻断。
|
|
3531
3680
|
try:
|
|
3532
3681
|
scratch_swept = hruntime.sweep_scratch(change_dir)
|
|
3533
|
-
except
|
|
3682
|
+
except Exception as exc: # noqa: BLE001 — 清草稿是收尾便利,任何失败都不得阻断关门
|
|
3534
3683
|
scratch_swept = {"ok": False, "code": "SCRATCH_SWEEP_FAILED", "error": str(exc)}
|
|
3535
3684
|
|
|
3685
|
+
release = hc.release_lease(
|
|
3686
|
+
project,
|
|
3687
|
+
change_id=resolved["changeId"],
|
|
3688
|
+
phase=args.phase,
|
|
3689
|
+
run_id=run_id,
|
|
3690
|
+
)
|
|
3691
|
+
if not release.get("ok"):
|
|
3692
|
+
if capsule is not None:
|
|
3693
|
+
close_transaction.update({
|
|
3694
|
+
"status": "RELEASE_PENDING",
|
|
3695
|
+
"retryable": True,
|
|
3696
|
+
"lastError": release,
|
|
3697
|
+
"updatedAt": he.now_iso(),
|
|
3698
|
+
})
|
|
3699
|
+
write_phase_capsule(change_dir, args.phase, run_id, capsule)
|
|
3700
|
+
return emit_error(
|
|
3701
|
+
str(release.get("code", "LEASE_RELEASE_FAILED")),
|
|
3702
|
+
str(release.get("message", "lease release failed")),
|
|
3703
|
+
as_json=as_json,
|
|
3704
|
+
extra={
|
|
3705
|
+
**{k: v for k, v in release.items() if k not in {"ok", "message", "code"}},
|
|
3706
|
+
"localCloseComplete": True,
|
|
3707
|
+
"retryable": True,
|
|
3708
|
+
"recoveryAction": (
|
|
3709
|
+
"本地关门与上下文交接均已完成,仅租约释放失败——原样重跑同一 close "
|
|
3710
|
+
"命令即幂等续跑(phase.end/handoff 幂等命中,仅重试释放)"
|
|
3711
|
+
),
|
|
3712
|
+
},
|
|
3713
|
+
)
|
|
3714
|
+
|
|
3715
|
+
if capsule is not None:
|
|
3716
|
+
close_transaction.update({
|
|
3717
|
+
"status": "CLOSED",
|
|
3718
|
+
"retryable": False,
|
|
3719
|
+
"leaseReleased": True,
|
|
3720
|
+
"updatedAt": he.now_iso(),
|
|
3721
|
+
})
|
|
3722
|
+
capsule["closedAt"] = he.now_iso()
|
|
3723
|
+
capsule["closeStatus"] = close_status
|
|
3724
|
+
write_phase_capsule(change_dir, args.phase, run_id, capsule)
|
|
3725
|
+
|
|
3536
3726
|
payload = {
|
|
3537
3727
|
"ok": True,
|
|
3538
3728
|
"code": close_code,
|
|
@@ -3553,11 +3743,21 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3553
3743
|
"gateRecovery": recovery_result,
|
|
3554
3744
|
"scratchSwept": scratch_swept,
|
|
3555
3745
|
}
|
|
3746
|
+
if derived_to_phase is not None:
|
|
3747
|
+
# plain close 自动派生的后继,调用方必须看得到这不是显式选择。
|
|
3748
|
+
payload["derivedToPhase"] = derived_to_phase
|
|
3556
3749
|
if lease_lapsed is not None:
|
|
3557
3750
|
# 与 context transition receipt 的 leaseLapsed 同名同义:记录这个阶段
|
|
3558
3751
|
# 跑过了 TTL 且租约是自动重取的,不隐藏。
|
|
3559
3752
|
payload["leaseLapsed"] = lease_lapsed
|
|
3560
3753
|
emit(payload, as_json=as_json)
|
|
3754
|
+
_emit_close_summary(
|
|
3755
|
+
close_code,
|
|
3756
|
+
args.phase,
|
|
3757
|
+
close_status,
|
|
3758
|
+
getattr(args, "to_phase", None),
|
|
3759
|
+
derived=derived_to_phase is not None,
|
|
3760
|
+
)
|
|
3561
3761
|
return 0
|
|
3562
3762
|
|
|
3563
3763
|
|
|
@@ -3754,7 +3954,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3754
3954
|
p_begin.add_argument("--executor-model", default=None)
|
|
3755
3955
|
p_begin.set_defaults(func=cmd_begin)
|
|
3756
3956
|
|
|
3757
|
-
p_close = sub.add_parser(
|
|
3957
|
+
p_close = sub.add_parser(
|
|
3958
|
+
"close",
|
|
3959
|
+
parents=[shared],
|
|
3960
|
+
help="关闭阶段门禁(关门只调本命令;上下文交接由 --to-phase 内联完成,不要再单独调 harness_context.py close)",
|
|
3961
|
+
)
|
|
3758
3962
|
p_close.add_argument("--phase", required=True)
|
|
3759
3963
|
p_close.add_argument("--change", default=None)
|
|
3760
3964
|
p_close.add_argument(
|
|
@@ -575,6 +575,115 @@ def cmd_write_dispositions(args: argparse.Namespace) -> int:
|
|
|
575
575
|
return _emit(write_dispositions(Path(args.change_dir), doc), as_json=True)
|
|
576
576
|
|
|
577
577
|
|
|
578
|
+
def _latest_review_run_id(change_dir: Path) -> str | None:
|
|
579
|
+
"""从 events.ndjson 取最近一轮 review phase.start 的 run_id(best-effort)。"""
|
|
580
|
+
candidates = [change_dir / "events.ndjson"]
|
|
581
|
+
state_events = _state_dir(change_dir) / "events.ndjson"
|
|
582
|
+
if state_events != candidates[0]:
|
|
583
|
+
candidates.append(state_events)
|
|
584
|
+
run_id: str | None = None
|
|
585
|
+
for path in candidates:
|
|
586
|
+
try:
|
|
587
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
588
|
+
except OSError:
|
|
589
|
+
continue
|
|
590
|
+
for line in lines:
|
|
591
|
+
try:
|
|
592
|
+
item = json.loads(line)
|
|
593
|
+
except json.JSONDecodeError:
|
|
594
|
+
continue
|
|
595
|
+
if (
|
|
596
|
+
isinstance(item, dict)
|
|
597
|
+
and item.get("type") == "phase.start"
|
|
598
|
+
and item.get("phase") == "review"
|
|
599
|
+
and isinstance(item.get("run_id"), str)
|
|
600
|
+
and item["run_id"].strip()
|
|
601
|
+
):
|
|
602
|
+
run_id = item["run_id"]
|
|
603
|
+
return run_id
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def scaffold(change_dir: Path, run_id: str | None) -> dict[str, Any]:
|
|
607
|
+
"""按当前轮次生成 review-findings / fixback-dispositions 的写入骨架。
|
|
608
|
+
|
|
609
|
+
sidecar 的 schema 以前只能从 harness_review.py 源码里挖;骨架直接能喂给
|
|
610
|
+
write-findings / write-dispositions --stdin,消除手拼 JSON 的整类错误。
|
|
611
|
+
已有 findings 时,骨架为每条 finding 生成一条占位处置(disposition=OPEN,
|
|
612
|
+
由调用方逐条改判),并复用 findings 的 runId,保证两份 sidecar 同属一轮。
|
|
613
|
+
"""
|
|
614
|
+
change_dir = Path(change_dir)
|
|
615
|
+
findings_doc = _load_findings(change_dir)
|
|
616
|
+
if findings_doc is not None:
|
|
617
|
+
effective_run_id = str(findings_doc.get("runId") or "").strip()
|
|
618
|
+
if not effective_run_id:
|
|
619
|
+
return {
|
|
620
|
+
"ok": False,
|
|
621
|
+
"code": "FINDINGS_RUN_ID_MISSING",
|
|
622
|
+
"problems": ["review-findings.json 缺少 runId,无法生成同轮处置骨架"],
|
|
623
|
+
}
|
|
624
|
+
dispositions = [
|
|
625
|
+
{"findingId": f["id"], "disposition": "OPEN"}
|
|
626
|
+
for f in findings_doc.get("findings", [])
|
|
627
|
+
if isinstance(f, dict) and isinstance(f.get("id"), str)
|
|
628
|
+
]
|
|
629
|
+
return {
|
|
630
|
+
"ok": True,
|
|
631
|
+
"code": "REVIEW_SCAFFOLD_DISPOSITIONS",
|
|
632
|
+
"runId": effective_run_id,
|
|
633
|
+
"dispositionsInput": {
|
|
634
|
+
"schemaVersion": 1,
|
|
635
|
+
"runId": effective_run_id,
|
|
636
|
+
"dispositions": dispositions,
|
|
637
|
+
},
|
|
638
|
+
"writeCommand": (
|
|
639
|
+
"harness_review.py write-dispositions "
|
|
640
|
+
f"--change-dir {change_dir} --stdin"
|
|
641
|
+
),
|
|
642
|
+
"note": "逐条把 disposition 改判为 FIXED/ACCEPTED_RISK/DEFERRED/NOT_APPLICABLE;"
|
|
643
|
+
"OPEN 表示待修复,gate 关门时所有 finding 都必须有处置记录。",
|
|
644
|
+
}
|
|
645
|
+
effective_run_id = (run_id or "").strip() or _latest_review_run_id(change_dir)
|
|
646
|
+
if not effective_run_id:
|
|
647
|
+
return {
|
|
648
|
+
"ok": False,
|
|
649
|
+
"code": "SCAFFOLD_RUN_ID_REQUIRED",
|
|
650
|
+
"problems": [
|
|
651
|
+
"无法从 events.ndjson 推断本轮 review runId;请显式传 --run-id"
|
|
652
|
+
],
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
"ok": True,
|
|
656
|
+
"code": "REVIEW_SCAFFOLD_FINDINGS",
|
|
657
|
+
"runId": effective_run_id,
|
|
658
|
+
"findingsInput": {
|
|
659
|
+
"schemaVersion": 1,
|
|
660
|
+
"runId": effective_run_id,
|
|
661
|
+
"changeName": change_dir.name,
|
|
662
|
+
"findings": [],
|
|
663
|
+
"_exampleFinding": {
|
|
664
|
+
"dimension": "正确性",
|
|
665
|
+
"severity": "YELLOW",
|
|
666
|
+
"path": "src/example.ts",
|
|
667
|
+
"line": 1,
|
|
668
|
+
"title": "一句话问题标题",
|
|
669
|
+
"fixbackAction": "code",
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
"writeCommand": (
|
|
673
|
+
f"harness_review.py write-findings --change-dir {change_dir} --stdin"
|
|
674
|
+
),
|
|
675
|
+
"note": "无发现时保持 findings 为空数组即合法;write-findings 会为每条 "
|
|
676
|
+
"finding 分配稳定 id。落地后重跑 scaffold 生成对应的处置骨架。",
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def cmd_scaffold(args: argparse.Namespace) -> int:
|
|
681
|
+
return _emit(
|
|
682
|
+
scaffold(Path(args.change_dir), getattr(args, "run_id", None)),
|
|
683
|
+
as_json=True,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
|
|
578
687
|
def cmd_status(args: argparse.Namespace) -> int:
|
|
579
688
|
return _emit(status(Path(args.change_dir)), as_json=True)
|
|
580
689
|
|
|
@@ -611,6 +720,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
611
720
|
)
|
|
612
721
|
p_dispositions.set_defaults(func=cmd_write_dispositions)
|
|
613
722
|
|
|
723
|
+
p_scaffold = sub.add_parser(
|
|
724
|
+
"scaffold",
|
|
725
|
+
help="按当前轮次生成 review-findings / fixback-dispositions 的写入骨架",
|
|
726
|
+
)
|
|
727
|
+
p_scaffold.add_argument("--change-dir", required=True)
|
|
728
|
+
p_scaffold.add_argument(
|
|
729
|
+
"--run-id",
|
|
730
|
+
help="本轮 review runId;缺省从 events.ndjson 最新的 review phase.start 推断",
|
|
731
|
+
)
|
|
732
|
+
p_scaffold.set_defaults(func=cmd_scaffold)
|
|
733
|
+
|
|
614
734
|
p_status = sub.add_parser("status")
|
|
615
735
|
p_status.add_argument("--change-dir", required=True)
|
|
616
736
|
p_status.set_defaults(func=cmd_status)
|
|
@@ -324,6 +324,7 @@ def _sensitive_candidates(
|
|
|
324
324
|
for pattern in (_SENSITIVE_ASSIGNMENT, _AUTHORIZATION_ASSIGNMENT)
|
|
325
325
|
for match in pattern.finditer(raw)
|
|
326
326
|
if not _is_sensitive_placeholder(match.group("value"))
|
|
327
|
+
and not _is_harness_internal_key(raw, match)
|
|
327
328
|
),
|
|
328
329
|
None,
|
|
329
330
|
)
|
|
@@ -337,6 +338,18 @@ def _sensitive_candidates(
|
|
|
337
338
|
return candidates
|
|
338
339
|
|
|
339
340
|
|
|
341
|
+
def _is_harness_internal_key(raw: bytes, match: re.Match[bytes]) -> bool:
|
|
342
|
+
"""harness 自己生成的系统字段不是用户秘密。
|
|
343
|
+
|
|
344
|
+
plan finalize 发布日志(meta/publication-journals/*.json)里的
|
|
345
|
+
``recovery_token`` 是内部续跑凭证,会被通用 ``token`` 规则误伤
|
|
346
|
+
(2026-08-30 sales-insight-agent archive 实测:两个发布日志串行阻断)。
|
|
347
|
+
正则从关键字(如 ``token``)处起匹配,豁免看关键字前面的字段名前缀。
|
|
348
|
+
"""
|
|
349
|
+
prefix = raw[max(0, match.start() - 16):match.start()]
|
|
350
|
+
return bool(re.search(rb'(?i)recovery_["\']?$', prefix))
|
|
351
|
+
|
|
352
|
+
|
|
340
353
|
def _is_sensitive_placeholder(value: bytes) -> bool:
|
|
341
354
|
text = value.decode("utf-8", errors="ignore").strip().strip("`'").strip()
|
|
342
355
|
return bool(
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
|
|
4
4
|
/harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=626fde5b3e847963; overlay=none; agent=pi; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -126,11 +126,12 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
|
|
|
126
126
|
|
|
127
127
|
`execute` 内部运行自动门禁。实际计划最后阶段已终态、边界快照存在且 `status.archivable=true` 时直接执行;未满足时返回阻断且不移动原目录。用户取消后不得重跑。
|
|
128
128
|
|
|
129
|
-
### 二·A
|
|
129
|
+
### 二·A、常见阻断的正规出路(不要自己拼 python -c)
|
|
130
130
|
|
|
131
131
|
| 阻断 | 出路 |
|
|
132
132
|
|------|------|
|
|
133
|
-
| `
|
|
133
|
+
| `PROJECT_RELEASE_POLICY_BLOCKED` / `LOCAL_RELEASE_NOT_AUTHORIZED`(本地可复现证据未获发布授权) | `python <skills-root>/scripts/harness_change.py allow-local-release --change <cn> --json`(在 `meta/gate-policy.json` 写入 `candidateVerification.allowLocalRelease=true`,幂等,保留既有策略内容)。**不要**手改 `gate-policy.json` |
|
|
134
|
+
| `SENSITIVE_EVIDENCE_UNQUARANTINED`(默认只告警,`HUNTER_HARNESS_SENSITIVE_SCAN=block` 时才阻断) | 报错的 `nextAction` 自带含全部 `--file` 的完整命令;等价于 `python <skills-root>/scripts/harness_runtime.py quarantine-evidence --project . --change-dir ".harness/changes/<cn>" --file "<相对 change-dir 的路径>" --reason "<为什么是敏感证据>" --json`(`--file` 可重复)。私有根默认已与项目同盘、且在项目根之外——**不要**手工指定项目内的路径,归档的密钥扫描会以 `SECRET_SCAN_PRIVATE_PATH_IN_COPY_ROOT` 拒绝。harness 自生成发布日志里的 `recovery_token` 是系统字段,0.4.7(workflow-harness)起扫描器已豁免,无需隔离 |
|
|
134
135
|
| `DIFF_ZERO_WITH_NONEMPTY_COMMIT`(提交范围非空但 filesChanged=0) | 契约缺 `ownership.productPaths`,全部改动被判为 `foreignPaths`。用 `python <skills-root>/scripts/harness_change.py declare-ownership --change <cn> --product-path "<目录前缀或精确文件>" --json` 按计划的实际改动范围声明(可重复;只收精确路径,不支持通配)。**不要**手改 `change-context.json` |
|
|
135
136
|
|
|
136
137
|
### 二·A·1、发布内容预检与服务端 422
|
|
@@ -3,7 +3,7 @@ name: harness-codebase-map
|
|
|
3
3
|
description: 分析现有代码库并生成 .harness/codebase/map/
|
|
4
4
|
下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=626fde5b3e847963; overlay=none; agent=pi; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|