hexcli 2.19.0__tar.gz → 2.20.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.
- {hexcli-2.19.0 → hexcli-2.20.1}/CHANGELOG.md +49 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/PKG-INFO +1 -1
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/__init__.py +1 -1
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/agent.py +57 -9
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/tools.py +13 -2
- {hexcli-2.19.0 → hexcli-2.20.1}/.gitignore +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/Hex CLI.cmd +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/LICENSE +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/README.md +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/assets/hexcli.ico +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/assets/hexcli.png +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/cancel.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/chatlog.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/commands.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/compaction.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/config.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/diffview.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/distribution.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/doctor.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/editing.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/http_client.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/launcher.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/lineedit.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/llm.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/lockfile.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/markdown_stream.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/memory.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/network.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/parsing.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/paths.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/prompts.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/psparse.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/repl.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/safety.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/sessions.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/setup_wizard.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/statusbar.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/stream_render.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/telemetry.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/hexcli/ui.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/install.ps1 +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/launcher.py +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/pyproject.toml +0 -0
- {hexcli-2.19.0 → hexcli-2.20.1}/shellai.example.json +0 -0
|
@@ -4,6 +4,55 @@ Full evidence for every claim below — including the experiments that failed
|
|
|
4
4
|
lives in `docs/V2_PLAN.md` §14. Numbers are pass^k over repeated live runs on
|
|
5
5
|
the Hexagon NPU, not single-run anecdotes.
|
|
6
6
|
|
|
7
|
+
## 2.20.1 — 2026-10-10
|
|
8
|
+
|
|
9
|
+
A patch release: nothing the model reads changes. The turn's return value
|
|
10
|
+
and the history entry for a stopped turn are byte-identical to 2.20.0.
|
|
11
|
+
|
|
12
|
+
- A turn the loop stops now says which tool failed, on which file, and
|
|
13
|
+
whether any file changed: "Stopped after edit_file failed three times on
|
|
14
|
+
calculator.py; no file was changed." instead of "Stopped: edit_file failed
|
|
15
|
+
three times in a row." 33 recorded turns ended on a loop or step-limit stop
|
|
16
|
+
with only the raw `old_string was not found` error behind them, and none of
|
|
17
|
+
them said whether the file had been touched. The file clause comes from the
|
|
18
|
+
writes that landed this turn, not from the attempts, so a turn that wrote
|
|
19
|
+
`notes.txt` and then got stuck on `calculator.py` names `notes.txt`; a turn
|
|
20
|
+
that never tried to change a file gets no clause.
|
|
21
|
+
- Off a terminal (`hex "..." | clip`, scripts) a stopped turn printed
|
|
22
|
+
nothing at all. It prints the stop sentence. The raw tool output still goes
|
|
23
|
+
to the session history, where the next turn reads it.
|
|
24
|
+
|
|
25
|
+
## 2.20.0 — 2026-09-18
|
|
26
|
+
|
|
27
|
+
A minor release: a tool result the model reads changes (`run_code` on a
|
|
28
|
+
program that waits for the keyboard).
|
|
29
|
+
|
|
30
|
+
> **Gate: PASS** on the pinned 24-case set: 21 cases held 5/5 in the arm;
|
|
31
|
+
> `casual-4`, `factual-4` and `memory-1` lost four, two and four runs to
|
|
32
|
+
> backend timeouts and were re-run on a fresh server at six runs, 6/6 each.
|
|
33
|
+
> Own arm, seed 20260918. **Platform: 13 invalid of 245 runs (5.3 %)**, the
|
|
34
|
+
> server's working set again above 6 GB with 1.6 GB free, so the ceiling
|
|
35
|
+
> comparison carries the `[PLATFORM]` flag: run-level 402/514 vs 166/217,
|
|
36
|
+
> −1.7 %, Fisher p = 0.63; pass^k 26 → 30 of 46 shared cases, McNemar
|
|
37
|
+
> p = 0.13. The change did what it is for: `run_code` timed out 0 times in
|
|
38
|
+
> the arm against 10 in the 2.12.0 arm (all of them `make-py-1`'s
|
|
39
|
+
> calculator waiting on `input()`), the note fired twice, and `make-py-1`
|
|
40
|
+
> stayed 5/5. Smoke 10/10 on a fresh server.
|
|
41
|
+
|
|
42
|
+
- `run_code` gives the program no keyboard, and says so when the program
|
|
43
|
+
asked for one. The child inherited the terminal's standard input, so a
|
|
44
|
+
program that calls `input()` sat waiting on the owner's console until the
|
|
45
|
+
tool's timeout: on 2026-09-18 "create a python cli hilo guessing game and
|
|
46
|
+
run it" wrote a working game and then waited the full ten seconds, twice,
|
|
47
|
+
to report "TIMEOUT". Standard input is now closed for the child, so
|
|
48
|
+
`input()` raises `EOFError` at once, and when it does the result adds one
|
|
49
|
+
line: the program reads from the keyboard, `run_code` has none to give
|
|
50
|
+
it, and it has to be started in a terminal, with the command. On record:
|
|
51
|
+
12 `run_code` timeouts whose output ends in a prompt (ten of them the
|
|
52
|
+
`make-py-1` calculator) and 19 runs that already hit `EOFError` under the
|
|
53
|
+
eval runner, which has no terminal; 0.6 % of recorded runs, all of them
|
|
54
|
+
this shape.
|
|
55
|
+
|
|
7
56
|
## 2.19.0 — 2026-09-18
|
|
8
57
|
|
|
9
58
|
A minor release: nudge text the model reads is new (the claimed-fix rule)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hexcli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.20.1
|
|
4
4
|
Summary: Local Hexagon NPU terminal agent for Snapdragon X Elite Windows ARM64
|
|
5
5
|
Project-URL: Homepage, https://github.com/NathanL15/Hex-CLI
|
|
6
6
|
Project-URL: Repository, https://github.com/NathanL15/Hex-CLI
|
|
@@ -65,17 +65,48 @@ last_streamed_matches = llm.last_streamed_matches
|
|
|
65
65
|
_LAST_TURN_STOP: str | None = None
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
# The one sentence the user gets for a stopped turn. The turn's return value
|
|
69
|
+
# stays the raw tool output because the history keeps it for the next turn;
|
|
70
|
+
# this is what the console notice and a pipe show instead.
|
|
71
|
+
_LAST_STOP_NOTICE: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _mark_turn_stopped(how: str, notice: str = "") -> None:
|
|
75
|
+
global _LAST_TURN_STOP, _LAST_STOP_NOTICE
|
|
70
76
|
_LAST_TURN_STOP = how
|
|
77
|
+
_LAST_STOP_NOTICE = notice
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def last_stop_notice() -> str:
|
|
81
|
+
return _LAST_STOP_NOTICE
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _stop_notice(how: str, tool: str = "", path: str = "", *,
|
|
85
|
+
identical: bool = False, changed: list[str] | None = None,
|
|
86
|
+
tried_to_change: bool = False) -> str:
|
|
87
|
+
"""Say why a turn stopped and whether any file changed, in one sentence."""
|
|
88
|
+
if how == "loop":
|
|
89
|
+
what = "returned the same result" if identical else "failed"
|
|
90
|
+
where = f" on {Path(path).name}" if path else ""
|
|
91
|
+
head = f"Stopped after {tool} {what} three times{where}"
|
|
92
|
+
else:
|
|
93
|
+
head = "Stopped at the step limit"
|
|
94
|
+
names = list(dict.fromkeys(Path(p).name for p in (changed or [])))
|
|
95
|
+
if names:
|
|
96
|
+
verb = "was" if len(names) == 1 else "were"
|
|
97
|
+
return f"{head}; {', '.join(names)} {verb} changed."
|
|
98
|
+
if tried_to_change:
|
|
99
|
+
return f"{head}; no file was changed."
|
|
100
|
+
return f"{head}."
|
|
71
101
|
|
|
72
102
|
|
|
73
103
|
def clear_turn_stop() -> None:
|
|
74
104
|
"""Called by the REPL before each turn. Also forgets what the previous
|
|
75
105
|
turn streamed: a turn that never calls the model (help, small talk)
|
|
76
106
|
must not match the box-skip against stale text."""
|
|
77
|
-
global _LAST_TURN_STOP
|
|
107
|
+
global _LAST_TURN_STOP, _LAST_STOP_NOTICE
|
|
78
108
|
_LAST_TURN_STOP = None
|
|
109
|
+
_LAST_STOP_NOTICE = ""
|
|
79
110
|
llm._LAST_STREAMED_TEXT = ""
|
|
80
111
|
|
|
81
112
|
|
|
@@ -1166,6 +1197,7 @@ def _run_autopilot_turn(
|
|
|
1166
1197
|
# One nudge per turn — it guides, never traps.
|
|
1167
1198
|
_unverified_mutation = False
|
|
1168
1199
|
_mutated_ok = False # a write/edit/append that did not error, this turn
|
|
1200
|
+
_changed_paths: list[str] = [] # the paths those landed on, for a stop notice
|
|
1169
1201
|
_verify_nudge_used = False
|
|
1170
1202
|
# The user asked for the tests to be run: what the run tools executed
|
|
1171
1203
|
# this turn, so a finish without a test run can be sent back once.
|
|
@@ -1478,6 +1510,8 @@ def _run_autopilot_turn(
|
|
|
1478
1510
|
if tool_name in {"edit_file", "write_file", "append_file"} and not _is_error:
|
|
1479
1511
|
_unverified_mutation = True
|
|
1480
1512
|
_mutated_ok = True
|
|
1513
|
+
if tool_path:
|
|
1514
|
+
_changed_paths.append(str(tool_path))
|
|
1481
1515
|
elif tool_name in {"read_file", "run_code", "verify_syntax", "lint_code",
|
|
1482
1516
|
"run_command"} and not _is_error:
|
|
1483
1517
|
_unverified_mutation = False
|
|
@@ -1497,10 +1531,14 @@ def _run_autopilot_turn(
|
|
|
1497
1531
|
and all(err for _t, _tgt, err, _out in _loop_tracker)
|
|
1498
1532
|
and len({(t, tgt) for t, tgt, _e, _out in _loop_tracker}) == 1)
|
|
1499
1533
|
if _identical_trip or _failure_trip:
|
|
1500
|
-
what = f"{tool_name} returned the same result" if _identical_trip else f"{tool_name} failed"
|
|
1501
1534
|
if not _in_delegate: # a sub-agent's stop is its own result, not the turn's
|
|
1502
|
-
|
|
1503
|
-
|
|
1535
|
+
notice = _stop_notice(
|
|
1536
|
+
"loop", tool_name, str(tool_path or ""),
|
|
1537
|
+
identical=_identical_trip and not _failure_trip,
|
|
1538
|
+
changed=_changed_paths,
|
|
1539
|
+
tried_to_change=any(t in _MUTATING_TOOLS for t in tools_used))
|
|
1540
|
+
cprint(f"\n ⚠ {notice}", C.BYELLOW)
|
|
1541
|
+
_mark_turn_stopped("loop", notice)
|
|
1504
1542
|
memory.maybe_index_turn(config, query, tools_used, touched_paths, outcome="error_loop")
|
|
1505
1543
|
if session:
|
|
1506
1544
|
_record_undo_snapshots(session, _turn_snapshots)
|
|
@@ -1511,8 +1549,11 @@ def _run_autopilot_turn(
|
|
|
1511
1549
|
|
|
1512
1550
|
memory.maybe_index_turn(config, query, tools_used, touched_paths, outcome="step_limit")
|
|
1513
1551
|
if not _in_delegate: # a sub-agent's cap is routine; its text comes back as a tool result
|
|
1514
|
-
|
|
1515
|
-
|
|
1552
|
+
notice = _stop_notice(
|
|
1553
|
+
"step_limit", changed=_changed_paths,
|
|
1554
|
+
tried_to_change=any(t in _MUTATING_TOOLS for t in tools_used))
|
|
1555
|
+
cprint(f"\n ⚠ {notice}", C.BYELLOW)
|
|
1556
|
+
_mark_turn_stopped("step_limit", notice)
|
|
1516
1557
|
if session:
|
|
1517
1558
|
_record_undo_snapshots(session, _turn_snapshots)
|
|
1518
1559
|
_probe(probe, "on_end", "step_limit", last_tool_output or "Done.")
|
|
@@ -2057,7 +2098,14 @@ def one_shot_autopilot(config: dict[str, Any], query: str, shell_exe: str) -> in
|
|
|
2057
2098
|
tel.record_turn(turn)
|
|
2058
2099
|
append_session_message(session, "assistant", message)
|
|
2059
2100
|
sync_session_store(sessions, session)
|
|
2060
|
-
if
|
|
2101
|
+
if last_turn_stopped():
|
|
2102
|
+
if sys.stdout.isatty():
|
|
2103
|
+
print()
|
|
2104
|
+
else:
|
|
2105
|
+
# A pipe used to get nothing for a stopped turn. It gets the stop
|
|
2106
|
+
# sentence; the raw tool output stays in the history only.
|
|
2107
|
+
print(last_stop_notice())
|
|
2108
|
+
elif last_streamed_matches(message):
|
|
2061
2109
|
if sys.stdout.isatty():
|
|
2062
2110
|
print()
|
|
2063
2111
|
elif not sys.stdout.isatty():
|
|
@@ -918,8 +918,13 @@ def run_code_tool(
|
|
|
918
918
|
)
|
|
919
919
|
cmd = [*cmd_prefix, str(path), *[str(a) for a in run_args]]
|
|
920
920
|
try:
|
|
921
|
+
# No keyboard. With the parent's stdin inherited, a program that
|
|
922
|
+
# calls input() sat on the owner's terminal until the timeout (10 s,
|
|
923
|
+
# twice, 2026-09-18: "create a python cli hilo guessing game and run
|
|
924
|
+
# it"); with stdin closed it raises EOFError at once, and the note
|
|
925
|
+
# below tells the model what that means.
|
|
921
926
|
proc = subprocess.Popen(
|
|
922
|
-
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
927
|
+
cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
923
928
|
text=True, encoding="utf-8", errors="replace",
|
|
924
929
|
)
|
|
925
930
|
except (PermissionError, FileNotFoundError, OSError) as exc:
|
|
@@ -940,10 +945,16 @@ def run_code_tool(
|
|
|
940
945
|
out = trim_text(stdout, output_limit)
|
|
941
946
|
err = trim_text(stderr, output_limit)
|
|
942
947
|
ui.tool_event("run", f"{path} (exit {proc.returncode})")
|
|
948
|
+
note = ""
|
|
949
|
+
if "EOFError" in stderr:
|
|
950
|
+
note = (f"\n\nThe program stopped where it reads from the keyboard (input()). "
|
|
951
|
+
f"run_code has no keyboard to give it, so it cannot be played or driven "
|
|
952
|
+
f"here; it has to be started in a terminal: python {path.name}. The code "
|
|
953
|
+
f"up to that point ran.")
|
|
943
954
|
return (
|
|
944
955
|
f"Exit code: {proc.returncode}\n\n"
|
|
945
956
|
f"[stdout]\n{out or '(empty)'}\n\n"
|
|
946
|
-
f"[stderr]\n{err or '(empty)'}"
|
|
957
|
+
f"[stderr]\n{err or '(empty)'}{note}"
|
|
947
958
|
)
|
|
948
959
|
|
|
949
960
|
|
|
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
|
|
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
|