hexcli 2.16.0__tar.gz → 2.17.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.16.0 → hexcli-2.17.0}/CHANGELOG.md +35 -0
  2. {hexcli-2.16.0 → hexcli-2.17.0}/PKG-INFO +1 -1
  3. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/__init__.py +1 -1
  4. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/parsing.py +49 -4
  5. {hexcli-2.16.0 → hexcli-2.17.0}/.gitignore +0 -0
  6. {hexcli-2.16.0 → hexcli-2.17.0}/Hex CLI.cmd +0 -0
  7. {hexcli-2.16.0 → hexcli-2.17.0}/LICENSE +0 -0
  8. {hexcli-2.16.0 → hexcli-2.17.0}/README.md +0 -0
  9. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/agent.py +0 -0
  10. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/assets/hexcli.ico +0 -0
  11. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/assets/hexcli.png +0 -0
  12. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/cancel.py +0 -0
  13. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/chatlog.py +0 -0
  14. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/commands.py +0 -0
  15. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/compaction.py +0 -0
  16. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/config.py +0 -0
  17. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/diffview.py +0 -0
  18. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/distribution.py +0 -0
  19. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/doctor.py +0 -0
  20. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/editing.py +0 -0
  21. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/http_client.py +0 -0
  22. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/launcher.py +0 -0
  23. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/lineedit.py +0 -0
  24. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/llm.py +0 -0
  25. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/lockfile.py +0 -0
  26. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/markdown_stream.py +0 -0
  27. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/memory.py +0 -0
  28. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/network.py +0 -0
  29. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/paths.py +0 -0
  30. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/prompts.py +0 -0
  31. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/psparse.py +0 -0
  32. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/repl.py +0 -0
  33. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/safety.py +0 -0
  34. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/sessions.py +0 -0
  35. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/setup_wizard.py +0 -0
  36. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/statusbar.py +0 -0
  37. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/stream_render.py +0 -0
  38. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/telemetry.py +0 -0
  39. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/tools.py +0 -0
  40. {hexcli-2.16.0 → hexcli-2.17.0}/hexcli/ui.py +0 -0
  41. {hexcli-2.16.0 → hexcli-2.17.0}/install.ps1 +0 -0
  42. {hexcli-2.16.0 → hexcli-2.17.0}/launcher.py +0 -0
  43. {hexcli-2.16.0 → hexcli-2.17.0}/pyproject.toml +0 -0
  44. {hexcli-2.16.0 → hexcli-2.17.0}/shellai.example.json +0 -0
