hexcli 2.18.0__tar.gz → 2.20.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.
- {hexcli-2.18.0 → hexcli-2.20.0}/CHANGELOG.md +74 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/PKG-INFO +1 -1
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/__init__.py +1 -1
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/agent.py +38 -6
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/tools.py +13 -2
- {hexcli-2.18.0 → hexcli-2.20.0}/.gitignore +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/Hex CLI.cmd +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/LICENSE +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/README.md +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/assets/hexcli.ico +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/assets/hexcli.png +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/cancel.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/chatlog.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/commands.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/compaction.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/config.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/diffview.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/distribution.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/doctor.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/editing.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/http_client.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/launcher.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/lineedit.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/llm.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/lockfile.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/markdown_stream.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/memory.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/network.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/parsing.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/paths.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/prompts.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/psparse.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/repl.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/safety.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/sessions.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/setup_wizard.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/statusbar.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/stream_render.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/telemetry.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/hexcli/ui.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/install.ps1 +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/launcher.py +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/pyproject.toml +0 -0
- {hexcli-2.18.0 → hexcli-2.20.0}/shellai.example.json +0 -0
|
@@ -4,6 +4,80 @@ 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.0 — 2026-09-18
|
|
8
|
+
|
|
9
|
+
A minor release: a tool result the model reads changes (`run_code` on a
|
|
10
|
+
program that waits for the keyboard).
|
|
11
|
+
|
|
12
|
+
> **Gate: PASS** on the pinned 24-case set: 21 cases held 5/5 in the arm;
|
|
13
|
+
> `casual-4`, `factual-4` and `memory-1` lost four, two and four runs to
|
|
14
|
+
> backend timeouts and were re-run on a fresh server at six runs, 6/6 each.
|
|
15
|
+
> Own arm, seed 20260918. **Platform: 13 invalid of 245 runs (5.3 %)**, the
|
|
16
|
+
> server's working set again above 6 GB with 1.6 GB free, so the ceiling
|
|
17
|
+
> comparison carries the `[PLATFORM]` flag: run-level 402/514 vs 166/217,
|
|
18
|
+
> −1.7 %, Fisher p = 0.63; pass^k 26 → 30 of 46 shared cases, McNemar
|
|
19
|
+
> p = 0.13. The change did what it is for: `run_code` timed out 0 times in
|
|
20
|
+
> the arm against 10 in the 2.12.0 arm (all of them `make-py-1`'s
|
|
21
|
+
> calculator waiting on `input()`), the note fired twice, and `make-py-1`
|
|
22
|
+
> stayed 5/5. Smoke 10/10 on a fresh server.
|
|
23
|
+
|
|
24
|
+
- `run_code` gives the program no keyboard, and says so when the program
|
|
25
|
+
asked for one. The child inherited the terminal's standard input, so a
|
|
26
|
+
program that calls `input()` sat waiting on the owner's console until the
|
|
27
|
+
tool's timeout: on 2026-09-18 "create a python cli hilo guessing game and
|
|
28
|
+
run it" wrote a working game and then waited the full ten seconds, twice,
|
|
29
|
+
to report "TIMEOUT". Standard input is now closed for the child, so
|
|
30
|
+
`input()` raises `EOFError` at once, and when it does the result adds one
|
|
31
|
+
line: the program reads from the keyboard, `run_code` has none to give
|
|
32
|
+
it, and it has to be started in a terminal, with the command. On record:
|
|
33
|
+
12 `run_code` timeouts whose output ends in a prompt (ten of them the
|
|
34
|
+
`make-py-1` calculator) and 19 runs that already hit `EOFError` under the
|
|
35
|
+
eval runner, which has no terminal; 0.6 % of recorded runs, all of them
|
|
36
|
+
this shape.
|
|
37
|
+
|
|
38
|
+
## 2.19.0 — 2026-09-18
|
|
39
|
+
|
|
40
|
+
A minor release: nudge text the model reads is new (the claimed-fix rule)
|
|
41
|
+
and two existing rules fire where they did not.
|
|
42
|
+
|
|
43
|
+
> **Gate: PASS** on the pinned 24-case set: 21 cases held 5/5 in the arm;
|
|
44
|
+
> `missing-file-1`, `missing-file-2` and `regression-count-1` lost one, three
|
|
45
|
+
> and one runs to backend timeouts and were re-run on a fresh server at six
|
|
46
|
+
> runs, 6/6 each. Own arm, seed 20260918. **Platform: 16 invalid of 245 runs
|
|
47
|
+
> (6.5 %)** -- the model server's working set grew from 3 GB to 6.7 GB over
|
|
48
|
+
> the hour and the machine fell to 1.2 GB free, so the ceiling comparison
|
|
49
|
+
> carries the `[PLATFORM]` flag: run-level 391/502 vs 161/214, −2.6 %, Fisher p = 0.44;
|
|
50
|
+
> pass^k 26 → 30 of 46 shared cases, McNemar p = 0.13; every ceiling drop
|
|
51
|
+
> (`livestate-1` 1/4, `tests-claim-1` 0/2, `claims-1` 1/5) is on a case that
|
|
52
|
+
> lost runs to timeouts. **The new and widened rules did not fire once in
|
|
53
|
+
> the arm**, so the arm is evidence of no regression on the gated set and
|
|
54
|
+
> the effect rests on the replay below. Smoke 10/10 on a fresh server.
|
|
55
|
+
|
|
56
|
+
- The intent nudge counts only a mutation that landed, catches a claimed
|
|
57
|
+
fix with no change behind it, and reads "cannot be created" however the
|
|
58
|
+
sentence goes on. Three gaps, all from the owner's 2026-09-18 session.
|
|
59
|
+
The loop had been passing "a tool with a path was called" as *mutated*,
|
|
60
|
+
which an `edit_file` on a missing file satisfies, so when the turn ended
|
|
61
|
+
"A calculator app cannot be created without a matching file" right after
|
|
62
|
+
exactly that error, the create rule stayed silent -- and its wording only
|
|
63
|
+
knew "cannot be created in this environment" anyway. Later, "fix it" was
|
|
64
|
+
answered with "The file has been fixed by removing the malformed line"
|
|
65
|
+
after a `run_code` that showed the SyntaxError and no edit at all; nothing
|
|
66
|
+
looked at that. Now *mutated* means a write, edit or append that did not
|
|
67
|
+
error; the denial wording matches `cannot be (built|created|done|made)`
|
|
68
|
+
with any continuation; and a request to fix, correct, update or change
|
|
69
|
+
something (an instruction, not a question) answered with "has been fixed",
|
|
70
|
+
"was corrected", "is now updated" and no mutation gets one nudge to make
|
|
71
|
+
the change or say plainly that nothing was changed. Replayed over the 206
|
|
72
|
+
real turns and 5,389 recorded runs with the loop's own semantics: on real
|
|
73
|
+
turns the nudge fires 11 times, all genuine (two new: the refusal above
|
|
74
|
+
and the claimed fix); on recorded runs the new rule fires on seven, five
|
|
75
|
+
of them `ambiguous-2` answering "Update the file." with "Updated the file
|
|
76
|
+
as requested" and no tool call, two of them `tests-claim-1` claiming a
|
|
77
|
+
correction it never made. The question form is what keeps `memory-1`
|
|
78
|
+
("which file did you fix?") out: its honest recall fired 14 times in the
|
|
79
|
+
first draft and zero now.
|
|
80
|
+
|
|
7
81
|
## 2.18.0 — 2026-09-18
|
|
8
82
|
|
|
9
83
|
A minor release: what `write_file` writes for one shape of body changes,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hexcli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.20.0
|
|
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
|
|
@@ -1165,6 +1165,7 @@ def _run_autopilot_turn(
|
|
|
1165
1165
|
# must observe something (run/read/check) before its answer is accepted.
|
|
1166
1166
|
# One nudge per turn — it guides, never traps.
|
|
1167
1167
|
_unverified_mutation = False
|
|
1168
|
+
_mutated_ok = False # a write/edit/append that did not error, this turn
|
|
1168
1169
|
_verify_nudge_used = False
|
|
1169
1170
|
# The user asked for the tests to be run: what the run tools executed
|
|
1170
1171
|
# this turn, so a finish without a test run can be sent back once.
|
|
@@ -1343,8 +1344,8 @@ def _run_autopilot_turn(
|
|
|
1343
1344
|
# claimed to have checked with no tool call at all.
|
|
1344
1345
|
if not _intent_nudge_used and config.get("require_verification", True):
|
|
1345
1346
|
_intent_text = _intent_nudge(
|
|
1346
|
-
query, msg, mutated=
|
|
1347
|
-
tools_used=tools_used)
|
|
1347
|
+
query, msg, mutated=_mutated_ok, ran=_ran_anything,
|
|
1348
|
+
tools_used=tools_used, touched=bool(touched_paths))
|
|
1348
1349
|
if _intent_text:
|
|
1349
1350
|
_intent_nudge_used = True
|
|
1350
1351
|
messages.append({"role": "assistant", "content": strip_thinking(raw)})
|
|
@@ -1476,6 +1477,7 @@ def _run_autopilot_turn(
|
|
|
1476
1477
|
_is_error = tool_output.lstrip().startswith("Error:")
|
|
1477
1478
|
if tool_name in {"edit_file", "write_file", "append_file"} and not _is_error:
|
|
1478
1479
|
_unverified_mutation = True
|
|
1480
|
+
_mutated_ok = True
|
|
1479
1481
|
elif tool_name in {"read_file", "run_code", "verify_syntax", "lint_code",
|
|
1480
1482
|
"run_command"} and not _is_error:
|
|
1481
1483
|
_unverified_mutation = False
|
|
@@ -1790,9 +1792,22 @@ _WANTS_FIND_RE = re.compile(
|
|
|
1790
1792
|
# means, which for a file-writing agent is false.
|
|
1791
1793
|
_DENIES_MEANS_RE = re.compile(
|
|
1792
1794
|
r"\b(no tools? (are )?available|not feasible|unable to (build|create|make|generate)"
|
|
1793
|
-
r"|cannot be (built|created|done
|
|
1795
|
+
r"|cannot be (built|created|done|made)\b"
|
|
1794
1796
|
r"|outside the (scope|capabilities)|no (tool|way|mechanism) (to|for)\b)", re.IGNORECASE)
|
|
1795
1797
|
|
|
1798
|
+
# "fix it" answered with "has been fixed" and no file changed. 2026-09-18
|
|
1799
|
+
# 07:03: "The file has been fixed by removing the malformed line" after a
|
|
1800
|
+
# run_code that showed the SyntaxError and no edit at all. The request has
|
|
1801
|
+
# to be an instruction, not a question: memory-1 asks "which file did you
|
|
1802
|
+
# fix?" and the honest recall "the syntax error was fixed in buggy_calc.py"
|
|
1803
|
+
# must not fire (it did, in the first draft, 14 runs of a 12/12 case).
|
|
1804
|
+
_WANTS_FIX_RE = re.compile(
|
|
1805
|
+
r"\b(fix|correct|repair|patch|update|change|modify|edit)\b", re.IGNORECASE)
|
|
1806
|
+
_CLAIMS_FIXED_RE = re.compile(
|
|
1807
|
+
r"\b(has been|have been|was|were|is now|are now|successfully)\s+"
|
|
1808
|
+
r"(fixed|corrected|updated|modified|changed|repaired|resolved|patched)\b"
|
|
1809
|
+
r"|\b(fixed|corrected|updated|modified|repaired|patched) (the|it|this)\b", re.IGNORECASE)
|
|
1810
|
+
|
|
1796
1811
|
_CLAIMS_NEGATIVE_RE = re.compile(
|
|
1797
1812
|
r"\b(no|none|not|nothing|couldn't|could not|unable to)\b[^.]{0,40}"
|
|
1798
1813
|
r"\b(found|find|locate|exists?|present)\b", re.IGNORECASE)
|
|
@@ -1926,10 +1941,22 @@ def _contradicted_not_found(msg: str, outputs: list[str]) -> str:
|
|
|
1926
1941
|
|
|
1927
1942
|
|
|
1928
1943
|
def _intent_nudge(query: str, msg: str, *, mutated: bool, ran: bool,
|
|
1929
|
-
tools_used: list[str]) -> str:
|
|
1944
|
+
tools_used: list[str], touched: bool | None = None) -> str:
|
|
1930
1945
|
"""One nudge when the turn did none of the work the request implies.
|
|
1931
|
-
Empty string when there is nothing to say.
|
|
1932
|
-
|
|
1946
|
+
Empty string when there is nothing to say.
|
|
1947
|
+
|
|
1948
|
+
`mutated` is a write/edit/append that SUCCEEDED this turn. Until
|
|
1949
|
+
2026-09-18 the loop passed "any tool with a path was called", which is
|
|
1950
|
+
true after a read_file or after an edit_file that errored, so the
|
|
1951
|
+
create rule below was silenced by the very failure it exists for: the
|
|
1952
|
+
owner's "please create a working calculator app" turn ended "A
|
|
1953
|
+
calculator app cannot be created without a matching file" right after
|
|
1954
|
+
an edit_file on a file that did not exist. `touched` keeps the run
|
|
1955
|
+
rule's older, wider sense (a file was at least read) for callers that
|
|
1956
|
+
have it."""
|
|
1957
|
+
if touched is None:
|
|
1958
|
+
touched = mutated
|
|
1959
|
+
if (_WANTS_RUN_RE.search(query) and touched and not ran
|
|
1933
1960
|
and not _asks_to_run_tests(query)):
|
|
1934
1961
|
return ("You were asked to run it and nothing was run this turn. Run it now "
|
|
1935
1962
|
"with run_code (or run_command) and report the output you actually "
|
|
@@ -1949,6 +1976,11 @@ def _intent_nudge(query: str, msg: str, *, mutated: bool, ran: bool,
|
|
|
1949
1976
|
return ("You said you checked, but this turn made no tool call at all. "
|
|
1950
1977
|
"Check it with a tool and report what the tool returned, or say "
|
|
1951
1978
|
"plainly that you did not check. Respond with JSON only.")
|
|
1979
|
+
if (_WANTS_FIX_RE.search(query) and "?" not in query and not mutated
|
|
1980
|
+
and _CLAIMS_FIXED_RE.search(msg)):
|
|
1981
|
+
return ("You said it was fixed, but no file was changed this turn. Make "
|
|
1982
|
+
"the change with edit_file (or write_file) and then verify it, or "
|
|
1983
|
+
"say plainly that nothing was changed. Respond with JSON only.")
|
|
1952
1984
|
return ""
|
|
1953
1985
|
|
|
1954
1986
|
|
|
@@ -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
|