forgexa-cli 1.85.1__tar.gz → 1.85.3__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.85.1 → forgexa_cli-1.85.3}/PKG-INFO +1 -1
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/agent_core.py +258 -6
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/daemon.py +35 -11
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/pyproject.toml +1 -1
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/README.md +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/setup.cfg +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.85.1 → forgexa_cli-1.85.3}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.85.
|
|
2
|
+
__version__ = "1.85.3"
|
|
@@ -68,16 +68,125 @@ except ImportError:
|
|
|
68
68
|
logger = logging.getLogger("agent_core")
|
|
69
69
|
|
|
70
70
|
IMPLEMENTATION_AUDIT_PROTOCOL_VERSION = 3
|
|
71
|
+
IMPLEMENTATION_ENVIRONMENT_BLOCKERS = frozenset({
|
|
72
|
+
"environment_unavailable", "service_not_running", "dependency_unavailable",
|
|
73
|
+
"permission_required", "test_data_unavailable", "target_mismatch",
|
|
74
|
+
})
|
|
75
|
+
TASK_FAILURE_DOMAINS = frozenset({"code_quality", "execution", "environment", "evidence", "delivery", "plan", "verification"})
|
|
71
76
|
LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS = frozenset({
|
|
72
77
|
"validation_issues", "validation_issue_details", "timeout_recovery_validation_issues",
|
|
73
78
|
"timeout_recovery_original_failure", "task_delivery_issue",
|
|
74
79
|
})
|
|
75
80
|
|
|
76
81
|
|
|
77
|
-
def
|
|
82
|
+
def verification_obligation_fingerprint(task: dict) -> str:
|
|
83
|
+
task_input = task.get("input") if isinstance(task.get("input"), dict) else {}
|
|
84
|
+
responsibility = {
|
|
85
|
+
"id": task.get("id"), "type": task.get("type"), "description": task.get("description") or "",
|
|
86
|
+
"items": task.get("items") or [], "depends_on": task.get("depends_on") or [],
|
|
87
|
+
"files": task_input.get("files") or task.get("files") or [],
|
|
88
|
+
}
|
|
89
|
+
return hashlib.sha256(json.dumps(responsibility, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def approved_contribution_review_instructions(baseline: dict, audit_contract: dict | None) -> str:
|
|
93
|
+
task = baseline.get("task") or {}
|
|
94
|
+
obligations = baseline.get("verification_obligations") or {}
|
|
95
|
+
if (
|
|
96
|
+
not isinstance(audit_contract, dict) or audit_contract.get("version") != 2
|
|
97
|
+
or audit_contract.get("plan_id") != baseline.get("plan_id") or audit_contract.get("subject_task_id") != task.get("id")
|
|
98
|
+
or audit_contract.get("final_verification") is not False or task.get("type") not in ("coding", "fix")
|
|
99
|
+
or not isinstance(obligations, dict) or type(obligations.get("version")) is not int or obligations["version"] != 1
|
|
100
|
+
):
|
|
101
|
+
return ""
|
|
102
|
+
verifiers = [check for check in obligations.get("checks") or [] if isinstance(check, dict)
|
|
103
|
+
and check.get("type") in ("testing", "verify") and check.get("task_id") != task["id"]]
|
|
104
|
+
if not verifiers:
|
|
105
|
+
return ""
|
|
106
|
+
example = {"contribution_check": {"check_id": str(task["id"]) + ":assigned", "status": "unverified",
|
|
107
|
+
"evidence": ["<independent source and every assigned check evidence>"]},
|
|
108
|
+
"pending_checks": [{"check_id": verifiers[0]["check_id"], "task_id": verifiers[0]["task_id"],
|
|
109
|
+
"status": "unverified", "evidence": ["<observed outstanding approved verification>"]}]}
|
|
110
|
+
return (
|
|
111
|
+
"\n## Assigned Contribution and Pending Verification\n"
|
|
112
|
+
"Within the bound plan_conformance object, contribution_check covers all requirements and mandatory checks of this task, "
|
|
113
|
+
"not only static source. When independently evidenced as conformant, other approved verification owners may remain pending. "
|
|
114
|
+
"Use the exact frozen check_id/task_id pairs; this does not authorize moving an own check to another owner, "
|
|
115
|
+
"inventing an owner, waiving a check or changing its phase. Keep the overall status unverified while pending_checks remain. "
|
|
116
|
+
"The score and verdict assess the assigned contribution, not unfinished work assigned elsewhere. "
|
|
117
|
+
"A real own gap stays deviation or unverified. The default example is not completion evidence.\n"
|
|
118
|
+
"Scoped contribution fields example:\n" + json.dumps(example, ensure_ascii=True, separators=(",", ":"))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _assess_contribution_conformance(baseline: dict, report: dict, audit_contract: dict | None, result: dict) -> dict:
|
|
123
|
+
obligations = baseline.get("verification_obligations")
|
|
124
|
+
subject = result["task_id"]
|
|
125
|
+
if (
|
|
126
|
+
not isinstance(audit_contract, dict) or audit_contract.get("version") != 2
|
|
127
|
+
or audit_contract.get("plan_id") != baseline.get("plan_id")
|
|
128
|
+
or audit_contract.get("subject_task_id") != subject or audit_contract.get("final_verification") is not False
|
|
129
|
+
or (baseline.get("task") or {}).get("type") not in ("coding", "fix")
|
|
130
|
+
or not isinstance(obligations, dict) or type(obligations.get("version")) is not int or obligations["version"] != 1
|
|
131
|
+
):
|
|
132
|
+
return result
|
|
133
|
+
checks = obligations.get("checks")
|
|
134
|
+
if not isinstance(checks, list) or any(not isinstance(check, dict) for check in checks):
|
|
135
|
+
return result
|
|
136
|
+
by_id = {check.get("check_id"): check for check in checks if isinstance(check.get("check_id"), str)}
|
|
137
|
+
own = by_id.get(str(subject) + ":assigned")
|
|
138
|
+
tasks = audit_contract.get("tasks")
|
|
139
|
+
if not isinstance(tasks, list) or any(not isinstance(task, dict) or not isinstance(task.get("id"), str) for task in tasks):
|
|
140
|
+
return result
|
|
141
|
+
approved = {task["id"]: task for task in tasks}
|
|
142
|
+
contribution = report.get("contribution_check")
|
|
143
|
+
pending = report.get("pending_checks")
|
|
144
|
+
if (
|
|
145
|
+
len(by_id) != len(checks) or not isinstance(own, dict) or own.get("task_id") != subject
|
|
146
|
+
or subject not in approved or own.get("task_fingerprint") != verification_obligation_fingerprint(approved[subject])
|
|
147
|
+
or own.get("task_fingerprint") != verification_obligation_fingerprint(baseline["task"])
|
|
148
|
+
or own.get("required") is not True or not isinstance(contribution, dict)
|
|
149
|
+
or contribution.get("check_id") != own["check_id"] or contribution.get("status") != "conformant"
|
|
150
|
+
or contribution.get("plan_revision") is not None
|
|
151
|
+
or not isinstance(pending, list) or not pending
|
|
152
|
+
):
|
|
153
|
+
return result
|
|
154
|
+
evidence = contribution.get("evidence")
|
|
155
|
+
if not isinstance(evidence, list) or not evidence or any(not isinstance(item, str) or not item.strip() for item in evidence):
|
|
156
|
+
return result
|
|
157
|
+
normalized = []
|
|
158
|
+
seen = set()
|
|
159
|
+
for check in pending:
|
|
160
|
+
if not isinstance(check, dict) or not isinstance(check.get("check_id"), str):
|
|
161
|
+
return result
|
|
162
|
+
binding = by_id.get(check["check_id"])
|
|
163
|
+
references = check.get("evidence")
|
|
164
|
+
reason = check.get("blocking_reason")
|
|
165
|
+
if (
|
|
166
|
+
not isinstance(binding, dict) or check["check_id"] in seen or binding.get("required") is not True
|
|
167
|
+
or check.get("task_id") == subject or binding.get("task_id") != check.get("task_id")
|
|
168
|
+
or binding.get("type") not in ("testing", "verify") or approved.get(check.get("task_id"), {}).get("type") != binding.get("type")
|
|
169
|
+
or binding.get("task_fingerprint") != verification_obligation_fingerprint(approved.get(check.get("task_id"), {}))
|
|
170
|
+
or check.get("status") not in ("blocked", "unverified") or check.get("plan_revision") is not None
|
|
171
|
+
or not isinstance(references, list) or not references
|
|
172
|
+
or any(not isinstance(item, str) or not item.strip() for item in references)
|
|
173
|
+
or reason is not None and (not isinstance(reason, str) or reason not in IMPLEMENTATION_ENVIRONMENT_BLOCKERS)
|
|
174
|
+
):
|
|
175
|
+
return result
|
|
176
|
+
seen.add(check["check_id"])
|
|
177
|
+
normalized.append({"check_id": check["check_id"], "task_id": check["task_id"], "status": check["status"],
|
|
178
|
+
"evidence_refs": [hashlib.sha256(item.encode()).hexdigest() for item in references],
|
|
179
|
+
**({"blocking_reason": reason} if reason is not None else {})})
|
|
180
|
+
result.update(node_complete=True, contribution_check={"check_id": own["check_id"], "status": "conformant",
|
|
181
|
+
"evidence_refs": [hashlib.sha256(item.encode()).hexdigest() for item in evidence]}, pending_checks=normalized,
|
|
182
|
+
verification_pending=sorted({check["task_id"] for check in normalized}))
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def assess_approved_plan_conformance(baseline: dict, report: object, *, audit_contract: dict | None = None) -> dict:
|
|
78
187
|
result = {
|
|
79
188
|
"status": "unverified", "baseline_fingerprint": baseline.get("fingerprint"),
|
|
80
|
-
"task_id": (baseline.get("task") or {}).get("id"), "evidence_refs": [],
|
|
189
|
+
"task_id": (baseline.get("task") or {}).get("id"), "evidence_refs": [], "node_complete": False,
|
|
81
190
|
}
|
|
82
191
|
expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
|
|
83
192
|
fingerprint = hashlib.sha256(json.dumps(
|
|
@@ -109,8 +218,20 @@ def assess_approved_plan_conformance(baseline: dict, report: object) -> dict:
|
|
|
109
218
|
result["reason"] = "approved_plan_review_evidence_missing"
|
|
110
219
|
result["diagnostic_code"] = "approved_plan_review_evidence_missing"
|
|
111
220
|
return result
|
|
221
|
+
contribution = report.get("contribution_check")
|
|
222
|
+
if report["status"] == "conformant" and (
|
|
223
|
+
bool(report.get("pending_checks"))
|
|
224
|
+
or "pending_checks" in report and not isinstance(report["pending_checks"], list)
|
|
225
|
+
or contribution is not None and (not isinstance(contribution, dict) or contribution.get("status") != "conformant")
|
|
226
|
+
):
|
|
227
|
+
result["reason"] = "approved_plan_review_missing_or_mismatched"
|
|
228
|
+
result["diagnostic_code"] = "approved_plan_review_contribution_conflict"
|
|
229
|
+
return result
|
|
112
230
|
result["status"] = report["status"]
|
|
113
231
|
result["evidence_refs"] = [hashlib.sha256(item.encode()).hexdigest() for item in evidence]
|
|
232
|
+
result["node_complete"] = report["status"] == "conformant"
|
|
233
|
+
if report["status"] == "unverified" and report.get("contribution_check") is not None:
|
|
234
|
+
return _assess_contribution_conformance(baseline, report, audit_contract, result)
|
|
114
235
|
return result
|
|
115
236
|
|
|
116
237
|
|
|
@@ -238,6 +359,8 @@ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
|
|
|
238
359
|
check = {"status": row["status"], "evidence": evidence}
|
|
239
360
|
if row["status"] != "covered":
|
|
240
361
|
check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
|
|
362
|
+
if row.get("blocking_reason") in IMPLEMENTATION_ENVIRONMENT_BLOCKERS:
|
|
363
|
+
check["blocking_reason"] = row["blocking_reason"]
|
|
241
364
|
if row.get("plan_revision"):
|
|
242
365
|
check["plan_revision"] = {
|
|
243
366
|
"reason": "Local evidence conflicts with the approved plan",
|
|
@@ -281,7 +404,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
281
404
|
checks.append(row)
|
|
282
405
|
continue
|
|
283
406
|
check = {"task_id": row.get("task_id"), **{
|
|
284
|
-
key: implementation[key] for key in ("status", "evidence", "description", "files", "plan_revision")
|
|
407
|
+
key: implementation[key] for key in ("status", "evidence", "description", "files", "plan_revision", "blocking_reason")
|
|
285
408
|
if key in implementation
|
|
286
409
|
}}
|
|
287
410
|
if row.get("plan_revision") is not None and implementation.get("status") != "covered":
|
|
@@ -296,6 +419,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
296
419
|
"task_id": task_id, "status": row.get("status"),
|
|
297
420
|
"description": row.get("description") or "Runtime acceptance remains pending",
|
|
298
421
|
"files": [], "evidence": row.get("evidence") or [],
|
|
422
|
+
**({"blocking_reason": row["blocking_reason"]} if row.get("blocking_reason") else {}),
|
|
299
423
|
} for task_id in pending_verification_ids)
|
|
300
424
|
implementation_statuses = {row["task_id"]: row["status"] for row in checks}
|
|
301
425
|
deferred_contract = contract.get("deferred_acceptance_verification")
|
|
@@ -363,6 +487,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
363
487
|
pending.extend({
|
|
364
488
|
"task_id": task_id, "status": row["status"], "description": row["description"],
|
|
365
489
|
"files": row.get("files") or [], "evidence": row["evidence"],
|
|
490
|
+
**({"blocking_reason": row["blocking_reason"]} if row.get("blocking_reason") else {}),
|
|
366
491
|
} for task_id in pending_verification_ids)
|
|
367
492
|
acceptance_checks.append(row)
|
|
368
493
|
result = assess_implementation_audit({
|
|
@@ -443,6 +568,13 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
443
568
|
):
|
|
444
569
|
issues.append(f"Implementation {kind} check requires an explicit status and concrete evidence")
|
|
445
570
|
continue
|
|
571
|
+
blocking_reason = row.get("blocking_reason")
|
|
572
|
+
if blocking_reason is not None and (
|
|
573
|
+
not isinstance(blocking_reason, str) or blocking_reason not in IMPLEMENTATION_ENVIRONMENT_BLOCKERS
|
|
574
|
+
or status not in ("blocked", "unverified") or row.get("plan_revision") is not None
|
|
575
|
+
):
|
|
576
|
+
issues.append("Implementation blocking_reason must identify an allowed unmet environment prerequisite on a blocked or unverified check")
|
|
577
|
+
continue
|
|
446
578
|
if status == "covered" and row.get("plan_revision") is not None:
|
|
447
579
|
issues.append(
|
|
448
580
|
"A covered implementation check cannot request a plan revision; "
|
|
@@ -541,6 +673,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
541
673
|
"task_id": task_id, "status": status, "description": description,
|
|
542
674
|
"files": paths, "evidence": evidence,
|
|
543
675
|
**({"plan_revision": revision} if revision is not None else {}),
|
|
676
|
+
**({"blocking_reason": blocking_reason} if blocking_reason is not None else {}),
|
|
544
677
|
} for task_id in related)
|
|
545
678
|
required = expected if contract.get("final_verification") else {subject}
|
|
546
679
|
if required - checked.keys():
|
|
@@ -563,6 +696,57 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
563
696
|
return result
|
|
564
697
|
|
|
565
698
|
|
|
699
|
+
def implementation_audit_failure_domain(contract: dict, assessment: dict) -> str | None:
|
|
700
|
+
if not assessment.get("report_valid"):
|
|
701
|
+
return "evidence"
|
|
702
|
+
if assessment.get("node_complete"):
|
|
703
|
+
return None
|
|
704
|
+
if assessment.get("status") == "plan_revision_required":
|
|
705
|
+
return "plan"
|
|
706
|
+
subject = contract.get("subject_task_id")
|
|
707
|
+
findings = [finding for finding in assessment.get("findings") or [] if finding.get("task_id") == subject]
|
|
708
|
+
if findings and all(
|
|
709
|
+
finding.get("status") in ("blocked", "unverified")
|
|
710
|
+
and finding.get("blocking_reason") in IMPLEMENTATION_ENVIRONMENT_BLOCKERS for finding in findings
|
|
711
|
+
):
|
|
712
|
+
return "environment"
|
|
713
|
+
role = next((task.get("type") for task in contract.get("tasks") or [] if task.get("id") == subject), None)
|
|
714
|
+
if role in ("coding", "fix") and any(finding.get("status") == "gap" for finding in findings):
|
|
715
|
+
return "code_quality"
|
|
716
|
+
return "verification"
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def task_failure_domain(
|
|
720
|
+
failure_code: str | None, *, audit_contract: dict | None = None,
|
|
721
|
+
audit_assessment: dict | None = None, conformance: dict | None = None,
|
|
722
|
+
payload_failed: bool = False,
|
|
723
|
+
) -> str:
|
|
724
|
+
if failure_code in {
|
|
725
|
+
"planned_task_incomplete", "planned_task_baseline_invalid", "approved_plan_baseline_modified",
|
|
726
|
+
"remediation_scope_violation", "verification_work_item_scope_violation", "uncommitted_workspace_changes",
|
|
727
|
+
"forbidden_branch_merge",
|
|
728
|
+
}:
|
|
729
|
+
return "delivery"
|
|
730
|
+
if failure_code == "implementation_plan_revision_required":
|
|
731
|
+
return "plan"
|
|
732
|
+
if payload_failed:
|
|
733
|
+
return "execution"
|
|
734
|
+
if failure_code == "implementation_audit_incomplete":
|
|
735
|
+
return implementation_audit_failure_domain(audit_contract or {}, audit_assessment or {}) or "verification"
|
|
736
|
+
if failure_code in {"approved_plan_deviation", "approved_plan_conformance_unverified"}:
|
|
737
|
+
assessment = conformance or {}
|
|
738
|
+
if assessment.get("reason") == "approved_plan_review_unavailable":
|
|
739
|
+
return "execution"
|
|
740
|
+
if assessment.get("reason") or assessment.get("diagnostic_code"):
|
|
741
|
+
return "evidence"
|
|
742
|
+
return "code_quality" if assessment.get("status") == "deviation" else "verification"
|
|
743
|
+
if failure_code == "acceptance_criteria_uncovered":
|
|
744
|
+
return "verification"
|
|
745
|
+
if failure_code in {"validation_unavailable", "validation_retry_exhausted"}:
|
|
746
|
+
return "evidence"
|
|
747
|
+
return "execution"
|
|
748
|
+
|
|
749
|
+
|
|
566
750
|
_WORKSPACE_INSTRUCTION_PATHS = (
|
|
567
751
|
"AGENTS.md",
|
|
568
752
|
"CLAUDE.md",
|
|
@@ -1889,7 +2073,7 @@ def has_agent_token_usage(*sources: object) -> bool:
|
|
|
1889
2073
|
return False
|
|
1890
2074
|
|
|
1891
2075
|
|
|
1892
|
-
def _extract_output_signals(text: str) -> dict[str, Any]:
|
|
2076
|
+
def _extract_output_signals(text: str, usage_source: object = None) -> dict[str, Any]:
|
|
1893
2077
|
"""Parse stdout/stderr-like streams for success and failure signals.
|
|
1894
2078
|
|
|
1895
2079
|
Uses structural signal detection — NOT keyword matching on result text.
|
|
@@ -1933,6 +2117,8 @@ def _extract_output_signals(text: str) -> dict[str, Any]:
|
|
|
1933
2117
|
|
|
1934
2118
|
if ev_type == "turn.completed":
|
|
1935
2119
|
has_turn_completed = True
|
|
2120
|
+
has_turn_failed = False
|
|
2121
|
+
error_messages.clear()
|
|
1936
2122
|
elif ev_type == "turn.failed":
|
|
1937
2123
|
has_turn_failed = True
|
|
1938
2124
|
err = data.get("error") or {}
|
|
@@ -1962,10 +2148,18 @@ def _extract_output_signals(text: str) -> dict[str, Any]:
|
|
|
1962
2148
|
# Claude result format: {"type":"result","result":"...","is_error":false,...}
|
|
1963
2149
|
if "exitCode" in data:
|
|
1964
2150
|
# Copilot JSONL result
|
|
1965
|
-
|
|
2151
|
+
raw_exit_code = data["exitCode"]
|
|
2152
|
+
if type(raw_exit_code) is not int and not (
|
|
2153
|
+
isinstance(raw_exit_code, str) and re.fullmatch(r"[+-]?[0-9]+", raw_exit_code)
|
|
2154
|
+
):
|
|
2155
|
+
error_messages.append("Agent result has an invalid exitCode")
|
|
2156
|
+
continue
|
|
2157
|
+
exit_code = int(raw_exit_code)
|
|
1966
2158
|
if exit_code == 0:
|
|
1967
2159
|
has_result = True
|
|
1968
2160
|
has_meaningful_content = True
|
|
2161
|
+
has_turn_failed = False
|
|
2162
|
+
error_messages.clear()
|
|
1969
2163
|
else:
|
|
1970
2164
|
error_messages.append(f"Copilot exited with code {exit_code}")
|
|
1971
2165
|
else:
|
|
@@ -1996,12 +2190,14 @@ def _extract_output_signals(text: str) -> dict[str, Any]:
|
|
|
1996
2190
|
# events appeared, the CLI never made an API call. The result
|
|
1997
2191
|
# text is a CLI-level error (e.g. "API Error: Connection error.")
|
|
1998
2192
|
# rather than the agent's actual work output.
|
|
1999
|
-
no_api_call = not has_agent_token_usage(data) and not has_assistant_events
|
|
2193
|
+
no_api_call = not has_agent_token_usage(data, usage_source) and not has_assistant_events
|
|
2000
2194
|
if no_api_call and result_text:
|
|
2001
2195
|
error_messages.append(result_text)
|
|
2002
2196
|
else:
|
|
2003
2197
|
has_result = True
|
|
2004
2198
|
has_meaningful_content = True
|
|
2199
|
+
has_turn_failed = False
|
|
2200
|
+
error_messages.clear()
|
|
2005
2201
|
elif ev_type == "session.error":
|
|
2006
2202
|
err = data.get("data") or {}
|
|
2007
2203
|
if isinstance(err, dict):
|
|
@@ -4988,6 +5184,62 @@ def is_copilot_model_discovery_auth_error(*messages: str | None) -> bool:
|
|
|
4988
5184
|
)
|
|
4989
5185
|
|
|
4990
5186
|
|
|
5187
|
+
# Transport failures a plain re-run can recover from. 421 Misdirected Request
|
|
5188
|
+
# is deliberately absent: it keeps its own recovery (retry without token env
|
|
5189
|
+
# vars) because it has historically indicated a credential-routing mismatch,
|
|
5190
|
+
# not a network blip.
|
|
5191
|
+
COPILOT_TRANSIENT_TRANSPORT_ERROR_PATTERNS = (
|
|
5192
|
+
"timed out",
|
|
5193
|
+
"timeout",
|
|
5194
|
+
"unexpected eof",
|
|
5195
|
+
"error sending request",
|
|
5196
|
+
"connection reset",
|
|
5197
|
+
"connection refused",
|
|
5198
|
+
"connection timed out",
|
|
5199
|
+
"connection error",
|
|
5200
|
+
"connection closed",
|
|
5201
|
+
"connection terminated",
|
|
5202
|
+
"network is unreachable",
|
|
5203
|
+
"no such host",
|
|
5204
|
+
"temporary failure in name resolution",
|
|
5205
|
+
)
|
|
5206
|
+
|
|
5207
|
+
COPILOT_STABLE_AUTH_ERROR_PATTERNS = (
|
|
5208
|
+
"unauthorized",
|
|
5209
|
+
"forbidden",
|
|
5210
|
+
"http 401",
|
|
5211
|
+
"http 403",
|
|
5212
|
+
"not logged in",
|
|
5213
|
+
"authentication required",
|
|
5214
|
+
"invalid api key",
|
|
5215
|
+
"your github token may be invalid",
|
|
5216
|
+
"rejected the cli github credential",
|
|
5217
|
+
)
|
|
5218
|
+
|
|
5219
|
+
|
|
5220
|
+
def is_copilot_model_discovery_transient_transport_error(*messages: str | None) -> bool:
|
|
5221
|
+
"""Return whether Copilot failed model discovery on a transient transport error.
|
|
5222
|
+
|
|
5223
|
+
Matches connect timeouts, unexpected EOF, resets and DNS hiccups during
|
|
5224
|
+
model discovery — the class of failures observed as intermittent on
|
|
5225
|
+
unstable routes (a daemon-side rerun typically succeeds, and the same
|
|
5226
|
+
login works interactively), as opposed to stable auth/config breakage.
|
|
5227
|
+
Copilot appends its remediation boilerplate ("To resolve this, ... run
|
|
5228
|
+
'/login' ...") to every discovery failure, so auth patterns are only
|
|
5229
|
+
trusted when they appear before that boilerplate.
|
|
5230
|
+
"""
|
|
5231
|
+
detail = " ".join(str(message or "").lower() for message in messages)
|
|
5232
|
+
detail = detail.partition("to resolve this")[0]
|
|
5233
|
+
if not (
|
|
5234
|
+
"retrieve the list of available models" in detail
|
|
5235
|
+
or "failed to load models" in detail
|
|
5236
|
+
):
|
|
5237
|
+
return False
|
|
5238
|
+
if any(pattern in detail for pattern in COPILOT_STABLE_AUTH_ERROR_PATTERNS):
|
|
5239
|
+
return False
|
|
5240
|
+
return any(pattern in detail for pattern in COPILOT_TRANSIENT_TRANSPORT_ERROR_PATTERNS)
|
|
5241
|
+
|
|
5242
|
+
|
|
4991
5243
|
def is_runtime_resource_exhaustion_error(*messages: str | None) -> bool:
|
|
4992
5244
|
"""Return whether the OS refused an agent process, thread, or file watch."""
|
|
4993
5245
|
detail = " ".join(str(message or "").lower() for message in messages)
|
|
@@ -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.85.
|
|
1237
|
+
DAEMON_VERSION = "1.85.3"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -1809,6 +1809,7 @@ def _build_runtime_critic_prompt(
|
|
|
1809
1809
|
"failed checks remain blocking. State the concrete missing evidence; never invent a run, "
|
|
1810
1810
|
"treat asset existence as proof of correctness, or claim final QA or release acceptance."
|
|
1811
1811
|
)
|
|
1812
|
+
parts.append(agent_core.approved_contribution_review_instructions(baseline, audit_contract))
|
|
1812
1813
|
else:
|
|
1813
1814
|
planned_context = {
|
|
1814
1815
|
key: (task.input_data or {}).get(key)
|
|
@@ -6439,8 +6440,8 @@ class ProcessManager:
|
|
|
6439
6440
|
int(result.metrics.get("token_input", 0) or 0)
|
|
6440
6441
|
+ int(result.metrics.get("token_output", 0) or 0)
|
|
6441
6442
|
) > 0
|
|
6442
|
-
signals =
|
|
6443
|
-
"\n".join(part for part in (stdout, stderr) if part)
|
|
6443
|
+
signals = agent_core._extract_output_signals(
|
|
6444
|
+
"\n".join(part for part in (stdout, stderr) if part), result.metrics,
|
|
6444
6445
|
)
|
|
6445
6446
|
if not has_token_usage and ProcessManager._should_scan_short_success_stdout(stdout, signals):
|
|
6446
6447
|
error_channels = "\n".join(filter(None, [error_channels, stdout.strip()]))
|
|
@@ -8484,15 +8485,18 @@ class ProcessManager:
|
|
|
8484
8485
|
|
|
8485
8486
|
model_discovery_detail = "\n".join([*structured_errors, stderr]).lower()
|
|
8486
8487
|
if (
|
|
8487
|
-
|
|
8488
|
-
|
|
8488
|
+
agent_core.is_copilot_model_discovery_transient_transport_error(
|
|
8489
|
+
*structured_errors, stderr,
|
|
8490
|
+
)
|
|
8491
|
+
and "misdirected request" not in model_discovery_detail
|
|
8489
8492
|
and not _retry_model_discovery_timeout
|
|
8490
8493
|
and not (copilot_exit == 0 and returncode == 0)
|
|
8491
8494
|
and not signals["has_meaningful_content"]
|
|
8492
8495
|
and not self._copilot_called_any_tools(stdout)
|
|
8493
8496
|
):
|
|
8494
8497
|
logger.warning(
|
|
8495
|
-
"Copilot model discovery
|
|
8498
|
+
"Copilot model discovery hit a transient transport error "
|
|
8499
|
+
"(timeout / connect reset) before work for task %s; "
|
|
8496
8500
|
"retrying once with the same credentials and configuration",
|
|
8497
8501
|
task_id,
|
|
8498
8502
|
)
|
|
@@ -14289,7 +14293,8 @@ class RuntimeDaemon:
|
|
|
14289
14293
|
**metadata,
|
|
14290
14294
|
}
|
|
14291
14295
|
conformance = (
|
|
14292
|
-
agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance")
|
|
14296
|
+
agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance"),
|
|
14297
|
+
audit_contract=(task.input_data or {}).get("implementation_audit_contract"))
|
|
14293
14298
|
if isinstance(baseline, dict) else None
|
|
14294
14299
|
)
|
|
14295
14300
|
if conformance is not None and conformance["status"] != "conformant":
|
|
@@ -14317,6 +14322,15 @@ class RuntimeDaemon:
|
|
|
14317
14322
|
"evidence": conformance["evidence_refs"],
|
|
14318
14323
|
},
|
|
14319
14324
|
}
|
|
14325
|
+
if conformance.get("node_complete") and conformance["status"] == "unverified":
|
|
14326
|
+
contribution = conformance["contribution_check"]
|
|
14327
|
+
structured_result["plan_conformance"]["contribution_check"] = {
|
|
14328
|
+
"check_id": contribution["check_id"], "status": contribution["status"], "evidence": contribution["evidence_refs"],
|
|
14329
|
+
}
|
|
14330
|
+
structured_result["plan_conformance"]["pending_checks"] = [{
|
|
14331
|
+
"check_id": check["check_id"], "task_id": check["task_id"], "status": check["status"], "evidence": check["evidence_refs"],
|
|
14332
|
+
**({"blocking_reason": check["blocking_reason"]} if "blocking_reason" in check else {}),
|
|
14333
|
+
} for check in conformance["pending_checks"]]
|
|
14320
14334
|
encoded_result = json.dumps(
|
|
14321
14335
|
structured_result,
|
|
14322
14336
|
ensure_ascii=True,
|
|
@@ -14466,6 +14480,7 @@ class RuntimeDaemon:
|
|
|
14466
14480
|
audit_contract = input_data.get("implementation_audit_contract")
|
|
14467
14481
|
result.metrics.pop("implementation_audit_node_complete", None)
|
|
14468
14482
|
result.metrics.pop("implementation_audit_subject_blocked", None)
|
|
14483
|
+
result.metrics.pop("failure_domain", None)
|
|
14469
14484
|
if isinstance(audit_contract, dict):
|
|
14470
14485
|
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
14471
14486
|
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
@@ -14476,13 +14491,17 @@ class RuntimeDaemon:
|
|
|
14476
14491
|
if isinstance(audit_report, dict):
|
|
14477
14492
|
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
14478
14493
|
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
14494
|
+
failure_domain = agent_core.implementation_audit_failure_domain(audit_contract, audit_result)
|
|
14495
|
+
if failure_domain is not None:
|
|
14496
|
+
result.metrics["failure_domain"] = failure_domain
|
|
14479
14497
|
result.metrics["implementation_audit_node_complete"] = bool(
|
|
14480
14498
|
audit_result["report_valid"] and audit_result["node_complete"]
|
|
14481
14499
|
)
|
|
14482
14500
|
result.metrics["implementation_audit_subject_blocked"] = bool(
|
|
14483
14501
|
audit_result["report_valid"] and not audit_result["node_complete"]
|
|
14484
|
-
and
|
|
14485
|
-
|
|
14502
|
+
and (failure_domain == "environment" or any(
|
|
14503
|
+
finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
|
|
14504
|
+
for finding in audit_result.get("findings") or []))
|
|
14486
14505
|
)
|
|
14487
14506
|
if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
|
|
14488
14507
|
result.status = "failed"
|
|
@@ -14504,6 +14523,7 @@ class RuntimeDaemon:
|
|
|
14504
14523
|
delivered_files = list(result.files_changed)
|
|
14505
14524
|
delivered_files.extend(input_data.get("planned_task_predecessor_files") or [])
|
|
14506
14525
|
for missing_path in agent_core.missing_planned_task_files(input_data, delivered_files):
|
|
14526
|
+
result.metrics["failure_domain"] = "delivery"
|
|
14507
14527
|
issues.append(_OutputValidationIssue(
|
|
14508
14528
|
"planned_task_incomplete",
|
|
14509
14529
|
f"Planned task file has no implementation evidence: {missing_path}. "
|
|
@@ -16508,7 +16528,10 @@ class RuntimeDaemon:
|
|
|
16508
16528
|
])
|
|
16509
16529
|
|
|
16510
16530
|
if (
|
|
16511
|
-
|
|
16531
|
+
result.metrics.get("failure_domain") == "environment" and all(
|
|
16532
|
+
issue.code in {"implementation_audit_incomplete", "acceptance_criteria_uncovered"} for issue in issues
|
|
16533
|
+
)
|
|
16534
|
+
or agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16512
16535
|
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16513
16536
|
):
|
|
16514
16537
|
result.status = "failed"
|
|
@@ -16711,7 +16734,8 @@ class RuntimeDaemon:
|
|
|
16711
16734
|
result.failure_code = "implementation_audit_incomplete"
|
|
16712
16735
|
result.error = "; ".join(remaining[:3])
|
|
16713
16736
|
if (
|
|
16714
|
-
|
|
16737
|
+
result.metrics.get("failure_domain") == "environment"
|
|
16738
|
+
or agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16715
16739
|
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16716
16740
|
):
|
|
16717
16741
|
result.error += ". Resolve the reported verification prerequisites before retrying."
|
|
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
|