@@ -4,6 +4,41 @@ 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.17.0 — 2026-09-18
8
+
9
+ A minor release: the parser accepts a reply it used to send back for a
10
+ retry, so what the model sees after such a reply changes.
11
+
12
+ > **Gate: PASS** on the pinned 24-case set, all 24 held at 5 runs with no
13
+ > recheck. Own arm, fresh server, seed 20260918, 6 invalid of 245 runs.
14
+ > Against the v2.11.1 12-run baseline: run-level 402/514 vs 174/224, −0.5 %,
15
+ > Fisher p = 0.92; pass^k 26 → 31 of 46 shared cases, McNemar p = 0.13. The
16
+ > close fired on five replies in the arm (`claims-1`, `claims-2`, two in
17
+ > `tests-claim-1`, one in `trap-3`); `claims-1` 3/5 against 3/11, `trap-3`
18
+ > 0/5 as before. Smoke 9/10 on a fresh server; the miss was `factual-1`,
19
+ > re-measured 7/10 against its recorded 79 % (p = 0.67).
20
+
21
+ - A reply that stops before its last closing brace is closed and decoded.
22
+ In the owner's 2026-09-18 session two `write_file` replies, 2,239 and 782
23
+ characters, ended at `"}` with the outer object never closed. The parser's
24
+ stray-quote repair took the decoder's "Expecting ',' delimiter" at the end
25
+ of the text as a quote that had ended a string early, escaped the
26
+ content's own closing quote, and the decoder then reported an unterminated
27
+ string; the retry feedback told the model to fix its quoting, the model
28
+ escaped everything twice, and the file it wrote had `\n` and `\"` in it as
29
+ text. Every step after that in the session was the model failing to repair
30
+ that file. When the decoder's error sits at the very end of the reply,
31
+ every string is closed and the brace depth is still positive, the missing
32
+ braces are appended and the object decodes. Measured over the 10,772
33
+ replies on record: 185 fail to decode, and 27 of them now decode (all one
34
+ brace short; 23 in `claims-1`, `claims-2` and `tests-claim-1`, four in the
35
+ owner's sessions). The close is only tried on the reply as sent: three
36
+ more replies are a brace short *after* a stray quote, and a reply whose
37
+ content string was cut off and then followed by a second object would
38
+ otherwise be glued into one, so those stay retries. A reply cut off inside
39
+ a string is still reported as cut off, and a stray quote in the middle is
40
+ still repaired as before; the other 158 undecodable replies are unchanged.
41
+
7
42
  ## 2.16.0 — 2026-09-18
8
43
 
9
44
  A minor release: a tool result the model reads changes (`find_files` returns
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hexcli
3
- Version: 2.16.0
3
+ Version: 2.17.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.16.0"
6
+ __version__ = "2.17.0"
@@ -126,6 +126,49 @@ def _repair_stray_quote(text: str, err: json.JSONDecodeError) -> str | None:
126
126
  return text[:q] + "\\" + text[q:]
127
127
 
128
128
 
129
+ def _close_unfinished_object(text: str, start: int, err: json.JSONDecodeError) -> str | None:
130
+ """The reply ended before its last closing brace(s): `{"action":"write_file",
131
+ "args":{...,"content":"..."}` and then nothing. The decoder reports
132
+ "Expecting ',' delimiter" AT THE END of the text; every string is closed
133
+ and the brace depth is still positive. Appending the missing braces is
134
+ the whole repair. 30 of the 185 undecodable replies on record are this
135
+ shape, all one brace short, four of them in the owner's sessions
136
+ (2026-09-13 and 2026-09-18), and the stray-quote repair below used to
137
+ make them worse: it escaped the content's own closing quote, the decoder
138
+ then said "Unterminated string", and the retry feedback told the model
139
+ to fix its quoting -- which it did by escaping everything twice, and the
140
+ file it then wrote had backslashes in it. None when the error is not at
141
+ the end, a string is still open, or the braces already balance."""
142
+ if err.pos < len(text.rstrip()):
143
+ return None
144
+ depth, in_string, escaped = 0, False, False
145
+ for ch in text[start:]:
146
+ if in_string:
147
+ if escaped:
148
+ escaped = False
149
+ elif ch == "\\":
150
+ escaped = True
151
+ elif ch == '"':
152
+ in_string = False
153
+ elif ch == '"':
154
+ in_string = True
155
+ elif ch == "{":
156
+ depth += 1
157
+ elif ch == "}":
158
+ depth -= 1
159
+ if in_string or depth <= 0:
160
+ return None
161
+ return text.rstrip() + "}" * depth
162
+
163
+
164
+ # Only the reply AS SENT is closed this way. Once a stray-quote repair has
165
+ # changed the text, a positive depth at the end can be the repair's own
166
+ # doing (a write whose content string was cut off, followed by a second
167
+ # object: the repairs walk the quotes back until the text "balances" and
168
+ # the close would then glue the two objects into one), and that reply must
169
+ # stay a retry -- evals/test_backports.py pins it.
170
+
171
+
129
172
  def _loads_object(text: str) -> dict[str, Any] | None:
130
173
  """The first JSON object in `text`, decoded from its first brace and
131
174
  ignoring whatever follows it (a second batched action, prose). Stray
@@ -133,12 +176,13 @@ def _loads_object(text: str) -> dict[str, Any] | None:
133
176
  start = text.find("{")
134
177
  if start < 0:
135
178
  return None
136
- for _ in range(_MAX_QUOTE_REPAIRS + 1):
179
+ for attempt in range(_MAX_QUOTE_REPAIRS + 1):
137
180
  try:
138
181
  parsed, _end = _DECODER.raw_decode(text, start)
139
182
  return parsed if isinstance(parsed, dict) else None
140
183
  except json.JSONDecodeError as err:
141
- fixed = _repair_stray_quote(text, err)
184
+ fixed = (_close_unfinished_object(text, start, err) if attempt == 0 else None) \
185
+ or _repair_stray_quote(text, err)
142
186
  if fixed is None:
143
187
  return None
144
188
  text = fixed
@@ -155,13 +199,14 @@ def _decode_failure(text: str) -> tuple[json.JSONDecodeError | None, str]:
155
199
  if start < 0:
156
200
  return None, text
157
201
  last: json.JSONDecodeError | None = None
158
- for _ in range(_MAX_QUOTE_REPAIRS + 1):
202
+ for attempt in range(_MAX_QUOTE_REPAIRS + 1):
159
203
  try:
160
204
  _DECODER.raw_decode(text, start)
161
205
  return None, text
162
206
  except json.JSONDecodeError as err:
163
207
  last = err
164
- fixed = _repair_stray_quote(text, err)
208
+ fixed = (_close_unfinished_object(text, start, err) if attempt == 0 else None) \
209
+ or _repair_stray_quote(text, err)
165
210
  if fixed is None:
166
211
  return err, text
167
212
  text = fixed
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