@hunter-harness/workflow-harness 0.2.54 → 0.2.56
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/README.md +3 -3
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +8 -4
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +3 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/claude-code/harness-submit/checklist.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 +8 -6
- package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/claude-code/scripts/harness_context.py +236 -63
- package/harness/bundles/general/claude-code/scripts/harness_events.py +86 -5
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +3 -3
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +8 -4
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -6
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +236 -63
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +86 -5
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +3 -3
- package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +3 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +8 -6
- package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-test/reference.md +2 -2
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/codex/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/codex/scripts/harness_context.py +236 -63
- package/harness/bundles/general/codex/scripts/harness_events.py +86 -5
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/codex/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/codex/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +3 -3
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +3 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -6
- package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/cursor/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/cursor/scripts/harness_context.py +236 -63
- package/harness/bundles/general/cursor/scripts/harness_events.py +86 -5
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/cursor/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +3 -3
- package/harness/bundles/java/claude-code/contracts/workflow-policy.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 +21 -33
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +8 -4
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/claude-code/harness-submit/checklist.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 +8 -6
- package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/claude-code/scripts/harness_context.py +236 -63
- package/harness/bundles/java/claude-code/scripts/harness_events.py +86 -5
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +3 -3
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +8 -4
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -6
- package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +236 -63
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +86 -5
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +3 -3
- package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-run/reference.md +2 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +8 -6
- package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-test/reference.md +1 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/codex/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/codex/scripts/harness_context.py +236 -63
- package/harness/bundles/java/codex/scripts/harness_events.py +86 -5
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/codex/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/codex/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +3 -3
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -6
- package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/cursor/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/cursor/scripts/harness_context.py +236 -63
- package/harness/bundles/java/cursor/scripts/harness_events.py +86 -5
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/cursor/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
- package/harness/manifests/general/claude-code.json +32 -32
- package/harness/manifests/general/codebuddy.json +32 -32
- package/harness/manifests/general/codex.json +32 -32
- package/harness/manifests/general/cursor.json +32 -32
- package/harness/manifests/java/claude-code.json +35 -35
- package/harness/manifests/java/codebuddy.json +35 -35
- package/harness/manifests/java/codex.json +35 -35
- package/harness/manifests/java/cursor.json +35 -35
- package/hunter-workflow-family.json +5 -4
- package/package.json +1 -1
|
@@ -23,6 +23,18 @@ PHASE_GRAPH = {
|
|
|
23
23
|
"submit": (),
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
WORKFLOW_PHASES = (
|
|
27
|
+
"plan",
|
|
28
|
+
"run",
|
|
29
|
+
"test",
|
|
30
|
+
"review",
|
|
31
|
+
"package",
|
|
32
|
+
"apidoc",
|
|
33
|
+
"submit",
|
|
34
|
+
"merge",
|
|
35
|
+
"archive",
|
|
36
|
+
)
|
|
37
|
+
|
|
26
38
|
DISPLAY_TITLE_MAX_LENGTH = 80
|
|
27
39
|
|
|
28
40
|
|
|
@@ -206,6 +218,107 @@ def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
|
|
|
206
218
|
return contract_root, contract, state_root
|
|
207
219
|
|
|
208
220
|
|
|
221
|
+
def _phase_plan(contract_root: Path) -> tuple[list[str] | None, str]:
|
|
222
|
+
policy_path = contract_root / "meta" / "gate-policy.json"
|
|
223
|
+
if not policy_path.is_file():
|
|
224
|
+
return None, "legacy"
|
|
225
|
+
try:
|
|
226
|
+
policy = _read_json(policy_path)
|
|
227
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
228
|
+
return None, "legacy"
|
|
229
|
+
for field, source in (
|
|
230
|
+
("plannedPhases", "change"),
|
|
231
|
+
("defaultPhases", "policy-default"),
|
|
232
|
+
):
|
|
233
|
+
raw = policy.get(field)
|
|
234
|
+
if not isinstance(raw, list):
|
|
235
|
+
continue
|
|
236
|
+
phases = [str(item).strip() for item in raw if str(item).strip()]
|
|
237
|
+
if (
|
|
238
|
+
phases
|
|
239
|
+
and len(phases) == len(set(phases))
|
|
240
|
+
and all(item in WORKFLOW_PHASES for item in phases)
|
|
241
|
+
):
|
|
242
|
+
return phases, source
|
|
243
|
+
return None, "legacy"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _allowed_next_phases(contract_root: Path, from_phase: str) -> list[str]:
|
|
247
|
+
planned, _source = _phase_plan(contract_root)
|
|
248
|
+
if planned is None:
|
|
249
|
+
return list(PHASE_GRAPH.get(from_phase, ()))
|
|
250
|
+
if from_phase not in planned:
|
|
251
|
+
return []
|
|
252
|
+
index = planned.index(from_phase)
|
|
253
|
+
allowed = planned[index + 1 : index + 2]
|
|
254
|
+
if from_phase in {"test", "review"} and "run" in planned:
|
|
255
|
+
allowed = [*allowed, "run"]
|
|
256
|
+
return list(dict.fromkeys(allowed))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def configure_phase_plan(
|
|
260
|
+
project: Path,
|
|
261
|
+
change: str,
|
|
262
|
+
*,
|
|
263
|
+
phases: list[str],
|
|
264
|
+
operator: str,
|
|
265
|
+
reason: str,
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Persist the single phase plan consumed by Context, Archive and Platform."""
|
|
268
|
+
project = Path(project).resolve()
|
|
269
|
+
try:
|
|
270
|
+
contract_root, _contract_data, _state_root = _contract(project, change)
|
|
271
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
272
|
+
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
273
|
+
normalized = [str(item).strip() for item in phases if str(item).strip()]
|
|
274
|
+
if (
|
|
275
|
+
not normalized
|
|
276
|
+
or len(normalized) != len(set(normalized))
|
|
277
|
+
or any(item not in WORKFLOW_PHASES for item in normalized)
|
|
278
|
+
or normalized[0] != "plan"
|
|
279
|
+
or normalized[-1] != "archive"
|
|
280
|
+
):
|
|
281
|
+
return {
|
|
282
|
+
"ok": False,
|
|
283
|
+
"code": "PHASE_PLAN_INVALID",
|
|
284
|
+
"message": "phase plan must be unique, start with plan, and end with archive",
|
|
285
|
+
"allowedPhases": list(WORKFLOW_PHASES),
|
|
286
|
+
}
|
|
287
|
+
if not operator.strip() or not reason.strip():
|
|
288
|
+
return {
|
|
289
|
+
"ok": False,
|
|
290
|
+
"code": "PHASE_PLAN_JUSTIFICATION_REQUIRED",
|
|
291
|
+
}
|
|
292
|
+
policy_path = contract_root / "meta" / "gate-policy.json"
|
|
293
|
+
policy = _read_json(policy_path) if policy_path.is_file() else {"schemaVersion": 1}
|
|
294
|
+
skipped = [
|
|
295
|
+
{
|
|
296
|
+
"phase": phase,
|
|
297
|
+
"reason": reason.strip(),
|
|
298
|
+
"operator": operator.strip(),
|
|
299
|
+
"decidedAt": _now().isoformat(),
|
|
300
|
+
}
|
|
301
|
+
for phase in WORKFLOW_PHASES
|
|
302
|
+
if phase not in normalized and phase != "merge"
|
|
303
|
+
]
|
|
304
|
+
policy["plannedPhases"] = normalized
|
|
305
|
+
policy["skippedPhases"] = skipped
|
|
306
|
+
policy["phasePlan"] = {
|
|
307
|
+
"source": "change-override",
|
|
308
|
+
"operator": operator.strip(),
|
|
309
|
+
"reason": reason.strip(),
|
|
310
|
+
"updatedAt": _now().isoformat(),
|
|
311
|
+
}
|
|
312
|
+
_write_json_atomic(policy_path, policy)
|
|
313
|
+
return {
|
|
314
|
+
"ok": True,
|
|
315
|
+
"code": "PHASE_PLAN_CONFIGURED",
|
|
316
|
+
"plannedPhases": normalized,
|
|
317
|
+
"skippedPhases": skipped,
|
|
318
|
+
"path": str(policy_path),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
209
322
|
def _active_changes(project: Path) -> list[str]:
|
|
210
323
|
root = project.resolve() / ".harness" / "changes"
|
|
211
324
|
if not root.is_dir():
|
|
@@ -319,6 +432,14 @@ def prepare_context(
|
|
|
319
432
|
and lifecycle.get("status") not in {None, "active"}
|
|
320
433
|
):
|
|
321
434
|
return {"ok": False, "code": "CHANGE_NOT_ACTIVE", "changeName": change}
|
|
435
|
+
planned_phases, phase_plan_source = _phase_plan(contract_root)
|
|
436
|
+
if planned_phases is not None and phase not in planned_phases:
|
|
437
|
+
return {
|
|
438
|
+
"ok": False,
|
|
439
|
+
"code": "PHASE_NOT_PLANNED",
|
|
440
|
+
"phase": phase,
|
|
441
|
+
"plannedPhases": planned_phases,
|
|
442
|
+
}
|
|
322
443
|
paths = _paths(state_root)
|
|
323
444
|
paths["runtime"].mkdir(parents=True, exist_ok=True)
|
|
324
445
|
recovery: dict[str, Any] | None = None
|
|
@@ -395,9 +516,13 @@ def prepare_context(
|
|
|
395
516
|
},
|
|
396
517
|
)
|
|
397
518
|
next_phases = (
|
|
398
|
-
|
|
399
|
-
if
|
|
400
|
-
else
|
|
519
|
+
_allowed_next_phases(contract_root, phase)
|
|
520
|
+
if planned_phases is not None
|
|
521
|
+
else (
|
|
522
|
+
list(PHASE_GRAPH.get(str(latest.get("toPhase")), ()))
|
|
523
|
+
if latest
|
|
524
|
+
else ["plan", "run"]
|
|
525
|
+
)
|
|
401
526
|
)
|
|
402
527
|
return {
|
|
403
528
|
"ok": True,
|
|
@@ -408,6 +533,8 @@ def prepare_context(
|
|
|
408
533
|
"contractRoot": str(contract_root),
|
|
409
534
|
"stateRoot": str(state_root),
|
|
410
535
|
"nextPhases": next_phases,
|
|
536
|
+
"plannedPhases": planned_phases,
|
|
537
|
+
"phasePlanSource": phase_plan_source,
|
|
411
538
|
"legacyBootstrap": not transitions,
|
|
412
539
|
"latestTransition": latest,
|
|
413
540
|
"lease": lease,
|
|
@@ -423,42 +550,19 @@ def _invalidate_for_fixback(
|
|
|
423
550
|
from_phase: str,
|
|
424
551
|
to_phase: str,
|
|
425
552
|
) -> dict[str, Any]:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
ledger = _read_json(ledger_path)
|
|
431
|
-
except (OSError, ValueError, json.JSONDecodeError):
|
|
432
|
-
ledger = {}
|
|
433
|
-
targets = ledger.get("verificationTargets")
|
|
434
|
-
if isinstance(targets, dict):
|
|
435
|
-
for target_id, target in targets.items():
|
|
436
|
-
if not isinstance(target, dict):
|
|
437
|
-
continue
|
|
438
|
-
target["reusable"] = False
|
|
439
|
-
target["invalidation"] = {
|
|
440
|
-
"code": "FIXBACK_INVALIDATED",
|
|
441
|
-
"transitionHash": transition_hash,
|
|
442
|
-
"fromPhase": from_phase,
|
|
443
|
-
"toPhase": to_phase,
|
|
444
|
-
"invalidatedAt": _now().isoformat(),
|
|
445
|
-
}
|
|
446
|
-
invalidated.append(str(target_id))
|
|
447
|
-
validations = ledger.get("validations")
|
|
448
|
-
if isinstance(validations, dict):
|
|
449
|
-
for entry in validations.values():
|
|
450
|
-
if isinstance(entry, dict):
|
|
451
|
-
entry["reusable"] = False
|
|
452
|
-
entry["invalidation"] = {
|
|
453
|
-
"code": "FIXBACK_INVALIDATED",
|
|
454
|
-
"transitionHash": transition_hash,
|
|
455
|
-
}
|
|
456
|
-
_write_json_atomic(ledger_path, ledger)
|
|
553
|
+
# A review→run handoff happens before product files are changed. Invalidating
|
|
554
|
+
# every target here made unrelated API/build evidence unusable. The Fixback
|
|
555
|
+
# resolver now invalidates only entries whose inputsFiles intersect the
|
|
556
|
+
# issue's actual changedFiles.
|
|
457
557
|
record = {
|
|
458
558
|
"schemaVersion": 1,
|
|
459
|
-
"code": "
|
|
559
|
+
"code": "FIXBACK_INVALIDATION_DEFERRED",
|
|
460
560
|
"transitionHash": transition_hash,
|
|
461
|
-
"
|
|
561
|
+
"fromPhase": from_phase,
|
|
562
|
+
"toPhase": to_phase,
|
|
563
|
+
"targetIds": [],
|
|
564
|
+
"deferred": True,
|
|
565
|
+
"reason": "changed-files-not-known",
|
|
462
566
|
"createdAt": _now().isoformat(),
|
|
463
567
|
}
|
|
464
568
|
_append_ndjson(
|
|
@@ -474,30 +578,103 @@ def close_transition(
|
|
|
474
578
|
*,
|
|
475
579
|
from_phase: str,
|
|
476
580
|
to_phase: str,
|
|
477
|
-
executor: str,
|
|
581
|
+
executor: str | None,
|
|
478
582
|
artifacts: list[str] | None = None,
|
|
479
583
|
status: str = "OK",
|
|
480
584
|
) -> dict[str, Any]:
|
|
481
585
|
project = Path(project).resolve()
|
|
482
|
-
|
|
586
|
+
try:
|
|
587
|
+
contract_root, _contract_data, state_root = _contract(project, change)
|
|
588
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
589
|
+
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
590
|
+
allowed_next = _allowed_next_phases(contract_root, from_phase)
|
|
591
|
+
if to_phase not in allowed_next:
|
|
592
|
+
planned_phases, source = _phase_plan(contract_root)
|
|
483
593
|
return {
|
|
484
594
|
"ok": False,
|
|
485
595
|
"code": "TRANSITION_ILLEGAL",
|
|
486
596
|
"fromPhase": from_phase,
|
|
487
597
|
"toPhase": to_phase,
|
|
598
|
+
"allowedNextPhases": allowed_next,
|
|
599
|
+
"plannedPhases": planned_phases,
|
|
600
|
+
"phasePlanSource": source,
|
|
488
601
|
}
|
|
489
|
-
try:
|
|
490
|
-
contract_root, _contract_data, state_root = _contract(project, change)
|
|
491
|
-
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
492
|
-
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
493
602
|
paths = _paths(state_root)
|
|
603
|
+
|
|
604
|
+
artifact_entries: list[dict[str, Any]] = []
|
|
605
|
+
for raw in artifacts or []:
|
|
606
|
+
requested = Path(raw)
|
|
607
|
+
candidates = (
|
|
608
|
+
[requested]
|
|
609
|
+
if requested.is_absolute()
|
|
610
|
+
else [project / requested, contract_root / requested]
|
|
611
|
+
)
|
|
612
|
+
valid: list[Path] = []
|
|
613
|
+
for candidate in candidates:
|
|
614
|
+
try:
|
|
615
|
+
resolved_candidate = candidate.resolve()
|
|
616
|
+
except OSError:
|
|
617
|
+
continue
|
|
618
|
+
if resolved_candidate in valid:
|
|
619
|
+
continue
|
|
620
|
+
if resolved_candidate.is_relative_to(project) and resolved_candidate.is_file():
|
|
621
|
+
valid.append(resolved_candidate)
|
|
622
|
+
if len(valid) > 1:
|
|
623
|
+
return {
|
|
624
|
+
"ok": False,
|
|
625
|
+
"code": "TRANSITION_ARTIFACT_AMBIGUOUS",
|
|
626
|
+
"path": raw,
|
|
627
|
+
"candidates": [item.relative_to(project).as_posix() for item in valid],
|
|
628
|
+
"message": "relative artifact exists under both project and change roots",
|
|
629
|
+
}
|
|
630
|
+
if not valid:
|
|
631
|
+
return {
|
|
632
|
+
"ok": False,
|
|
633
|
+
"code": "TRANSITION_ARTIFACT_INVALID",
|
|
634
|
+
"path": raw,
|
|
635
|
+
"acceptedBases": [str(project), str(contract_root)],
|
|
636
|
+
"examples": [
|
|
637
|
+
str(contract_root / "meta" / "plan-finalization.json"),
|
|
638
|
+
f".harness/changes/{change}/meta/plan-finalization.json",
|
|
639
|
+
"meta/plan-finalization.json",
|
|
640
|
+
],
|
|
641
|
+
}
|
|
642
|
+
resolved_artifact = valid[0]
|
|
643
|
+
artifact_entries.append(
|
|
644
|
+
{
|
|
645
|
+
"path": resolved_artifact.relative_to(project).as_posix(),
|
|
646
|
+
"sha256": _sha256(resolved_artifact),
|
|
647
|
+
}
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
transitions = _read_ndjson(paths["transitions"])
|
|
494
651
|
if not paths["lease"].is_file():
|
|
652
|
+
latest = transitions[-1] if transitions else None
|
|
653
|
+
if (
|
|
654
|
+
isinstance(latest, dict)
|
|
655
|
+
and latest.get("fromPhase") == from_phase
|
|
656
|
+
and latest.get("toPhase") == to_phase
|
|
657
|
+
and latest.get("status") == status
|
|
658
|
+
and latest.get("artifacts") == artifact_entries
|
|
659
|
+
and (executor is None or latest.get("executor") == executor)
|
|
660
|
+
):
|
|
661
|
+
return {
|
|
662
|
+
"ok": True,
|
|
663
|
+
"code": "TRANSITION_ALREADY_CLOSED",
|
|
664
|
+
"idempotent": True,
|
|
665
|
+
"receipt": latest,
|
|
666
|
+
"path": str(paths["transitions"]),
|
|
667
|
+
"invalidation": None,
|
|
668
|
+
}
|
|
495
669
|
return {"ok": False, "code": "CONTEXT_LEASE_REQUIRED"}
|
|
496
670
|
try:
|
|
497
671
|
lease = _read_json(paths["lease"])
|
|
498
672
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
499
673
|
return {"ok": False, "code": "CONTEXT_LEASE_INVALID"}
|
|
500
|
-
|
|
674
|
+
effective_executor = executor or str(lease.get("owner") or "")
|
|
675
|
+
if not effective_executor:
|
|
676
|
+
return {"ok": False, "code": "CONTEXT_EXECUTOR_REQUIRED"}
|
|
677
|
+
if lease.get("owner") != effective_executor or lease.get("phase") != from_phase:
|
|
501
678
|
return {
|
|
502
679
|
"ok": False,
|
|
503
680
|
"code": "CONTEXT_LEASE_MISMATCH",
|
|
@@ -508,25 +685,6 @@ def close_transition(
|
|
|
508
685
|
if expiry is None or _now() >= expiry:
|
|
509
686
|
return {"ok": False, "code": "CONTEXT_LEASE_EXPIRED"}
|
|
510
687
|
|
|
511
|
-
artifact_entries: list[dict[str, Any]] = []
|
|
512
|
-
for raw in artifacts or []:
|
|
513
|
-
path = Path(raw)
|
|
514
|
-
if not path.is_absolute():
|
|
515
|
-
path = contract_root / path
|
|
516
|
-
resolved = path.resolve()
|
|
517
|
-
if not resolved.is_relative_to(project) or not resolved.is_file():
|
|
518
|
-
return {
|
|
519
|
-
"ok": False,
|
|
520
|
-
"code": "TRANSITION_ARTIFACT_INVALID",
|
|
521
|
-
"path": str(path),
|
|
522
|
-
}
|
|
523
|
-
artifact_entries.append(
|
|
524
|
-
{
|
|
525
|
-
"path": resolved.relative_to(project).as_posix(),
|
|
526
|
-
"sha256": _sha256(resolved),
|
|
527
|
-
}
|
|
528
|
-
)
|
|
529
|
-
transitions = _read_ndjson(paths["transitions"])
|
|
530
688
|
previous_hash = transitions[-1].get("receiptHash") if transitions else None
|
|
531
689
|
attempt = (
|
|
532
690
|
1
|
|
@@ -543,7 +701,7 @@ def close_transition(
|
|
|
543
701
|
"fromPhase": from_phase,
|
|
544
702
|
"toPhase": to_phase,
|
|
545
703
|
"status": status,
|
|
546
|
-
"executor":
|
|
704
|
+
"executor": effective_executor,
|
|
547
705
|
"productCommit": _head(project),
|
|
548
706
|
"artifacts": artifact_entries,
|
|
549
707
|
"attempt": attempt,
|
|
@@ -724,6 +882,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
724
882
|
begin.add_argument("--change", required=True)
|
|
725
883
|
begin.add_argument("--phase", required=True)
|
|
726
884
|
begin.add_argument("--executor", required=True)
|
|
885
|
+
configure = sub.add_parser("configure-plan")
|
|
886
|
+
configure.add_argument("--json", action="store_true")
|
|
887
|
+
configure.add_argument("--project", required=True, type=Path)
|
|
888
|
+
configure.add_argument("--change", required=True)
|
|
889
|
+
configure.add_argument("--phases", required=True)
|
|
890
|
+
configure.add_argument("--operator", required=True)
|
|
891
|
+
configure.add_argument("--reason", required=True)
|
|
727
892
|
view = sub.add_parser("view")
|
|
728
893
|
view.add_argument("--json", action="store_true")
|
|
729
894
|
view.add_argument("--project", required=True, type=Path)
|
|
@@ -759,6 +924,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
759
924
|
phase=args.phase,
|
|
760
925
|
executor=args.executor,
|
|
761
926
|
)
|
|
927
|
+
elif args.command == "configure-plan":
|
|
928
|
+
result = configure_phase_plan(
|
|
929
|
+
args.project,
|
|
930
|
+
args.change,
|
|
931
|
+
phases=[item.strip() for item in args.phases.split(",") if item.strip()],
|
|
932
|
+
operator=args.operator,
|
|
933
|
+
reason=args.reason,
|
|
934
|
+
)
|
|
762
935
|
else:
|
|
763
936
|
result = context_view(args.project, args.change)
|
|
764
937
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
@@ -41,7 +41,7 @@ def _project_root_for_change(change_dir: Path) -> Path | None:
|
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def nudge_remote_sync(change_dir: Path) -> None:
|
|
45
45
|
"""Best-effort wake-up after the event append has been flushed to disk."""
|
|
46
46
|
project_root = _project_root_for_change(change_dir)
|
|
47
47
|
if project_root is None:
|
|
@@ -55,6 +55,11 @@ def _nudge_remote_sync(change_dir: Path) -> None:
|
|
|
55
55
|
return
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def _nudge_remote_sync(change_dir: Path) -> None:
|
|
59
|
+
"""Compatibility wrapper for older callers and focused tests."""
|
|
60
|
+
nudge_remote_sync(change_dir)
|
|
61
|
+
|
|
62
|
+
|
|
58
63
|
if hasattr(sys.stdout, "reconfigure"):
|
|
59
64
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
60
65
|
if hasattr(sys.stderr, "reconfigure"):
|
|
@@ -66,6 +71,8 @@ EVENT_TYPES = frozenset(
|
|
|
66
71
|
"phase.start",
|
|
67
72
|
"phase.end",
|
|
68
73
|
"phase.auto_sealed",
|
|
74
|
+
"gate.blocked",
|
|
75
|
+
"gate.recovered",
|
|
69
76
|
"command",
|
|
70
77
|
"verification",
|
|
71
78
|
"artifact",
|
|
@@ -121,6 +128,12 @@ OPTIONAL_FIELDS = (
|
|
|
121
128
|
"renamed_from",
|
|
122
129
|
"renamed_to",
|
|
123
130
|
"change_uuid",
|
|
131
|
+
"execution_mode",
|
|
132
|
+
"decision_reason_code",
|
|
133
|
+
"fallback_reason_code",
|
|
134
|
+
"trigger",
|
|
135
|
+
"from_phase",
|
|
136
|
+
"result_status",
|
|
124
137
|
)
|
|
125
138
|
|
|
126
139
|
_PROVENANCE_FIELDS = frozenset(
|
|
@@ -139,6 +152,12 @@ _PROVENANCE_FIELDS = frozenset(
|
|
|
139
152
|
"orchestration_active_ms",
|
|
140
153
|
"wall_clock_ms",
|
|
141
154
|
"user_wait_ms",
|
|
155
|
+
"execution_mode",
|
|
156
|
+
"decision_reason_code",
|
|
157
|
+
"fallback_reason_code",
|
|
158
|
+
"trigger",
|
|
159
|
+
"from_phase",
|
|
160
|
+
"result_status",
|
|
142
161
|
}
|
|
143
162
|
)
|
|
144
163
|
_EVENT_ALLOWED_FIELDS = {
|
|
@@ -148,6 +167,10 @@ _EVENT_ALLOWED_FIELDS = {
|
|
|
148
167
|
)
|
|
149
168
|
| _PROVENANCE_FIELDS,
|
|
150
169
|
"phase.auto_sealed": frozenset({"status", "reason", "note"}) | _PROVENANCE_FIELDS,
|
|
170
|
+
"gate.blocked": frozenset({"status", "code", "reason", "note"})
|
|
171
|
+
| _PROVENANCE_FIELDS,
|
|
172
|
+
"gate.recovered": frozenset({"status", "code", "reason", "note"})
|
|
173
|
+
| _PROVENANCE_FIELDS,
|
|
151
174
|
"command": frozenset({"command", "exit_code", "duration_ms", "note"})
|
|
152
175
|
| _PROVENANCE_FIELDS,
|
|
153
176
|
"verification": frozenset(
|
|
@@ -1698,6 +1721,22 @@ def append_with_auto_seal(
|
|
|
1698
1721
|
else []
|
|
1699
1722
|
)
|
|
1700
1723
|
)
|
|
1724
|
+
if event_type in {"phase.start", "phase.end", "phase.auto_sealed"} and phase:
|
|
1725
|
+
phase_attempts = split_phase_attempts(
|
|
1726
|
+
[item for item in loaded if item.get("phase") == phase]
|
|
1727
|
+
)
|
|
1728
|
+
latest_attempt = max(
|
|
1729
|
+
(
|
|
1730
|
+
int(item.get("attempt"))
|
|
1731
|
+
for item in phase_attempts
|
|
1732
|
+
if isinstance(item.get("attempt"), int)
|
|
1733
|
+
),
|
|
1734
|
+
default=0,
|
|
1735
|
+
)
|
|
1736
|
+
if not isinstance(event.get("attempt"), int) or int(event["attempt"]) <= 0:
|
|
1737
|
+
event["attempt"] = (
|
|
1738
|
+
latest_attempt + 1 if event_type == "phase.start" else latest_attempt or 1
|
|
1739
|
+
)
|
|
1701
1740
|
if event_type == "phase.start" and phase:
|
|
1702
1741
|
auto_sealed = seal_open_phase_attempts(loaded, phase=phase)
|
|
1703
1742
|
for seal_event in auto_sealed:
|
|
@@ -1765,18 +1804,45 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1765
1804
|
"warning: verification missing --name or --status",
|
|
1766
1805
|
file=sys.stderr,
|
|
1767
1806
|
)
|
|
1768
|
-
line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
1769
1807
|
phase_closed = False
|
|
1770
1808
|
projection_error: str | None = None
|
|
1809
|
+
attempt_error: str | None = None
|
|
1771
1810
|
auto_sealed_events: list[dict[str, Any]] = []
|
|
1772
1811
|
try:
|
|
1773
1812
|
with event_file_lock(lock_path):
|
|
1774
1813
|
existing_events = (
|
|
1775
1814
|
load_events(path)
|
|
1776
1815
|
if args.type in {"phase.end", "correction", "phase.start"}
|
|
1816
|
+
or event.get("run_id") is not None
|
|
1817
|
+
or event.get("attempt") is not None
|
|
1777
1818
|
else []
|
|
1778
1819
|
)
|
|
1779
|
-
|
|
1820
|
+
run_id = event.get("run_id")
|
|
1821
|
+
if run_id is not None:
|
|
1822
|
+
run_attempts = {
|
|
1823
|
+
int(item["attempt"])
|
|
1824
|
+
for item in existing_events
|
|
1825
|
+
if item.get("phase") == event.get("phase")
|
|
1826
|
+
and item.get("run_id") == run_id
|
|
1827
|
+
and isinstance(item.get("attempt"), int)
|
|
1828
|
+
and int(item["attempt"]) > 0
|
|
1829
|
+
}
|
|
1830
|
+
if run_attempts:
|
|
1831
|
+
expected_attempt = max(run_attempts)
|
|
1832
|
+
explicit_attempt = event.get("attempt")
|
|
1833
|
+
if (
|
|
1834
|
+
isinstance(explicit_attempt, int)
|
|
1835
|
+
and explicit_attempt != expected_attempt
|
|
1836
|
+
):
|
|
1837
|
+
attempt_error = (
|
|
1838
|
+
"EVENT_ATTEMPT_CONFLICT: run_id is already bound to "
|
|
1839
|
+
f"attempt {expected_attempt}, received {explicit_attempt}"
|
|
1840
|
+
)
|
|
1841
|
+
else:
|
|
1842
|
+
event["attempt"] = expected_attempt
|
|
1843
|
+
if attempt_error is not None:
|
|
1844
|
+
pass
|
|
1845
|
+
elif args.type == "phase.end":
|
|
1780
1846
|
phase_closed = phase_end_already_recorded(existing_events, event)
|
|
1781
1847
|
elif args.type == "correction":
|
|
1782
1848
|
try:
|
|
@@ -1797,13 +1863,16 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1797
1863
|
phase=args.phase,
|
|
1798
1864
|
seal_reason=inferred_reason,
|
|
1799
1865
|
)
|
|
1800
|
-
if not phase_closed and projection_error is None:
|
|
1866
|
+
if not phase_closed and projection_error is None and attempt_error is None:
|
|
1801
1867
|
for seal_event in auto_sealed_events:
|
|
1802
1868
|
atomic_append_line(
|
|
1803
1869
|
path,
|
|
1804
1870
|
json.dumps(seal_event, ensure_ascii=False, separators=(",", ":")),
|
|
1805
1871
|
)
|
|
1806
|
-
atomic_append_line(
|
|
1872
|
+
atomic_append_line(
|
|
1873
|
+
path,
|
|
1874
|
+
json.dumps(event, ensure_ascii=False, separators=(",", ":")),
|
|
1875
|
+
)
|
|
1807
1876
|
except (OSError, TimeoutError, ValueError) as exc:
|
|
1808
1877
|
return emit_error(f"append failed: {exc}", as_json=as_json)
|
|
1809
1878
|
if phase_closed:
|
|
@@ -1819,6 +1888,12 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1819
1888
|
as_json=as_json,
|
|
1820
1889
|
error_code=error_code,
|
|
1821
1890
|
)
|
|
1891
|
+
if attempt_error is not None:
|
|
1892
|
+
return emit_error(
|
|
1893
|
+
attempt_error,
|
|
1894
|
+
as_json=as_json,
|
|
1895
|
+
error_code="EVENT_ATTEMPT_CONFLICT",
|
|
1896
|
+
)
|
|
1822
1897
|
|
|
1823
1898
|
_nudge_remote_sync(change_dir)
|
|
1824
1899
|
|
|
@@ -1967,6 +2042,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1967
2042
|
p_append.add_argument("--orchestration-active-ms", type=int, default=None)
|
|
1968
2043
|
p_append.add_argument("--wall-clock-ms", type=int, default=None)
|
|
1969
2044
|
p_append.add_argument("--user-wait-ms", type=int, default=None)
|
|
2045
|
+
p_append.add_argument("--execution-mode", default=None)
|
|
2046
|
+
p_append.add_argument("--decision-reason-code", default=None)
|
|
2047
|
+
p_append.add_argument("--fallback-reason-code", default=None)
|
|
2048
|
+
p_append.add_argument("--trigger", default=None)
|
|
2049
|
+
p_append.add_argument("--from-phase", default=None)
|
|
2050
|
+
p_append.add_argument("--result-status", default=None)
|
|
1970
2051
|
p_append.add_argument(
|
|
1971
2052
|
"--legacy-lenient",
|
|
1972
2053
|
action="store_true",
|