forgexa-cli 1.76.2__tar.gz → 1.76.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.76.2 → forgexa_cli-1.76.3}/PKG-INFO +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/agent_core.py +93 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/daemon.py +48 -3
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/runtime_evidence.py +80 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/pyproject.toml +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/README.md +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/setup.cfg +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.76.3}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.76.
|
|
2
|
+
__version__ = "1.76.3"
|
|
@@ -103,6 +103,99 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
107
|
+
"""Validate implementation coverage independently of later runtime acceptance."""
|
|
108
|
+
issues: list[str] = []
|
|
109
|
+
findings: list[dict] = []
|
|
110
|
+
result = {
|
|
111
|
+
"audit_id": contract.get("audit_id"), "report_valid": False,
|
|
112
|
+
"node_complete": False, "status": "incomplete", "issues": issues,
|
|
113
|
+
"findings": findings, "followup_task_ids": [], "report": report,
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
|
|
117
|
+
or not isinstance(report, dict) or report.get("audit_id") != contract["audit_id"]
|
|
118
|
+
):
|
|
119
|
+
issues.append("Implementation audit is missing or belongs to another remediation task")
|
|
120
|
+
return result
|
|
121
|
+
if set(report) - {"audit_id", "task_checks", "acceptance_checks"}:
|
|
122
|
+
issues.append("Implementation audit contains unsupported fields")
|
|
123
|
+
expected = {task["id"] for task in contract.get("tasks") or []}
|
|
124
|
+
subject = contract.get("subject_task_id")
|
|
125
|
+
if not expected or subject not in expected:
|
|
126
|
+
issues.append("Implementation audit has no approved task baseline")
|
|
127
|
+
task_checks = report.get("task_checks")
|
|
128
|
+
acceptance_checks = report.get("acceptance_checks")
|
|
129
|
+
if not isinstance(task_checks, list) or not isinstance(acceptance_checks, list):
|
|
130
|
+
issues.append("Implementation audit requires task_checks and acceptance_checks lists")
|
|
131
|
+
return result
|
|
132
|
+
checked: dict[str, str] = {}
|
|
133
|
+
checked_criteria: set[int] = set()
|
|
134
|
+
criterion_count = len(contract.get("acceptance_criteria") or [])
|
|
135
|
+
for kind, rows in (("task", task_checks), ("acceptance", acceptance_checks)):
|
|
136
|
+
for row in rows:
|
|
137
|
+
if not isinstance(row, dict):
|
|
138
|
+
issues.append(f"Invalid implementation {kind} check")
|
|
139
|
+
continue
|
|
140
|
+
evidence = row.get("evidence")
|
|
141
|
+
status = row.get("status")
|
|
142
|
+
if (
|
|
143
|
+
not isinstance(status, str) or status not in {"covered", "gap", "blocked", "unverified"}
|
|
144
|
+
or not isinstance(evidence, list) or not evidence
|
|
145
|
+
or any(not isinstance(item, str) or not item.strip() for item in evidence)
|
|
146
|
+
):
|
|
147
|
+
issues.append(f"Implementation {kind} check requires an explicit status and concrete evidence")
|
|
148
|
+
continue
|
|
149
|
+
if kind == "task":
|
|
150
|
+
task_id = row.get("task_id")
|
|
151
|
+
if not isinstance(task_id, str) or task_id not in expected or task_id in checked:
|
|
152
|
+
issues.append("Implementation audit has unknown or duplicate task IDs")
|
|
153
|
+
continue
|
|
154
|
+
checked[task_id] = status
|
|
155
|
+
related = [task_id]
|
|
156
|
+
else:
|
|
157
|
+
index = row.get("index")
|
|
158
|
+
related = row.get("task_ids")
|
|
159
|
+
if (
|
|
160
|
+
type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
|
|
161
|
+
or not isinstance(related, list) or not related
|
|
162
|
+
or any(not isinstance(task_id, str) or task_id not in expected for task_id in related)
|
|
163
|
+
):
|
|
164
|
+
issues.append("Implementation audit has invalid acceptance criterion or task references")
|
|
165
|
+
continue
|
|
166
|
+
checked_criteria.add(index)
|
|
167
|
+
if status != "covered":
|
|
168
|
+
description = row.get("description")
|
|
169
|
+
paths = row.get("files") or []
|
|
170
|
+
if not isinstance(description, str) or not description.strip() or not isinstance(paths, list):
|
|
171
|
+
issues.append("Implementation gaps require an actionable description and file list")
|
|
172
|
+
continue
|
|
173
|
+
if any(
|
|
174
|
+
not isinstance(path, str) or not path.strip()
|
|
175
|
+
or PurePosixPath(path).is_absolute() or ".." in PurePosixPath(path).parts
|
|
176
|
+
for path in paths
|
|
177
|
+
):
|
|
178
|
+
issues.append("Implementation gap paths must be repository-relative")
|
|
179
|
+
continue
|
|
180
|
+
findings.extend({
|
|
181
|
+
"task_id": task_id, "status": status, "description": description,
|
|
182
|
+
"files": paths, "evidence": evidence,
|
|
183
|
+
} for task_id in related)
|
|
184
|
+
required = expected if contract.get("final_verification") else {subject}
|
|
185
|
+
if required - checked.keys():
|
|
186
|
+
issues.append("Implementation audit omitted required approved tasks")
|
|
187
|
+
if contract.get("final_verification") and len(checked_criteria) != criterion_count:
|
|
188
|
+
issues.append("Implementation audit omitted acceptance criteria")
|
|
189
|
+
result["report_valid"] = not issues
|
|
190
|
+
result["node_complete"] = (
|
|
191
|
+
not issues and checked.get(subject) == "covered"
|
|
192
|
+
and not any(finding["task_id"] == subject for finding in findings)
|
|
193
|
+
)
|
|
194
|
+
result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
|
|
195
|
+
result["status"] = "complete" if not issues and not findings else "incomplete"
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
106
199
|
_WORKSPACE_INSTRUCTION_PATHS = (
|
|
107
200
|
"AGENTS.md",
|
|
108
201
|
"CLAUDE.md",
|
|
@@ -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.76.
|
|
1237
|
+
DAEMON_VERSION = "1.76.3"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -13404,6 +13404,17 @@ class RuntimeDaemon:
|
|
|
13404
13404
|
result.git = {}
|
|
13405
13405
|
|
|
13406
13406
|
if result.status == "success":
|
|
13407
|
+
acceptance = result.metrics.get("acceptance_verification") or {}
|
|
13408
|
+
if acceptance.get("status") == "pending_final_qa":
|
|
13409
|
+
await reporter.report_progress(
|
|
13410
|
+
task.task_id, 94, "acceptance_pending_final_qa",
|
|
13411
|
+
execution_attempt_id=task.execution_attempt_id,
|
|
13412
|
+
output_lines=[
|
|
13413
|
+
"[daemon] E2E test assets are prepared; acceptance remains pending "
|
|
13414
|
+
"post-deployment final QA: " + ", ".join(acceptance["criteria"]),
|
|
13415
|
+
"[daemon] No browser acceptance or release approval is claimed.",
|
|
13416
|
+
],
|
|
13417
|
+
)
|
|
13407
13418
|
if is_local_bind:
|
|
13408
13419
|
# Restricted commit/push adapter for the user's real
|
|
13409
13420
|
# directory: never `git add -A` sensitive paths, never
|
|
@@ -13852,6 +13863,21 @@ class RuntimeDaemon:
|
|
|
13852
13863
|
req_type = input_data.get("requirement_type", "feature")
|
|
13853
13864
|
verification_work_item = bool(input_data.get("verification_work_item"))
|
|
13854
13865
|
|
|
13866
|
+
audit_contract = input_data.get("implementation_audit_contract")
|
|
13867
|
+
if isinstance(audit_contract, dict):
|
|
13868
|
+
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
13869
|
+
parsed = _daemon_extract_json(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
13870
|
+
audit_report = parsed.get("implementation_audit") if isinstance(parsed, dict) else None
|
|
13871
|
+
result.observations = [
|
|
13872
|
+
observation for observation in result.observations
|
|
13873
|
+
if observation.get("type") != "implementation_audit"
|
|
13874
|
+
]
|
|
13875
|
+
if isinstance(audit_report, dict):
|
|
13876
|
+
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
13877
|
+
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
13878
|
+
if not audit_result["report_valid"] or not audit_result["node_complete"]:
|
|
13879
|
+
issues.extend(audit_result["issues"] or ["The selected approved task still has implementation gaps"])
|
|
13880
|
+
|
|
13855
13881
|
if node_type == "coding" or (
|
|
13856
13882
|
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
13857
13883
|
):
|
|
@@ -14319,6 +14345,7 @@ class RuntimeDaemon:
|
|
|
14319
14345
|
))
|
|
14320
14346
|
|
|
14321
14347
|
# --- coverage-matrix.json validation ---
|
|
14348
|
+
result.metrics.pop("acceptance_verification", None)
|
|
14322
14349
|
if req_type == "task":
|
|
14323
14350
|
result.metrics.pop("acceptance_criteria_manual_review_required", None)
|
|
14324
14351
|
cm_path = base / "coverage-matrix.json"
|
|
@@ -14361,9 +14388,27 @@ class RuntimeDaemon:
|
|
|
14361
14388
|
"coverage_matrix_invalid"
|
|
14362
14389
|
)
|
|
14363
14390
|
else:
|
|
14364
|
-
|
|
14391
|
+
pending, contract_issues = (
|
|
14392
|
+
runtime_evidence.validate_deferred_acceptance_criteria(
|
|
14393
|
+
_input, cm_data, _tc_data,
|
|
14394
|
+
)
|
|
14395
|
+
)
|
|
14396
|
+
issues.extend(contract_issues)
|
|
14397
|
+
if pending:
|
|
14398
|
+
contract = _input["deferred_acceptance_verification"]
|
|
14399
|
+
result.metrics["acceptance_verification"] = {
|
|
14400
|
+
"status": "pending_final_qa",
|
|
14401
|
+
"analysis_revision_id": contract["analysis_revision_id"],
|
|
14402
|
+
"criteria": [ac["id"] for ac in ac_list if ac["id"] in pending],
|
|
14403
|
+
}
|
|
14404
|
+
uncovered = [
|
|
14405
|
+
ac for ac in ac_list
|
|
14406
|
+
if ac["status"] != "covered" and ac["id"] not in pending
|
|
14407
|
+
]
|
|
14365
14408
|
if uncovered:
|
|
14366
|
-
ids = ", ".join(ac["id"] for ac in uncovered[:
|
|
14409
|
+
ids = ", ".join(ac["id"] for ac in uncovered[:20])
|
|
14410
|
+
if len(uncovered) > 20:
|
|
14411
|
+
ids += f" (+{len(uncovered) - 20} more)"
|
|
14367
14412
|
issues.append(
|
|
14368
14413
|
"Uncovered acceptance criteria in "
|
|
14369
14414
|
f"coverage-matrix.json: {ids}"
|
|
@@ -783,6 +783,86 @@ def parse_acceptance_criteria_coverage_artifacts(
|
|
|
783
783
|
}
|
|
784
784
|
|
|
785
785
|
|
|
786
|
+
def validate_deferred_acceptance_criteria(
|
|
787
|
+
input_data: dict[str, Any],
|
|
788
|
+
coverage_matrix: dict[str, Any],
|
|
789
|
+
test_cases: dict[str, Any] | None,
|
|
790
|
+
) -> tuple[set[str], list[str]]:
|
|
791
|
+
"""Validate a server-owned final-QA handoff without claiming test success."""
|
|
792
|
+
contract = input_data.get("deferred_acceptance_verification")
|
|
793
|
+
if contract is None:
|
|
794
|
+
return set(), []
|
|
795
|
+
snapshot = input_data.get("qa_test_level_policy_snapshot") or {}
|
|
796
|
+
policy = snapshot.get("policy") if isinstance(snapshot, dict) else None
|
|
797
|
+
levels = policy.get("levels") if isinstance(policy, dict) else None
|
|
798
|
+
e2e_policy = levels.get("e2e") if isinstance(levels, dict) else None
|
|
799
|
+
if (
|
|
800
|
+
not isinstance(contract, dict)
|
|
801
|
+
or not isinstance(snapshot, dict)
|
|
802
|
+
or not isinstance(policy, dict)
|
|
803
|
+
or policy.get("enforcement") != "enforce"
|
|
804
|
+
or not isinstance(e2e_policy, dict)
|
|
805
|
+
or contract.get("version") != 1
|
|
806
|
+
or contract.get("execution_phase") != "verification_final"
|
|
807
|
+
or not isinstance(snapshot.get("fingerprint"), str)
|
|
808
|
+
or contract.get("qa_test_level_policy_fingerprint") != snapshot["fingerprint"]
|
|
809
|
+
or e2e_policy.get("enabled") is not True
|
|
810
|
+
or e2e_policy.get("workflow_auto_run") is not True
|
|
811
|
+
):
|
|
812
|
+
return set(), ["Final QA acceptance contract does not match the frozen testing policy"]
|
|
813
|
+
|
|
814
|
+
raw_expected = contract.get("acceptance_criteria_ids")
|
|
815
|
+
raw_deferred = contract.get("criteria")
|
|
816
|
+
if (
|
|
817
|
+
not isinstance(raw_expected, list)
|
|
818
|
+
or not raw_expected
|
|
819
|
+
or not isinstance(raw_deferred, list)
|
|
820
|
+
or not raw_deferred
|
|
821
|
+
):
|
|
822
|
+
return set(), ["Final QA acceptance contract has no frozen acceptance criteria"]
|
|
823
|
+
expected = {_canonical_acceptance_criteria_reference(value) for value in raw_expected}
|
|
824
|
+
criteria = coverage_matrix["acceptance_criteria"]
|
|
825
|
+
identifiers = [_canonical_acceptance_criteria_reference(item["id"]) for item in criteria]
|
|
826
|
+
if None in expected or None in identifiers or len(set(identifiers)) != len(identifiers):
|
|
827
|
+
return set(), ["coverage-matrix.json has invalid or duplicate acceptance criterion IDs"]
|
|
828
|
+
if set(identifiers) != expected:
|
|
829
|
+
return set(), ["coverage-matrix.json does not match the frozen acceptance criteria"]
|
|
830
|
+
|
|
831
|
+
cases = (test_cases or {}).get("test_cases") or []
|
|
832
|
+
prepared_intents = {
|
|
833
|
+
case.get("intent_id") or case.get("test_intent_id")
|
|
834
|
+
for case in cases
|
|
835
|
+
if isinstance(case, dict)
|
|
836
|
+
and case.get("test_type") == "e2e"
|
|
837
|
+
and isinstance(case.get("intent_id") or case.get("test_intent_id"), str)
|
|
838
|
+
and case.get("id")
|
|
839
|
+
and case.get("script_path")
|
|
840
|
+
and case.get("script_content")
|
|
841
|
+
}
|
|
842
|
+
eligible = set()
|
|
843
|
+
for item in raw_deferred:
|
|
844
|
+
if not isinstance(item, dict):
|
|
845
|
+
return set(), ["Final QA acceptance contract has an invalid criterion mapping"]
|
|
846
|
+
identifier = _canonical_acceptance_criteria_reference(item.get("id"))
|
|
847
|
+
intent_ids = item.get("test_intent_ids")
|
|
848
|
+
if (
|
|
849
|
+
identifier not in expected
|
|
850
|
+
or not isinstance(intent_ids, list)
|
|
851
|
+
or not intent_ids
|
|
852
|
+
or any(not isinstance(value, str) or not value for value in intent_ids)
|
|
853
|
+
):
|
|
854
|
+
return set(), ["Final QA acceptance contract has an invalid test intent mapping"]
|
|
855
|
+
if not set(intent_ids).issubset(prepared_intents):
|
|
856
|
+
return set(), [f"Final QA acceptance criterion {identifier} has no prepared E2E test case"]
|
|
857
|
+
eligible.add(identifier)
|
|
858
|
+
return {
|
|
859
|
+
item["id"] for item, identifier in zip(criteria, identifiers)
|
|
860
|
+
if identifier in eligible
|
|
861
|
+
and item["status"] == "unverifiable"
|
|
862
|
+
and item.get("covered_by", []) == []
|
|
863
|
+
}, []
|
|
864
|
+
|
|
865
|
+
|
|
786
866
|
async def run_acceptance_criteria_coverage_evidence(
|
|
787
867
|
workspace_path: str | Path,
|
|
788
868
|
check: object,
|
|
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
|