hexcli 2.13.0__tar.gz → 2.15.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.
Files changed (44) hide show
  1. {hexcli-2.13.0 → hexcli-2.15.0}/CHANGELOG.md +74 -0
  2. {hexcli-2.13.0 → hexcli-2.15.0}/PKG-INFO +1 -1
  3. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/__init__.py +1 -1
  4. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/agent.py +9 -5
  5. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/editing.py +19 -6
  6. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/tools.py +11 -0
  7. {hexcli-2.13.0 → hexcli-2.15.0}/.gitignore +0 -0
  8. {hexcli-2.13.0 → hexcli-2.15.0}/Hex CLI.cmd +0 -0
  9. {hexcli-2.13.0 → hexcli-2.15.0}/LICENSE +0 -0
  10. {hexcli-2.13.0 → hexcli-2.15.0}/README.md +0 -0
  11. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/assets/hexcli.ico +0 -0
  12. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/assets/hexcli.png +0 -0
  13. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/cancel.py +0 -0
  14. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/chatlog.py +0 -0
  15. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/commands.py +0 -0
  16. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/compaction.py +0 -0
  17. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/config.py +0 -0
  18. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/diffview.py +0 -0
  19. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/distribution.py +0 -0
  20. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/doctor.py +0 -0
  21. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/http_client.py +0 -0
  22. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/launcher.py +0 -0
  23. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/lineedit.py +0 -0
  24. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/llm.py +0 -0
  25. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/lockfile.py +0 -0
  26. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/markdown_stream.py +0 -0
  27. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/memory.py +0 -0
  28. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/network.py +0 -0
  29. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/parsing.py +0 -0
  30. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/paths.py +0 -0
  31. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/prompts.py +0 -0
  32. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/psparse.py +0 -0
  33. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/repl.py +0 -0
  34. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/safety.py +0 -0
  35. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/sessions.py +0 -0
  36. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/setup_wizard.py +0 -0
  37. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/statusbar.py +0 -0
  38. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/stream_render.py +0 -0
  39. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/telemetry.py +0 -0
  40. {hexcli-2.13.0 → hexcli-2.15.0}/hexcli/ui.py +0 -0
  41. {hexcli-2.13.0 → hexcli-2.15.0}/install.ps1 +0 -0
  42. {hexcli-2.13.0 → hexcli-2.15.0}/launcher.py +0 -0
  43. {hexcli-2.13.0 → hexcli-2.15.0}/pyproject.toml +0 -0
  44. {hexcli-2.13.0 → hexcli-2.15.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.15.0 — 2026-09-18
8
+
9
+ A minor release: one tool error the model reads is new (`edit_file` on
10
+ identical strings).
11
+
12
+ > **Gate: PASS** on the pinned 24-case set after one recheck (`agentic-5`
13
+ > 4/5, a counting answer with no edit in it, then 6/6). Own arm, fresh
14
+ > server, seed 20260917, 9 invalid of 245 runs. Against the v2.11.1 12-run
15
+ > baseline: run-level 402/514 vs 172/221, −0.4 %, Fisher p = 0.92; pass^k
16
+ > 26 → 28 of 46 shared cases, McNemar p = 0.63. The error fired in the arm
17
+ > on 3 `tests-claim-1` runs and nowhere else; that case went 4/5 against
18
+ > 12/24 (p = 0.34, ceiling). `self-correct-1` read 2/5 in the arm on a
19
+ > server 75 minutes into its traffic (the arm's invalid runs were climbing),
20
+ > and the error had not fired in any of its runs; a paired control on a
21
+ > fresh server, ten runs each on the parent tree and this one, came back
22
+ > **10/10 vs 9/10, p = 1.0**. In the one control run where the error did
23
+ > fire the model had sent a hallucinated `main()` as both strings, was told
24
+ > nothing changed, read the file, and then made the real fix. Smoke miss
25
+ > on `agentic-2` re-measured 9/10 against its recorded 75 %.
26
+
27
+ - `edit_file` refuses an edit whose `old_string` and `new_string` are the
28
+ same text, instead of answering "Edited". 37 of the 840 `edit_file` calls
29
+ on record sent identical strings and were told the edit landed; the model
30
+ then ran or read the file believing it had changed it (`tests-claim-1`:
31
+ "The median function was correctly fixed", after an edit of `mean` to
32
+ itself; `runit-1` before 2.14.0: the broken line edited to itself, twice,
33
+ to the step limit). The tool now says nothing changed and what to send
34
+ instead, and leaves the file byte-identical. A missing file still reports
35
+ the path first.
36
+
37
+ ## 2.14.0 — 2026-09-17
38
+
39
+ A minor release: one tool behaves differently for the model (a `write_file`
40
+ body that was JSON-escaped twice is decoded), plus a test-hygiene fix.
41
+
42
+ > **Gate: PASS** on the pinned 24-case set, all 24 held at 5 runs with no
43
+ > recheck. Own arm, fresh server, seed 20260917, 0 invalid of 245 runs.
44
+ > Against the v2.11.1 12-run baseline: run-level 402/514 vs 179/230, −0.4 %,
45
+ > Fisher p = 0.92; pass^k 26 → 30 of 46 shared cases, McNemar p = 0.22. The
46
+ > rule fired in the arm on 3 of 5 `runit-1` writes and on nothing else
47
+ > (one `trap-1` poem hit the older newline shape, as before). `runit-1`:
48
+ > 5/5 in the arm and 15/15 in a follow-up on the same server, **20/20
49
+ > against 5/10 on 2.12.0, p = 0.002**. Ceiling cases that moved: `agentic-3`
50
+ > 3/5 (p = 0.17 against 18/20; both misses are the model writing invalid
51
+ > JSON through `edit_file`, where this rule does not run), `ambiguous-1`
52
+ > 4/5 (p = 0.50). Smoke 10/10 on a fresh server before the arm.
53
+
54
+ - `write_file` decodes a body whose every quote is escaped. `runit-1`
55
+ ("Write hello.py that prints Hello, world and then run it") sat at 5/10
56
+ in the 2.12.0 arm, and not one of the misses was the "and run it" the
57
+ case was written for: every run wrote the file and ran it. The model
58
+ escaped its JSON twice, `{"content":"print(\\\"Hello, world\\\")"}`, so
59
+ the file held `print(\"Hello, world\")`, a SyntaxError; it then "fixed"
60
+ the line by editing it to itself, twice, until the step limit. The
61
+ double-escape rule from 2.9.0 only knew the newline shape (literal `\n`
62
+ and no real line break). It now also knows the quote shape — at least one
63
+ `\"` and not a single bare `"` — and decodes the body once, as before.
64
+ Measured over the 511 `write_file` bodies on record: 11 have the shape,
65
+ all of them this defect in this case, and none has both escaped and bare
66
+ quotes, so a body with even one bare quote is left exactly as sent.
67
+ `append_file` is untouched: 0 of its 17 recorded bodies have either shape.
68
+
69
+ - A test suite was writing into the owner's real chat log. The shell wiring
70
+ test in `evals/test_product_shell.py` drove `hexcli.agent` with the mock
71
+ backend and a temp config that left `chat_log_enabled` on, so every run
72
+ of the suite added a "say hello" session under `~/.shellai/chatlog/` — 93
73
+ of them by 2026-09-17, beside the 127 real sessions. The 2.12.0 exposure
74
+ counts were taken over that mix: "306 real turns" was 196 real turns and
75
+ 93 scripted ones (the 11 firings were all on real turns, so the true
76
+ positives stand; the denominator did not). The test now turns the log
77
+ off, the corpus loader skips mock-backend sessions, the mock files were
78
+ moved aside, and the intent nudge's recorded rate is corrected to 9 of
79
+ 196 real turns.
80
+
7
81
  ## 2.13.0 — 2026-09-17
8
82
 
9
83
  - Commands are classified by the name the shell will really run, not by the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hexcli
3
- Version: 2.13.0
3
+ Version: 2.15.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
@@ -3,4 +3,4 @@
3
3
  # The one place the version is written. pyproject.toml reads it (hatch
4
4
  # dynamic version), agent.VERSION re-exports it, and CI refuses a release
5
5
  # tag that does not match it.
6
- __version__ = "2.13.0"
6
+ __version__ = "2.15.0"
@@ -1741,8 +1741,11 @@ def _retry_echo(raw: str) -> str:
1741
1741
  # none of the four can fire on it. evals/test_agent_loop.py pins that.
1742
1742
  #
1743
1743
  # Every guard below is here because an earlier draft fired on something
1744
- # that was already RIGHT. They were found by replaying 306 real chat-log
1745
- # turns and 1,366 recorded eval runs against the detectors:
1744
+ # that was already RIGHT. They were found by replaying the real chat-log
1745
+ # turns and 1,366 recorded eval runs against the detectors (the count
1746
+ # first written here, 306 turns, included 93 that a test suite had
1747
+ # written into the real log with the mock backend; the owner's own
1748
+ # sessions are 196 turns, re-counted 2026-09-17):
1746
1749
  #
1747
1750
  # * "the condition is checked before the loop" — prose about code the
1748
1751
  # user pasted, in an answer that never touches the filesystem (two real
@@ -1763,9 +1766,10 @@ def _retry_echo(raw: str) -> str:
1763
1766
  # because a denied call is not in tools_used. Telling them apart needs
1764
1767
  # the denial recorded, which is a separate change.
1765
1768
  #
1766
- # After the guards, 11 of the 306 real turns fire and every one is a
1767
- # genuine miss; 1 of the 1,366 eval runs fires, a self-correct-1 run that
1768
- # claimed to have checked and fixed with no tool call at all.
1769
+ # After the guards, 9 of the 196 real turns fire and every one is a
1770
+ # genuine miss (the calculator, web-app, HiLo and resume sessions); 1 of
1771
+ # the 1,366 eval runs fires, a self-correct-1 run that claimed to have
1772
+ # checked and fixed with no tool call at all.
1769
1773
 
1770
1774
  _WANTS_RUN_RE = re.compile(
1771
1775
  r"\b(and|then|,)\s+(run|execute|start|launch)\s+(it|them|the\s+\w+)\b"
@@ -142,6 +142,7 @@ def unescape_json(text: str) -> str:
142
142
 
143
143
  _DELTA_TOKEN_RE = re.compile(r"\w+|\s+|[^\w\s]")
144
144
  _ESCAPED_RE = re.compile(r'\\["nt\\]')
145
+ _BARE_QUOTE_RE = re.compile(r'(?<!\\)"')
145
146
  _DELTA_MAX_HUNKS = 4
146
147
  _DELTA_MAX_REPLACED_CHARS = 40
147
148
  _DELTA_MAX_INSERTED_CHARS = 400
@@ -149,12 +150,24 @@ _DELTA_MIN_MATCHED = 0.6 # fraction of old_string's tokens found in the window
149
150
 
150
151
 
151
152
  def looks_double_escaped(text: str) -> bool:
152
- """A file body with two or more literal backslash-n sequences and not a
153
- single real line break is JSON escaped a second time (write_file,
154
- persona_guard 2026-09-12: 'import re\\n\\n\\n# Regex …' written as one
155
- line). A real one-line file that spells "\\n" on purpose is rarer than the
156
- model's habit; anything with a genuine newline is left alone."""
157
- return "\n" not in text and text.count("\\n") >= 2
153
+ """A file body JSON-escaped a second time, in either of the two shapes
154
+ the model actually produces.
155
+
156
+ Newlines: two or more literal backslash-n sequences and not a single
157
+ real line break (write_file, persona_guard 2026-09-12: 'import
158
+ re\\n\\n\\n# Regex …' written as one line). A real one-line file that
159
+ spells "\\n" on purpose is rarer than the model's habit; anything with a
160
+ genuine newline is left alone.
161
+
162
+ Quotes: at least one backslash-quote and not a single bare quote
163
+ (runit-1, 2026-09-17: `print(\\"Hello, world\\")` as the whole of
164
+ hello.py, a SyntaxError the model then "fixed" by editing the line to
165
+ itself until the step limit). Over the 511 write_file bodies on record
166
+ 11 have this shape and every one is that defect; none has both escaped
167
+ and bare quotes, so a body with even one bare quote is left alone."""
168
+ if "\n" not in text and text.count("\\n") >= 2:
169
+ return True
170
+ return '\\"' in text and _BARE_QUOTE_RE.search(text) is None
158
171
 
159
172
 
160
173
  def _unescape_json(text: str) -> str:
@@ -423,6 +423,17 @@ def edit_file_tool(path_text: str, old_string: str, new_string: str) -> str:
423
423
  raise RuntimeError("edit_file requires a non-empty 'old_string'. Use write_file to overwrite the whole file.")
424
424
  if not path.exists():
425
425
  raise RuntimeError(f"File not found: {path}.{missing_path_hint(path)}")
426
+ if old_string == new_string:
427
+ # 37 of the 840 edit_file calls on record (2026-09-17) sent the same
428
+ # text as old and new and were told "Edited"; the model then ran or
429
+ # read the file believing it had changed it (tests-claim-1: "The
430
+ # median function was correctly fixed", after an edit of mean to
431
+ # itself). A no-op is not an edit, and saying so is the only way the
432
+ # next step can be the change that was meant.
433
+ raise RuntimeError(
434
+ f"old_string and new_string are identical, so nothing changed in {path}. "
435
+ "Put the changed text in new_string, or read_file to see what is there now."
436
+ )
426
437
  content = path.read_text(encoding="utf-8")
427
438
  tier = "exact"
428
439
  if content.count(old_string) == 1:
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