forgexa-cli 1.77.0__tar.gz → 1.77.2__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.77.0 → forgexa_cli-1.77.2}/PKG-INFO +1 -1
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/agent_core.py +96 -15
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/daemon.py +18 -6
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/runtime_evidence.py +2 -2
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/pyproject.toml +1 -1
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/README.md +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/setup.cfg +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.77.
|
|
2
|
+
__version__ = "1.77.2"
|
|
@@ -67,6 +67,15 @@ except ImportError:
|
|
|
67
67
|
logger = logging.getLogger("agent_core")
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def is_implementation_testing_task(node_type: str, input_data: dict) -> bool:
|
|
71
|
+
"""Retain the testing responsibility of a phase-remediation wrapper."""
|
|
72
|
+
fix_context = input_data.get("fix_context")
|
|
73
|
+
return node_type == "testing" or (
|
|
74
|
+
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
75
|
+
and isinstance(fix_context, dict) and fix_context.get("failed_node_type") == "testing"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
70
79
|
def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> list[str]:
|
|
71
80
|
"""Find planned files without delivery evidence, including grouped sub-tasks."""
|
|
72
81
|
def normalize_path(value: object) -> str | None:
|
|
@@ -133,40 +142,104 @@ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
|
|
|
133
142
|
|
|
134
143
|
if not assess_implementation_audit(contract, report)["report_valid"]:
|
|
135
144
|
return None
|
|
145
|
+
def redact_check(row: dict) -> dict:
|
|
146
|
+
evidence = []
|
|
147
|
+
for reference in row["evidence"][:20]:
|
|
148
|
+
digest = reference.removeprefix("local-evidence-sha256:")
|
|
149
|
+
evidence.append(reference if digest != reference and len(digest) == 64
|
|
150
|
+
and all(character in "0123456789abcdef" for character in digest)
|
|
151
|
+
else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
|
|
152
|
+
check = {"status": row["status"], "evidence": evidence}
|
|
153
|
+
if row["status"] != "covered":
|
|
154
|
+
check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
|
|
155
|
+
if row.get("plan_revision"):
|
|
156
|
+
check["plan_revision"] = {
|
|
157
|
+
"reason": "Local evidence conflicts with the approved plan",
|
|
158
|
+
"proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
|
|
159
|
+
}
|
|
160
|
+
return check
|
|
161
|
+
|
|
136
162
|
summary = {"audit_id": contract["audit_id"], "task_checks": [], "acceptance_checks": []}
|
|
163
|
+
coding_ids = {task["id"] for task in contract.get("tasks") or [] if task.get("type") in ("coding", "fix")}
|
|
137
164
|
for key in ("task_checks", "acceptance_checks"):
|
|
138
165
|
for row in report[key]:
|
|
139
|
-
|
|
140
|
-
for reference in row["evidence"][:20]:
|
|
141
|
-
digest = reference.removeprefix("local-evidence-sha256:")
|
|
142
|
-
evidence.append(reference if digest != reference and len(digest) == 64
|
|
143
|
-
and all(character in "0123456789abcdef" for character in digest)
|
|
144
|
-
else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
|
|
145
|
-
check = {"status": row["status"], "evidence": evidence}
|
|
166
|
+
check = redact_check(row)
|
|
146
167
|
if key == "task_checks":
|
|
147
168
|
check["task_id"] = row["task_id"]
|
|
169
|
+
if contract.get("version") == 2 and row["task_id"] in coding_ids and isinstance(row.get("implementation_check"), dict):
|
|
170
|
+
check["implementation_check"] = redact_check(row["implementation_check"])
|
|
148
171
|
else:
|
|
149
172
|
check.update(index=row["index"], task_ids=row["task_ids"])
|
|
150
|
-
if row["status"] != "covered":
|
|
151
|
-
check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
|
|
152
|
-
if row.get("plan_revision"):
|
|
153
|
-
check["plan_revision"] = {
|
|
154
|
-
"reason": "Local evidence conflicts with the approved plan",
|
|
155
|
-
"proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
|
|
156
|
-
}
|
|
157
173
|
summary[key].append(check)
|
|
158
174
|
return summary
|
|
159
175
|
|
|
160
176
|
|
|
177
|
+
def _assess_implementation_contributions(contract: dict, report: object) -> dict:
|
|
178
|
+
original = assess_implementation_audit({**contract, "version": 1}, report)
|
|
179
|
+
if not original["report_valid"]:
|
|
180
|
+
return original
|
|
181
|
+
task_types = {task["id"]: task.get("type") for task in contract.get("tasks") or []}
|
|
182
|
+
verification_ids = [task_id for task_id, task_type in task_types.items() if task_type in ("testing", "verify")]
|
|
183
|
+
verification_statuses = {row["task_id"]: row["status"] for row in report["task_checks"]}
|
|
184
|
+
checks = []
|
|
185
|
+
issues = []
|
|
186
|
+
pending = []
|
|
187
|
+
for row in report["task_checks"]:
|
|
188
|
+
if not isinstance(row, dict) or task_types.get(row.get("task_id")) not in ("coding", "fix"):
|
|
189
|
+
checks.append(row)
|
|
190
|
+
continue
|
|
191
|
+
implementation = row.get("implementation_check")
|
|
192
|
+
if not isinstance(implementation, dict):
|
|
193
|
+
issues.append(f"Approved coding task '{row.get('task_id')}' requires an evidenced implementation_check")
|
|
194
|
+
checks.append(row)
|
|
195
|
+
continue
|
|
196
|
+
check = {"task_id": row.get("task_id"), **{
|
|
197
|
+
key: implementation[key] for key in ("status", "evidence", "description", "files", "plan_revision")
|
|
198
|
+
if key in implementation
|
|
199
|
+
}}
|
|
200
|
+
if row.get("plan_revision") is not None:
|
|
201
|
+
check["plan_revision"] = row["plan_revision"]
|
|
202
|
+
checks.append(check)
|
|
203
|
+
if implementation.get("status") == "covered" and row.get("status") == "gap":
|
|
204
|
+
issues.append(f"Approved task '{row.get('task_id')}' reports a gap despite claiming implementation coverage")
|
|
205
|
+
if implementation.get("status") == "covered" and row.get("status") in ("blocked", "unverified"):
|
|
206
|
+
if not verification_ids:
|
|
207
|
+
issues.append("Runtime acceptance is pending but the approved plan has no verification task")
|
|
208
|
+
pending.extend({
|
|
209
|
+
"task_id": task_id, "status": row.get("status"),
|
|
210
|
+
"description": row.get("description") or "Runtime acceptance remains pending",
|
|
211
|
+
"files": [], "evidence": row.get("evidence") or [],
|
|
212
|
+
} for task_id in verification_ids if verification_statuses.get(task_id) != "covered")
|
|
213
|
+
result = assess_implementation_audit({**contract, "version": 1}, {**report, "task_checks": checks})
|
|
214
|
+
result["report"] = report
|
|
215
|
+
result["issues"].extend(issues)
|
|
216
|
+
result["report_valid"] = not result["issues"]
|
|
217
|
+
result["node_complete"] = result["node_complete"] and not issues
|
|
218
|
+
result["verification_pending"] = sorted({item["task_id"] for item in pending})
|
|
219
|
+
result["findings"].extend(pending)
|
|
220
|
+
for request in original["revision_requests"]:
|
|
221
|
+
if request not in result["revision_requests"]:
|
|
222
|
+
result["revision_requests"].append(request)
|
|
223
|
+
result["followup_task_ids"] = sorted({finding["task_id"] for finding in result["findings"]})
|
|
224
|
+
if issues or pending:
|
|
225
|
+
result["status"] = "incomplete"
|
|
226
|
+
if result["report_valid"] and result["revision_requests"]:
|
|
227
|
+
result["status"] = "plan_revision_required"
|
|
228
|
+
result["node_complete"] = False
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
|
|
161
232
|
def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
162
233
|
"""Validate implementation coverage independently of later runtime acceptance."""
|
|
234
|
+
if contract.get("version") == 2:
|
|
235
|
+
return _assess_implementation_contributions(contract, report)
|
|
163
236
|
issues: list[str] = []
|
|
164
237
|
findings: list[dict] = []
|
|
165
238
|
result = {
|
|
166
239
|
"audit_id": contract.get("audit_id"), "report_valid": False,
|
|
167
240
|
"node_complete": False, "status": "incomplete", "issues": issues,
|
|
168
241
|
"findings": findings, "followup_task_ids": [], "report": report,
|
|
169
|
-
"revision_requests": [],
|
|
242
|
+
"revision_requests": [], "blocking_reasons": [],
|
|
170
243
|
}
|
|
171
244
|
if (
|
|
172
245
|
not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
|
|
@@ -226,6 +299,10 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
226
299
|
issues.append("Implementation audit has invalid acceptance criterion or task references")
|
|
227
300
|
continue
|
|
228
301
|
checked_criteria.add(index)
|
|
302
|
+
if len(related) > 1:
|
|
303
|
+
unresolved_contributors = [task_id for task_id in related if checked.get(task_id) != "covered"]
|
|
304
|
+
if unresolved_contributors:
|
|
305
|
+
related = unresolved_contributors
|
|
229
306
|
if status != "covered":
|
|
230
307
|
description = row.get("description")
|
|
231
308
|
paths = row.get("files") or []
|
|
@@ -266,6 +343,10 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
266
343
|
not issues and checked.get(subject) == "covered"
|
|
267
344
|
and not any(finding["task_id"] == subject for finding in findings)
|
|
268
345
|
)
|
|
346
|
+
result["blocking_reasons"] = list(dict.fromkeys(
|
|
347
|
+
f"Approved task '{subject}' is {finding['status']}: {finding['description']}"
|
|
348
|
+
for finding in findings if finding["task_id"] == subject
|
|
349
|
+
))
|
|
269
350
|
result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
|
|
270
351
|
result["status"] = "complete" if not issues and not findings else "incomplete"
|
|
271
352
|
if result["report_valid"] and result["revision_requests"]:
|
|
@@ -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.77.
|
|
1237
|
+
DAEMON_VERSION = "1.77.2"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -9915,6 +9915,7 @@ class ServerConnection:
|
|
|
9915
9915
|
"acceptance_verification": {
|
|
9916
9916
|
"protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
|
|
9917
9917
|
},
|
|
9918
|
+
"implementation_audit": {"protocol_version": 2},
|
|
9918
9919
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
9919
9920
|
},
|
|
9920
9921
|
"server_url": self.server_url,
|
|
@@ -10338,6 +10339,7 @@ class ServerConnection:
|
|
|
10338
10339
|
"acceptance_verification": {
|
|
10339
10340
|
"protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
|
|
10340
10341
|
},
|
|
10342
|
+
"implementation_audit": {"protocol_version": 2},
|
|
10341
10343
|
"local_evidence": {"protocol_version": 1, "structured_summary": True},
|
|
10342
10344
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
10343
10345
|
},
|
|
@@ -13875,6 +13877,12 @@ class RuntimeDaemon:
|
|
|
13875
13877
|
issues: list[str] = []
|
|
13876
13878
|
node_type = task.node_type
|
|
13877
13879
|
input_data = task.input_data or {}
|
|
13880
|
+
fix_context = input_data.get("fix_context")
|
|
13881
|
+
if node_type == "testing" or (
|
|
13882
|
+
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
13883
|
+
and isinstance(fix_context, dict) and fix_context.get("failed_node_type") == "testing"
|
|
13884
|
+
):
|
|
13885
|
+
node_type = "testing"
|
|
13878
13886
|
req_type = input_data.get("requirement_type", "feature")
|
|
13879
13887
|
verification_work_item = bool(input_data.get("verification_work_item"))
|
|
13880
13888
|
|
|
@@ -13895,7 +13903,9 @@ class RuntimeDaemon:
|
|
|
13895
13903
|
result.error = "Approved analysis and task plan require revision before implementation can continue"
|
|
13896
13904
|
issues.append(result.error)
|
|
13897
13905
|
elif not audit_result["report_valid"] or not audit_result["node_complete"]:
|
|
13898
|
-
issues.extend(audit_result["issues"] or
|
|
13906
|
+
issues.extend(audit_result["issues"] or audit_result.get("blocking_reasons") or [
|
|
13907
|
+
"The selected approved task still has implementation gaps",
|
|
13908
|
+
])
|
|
13899
13909
|
|
|
13900
13910
|
if node_type == "coding" or (
|
|
13901
13911
|
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
@@ -15880,7 +15890,7 @@ class RuntimeDaemon:
|
|
|
15880
15890
|
])
|
|
15881
15891
|
|
|
15882
15892
|
if (
|
|
15883
|
-
task.node_type
|
|
15893
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
15884
15894
|
and all(
|
|
15885
15895
|
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
15886
15896
|
for issue in issues
|
|
@@ -16003,7 +16013,7 @@ class RuntimeDaemon:
|
|
|
16003
16013
|
*[f"[daemon] {issue}" for issue in remaining],
|
|
16004
16014
|
])
|
|
16005
16015
|
if (
|
|
16006
|
-
task.node_type
|
|
16016
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16007
16017
|
and all(
|
|
16008
16018
|
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
16009
16019
|
for issue in remaining
|
|
@@ -16162,7 +16172,7 @@ class RuntimeDaemon:
|
|
|
16162
16172
|
result.metrics["validation_issue_categories"] = sorted(
|
|
16163
16173
|
{_categorize_validation_issue(str(issue)) for issue in issues}
|
|
16164
16174
|
)
|
|
16165
|
-
if task.node_type
|
|
16175
|
+
if not agent_core.is_implementation_testing_task(task.node_type, task.input_data or {}):
|
|
16166
16176
|
return
|
|
16167
16177
|
input_data = task.input_data or {}
|
|
16168
16178
|
context = input_data.get("context") or {}
|
|
@@ -17616,7 +17626,9 @@ class RuntimeDaemon:
|
|
|
17616
17626
|
before_sha: str,
|
|
17617
17627
|
) -> None:
|
|
17618
17628
|
"""Bind generated testing artifacts to the committed testing attempt."""
|
|
17619
|
-
if task.workspace_resolution != "cloud_worktree" or
|
|
17629
|
+
if task.workspace_resolution != "cloud_worktree" or not agent_core.is_implementation_testing_task(
|
|
17630
|
+
task.node_type, task.input_data or {},
|
|
17631
|
+
):
|
|
17620
17632
|
return
|
|
17621
17633
|
|
|
17622
17634
|
after_sha = str((result.git or {}).get("commit_sha") or "").strip()
|
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
17
|
RUNTIME_EVIDENCE_PLAN_VERSION = 1
|
|
18
|
-
DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION =
|
|
18
|
+
DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION = 3
|
|
19
19
|
TEST_COVERAGE_CHECK_KIND = "test_coverage"
|
|
20
20
|
STATIC_CHECK_KIND = "static_check"
|
|
21
21
|
TYPE_CHECK_KIND = "type_check"
|
|
@@ -804,7 +804,7 @@ def validate_deferred_acceptance_criteria(
|
|
|
804
804
|
or policy.get("enforcement") != "enforce"
|
|
805
805
|
or not isinstance(e2e_policy, dict)
|
|
806
806
|
or type(contract.get("version")) is not int
|
|
807
|
-
or contract["version"] not in (1, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
|
|
807
|
+
or contract["version"] not in (1, 2, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
|
|
808
808
|
or contract.get("execution_phase") != "verification_final"
|
|
809
809
|
or not isinstance(snapshot.get("fingerprint"), str)
|
|
810
810
|
or contract.get("qa_test_level_policy_fingerprint") != snapshot["fingerprint"]
|
|
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
|