forgexa-cli 1.85.2__tar.gz → 1.85.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.85.2 → forgexa_cli-1.85.4}/PKG-INFO +1 -1
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/agent_core.py +118 -2
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/daemon.py +141 -19
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/pyproject.toml +1 -1
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/README.md +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/setup.cfg +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.85.2 → forgexa_cli-1.85.4}/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.4"
|
|
@@ -79,10 +79,114 @@ LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS = frozenset({
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
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:
|
|
83
187
|
result = {
|
|
84
188
|
"status": "unverified", "baseline_fingerprint": baseline.get("fingerprint"),
|
|
85
|
-
"task_id": (baseline.get("task") or {}).get("id"), "evidence_refs": [],
|
|
189
|
+
"task_id": (baseline.get("task") or {}).get("id"), "evidence_refs": [], "node_complete": False,
|
|
86
190
|
}
|
|
87
191
|
expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
|
|
88
192
|
fingerprint = hashlib.sha256(json.dumps(
|
|
@@ -114,8 +218,20 @@ def assess_approved_plan_conformance(baseline: dict, report: object) -> dict:
|
|
|
114
218
|
result["reason"] = "approved_plan_review_evidence_missing"
|
|
115
219
|
result["diagnostic_code"] = "approved_plan_review_evidence_missing"
|
|
116
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
|
|
117
230
|
result["status"] = report["status"]
|
|
118
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)
|
|
119
235
|
return result
|
|
120
236
|
|
|
121
237
|
|
|
@@ -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.4"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -1663,6 +1663,8 @@ _RUNTIME_CRITIC_TIMEOUT_SECONDS = 300
|
|
|
1663
1663
|
_RUNTIME_CRITIC_DIFF_MAX_CHARS = 30_000
|
|
1664
1664
|
_RUNTIME_CRITIC_OUTPUT_MAX_CHARS = 20_000
|
|
1665
1665
|
_RUNTIME_CRITIC_SOURCE_MAX_BYTES = 1_000_000
|
|
1666
|
+
_RUNTIME_CRITIC_DESIGN_MAX_FILES = 128
|
|
1667
|
+
_RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS = 200
|
|
1666
1668
|
_RUNTIME_CRITIC_SYSTEM_PROMPT = """You are the Critic Agent for a software delivery gate.
|
|
1667
1669
|
Review only the supplied task context, code diff and committed source snapshots. You are running in an
|
|
1668
1670
|
isolated review capsule, not the project workspace. Do not write or modify files,
|
|
@@ -1809,6 +1811,7 @@ def _build_runtime_critic_prompt(
|
|
|
1809
1811
|
"failed checks remain blocking. State the concrete missing evidence; never invent a run, "
|
|
1810
1812
|
"treat asset existence as proof of correctness, or claim final QA or release acceptance."
|
|
1811
1813
|
)
|
|
1814
|
+
parts.append(agent_core.approved_contribution_review_instructions(baseline, audit_contract))
|
|
1812
1815
|
else:
|
|
1813
1816
|
planned_context = {
|
|
1814
1817
|
key: (task.input_data or {}).get(key)
|
|
@@ -3074,9 +3077,25 @@ def _runtime_critic_reference_candidates(
|
|
|
3074
3077
|
php_context and all(re.fullmatch(r"[A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*", part) for part in Path(candidate).parts)
|
|
3075
3078
|
):
|
|
3076
3079
|
directories.add(candidate)
|
|
3080
|
+
alias_roots = {}
|
|
3081
|
+
for path in [*source_paths, *explicit]:
|
|
3082
|
+
parts = Path(path).parts
|
|
3083
|
+
if safe_path(path) and len(parts) > 4 and parts[:2] == ("app", "code"):
|
|
3084
|
+
alias_roots.setdefault(parts[2], set()).add("app/code")
|
|
3085
|
+
elif safe_path(path) and len(parts) > 2 and parts[0] == "vendor":
|
|
3086
|
+
alias_roots.setdefault(parts[1], set()).add("vendor")
|
|
3087
|
+
for reference in tuple(explicit):
|
|
3088
|
+
roots = alias_roots.get(Path(reference).parts[0], set())
|
|
3089
|
+
if len(roots) == 1:
|
|
3090
|
+
rooted = safe_path(next(iter(roots)) + "/" + reference)
|
|
3091
|
+
if rooted is not None and (inventory is None or rooted in inventory):
|
|
3092
|
+
explicit.add(rooted)
|
|
3093
|
+
directories.add(Path(rooted).parent.as_posix())
|
|
3094
|
+
php_context = php_context or any(Path(path).suffix.lower() == ".php" for path in explicit)
|
|
3077
3095
|
names = set(re.findall(
|
|
3078
3096
|
r"(?<![\w./\\:-])([A-Za-z0-9_][A-Za-z0-9_.-]*\.(?:php|py|tsx?|jsx?|xml|json|md|ya?ml|toml|html|phtml))\b", text,
|
|
3079
3097
|
))
|
|
3098
|
+
names.update(re.findall(r"(?<![\w./\\:-])(composer\.lock)\b", text))
|
|
3080
3099
|
if php_context:
|
|
3081
3100
|
names.update(name + ".php" for name in re.findall(
|
|
3082
3101
|
r"(?<![\w./\\:-])([A-Z][A-Za-z0-9_]*)::[A-Za-z_][A-Za-z0-9_]*\b", text,
|
|
@@ -3086,14 +3105,30 @@ def _runtime_critic_reference_candidates(
|
|
|
3086
3105
|
r"(?<![\w.\\:-])(?=[A-Za-z0-9]*[a-z])([A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*)(?![\w.\\:-])", bare_text,
|
|
3087
3106
|
))
|
|
3088
3107
|
module_roots = sorted({
|
|
3089
|
-
"/".join(Path(path).parts[:4]) for path in source_paths
|
|
3108
|
+
"/".join(Path(path).parts[:4]) for path in [*source_paths, *explicit]
|
|
3090
3109
|
if safe_path(path) and len(Path(path).parts) > 4 and Path(path).parts[:2] == ("app", "code")
|
|
3091
3110
|
and Path(path).suffix.lower() == ".php"
|
|
3092
3111
|
})[:5]
|
|
3093
3112
|
known_names = {Path(path).name for path in [*source_paths, *explicit]}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3113
|
+
root_files = names & {"composer.lock", "AGENTS.md"}
|
|
3114
|
+
candidates = set(explicit) | root_files
|
|
3115
|
+
if any(name.endswith("Factory.php") for name in names | known_names):
|
|
3116
|
+
configurations = {f"{root}/etc/di.xml" for root in module_roots}
|
|
3117
|
+
if inventory is not None:
|
|
3118
|
+
configurations.intersection_update(inventory)
|
|
3119
|
+
if inventory is None or len(configurations) == 1:
|
|
3120
|
+
candidates.update(configurations)
|
|
3121
|
+
for reference in explicit:
|
|
3122
|
+
if Path(reference).suffix.lower() != ".xml" or reference.startswith(("app/code/", "vendor/")):
|
|
3123
|
+
continue
|
|
3124
|
+
module_references = {f"{root}/{reference}" for root in module_roots}
|
|
3125
|
+
if inventory is not None:
|
|
3126
|
+
module_references.intersection_update(inventory)
|
|
3127
|
+
if len(module_references) != 1:
|
|
3128
|
+
continue
|
|
3129
|
+
candidates.update(module_references)
|
|
3130
|
+
for name in sorted(names - known_names - root_files)[:20]:
|
|
3131
|
+
if inventory is not None and module_roots and Path(name).suffix.lower() in {".php", ".xml"}:
|
|
3097
3132
|
matches = [path for path in inventory if Path(path).name == name
|
|
3098
3133
|
and any(path.startswith(root + "/") for root in module_roots)]
|
|
3099
3134
|
if len(matches) == 1 and (path := safe_path(matches[0])) is not None:
|
|
@@ -3101,9 +3136,14 @@ def _runtime_critic_reference_candidates(
|
|
|
3101
3136
|
continue
|
|
3102
3137
|
candidates.update(path for directory in sorted(directories)[:5]
|
|
3103
3138
|
if (path := safe_path(f"{directory}/{name}")) is not None)
|
|
3104
|
-
if name.
|
|
3139
|
+
if Path(name).suffix.lower() in {".php", ".xml"}:
|
|
3105
3140
|
candidates.update(f"{root}/{name}" for root in module_roots)
|
|
3106
|
-
|
|
3141
|
+
primary = (explicit | root_files) - set(source_paths)
|
|
3142
|
+
ordered = sorted(primary, key=lambda path: (
|
|
3143
|
+
path not in root_files, inventory is None or path not in inventory,
|
|
3144
|
+
not path.startswith(("app/code/", "vendor/", "setup/", "build/", "config/")), path,
|
|
3145
|
+
))
|
|
3146
|
+
return (ordered + sorted(candidates - primary - set(source_paths)))[:128]
|
|
3107
3147
|
|
|
3108
3148
|
|
|
3109
3149
|
def _try_acquire_local_workspace_file_lock(binding_id: str, workspace_path: Path | None = None):
|
|
@@ -4007,8 +4047,15 @@ class WorkspaceManager:
|
|
|
4007
4047
|
|
|
4008
4048
|
if remote_branch.strip():
|
|
4009
4049
|
try:
|
|
4050
|
+
# The tracking ref is a cache of remote state and the remote is
|
|
4051
|
+
# authoritative (its existence and tip were just confirmed via
|
|
4052
|
+
# ls-remote). Update it even when the remote was rewritten: a
|
|
4053
|
+
# fresh-analysis reset legitimately force-pushes this branch,
|
|
4054
|
+
# and a non-forced refspec is rejected non-fast-forward against
|
|
4055
|
+
# the stale tracking ref forever — the same cache-alignment
|
|
4056
|
+
# rule every other fetch in this file already follows.
|
|
4010
4057
|
await self._git(
|
|
4011
|
-
"fetch", "origin", f"{target_branch}:refs/remotes/origin/{target_branch}",
|
|
4058
|
+
"fetch", "origin", f"+{target_branch}:refs/remotes/origin/{target_branch}",
|
|
4012
4059
|
cwd=local_path,
|
|
4013
4060
|
)
|
|
4014
4061
|
except RuntimeError as exc:
|
|
@@ -4020,6 +4067,19 @@ class WorkspaceManager:
|
|
|
4020
4067
|
"rev-list", f"origin/{target_branch}..HEAD", "--count", cwd=local_path,
|
|
4021
4068
|
)
|
|
4022
4069
|
if ahead_output.strip() not in ("", "0"):
|
|
4070
|
+
behind_output = await self._git(
|
|
4071
|
+
"rev-list", f"HEAD..origin/{target_branch}", "--count", cwd=local_path,
|
|
4072
|
+
)
|
|
4073
|
+
if behind_output.strip() not in ("", "0"):
|
|
4074
|
+
raise RuntimeError(
|
|
4075
|
+
f"local_binding_unavailable: local branch '{target_branch}' diverged "
|
|
4076
|
+
f"from origin/{target_branch} (ahead {ahead_output.strip()}, behind "
|
|
4077
|
+
f"{behind_output.strip()}); the remote branch was rewritten — for "
|
|
4078
|
+
"example by a fresh-analysis reset, whose retired tips are preserved "
|
|
4079
|
+
"under 'forgexa/fresh-analysis-backup/*' tags. Align your local branch "
|
|
4080
|
+
f"(git reset --hard origin/{target_branch}) after keeping anything you "
|
|
4081
|
+
"need, then retry."
|
|
4082
|
+
)
|
|
4023
4083
|
raise RuntimeError(
|
|
4024
4084
|
f"local_binding_unavailable: local branch '{target_branch}' has "
|
|
4025
4085
|
f"{ahead_output.strip()} unpushed commit(s) ahead of origin — push them "
|
|
@@ -4057,7 +4117,7 @@ class WorkspaceManager:
|
|
|
4057
4117
|
|
|
4058
4118
|
try:
|
|
4059
4119
|
await self._git(
|
|
4060
|
-
"fetch", "origin", f"{default_branch}:refs/remotes/origin/{default_branch}",
|
|
4120
|
+
"fetch", "origin", f"+{default_branch}:refs/remotes/origin/{default_branch}",
|
|
4061
4121
|
cwd=local_path,
|
|
4062
4122
|
)
|
|
4063
4123
|
local_head = (await self._git("rev-parse", "HEAD", cwd=local_path)).strip()
|
|
@@ -5676,7 +5736,7 @@ class WorkspaceManager:
|
|
|
5676
5736
|
try:
|
|
5677
5737
|
await self._git(
|
|
5678
5738
|
"fetch", "origin",
|
|
5679
|
-
f"{default_branch}:refs/remotes/origin/{default_branch}",
|
|
5739
|
+
f"+{default_branch}:refs/remotes/origin/{default_branch}",
|
|
5680
5740
|
cwd=main_repo,
|
|
5681
5741
|
timeout=settings.GIT_FETCH_TIMEOUT,
|
|
5682
5742
|
project_key=project_key,
|
|
@@ -14017,6 +14077,7 @@ class RuntimeDaemon:
|
|
|
14017
14077
|
)
|
|
14018
14078
|
if isinstance(baseline, dict) and re.fullmatch(r"[0-9a-fA-F]{40}", after_sha):
|
|
14019
14079
|
planned = baseline.get("task") or {}
|
|
14080
|
+
source_deadline = time.monotonic() + _RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS if planned.get("type") == "design" else None
|
|
14020
14081
|
paths = agent_core.missing_planned_task_files({
|
|
14021
14082
|
"files": planned.get("files") or [], "items": planned.get("items") or [],
|
|
14022
14083
|
}, [])
|
|
@@ -14108,17 +14169,52 @@ class RuntimeDaemon:
|
|
|
14108
14169
|
"items": related.get("items") or [],
|
|
14109
14170
|
}, []))
|
|
14110
14171
|
reference_baseline = {**baseline, "related_tasks": reference_tasks}
|
|
14172
|
+
document_sources = {}
|
|
14173
|
+
requirement_key = str(getattr(task, "requirement_key", "") or "")
|
|
14174
|
+
if source_deadline is not None and requirement_key:
|
|
14175
|
+
document_paths = [path for path in dict.fromkeys([*assigned_paths, *changed_files])
|
|
14176
|
+
if path.startswith(f"docs/requirements/{requirement_key}/implement/")
|
|
14177
|
+
and path.endswith(".md") and ".." not in Path(path).parts
|
|
14178
|
+
and re.fullmatch(r"[A-Za-z0-9_./-]+", path) and not _is_sensitive_local_path(path)][:8]
|
|
14179
|
+
document_bytes = 0
|
|
14180
|
+
for path in document_paths:
|
|
14181
|
+
seconds_left = source_deadline - time.monotonic()
|
|
14182
|
+
if seconds_left <= 0:
|
|
14183
|
+
coverage["source_collection_timed_out"] = True
|
|
14184
|
+
coverage["design_reference_documents_unavailable"] = True
|
|
14185
|
+
break
|
|
14186
|
+
try:
|
|
14187
|
+
source = await self.workspace_manager._git(
|
|
14188
|
+
"show", f"{after_sha}:{path}", cwd=workspace_path, timeout=min(10, max(1, int(seconds_left))),
|
|
14189
|
+
)
|
|
14190
|
+
encoded_size = len(source.encode("utf-8"))
|
|
14191
|
+
if document_bytes + encoded_size > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
14192
|
+
coverage["design_reference_documents_unavailable"] = True
|
|
14193
|
+
continue
|
|
14194
|
+
document_sources[path] = source
|
|
14195
|
+
document_bytes += encoded_size
|
|
14196
|
+
except Exception:
|
|
14197
|
+
coverage["design_reference_documents_unavailable"] = True
|
|
14198
|
+
reference_baseline = {**reference_baseline, "task": {
|
|
14199
|
+
**planned, "description": str(planned.get("description") or "") + "\n" + "\n".join(document_sources.values()),
|
|
14200
|
+
}}
|
|
14111
14201
|
reference_candidates = _runtime_critic_reference_candidates(reference_baseline, primary_paths)
|
|
14112
14202
|
if reference_candidates:
|
|
14113
14203
|
try:
|
|
14114
14204
|
reference_modules = sorted({
|
|
14115
14205
|
Path(path).parent.as_posix() for path in reference_candidates
|
|
14116
14206
|
if len(Path(path).parts) == 5 and Path(path).parts[:2] == ("app", "code")
|
|
14117
|
-
and Path(path).suffix.lower()
|
|
14207
|
+
and Path(path).suffix.lower() in {".php", ".xml"}
|
|
14118
14208
|
})[:5]
|
|
14209
|
+
inventory_timeout = 10
|
|
14210
|
+
if source_deadline is not None:
|
|
14211
|
+
seconds_left = source_deadline - time.monotonic()
|
|
14212
|
+
if seconds_left <= 0:
|
|
14213
|
+
raise TimeoutError("Source collection deadline reached")
|
|
14214
|
+
inventory_timeout = min(10, max(1, int(seconds_left)))
|
|
14119
14215
|
inventory = await self.workspace_manager._git(
|
|
14120
14216
|
"ls-tree", "-r", "--name-only", after_sha, "--", *reference_candidates, *reference_modules,
|
|
14121
|
-
cwd=workspace_path, timeout=
|
|
14217
|
+
cwd=workspace_path, timeout=inventory_timeout,
|
|
14122
14218
|
)
|
|
14123
14219
|
if len(inventory.encode("utf-8")) > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
14124
14220
|
raise ValueError("Committed reference inventory exceeds its bounded evidence budget")
|
|
@@ -14133,15 +14229,25 @@ class RuntimeDaemon:
|
|
|
14133
14229
|
coverage["candidate_files"] = len(paths)
|
|
14134
14230
|
remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
|
|
14135
14231
|
source_context = []
|
|
14136
|
-
|
|
14232
|
+
source_file_limit = _RUNTIME_CRITIC_DESIGN_MAX_FILES if planned.get("type") == "design" else 20
|
|
14233
|
+
for path in paths[:source_file_limit]:
|
|
14234
|
+
source_timeout = 10
|
|
14235
|
+
if source_deadline is not None:
|
|
14236
|
+
seconds_left = source_deadline - time.monotonic()
|
|
14237
|
+
if seconds_left <= 0:
|
|
14238
|
+
coverage["source_collection_timed_out"] = True
|
|
14239
|
+
break
|
|
14240
|
+
source_timeout = min(10, max(1, int(seconds_left)))
|
|
14137
14241
|
normalized = Path(path)
|
|
14138
14242
|
if normalized.is_absolute() or ".." in normalized.parts or path.endswith("/"):
|
|
14139
14243
|
coverage["skipped_paths"] += 1
|
|
14140
14244
|
continue
|
|
14141
14245
|
try:
|
|
14142
|
-
source =
|
|
14143
|
-
|
|
14144
|
-
|
|
14246
|
+
source = document_sources.get(path)
|
|
14247
|
+
if source is None:
|
|
14248
|
+
source = await self.workspace_manager._git(
|
|
14249
|
+
"show", f"{after_sha}:{path}", cwd=workspace_path, timeout=source_timeout,
|
|
14250
|
+
)
|
|
14145
14251
|
except Exception as error:
|
|
14146
14252
|
coverage["unreadable_files"] += 1
|
|
14147
14253
|
detail = str(error).lower()
|
|
@@ -14175,6 +14281,7 @@ class RuntimeDaemon:
|
|
|
14175
14281
|
coverage["capsule_incomplete"] = (
|
|
14176
14282
|
coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
|
|
14177
14283
|
or coverage.get("verification_inventory_unavailable", False)
|
|
14284
|
+
or coverage.get("design_reference_documents_unavailable", False)
|
|
14178
14285
|
)
|
|
14179
14286
|
result.metrics["runtime_critic_coverage"] = coverage
|
|
14180
14287
|
if not diff:
|
|
@@ -14292,7 +14399,8 @@ class RuntimeDaemon:
|
|
|
14292
14399
|
**metadata,
|
|
14293
14400
|
}
|
|
14294
14401
|
conformance = (
|
|
14295
|
-
agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance")
|
|
14402
|
+
agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance"),
|
|
14403
|
+
audit_contract=(task.input_data or {}).get("implementation_audit_contract"))
|
|
14296
14404
|
if isinstance(baseline, dict) else None
|
|
14297
14405
|
)
|
|
14298
14406
|
if conformance is not None and conformance["status"] != "conformant":
|
|
@@ -14320,6 +14428,15 @@ class RuntimeDaemon:
|
|
|
14320
14428
|
"evidence": conformance["evidence_refs"],
|
|
14321
14429
|
},
|
|
14322
14430
|
}
|
|
14431
|
+
if conformance.get("node_complete") and conformance["status"] == "unverified":
|
|
14432
|
+
contribution = conformance["contribution_check"]
|
|
14433
|
+
structured_result["plan_conformance"]["contribution_check"] = {
|
|
14434
|
+
"check_id": contribution["check_id"], "status": contribution["status"], "evidence": contribution["evidence_refs"],
|
|
14435
|
+
}
|
|
14436
|
+
structured_result["plan_conformance"]["pending_checks"] = [{
|
|
14437
|
+
"check_id": check["check_id"], "task_id": check["task_id"], "status": check["status"], "evidence": check["evidence_refs"],
|
|
14438
|
+
**({"blocking_reason": check["blocking_reason"]} if "blocking_reason" in check else {}),
|
|
14439
|
+
} for check in conformance["pending_checks"]]
|
|
14323
14440
|
encoded_result = json.dumps(
|
|
14324
14441
|
structured_result,
|
|
14325
14442
|
ensure_ascii=True,
|
|
@@ -14512,6 +14629,7 @@ class RuntimeDaemon:
|
|
|
14512
14629
|
delivered_files = list(result.files_changed)
|
|
14513
14630
|
delivered_files.extend(input_data.get("planned_task_predecessor_files") or [])
|
|
14514
14631
|
for missing_path in agent_core.missing_planned_task_files(input_data, delivered_files):
|
|
14632
|
+
result.metrics["failure_domain"] = "delivery"
|
|
14515
14633
|
issues.append(_OutputValidationIssue(
|
|
14516
14634
|
"planned_task_incomplete",
|
|
14517
14635
|
f"Planned task file has no implementation evidence: {missing_path}. "
|
|
@@ -16516,7 +16634,10 @@ class RuntimeDaemon:
|
|
|
16516
16634
|
])
|
|
16517
16635
|
|
|
16518
16636
|
if (
|
|
16519
|
-
|
|
16637
|
+
result.metrics.get("failure_domain") == "environment" and all(
|
|
16638
|
+
issue.code in {"implementation_audit_incomplete", "acceptance_criteria_uncovered"} for issue in issues
|
|
16639
|
+
)
|
|
16640
|
+
or agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16520
16641
|
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16521
16642
|
):
|
|
16522
16643
|
result.status = "failed"
|
|
@@ -16719,7 +16840,8 @@ class RuntimeDaemon:
|
|
|
16719
16840
|
result.failure_code = "implementation_audit_incomplete"
|
|
16720
16841
|
result.error = "; ".join(remaining[:3])
|
|
16721
16842
|
if (
|
|
16722
|
-
|
|
16843
|
+
result.metrics.get("failure_domain") == "environment"
|
|
16844
|
+
or agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16723
16845
|
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16724
16846
|
):
|
|
16725
16847
|
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
|