forgexa-cli 1.84.2__tar.gz → 1.84.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.84.2 → forgexa_cli-1.84.4}/PKG-INFO +1 -1
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/agent_core.py +27 -9
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/daemon.py +173 -118
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/pyproject.toml +1 -1
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/README.md +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/setup.cfg +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.84.2 → forgexa_cli-1.84.4}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.84.
|
|
2
|
+
__version__ = "1.84.4"
|
|
@@ -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]
|
|
@@ -419,10 +429,18 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
419
429
|
index = row.get("index")
|
|
420
430
|
related = row.get("task_ids")
|
|
421
431
|
if type(index) is int and not 1 <= index <= criterion_count:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
432
|
+
if criterion_count == 0:
|
|
433
|
+
issues.append(
|
|
434
|
+
f"Acceptance index {index} is invalid because the frozen baseline has no acceptance criteria; "
|
|
435
|
+
"return acceptance_checks=[] and retain all approved task obligations. If approved documents "
|
|
436
|
+
"prove criteria are missing, request an evidenced plan_revision instead of inventing or "
|
|
437
|
+
"renumbering requirements"
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
issues.append(
|
|
441
|
+
f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
|
|
442
|
+
"report the baseline conflict instead of dropping or renumbering requirements"
|
|
443
|
+
)
|
|
426
444
|
continue
|
|
427
445
|
if (
|
|
428
446
|
type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
|
|
@@ -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.84.
|
|
1237
|
+
DAEMON_VERSION = "1.84.4"
|
|
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. "
|
|
@@ -3018,10 +3029,17 @@ def _is_sensitive_local_path(path: str) -> bool:
|
|
|
3018
3029
|
return any(pattern.search(normalized) for pattern in _LOCAL_BIND_SENSITIVE_PATH_PATTERNS)
|
|
3019
3030
|
|
|
3020
3031
|
|
|
3021
|
-
def _runtime_critic_reference_candidates(
|
|
3032
|
+
def _runtime_critic_reference_candidates(
|
|
3033
|
+
baseline: dict, source_paths: list[str], *, inventory: list[str] | None = None,
|
|
3034
|
+
) -> list[str]:
|
|
3022
3035
|
planned = baseline.get("task") or {}
|
|
3023
3036
|
texts = [str(planned.get("description") or "")]
|
|
3024
3037
|
texts.extend(str(item.get("description") or "") for item in planned.get("items") or [] if isinstance(item, dict))
|
|
3038
|
+
if planned.get("type") in ("testing", "review"):
|
|
3039
|
+
for related in baseline.get("related_tasks") or []:
|
|
3040
|
+
if isinstance(related, dict):
|
|
3041
|
+
texts.append(str(related.get("description") or ""))
|
|
3042
|
+
texts.extend(str(item.get("description") or "") for item in related.get("items") or [] if isinstance(item, dict))
|
|
3025
3043
|
if planned.get("type") == "design":
|
|
3026
3044
|
texts.append(json.dumps({"analysis": baseline.get("approved_analysis"),
|
|
3027
3045
|
"documents": baseline.get("analysis_documents")}, ensure_ascii=True)[:200_000])
|
|
@@ -3042,6 +3060,7 @@ def _runtime_critic_reference_candidates(baseline: dict, source_paths: list[str]
|
|
|
3042
3060
|
return None
|
|
3043
3061
|
return normalized
|
|
3044
3062
|
|
|
3063
|
+
php_context = any(Path(path).suffix.lower() == ".php" for path in source_paths)
|
|
3045
3064
|
directories = {Path(path).parent.as_posix() for path in source_paths if safe_path(path)} - {"."}
|
|
3046
3065
|
explicit = set()
|
|
3047
3066
|
for token in re.findall(r"(?<![\w./\\:-])[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+", text):
|
|
@@ -3051,16 +3070,39 @@ def _runtime_critic_reference_candidates(baseline: dict, source_paths: list[str]
|
|
|
3051
3070
|
if Path(candidate).suffix.lower() in suffixes:
|
|
3052
3071
|
explicit.add(candidate)
|
|
3053
3072
|
directories.add(Path(candidate).parent.as_posix())
|
|
3054
|
-
elif not Path(candidate).suffix
|
|
3073
|
+
elif not Path(candidate).suffix and not (
|
|
3074
|
+
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
|
+
):
|
|
3055
3076
|
directories.add(candidate)
|
|
3056
3077
|
names = set(re.findall(
|
|
3057
3078
|
r"(?<![\w./\\:-])([A-Za-z0-9_][A-Za-z0-9_.-]*\.(?:php|py|tsx?|jsx?|xml|json|md|ya?ml|toml|html|phtml))\b", text,
|
|
3058
3079
|
))
|
|
3080
|
+
if php_context:
|
|
3081
|
+
names.update(name + ".php" for name in re.findall(
|
|
3082
|
+
r"(?<![\w./\\:-])([A-Z][A-Za-z0-9_]*)::[A-Za-z_][A-Za-z0-9_]*\b", text,
|
|
3083
|
+
))
|
|
3084
|
+
bare_text = re.sub(r"(?<!\w)(?:[A-Za-z]:[\\/]|/|\.\.?/)[^\s,;()]+", " ", text)
|
|
3085
|
+
names.update(name + ".php" for name in re.findall(
|
|
3086
|
+
r"(?<![\w.\\:-])(?=[A-Za-z0-9]*[a-z])([A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*)(?![\w.\\:-])", bare_text,
|
|
3087
|
+
))
|
|
3088
|
+
module_roots = sorted({
|
|
3089
|
+
"/".join(Path(path).parts[:4]) for path in source_paths
|
|
3090
|
+
if safe_path(path) and len(Path(path).parts) > 4 and Path(path).parts[:2] == ("app", "code")
|
|
3091
|
+
and Path(path).suffix.lower() == ".php"
|
|
3092
|
+
})[:5]
|
|
3059
3093
|
known_names = {Path(path).name for path in [*source_paths, *explicit]}
|
|
3060
3094
|
candidates = set(explicit)
|
|
3061
3095
|
for name in sorted(names - known_names)[:20]:
|
|
3096
|
+
if inventory is not None and module_roots and name.endswith(".php"):
|
|
3097
|
+
matches = [path for path in inventory if Path(path).name == name
|
|
3098
|
+
and any(path.startswith(root + "/") for root in module_roots)]
|
|
3099
|
+
if len(matches) == 1 and (path := safe_path(matches[0])) is not None:
|
|
3100
|
+
candidates.add(path)
|
|
3101
|
+
continue
|
|
3062
3102
|
candidates.update(path for directory in sorted(directories)[:5]
|
|
3063
3103
|
if (path := safe_path(f"{directory}/{name}")) is not None)
|
|
3104
|
+
if name.endswith(".php"):
|
|
3105
|
+
candidates.update(f"{root}/{name}" for root in module_roots)
|
|
3064
3106
|
return sorted(candidates - set(source_paths))[:128]
|
|
3065
3107
|
|
|
3066
3108
|
|
|
@@ -3158,6 +3200,10 @@ class _WorkspaceFileLock:
|
|
|
3158
3200
|
handle.close()
|
|
3159
3201
|
|
|
3160
3202
|
|
|
3203
|
+
class _FreshAnalysisBranchError(RuntimeError):
|
|
3204
|
+
code = "fresh_analysis_branch_reset_blocked"
|
|
3205
|
+
|
|
3206
|
+
|
|
3161
3207
|
class WorkspaceManager:
|
|
3162
3208
|
"""Manages isolated workspaces per task using git worktrees."""
|
|
3163
3209
|
|
|
@@ -4787,6 +4833,55 @@ class WorkspaceManager:
|
|
|
4787
4833
|
pass
|
|
4788
4834
|
return False
|
|
4789
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
|
+
for name in (default_branch, branch_name):
|
|
4847
|
+
await self._git(
|
|
4848
|
+
"fetch", "origin", f"+{name}:refs/remotes/origin/{name}", cwd=repository,
|
|
4849
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4850
|
+
)
|
|
4851
|
+
old_sha = (await self._git("rev-parse", "--verify", f"refs/remotes/origin/{branch_name}", cwd=repository)).strip()
|
|
4852
|
+
default_sha = (await self._git("rev-parse", "--verify", f"refs/remotes/origin/{default_branch}", cwd=repository)).strip()
|
|
4853
|
+
if not all(re.fullmatch(r"[0-9a-fA-F]{40}", value) for value in (old_sha, default_sha)):
|
|
4854
|
+
raise RuntimeError("Fresh analysis requires verified remote branch commits")
|
|
4855
|
+
if old_sha == default_sha:
|
|
4856
|
+
return
|
|
4857
|
+
backup_ref = f"refs/tags/forgexa/fresh-analysis-backup/{branch_name}/{old_sha}"
|
|
4858
|
+
await self._git("check-ref-format", backup_ref, cwd=repository)
|
|
4859
|
+
existing = (await self._git(
|
|
4860
|
+
"ls-remote", "--tags", "origin", backup_ref, cwd=repository,
|
|
4861
|
+
timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
4862
|
+
)).strip()
|
|
4863
|
+
if existing:
|
|
4864
|
+
if existing.split()[0] != old_sha:
|
|
4865
|
+
raise RuntimeError("Fresh analysis recovery reference has a different commit")
|
|
4866
|
+
else:
|
|
4867
|
+
await self._git(
|
|
4868
|
+
"push", "origin", f"{old_sha}:{backup_ref}", cwd=repository,
|
|
4869
|
+
timeout=settings.GIT_PUSH_TIMEOUT, project_key=project_key,
|
|
4870
|
+
)
|
|
4871
|
+
backup_ready = True
|
|
4872
|
+
await self._git(
|
|
4873
|
+
"push", f"--force-with-lease={branch_ref}:{old_sha}", "origin", f"{default_sha}:{branch_ref}",
|
|
4874
|
+
cwd=repository, timeout=settings.GIT_PUSH_TIMEOUT, project_key=project_key,
|
|
4875
|
+
)
|
|
4876
|
+
await self._git("update-ref", f"refs/remotes/origin/{branch_name}", default_sha, cwd=repository)
|
|
4877
|
+
logger.info("Prepared fresh analysis branch %s after preserving %s", branch_name, backup_ref)
|
|
4878
|
+
except RuntimeError as error:
|
|
4879
|
+
recovery = f" Recovery reference: {backup_ref}." if backup_ready else ""
|
|
4880
|
+
raise _FreshAnalysisBranchError(
|
|
4881
|
+
"Fresh analysis branch preparation failed; do not run the Agent or delete the remote branch."
|
|
4882
|
+
+ recovery + " " + self._git_push_diagnostic(error)
|
|
4883
|
+
) from error
|
|
4884
|
+
|
|
4790
4885
|
async def _branch_has_impl_commits(
|
|
4791
4886
|
self, ws_path: Path, branch_name: str, default_branch: str,
|
|
4792
4887
|
) -> bool:
|
|
@@ -5208,19 +5303,8 @@ class WorkspaceManager:
|
|
|
5208
5303
|
fresh_start = False
|
|
5209
5304
|
|
|
5210
5305
|
if fresh_start:
|
|
5211
|
-
#
|
|
5212
|
-
#
|
|
5213
|
-
# — that would destroy prior design/coding/testing work.
|
|
5214
|
-
#
|
|
5215
|
-
# Previous behaviour blocked the reset whenever *any* remote commit
|
|
5216
|
-
# existed, including analysis-only commits (PRD.md, cleanup wipes).
|
|
5217
|
-
# That was overly conservative: a branch whose only commits are
|
|
5218
|
-
# analysis documents is safe to reset for a fresh re-analysis
|
|
5219
|
-
# because no code has been written yet.
|
|
5220
|
-
#
|
|
5221
|
-
# The new check distinguishes:
|
|
5222
|
-
# • analysis-only commits → safe to reset → allow fresh start
|
|
5223
|
-
# • implementation commits (design/coding/testing) → block reset
|
|
5306
|
+
# Preserve existing implementation history before preparing a
|
|
5307
|
+
# confirmed fresh baseline; a failed preparation must stop here.
|
|
5224
5308
|
branch_has_impl = False
|
|
5225
5309
|
ahead_count = 0
|
|
5226
5310
|
try:
|
|
@@ -5239,55 +5323,9 @@ class WorkspaceManager:
|
|
|
5239
5323
|
)
|
|
5240
5324
|
|
|
5241
5325
|
if branch_has_impl:
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
"commit(s) ahead of %s — deleting remote branch to ensure "
|
|
5245
|
-
"a clean analysis environment (user explicitly chose fresh analysis)",
|
|
5246
|
-
branch_name, default_branch,
|
|
5326
|
+
await self._prepare_cloud_fresh_branch(
|
|
5327
|
+
ws_path, branch_name, default_branch, project_key=project_key,
|
|
5247
5328
|
)
|
|
5248
|
-
# When the user explicitly chooses "fresh analysis", they want a clean
|
|
5249
|
-
# slate. Deleting the remote branch achieves this: the agent runs on
|
|
5250
|
-
# a new branch forked from default, completely free of prior
|
|
5251
|
-
# implementation commits that may have been wrong or irrelevant.
|
|
5252
|
-
# This is intentional and safe because:
|
|
5253
|
-
# 1. The user selected fresh analysis (not refine) — they accept
|
|
5254
|
-
# that prior work on this branch will be discarded.
|
|
5255
|
-
# 2. Keeping the old implementation commits would contaminate the
|
|
5256
|
-
# fresh analysis with incorrect context (the original bug report).
|
|
5257
|
-
# 3. "Refine" mode preserves the branch — fresh mode does not.
|
|
5258
|
-
try:
|
|
5259
|
-
await self._git(
|
|
5260
|
-
"push", "origin", "--delete", branch_name,
|
|
5261
|
-
cwd=ws_path,
|
|
5262
|
-
timeout=settings.GIT_PUSH_TIMEOUT,
|
|
5263
|
-
project_key=project_key,
|
|
5264
|
-
)
|
|
5265
|
-
logger.info(
|
|
5266
|
-
"Deleted remote branch %s for fresh analysis restart",
|
|
5267
|
-
branch_name,
|
|
5268
|
-
)
|
|
5269
|
-
# Also remove local branch reference so checkout -B below starts clean
|
|
5270
|
-
try:
|
|
5271
|
-
await self._git("branch", "-D", branch_name, cwd=ws_path)
|
|
5272
|
-
except RuntimeError:
|
|
5273
|
-
pass # Local branch may not exist; not an error
|
|
5274
|
-
# Remove the remote-tracking ref so git doesn't think the
|
|
5275
|
-
# branch still exists on the remote
|
|
5276
|
-
try:
|
|
5277
|
-
await self._git(
|
|
5278
|
-
"update-ref", "-d", f"refs/remotes/origin/{branch_name}",
|
|
5279
|
-
cwd=ws_path,
|
|
5280
|
-
)
|
|
5281
|
-
except RuntimeError:
|
|
5282
|
-
pass
|
|
5283
|
-
except RuntimeError as _del_err:
|
|
5284
|
-
logger.warning(
|
|
5285
|
-
"Failed to delete remote branch %s: %s — "
|
|
5286
|
-
"proceeding with reset to origin/%s instead",
|
|
5287
|
-
branch_name, _del_err, default_branch,
|
|
5288
|
-
)
|
|
5289
|
-
# fresh_start remains True: checkout -B below will recreate
|
|
5290
|
-
# the branch from origin/default_branch
|
|
5291
5329
|
elif ahead_count > 0:
|
|
5292
5330
|
# Branch has commits but they are all analysis-only (safe to reset).
|
|
5293
5331
|
logger.info(
|
|
@@ -5667,11 +5705,8 @@ class WorkspaceManager:
|
|
|
5667
5705
|
except RuntimeError as exc:
|
|
5668
5706
|
last_branch_fetch_error = self._summarize_git_error(exc)
|
|
5669
5707
|
|
|
5670
|
-
#
|
|
5671
|
-
#
|
|
5672
|
-
# We must do this check here (new worktree path) as well as in the existing
|
|
5673
|
-
# worktree path above, because the daemon that runs a fresh re-analysis may
|
|
5674
|
-
# never have processed this requirement before (no ws_path yet).
|
|
5708
|
+
# Prepare the same recoverable fresh baseline on runtimes without a
|
|
5709
|
+
# previous worktree for this requirement.
|
|
5675
5710
|
if fresh_start and branch_exists_remote:
|
|
5676
5711
|
# Check for implementation commits — same logic as in the existing worktree path.
|
|
5677
5712
|
_new_branch_has_impl = False
|
|
@@ -5691,44 +5726,9 @@ class WorkspaceManager:
|
|
|
5691
5726
|
)
|
|
5692
5727
|
|
|
5693
5728
|
if _new_branch_has_impl:
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
"commit(s) — deleting remote branch for a clean analysis environment",
|
|
5697
|
-
branch_name,
|
|
5729
|
+
await self._prepare_cloud_fresh_branch(
|
|
5730
|
+
main_repo, branch_name, default_branch, project_key=project_key,
|
|
5698
5731
|
)
|
|
5699
|
-
try:
|
|
5700
|
-
await self._git(
|
|
5701
|
-
"push", "origin", "--delete", branch_name,
|
|
5702
|
-
cwd=main_repo,
|
|
5703
|
-
timeout=settings.GIT_PUSH_TIMEOUT,
|
|
5704
|
-
project_key=project_key,
|
|
5705
|
-
)
|
|
5706
|
-
logger.info(
|
|
5707
|
-
"Deleted remote branch %s for fresh analysis restart (new worktree path)",
|
|
5708
|
-
branch_name,
|
|
5709
|
-
)
|
|
5710
|
-
# Remove local branch ref and remote-tracking ref
|
|
5711
|
-
try:
|
|
5712
|
-
await self._git("branch", "-D", branch_name, cwd=main_repo)
|
|
5713
|
-
except RuntimeError:
|
|
5714
|
-
pass
|
|
5715
|
-
try:
|
|
5716
|
-
await self._git(
|
|
5717
|
-
"update-ref", "-d", f"refs/remotes/origin/{branch_name}",
|
|
5718
|
-
cwd=main_repo,
|
|
5719
|
-
)
|
|
5720
|
-
except RuntimeError:
|
|
5721
|
-
pass
|
|
5722
|
-
# Branch no longer exists on remote
|
|
5723
|
-
branch_exists_remote = False
|
|
5724
|
-
except RuntimeError as _del_err2:
|
|
5725
|
-
logger.warning(
|
|
5726
|
-
"Failed to delete remote branch %s (new worktree path): %s — "
|
|
5727
|
-
"proceeding with worktree add from default branch",
|
|
5728
|
-
branch_name, _del_err2,
|
|
5729
|
-
)
|
|
5730
|
-
# Treat as if branch doesn't exist to force fresh worktree
|
|
5731
|
-
branch_exists_remote = False
|
|
5732
5732
|
|
|
5733
5733
|
if expect_branch and not fresh_start and not branch_exists_remote:
|
|
5734
5734
|
self._safe_rmtree(ws_path)
|
|
@@ -13988,6 +13988,7 @@ class RuntimeDaemon:
|
|
|
13988
13988
|
paths = agent_core.missing_planned_task_files({
|
|
13989
13989
|
"files": planned.get("files") or [], "items": planned.get("items") or [],
|
|
13990
13990
|
}, [])
|
|
13991
|
+
assigned_paths = list(paths)
|
|
13991
13992
|
primary_paths = list(paths)
|
|
13992
13993
|
if planned.get("type") in ("testing", "review") and getattr(task, "requirement_key", ""):
|
|
13993
13994
|
output_dir = str((task.input_data or {}).get("output_dir") or "").replace("\\", "/").rstrip("/")
|
|
@@ -14011,6 +14012,7 @@ class RuntimeDaemon:
|
|
|
14011
14012
|
path for path in support_paths.splitlines()[:20]
|
|
14012
14013
|
if path.startswith(directory + "/") and path.endswith((".ts", ".js", ".py"))
|
|
14013
14014
|
)
|
|
14015
|
+
reference_tasks = []
|
|
14014
14016
|
audit_contract = (task.input_data or {}).get("implementation_audit_contract")
|
|
14015
14017
|
if isinstance(audit_contract, dict) and baseline.get("plan_id") is not None and audit_contract.get("plan_id") == baseline["plan_id"]:
|
|
14016
14018
|
approved_tasks = {entry["id"]: entry for entry in audit_contract.get("tasks") or [] if isinstance(entry, dict) and isinstance(entry.get("id"), str)}
|
|
@@ -14038,6 +14040,8 @@ class RuntimeDaemon:
|
|
|
14038
14040
|
visited.add(dependency_id)
|
|
14039
14041
|
dependency = approved_tasks.get(dependency_id)
|
|
14040
14042
|
if dependency is not None:
|
|
14043
|
+
if audit_contract.get("subject_task_id") == planned.get("id"):
|
|
14044
|
+
reference_tasks.append(dependency)
|
|
14041
14045
|
dependency_paths = agent_core.missing_planned_task_files({
|
|
14042
14046
|
"files": (dependency.get("input") or {}).get("files") or [],
|
|
14043
14047
|
"items": dependency.get("items") or [],
|
|
@@ -14050,19 +14054,29 @@ class RuntimeDaemon:
|
|
|
14050
14054
|
"files": (related.get("input") or {}).get("files") or [],
|
|
14051
14055
|
"items": related.get("items") or [],
|
|
14052
14056
|
}, []))
|
|
14053
|
-
|
|
14057
|
+
reference_baseline = {**baseline, "related_tasks": reference_tasks}
|
|
14058
|
+
reference_candidates = _runtime_critic_reference_candidates(reference_baseline, primary_paths)
|
|
14054
14059
|
if reference_candidates:
|
|
14055
14060
|
try:
|
|
14061
|
+
reference_modules = sorted({
|
|
14062
|
+
Path(path).parent.as_posix() for path in reference_candidates
|
|
14063
|
+
if len(Path(path).parts) == 5 and Path(path).parts[:2] == ("app", "code")
|
|
14064
|
+
and Path(path).suffix.lower() == ".php"
|
|
14065
|
+
})[:5]
|
|
14056
14066
|
inventory = await self.workspace_manager._git(
|
|
14057
|
-
"ls-tree", "-r", "--name-only", after_sha, "--", *reference_candidates,
|
|
14067
|
+
"ls-tree", "-r", "--name-only", after_sha, "--", *reference_candidates, *reference_modules,
|
|
14058
14068
|
cwd=workspace_path, timeout=10,
|
|
14059
14069
|
)
|
|
14060
|
-
|
|
14070
|
+
if len(inventory.encode("utf-8")) > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
14071
|
+
raise ValueError("Committed reference inventory exceeds its bounded evidence budget")
|
|
14072
|
+
inventory_paths = inventory.splitlines()
|
|
14073
|
+
inventory_set = set(inventory_paths)
|
|
14074
|
+
reference_candidates = _runtime_critic_reference_candidates(reference_baseline, primary_paths, inventory=inventory_paths)
|
|
14075
|
+
confirmed = [path for path in reference_candidates if path in inventory_set]
|
|
14061
14076
|
paths = [*primary_paths, *confirmed, *paths]
|
|
14062
14077
|
except Exception:
|
|
14063
14078
|
coverage["reference_inventory_unavailable"] = True
|
|
14064
|
-
paths.
|
|
14065
|
-
paths = list(dict.fromkeys(paths))
|
|
14079
|
+
paths = list(dict.fromkeys([*assigned_paths, *changed_files, *paths]))
|
|
14066
14080
|
coverage["candidate_files"] = len(paths)
|
|
14067
14081
|
remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
|
|
14068
14082
|
source_context = []
|
|
@@ -14094,7 +14108,7 @@ class RuntimeDaemon:
|
|
|
14094
14108
|
diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
|
|
14095
14109
|
coverage["full_source_files"] = len(committed_sources)
|
|
14096
14110
|
coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
|
|
14097
|
-
coverage["capsule_incomplete"] = coverage["omitted_files"] > 0
|
|
14111
|
+
coverage["capsule_incomplete"] = coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
|
|
14098
14112
|
result.metrics["runtime_critic_coverage"] = coverage
|
|
14099
14113
|
if not diff:
|
|
14100
14114
|
return {
|
|
@@ -14207,8 +14221,23 @@ class RuntimeDaemon:
|
|
|
14207
14221
|
"agent_attempts": [attempt],
|
|
14208
14222
|
**metadata,
|
|
14209
14223
|
}
|
|
14210
|
-
|
|
14211
|
-
|
|
14224
|
+
conformance = (
|
|
14225
|
+
agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance"))
|
|
14226
|
+
if isinstance(baseline, dict) else None
|
|
14227
|
+
)
|
|
14228
|
+
if conformance is not None and conformance["status"] != "conformant":
|
|
14229
|
+
diagnostic_lines = [f"[daemon] Local independent review: {conformance['status']}."]
|
|
14230
|
+
if conformance.get("reason"):
|
|
14231
|
+
diagnostic_lines.append(
|
|
14232
|
+
"[daemon] Review contract diagnostic: " + conformance.get("diagnostic_code", conformance["reason"])
|
|
14233
|
+
)
|
|
14234
|
+
else:
|
|
14235
|
+
diagnostic_lines.extend(
|
|
14236
|
+
"[daemon] Review evidence: " + re.sub(r"https?://\S+", "[redacted-url]", item.replace("\x00", ""))[:600]
|
|
14237
|
+
for item in structured_result["plan_conformance"]["evidence"][:10]
|
|
14238
|
+
)
|
|
14239
|
+
_append_local_bind_output_log(task.task_id, diagnostic_lines)
|
|
14240
|
+
if not task.local_evidence_upload and conformance is not None:
|
|
14212
14241
|
score = structured_result.get("score")
|
|
14213
14242
|
verdict = structured_result.get("verdict")
|
|
14214
14243
|
structured_result = {
|
|
@@ -14369,6 +14398,7 @@ class RuntimeDaemon:
|
|
|
14369
14398
|
|
|
14370
14399
|
audit_contract = input_data.get("implementation_audit_contract")
|
|
14371
14400
|
result.metrics.pop("implementation_audit_node_complete", None)
|
|
14401
|
+
result.metrics.pop("implementation_audit_subject_blocked", None)
|
|
14372
14402
|
if isinstance(audit_contract, dict):
|
|
14373
14403
|
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
14374
14404
|
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
@@ -14382,6 +14412,11 @@ class RuntimeDaemon:
|
|
|
14382
14412
|
result.metrics["implementation_audit_node_complete"] = bool(
|
|
14383
14413
|
audit_result["report_valid"] and audit_result["node_complete"]
|
|
14384
14414
|
)
|
|
14415
|
+
result.metrics["implementation_audit_subject_blocked"] = bool(
|
|
14416
|
+
audit_result["report_valid"] and not audit_result["node_complete"]
|
|
14417
|
+
and any(finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
|
|
14418
|
+
for finding in audit_result.get("findings") or [])
|
|
14419
|
+
)
|
|
14385
14420
|
if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
|
|
14386
14421
|
result.status = "failed"
|
|
14387
14422
|
result.failure_code = "implementation_plan_revision_required"
|
|
@@ -16405,6 +16440,21 @@ class RuntimeDaemon:
|
|
|
16405
16440
|
*[f"[daemon] {issue}" for issue in issues],
|
|
16406
16441
|
])
|
|
16407
16442
|
|
|
16443
|
+
if (
|
|
16444
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16445
|
+
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16446
|
+
):
|
|
16447
|
+
result.status = "failed"
|
|
16448
|
+
result.failure_code = "implementation_audit_incomplete"
|
|
16449
|
+
result.error = (
|
|
16450
|
+
str(next((issue for issue in issues if issue.code == "implementation_audit_incomplete"), issues[0]))
|
|
16451
|
+
+ ". Resolve the reported verification prerequisites before retrying; "
|
|
16452
|
+
"rewriting artifacts does not execute blocked acceptance checks."
|
|
16453
|
+
)
|
|
16454
|
+
_record_output_validation_issues(result, issues)
|
|
16455
|
+
self._record_testing_telemetry(workspace_path, task, result, retries_used=attempt)
|
|
16456
|
+
return result
|
|
16457
|
+
|
|
16408
16458
|
if (
|
|
16409
16459
|
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16410
16460
|
and all(
|
|
@@ -16593,6 +16643,11 @@ class RuntimeDaemon:
|
|
|
16593
16643
|
elif audit_incomplete:
|
|
16594
16644
|
result.failure_code = "implementation_audit_incomplete"
|
|
16595
16645
|
result.error = "; ".join(remaining[:3])
|
|
16646
|
+
if (
|
|
16647
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16648
|
+
and result.metrics.get("implementation_audit_subject_blocked") is True
|
|
16649
|
+
):
|
|
16650
|
+
result.error += ". Resolve the reported verification prerequisites before retrying."
|
|
16596
16651
|
else:
|
|
16597
16652
|
result.failure_code = "validation_retry_exhausted"
|
|
16598
16653
|
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
|