forgexa-cli 1.11.0__tar.gz → 1.11.1__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.11.0 → forgexa_cli-1.11.1}/PKG-INFO +1 -1
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli/daemon.py +260 -18
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/pyproject.toml +1 -1
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/README.md +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.11.0 → forgexa_cli-1.11.1}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.11.
|
|
2
|
+
__version__ = "1.11.1"
|
|
@@ -404,7 +404,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
404
404
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
405
405
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
406
406
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
407
|
-
DAEMON_VERSION = "1.11.
|
|
407
|
+
DAEMON_VERSION = "1.11.1"
|
|
408
408
|
|
|
409
409
|
|
|
410
410
|
def _detect_client_type() -> str:
|
|
@@ -841,12 +841,16 @@ class AgentDiscovery:
|
|
|
841
841
|
extra_dirs.append(versions[0])
|
|
842
842
|
else:
|
|
843
843
|
# macOS + Linux shared paths
|
|
844
|
+
# Kimi Code 0.18.0+ standalone installer puts the binary here;
|
|
845
|
+
# KIMI_CODE_HOME overrides the data dir (officially documented).
|
|
846
|
+
kimi_home = Path(os.environ.get("KIMI_CODE_HOME", home / ".kimi-code"))
|
|
844
847
|
extra_dirs += [
|
|
845
848
|
home / ".local" / "bin", # pip / pipx installs, Claude Code
|
|
846
849
|
home / ".opencode" / "bin", # opencode
|
|
847
850
|
home / ".cargo" / "bin", # Rust / cargo installs
|
|
848
851
|
home / ".bun" / "bin", # bun
|
|
849
852
|
home / ".volta" / "bin", # volta (node version manager)
|
|
853
|
+
kimi_home / "bin", # Kimi Code 0.18.0+ (standalone installer)
|
|
850
854
|
]
|
|
851
855
|
if is_mac:
|
|
852
856
|
# Homebrew Intel + Apple Silicon
|
|
@@ -1212,7 +1216,8 @@ class WorkspaceManager:
|
|
|
1212
1216
|
if not is_fresh_start:
|
|
1213
1217
|
try:
|
|
1214
1218
|
await self._git(
|
|
1215
|
-
"fetch", "origin",
|
|
1219
|
+
"fetch", "origin",
|
|
1220
|
+
f"{branch_name}:refs/remotes/origin/{branch_name}",
|
|
1216
1221
|
cwd=ws_path, project_key=project_key,
|
|
1217
1222
|
)
|
|
1218
1223
|
except RuntimeError:
|
|
@@ -2491,6 +2496,48 @@ class ProcessManager:
|
|
|
2491
2496
|
changed_paths.update(self._normalize_repo_paths(paths))
|
|
2492
2497
|
return bool(required_paths & changed_paths)
|
|
2493
2498
|
|
|
2499
|
+
def _recover_timeout_from_workspace_changes(
|
|
2500
|
+
self,
|
|
2501
|
+
task: TaskInfo,
|
|
2502
|
+
workspace_path: Path,
|
|
2503
|
+
result: TaskResult,
|
|
2504
|
+
pre_commit_git: dict,
|
|
2505
|
+
) -> bool:
|
|
2506
|
+
"""Recover absolute-timeout failures after validating workspace output."""
|
|
2507
|
+
error_lower = (result.error or "").lower()
|
|
2508
|
+
is_absolute_timeout = (
|
|
2509
|
+
"absolute limit" in error_lower or "timed out after" in error_lower
|
|
2510
|
+
)
|
|
2511
|
+
changed_files = list(pre_commit_git.get("files_changed") or [])
|
|
2512
|
+
if not is_absolute_timeout or not changed_files:
|
|
2513
|
+
return False
|
|
2514
|
+
if not self.process_manager.has_meaningful_agent_output(result):
|
|
2515
|
+
return False
|
|
2516
|
+
|
|
2517
|
+
if result.files_changed:
|
|
2518
|
+
result.files_changed = sorted(set(result.files_changed) | set(changed_files))
|
|
2519
|
+
else:
|
|
2520
|
+
result.files_changed = changed_files
|
|
2521
|
+
if not result.lines_added:
|
|
2522
|
+
result.lines_added = int(pre_commit_git.get("lines_added", 0) or 0)
|
|
2523
|
+
if not result.lines_removed:
|
|
2524
|
+
result.lines_removed = int(pre_commit_git.get("lines_removed", 0) or 0)
|
|
2525
|
+
|
|
2526
|
+
if not self.process_manager._has_required_deliverable_updates(
|
|
2527
|
+
task,
|
|
2528
|
+
changed_files,
|
|
2529
|
+
result.files_changed,
|
|
2530
|
+
):
|
|
2531
|
+
return False
|
|
2532
|
+
|
|
2533
|
+
validation_issues = self._validate_outputs(workspace_path, task, result)
|
|
2534
|
+
if validation_issues:
|
|
2535
|
+
result.metrics["timeout_recovery_validation_issues"] = validation_issues
|
|
2536
|
+
return False
|
|
2537
|
+
|
|
2538
|
+
result.metrics["recovered_from_timeout_uncommitted_changes"] = True
|
|
2539
|
+
return True
|
|
2540
|
+
|
|
2494
2541
|
def _build_prompt(self, task: TaskInfo) -> str:
|
|
2495
2542
|
"""Build the prompt to send to the agent.
|
|
2496
2543
|
|
|
@@ -2542,6 +2589,7 @@ class ProcessManager:
|
|
|
2542
2589
|
task_id: str,
|
|
2543
2590
|
on_chunk: Any,
|
|
2544
2591
|
workspace_path: "Path | None" = None,
|
|
2592
|
+
stream_stderr: bool = False,
|
|
2545
2593
|
) -> tuple[str, str, int]:
|
|
2546
2594
|
"""Stream stdout line-by-line from a subprocess, flushing to on_chunk.
|
|
2547
2595
|
|
|
@@ -2551,6 +2599,12 @@ class ProcessManager:
|
|
|
2551
2599
|
- Stdout is read line-by-line so callers receive output in real time.
|
|
2552
2600
|
- Stderr is consumed concurrently via a background task to avoid pipe
|
|
2553
2601
|
deadlock when the process fills the stderr buffer.
|
|
2602
|
+
- When ``stream_stderr`` is True, stderr lines are also forwarded to
|
|
2603
|
+
``on_chunk`` in real time and merged into the returned stdout text.
|
|
2604
|
+
This is required for agents (e.g. Kimi Code 0.18.0+) that write all
|
|
2605
|
+
real-time progress to stderr, reserving stdout for a buffered final
|
|
2606
|
+
answer. Stderr activity also resets the idle timer so the agent is
|
|
2607
|
+
not killed while actively producing output.
|
|
2554
2608
|
- on_chunk(lines) is called with each decoded line so the caller can
|
|
2555
2609
|
forward to the progress reporter without waiting for completion.
|
|
2556
2610
|
- Idle timeout: if the agent produces no stdout for AGENT_IDLE_TIMEOUT
|
|
@@ -2582,7 +2636,28 @@ class ProcessManager:
|
|
|
2582
2636
|
stderr_chunks: list[bytes] = []
|
|
2583
2637
|
|
|
2584
2638
|
async def _read_stderr():
|
|
2585
|
-
if proc.stderr:
|
|
2639
|
+
if not proc.stderr:
|
|
2640
|
+
return
|
|
2641
|
+
if stream_stderr:
|
|
2642
|
+
# Agent (e.g. Kimi Code 0.18.0+) writes real-time progress to
|
|
2643
|
+
# stderr. Read line-by-line and forward to on_chunk so the
|
|
2644
|
+
# user sees output immediately. Also reset the idle timer so
|
|
2645
|
+
# the agent is not killed while actively working on stderr.
|
|
2646
|
+
while True:
|
|
2647
|
+
line_bytes = await proc.stderr.readline()
|
|
2648
|
+
if not line_bytes:
|
|
2649
|
+
break
|
|
2650
|
+
stderr_chunks.append(line_bytes)
|
|
2651
|
+
line = line_bytes.decode(errors="replace").rstrip("\n")
|
|
2652
|
+
if line:
|
|
2653
|
+
_last_activity_at[0] = time.monotonic()
|
|
2654
|
+
stdout_lines.append(line)
|
|
2655
|
+
if on_chunk:
|
|
2656
|
+
try:
|
|
2657
|
+
await on_chunk([line])
|
|
2658
|
+
except Exception:
|
|
2659
|
+
pass
|
|
2660
|
+
else:
|
|
2586
2661
|
data = await proc.stderr.read()
|
|
2587
2662
|
stderr_chunks.append(data)
|
|
2588
2663
|
|
|
@@ -2960,11 +3035,31 @@ class ProcessManager:
|
|
|
2960
3035
|
self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int, task_id: str,
|
|
2961
3036
|
on_chunk: Any = None,
|
|
2962
3037
|
) -> TaskResult:
|
|
2963
|
-
"""Run Kimi Code CLI in non-interactive
|
|
2964
|
-
|
|
2965
|
-
|
|
3038
|
+
"""Run Kimi Code CLI in non-interactive prompt mode.
|
|
3039
|
+
|
|
3040
|
+
Kimi Code 0.18.0+ removed ``--print`` and refuses to combine ``--yolo``
|
|
3041
|
+
or ``--auto`` with ``-p``. In prompt mode auto-approval is implicit,
|
|
3042
|
+
so a bare ``-p <prompt>`` is the correct non-interactive invocation.
|
|
3043
|
+
|
|
3044
|
+
Kimi 0.18.0+ writes all real-time progress (thinking, tool calls,
|
|
3045
|
+
status) to **stderr**, reserving stdout for a buffered final answer.
|
|
3046
|
+
We therefore stream stderr through ``on_chunk`` so the user sees
|
|
3047
|
+
output in real time instead of a blank panel until process exit.
|
|
3048
|
+
"""
|
|
3049
|
+
cmd = [agent.command, "-p", prompt]
|
|
3050
|
+
result = await self._run_cli(
|
|
3051
|
+
cmd, cwd, timeout, task_id, on_chunk=on_chunk,
|
|
3052
|
+
stream_stderr=True,
|
|
3053
|
+
)
|
|
2966
3054
|
parsed_metrics = self._parse_agent_jsonl_output(result.stdout)
|
|
2967
3055
|
result.metrics.update(parsed_metrics)
|
|
3056
|
+
# Kimi 0.18.0+ stores token usage in the session wire.jsonl file,
|
|
3057
|
+
# not in CLI output. Fall back to parsing that file when the
|
|
3058
|
+
# stdout/stderr parsers found nothing.
|
|
3059
|
+
if not result.metrics.get("token_input") and not result.metrics.get("token_output"):
|
|
3060
|
+
wire_metrics = self._parse_kimi_wire_metrics(result.stdout)
|
|
3061
|
+
if wire_metrics:
|
|
3062
|
+
result.metrics.update(wire_metrics)
|
|
2968
3063
|
return result
|
|
2969
3064
|
|
|
2970
3065
|
async def _run_copilot(
|
|
@@ -3076,6 +3171,7 @@ class ProcessManager:
|
|
|
3076
3171
|
stdin_input: str | None = None,
|
|
3077
3172
|
on_chunk: Any = None,
|
|
3078
3173
|
env: dict | None = None,
|
|
3174
|
+
stream_stderr: bool = False,
|
|
3079
3175
|
) -> TaskResult:
|
|
3080
3176
|
try:
|
|
3081
3177
|
proc = await asyncio.create_subprocess_exec(
|
|
@@ -3093,6 +3189,7 @@ class ProcessManager:
|
|
|
3093
3189
|
stdout, stderr, returncode = await self._stream_process(
|
|
3094
3190
|
proc, stdin_bytes, timeout, task_id, on_chunk,
|
|
3095
3191
|
workspace_path=cwd,
|
|
3192
|
+
stream_stderr=stream_stderr,
|
|
3096
3193
|
)
|
|
3097
3194
|
status = "success" if returncode == 0 else "failed"
|
|
3098
3195
|
return TaskResult(
|
|
@@ -3362,6 +3459,109 @@ class ProcessManager:
|
|
|
3362
3459
|
metrics["token_output"] = output
|
|
3363
3460
|
return metrics
|
|
3364
3461
|
|
|
3462
|
+
def _parse_kimi_wire_metrics(self, output: str) -> dict:
|
|
3463
|
+
"""Extract token usage from Kimi Code 0.18.0+ session wire.jsonl.
|
|
3464
|
+
|
|
3465
|
+
Kimi 0.18.0+ (full architecture rewrite) no longer prints
|
|
3466
|
+
``TokenUsage(...)`` repr lines to stdout/stderr. Instead, token
|
|
3467
|
+
usage is persisted in the session's ``wire.jsonl`` file as JSON
|
|
3468
|
+
events::
|
|
3469
|
+
|
|
3470
|
+
{"type":"usage.record",
|
|
3471
|
+
"model":"kimi-code/kimi-for-coding",
|
|
3472
|
+
"usage":{"inputOther":1241,"output":36,
|
|
3473
|
+
"inputCacheRead":14592,"inputCacheCreation":0},
|
|
3474
|
+
"usageScope":"turn"}
|
|
3475
|
+
|
|
3476
|
+
We extract the session id from the CLI output, resolve the session
|
|
3477
|
+
directory via ``session_index.jsonl``, then sum every
|
|
3478
|
+
``usage.record`` event to get cumulative token counts.
|
|
3479
|
+
"""
|
|
3480
|
+
import re as _re
|
|
3481
|
+
metrics: dict[str, Any] = {}
|
|
3482
|
+
|
|
3483
|
+
# 1. Extract session id from output — appears as:
|
|
3484
|
+
# "To resume this session: kimi -r session_<uuid>"
|
|
3485
|
+
# or JSON: "session_id":"session_<uuid>"
|
|
3486
|
+
session_match = _re.search(
|
|
3487
|
+
r"session_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
|
|
3488
|
+
output,
|
|
3489
|
+
)
|
|
3490
|
+
if not session_match:
|
|
3491
|
+
return metrics
|
|
3492
|
+
session_id = "session_" + session_match.group(1)
|
|
3493
|
+
|
|
3494
|
+
# 2. Resolve session directory via session_index.jsonl
|
|
3495
|
+
kimi_home = Path(os.environ.get("KIMI_CODE_HOME", Path.home() / ".kimi-code"))
|
|
3496
|
+
index_path = kimi_home / "session_index.jsonl"
|
|
3497
|
+
if not index_path.is_file():
|
|
3498
|
+
return metrics
|
|
3499
|
+
|
|
3500
|
+
session_dir: Path | None = None
|
|
3501
|
+
try:
|
|
3502
|
+
with open(index_path, encoding="utf-8", errors="replace") as fh:
|
|
3503
|
+
for line in fh:
|
|
3504
|
+
line = line.strip()
|
|
3505
|
+
if not line:
|
|
3506
|
+
continue
|
|
3507
|
+
try:
|
|
3508
|
+
entry = json.loads(line)
|
|
3509
|
+
except json.JSONDecodeError:
|
|
3510
|
+
continue
|
|
3511
|
+
if entry.get("sessionId") == session_id:
|
|
3512
|
+
sd = entry.get("sessionDir")
|
|
3513
|
+
if sd:
|
|
3514
|
+
session_dir = Path(sd)
|
|
3515
|
+
break
|
|
3516
|
+
except (OSError, IOError):
|
|
3517
|
+
return metrics
|
|
3518
|
+
|
|
3519
|
+
if not session_dir or not session_dir.is_dir():
|
|
3520
|
+
return metrics
|
|
3521
|
+
|
|
3522
|
+
# 3. Parse wire.jsonl for usage.record events
|
|
3523
|
+
wire_path = session_dir / "agents" / "main" / "wire.jsonl"
|
|
3524
|
+
if not wire_path.is_file():
|
|
3525
|
+
return metrics
|
|
3526
|
+
|
|
3527
|
+
total_input_other = 0
|
|
3528
|
+
total_output = 0
|
|
3529
|
+
total_cache_read = 0
|
|
3530
|
+
total_cache_creation = 0
|
|
3531
|
+
model: str | None = None
|
|
3532
|
+
|
|
3533
|
+
try:
|
|
3534
|
+
with open(wire_path, encoding="utf-8", errors="replace") as fh:
|
|
3535
|
+
for line in fh:
|
|
3536
|
+
line = line.strip()
|
|
3537
|
+
if not line:
|
|
3538
|
+
continue
|
|
3539
|
+
try:
|
|
3540
|
+
data = json.loads(line)
|
|
3541
|
+
except json.JSONDecodeError:
|
|
3542
|
+
continue
|
|
3543
|
+
if data.get("type") != "usage.record":
|
|
3544
|
+
continue
|
|
3545
|
+
usage = data.get("usage") or {}
|
|
3546
|
+
total_input_other += int(usage.get("inputOther") or 0)
|
|
3547
|
+
total_output += int(usage.get("output") or 0)
|
|
3548
|
+
total_cache_read += int(usage.get("inputCacheRead") or 0)
|
|
3549
|
+
total_cache_creation += int(usage.get("inputCacheCreation") or 0)
|
|
3550
|
+
if not model and data.get("model"):
|
|
3551
|
+
model = data["model"]
|
|
3552
|
+
except (OSError, IOError):
|
|
3553
|
+
return metrics
|
|
3554
|
+
|
|
3555
|
+
if total_output > 0 or total_input_other > 0 or total_cache_read > 0:
|
|
3556
|
+
metrics["token_input"] = (
|
|
3557
|
+
total_input_other + total_cache_read + total_cache_creation
|
|
3558
|
+
)
|
|
3559
|
+
metrics["token_output"] = total_output
|
|
3560
|
+
if model:
|
|
3561
|
+
metrics["model"] = model
|
|
3562
|
+
|
|
3563
|
+
return metrics
|
|
3564
|
+
|
|
3365
3565
|
def _parse_copilot_output(self, stdout: str) -> dict:
|
|
3366
3566
|
"""Extract metrics from GitHub Copilot CLI JSONL output.
|
|
3367
3567
|
|
|
@@ -4527,7 +4727,7 @@ class RuntimeDaemon:
|
|
|
4527
4727
|
"codex": "npm install -g @openai/codex",
|
|
4528
4728
|
"opencode": "curl -sSL https://opencode.ai/install | sh",
|
|
4529
4729
|
"gemini": "npm install -g @google/gemini-cli",
|
|
4530
|
-
"kimi": "curl -
|
|
4730
|
+
"kimi": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
|
|
4531
4731
|
"copilot": "Install VS Code GitHub Copilot Chat extension, or run: gh copilot. Then: gh auth login",
|
|
4532
4732
|
}
|
|
4533
4733
|
hint = _INSTALL_HINTS.get(task.agent_type, f"install the '{task.agent_type}' CLI tool")
|
|
@@ -4876,30 +5076,48 @@ class RuntimeDaemon:
|
|
|
4876
5076
|
or result.exit_code not in (None, -1) # crash: original guard
|
|
4877
5077
|
)
|
|
4878
5078
|
if can_attempt_recovery:
|
|
5079
|
+
original_exit_code = result.exit_code
|
|
4879
5080
|
committed_git = await self.process_manager._collect_git_info_vs_parent(workspace_path)
|
|
4880
5081
|
has_committed_changes = bool(committed_git.get("files_changed"))
|
|
4881
|
-
|
|
5082
|
+
has_uncommitted_changes = bool(pre_commit_git.get("files_changed"))
|
|
5083
|
+
has_no_uncommitted = not has_uncommitted_changes
|
|
4882
5084
|
has_tokens = (
|
|
4883
5085
|
int(result.metrics.get("token_input", 0) or 0)
|
|
4884
5086
|
+ int(result.metrics.get("token_output", 0) or 0)
|
|
4885
5087
|
) > 0
|
|
4886
5088
|
has_meaningful_output = self.process_manager.has_meaningful_agent_output(result)
|
|
5089
|
+
recovered_from_workspace_timeout = (
|
|
5090
|
+
self._recover_timeout_from_workspace_changes(
|
|
5091
|
+
task, workspace_path, result, pre_commit_git,
|
|
5092
|
+
)
|
|
5093
|
+
if has_uncommitted_changes else False
|
|
5094
|
+
)
|
|
4887
5095
|
# Timeout recovery requires stronger evidence: committed work + tokens.
|
|
4888
5096
|
# Crash recovery (original): committed + (tokens OR meaningful output).
|
|
4889
5097
|
sufficient_evidence = (
|
|
4890
|
-
|
|
5098
|
+
recovered_from_workspace_timeout
|
|
5099
|
+
or (has_committed_changes and has_no_uncommitted and has_tokens and has_meaningful_output)
|
|
4891
5100
|
if is_timeout_failure
|
|
4892
5101
|
else (has_committed_changes and has_no_uncommitted and (has_tokens or has_meaningful_output))
|
|
4893
5102
|
)
|
|
4894
5103
|
if sufficient_evidence:
|
|
4895
|
-
_reason =
|
|
5104
|
+
_reason = (
|
|
5105
|
+
"timed out after producing valid workspace changes"
|
|
5106
|
+
if recovered_from_workspace_timeout
|
|
5107
|
+
else (
|
|
5108
|
+
"timed out but already committed changes"
|
|
5109
|
+
if is_timeout_failure else f"exited with code {original_exit_code}"
|
|
5110
|
+
)
|
|
5111
|
+
)
|
|
4896
5112
|
logger.warning(
|
|
4897
5113
|
"Task %s agent %s — recovering as success",
|
|
4898
5114
|
task.task_id, _reason,
|
|
4899
5115
|
)
|
|
4900
5116
|
result.status = "success"
|
|
4901
5117
|
result.error = ""
|
|
4902
|
-
result.
|
|
5118
|
+
result.exit_code = 0
|
|
5119
|
+
result.failure_code = ""
|
|
5120
|
+
result.metrics["recovered_from_exit_code"] = original_exit_code
|
|
4903
5121
|
|
|
4904
5122
|
# 4.5 Layer 2: Validation gate — check outputs before committing
|
|
4905
5123
|
if result.status == "success":
|
|
@@ -5060,6 +5278,10 @@ class RuntimeDaemon:
|
|
|
5060
5278
|
result.lines_removed = result.git.get("lines_removed", 0)
|
|
5061
5279
|
else:
|
|
5062
5280
|
result.git = pre_commit_git
|
|
5281
|
+
if not result.files_changed and pre_commit_git.get("files_changed"):
|
|
5282
|
+
result.files_changed = pre_commit_git["files_changed"]
|
|
5283
|
+
result.lines_added = pre_commit_git.get("lines_added", 0)
|
|
5284
|
+
result.lines_removed = pre_commit_git.get("lines_removed", 0)
|
|
5063
5285
|
|
|
5064
5286
|
# 6. Report completion (include actual agent used if different from requested)
|
|
5065
5287
|
result.metrics["actual_agent"] = agent.agent_id
|
|
@@ -5385,7 +5607,7 @@ class RuntimeDaemon:
|
|
|
5385
5607
|
"codex": "npm install -g @openai/codex",
|
|
5386
5608
|
"opencode": "curl -sSL https://opencode.ai/install | sh",
|
|
5387
5609
|
"gemini": "npm install -g @google/gemini-cli",
|
|
5388
|
-
"kimi": "curl -
|
|
5610
|
+
"kimi": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
|
|
5389
5611
|
"copilot": "Install VS Code GitHub Copilot Chat extension, or run: gh copilot. Then: gh auth login",
|
|
5390
5612
|
}
|
|
5391
5613
|
hint = _INSTALL_HINTS.get(agent_type, f"install the '{agent_type}' CLI tool")
|
|
@@ -6283,6 +6505,23 @@ class RuntimeDaemon:
|
|
|
6283
6505
|
|
|
6284
6506
|
return "\n".join(parts).rstrip()
|
|
6285
6507
|
|
|
6508
|
+
async def _fetch_branch_tracking_ref(
|
|
6509
|
+
self,
|
|
6510
|
+
workspace_path: Path,
|
|
6511
|
+
branch: str,
|
|
6512
|
+
project_key: str = "default",
|
|
6513
|
+
*,
|
|
6514
|
+
timeout: int = 120,
|
|
6515
|
+
) -> None:
|
|
6516
|
+
"""Refresh origin/<branch> explicitly, even inside single-branch clones."""
|
|
6517
|
+
await self.workspace_manager._git(
|
|
6518
|
+
"fetch", "origin",
|
|
6519
|
+
f"{branch}:refs/remotes/origin/{branch}",
|
|
6520
|
+
cwd=workspace_path,
|
|
6521
|
+
timeout=timeout,
|
|
6522
|
+
project_key=project_key,
|
|
6523
|
+
)
|
|
6524
|
+
|
|
6286
6525
|
@staticmethod
|
|
6287
6526
|
def _is_test_path(path: str) -> bool:
|
|
6288
6527
|
p = path.lower()
|
|
@@ -7103,9 +7342,11 @@ class RuntimeDaemon:
|
|
|
7103
7342
|
# fail with "non-fast-forward". This is the single most reliable
|
|
7104
7343
|
# guard for cross-runtime / cross-machine collaboration scenarios.
|
|
7105
7344
|
try:
|
|
7106
|
-
await
|
|
7107
|
-
|
|
7108
|
-
|
|
7345
|
+
await self._fetch_branch_tracking_ref(
|
|
7346
|
+
workspace_path,
|
|
7347
|
+
branch,
|
|
7348
|
+
project_key,
|
|
7349
|
+
timeout=300,
|
|
7109
7350
|
)
|
|
7110
7351
|
except RuntimeError as _pre_push_fetch_exc:
|
|
7111
7352
|
logger.warning(
|
|
@@ -7265,9 +7506,10 @@ class RuntimeDaemon:
|
|
|
7265
7506
|
)
|
|
7266
7507
|
try:
|
|
7267
7508
|
# 1. Fetch the specific remote branch
|
|
7268
|
-
await
|
|
7269
|
-
|
|
7270
|
-
|
|
7509
|
+
await self._fetch_branch_tracking_ref(
|
|
7510
|
+
workspace_path,
|
|
7511
|
+
branch,
|
|
7512
|
+
project_key,
|
|
7271
7513
|
)
|
|
7272
7514
|
# 2. Rebase local commits onto the updated remote HEAD.
|
|
7273
7515
|
# Use -X theirs so that when the same file was modified by
|
|
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
|