agent-lifecycle-kit 1.50.1__py3-none-any.whl
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.
- agent_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Workflow state loading, validation and atomic persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agent_lifecycle.contracts import LifecycleError, read_json_object
|
|
12
|
+
from agent_lifecycle.contracts.canonical import canonical_bytes, canonical_digest
|
|
13
|
+
|
|
14
|
+
TERMINAL_PHASES = {"COMPLETE", "FAILED", "CANCELLED"}
|
|
15
|
+
EXECUTION_PHASES = {"RUNNING", "STEP_REVIEW", "REMEDIATING"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def now_iso() -> str:
|
|
19
|
+
return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_state(path: Path) -> dict[str, Any]:
|
|
23
|
+
state = read_json_object(path, label="workflow state")
|
|
24
|
+
if state.get("schemaVersion") != "agent-workflow-state.v3":
|
|
25
|
+
raise LifecycleError("unsupported-workflow-state", "workflow state schemaVersion is unsupported")
|
|
26
|
+
if not isinstance(state.get("stateRevision"), int) or state["stateRevision"] < 1:
|
|
27
|
+
raise LifecycleError("invalid-workflow-state", "stateRevision must be a positive integer")
|
|
28
|
+
if not isinstance(state.get("phase"), str) or not state["phase"]:
|
|
29
|
+
raise LifecycleError("invalid-workflow-state", "phase is required")
|
|
30
|
+
if not isinstance(state.get("tasks"), list):
|
|
31
|
+
raise LifecycleError("invalid-workflow-state", "tasks must be an array")
|
|
32
|
+
return state
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def state_identity(path: Path, state: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
data = canonical_bytes(state) + b"\n"
|
|
37
|
+
return {
|
|
38
|
+
"path": path.as_posix(),
|
|
39
|
+
"sha256": canonical_digest(state),
|
|
40
|
+
"bytes": len(data),
|
|
41
|
+
"stateRevision": state["stateRevision"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def require_expected_revision(state: dict[str, Any], expected_revision: int) -> None:
|
|
46
|
+
if state["stateRevision"] != expected_revision:
|
|
47
|
+
raise LifecycleError(
|
|
48
|
+
"state-revision-mismatch",
|
|
49
|
+
"workflow state revision mismatch",
|
|
50
|
+
{"expected": expected_revision, "actual": state["stateRevision"]},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def require_operation_unused(state: dict[str, Any], operation_id: str) -> None:
|
|
55
|
+
ledger = state.setdefault("operationLedger", {})
|
|
56
|
+
if not isinstance(ledger, dict):
|
|
57
|
+
raise LifecycleError("invalid-workflow-state", "operationLedger must be an object")
|
|
58
|
+
if operation_id in ledger:
|
|
59
|
+
raise LifecycleError("duplicate-operation", f"operation already recorded: {operation_id}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def record_operation(state: dict[str, Any], *, operation_id: str, event_type: str) -> None:
|
|
63
|
+
ledger = state.setdefault("operationLedger", {})
|
|
64
|
+
if not isinstance(ledger, dict):
|
|
65
|
+
raise LifecycleError("invalid-workflow-state", "operationLedger must be an object")
|
|
66
|
+
ledger[operation_id] = {
|
|
67
|
+
"stateRevision": state["stateRevision"],
|
|
68
|
+
"eventType": event_type,
|
|
69
|
+
"recordedAt": now_iso(),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def deadline_after(started_at: str, seconds: int) -> str:
|
|
74
|
+
try:
|
|
75
|
+
start = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
raise LifecycleError("invalid-timestamp", f"invalid timestamp: {started_at}") from exc
|
|
78
|
+
return (start + timedelta(seconds=seconds)).isoformat().replace("+00:00", "Z")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_state_replace(path: Path, state: dict[str, Any]) -> None:
|
|
82
|
+
data = canonical_bytes(state) + b"\n"
|
|
83
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
84
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
85
|
+
try:
|
|
86
|
+
with os.fdopen(os.open(tmp, flags, 0o644), "wb") as handle:
|
|
87
|
+
handle.write(data)
|
|
88
|
+
handle.flush()
|
|
89
|
+
os.fsync(handle.fileno())
|
|
90
|
+
os.replace(tmp, path)
|
|
91
|
+
_fsync_dir(path.parent)
|
|
92
|
+
finally:
|
|
93
|
+
if tmp.exists():
|
|
94
|
+
tmp.unlink()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def summarize_state(path: Path, state: dict[str, Any]) -> dict[str, Any]:
|
|
98
|
+
tasks = [
|
|
99
|
+
{
|
|
100
|
+
"id": task.get("id"),
|
|
101
|
+
"status": task.get("status"),
|
|
102
|
+
"attempt": task.get("attempt"),
|
|
103
|
+
"dependsOn": task.get("dependsOn", []),
|
|
104
|
+
"required": task.get("required", True),
|
|
105
|
+
}
|
|
106
|
+
for task in state["tasks"]
|
|
107
|
+
]
|
|
108
|
+
return {
|
|
109
|
+
"schemaVersion": "agent-workflow-status.v1",
|
|
110
|
+
"identity": state_identity(path, state),
|
|
111
|
+
"runId": state.get("runId"),
|
|
112
|
+
"packageId": state.get("packageId"),
|
|
113
|
+
"planRevision": state.get("planRevision"),
|
|
114
|
+
"planDigest": state.get("planDigest"),
|
|
115
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
116
|
+
"phase": state["phase"],
|
|
117
|
+
"blocker": state.get("blocker"),
|
|
118
|
+
"authorization": state.get("authorization"),
|
|
119
|
+
"tasks": tasks,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _fsync_dir(path: Path) -> None:
|
|
124
|
+
if os.name == "nt":
|
|
125
|
+
return
|
|
126
|
+
fd = os.open(path, os.O_RDONLY)
|
|
127
|
+
try:
|
|
128
|
+
os.fsync(fd)
|
|
129
|
+
finally:
|
|
130
|
+
os.close(fd)
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Task-level workflow transitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_lifecycle.contracts import LifecycleError, read_json_object
|
|
9
|
+
from agent_lifecycle.contracts.paths import is_under_repo_path, normalize_repo_path
|
|
10
|
+
from agent_lifecycle.workflow.artifacts import (
|
|
11
|
+
artifact_identity,
|
|
12
|
+
artifact_path,
|
|
13
|
+
next_available_attempt,
|
|
14
|
+
package_root,
|
|
15
|
+
)
|
|
16
|
+
from agent_lifecycle.workflow.gates import record_gate_receipts, validate_controller_gates
|
|
17
|
+
from agent_lifecycle.workflow.implementation_audit_gate import (
|
|
18
|
+
task_implementation_audit_required,
|
|
19
|
+
validate_task_implementation_audit_artifact,
|
|
20
|
+
)
|
|
21
|
+
from agent_lifecycle.workflow.model_usage import (
|
|
22
|
+
model_usage_receipt_required,
|
|
23
|
+
validate_attempt_model_route,
|
|
24
|
+
validate_task_model_usage_receipt,
|
|
25
|
+
)
|
|
26
|
+
from agent_lifecycle.workflow.operation_kernel import commit_state, load_for_update
|
|
27
|
+
from agent_lifecycle.workflow.query import status
|
|
28
|
+
from agent_lifecycle.workflow.reviews import validate_task_result, validate_task_review
|
|
29
|
+
from agent_lifecycle.workflow.selectors import find_task, ready_tasks, unlock_ready_tasks
|
|
30
|
+
from agent_lifecycle.workflow.state import (
|
|
31
|
+
deadline_after,
|
|
32
|
+
now_iso,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def start_task(
|
|
37
|
+
state_path: Path,
|
|
38
|
+
*,
|
|
39
|
+
task_id: str,
|
|
40
|
+
operation_id: str,
|
|
41
|
+
expected_revision: int,
|
|
42
|
+
source_revision: str,
|
|
43
|
+
reason: str,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
state = _mutable_state(state_path, operation_id, expected_revision)
|
|
46
|
+
if state["phase"] not in {"RUNNING", "REMEDIATING"}:
|
|
47
|
+
raise LifecycleError("invalid-phase", f"cannot start task from phase {state['phase']}")
|
|
48
|
+
_require_source_and_authorization(state, source_revision)
|
|
49
|
+
task = find_task(state, task_id)
|
|
50
|
+
if task.get("status") not in {"READY", "REWORK"}:
|
|
51
|
+
raise LifecycleError("invalid-task-status", f"task {task_id} is not launchable")
|
|
52
|
+
_require_dependencies_accepted(state, task)
|
|
53
|
+
_require_parallel_capacity(state)
|
|
54
|
+
attempt = next_available_attempt(state_path, state, task)
|
|
55
|
+
gate_receipts = validate_controller_gates(
|
|
56
|
+
state_path,
|
|
57
|
+
state,
|
|
58
|
+
task,
|
|
59
|
+
phase="pre-launch",
|
|
60
|
+
operation_id=operation_id,
|
|
61
|
+
attempt=attempt,
|
|
62
|
+
)
|
|
63
|
+
_mark_task_running(state, task, attempt, reason)
|
|
64
|
+
record_gate_receipts(task, gate_receipts)
|
|
65
|
+
commit_state(
|
|
66
|
+
state_path,
|
|
67
|
+
state,
|
|
68
|
+
operation_id=operation_id,
|
|
69
|
+
event_type="task-started",
|
|
70
|
+
payload={"taskId": task_id, "attempt": attempt, "reason": reason},
|
|
71
|
+
)
|
|
72
|
+
return status(state_path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def commit_task_result(
|
|
76
|
+
state_path: Path,
|
|
77
|
+
*,
|
|
78
|
+
task_id: str,
|
|
79
|
+
operation_id: str,
|
|
80
|
+
expected_revision: int,
|
|
81
|
+
source_revision: str,
|
|
82
|
+
result_path: str,
|
|
83
|
+
model_usage_receipt_path: str | None = None,
|
|
84
|
+
budget_targets_path: str | None = None,
|
|
85
|
+
reason: str,
|
|
86
|
+
) -> dict[str, Any]:
|
|
87
|
+
state = _mutable_state(state_path, operation_id, expected_revision)
|
|
88
|
+
if state["phase"] != "RUNNING":
|
|
89
|
+
raise LifecycleError("invalid-phase", "task result requires RUNNING phase")
|
|
90
|
+
if state.get("sourceRevision") != source_revision:
|
|
91
|
+
raise LifecycleError("source-revision-mismatch", "source revision mismatch")
|
|
92
|
+
task = find_task(state, task_id)
|
|
93
|
+
if task.get("status") != "RUNNING":
|
|
94
|
+
raise LifecycleError("invalid-task-status", f"task {task_id} is not RUNNING")
|
|
95
|
+
gate_receipts = validate_controller_gates(
|
|
96
|
+
state_path,
|
|
97
|
+
state,
|
|
98
|
+
task,
|
|
99
|
+
phase="post-attempt",
|
|
100
|
+
operation_id=operation_id,
|
|
101
|
+
attempt=int(task["attempt"]),
|
|
102
|
+
)
|
|
103
|
+
expected_path = artifact_path(task, "result", int(task["attempt"]))
|
|
104
|
+
if normalize_repo_path(result_path) != expected_path:
|
|
105
|
+
raise LifecycleError("artifact-path-mismatch", "task result path does not match frozen template")
|
|
106
|
+
root = package_root(state_path, state)
|
|
107
|
+
result = read_json_object(root / expected_path, label="task result")
|
|
108
|
+
identity = artifact_identity(root, expected_path, result)
|
|
109
|
+
validate_task_result(state, task, result, identity)
|
|
110
|
+
model_usage_identity = None
|
|
111
|
+
if model_usage_receipt_required(task):
|
|
112
|
+
if model_usage_receipt_path is None:
|
|
113
|
+
raise LifecycleError(
|
|
114
|
+
"model-usage-receipt-required",
|
|
115
|
+
"task result requires a model usage receipt for the attempt model route",
|
|
116
|
+
)
|
|
117
|
+
usage_path = normalize_repo_path(model_usage_receipt_path, label="model usage receipt")
|
|
118
|
+
receipt = read_json_object(root / usage_path, label="model usage receipt")
|
|
119
|
+
model_usage_identity = artifact_identity(root, usage_path, receipt)
|
|
120
|
+
validation = validate_task_model_usage_receipt(
|
|
121
|
+
state,
|
|
122
|
+
task,
|
|
123
|
+
receipt,
|
|
124
|
+
budget_targets=_read_budget_targets(root, budget_targets_path),
|
|
125
|
+
)
|
|
126
|
+
task["modelUsageReceipt"] = {
|
|
127
|
+
**model_usage_identity,
|
|
128
|
+
"operationId": receipt["operationId"],
|
|
129
|
+
"host": receipt["host"],
|
|
130
|
+
"modelClass": receipt["modelClass"],
|
|
131
|
+
"usage": dict(receipt["usage"]),
|
|
132
|
+
"validation": {
|
|
133
|
+
"status": validation["status"],
|
|
134
|
+
"receiptDigest": validation["receiptDigest"],
|
|
135
|
+
"routeDecisionDigest": validation.get("routeDecisionDigest"),
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
elif model_usage_receipt_path is not None:
|
|
139
|
+
raise LifecycleError(
|
|
140
|
+
"unexpected-model-usage-receipt",
|
|
141
|
+
"task attempt does not require a model usage receipt",
|
|
142
|
+
)
|
|
143
|
+
task["result"] = identity
|
|
144
|
+
task["status"] = "VERIFYING"
|
|
145
|
+
task["lastReason"] = reason
|
|
146
|
+
record_gate_receipts(task, gate_receipts)
|
|
147
|
+
state["phase"] = "STEP_REVIEW"
|
|
148
|
+
commit_state(
|
|
149
|
+
state_path,
|
|
150
|
+
state,
|
|
151
|
+
operation_id=operation_id,
|
|
152
|
+
event_type="task-result-committed",
|
|
153
|
+
payload={
|
|
154
|
+
"taskId": task_id,
|
|
155
|
+
"attempt": task["attempt"],
|
|
156
|
+
"result": identity,
|
|
157
|
+
"modelUsageReceipt": model_usage_identity,
|
|
158
|
+
"reason": reason,
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
return status(state_path)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def accept_task(
|
|
165
|
+
state_path: Path,
|
|
166
|
+
*,
|
|
167
|
+
task_id: str,
|
|
168
|
+
operation_id: str,
|
|
169
|
+
expected_revision: int,
|
|
170
|
+
review_path: str,
|
|
171
|
+
implementation_audit_path: str | None = None,
|
|
172
|
+
reason: str,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
state = _mutable_state(state_path, operation_id, expected_revision)
|
|
175
|
+
if state["phase"] != "STEP_REVIEW":
|
|
176
|
+
raise LifecycleError("invalid-phase", "task acceptance requires STEP_REVIEW phase")
|
|
177
|
+
task = find_task(state, task_id)
|
|
178
|
+
if task.get("status") != "VERIFYING":
|
|
179
|
+
raise LifecycleError("invalid-task-status", f"task {task_id} is not VERIFYING")
|
|
180
|
+
gate_receipts = validate_controller_gates(
|
|
181
|
+
state_path,
|
|
182
|
+
state,
|
|
183
|
+
task,
|
|
184
|
+
phase="pre-acceptance",
|
|
185
|
+
operation_id=operation_id,
|
|
186
|
+
attempt=int(task["attempt"]),
|
|
187
|
+
)
|
|
188
|
+
expected_path = artifact_path(task, "review", int(task["attempt"]))
|
|
189
|
+
if normalize_repo_path(review_path) != expected_path:
|
|
190
|
+
raise LifecycleError("artifact-path-mismatch", "task review path does not match frozen template")
|
|
191
|
+
root = package_root(state_path, state)
|
|
192
|
+
review = read_json_object(root / expected_path, label="task review")
|
|
193
|
+
identity = artifact_identity(root, expected_path, review)
|
|
194
|
+
validate_task_review(state, task, review)
|
|
195
|
+
result = _read_committed_result(root, task)
|
|
196
|
+
ownership_receipt = _validate_task_write_scope(state, task, result)
|
|
197
|
+
implementation_audit = _validate_implementation_audit(
|
|
198
|
+
state_path,
|
|
199
|
+
state,
|
|
200
|
+
task,
|
|
201
|
+
implementation_audit_path=implementation_audit_path,
|
|
202
|
+
)
|
|
203
|
+
record_gate_receipts(task, gate_receipts)
|
|
204
|
+
task["ownershipReceipt"] = ownership_receipt
|
|
205
|
+
if implementation_audit is not None:
|
|
206
|
+
task["implementationAuditReport"] = implementation_audit
|
|
207
|
+
_mark_task_accepted(state, task, review, identity, reason)
|
|
208
|
+
commit_state(
|
|
209
|
+
state_path,
|
|
210
|
+
state,
|
|
211
|
+
operation_id=operation_id,
|
|
212
|
+
event_type="task-accepted",
|
|
213
|
+
payload={"taskId": task_id, "attempt": task["attempt"], "review": task["review"], "reason": reason},
|
|
214
|
+
)
|
|
215
|
+
return status(state_path)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _validate_implementation_audit(
|
|
219
|
+
state_path: Path,
|
|
220
|
+
state: dict[str, Any],
|
|
221
|
+
task: dict[str, Any],
|
|
222
|
+
*,
|
|
223
|
+
implementation_audit_path: str | None,
|
|
224
|
+
) -> dict[str, Any] | None:
|
|
225
|
+
required = task_implementation_audit_required(state_path, state, task)
|
|
226
|
+
if implementation_audit_path is None:
|
|
227
|
+
if required:
|
|
228
|
+
raise LifecycleError(
|
|
229
|
+
"implementation-audit-required",
|
|
230
|
+
"task acceptance requires an accepted implementation audit report",
|
|
231
|
+
{"taskId": task.get("id")},
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
return validate_task_implementation_audit_artifact(state_path, state, task, implementation_audit_path)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _mutable_state(
|
|
238
|
+
state_path: Path,
|
|
239
|
+
operation_id: str,
|
|
240
|
+
expected_revision: int,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
return load_for_update(state_path, operation_id=operation_id, expected_revision=expected_revision)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _require_source_and_authorization(state: dict[str, Any], source_revision: str) -> None:
|
|
246
|
+
if state.get("sourceRevision") != source_revision:
|
|
247
|
+
raise LifecycleError("source-revision-mismatch", "source revision mismatch")
|
|
248
|
+
authorization = state.get("authorization")
|
|
249
|
+
if not isinstance(authorization, dict) or authorization.get("granted") is not True:
|
|
250
|
+
raise LifecycleError("authorization-required", "execution authorization is required")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _require_dependencies_accepted(state: dict[str, Any], task: dict[str, Any]) -> None:
|
|
254
|
+
accepted = {item.get("id") for item in state["tasks"] if item.get("status") == "ACCEPTED"}
|
|
255
|
+
missing = sorted(set(task.get("dependsOn", [])).difference(accepted))
|
|
256
|
+
if missing:
|
|
257
|
+
raise LifecycleError(
|
|
258
|
+
"task-dependencies-missing",
|
|
259
|
+
f"task {task.get('id')} dependencies are not accepted",
|
|
260
|
+
{"missing": missing},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _require_parallel_capacity(state: dict[str, Any]) -> None:
|
|
265
|
+
running = sum(
|
|
266
|
+
1
|
|
267
|
+
for item in state["tasks"]
|
|
268
|
+
if item.get("status") in {"RUNNING", "VALIDATING", "VERIFYING"}
|
|
269
|
+
)
|
|
270
|
+
max_parallel = int(state.get("budgets", {}).get("maxParallelTasks", 1))
|
|
271
|
+
if running >= max_parallel:
|
|
272
|
+
raise LifecycleError("parallelism-budget-exhausted", "maxParallelTasks budget reached")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _mark_task_running(
|
|
276
|
+
state: dict[str, Any],
|
|
277
|
+
task: dict[str, Any],
|
|
278
|
+
attempt: int,
|
|
279
|
+
reason: str,
|
|
280
|
+
) -> None:
|
|
281
|
+
task["attempt"] = attempt
|
|
282
|
+
task["status"] = "RUNNING"
|
|
283
|
+
task["usageIterations"] = []
|
|
284
|
+
task["controllerGateReceipts"] = []
|
|
285
|
+
task.pop("result", None)
|
|
286
|
+
task.pop("review", None)
|
|
287
|
+
task.pop("modelUsageReceipt", None)
|
|
288
|
+
task["attemptStartedAt"] = now_iso()
|
|
289
|
+
task["attemptBaseRevision"] = state.get("sourceRevision")
|
|
290
|
+
task["attemptDeadlineAt"] = deadline_after(
|
|
291
|
+
task["attemptStartedAt"],
|
|
292
|
+
int(state.get("budgets", {}).get("maxTaskWallSeconds", 3600)),
|
|
293
|
+
)
|
|
294
|
+
task["lastReason"] = reason
|
|
295
|
+
if isinstance(task.get("modelRoute"), dict) and task["modelRoute"]:
|
|
296
|
+
validate_attempt_model_route(task)
|
|
297
|
+
task["attemptModelRoute"] = {
|
|
298
|
+
**task["modelRoute"],
|
|
299
|
+
"attempt": attempt,
|
|
300
|
+
}
|
|
301
|
+
state["phase"] = "RUNNING"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _read_budget_targets(root: Path, budget_targets_path: str | None) -> dict[str, Any] | None:
|
|
305
|
+
if budget_targets_path is None:
|
|
306
|
+
return None
|
|
307
|
+
return read_json_object(
|
|
308
|
+
root / normalize_repo_path(budget_targets_path, label="budget targets"),
|
|
309
|
+
label="budget targets",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _mark_task_accepted(
|
|
314
|
+
state: dict[str, Any],
|
|
315
|
+
task: dict[str, Any],
|
|
316
|
+
review: dict[str, Any],
|
|
317
|
+
identity: dict[str, Any],
|
|
318
|
+
reason: str,
|
|
319
|
+
) -> None:
|
|
320
|
+
task["review"] = {
|
|
321
|
+
**identity,
|
|
322
|
+
"reviewId": review["reviewId"],
|
|
323
|
+
"reviewer": review["reviewer"]["id"],
|
|
324
|
+
"reviewerRunId": review["reviewer"]["runId"],
|
|
325
|
+
"surface": review["reviewer"]["surface"],
|
|
326
|
+
"verdict": review["verdict"],
|
|
327
|
+
}
|
|
328
|
+
task["status"] = "ACCEPTED"
|
|
329
|
+
task["lastReason"] = reason
|
|
330
|
+
task.pop("attemptStartedAt", None)
|
|
331
|
+
task.pop("attemptDeadlineAt", None)
|
|
332
|
+
unlock_ready_tasks(state)
|
|
333
|
+
state["phase"] = "RUNNING" if ready_tasks(state) else "FINAL_AUDIT"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _read_committed_result(root: Path, task: dict[str, Any]) -> dict[str, Any]:
|
|
337
|
+
result_identity = task.get("result")
|
|
338
|
+
if not isinstance(result_identity, dict) or not isinstance(result_identity.get("path"), str):
|
|
339
|
+
raise LifecycleError("missing-task-result", "task acceptance requires committed result")
|
|
340
|
+
return read_json_object(root / normalize_repo_path(result_identity["path"]), label="task result")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _validate_task_write_scope(
|
|
344
|
+
state: dict[str, Any],
|
|
345
|
+
task: dict[str, Any],
|
|
346
|
+
result: dict[str, Any],
|
|
347
|
+
) -> dict[str, Any]:
|
|
348
|
+
changed_files = result.get("changedFiles")
|
|
349
|
+
if not isinstance(changed_files, list) or not all(isinstance(path, str) for path in changed_files):
|
|
350
|
+
raise LifecycleError("task-result-invalid", "task result changedFiles must be a list of strings")
|
|
351
|
+
writes = [
|
|
352
|
+
normalize_repo_path(path)
|
|
353
|
+
for path in task.get("writes", [])
|
|
354
|
+
if isinstance(path, str)
|
|
355
|
+
]
|
|
356
|
+
if not writes:
|
|
357
|
+
if changed_files and (state.get("manifestPath") or state.get("packetSet")):
|
|
358
|
+
raise LifecycleError(
|
|
359
|
+
"task-write-scope-missing",
|
|
360
|
+
"adopted task state has changed files but no write scope",
|
|
361
|
+
{"taskId": task.get("id"), "changedFiles": changed_files},
|
|
362
|
+
)
|
|
363
|
+
return {
|
|
364
|
+
"schemaVersion": "agent-task-ownership-receipt.v1",
|
|
365
|
+
"status": "SKIPPED_NO_WRITE_SCOPE",
|
|
366
|
+
"runId": state.get("runId"),
|
|
367
|
+
"packageId": state.get("packageId"),
|
|
368
|
+
"taskId": task.get("id"),
|
|
369
|
+
"attempt": task.get("attempt"),
|
|
370
|
+
"planDigest": state.get("planDigest"),
|
|
371
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
372
|
+
"changedFileCount": len(changed_files),
|
|
373
|
+
"entries": [],
|
|
374
|
+
"blockers": [],
|
|
375
|
+
}
|
|
376
|
+
policy = state.get("writePolicy", {}) if isinstance(state.get("writePolicy"), dict) else {}
|
|
377
|
+
forbidden_roots = [
|
|
378
|
+
normalize_repo_path(path)
|
|
379
|
+
for path in policy.get("forbiddenWrites", [])
|
|
380
|
+
if isinstance(path, str)
|
|
381
|
+
]
|
|
382
|
+
read_only_roots = [
|
|
383
|
+
normalize_repo_path(path)
|
|
384
|
+
for path in policy.get("readOnly", [])
|
|
385
|
+
if isinstance(path, str)
|
|
386
|
+
]
|
|
387
|
+
entries: list[dict[str, Any]] = []
|
|
388
|
+
for raw_path in sorted(set(changed_files)):
|
|
389
|
+
path = normalize_repo_path(raw_path)
|
|
390
|
+
forbidden = [root for root in forbidden_roots if is_under_repo_path(path, root)]
|
|
391
|
+
read_only = [root for root in read_only_roots if is_under_repo_path(path, root)]
|
|
392
|
+
owned = [root for root in writes if is_under_repo_path(path, root)]
|
|
393
|
+
if forbidden:
|
|
394
|
+
entries.append({"path": path, "category": "forbidden", "matched": forbidden})
|
|
395
|
+
elif read_only:
|
|
396
|
+
entries.append({"path": path, "category": "read-only", "matched": read_only})
|
|
397
|
+
elif owned:
|
|
398
|
+
entries.append({"path": path, "category": "task-owned", "matched": owned})
|
|
399
|
+
else:
|
|
400
|
+
entries.append({"path": path, "category": "unowned"})
|
|
401
|
+
blockers = [
|
|
402
|
+
entry
|
|
403
|
+
for entry in entries
|
|
404
|
+
if entry["category"] in {"forbidden", "read-only", "unowned"}
|
|
405
|
+
]
|
|
406
|
+
receipt = {
|
|
407
|
+
"schemaVersion": "agent-task-ownership-receipt.v1",
|
|
408
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
409
|
+
"runId": state.get("runId"),
|
|
410
|
+
"packageId": state.get("packageId"),
|
|
411
|
+
"taskId": task.get("id"),
|
|
412
|
+
"attempt": task.get("attempt"),
|
|
413
|
+
"planDigest": state.get("planDigest"),
|
|
414
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
415
|
+
"changedFileCount": len(entries),
|
|
416
|
+
"entries": entries,
|
|
417
|
+
"blockers": blockers,
|
|
418
|
+
}
|
|
419
|
+
if blockers:
|
|
420
|
+
raise LifecycleError("task-ownership-violation", "task result changed files are outside task write scope", {"ownership": receipt})
|
|
421
|
+
return receipt
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Worktree isolation receipt helpers."""
|
|
2
|
+
|
|
3
|
+
from agent_lifecycle.worktree.isolation import (
|
|
4
|
+
build_attempt_isolation_receipt,
|
|
5
|
+
build_worktree_writeback_receipt,
|
|
6
|
+
validate_attempt_isolation_receipt,
|
|
7
|
+
validate_worktree_writeback_receipt,
|
|
8
|
+
validate_worktree_policy,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"build_attempt_isolation_receipt",
|
|
13
|
+
"build_worktree_writeback_receipt",
|
|
14
|
+
"validate_attempt_isolation_receipt",
|
|
15
|
+
"validate_worktree_writeback_receipt",
|
|
16
|
+
"validate_worktree_policy",
|
|
17
|
+
]
|