forgexa-cli 1.86.2__tar.gz → 1.86.4__tar.gz
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.
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/PKG-INFO +1 -1
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/agent_core.py +70 -3
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/daemon.py +49 -7
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/pyproject.toml +1 -1
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/README.md +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/setup.cfg +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.86.2 → forgexa_cli-1.86.4}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.86.
|
|
2
|
+
__version__ = "1.86.4"
|
|
@@ -309,6 +309,14 @@ def requires_task_delivery_baseline(node_type: str, task_input: dict) -> bool:
|
|
|
309
309
|
|
|
310
310
|
|
|
311
311
|
def extract_implementation_audit(text: str) -> dict | None:
|
|
312
|
+
return extract_agent_report(text, "implementation_audit")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def extract_execution_delivery(text: str) -> dict | None:
|
|
316
|
+
return extract_agent_report(text, "execution_delivery")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def extract_agent_report(text: str, field: str) -> dict | None:
|
|
312
320
|
"""Read the last complete audit envelope from mixed assistant output."""
|
|
313
321
|
decoder = json.JSONDecoder()
|
|
314
322
|
offset = 0
|
|
@@ -323,11 +331,11 @@ def extract_implementation_audit(text: str) -> dict | None:
|
|
|
323
331
|
except json.JSONDecodeError:
|
|
324
332
|
offset = start + 1
|
|
325
333
|
continue
|
|
326
|
-
if isinstance(value, dict) and
|
|
327
|
-
report = value[
|
|
334
|
+
if isinstance(value, dict) and field in value:
|
|
335
|
+
report = value[field] if isinstance(value[field], dict) else None
|
|
328
336
|
report_end = end
|
|
329
337
|
offset = end
|
|
330
|
-
if
|
|
338
|
+
if json.dumps(field) in text[report_end:]:
|
|
331
339
|
return None
|
|
332
340
|
return report
|
|
333
341
|
|
|
@@ -774,6 +782,51 @@ def lock_provenance_projection(path: str, source: str) -> str | None:
|
|
|
774
782
|
}, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
775
783
|
|
|
776
784
|
|
|
785
|
+
def implementation_delivery_contract(node_type: str, task_input: dict, *, supported: bool, graph_type: str = "execution") -> dict | None:
|
|
786
|
+
if supported is not True or not isinstance(node_type, str) or not isinstance(graph_type, str) or node_type not in {"design", "coding", "testing", "review", "fix"} or graph_type not in {"execution", "implementation", "fix"}:
|
|
787
|
+
return None
|
|
788
|
+
if task_input.get("verification_work_item") or task_input.get("execution_actor") == "human":
|
|
789
|
+
return None
|
|
790
|
+
recovery = task_input.get("verification_recovery")
|
|
791
|
+
if isinstance(recovery, dict) and recovery.get("mode") == "review_only" and not task_input.get("implementation_delivery"):
|
|
792
|
+
return None
|
|
793
|
+
baseline = task_input.get("approved_plan_baseline")
|
|
794
|
+
if not isinstance(baseline, dict) or assess_approved_plan_conformance(baseline, None).get("reason") == "approved_plan_baseline_invalid":
|
|
795
|
+
return None
|
|
796
|
+
task_id = (baseline.get("task") or {}).get("id")
|
|
797
|
+
if not isinstance(task_id, str) or not task_id:
|
|
798
|
+
return None
|
|
799
|
+
contract = {"version": 1, "mode": "execution_and_artifacts", "node_type": node_type, "graph_type": graph_type,
|
|
800
|
+
"task_id": task_id, "baseline_fingerprint": baseline.get("fingerprint"),
|
|
801
|
+
"qa_policy": task_input.get("qa_test_level_policy_snapshot"), "change_scope": task_input.get("change_scope")}
|
|
802
|
+
contract["fingerprint"] = hashlib.sha256(json.dumps(contract, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
|
|
803
|
+
return contract
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def uses_controlled_delivery(task_input: dict) -> bool:
|
|
807
|
+
contract = task_input.get("implementation_delivery")
|
|
808
|
+
if not isinstance(contract, dict):
|
|
809
|
+
return False
|
|
810
|
+
expected = implementation_delivery_contract(contract.get("node_type"), task_input, supported=True, graph_type=contract.get("graph_type"))
|
|
811
|
+
return expected is not None and contract == expected
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def assess_execution_delivery(contract: dict, report: object) -> dict:
|
|
815
|
+
if not isinstance(report, dict) or type(report.get("version")) is not int or report["version"] != 1:
|
|
816
|
+
return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration is missing or invalid"]}
|
|
817
|
+
if set(report) != {"version", "contract_fingerprint", "status"} or report.get("contract_fingerprint") != contract.get("fingerprint") or not isinstance(report.get("status"), str) or report["status"] not in {"complete", "partial", "blocked"}:
|
|
818
|
+
return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration does not match the current contract"]}
|
|
819
|
+
return {"report_valid": True, "status": report["status"], "issues": []}
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def redact_execution_delivery(report: object) -> dict | None:
|
|
823
|
+
if not isinstance(report, dict):
|
|
824
|
+
return None
|
|
825
|
+
return {"version": report.get("version") if type(report.get("version")) is int else 0,
|
|
826
|
+
"contract_fingerprint": report.get("contract_fingerprint") if isinstance(report.get("contract_fingerprint"), str) and re.fullmatch(r"[0-9a-f]{64}", report["contract_fingerprint"]) else "",
|
|
827
|
+
"status": report.get("status") if report.get("status") in ("complete", "partial", "blocked") else "unknown"}
|
|
828
|
+
|
|
829
|
+
|
|
777
830
|
def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None, policy_supported: bool = True) -> dict:
|
|
778
831
|
baseline = task_input.get("approved_plan_baseline")
|
|
779
832
|
reasons = []
|
|
@@ -810,6 +863,8 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
|
|
|
810
863
|
if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
|
|
811
864
|
reasons.append("scope_unknown")
|
|
812
865
|
decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
|
|
866
|
+
if uses_controlled_delivery(task_input):
|
|
867
|
+
decision.update(mode="advisory", decision_scope="controlled_delivery", delivery_contract=task_input["implementation_delivery"]["fingerprint"])
|
|
813
868
|
decision["fingerprint"] = hashlib.sha256(json.dumps({
|
|
814
869
|
**decision, "baseline_fingerprint": fingerprint,
|
|
815
870
|
"qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
|
|
@@ -819,6 +874,8 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
|
|
|
819
874
|
|
|
820
875
|
|
|
821
876
|
def review_blocks_completion(policy: dict, assessment: dict) -> bool:
|
|
877
|
+
if policy.get("decision_scope") == "controlled_delivery":
|
|
878
|
+
return False
|
|
822
879
|
return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
|
|
823
880
|
policy.get("mode") != "advisory" or assessment.get("status") == "deviation"
|
|
824
881
|
)
|
|
@@ -2168,6 +2225,7 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
|
|
|
2168
2225
|
contains words like 'rate limit', 'connection error', 'API error', etc.
|
|
2169
2226
|
(e.g. implementing a rate-limiter feature, or documenting error codes).
|
|
2170
2227
|
"""
|
|
2228
|
+
terminal_status = None
|
|
2171
2229
|
has_turn_completed = False
|
|
2172
2230
|
has_turn_failed = False
|
|
2173
2231
|
has_result = False
|
|
@@ -2193,10 +2251,12 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
|
|
|
2193
2251
|
ev_type = str(data.get("type", ""))
|
|
2194
2252
|
|
|
2195
2253
|
if ev_type == "turn.completed":
|
|
2254
|
+
terminal_status = "success"
|
|
2196
2255
|
has_turn_completed = True
|
|
2197
2256
|
has_turn_failed = False
|
|
2198
2257
|
error_messages.clear()
|
|
2199
2258
|
elif ev_type == "turn.failed":
|
|
2259
|
+
terminal_status = "failed"
|
|
2200
2260
|
has_turn_failed = True
|
|
2201
2261
|
err = data.get("error") or {}
|
|
2202
2262
|
if isinstance(err, dict):
|
|
@@ -2229,19 +2289,23 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
|
|
|
2229
2289
|
if type(raw_exit_code) is not int and not (
|
|
2230
2290
|
isinstance(raw_exit_code, str) and re.fullmatch(r"[+-]?[0-9]+", raw_exit_code)
|
|
2231
2291
|
):
|
|
2292
|
+
terminal_status = "failed"
|
|
2232
2293
|
error_messages.append("Agent result has an invalid exitCode")
|
|
2233
2294
|
continue
|
|
2234
2295
|
exit_code = int(raw_exit_code)
|
|
2235
2296
|
if exit_code == 0:
|
|
2297
|
+
terminal_status = "success"
|
|
2236
2298
|
has_result = True
|
|
2237
2299
|
has_meaningful_content = True
|
|
2238
2300
|
has_turn_failed = False
|
|
2239
2301
|
error_messages.clear()
|
|
2240
2302
|
else:
|
|
2303
|
+
terminal_status = "failed"
|
|
2241
2304
|
error_messages.append(f"Copilot exited with code {exit_code}")
|
|
2242
2305
|
else:
|
|
2243
2306
|
result_text = str(data.get("result", "") or "")
|
|
2244
2307
|
if data.get("is_error"):
|
|
2308
|
+
terminal_status = "failed"
|
|
2245
2309
|
raw_errors = data.get("errors")
|
|
2246
2310
|
if isinstance(raw_errors, list):
|
|
2247
2311
|
err_text = "; ".join(
|
|
@@ -2269,8 +2333,10 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
|
|
|
2269
2333
|
# rather than the agent's actual work output.
|
|
2270
2334
|
no_api_call = not has_agent_token_usage(data, usage_source) and not has_assistant_events
|
|
2271
2335
|
if no_api_call and result_text:
|
|
2336
|
+
terminal_status = "failed"
|
|
2272
2337
|
error_messages.append(result_text)
|
|
2273
2338
|
else:
|
|
2339
|
+
terminal_status = "success"
|
|
2274
2340
|
has_result = True
|
|
2275
2341
|
has_meaningful_content = True
|
|
2276
2342
|
has_turn_failed = False
|
|
@@ -2314,6 +2380,7 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
|
|
|
2314
2380
|
has_meaningful_content = True
|
|
2315
2381
|
|
|
2316
2382
|
return {
|
|
2383
|
+
"terminal_status": "failed" if error_messages or has_turn_failed else terminal_status,
|
|
2317
2384
|
"has_turn_completed": has_turn_completed,
|
|
2318
2385
|
"has_turn_failed": has_turn_failed,
|
|
2319
2386
|
"has_result": has_result,
|
|
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1234
1234
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1235
1235
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1236
1236
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1237
|
-
DAEMON_VERSION = "1.86.
|
|
1237
|
+
DAEMON_VERSION = "1.86.4"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -6502,6 +6502,8 @@ class ProcessManager:
|
|
|
6502
6502
|
signals = agent_core._extract_output_signals(
|
|
6503
6503
|
"\n".join(part for part in (stdout, stderr) if part), result.metrics,
|
|
6504
6504
|
)
|
|
6505
|
+
if result.exit_code == 0 and not error and (signals.get("terminal_status") == "success" or result.has_verified_completion is True and signals.get("terminal_status") != "failed"):
|
|
6506
|
+
error_channels = ""
|
|
6505
6507
|
if not has_token_usage and ProcessManager._should_scan_short_success_stdout(stdout, signals):
|
|
6506
6508
|
error_channels = "\n".join(filter(None, [error_channels, stdout.strip()]))
|
|
6507
6509
|
# Always include structured error messages parsed from JSONL stream events
|
|
@@ -10221,6 +10223,7 @@ class ServerConnection:
|
|
|
10221
10223
|
"task_delivery": {"protocol_version": 1},
|
|
10222
10224
|
"verification_recovery": {"protocol_version": 1},
|
|
10223
10225
|
"task_review_policy": {"protocol_version": 1},
|
|
10226
|
+
"implementation_delivery": {"protocol_version": 1},
|
|
10224
10227
|
"recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
|
|
10225
10228
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
10226
10229
|
},
|
|
@@ -10649,6 +10652,7 @@ class ServerConnection:
|
|
|
10649
10652
|
"task_delivery": {"protocol_version": 1},
|
|
10650
10653
|
"verification_recovery": {"protocol_version": 1},
|
|
10651
10654
|
"task_review_policy": {"protocol_version": 1},
|
|
10655
|
+
"implementation_delivery": {"protocol_version": 1},
|
|
10652
10656
|
"recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
|
|
10653
10657
|
"local_evidence": {"protocol_version": 1, "structured_summary": True},
|
|
10654
10658
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
@@ -11438,6 +11442,20 @@ class RuntimeDaemon:
|
|
|
11438
11442
|
if managed_policy is not _MANAGED_WSL_POLICY:
|
|
11439
11443
|
raise ValueError("wsl_runtime_entry_not_admitted")
|
|
11440
11444
|
self.managed_policy = managed_policy
|
|
11445
|
+
# Ops-tunable staging budget. Keep aligned with the server's
|
|
11446
|
+
# MAX_ANALYSIS_ATTACHMENT_COUNT: the server only admits analysis
|
|
11447
|
+
# selections within its own budget, so a lower value here would
|
|
11448
|
+
# reject tasks the server considered valid.
|
|
11449
|
+
try:
|
|
11450
|
+
self._TASK_ATTACHMENT_MAX_COUNT = max(
|
|
11451
|
+
1, int(os.environ.get("DAEMON_TASK_ATTACHMENT_MAX_COUNT", self._TASK_ATTACHMENT_MAX_COUNT))
|
|
11452
|
+
)
|
|
11453
|
+
except ValueError:
|
|
11454
|
+
logger.warning(
|
|
11455
|
+
"Ignoring invalid DAEMON_TASK_ATTACHMENT_MAX_COUNT=%r; using %d",
|
|
11456
|
+
os.environ.get("DAEMON_TASK_ATTACHMENT_MAX_COUNT"),
|
|
11457
|
+
self._TASK_ATTACHMENT_MAX_COUNT,
|
|
11458
|
+
)
|
|
11441
11459
|
# Compute stable hardware fingerprint from machine-id / MAC address.
|
|
11442
11460
|
# This allows the server to identify the same physical machine even when
|
|
11443
11461
|
# its IP address changes (e.g., DHCP reassignment, home vs. office).
|
|
@@ -14619,6 +14637,16 @@ class RuntimeDaemon:
|
|
|
14619
14637
|
issues: list[str] = []
|
|
14620
14638
|
node_type = task.node_type
|
|
14621
14639
|
input_data = task.input_data or {}
|
|
14640
|
+
controlled_delivery = agent_core.uses_controlled_delivery(input_data)
|
|
14641
|
+
if controlled_delivery:
|
|
14642
|
+
report = agent_core.extract_execution_delivery(_daemon_extract_agent_output(str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or ""), result.stdout))
|
|
14643
|
+
delivery = agent_core.assess_execution_delivery(input_data["implementation_delivery"], report)
|
|
14644
|
+
result.metrics["execution_delivery"] = agent_core.redact_execution_delivery(report)
|
|
14645
|
+
result.metrics["execution_delivery_valid"] = delivery["report_valid"]
|
|
14646
|
+
if not delivery["report_valid"]:
|
|
14647
|
+
issues.extend(_OutputValidationIssue("validation_unavailable", message) for message in delivery["issues"])
|
|
14648
|
+
elif delivery["status"] != "complete":
|
|
14649
|
+
issues.append(_OutputValidationIssue("planned_task_incomplete", "Agent explicitly reported incomplete execution delivery"))
|
|
14622
14650
|
fix_context = input_data.get("fix_context")
|
|
14623
14651
|
if node_type == "testing" or (
|
|
14624
14652
|
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
@@ -14651,18 +14679,18 @@ class RuntimeDaemon:
|
|
|
14651
14679
|
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
14652
14680
|
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
14653
14681
|
failure_domain = agent_core.implementation_audit_failure_domain(audit_contract, audit_result)
|
|
14654
|
-
if failure_domain is not None:
|
|
14682
|
+
if failure_domain is not None and not controlled_delivery:
|
|
14655
14683
|
result.metrics["failure_domain"] = failure_domain
|
|
14656
14684
|
result.metrics["implementation_audit_node_complete"] = bool(
|
|
14657
|
-
audit_result["report_valid"] and audit_result["node_complete"]
|
|
14685
|
+
controlled_delivery or audit_result["report_valid"] and audit_result["node_complete"]
|
|
14658
14686
|
)
|
|
14659
14687
|
result.metrics["implementation_audit_subject_blocked"] = bool(
|
|
14660
|
-
audit_result["report_valid"] and not audit_result["node_complete"]
|
|
14688
|
+
not controlled_delivery and audit_result["report_valid"] and not audit_result["node_complete"]
|
|
14661
14689
|
and (failure_domain == "environment" or any(
|
|
14662
14690
|
finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
|
|
14663
14691
|
for finding in audit_result.get("findings") or []))
|
|
14664
14692
|
)
|
|
14665
|
-
if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
|
|
14693
|
+
if not controlled_delivery and audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
|
|
14666
14694
|
result.status = "failed"
|
|
14667
14695
|
result.failure_code = "implementation_plan_revision_required"
|
|
14668
14696
|
requests = audit_result.get("blocking_revision_requests", audit_result["revision_requests"])
|
|
@@ -14670,7 +14698,7 @@ class RuntimeDaemon:
|
|
|
14670
14698
|
f"{request['task_id']}: {request['reason']}" for request in requests[:3]
|
|
14671
14699
|
)
|
|
14672
14700
|
issues.append(_OutputValidationIssue(result.failure_code, result.error))
|
|
14673
|
-
elif not audit_result["report_valid"] or not audit_result["node_complete"]:
|
|
14701
|
+
elif not controlled_delivery and (not audit_result["report_valid"] or not audit_result["node_complete"]):
|
|
14674
14702
|
issues.extend(_OutputValidationIssue("implementation_audit_incomplete", issue) for issue in (
|
|
14675
14703
|
audit_result["issues"] or audit_result.get("blocking_reasons") or [
|
|
14676
14704
|
"The selected approved task still has implementation gaps",
|
|
@@ -15211,7 +15239,9 @@ class RuntimeDaemon:
|
|
|
15211
15239
|
ac for ac in ac_list
|
|
15212
15240
|
if ac["status"] != "covered" and ac["id"] not in pending
|
|
15213
15241
|
]
|
|
15214
|
-
if uncovered:
|
|
15242
|
+
if uncovered and controlled_delivery:
|
|
15243
|
+
result.metrics["business_acceptance"] = {"status": "pending_human_qa", "criteria": [ac["id"] for ac in uncovered]}
|
|
15244
|
+
elif uncovered:
|
|
15215
15245
|
ids = ", ".join(ac["id"] for ac in uncovered[:20])
|
|
15216
15246
|
if len(uncovered) > 20:
|
|
15217
15247
|
ids += f" (+{len(uncovered) - 20} more)"
|
|
@@ -16686,6 +16716,18 @@ class RuntimeDaemon:
|
|
|
16686
16716
|
*[f"[daemon] {issue}" for issue in issues],
|
|
16687
16717
|
])
|
|
16688
16718
|
|
|
16719
|
+
if (
|
|
16720
|
+
agent_core.uses_controlled_delivery(task.input_data or {})
|
|
16721
|
+
and result.metrics.get("execution_delivery_valid") is True
|
|
16722
|
+
and (result.metrics.get("execution_delivery") or {}).get("status") in {"partial", "blocked"}
|
|
16723
|
+
):
|
|
16724
|
+
result.status = "failed"
|
|
16725
|
+
result.failure_code = "planned_task_incomplete"
|
|
16726
|
+
result.error = "Agent explicitly reported incomplete execution delivery; preserve work and resolve prerequisites before an authorized retry"
|
|
16727
|
+
_record_output_validation_issues(result, issues)
|
|
16728
|
+
self._record_testing_telemetry(workspace_path, task, result, retries_used=attempt)
|
|
16729
|
+
return result
|
|
16730
|
+
|
|
16689
16731
|
if (
|
|
16690
16732
|
result.metrics.get("failure_domain") == "environment" and all(
|
|
16691
16733
|
issue.code in {"implementation_audit_incomplete", "acceptance_criteria_uncovered"} for issue in issues
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|