forgexa-cli 1.84.3__tar.gz → 1.85.0__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.84.3 → forgexa_cli-1.85.0}/PKG-INFO +1 -1
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/agent_core.py +47 -9
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/daemon.py +150 -112
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/pyproject.toml +1 -1
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/README.md +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/setup.cfg +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.84.3 → forgexa_cli-1.85.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.85.0"
|
|
@@ -85,19 +85,29 @@ def assess_approved_plan_conformance(baseline: dict, report: object) -> dict:
|
|
|
85
85
|
).encode()).hexdigest()
|
|
86
86
|
if baseline.get("version") != 1 or result["baseline_fingerprint"] != fingerprint:
|
|
87
87
|
result["reason"] = "approved_plan_baseline_invalid"
|
|
88
|
+
result["diagnostic_code"] = "approved_plan_baseline_invalid"
|
|
88
89
|
return result
|
|
89
|
-
if not isinstance(report, dict)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if not isinstance(report, dict):
|
|
91
|
+
result["reason"] = "approved_plan_review_missing_or_mismatched"
|
|
92
|
+
result["diagnostic_code"] = "approved_plan_review_missing" if report is None else "approved_plan_review_invalid_payload"
|
|
93
|
+
return result
|
|
94
|
+
diagnostic = None
|
|
95
|
+
if report.get("baseline_fingerprint") != fingerprint:
|
|
96
|
+
diagnostic = "approved_plan_review_baseline_mismatch"
|
|
97
|
+
elif report.get("task_id") != result["task_id"]:
|
|
98
|
+
diagnostic = "approved_plan_review_task_mismatch"
|
|
99
|
+
elif report.get("status") not in ("conformant", "deviation", "unverified"):
|
|
100
|
+
diagnostic = "approved_plan_review_status_invalid"
|
|
101
|
+
if diagnostic is not None:
|
|
94
102
|
result["reason"] = "approved_plan_review_missing_or_mismatched"
|
|
103
|
+
result["diagnostic_code"] = diagnostic
|
|
95
104
|
return result
|
|
96
105
|
evidence = report.get("evidence")
|
|
97
106
|
if not isinstance(evidence, list) or not evidence or any(
|
|
98
107
|
not isinstance(item, str) or not item.strip() for item in evidence
|
|
99
108
|
):
|
|
100
109
|
result["reason"] = "approved_plan_review_evidence_missing"
|
|
110
|
+
result["diagnostic_code"] = "approved_plan_review_evidence_missing"
|
|
101
111
|
return result
|
|
102
112
|
result["status"] = report["status"]
|
|
103
113
|
result["evidence_refs"] = [hashlib.sha256(item.encode()).hexdigest() for item in evidence]
|
|
@@ -295,20 +305,39 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
295
305
|
and len(criteria) == len(reserved_ids)
|
|
296
306
|
and reserved_ids == [criterion.get("id") for criterion in criteria if isinstance(criterion, dict)]
|
|
297
307
|
):
|
|
308
|
+
index_map = deferred_contract.get("acceptance_criteria_indices")
|
|
309
|
+
expected_indices = {identifier: index for index, identifier in enumerate(reserved_ids, 1)}
|
|
310
|
+
valid_index_map = (
|
|
311
|
+
isinstance(index_map, dict) and set(index_map) == set(expected_indices)
|
|
312
|
+
and all(type(value) is int and value == expected_indices[identifier] for identifier, value in index_map.items())
|
|
313
|
+
)
|
|
298
314
|
for criterion_id in reserved_ids:
|
|
299
315
|
match = re.fullmatch(r"AC-([0-9]+)", criterion_id)
|
|
300
|
-
|
|
316
|
+
index = index_map[criterion_id] if valid_index_map else int(match.group(1)) if index_map is None and match else 0
|
|
317
|
+
if not 1 <= index <= len(contract.get("acceptance_criteria") or []):
|
|
301
318
|
reserved_indexes.clear()
|
|
302
319
|
break
|
|
303
|
-
reserved_indexes.add(
|
|
320
|
+
reserved_indexes.add(index)
|
|
304
321
|
if len(reserved_indexes) != len(reserved_ids):
|
|
305
322
|
reserved_indexes.clear()
|
|
306
323
|
acceptance_checks = []
|
|
307
324
|
deferred_acceptance = []
|
|
325
|
+
subject = contract.get("subject_task_id")
|
|
326
|
+
prepared_verifier = (
|
|
327
|
+
not contract.get("final_verification") and task_types.get(subject) in ("testing", "verify")
|
|
328
|
+
and implementation_statuses.get(subject) == "covered"
|
|
329
|
+
)
|
|
308
330
|
for row in report["acceptance_checks"]:
|
|
309
331
|
if (
|
|
310
332
|
row["index"] in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
|
|
311
|
-
and
|
|
333
|
+
and (
|
|
334
|
+
all(implementation_statuses.get(task_id) == "covered" for task_id in row["task_ids"])
|
|
335
|
+
or prepared_verifier and subject in row["task_ids"] and all(
|
|
336
|
+
implementation_statuses.get(task_id) == "covered"
|
|
337
|
+
or task_id not in implementation_statuses and task_types.get(task_id) in ("coding", "fix")
|
|
338
|
+
for task_id in row["task_ids"]
|
|
339
|
+
)
|
|
340
|
+
)
|
|
312
341
|
):
|
|
313
342
|
deferred_acceptance.append(row["index"])
|
|
314
343
|
continue
|
|
@@ -448,7 +477,16 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
448
477
|
description = row.get("description")
|
|
449
478
|
paths = row.get("files") or []
|
|
450
479
|
if not isinstance(description, str) or not description.strip() or not isinstance(paths, list):
|
|
451
|
-
|
|
480
|
+
reference = f"task '{row['task_id']}'" if kind == "task" else f"acceptance index {row['index']}"
|
|
481
|
+
invalid_fields = []
|
|
482
|
+
if not isinstance(description, str) or not description.strip():
|
|
483
|
+
invalid_fields.append("description must be non-empty text")
|
|
484
|
+
if not isinstance(paths, list):
|
|
485
|
+
invalid_fields.append("files must be a list; use files=[] when no source file is implicated")
|
|
486
|
+
issues.append(
|
|
487
|
+
"Implementation gaps require an actionable description and file list; "
|
|
488
|
+
+ reference + ": " + "; ".join(invalid_fields)
|
|
489
|
+
)
|
|
452
490
|
continue
|
|
453
491
|
if any(
|
|
454
492
|
not isinstance(path, str) or not path.strip()
|
|
@@ -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.
|
|
1237
|
+
DAEMON_VERSION = "1.85.0"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -1668,7 +1668,8 @@ Review only the supplied task context, code diff and committed source snapshots.
|
|
|
1668
1668
|
isolated review capsule, not the project workspace. Do not write or modify files,
|
|
1669
1669
|
run mutating commands, create commits, or make network changes.
|
|
1670
1670
|
|
|
1671
|
-
Return exactly one JSON object with
|
|
1671
|
+
Return exactly one JSON object. This base schema must be extended with the task-bound
|
|
1672
|
+
plan_conformance field when a frozen approved baseline is supplied:
|
|
1672
1673
|
{
|
|
1673
1674
|
"verdict": "approve" | "request_changes" | "reject",
|
|
1674
1675
|
"score": 0.0 to 1.0,
|
|
@@ -1741,9 +1742,19 @@ def _build_runtime_critic_prompt(
|
|
|
1741
1742
|
"Implementation details may refine it but cannot silently replace its responsibilities, "
|
|
1742
1743
|
"interfaces, required files or acceptance criteria. Report insufficient evidence honestly.",
|
|
1743
1744
|
json.dumps(baseline, ensure_ascii=True, sort_keys=True),
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1745
|
+
"Bound review response example:\n" + json.dumps({
|
|
1746
|
+
"verdict": "request_changes", "score": 0.5,
|
|
1747
|
+
"summary": "<independent assessment of the supplied source and required checks>",
|
|
1748
|
+
"dimension_scores": {}, "issues": [], "plan_conformance": {
|
|
1749
|
+
"baseline_fingerprint": baseline.get("fingerprint"),
|
|
1750
|
+
"task_id": (baseline.get("task") or {}).get("id"), "status": "unverified",
|
|
1751
|
+
"evidence": ["<concrete independently inspected source or check evidence>"],
|
|
1752
|
+
},
|
|
1753
|
+
}, ensure_ascii=True, separators=(",", ":")),
|
|
1754
|
+
"The task-bound field is mandatory, including when issues is empty. Preserve its exact task_id "
|
|
1755
|
+
"and baseline_fingerprint; do not substitute a requirement key, node UUID or your own digest. "
|
|
1756
|
+
"Replace placeholder evidence with observed facts. Choose conformant, deviation or unverified "
|
|
1757
|
+
"honestly; the example defaults to unverified and is not completion evidence. "
|
|
1747
1758
|
"Assess every assigned obligation, including behavior omitted from the diff. "
|
|
1748
1759
|
"Use deviation for an actual conflict or omission; use unverified when supplied evidence "
|
|
1749
1760
|
"cannot establish conformance. A high quality score never replaces this conclusion. "
|
|
@@ -3189,6 +3200,10 @@ class _WorkspaceFileLock:
|
|
|
3189
3200
|
handle.close()
|
|
3190
3201
|
|
|
3191
3202
|
|
|
3203
|
+
class _FreshAnalysisBranchError(RuntimeError):
|
|
3204
|
+
code = "fresh_analysis_branch_reset_blocked"
|
|
3205
|
+
|
|
3206
|
+
|
|
3192
3207
|
class WorkspaceManager:
|
|
3193
3208
|
"""Manages isolated workspaces per task using git worktrees."""
|
|
3194
3209
|
|
|
@@ -4818,6 +4833,83 @@ class WorkspaceManager:
|
|
|
4818
4833
|
pass
|
|
4819
4834
|
return False
|
|
4820
4835
|
|
|
4836
|
+
async def _prepare_cloud_fresh_branch(
|
|
4837
|
+
self, repository: Path, branch_name: str, default_branch: str, *, project_key: str,
|
|
4838
|
+
) -> None:
|
|
4839
|
+
branch_ref = f"refs/heads/{branch_name}"
|
|
4840
|
+
backup_ref = None
|
|
4841
|
+
backup_ready = False
|
|
4842
|
+
try:
|
|
4843
|
+
if branch_name == default_branch:
|
|
4844
|
+
raise RuntimeError("Fresh analysis cannot reset the project default branch")
|
|
4845
|
+
await self._git("check-ref-format", branch_ref, cwd=repository)
|
|
4846
|
+
await self._git(
|
|
4847
|
+
"fetch", "origin", f"+{default_branch}:refs/remotes/origin/{default_branch}", cwd=repository,
|
|
4848
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4849
|
+
)
|
|
4850
|
+
tracking_ref = f"refs/remotes/origin/{branch_name}"
|
|
4851
|
+
advertised = (await self._git(
|
|
4852
|
+
"ls-remote", "--heads", "origin", branch_ref, cwd=repository,
|
|
4853
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4854
|
+
)).strip()
|
|
4855
|
+
if not advertised:
|
|
4856
|
+
await self._git("update-ref", "-d", tracking_ref, cwd=repository)
|
|
4857
|
+
logger.info("Fresh analysis branch %s is absent on origin; removed its stale tracking reference", branch_name)
|
|
4858
|
+
return
|
|
4859
|
+
remote_fields = advertised.split()
|
|
4860
|
+
if len(remote_fields) != 2 or remote_fields[1] != branch_ref or not re.fullmatch(r"[0-9a-fA-F]{40}", remote_fields[0]):
|
|
4861
|
+
raise RuntimeError("Fresh analysis requires an exact remote branch advertisement")
|
|
4862
|
+
try:
|
|
4863
|
+
await self._git(
|
|
4864
|
+
"fetch", "origin", f"+{branch_name}:{tracking_ref}", cwd=repository,
|
|
4865
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4866
|
+
)
|
|
4867
|
+
except RuntimeError:
|
|
4868
|
+
remaining = (await self._git(
|
|
4869
|
+
"ls-remote", "--heads", "origin", branch_ref, cwd=repository,
|
|
4870
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4871
|
+
)).strip()
|
|
4872
|
+
if remaining:
|
|
4873
|
+
raise
|
|
4874
|
+
await self._git("update-ref", "-d", tracking_ref, cwd=repository)
|
|
4875
|
+
logger.info("Fresh analysis branch %s disappeared during fetch; removed its stale tracking reference", branch_name)
|
|
4876
|
+
return
|
|
4877
|
+
old_sha = (await self._git("rev-parse", "--verify", f"refs/remotes/origin/{branch_name}", cwd=repository)).strip()
|
|
4878
|
+
default_sha = (await self._git("rev-parse", "--verify", f"refs/remotes/origin/{default_branch}", cwd=repository)).strip()
|
|
4879
|
+
if not all(re.fullmatch(r"[0-9a-fA-F]{40}", value) for value in (old_sha, default_sha)):
|
|
4880
|
+
raise RuntimeError("Fresh analysis requires verified remote branch commits")
|
|
4881
|
+
if old_sha != remote_fields[0]:
|
|
4882
|
+
raise RuntimeError("Remote requirement branch changed during fresh analysis preparation")
|
|
4883
|
+
if old_sha == default_sha:
|
|
4884
|
+
return
|
|
4885
|
+
backup_ref = f"refs/tags/forgexa/fresh-analysis-backup/{branch_name}/{old_sha}"
|
|
4886
|
+
await self._git("check-ref-format", backup_ref, cwd=repository)
|
|
4887
|
+
existing = (await self._git(
|
|
4888
|
+
"ls-remote", "--tags", "origin", backup_ref, cwd=repository,
|
|
4889
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4890
|
+
)).strip()
|
|
4891
|
+
if existing:
|
|
4892
|
+
if existing.split()[0] != old_sha:
|
|
4893
|
+
raise RuntimeError("Fresh analysis recovery reference has a different commit")
|
|
4894
|
+
else:
|
|
4895
|
+
await self._git(
|
|
4896
|
+
"push", "origin", f"{old_sha}:{backup_ref}", cwd=repository,
|
|
4897
|
+
timeout=settings.GIT_PUSH_TIMEOUT, project_key=project_key,
|
|
4898
|
+
)
|
|
4899
|
+
backup_ready = True
|
|
4900
|
+
await self._git(
|
|
4901
|
+
"push", f"--force-with-lease={branch_ref}:{old_sha}", "origin", f"{default_sha}:{branch_ref}",
|
|
4902
|
+
cwd=repository, timeout=settings.GIT_PUSH_TIMEOUT, project_key=project_key,
|
|
4903
|
+
)
|
|
4904
|
+
await self._git("update-ref", f"refs/remotes/origin/{branch_name}", default_sha, cwd=repository)
|
|
4905
|
+
logger.info("Prepared fresh analysis branch %s after preserving %s", branch_name, backup_ref)
|
|
4906
|
+
except RuntimeError as error:
|
|
4907
|
+
recovery = f" Recovery reference: {backup_ref}." if backup_ready else ""
|
|
4908
|
+
raise _FreshAnalysisBranchError(
|
|
4909
|
+
"Fresh analysis branch preparation failed; do not run the Agent or delete the remote branch."
|
|
4910
|
+
+ recovery + " " + self._git_push_diagnostic(error)
|
|
4911
|
+
) from error
|
|
4912
|
+
|
|
4821
4913
|
async def _branch_has_impl_commits(
|
|
4822
4914
|
self, ws_path: Path, branch_name: str, default_branch: str,
|
|
4823
4915
|
) -> bool:
|
|
@@ -5239,19 +5331,8 @@ class WorkspaceManager:
|
|
|
5239
5331
|
fresh_start = False
|
|
5240
5332
|
|
|
5241
5333
|
if fresh_start:
|
|
5242
|
-
#
|
|
5243
|
-
#
|
|
5244
|
-
# — that would destroy prior design/coding/testing work.
|
|
5245
|
-
#
|
|
5246
|
-
# Previous behaviour blocked the reset whenever *any* remote commit
|
|
5247
|
-
# existed, including analysis-only commits (PRD.md, cleanup wipes).
|
|
5248
|
-
# That was overly conservative: a branch whose only commits are
|
|
5249
|
-
# analysis documents is safe to reset for a fresh re-analysis
|
|
5250
|
-
# because no code has been written yet.
|
|
5251
|
-
#
|
|
5252
|
-
# The new check distinguishes:
|
|
5253
|
-
# • analysis-only commits → safe to reset → allow fresh start
|
|
5254
|
-
# • implementation commits (design/coding/testing) → block reset
|
|
5334
|
+
# Preserve existing implementation history before preparing a
|
|
5335
|
+
# confirmed fresh baseline; a failed preparation must stop here.
|
|
5255
5336
|
branch_has_impl = False
|
|
5256
5337
|
ahead_count = 0
|
|
5257
5338
|
try:
|
|
@@ -5270,55 +5351,9 @@ class WorkspaceManager:
|
|
|
5270
5351
|
)
|
|
5271
5352
|
|
|
5272
5353
|
if branch_has_impl:
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
"commit(s) ahead of %s — deleting remote branch to ensure "
|
|
5276
|
-
"a clean analysis environment (user explicitly chose fresh analysis)",
|
|
5277
|
-
branch_name, default_branch,
|
|
5354
|
+
await self._prepare_cloud_fresh_branch(
|
|
5355
|
+
ws_path, branch_name, default_branch, project_key=project_key,
|
|
5278
5356
|
)
|
|
5279
|
-
# When the user explicitly chooses "fresh analysis", they want a clean
|
|
5280
|
-
# slate. Deleting the remote branch achieves this: the agent runs on
|
|
5281
|
-
# a new branch forked from default, completely free of prior
|
|
5282
|
-
# implementation commits that may have been wrong or irrelevant.
|
|
5283
|
-
# This is intentional and safe because:
|
|
5284
|
-
# 1. The user selected fresh analysis (not refine) — they accept
|
|
5285
|
-
# that prior work on this branch will be discarded.
|
|
5286
|
-
# 2. Keeping the old implementation commits would contaminate the
|
|
5287
|
-
# fresh analysis with incorrect context (the original bug report).
|
|
5288
|
-
# 3. "Refine" mode preserves the branch — fresh mode does not.
|
|
5289
|
-
try:
|
|
5290
|
-
await self._git(
|
|
5291
|
-
"push", "origin", "--delete", branch_name,
|
|
5292
|
-
cwd=ws_path,
|
|
5293
|
-
timeout=settings.GIT_PUSH_TIMEOUT,
|
|
5294
|
-
project_key=project_key,
|
|
5295
|
-
)
|
|
5296
|
-
logger.info(
|
|
5297
|
-
"Deleted remote branch %s for fresh analysis restart",
|
|
5298
|
-
branch_name,
|
|
5299
|
-
)
|
|
5300
|
-
# Also remove local branch reference so checkout -B below starts clean
|
|
5301
|
-
try:
|
|
5302
|
-
await self._git("branch", "-D", branch_name, cwd=ws_path)
|
|
5303
|
-
except RuntimeError:
|
|
5304
|
-
pass # Local branch may not exist; not an error
|
|
5305
|
-
# Remove the remote-tracking ref so git doesn't think the
|
|
5306
|
-
# branch still exists on the remote
|
|
5307
|
-
try:
|
|
5308
|
-
await self._git(
|
|
5309
|
-
"update-ref", "-d", f"refs/remotes/origin/{branch_name}",
|
|
5310
|
-
cwd=ws_path,
|
|
5311
|
-
)
|
|
5312
|
-
except RuntimeError:
|
|
5313
|
-
pass
|
|
5314
|
-
except RuntimeError as _del_err:
|
|
5315
|
-
logger.warning(
|
|
5316
|
-
"Failed to delete remote branch %s: %s — "
|
|
5317
|
-
"proceeding with reset to origin/%s instead",
|
|
5318
|
-
branch_name, _del_err, default_branch,
|
|
5319
|
-
)
|
|
5320
|
-
# fresh_start remains True: checkout -B below will recreate
|
|
5321
|
-
# the branch from origin/default_branch
|
|
5322
5357
|
elif ahead_count > 0:
|
|
5323
5358
|
# Branch has commits but they are all analysis-only (safe to reset).
|
|
5324
5359
|
logger.info(
|
|
@@ -5698,11 +5733,8 @@ class WorkspaceManager:
|
|
|
5698
5733
|
except RuntimeError as exc:
|
|
5699
5734
|
last_branch_fetch_error = self._summarize_git_error(exc)
|
|
5700
5735
|
|
|
5701
|
-
#
|
|
5702
|
-
#
|
|
5703
|
-
# We must do this check here (new worktree path) as well as in the existing
|
|
5704
|
-
# worktree path above, because the daemon that runs a fresh re-analysis may
|
|
5705
|
-
# never have processed this requirement before (no ws_path yet).
|
|
5736
|
+
# Prepare the same recoverable fresh baseline on runtimes without a
|
|
5737
|
+
# previous worktree for this requirement.
|
|
5706
5738
|
if fresh_start and branch_exists_remote:
|
|
5707
5739
|
# Check for implementation commits — same logic as in the existing worktree path.
|
|
5708
5740
|
_new_branch_has_impl = False
|
|
@@ -5722,44 +5754,9 @@ class WorkspaceManager:
|
|
|
5722
5754
|
)
|
|
5723
5755
|
|
|
5724
5756
|
if _new_branch_has_impl:
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
"commit(s) — deleting remote branch for a clean analysis environment",
|
|
5728
|
-
branch_name,
|
|
5757
|
+
await self._prepare_cloud_fresh_branch(
|
|
5758
|
+
main_repo, branch_name, default_branch, project_key=project_key,
|
|
5729
5759
|
)
|
|
5730
|
-
try:
|
|
5731
|
-
await self._git(
|
|
5732
|
-
"push", "origin", "--delete", branch_name,
|
|
5733
|
-
cwd=main_repo,
|
|
5734
|
-
timeout=settings.GIT_PUSH_TIMEOUT,
|
|
5735
|
-
project_key=project_key,
|
|
5736
|
-
)
|
|
5737
|
-
logger.info(
|
|
5738
|
-
"Deleted remote branch %s for fresh analysis restart (new worktree path)",
|
|
5739
|
-
branch_name,
|
|
5740
|
-
)
|
|
5741
|
-
# Remove local branch ref and remote-tracking ref
|
|
5742
|
-
try:
|
|
5743
|
-
await self._git("branch", "-D", branch_name, cwd=main_repo)
|
|
5744
|
-
except RuntimeError:
|
|
5745
|
-
pass
|
|
5746
|
-
try:
|
|
5747
|
-
await self._git(
|
|
5748
|
-
"update-ref", "-d", f"refs/remotes/origin/{branch_name}",
|
|
5749
|
-
cwd=main_repo,
|
|
5750
|
-
)
|
|
5751
|
-
except RuntimeError:
|
|
5752
|
-
pass
|
|
5753
|
-
# Branch no longer exists on remote
|
|
5754
|
-
branch_exists_remote = False
|
|
5755
|
-
except RuntimeError as _del_err2:
|
|
5756
|
-
logger.warning(
|
|
5757
|
-
"Failed to delete remote branch %s (new worktree path): %s — "
|
|
5758
|
-
"proceeding with worktree add from default branch",
|
|
5759
|
-
branch_name, _del_err2,
|
|
5760
|
-
)
|
|
5761
|
-
# Treat as if branch doesn't exist to force fresh worktree
|
|
5762
|
-
branch_exists_remote = False
|
|
5763
5760
|
|
|
5764
5761
|
if expect_branch and not fresh_start and not branch_exists_remote:
|
|
5765
5762
|
self._safe_rmtree(ws_path)
|
|
@@ -14019,6 +14016,7 @@ class RuntimeDaemon:
|
|
|
14019
14016
|
paths = agent_core.missing_planned_task_files({
|
|
14020
14017
|
"files": planned.get("files") or [], "items": planned.get("items") or [],
|
|
14021
14018
|
}, [])
|
|
14019
|
+
assigned_paths = list(paths)
|
|
14022
14020
|
primary_paths = list(paths)
|
|
14023
14021
|
if planned.get("type") in ("testing", "review") and getattr(task, "requirement_key", ""):
|
|
14024
14022
|
output_dir = str((task.input_data or {}).get("output_dir") or "").replace("\\", "/").rstrip("/")
|
|
@@ -14106,8 +14104,7 @@ class RuntimeDaemon:
|
|
|
14106
14104
|
paths = [*primary_paths, *confirmed, *paths]
|
|
14107
14105
|
except Exception:
|
|
14108
14106
|
coverage["reference_inventory_unavailable"] = True
|
|
14109
|
-
paths.
|
|
14110
|
-
paths = list(dict.fromkeys(paths))
|
|
14107
|
+
paths = list(dict.fromkeys([*assigned_paths, *changed_files, *paths]))
|
|
14111
14108
|
coverage["candidate_files"] = len(paths)
|
|
14112
14109
|
remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
|
|
14113
14110
|
source_context = []
|
|
@@ -14252,8 +14249,23 @@ class RuntimeDaemon:
|
|
|
14252
14249
|
"agent_attempts": [attempt],
|
|
14253
14250
|
**metadata,
|
|
14254
14251
|
}
|
|
14255
|
-
|
|
14256
|
-
|
|
14252
|
+
conformance = (
|
|
14253
|
+
agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance"))
|
|
14254
|
+
if isinstance(baseline, dict) else None
|
|
14255
|
+
)
|
|
14256
|
+
if conformance is not None and conformance["status"] != "conformant":
|
|
14257
|
+
diagnostic_lines = [f"[daemon] Local independent review: {conformance['status']}."]
|
|
14258
|
+
if conformance.get("reason"):
|
|
14259
|
+
diagnostic_lines.append(
|
|
14260
|
+
"[daemon] Review contract diagnostic: " + conformance.get("diagnostic_code", conformance["reason"])
|
|
14261
|
+
)
|
|
14262
|
+
else:
|
|
14263
|
+
diagnostic_lines.extend(
|
|
14264
|
+
"[daemon] Review evidence: " + re.sub(r"https?://\S+", "[redacted-url]", item.replace("\x00", ""))[:600]
|
|
14265
|
+
for item in structured_result["plan_conformance"]["evidence"][:10]
|
|
14266
|
+
)
|
|
14267
|
+
_append_local_bind_output_log(task.task_id, diagnostic_lines)
|
|
14268
|
+
if not task.local_evidence_upload and conformance is not None:
|
|
14257
14269
|
score = structured_result.get("score")
|
|
14258
14270
|
verdict = structured_result.get("verdict")
|
|
14259
14271
|
structured_result = {
|
|
@@ -14414,6 +14426,7 @@ class RuntimeDaemon:
|
|
|
14414
14426
|
|
|
14415
14427
|
audit_contract = input_data.get("implementation_audit_contract")
|
|
14416
14428
|
result.metrics.pop("implementation_audit_node_complete", None)
|
|
14429
|
+
result.metrics.pop("implementation_audit_subject_blocked", None)
|
|
14417
14430
|
if isinstance(audit_contract, dict):
|
|
14418
14431
|
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
14419
14432
|
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
@@ -14427,6 +14440,11 @@ class RuntimeDaemon:
|
|
|
14427
14440
|
result.metrics["implementation_audit_node_complete"] = bool(
|
|
14428
14441
|
audit_result["report_valid"] and audit_result["node_complete"]
|
|
14429
14442
|
)
|
|
14443
|
+
result.metrics["implementation_audit_subject_blocked"] = bool(
|
|
14444
|
+
audit_result["report_valid"] and not audit_result["node_complete"]
|
|
14445
|
+
and any(finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
|
|
14446
|
+
for finding in audit_result.get("findings") or [])
|
|
14447
|
+
)
|
|
14430
14448
|
if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
|
|
14431
14449
|
result.status = "failed"
|
|
14432
14450
|
result.failure_code = "implementation_plan_revision_required"
|
|
@@ -16450,6 +16468,21 @@ class RuntimeDaemon:
|
|
|
16450
16468
|
*[f"[daemon] {issue}" for issue in issues],
|
|
16451
16469
|
])
|
|
16452
16470
|
|
|
16471
|
+
if (
|
|
16472
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16473
|
+
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16474
|
+
):
|
|
16475
|
+
result.status = "failed"
|
|
16476
|
+
result.failure_code = "implementation_audit_incomplete"
|
|
16477
|
+
result.error = (
|
|
16478
|
+
str(next((issue for issue in issues if issue.code == "implementation_audit_incomplete"), issues[0]))
|
|
16479
|
+
+ ". Resolve the reported verification prerequisites before retrying; "
|
|
16480
|
+
"rewriting artifacts does not execute blocked acceptance checks."
|
|
16481
|
+
)
|
|
16482
|
+
_record_output_validation_issues(result, issues)
|
|
16483
|
+
self._record_testing_telemetry(workspace_path, task, result, retries_used=attempt)
|
|
16484
|
+
return result
|
|
16485
|
+
|
|
16453
16486
|
if (
|
|
16454
16487
|
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16455
16488
|
and all(
|
|
@@ -16638,6 +16671,11 @@ class RuntimeDaemon:
|
|
|
16638
16671
|
elif audit_incomplete:
|
|
16639
16672
|
result.failure_code = "implementation_audit_incomplete"
|
|
16640
16673
|
result.error = "; ".join(remaining[:3])
|
|
16674
|
+
if (
|
|
16675
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16676
|
+
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16677
|
+
):
|
|
16678
|
+
result.error += ". Resolve the reported verification prerequisites before retrying."
|
|
16641
16679
|
else:
|
|
16642
16680
|
result.failure_code = "validation_retry_exhausted"
|
|
16643
16681
|
result.error = (
|
|
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
|