@hunter-harness/workflow-harness 0.2.43 → 0.2.44
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/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/claude-code/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/claude-code/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/claude-code/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/claude-code/contracts/service-session.schema.json +147 -0
- 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 +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/claude-code/scripts/harness_change.py +137 -2
- package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/claude-code/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/claude-code/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/claude-code/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/claude-code/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/claude-code/scripts/harness_verification.py +79 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/codebuddy/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/codebuddy/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/codebuddy/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/codebuddy/contracts/service-session.schema.json +147 -0
- 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 +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +137 -2
- package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/codebuddy/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/codebuddy/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/codebuddy/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/codebuddy/scripts/harness_verification.py +79 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/codex/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/codex/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/codex/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/codex/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/codex/contracts/service-session.schema.json +147 -0
- 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 +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 +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/codex/scripts/harness_change.py +137 -2
- package/harness/bundles/general/codex/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/codex/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/codex/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/codex/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/codex/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/codex/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/codex/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/codex/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/codex/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/codex/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/codex/scripts/harness_verification.py +79 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/cursor/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/cursor/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/cursor/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/cursor/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/cursor/contracts/service-session.schema.json +147 -0
- 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 +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 +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/cursor/scripts/harness_change.py +137 -2
- package/harness/bundles/general/cursor/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/cursor/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/cursor/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/cursor/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/cursor/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/cursor/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/cursor/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/cursor/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/cursor/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/claude-code/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/claude-code/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/claude-code/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/claude-code/contracts/service-session.schema.json +147 -0
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/claude-code/scripts/harness_change.py +137 -2
- package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/claude-code/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/claude-code/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/claude-code/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/claude-code/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/claude-code/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/codebuddy/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/codebuddy/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/codebuddy/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/codebuddy/contracts/service-session.schema.json +147 -0
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +137 -2
- package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/codebuddy/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/codebuddy/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/codebuddy/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/codebuddy/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/codex/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/codex/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/codex/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/codex/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/codex/contracts/service-session.schema.json +147 -0
- 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 +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 +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/codex/scripts/harness_change.py +137 -2
- package/harness/bundles/java/codex/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/codex/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/codex/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/codex/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/codex/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/codex/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/codex/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/codex/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/codex/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/codex/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/codex/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/cursor/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/cursor/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/cursor/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/cursor/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/cursor/contracts/service-session.schema.json +147 -0
- 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 +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 +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/cursor/scripts/harness_change.py +137 -2
- package/harness/bundles/java/cursor/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/cursor/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/cursor/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/cursor/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/cursor/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/cursor/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/cursor/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/cursor/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/cursor/scripts/harness_verification.py +79 -0
- package/harness/manifests/general/claude-code.json +58 -26
- package/harness/manifests/general/codebuddy.json +58 -26
- package/harness/manifests/general/codex.json +58 -26
- package/harness/manifests/general/cursor.json +58 -26
- package/harness/manifests/java/claude-code.json +60 -28
- package/harness/manifests/java/codebuddy.json +60 -28
- package/harness/manifests/java/codex.json +60 -28
- package/harness/manifests/java/cursor.json +60 -28
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -27,7 +27,7 @@ import sys
|
|
|
27
27
|
import time
|
|
28
28
|
import uuid
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import Any
|
|
30
|
+
from typing import Any, Callable, Mapping
|
|
31
31
|
|
|
32
32
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
33
33
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
@@ -586,12 +586,14 @@ def _claim_lease_locked(
|
|
|
586
586
|
run_id: str,
|
|
587
587
|
ttl_seconds: int,
|
|
588
588
|
steal: bool = False,
|
|
589
|
+
expected_generation: int | None = None,
|
|
589
590
|
) -> dict[str, Any]:
|
|
590
591
|
path = _lease_path(project_root, change_id)
|
|
591
592
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
592
593
|
now = dt.datetime.now().astimezone()
|
|
593
594
|
expires = now + dt.timedelta(seconds=max(1, ttl_seconds))
|
|
594
595
|
new_lease = {
|
|
596
|
+
"leaseId": str(uuid.uuid4()),
|
|
595
597
|
"changeId": change_id,
|
|
596
598
|
"phase": phase,
|
|
597
599
|
"runId": run_id,
|
|
@@ -599,6 +601,7 @@ def _claim_lease_locked(
|
|
|
599
601
|
"acquiredAt": now.isoformat(timespec="milliseconds"),
|
|
600
602
|
"expiresAt": expires.isoformat(timespec="milliseconds"),
|
|
601
603
|
"ttlSeconds": ttl_seconds,
|
|
604
|
+
"generation": 1,
|
|
602
605
|
}
|
|
603
606
|
if path.is_file():
|
|
604
607
|
try:
|
|
@@ -606,6 +609,12 @@ def _claim_lease_locked(
|
|
|
606
609
|
except (OSError, json.JSONDecodeError):
|
|
607
610
|
existing = None
|
|
608
611
|
if isinstance(existing, dict) and not _lease_expired(existing):
|
|
612
|
+
if expected_generation is not None and existing.get("generation") != expected_generation:
|
|
613
|
+
return {
|
|
614
|
+
"ok": False,
|
|
615
|
+
"code": "LEASE_GENERATION_CONFLICT",
|
|
616
|
+
"holder": existing,
|
|
617
|
+
}
|
|
609
618
|
same_owner = str(existing.get("runId")) == run_id
|
|
610
619
|
if same_owner:
|
|
611
620
|
existing.update(
|
|
@@ -614,6 +623,7 @@ def _claim_lease_locked(
|
|
|
614
623
|
"pid": os.getpid(),
|
|
615
624
|
"expiresAt": expires.isoformat(timespec="milliseconds"),
|
|
616
625
|
"refreshedAt": now.isoformat(timespec="milliseconds"),
|
|
626
|
+
"generation": int(existing.get("generation") or 1) + 1,
|
|
617
627
|
}
|
|
618
628
|
)
|
|
619
629
|
_write_json(path, existing)
|
|
@@ -637,6 +647,7 @@ def claim_lease(
|
|
|
637
647
|
run_id: str,
|
|
638
648
|
ttl_seconds: int,
|
|
639
649
|
steal: bool = False,
|
|
650
|
+
expected_generation: int | None = None,
|
|
640
651
|
) -> dict[str, Any]:
|
|
641
652
|
path = _lease_path(project_root, change_id)
|
|
642
653
|
with _exclusive_file_lock(path.with_suffix(".lock")):
|
|
@@ -647,6 +658,7 @@ def claim_lease(
|
|
|
647
658
|
run_id=run_id,
|
|
648
659
|
ttl_seconds=ttl_seconds,
|
|
649
660
|
steal=steal,
|
|
661
|
+
expected_generation=expected_generation,
|
|
650
662
|
)
|
|
651
663
|
|
|
652
664
|
|
|
@@ -656,6 +668,8 @@ def _release_lease_locked(
|
|
|
656
668
|
change_id: str,
|
|
657
669
|
phase: str,
|
|
658
670
|
run_id: str,
|
|
671
|
+
lease_id: str | None = None,
|
|
672
|
+
generation: int | None = None,
|
|
659
673
|
) -> dict[str, Any]:
|
|
660
674
|
path = _lease_path(project_root, change_id)
|
|
661
675
|
if not path.is_file():
|
|
@@ -688,6 +702,18 @@ def _release_lease_locked(
|
|
|
688
702
|
"message": f"lease phase is {existing.get('phase')}, not {phase}",
|
|
689
703
|
"holder": existing,
|
|
690
704
|
}
|
|
705
|
+
if lease_id is not None and str(existing.get("leaseId")) != lease_id:
|
|
706
|
+
return {
|
|
707
|
+
"ok": False,
|
|
708
|
+
"code": "LEASE_ID_MISMATCH",
|
|
709
|
+
"holder": existing,
|
|
710
|
+
}
|
|
711
|
+
if generation is not None and existing.get("generation") != generation:
|
|
712
|
+
return {
|
|
713
|
+
"ok": False,
|
|
714
|
+
"code": "LEASE_GENERATION_CONFLICT",
|
|
715
|
+
"holder": existing,
|
|
716
|
+
}
|
|
691
717
|
path.unlink(missing_ok=True)
|
|
692
718
|
return {"ok": True, "code": "LEASE_RELEASED", "changeId": change_id, "phase": phase}
|
|
693
719
|
|
|
@@ -698,11 +724,18 @@ def release_lease(
|
|
|
698
724
|
change_id: str,
|
|
699
725
|
phase: str,
|
|
700
726
|
run_id: str,
|
|
727
|
+
lease_id: str | None = None,
|
|
728
|
+
generation: int | None = None,
|
|
701
729
|
) -> dict[str, Any]:
|
|
702
730
|
path = _lease_path(project_root, change_id)
|
|
703
731
|
with _exclusive_file_lock(path.with_suffix(".lock")):
|
|
704
732
|
return _release_lease_locked(
|
|
705
|
-
project_root,
|
|
733
|
+
project_root,
|
|
734
|
+
change_id=change_id,
|
|
735
|
+
phase=phase,
|
|
736
|
+
run_id=run_id,
|
|
737
|
+
lease_id=lease_id,
|
|
738
|
+
generation=generation,
|
|
706
739
|
)
|
|
707
740
|
|
|
708
741
|
|
|
@@ -712,6 +745,8 @@ def lease_port(
|
|
|
712
745
|
change_id: str,
|
|
713
746
|
run_id: str,
|
|
714
747
|
port_range: tuple[int, int],
|
|
748
|
+
generation: int = 1,
|
|
749
|
+
listener_identity: Mapping[str, Any] | None = None,
|
|
715
750
|
) -> dict[str, Any]:
|
|
716
751
|
start, end = port_range
|
|
717
752
|
if start > end:
|
|
@@ -749,6 +784,7 @@ def lease_port(
|
|
|
749
784
|
"leaseId": lease_id,
|
|
750
785
|
"changeId": change_id,
|
|
751
786
|
"runId": run_id,
|
|
787
|
+
"generation": generation,
|
|
752
788
|
"pid": os.getpid(),
|
|
753
789
|
"port": port,
|
|
754
790
|
"acquiredAt": now_iso(),
|
|
@@ -756,6 +792,9 @@ def lease_port(
|
|
|
756
792
|
dt.datetime.now().astimezone() + dt.timedelta(hours=4)
|
|
757
793
|
).isoformat(timespec="milliseconds"),
|
|
758
794
|
}
|
|
795
|
+
if isinstance(listener_identity, Mapping):
|
|
796
|
+
entry["listenerIdentity"] = dict(listener_identity)
|
|
797
|
+
entry["listenerProofRequired"] = True
|
|
759
798
|
leases.append(entry)
|
|
760
799
|
registry["leases"] = leases
|
|
761
800
|
_write_json(registry_path, registry)
|
|
@@ -774,6 +813,9 @@ def release_port(
|
|
|
774
813
|
run_id: str,
|
|
775
814
|
port: int | None = None,
|
|
776
815
|
lease_id: str | None = None,
|
|
816
|
+
generation: int | None = None,
|
|
817
|
+
listener_identity: Mapping[str, Any] | None = None,
|
|
818
|
+
listener_observer: Callable[[int], Mapping[str, Any] | None] | None = None,
|
|
777
819
|
) -> dict[str, Any]:
|
|
778
820
|
"""Release port leases by subset (retro §5.16).
|
|
779
821
|
|
|
@@ -802,6 +844,12 @@ def release_port(
|
|
|
802
844
|
]
|
|
803
845
|
# Validate that the lease belongs to this change
|
|
804
846
|
for item in to_release:
|
|
847
|
+
if generation is not None and item.get("generation") != generation:
|
|
848
|
+
return {
|
|
849
|
+
"ok": False,
|
|
850
|
+
"code": "PORT_LEASE_GENERATION_CONFLICT",
|
|
851
|
+
"holder": item,
|
|
852
|
+
}
|
|
805
853
|
if str(item.get("changeId")) != change_id:
|
|
806
854
|
return {
|
|
807
855
|
"ok": False,
|
|
@@ -818,6 +866,12 @@ def release_port(
|
|
|
818
866
|
]
|
|
819
867
|
# Validate runId matches
|
|
820
868
|
for item in to_release:
|
|
869
|
+
if generation is not None and item.get("generation") != generation:
|
|
870
|
+
return {
|
|
871
|
+
"ok": False,
|
|
872
|
+
"code": "PORT_LEASE_GENERATION_CONFLICT",
|
|
873
|
+
"holder": item,
|
|
874
|
+
}
|
|
821
875
|
if str(item.get("runId")) != run_id:
|
|
822
876
|
return {
|
|
823
877
|
"ok": False,
|
|
@@ -840,6 +894,47 @@ def release_port(
|
|
|
840
894
|
and str(item.get("changeId")) == change_id
|
|
841
895
|
and str(item.get("runId")) == run_id
|
|
842
896
|
]
|
|
897
|
+
if generation is not None:
|
|
898
|
+
mismatched = [
|
|
899
|
+
item for item in to_release if item.get("generation") != generation
|
|
900
|
+
]
|
|
901
|
+
if mismatched:
|
|
902
|
+
return {
|
|
903
|
+
"ok": False,
|
|
904
|
+
"code": "PORT_LEASE_GENERATION_CONFLICT",
|
|
905
|
+
"holder": mismatched[0],
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
for item in to_release:
|
|
909
|
+
expected_listener = item.get("listenerIdentity")
|
|
910
|
+
if not isinstance(expected_listener, Mapping):
|
|
911
|
+
continue
|
|
912
|
+
observed_listener = (
|
|
913
|
+
listener_observer(int(item["port"]))
|
|
914
|
+
if listener_observer is not None and isinstance(item.get("port"), int)
|
|
915
|
+
else listener_identity
|
|
916
|
+
)
|
|
917
|
+
if not isinstance(observed_listener, Mapping):
|
|
918
|
+
return {
|
|
919
|
+
"ok": False,
|
|
920
|
+
"code": "LISTENER_IDENTITY_UNVERIFIABLE",
|
|
921
|
+
"message": "listener identity could not be independently observed",
|
|
922
|
+
"holder": item,
|
|
923
|
+
}
|
|
924
|
+
try:
|
|
925
|
+
from harness_process import verify_process_identity
|
|
926
|
+
|
|
927
|
+
decision = verify_process_identity(expected_listener, observed_listener)
|
|
928
|
+
except (ImportError, TypeError, ValueError):
|
|
929
|
+
decision = {"ok": False, "reasonCode": "IDENTITY_UNVERIFIABLE"}
|
|
930
|
+
if decision.get("ok") is not True:
|
|
931
|
+
return {
|
|
932
|
+
"ok": False,
|
|
933
|
+
"code": "LISTENER_IDENTITY_UNVERIFIABLE",
|
|
934
|
+
"message": "listener identity did not match the leased service",
|
|
935
|
+
"details": decision,
|
|
936
|
+
"holder": item,
|
|
937
|
+
}
|
|
843
938
|
|
|
844
939
|
if not to_release:
|
|
845
940
|
# Check if there are other leases under this changeId (different runId)
|
|
@@ -1011,6 +1106,7 @@ def cmd_claim(args: argparse.Namespace) -> int:
|
|
|
1011
1106
|
run_id=args.run_id,
|
|
1012
1107
|
ttl_seconds=int(args.ttl_seconds),
|
|
1013
1108
|
steal=bool(args.steal),
|
|
1109
|
+
expected_generation=getattr(args, "expected_generation", None),
|
|
1014
1110
|
)
|
|
1015
1111
|
if payload.get("ok"):
|
|
1016
1112
|
emit(payload, as_json=bool(args.json))
|
|
@@ -1030,6 +1126,8 @@ def cmd_release(args: argparse.Namespace) -> int:
|
|
|
1030
1126
|
change_id=args.change,
|
|
1031
1127
|
phase=args.phase,
|
|
1032
1128
|
run_id=args.run_id,
|
|
1129
|
+
lease_id=getattr(args, "lease_id", None),
|
|
1130
|
+
generation=getattr(args, "generation", None),
|
|
1033
1131
|
)
|
|
1034
1132
|
if payload.get("ok"):
|
|
1035
1133
|
emit(payload, as_json=bool(args.json))
|
|
@@ -1051,11 +1149,26 @@ def cmd_lease_port(args: argparse.Namespace) -> int:
|
|
|
1051
1149
|
f"expected --range <start-end>, got {args.range!r}",
|
|
1052
1150
|
as_json=bool(args.json),
|
|
1053
1151
|
)
|
|
1152
|
+
listener_identity = None
|
|
1153
|
+
if getattr(args, "listener_identity_json", None):
|
|
1154
|
+
try:
|
|
1155
|
+
parsed_listener = json.loads(args.listener_identity_json)
|
|
1156
|
+
if not isinstance(parsed_listener, dict):
|
|
1157
|
+
raise ValueError("listener identity must be an object")
|
|
1158
|
+
listener_identity = parsed_listener
|
|
1159
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1160
|
+
return emit_error(
|
|
1161
|
+
"LISTENER_IDENTITY_UNVERIFIABLE",
|
|
1162
|
+
str(exc),
|
|
1163
|
+
as_json=bool(args.json),
|
|
1164
|
+
)
|
|
1054
1165
|
payload = lease_port(
|
|
1055
1166
|
project,
|
|
1056
1167
|
change_id=args.change,
|
|
1057
1168
|
run_id=args.run_id,
|
|
1058
1169
|
port_range=parsed,
|
|
1170
|
+
generation=int(getattr(args, "generation", 1)),
|
|
1171
|
+
listener_identity=listener_identity,
|
|
1059
1172
|
)
|
|
1060
1173
|
if payload.get("ok"):
|
|
1061
1174
|
emit(payload, as_json=bool(args.json))
|
|
@@ -1070,12 +1183,27 @@ def cmd_lease_port(args: argparse.Namespace) -> int:
|
|
|
1070
1183
|
|
|
1071
1184
|
def cmd_release_port(args: argparse.Namespace) -> int:
|
|
1072
1185
|
project = resolve_main_project_root()
|
|
1186
|
+
listener_identity = None
|
|
1187
|
+
if getattr(args, "listener_identity_json", None):
|
|
1188
|
+
try:
|
|
1189
|
+
parsed_listener = json.loads(args.listener_identity_json)
|
|
1190
|
+
if not isinstance(parsed_listener, dict):
|
|
1191
|
+
raise ValueError("listener identity must be an object")
|
|
1192
|
+
listener_identity = parsed_listener
|
|
1193
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1194
|
+
return emit_error(
|
|
1195
|
+
"LISTENER_IDENTITY_UNVERIFIABLE",
|
|
1196
|
+
str(exc),
|
|
1197
|
+
as_json=bool(args.json),
|
|
1198
|
+
)
|
|
1073
1199
|
payload = release_port(
|
|
1074
1200
|
project,
|
|
1075
1201
|
change_id=args.change,
|
|
1076
1202
|
run_id=args.run_id,
|
|
1077
1203
|
port=getattr(args, "port", None),
|
|
1078
1204
|
lease_id=getattr(args, "lease_id", None),
|
|
1205
|
+
generation=getattr(args, "generation", None),
|
|
1206
|
+
listener_identity=listener_identity,
|
|
1079
1207
|
)
|
|
1080
1208
|
if payload.get("ok"):
|
|
1081
1209
|
emit(payload, as_json=bool(args.json))
|
|
@@ -1329,18 +1457,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1329
1457
|
p_claim.add_argument("--run-id", required=True)
|
|
1330
1458
|
p_claim.add_argument("--ttl-seconds", type=int, default=3600)
|
|
1331
1459
|
p_claim.add_argument("--steal", action="store_true")
|
|
1460
|
+
p_claim.add_argument("--expected-generation", type=int, default=None)
|
|
1332
1461
|
p_claim.set_defaults(func=cmd_claim)
|
|
1333
1462
|
|
|
1334
1463
|
p_release = sub.add_parser("release", parents=[shared])
|
|
1335
1464
|
p_release.add_argument("--change", required=True)
|
|
1336
1465
|
p_release.add_argument("--phase", required=True)
|
|
1337
1466
|
p_release.add_argument("--run-id", required=True)
|
|
1467
|
+
p_release.add_argument("--lease-id", default=None)
|
|
1468
|
+
p_release.add_argument("--generation", type=int, default=None)
|
|
1338
1469
|
p_release.set_defaults(func=cmd_release)
|
|
1339
1470
|
|
|
1340
1471
|
p_port = sub.add_parser("lease-port", parents=[shared])
|
|
1341
1472
|
p_port.add_argument("--change", required=True)
|
|
1342
1473
|
p_port.add_argument("--run-id", required=True)
|
|
1343
1474
|
p_port.add_argument("--range", required=True)
|
|
1475
|
+
p_port.add_argument("--generation", type=int, default=1)
|
|
1476
|
+
p_port.add_argument("--listener-identity-json", default=None)
|
|
1344
1477
|
p_port.set_defaults(func=cmd_lease_port)
|
|
1345
1478
|
|
|
1346
1479
|
p_port_release = sub.add_parser("release-port", parents=[shared])
|
|
@@ -1348,6 +1481,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1348
1481
|
p_port_release.add_argument("--run-id", required=True)
|
|
1349
1482
|
p_port_release.add_argument("--port", type=int, default=None)
|
|
1350
1483
|
p_port_release.add_argument("--lease-id", default=None)
|
|
1484
|
+
p_port_release.add_argument("--generation", type=int, default=None)
|
|
1485
|
+
p_port_release.add_argument("--listener-identity-json", default=None)
|
|
1351
1486
|
p_port_release.set_defaults(func=cmd_release_port)
|
|
1352
1487
|
|
|
1353
1488
|
p_lock = sub.add_parser("integration-lock", parents=[shared])
|
|
@@ -30,6 +30,131 @@ def _integer(value: Any) -> int:
|
|
|
30
30
|
return 0
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _timestamp(value: Any) -> dt.datetime | None:
|
|
34
|
+
if isinstance(value, dt.datetime):
|
|
35
|
+
parsed = value
|
|
36
|
+
elif value:
|
|
37
|
+
try:
|
|
38
|
+
parsed = dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
39
|
+
except (TypeError, ValueError):
|
|
40
|
+
return None
|
|
41
|
+
else:
|
|
42
|
+
return None
|
|
43
|
+
if parsed.tzinfo is None:
|
|
44
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
45
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _now_timestamp(value: Any = None) -> dt.datetime:
|
|
49
|
+
return _timestamp(value) or dt.datetime.now(dt.timezone.utc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _seconds(value: Any) -> float | None:
|
|
53
|
+
try:
|
|
54
|
+
parsed = float(value)
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
return None
|
|
57
|
+
return parsed if parsed > 0 else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def classify_progress(
|
|
61
|
+
session: dict[str, Any],
|
|
62
|
+
*,
|
|
63
|
+
now: Any = None,
|
|
64
|
+
historical_durations: list[int | float] | None = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""Classify observable run state without changing execution policy.
|
|
67
|
+
|
|
68
|
+
The classifier is intentionally conservative: incomplete clock/heartbeat
|
|
69
|
+
data produces explicit ``INCOMPLETE``/``NO_BUDGET`` facts instead of an
|
|
70
|
+
invented ETA or a guessed failure cause.
|
|
71
|
+
"""
|
|
72
|
+
status = str(session.get("status") or "RUNNING").upper()
|
|
73
|
+
terminal = {
|
|
74
|
+
"OK": "COMPLETED",
|
|
75
|
+
"PASS": "COMPLETED",
|
|
76
|
+
"COMPLETED": "COMPLETED",
|
|
77
|
+
"FAIL": "FAILED",
|
|
78
|
+
"FAILED": "FAILED",
|
|
79
|
+
"CANCELLED": "CANCELLED",
|
|
80
|
+
"CANCELED": "CANCELLED",
|
|
81
|
+
"INCOMPLETE": "INCOMPLETE",
|
|
82
|
+
}
|
|
83
|
+
started = _timestamp(session.get("startedAt") or session.get("createdAt"))
|
|
84
|
+
current = _now_timestamp(now)
|
|
85
|
+
elapsed_seconds: float | None = None
|
|
86
|
+
if started is not None:
|
|
87
|
+
elapsed_seconds = max(0.0, (current - started).total_seconds())
|
|
88
|
+
|
|
89
|
+
timeout_seconds = _seconds(session.get("timeoutSeconds"))
|
|
90
|
+
expected_seconds = _seconds(
|
|
91
|
+
session.get("expectedDurationSeconds")
|
|
92
|
+
or session.get("estimatedDurationSeconds")
|
|
93
|
+
)
|
|
94
|
+
budget_seconds = _seconds(
|
|
95
|
+
session.get("budgetSeconds")
|
|
96
|
+
or session.get("maxDurationSeconds")
|
|
97
|
+
or expected_seconds
|
|
98
|
+
or timeout_seconds
|
|
99
|
+
)
|
|
100
|
+
if elapsed_seconds is None or budget_seconds is None:
|
|
101
|
+
budget_state = "NO_BUDGET"
|
|
102
|
+
elif elapsed_seconds >= budget_seconds:
|
|
103
|
+
budget_state = "OVER_BUDGET"
|
|
104
|
+
else:
|
|
105
|
+
budget_state = "WITHIN_BUDGET"
|
|
106
|
+
|
|
107
|
+
progress_state = terminal.get(status)
|
|
108
|
+
diagnostic_required = False
|
|
109
|
+
heartbeat_grace = _seconds(
|
|
110
|
+
session.get("heartbeatGraceSeconds")
|
|
111
|
+
or session.get("heartbeatTimeoutSeconds")
|
|
112
|
+
or 60
|
|
113
|
+
) or 60
|
|
114
|
+
heartbeat = _timestamp(session.get("lastHeartbeatAt"))
|
|
115
|
+
output = _timestamp(
|
|
116
|
+
session.get("lastOutputAt") or session.get("lastLogAt")
|
|
117
|
+
)
|
|
118
|
+
heartbeat_gap = (
|
|
119
|
+
max(0.0, (current - heartbeat).total_seconds())
|
|
120
|
+
if heartbeat is not None
|
|
121
|
+
else None
|
|
122
|
+
)
|
|
123
|
+
output_gap = (
|
|
124
|
+
max(0.0, (current - output).total_seconds()) if output is not None else None
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if progress_state is None:
|
|
128
|
+
progress_state = "RUNNING"
|
|
129
|
+
if timeout_seconds is not None and elapsed_seconds is not None and elapsed_seconds >= timeout_seconds:
|
|
130
|
+
progress_state = "TIMEOUT"
|
|
131
|
+
elif output_gap is not None and output_gap > heartbeat_grace:
|
|
132
|
+
progress_state = "NO_OUTPUT_PROCESS_ACTIVE"
|
|
133
|
+
elif heartbeat_gap is None or heartbeat_gap > heartbeat_grace:
|
|
134
|
+
progress_state = "HEARTBEAT_LOST"
|
|
135
|
+
elif _integer(session.get("resourceWaitMs")) > 0 or session.get("resourceWaitActive") is True:
|
|
136
|
+
progress_state = "RESOURCE_WAIT"
|
|
137
|
+
elif expected_seconds is not None and elapsed_seconds is not None and elapsed_seconds > expected_seconds:
|
|
138
|
+
progress_state = "SLOW_PROGRESSING"
|
|
139
|
+
diagnostic_required = progress_state in {
|
|
140
|
+
"TIMEOUT",
|
|
141
|
+
"HEARTBEAT_LOST",
|
|
142
|
+
"NO_OUTPUT_PROCESS_ACTIVE",
|
|
143
|
+
} or budget_state == "OVER_BUDGET"
|
|
144
|
+
elif progress_state not in {"COMPLETED", "FAILED", "CANCELLED"}:
|
|
145
|
+
diagnostic_required = True
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"progressState": progress_state,
|
|
149
|
+
"budgetState": budget_state,
|
|
150
|
+
"diagnosticRequired": diagnostic_required,
|
|
151
|
+
"elapsedSeconds": int(elapsed_seconds) if elapsed_seconds is not None else None,
|
|
152
|
+
"budgetSeconds": budget_seconds,
|
|
153
|
+
"heartbeatGapSeconds": int(heartbeat_gap) if heartbeat_gap is not None else None,
|
|
154
|
+
"outputGapSeconds": int(output_gap) if output_gap is not None else None,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
33
158
|
def _failure_class(session: dict[str, Any]) -> str | None:
|
|
34
159
|
status = str(session.get("status") or "").upper()
|
|
35
160
|
reason = str(session.get("reasonCode") or "").upper()
|
|
@@ -128,10 +253,27 @@ def build_efficiency_summary(
|
|
|
128
253
|
invalidations: list[dict[str, Any]],
|
|
129
254
|
) -> dict[str, Any]:
|
|
130
255
|
failure_counts = {key: 0 for key in FAILURE_CLASSES}
|
|
256
|
+
progress_states: Counter[str] = Counter()
|
|
257
|
+
budget_states: Counter[str] = Counter()
|
|
258
|
+
slow_stages: Counter[str] = Counter()
|
|
131
259
|
for session in run_sessions:
|
|
132
260
|
classification = _failure_class(session)
|
|
133
261
|
if classification is not None:
|
|
134
262
|
failure_counts[classification] += 1
|
|
263
|
+
progress = str(
|
|
264
|
+
session.get("progressState")
|
|
265
|
+
or classify_progress(session).get("progressState")
|
|
266
|
+
or "INCOMPLETE"
|
|
267
|
+
).upper()
|
|
268
|
+
budget = str(
|
|
269
|
+
session.get("budgetState")
|
|
270
|
+
or classify_progress(session).get("budgetState")
|
|
271
|
+
or "NO_BUDGET"
|
|
272
|
+
).upper()
|
|
273
|
+
progress_states[progress] += 1
|
|
274
|
+
budget_states[budget] += 1
|
|
275
|
+
if progress == "SLOW_PROGRESSING":
|
|
276
|
+
slow_stages[str(session.get("stage") or session.get("verification") or "unknown")] += 1
|
|
135
277
|
environment_actions: Counter[str] = Counter()
|
|
136
278
|
for item in environment_receipts:
|
|
137
279
|
action = str(item.get("action") or "").lower()
|
|
@@ -183,6 +325,9 @@ def build_efficiency_summary(
|
|
|
183
325
|
for item in run_sessions
|
|
184
326
|
),
|
|
185
327
|
"failureClasses": failure_counts,
|
|
328
|
+
"progressStates": dict(sorted(progress_states.items())),
|
|
329
|
+
"budgetStates": dict(sorted(budget_states.items())),
|
|
330
|
+
"slowStages": dict(sorted(slow_stages.items())),
|
|
186
331
|
"invalidationReasons": _counts(
|
|
187
332
|
str(item.get("reasonCode") or "UNKNOWN")
|
|
188
333
|
for item in invalidations
|
|
@@ -226,6 +371,10 @@ def compact_progress_view(
|
|
|
226
371
|
"typicalSeconds": int(median(durations)),
|
|
227
372
|
"highSeconds": durations[-1],
|
|
228
373
|
}
|
|
374
|
+
classification = classify_progress(
|
|
375
|
+
session,
|
|
376
|
+
historical_durations=historical_durations,
|
|
377
|
+
)
|
|
229
378
|
return {
|
|
230
379
|
"verification": session.get("verification"),
|
|
231
380
|
"stage": session.get("stage"),
|
|
@@ -235,6 +384,10 @@ def compact_progress_view(
|
|
|
235
384
|
"eta": eta,
|
|
236
385
|
"resourceWait": list(session.get("resourceLocks") or []),
|
|
237
386
|
"status": session.get("status", "RUNNING"),
|
|
387
|
+
"progressState": classification["progressState"],
|
|
388
|
+
"budgetState": classification["budgetState"],
|
|
389
|
+
"diagnosticRequired": classification["diagnosticRequired"],
|
|
390
|
+
"elapsedSeconds": classification["elapsedSeconds"],
|
|
238
391
|
}
|
|
239
392
|
|
|
240
393
|
|
|
@@ -23,10 +23,31 @@ from contextlib import contextmanager
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any, Iterator
|
|
25
25
|
|
|
26
|
-
import
|
|
26
|
+
import harness_process as hprocess
|
|
27
27
|
from harness_paths import resolve_state_dir_for_contract
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _verify_recorded_owner(owner: dict[str, Any]) -> bool | None:
|
|
31
|
+
try:
|
|
32
|
+
pid = int(owner.get("pid") or 0)
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
return None
|
|
35
|
+
observed = hprocess.observe_process_identity(pid)
|
|
36
|
+
decision = hprocess.verify_process_identity(
|
|
37
|
+
{
|
|
38
|
+
"pid": pid,
|
|
39
|
+
"createdAt": owner.get("startedAt"),
|
|
40
|
+
"executable": owner.get("executable"),
|
|
41
|
+
},
|
|
42
|
+
observed,
|
|
43
|
+
)
|
|
44
|
+
if decision.get("ok") is True:
|
|
45
|
+
return True
|
|
46
|
+
if decision.get("reasonCode") == "IDENTITY_UNVERIFIABLE":
|
|
47
|
+
return None
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
30
51
|
if hasattr(sys.stdout, "reconfigure"):
|
|
31
52
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
32
53
|
if hasattr(sys.stderr, "reconfigure"):
|
|
@@ -358,7 +379,7 @@ def _lease_registry_guard(lease_root: Path) -> Iterator[None]:
|
|
|
358
379
|
except (OSError, json.JSONDecodeError):
|
|
359
380
|
holder = {}
|
|
360
381
|
pid = holder.get("pid") if isinstance(holder, dict) else None
|
|
361
|
-
if isinstance(pid, int) and pid > 0 and not
|
|
382
|
+
if isinstance(pid, int) and pid > 0 and not hprocess.is_pid_alive(pid):
|
|
362
383
|
try:
|
|
363
384
|
lock_path.unlink()
|
|
364
385
|
continue
|
|
@@ -444,22 +465,40 @@ def execute_provider_operation(
|
|
|
444
465
|
raise ValueError("ENVIRONMENT_PROVIDER_LEASE_INVALID")
|
|
445
466
|
executable = shutil.which(argv[0]) or str(Path(argv[0]).expanduser().resolve())
|
|
446
467
|
started_at = now_iso()
|
|
447
|
-
|
|
468
|
+
spawned = hprocess.spawn_structured_argv(
|
|
448
469
|
argv,
|
|
449
|
-
cwd=
|
|
450
|
-
|
|
470
|
+
cwd=project.resolve(),
|
|
471
|
+
environment={},
|
|
472
|
+
owner_token=f"environment:{change_id}:{stack_id}:{uuid.uuid4().hex}",
|
|
451
473
|
stdout=subprocess.PIPE,
|
|
452
474
|
stderr=subprocess.PIPE,
|
|
453
|
-
shell=False,
|
|
454
475
|
)
|
|
455
|
-
|
|
476
|
+
process = spawned.process
|
|
477
|
+
created = hprocess.get_process_create_time(process.pid)
|
|
478
|
+
cleanup_complete = False
|
|
456
479
|
try:
|
|
457
480
|
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
|
458
481
|
timed_out = False
|
|
459
482
|
except subprocess.TimeoutExpired:
|
|
460
483
|
timed_out = True
|
|
461
|
-
|
|
484
|
+
hprocess.capture_owned_members(spawned)
|
|
485
|
+
cleanup = hprocess.terminate_owned_tree(
|
|
486
|
+
spawned.attestation,
|
|
487
|
+
spawned.ownershipProof,
|
|
488
|
+
{"graceSeconds": 3.0},
|
|
489
|
+
)
|
|
490
|
+
cleanup_complete = bool(cleanup.get("cleanupComplete"))
|
|
462
491
|
stdout, stderr = process.communicate()
|
|
492
|
+
finally:
|
|
493
|
+
if process.poll() is None:
|
|
494
|
+
hprocess.capture_owned_members(spawned)
|
|
495
|
+
cleanup = hprocess.terminate_owned_tree(
|
|
496
|
+
spawned.attestation,
|
|
497
|
+
spawned.ownershipProof,
|
|
498
|
+
{"graceSeconds": 3.0},
|
|
499
|
+
)
|
|
500
|
+
cleanup_complete = bool(cleanup.get("cleanupComplete"))
|
|
501
|
+
spawned.close()
|
|
463
502
|
operation_id = uuid.uuid4().hex
|
|
464
503
|
receipt_path = _provider_operation_root(root) / f"{operation}-{operation_id}.json"
|
|
465
504
|
receipt: dict[str, Any] = {
|
|
@@ -490,6 +529,9 @@ def execute_provider_operation(
|
|
|
490
529
|
"utf-8"
|
|
491
530
|
)
|
|
492
531
|
),
|
|
532
|
+
"processAttestation": spawned.attestation,
|
|
533
|
+
"ownershipProof": spawned.ownershipProof,
|
|
534
|
+
"cleanupComplete": cleanup_complete or process.poll() is not None,
|
|
493
535
|
},
|
|
494
536
|
"exitCode": process.returncode,
|
|
495
537
|
"timedOut": timed_out,
|
|
@@ -716,7 +758,7 @@ def classify_leases(lease_root: Path) -> list[dict[str, Any]]:
|
|
|
716
758
|
item["classification"] = "OWNER_IDENTITY_INCOMPLETE"
|
|
717
759
|
results.append(item)
|
|
718
760
|
continue
|
|
719
|
-
if not
|
|
761
|
+
if not hprocess.is_pid_alive(owner_pid):
|
|
720
762
|
item["classification"] = "RECLAIMABLE"
|
|
721
763
|
results.append(item)
|
|
722
764
|
continue
|
|
@@ -728,13 +770,7 @@ def classify_leases(lease_root: Path) -> list[dict[str, Any]]:
|
|
|
728
770
|
)
|
|
729
771
|
results.append(item)
|
|
730
772
|
continue
|
|
731
|
-
identity =
|
|
732
|
-
{
|
|
733
|
-
"pid": owner_pid,
|
|
734
|
-
"startedAt": owner["startedAt"],
|
|
735
|
-
"processIdentity": {"executable": owner["executable"]},
|
|
736
|
-
}
|
|
737
|
-
)
|
|
773
|
+
identity = _verify_recorded_owner(owner)
|
|
738
774
|
item["classification"] = (
|
|
739
775
|
"OWNER_ACTIVE"
|
|
740
776
|
if identity is True
|
|
@@ -1021,13 +1057,7 @@ def _acquire_lease_locked(
|
|
|
1021
1057
|
owner_pid = int(owner.get("pid"))
|
|
1022
1058
|
except (TypeError, ValueError):
|
|
1023
1059
|
owner_pid = 0
|
|
1024
|
-
owner_verified =
|
|
1025
|
-
{
|
|
1026
|
-
"pid": owner_pid,
|
|
1027
|
-
"startedAt": owner.get("startedAt"),
|
|
1028
|
-
"processIdentity": {"executable": owner.get("executable")},
|
|
1029
|
-
}
|
|
1030
|
-
) is True
|
|
1060
|
+
owner_verified = _verify_recorded_owner(owner) is True
|
|
1031
1061
|
expires_at = _parse_expiry(current.get("expiresAt"))
|
|
1032
1062
|
unexpired = (
|
|
1033
1063
|
expires_at is not None
|
|
@@ -1106,10 +1136,10 @@ def _acquire_lease_locked(
|
|
|
1106
1136
|
"resetCount": reset_count + (1 if is_reset else 0),
|
|
1107
1137
|
"owner": {
|
|
1108
1138
|
"pid": os.getpid(),
|
|
1109
|
-
"executable":
|
|
1139
|
+
"executable": hprocess.get_process_executable(os.getpid())
|
|
1110
1140
|
or str(Path(sys.executable).resolve()),
|
|
1111
1141
|
"startedAt": (
|
|
1112
|
-
|
|
1142
|
+
hprocess.get_process_create_time(os.getpid())
|
|
1113
1143
|
or started
|
|
1114
1144
|
).isoformat(timespec="seconds"),
|
|
1115
1145
|
},
|