@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
|
@@ -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)
|
|
@@ -1614,6 +1614,35 @@ def _scenario_receipt_error(code: str, message: str) -> dict[str, Any]:
|
|
|
1614
1614
|
return {"ok": False, "code": code, "error": message}
|
|
1615
1615
|
|
|
1616
1616
|
|
|
1617
|
+
def _resolve_receipt_path(
|
|
1618
|
+
raw: str, change_dir: Path
|
|
1619
|
+
) -> tuple[Path | None, list[Path]]:
|
|
1620
|
+
"""Resolve --scenario-receipt-file against CWD *and* the change dir.
|
|
1621
|
+
|
|
1622
|
+
Skills write receipts under ``<change-dir>/runtime/``, so a bare
|
|
1623
|
+
``runtime/scenario-receipt-*.json`` is the natural thing to pass. Resolving
|
|
1624
|
+
only against the CWD made that fail with a bare ENOENT. Return the first
|
|
1625
|
+
existing candidate plus every path tried, so the error can name them.
|
|
1626
|
+
"""
|
|
1627
|
+
candidate = Path(raw).expanduser()
|
|
1628
|
+
if candidate.is_absolute():
|
|
1629
|
+
resolved = candidate.resolve()
|
|
1630
|
+
return (resolved if resolved.is_file() else None), [resolved]
|
|
1631
|
+
candidates = [
|
|
1632
|
+
(Path.cwd() / candidate).resolve(),
|
|
1633
|
+
(change_dir / candidate).resolve(),
|
|
1634
|
+
]
|
|
1635
|
+
# De-duplicate while preserving order (CWD == change_dir is common).
|
|
1636
|
+
unique: list[Path] = []
|
|
1637
|
+
for item in candidates:
|
|
1638
|
+
if item not in unique:
|
|
1639
|
+
unique.append(item)
|
|
1640
|
+
for item in unique:
|
|
1641
|
+
if item.is_file():
|
|
1642
|
+
return item, unique
|
|
1643
|
+
return None, unique
|
|
1644
|
+
|
|
1645
|
+
|
|
1617
1646
|
def _canonical_string_list(
|
|
1618
1647
|
receipt: dict[str, Any],
|
|
1619
1648
|
field: str,
|
|
@@ -2786,17 +2815,37 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2786
2815
|
if manifest_schema >= 2 and not _nonempty_str(receipt_file):
|
|
2787
2816
|
return emit_error(
|
|
2788
2817
|
"scenario-manifest schemaVersion 2 requires "
|
|
2789
|
-
"--scenario-receipt-file"
|
|
2818
|
+
"--scenario-receipt-file; generate a skeleton with: "
|
|
2819
|
+
"harness_ledger.py scenario-receipt-template "
|
|
2820
|
+
f"--change-dir {change_dir} --scenario-ids "
|
|
2821
|
+
f"{','.join(ids)} --runner <name> --out "
|
|
2822
|
+
"runtime/scenario-receipt-<verification>.json",
|
|
2790
2823
|
as_json=as_json,
|
|
2791
2824
|
error_code="SCENARIO_RECEIPT_REQUIRED",
|
|
2825
|
+
extra={"template": "scenario-receipt-template"},
|
|
2792
2826
|
)
|
|
2793
2827
|
if _nonempty_str(receipt_file):
|
|
2828
|
+
resolved_receipt, receipt_candidates = _resolve_receipt_path(
|
|
2829
|
+
str(receipt_file), change_dir
|
|
2830
|
+
)
|
|
2831
|
+
if resolved_receipt is None:
|
|
2832
|
+
return emit_error(
|
|
2833
|
+
"scenario receipt not found; tried: "
|
|
2834
|
+
+ ", ".join(str(c) for c in receipt_candidates),
|
|
2835
|
+
as_json=as_json,
|
|
2836
|
+
error_code="SCENARIO_RECEIPT_NOT_FOUND",
|
|
2837
|
+
extra={
|
|
2838
|
+
"triedPaths": [str(c) for c in receipt_candidates],
|
|
2839
|
+
"hint": (
|
|
2840
|
+
"--scenario-receipt-file accepts an absolute "
|
|
2841
|
+
"path, a CWD-relative path, or a path relative "
|
|
2842
|
+
"to --change-dir"
|
|
2843
|
+
),
|
|
2844
|
+
},
|
|
2845
|
+
)
|
|
2794
2846
|
try:
|
|
2795
2847
|
receipt_payload = json.loads(
|
|
2796
|
-
|
|
2797
|
-
.expanduser()
|
|
2798
|
-
.resolve()
|
|
2799
|
-
.read_text(encoding="utf-8-sig")
|
|
2848
|
+
resolved_receipt.read_text(encoding="utf-8-sig")
|
|
2800
2849
|
)
|
|
2801
2850
|
except (OSError, json.JSONDecodeError) as exc:
|
|
2802
2851
|
return emit_error(
|
|
@@ -2953,6 +3002,160 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2953
3002
|
return 0
|
|
2954
3003
|
|
|
2955
3004
|
|
|
3005
|
+
def cmd_scenario_receipt_template(args: argparse.Namespace) -> int:
|
|
3006
|
+
"""Emit a schema-v2 receipt skeleton built from the scenario manifest.
|
|
3007
|
+
|
|
3008
|
+
Without this, agents had to read harness_ledger.py source to learn the
|
|
3009
|
+
receipt shape. The skeleton is already valid for `record`; the caller only
|
|
3010
|
+
has to correct `status` for any test that did not pass.
|
|
3011
|
+
"""
|
|
3012
|
+
as_json = bool(args.json)
|
|
3013
|
+
change_dir = resolve_path(args.change_dir)
|
|
3014
|
+
ids = [s.strip() for s in str(args.scenario_ids).split(",") if s.strip()]
|
|
3015
|
+
if not ids:
|
|
3016
|
+
return emit_error(
|
|
3017
|
+
"--scenario-ids must list at least one scenario ID",
|
|
3018
|
+
as_json=as_json,
|
|
3019
|
+
error_code="SCENARIO_IDS_REQUIRED",
|
|
3020
|
+
)
|
|
3021
|
+
if len(ids) != len(set(ids)):
|
|
3022
|
+
return emit_error(
|
|
3023
|
+
"scenario IDs must be unique",
|
|
3024
|
+
as_json=as_json,
|
|
3025
|
+
error_code="SCENARIO_ID_DUPLICATE",
|
|
3026
|
+
)
|
|
3027
|
+
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
3028
|
+
try:
|
|
3029
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
|
3030
|
+
except FileNotFoundError:
|
|
3031
|
+
return emit_error(
|
|
3032
|
+
f"scenario manifest does not exist: {manifest_path}",
|
|
3033
|
+
as_json=as_json,
|
|
3034
|
+
error_code="SCENARIO_MANIFEST_MISSING",
|
|
3035
|
+
)
|
|
3036
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
3037
|
+
return emit_error(
|
|
3038
|
+
f"scenario manifest unreadable: {exc}",
|
|
3039
|
+
as_json=as_json,
|
|
3040
|
+
error_code="SCENARIO_MANIFEST_INVALID",
|
|
3041
|
+
)
|
|
3042
|
+
scenarios = manifest.get("scenarios") if isinstance(manifest, dict) else None
|
|
3043
|
+
if not isinstance(scenarios, list):
|
|
3044
|
+
return emit_error(
|
|
3045
|
+
"scenario-manifest.json must contain a scenarios array",
|
|
3046
|
+
as_json=as_json,
|
|
3047
|
+
error_code="SCENARIO_MANIFEST_INVALID",
|
|
3048
|
+
)
|
|
3049
|
+
by_id = {
|
|
3050
|
+
str(item.get("id") or "").strip(): item
|
|
3051
|
+
for item in scenarios
|
|
3052
|
+
if isinstance(item, dict) and _nonempty_str(item.get("id"))
|
|
3053
|
+
}
|
|
3054
|
+
unknown = sorted(set(ids) - set(by_id))
|
|
3055
|
+
if unknown:
|
|
3056
|
+
return emit_error(
|
|
3057
|
+
"scenario IDs are not declared in scenario-manifest.json: "
|
|
3058
|
+
+ ", ".join(unknown),
|
|
3059
|
+
as_json=as_json,
|
|
3060
|
+
error_code="SCENARIO_ID_UNKNOWN",
|
|
3061
|
+
)
|
|
3062
|
+
status = str(args.status).strip().upper()
|
|
3063
|
+
if status not in {"PASSED", "FAILED", "SKIPPED"}:
|
|
3064
|
+
return emit_error(
|
|
3065
|
+
"--status must be one of PASSED|FAILED|SKIPPED",
|
|
3066
|
+
as_json=as_json,
|
|
3067
|
+
error_code="SCENARIO_RECEIPT_INVALID",
|
|
3068
|
+
)
|
|
3069
|
+
attempt = int(args.attempt)
|
|
3070
|
+
if attempt < 1:
|
|
3071
|
+
return emit_error(
|
|
3072
|
+
"--attempt must be a positive integer",
|
|
3073
|
+
as_json=as_json,
|
|
3074
|
+
error_code="SCENARIO_RECEIPT_INVALID",
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
identities: list[tuple[str, str, str]] = []
|
|
3078
|
+
incomplete: list[str] = []
|
|
3079
|
+
for scenario_id in ids:
|
|
3080
|
+
scenario = by_id[scenario_id]
|
|
3081
|
+
if any(
|
|
3082
|
+
not _nonempty_str(scenario.get(field))
|
|
3083
|
+
for field in ("executableTestId", "testFile", "testTitle")
|
|
3084
|
+
):
|
|
3085
|
+
incomplete.append(scenario_id)
|
|
3086
|
+
continue
|
|
3087
|
+
identities.append(
|
|
3088
|
+
(
|
|
3089
|
+
str(scenario["executableTestId"]).strip(),
|
|
3090
|
+
str(scenario["testFile"]).strip(),
|
|
3091
|
+
str(scenario["testTitle"]).strip(),
|
|
3092
|
+
)
|
|
3093
|
+
)
|
|
3094
|
+
if incomplete:
|
|
3095
|
+
return emit_error(
|
|
3096
|
+
"scenarios are missing executableTestId/testFile/testTitle in the "
|
|
3097
|
+
"manifest: " + ", ".join(incomplete),
|
|
3098
|
+
as_json=as_json,
|
|
3099
|
+
error_code="SCENARIO_MANIFEST_INVALID",
|
|
3100
|
+
extra={"incomplete": incomplete},
|
|
3101
|
+
)
|
|
3102
|
+
|
|
3103
|
+
runner: dict[str, Any] = {"name": str(args.runner).strip()}
|
|
3104
|
+
if _nonempty_str(getattr(args, "runner_version", None)):
|
|
3105
|
+
runner["version"] = str(args.runner_version).strip()
|
|
3106
|
+
receipt = {
|
|
3107
|
+
"schemaVersion": 1,
|
|
3108
|
+
"runner": runner,
|
|
3109
|
+
"attempt": attempt,
|
|
3110
|
+
"declared": [test_id for test_id, _, _ in identities],
|
|
3111
|
+
"selected": [test_id for test_id, _, _ in identities],
|
|
3112
|
+
"collected": [
|
|
3113
|
+
{"testId": test_id, "file": file, "title": title}
|
|
3114
|
+
for test_id, file, title in identities
|
|
3115
|
+
],
|
|
3116
|
+
"executed": [
|
|
3117
|
+
{
|
|
3118
|
+
"testId": test_id,
|
|
3119
|
+
"file": file,
|
|
3120
|
+
"title": title,
|
|
3121
|
+
"attempt": attempt,
|
|
3122
|
+
"status": status,
|
|
3123
|
+
}
|
|
3124
|
+
for test_id, file, title in identities
|
|
3125
|
+
],
|
|
3126
|
+
}
|
|
3127
|
+
body = json.dumps(receipt, ensure_ascii=False, indent=2) + "\n"
|
|
3128
|
+
|
|
3129
|
+
out_raw = getattr(args, "out", None)
|
|
3130
|
+
if not _nonempty_str(out_raw):
|
|
3131
|
+
sys.stdout.write(body)
|
|
3132
|
+
return 0
|
|
3133
|
+
out_path = Path(str(out_raw)).expanduser()
|
|
3134
|
+
if not out_path.is_absolute():
|
|
3135
|
+
out_path = change_dir / out_path
|
|
3136
|
+
out_path = out_path.resolve()
|
|
3137
|
+
try:
|
|
3138
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3139
|
+
out_path.write_text(body, encoding="utf-8", newline="\n")
|
|
3140
|
+
except OSError as exc:
|
|
3141
|
+
return emit_error(
|
|
3142
|
+
f"cannot write scenario receipt: {exc}",
|
|
3143
|
+
as_json=as_json,
|
|
3144
|
+
error_code="SCENARIO_RECEIPT_WRITE_FAILED",
|
|
3145
|
+
)
|
|
3146
|
+
payload = {
|
|
3147
|
+
"ok": True,
|
|
3148
|
+
"action": "scenario-receipt-template",
|
|
3149
|
+
"path": str(out_path),
|
|
3150
|
+
"scenarioIds": ids,
|
|
3151
|
+
}
|
|
3152
|
+
if as_json:
|
|
3153
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
3154
|
+
else:
|
|
3155
|
+
sys.stdout.write(f"{out_path}\n")
|
|
3156
|
+
return 0
|
|
3157
|
+
|
|
3158
|
+
|
|
2956
3159
|
def cmd_diff_hash(args: argparse.Namespace) -> int:
|
|
2957
3160
|
as_json = bool(args.json)
|
|
2958
3161
|
repo_raw = getattr(args, "repo", None)
|
|
@@ -3234,6 +3437,42 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3234
3437
|
)
|
|
3235
3438
|
p_diff.set_defaults(func=cmd_diff_hash)
|
|
3236
3439
|
|
|
3440
|
+
p_receipt = sub.add_parser(
|
|
3441
|
+
"scenario-receipt-template",
|
|
3442
|
+
parents=[shared_json],
|
|
3443
|
+
help=(
|
|
3444
|
+
"emit a schema-v2 scenario execution receipt skeleton "
|
|
3445
|
+
"pre-filled from meta/scenario-manifest.json"
|
|
3446
|
+
),
|
|
3447
|
+
)
|
|
3448
|
+
p_receipt.add_argument("--change-dir", required=True)
|
|
3449
|
+
p_receipt.add_argument(
|
|
3450
|
+
"--scenario-ids",
|
|
3451
|
+
required=True,
|
|
3452
|
+
help="comma-separated scenario IDs to include (must exist in the manifest)",
|
|
3453
|
+
)
|
|
3454
|
+
p_receipt.add_argument(
|
|
3455
|
+
"--runner",
|
|
3456
|
+
required=True,
|
|
3457
|
+
help="test runner name recorded in the receipt (e.g. vitest / maven-surefire)",
|
|
3458
|
+
)
|
|
3459
|
+
p_receipt.add_argument("--runner-version", default=None)
|
|
3460
|
+
p_receipt.add_argument("--attempt", type=int, default=1)
|
|
3461
|
+
p_receipt.add_argument(
|
|
3462
|
+
"--status",
|
|
3463
|
+
default="PASSED",
|
|
3464
|
+
help="per-test status written into executed[] (default: PASSED)",
|
|
3465
|
+
)
|
|
3466
|
+
p_receipt.add_argument(
|
|
3467
|
+
"--out",
|
|
3468
|
+
default=None,
|
|
3469
|
+
help=(
|
|
3470
|
+
"write the skeleton to this path instead of stdout; relative paths "
|
|
3471
|
+
"resolve against --change-dir (same rule as --scenario-receipt-file)"
|
|
3472
|
+
),
|
|
3473
|
+
)
|
|
3474
|
+
p_receipt.set_defaults(func=cmd_scenario_receipt_template)
|
|
3475
|
+
|
|
3237
3476
|
return parser
|
|
3238
3477
|
|
|
3239
3478
|
|