@caupulican/pi-adaptative 0.85.0 → 0.85.4

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 (267) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/bundled-resources/runtimes/pi-shell-engine/arithmetic.py +364 -0
  3. package/dist/bundled-resources/runtimes/pi-shell-engine/context.py +2 -1
  4. package/dist/bundled-resources/runtimes/pi-shell-engine/errors.py +16 -0
  5. package/dist/bundled-resources/runtimes/pi-shell-engine/exec.py +137 -14
  6. package/dist/bundled-resources/runtimes/pi-shell-engine/nodes.py +23 -2
  7. package/dist/bundled-resources/runtimes/pi-shell-engine/parser.py +134 -6
  8. package/dist/bundled-resources/runtimes/pi-shell-engine/tokens.py +25 -2
  9. package/dist/core/agent-session-services.d.ts.map +1 -1
  10. package/dist/core/agent-session-services.js +2 -0
  11. package/dist/core/agent-session-services.js.map +1 -1
  12. package/dist/core/agent-session.d.ts +6 -5
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +84 -87
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/auth-guidance.d.ts.map +1 -1
  17. package/dist/core/auth-guidance.js +3 -0
  18. package/dist/core/auth-guidance.js.map +1 -1
  19. package/dist/core/autonomy/lane-tool-surface.d.ts +2 -0
  20. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  21. package/dist/core/autonomy/lane-tool-surface.js +4 -3
  22. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  23. package/dist/core/background-lane-controller.d.ts +4 -0
  24. package/dist/core/background-lane-controller.d.ts.map +1 -1
  25. package/dist/core/background-lane-controller.js +2 -0
  26. package/dist/core/background-lane-controller.js.map +1 -1
  27. package/dist/core/background-tool-task-controller.d.ts +12 -7
  28. package/dist/core/background-tool-task-controller.d.ts.map +1 -1
  29. package/dist/core/background-tool-task-controller.js +55 -19
  30. package/dist/core/background-tool-task-controller.js.map +1 -1
  31. package/dist/core/bedrock-scope.d.ts +26 -0
  32. package/dist/core/bedrock-scope.d.ts.map +1 -0
  33. package/dist/core/bedrock-scope.js +178 -0
  34. package/dist/core/bedrock-scope.js.map +1 -0
  35. package/dist/core/compaction-controller.d.ts +2 -0
  36. package/dist/core/compaction-controller.d.ts.map +1 -1
  37. package/dist/core/compaction-controller.js +35 -13
  38. package/dist/core/compaction-controller.js.map +1 -1
  39. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
  40. package/dist/core/delegation/worker-delegation-controller.js +6 -0
  41. package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
  42. package/dist/core/extensions/loader.d.ts.map +1 -1
  43. package/dist/core/extensions/loader.js +3 -0
  44. package/dist/core/extensions/loader.js.map +1 -1
  45. package/dist/core/failure-corpus.d.ts +26 -1
  46. package/dist/core/failure-corpus.d.ts.map +1 -1
  47. package/dist/core/failure-corpus.js +17 -0
  48. package/dist/core/failure-corpus.js.map +1 -1
  49. package/dist/core/foreground-recovery-controller.d.ts +25 -2
  50. package/dist/core/foreground-recovery-controller.d.ts.map +1 -1
  51. package/dist/core/foreground-recovery-controller.js +111 -2
  52. package/dist/core/foreground-recovery-controller.js.map +1 -1
  53. package/dist/core/foreground-terminal-handoff-controller.d.ts +27 -0
  54. package/dist/core/foreground-terminal-handoff-controller.d.ts.map +1 -0
  55. package/dist/core/foreground-terminal-handoff-controller.js +56 -0
  56. package/dist/core/foreground-terminal-handoff-controller.js.map +1 -0
  57. package/dist/core/goal-loop-controller.d.ts.map +1 -1
  58. package/dist/core/goal-loop-controller.js +5 -0
  59. package/dist/core/goal-loop-controller.js.map +1 -1
  60. package/dist/core/goals/goal-auto-continue-controller.d.ts +3 -0
  61. package/dist/core/goals/goal-auto-continue-controller.d.ts.map +1 -1
  62. package/dist/core/goals/goal-auto-continue-controller.js +28 -7
  63. package/dist/core/goals/goal-auto-continue-controller.js.map +1 -1
  64. package/dist/core/human-input-controller.d.ts +9 -0
  65. package/dist/core/human-input-controller.d.ts.map +1 -1
  66. package/dist/core/human-input-controller.js +60 -5
  67. package/dist/core/human-input-controller.js.map +1 -1
  68. package/dist/core/human-input.d.ts +12 -0
  69. package/dist/core/human-input.d.ts.map +1 -1
  70. package/dist/core/human-input.js +59 -12
  71. package/dist/core/human-input.js.map +1 -1
  72. package/dist/core/local-runtime-controller.d.ts.map +1 -1
  73. package/dist/core/local-runtime-controller.js +15 -9
  74. package/dist/core/local-runtime-controller.js.map +1 -1
  75. package/dist/core/memory/providers/file-store.d.ts +1 -0
  76. package/dist/core/memory/providers/file-store.d.ts.map +1 -1
  77. package/dist/core/memory/providers/file-store.js +252 -125
  78. package/dist/core/memory/providers/file-store.js.map +1 -1
  79. package/dist/core/model-capability.d.ts +15 -1
  80. package/dist/core/model-capability.d.ts.map +1 -1
  81. package/dist/core/model-capability.js +24 -1
  82. package/dist/core/model-capability.js.map +1 -1
  83. package/dist/core/model-registry.d.ts +5 -0
  84. package/dist/core/model-registry.d.ts.map +1 -1
  85. package/dist/core/model-registry.js +18 -0
  86. package/dist/core/model-registry.js.map +1 -1
  87. package/dist/core/model-router-controller.d.ts +6 -6
  88. package/dist/core/model-router-controller.d.ts.map +1 -1
  89. package/dist/core/model-router-controller.js +5 -5
  90. package/dist/core/model-router-controller.js.map +1 -1
  91. package/dist/core/model-tool-protocol.d.ts +3 -2
  92. package/dist/core/model-tool-protocol.d.ts.map +1 -1
  93. package/dist/core/model-tool-protocol.js +5 -1
  94. package/dist/core/model-tool-protocol.js.map +1 -1
  95. package/dist/core/models/context-sizing.d.ts +1 -0
  96. package/dist/core/models/context-sizing.d.ts.map +1 -1
  97. package/dist/core/models/context-sizing.js +4 -1
  98. package/dist/core/models/context-sizing.js.map +1 -1
  99. package/dist/core/models/default-model-suggestions.d.ts.map +1 -1
  100. package/dist/core/models/default-model-suggestions.js +20 -10
  101. package/dist/core/models/default-model-suggestions.js.map +1 -1
  102. package/dist/core/models/llamacpp-runtime.d.ts +32 -11
  103. package/dist/core/models/llamacpp-runtime.d.ts.map +1 -1
  104. package/dist/core/models/llamacpp-runtime.js +79 -28
  105. package/dist/core/models/llamacpp-runtime.js.map +1 -1
  106. package/dist/core/models/local-execution-planner.d.ts +24 -0
  107. package/dist/core/models/local-execution-planner.d.ts.map +1 -0
  108. package/dist/core/models/local-execution-planner.js +119 -0
  109. package/dist/core/models/local-execution-planner.js.map +1 -0
  110. package/dist/core/models/local-inference-profile.d.ts +34 -0
  111. package/dist/core/models/local-inference-profile.d.ts.map +1 -0
  112. package/dist/core/models/local-inference-profile.js +74 -0
  113. package/dist/core/models/local-inference-profile.js.map +1 -0
  114. package/dist/core/models/local-runtime.d.ts +23 -4
  115. package/dist/core/models/local-runtime.d.ts.map +1 -1
  116. package/dist/core/models/local-runtime.js +11 -8
  117. package/dist/core/models/local-runtime.js.map +1 -1
  118. package/dist/core/models/managed-ollama-model.d.ts +26 -0
  119. package/dist/core/models/managed-ollama-model.d.ts.map +1 -0
  120. package/dist/core/models/managed-ollama-model.js +59 -0
  121. package/dist/core/models/managed-ollama-model.js.map +1 -0
  122. package/dist/core/models/model-ref.js +11 -11
  123. package/dist/core/models/model-ref.js.map +1 -1
  124. package/dist/core/models/prism-llamacpp-lifecycle.d.ts +10 -20
  125. package/dist/core/models/prism-llamacpp-lifecycle.d.ts.map +1 -1
  126. package/dist/core/models/prism-llamacpp-lifecycle.js +11 -31
  127. package/dist/core/models/prism-llamacpp-lifecycle.js.map +1 -1
  128. package/dist/core/models/prism-llamacpp-server-probe.d.ts +10 -0
  129. package/dist/core/models/prism-llamacpp-server-probe.d.ts.map +1 -0
  130. package/dist/core/models/prism-llamacpp-server-probe.js +36 -0
  131. package/dist/core/models/prism-llamacpp-server-probe.js.map +1 -0
  132. package/dist/core/models/runtime-arbiter.d.ts +3 -0
  133. package/dist/core/models/runtime-arbiter.d.ts.map +1 -1
  134. package/dist/core/models/runtime-arbiter.js +2 -1
  135. package/dist/core/models/runtime-arbiter.js.map +1 -1
  136. package/dist/core/models/runtime-process.d.ts +7 -1
  137. package/dist/core/models/runtime-process.d.ts.map +1 -1
  138. package/dist/core/models/runtime-process.js +4 -2
  139. package/dist/core/models/runtime-process.js.map +1 -1
  140. package/dist/core/process-matrix/runtime.js +43 -11
  141. package/dist/core/process-matrix/runtime.js.map +1 -1
  142. package/dist/core/research/research-lane-controller.d.ts.map +1 -1
  143. package/dist/core/research/research-lane-controller.js +14 -5
  144. package/dist/core/research/research-lane-controller.js.map +1 -1
  145. package/dist/core/resource-loader.d.ts +2 -0
  146. package/dist/core/resource-loader.d.ts.map +1 -1
  147. package/dist/core/resource-loader.js +18 -6
  148. package/dist/core/resource-loader.js.map +1 -1
  149. package/dist/core/runtime-builder.d.ts +5 -1
  150. package/dist/core/runtime-builder.d.ts.map +1 -1
  151. package/dist/core/runtime-builder.js +37 -4
  152. package/dist/core/runtime-builder.js.map +1 -1
  153. package/dist/core/scout-controller.d.ts +3 -2
  154. package/dist/core/scout-controller.d.ts.map +1 -1
  155. package/dist/core/scout-controller.js +1 -0
  156. package/dist/core/scout-controller.js.map +1 -1
  157. package/dist/core/sdk.d.ts.map +1 -1
  158. package/dist/core/sdk.js +10 -8
  159. package/dist/core/sdk.js.map +1 -1
  160. package/dist/core/settings-manager.d.ts +13 -0
  161. package/dist/core/settings-manager.d.ts.map +1 -1
  162. package/dist/core/settings-manager.js +59 -0
  163. package/dist/core/settings-manager.js.map +1 -1
  164. package/dist/core/system-prompt-builder.d.ts +5 -2
  165. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  166. package/dist/core/system-prompt-builder.js +22 -10
  167. package/dist/core/system-prompt-builder.js.map +1 -1
  168. package/dist/core/system-prompt.d.ts +3 -0
  169. package/dist/core/system-prompt.d.ts.map +1 -1
  170. package/dist/core/system-prompt.js +181 -55
  171. package/dist/core/system-prompt.js.map +1 -1
  172. package/dist/core/tool-protocol-controller.d.ts.map +1 -1
  173. package/dist/core/tool-protocol-controller.js +5 -3
  174. package/dist/core/tool-protocol-controller.js.map +1 -1
  175. package/dist/core/tool-recovery-log-records.d.ts +33 -1
  176. package/dist/core/tool-recovery-log-records.d.ts.map +1 -1
  177. package/dist/core/tool-recovery-log-records.js +59 -12
  178. package/dist/core/tool-recovery-log-records.js.map +1 -1
  179. package/dist/core/tool-recovery-logger.d.ts +12 -1
  180. package/dist/core/tool-recovery-logger.d.ts.map +1 -1
  181. package/dist/core/tool-recovery-logger.js +24 -1
  182. package/dist/core/tool-recovery-logger.js.map +1 -1
  183. package/dist/core/tool-repair-health.d.ts.map +1 -1
  184. package/dist/core/tool-repair-health.js +6 -0
  185. package/dist/core/tool-repair-health.js.map +1 -1
  186. package/dist/core/tools/bash.d.ts +2 -2
  187. package/dist/core/tools/bash.d.ts.map +1 -1
  188. package/dist/core/tools/bash.js +2 -2
  189. package/dist/core/tools/bash.js.map +1 -1
  190. package/dist/core/tools/edit-diff.d.ts +2 -0
  191. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  192. package/dist/core/tools/edit-diff.js +9 -1
  193. package/dist/core/tools/edit-diff.js.map +1 -1
  194. package/dist/core/tools/edit.d.ts +5 -8
  195. package/dist/core/tools/edit.d.ts.map +1 -1
  196. package/dist/core/tools/edit.js +88 -55
  197. package/dist/core/tools/edit.js.map +1 -1
  198. package/dist/core/tools/file-mutation-intent.d.ts +42 -15
  199. package/dist/core/tools/file-mutation-intent.d.ts.map +1 -1
  200. package/dist/core/tools/file-mutation-intent.js +271 -77
  201. package/dist/core/tools/file-mutation-intent.js.map +1 -1
  202. package/dist/core/tools/find.d.ts +2 -1
  203. package/dist/core/tools/find.d.ts.map +1 -1
  204. package/dist/core/tools/find.js +2 -6
  205. package/dist/core/tools/find.js.map +1 -1
  206. package/dist/core/tools/grep.d.ts +2 -1
  207. package/dist/core/tools/grep.d.ts.map +1 -1
  208. package/dist/core/tools/grep.js +2 -6
  209. package/dist/core/tools/grep.js.map +1 -1
  210. package/dist/core/tools/index.d.ts +1 -1
  211. package/dist/core/tools/index.d.ts.map +1 -1
  212. package/dist/core/tools/index.js +1 -1
  213. package/dist/core/tools/index.js.map +1 -1
  214. package/dist/core/tools/managed-search-tool.d.ts +8 -0
  215. package/dist/core/tools/managed-search-tool.d.ts.map +1 -0
  216. package/dist/core/tools/managed-search-tool.js +10 -0
  217. package/dist/core/tools/managed-search-tool.js.map +1 -0
  218. package/dist/core/tools/shell-command-parser.d.ts +4 -3
  219. package/dist/core/tools/shell-command-parser.d.ts.map +1 -1
  220. package/dist/core/tools/shell-command-parser.js +32 -7
  221. package/dist/core/tools/shell-command-parser.js.map +1 -1
  222. package/dist/core/tools/shell-contract-router.d.ts.map +1 -1
  223. package/dist/core/tools/shell-contract-router.js +23 -1
  224. package/dist/core/tools/shell-contract-router.js.map +1 -1
  225. package/dist/core/tools/windows-shell-engine.js +1 -1
  226. package/dist/core/tools/windows-shell-engine.js.map +1 -1
  227. package/dist/core/tools/write.d.ts +5 -10
  228. package/dist/core/tools/write.d.ts.map +1 -1
  229. package/dist/core/tools/write.js +92 -73
  230. package/dist/core/tools/write.js.map +1 -1
  231. package/dist/index.d.ts +1 -1
  232. package/dist/index.d.ts.map +1 -1
  233. package/dist/index.js +1 -1
  234. package/dist/index.js.map +1 -1
  235. package/dist/modes/interactive/auth-dialogs-controller.d.ts +6 -0
  236. package/dist/modes/interactive/auth-dialogs-controller.d.ts.map +1 -1
  237. package/dist/modes/interactive/auth-dialogs-controller.js +59 -12
  238. package/dist/modes/interactive/auth-dialogs-controller.js.map +1 -1
  239. package/dist/modes/interactive/local-model-commands.d.ts +1 -1
  240. package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
  241. package/dist/modes/interactive/local-model-commands.js +51 -8
  242. package/dist/modes/interactive/local-model-commands.js.map +1 -1
  243. package/dist/utils/paths.js +1 -1
  244. package/dist/utils/paths.js.map +1 -1
  245. package/dist/utils/tools-manager.d.ts +18 -1
  246. package/dist/utils/tools-manager.d.ts.map +1 -1
  247. package/dist/utils/tools-manager.js +73 -43
  248. package/dist/utils/tools-manager.js.map +1 -1
  249. package/docs/models.md +1 -1
  250. package/docs/providers.md +34 -10
  251. package/docs/settings.md +1 -1
  252. package/docs/tool-boundary-performance-roadmap-2026-08-02.md +21 -18
  253. package/docs/tool-repair.md +3 -3
  254. package/docs/windows.md +7 -5
  255. package/examples/extensions/built-in-tool-renderer.ts +1 -1
  256. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  257. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  258. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  259. package/examples/extensions/minimal-mode.ts +1 -1
  260. package/examples/extensions/prompt-customizer.ts +2 -2
  261. package/examples/extensions/sandbox/package-lock.json +2 -2
  262. package/examples/extensions/sandbox/package.json +1 -1
  263. package/examples/extensions/ssh.ts +17 -0
  264. package/examples/extensions/with-deps/package-lock.json +2 -2
  265. package/examples/extensions/with-deps/package.json +1 -1
  266. package/npm-shrinkwrap.json +240 -260
  267. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,3 +1,57 @@
1
+ ## [0.85.4] - 2026-08-04
2
+
3
+ ### Breaking Changes
4
+
5
+ - Removed model-visible `action` and `intentId` phases from `write` and `edit`; callers now send one semantic mutation containing the path and payload while the harness owns preflight.
6
+
7
+ ### Added
8
+
9
+ - Added a Pi-managed local execution path for the complete Bonsai and Ternary-Bonsai GGUF families through a pinned Prism llama.cpp runtime, with exact model identity checks, bounded host-derived resource profiles, warm residency, validated paired-drafter policy, and complete Windows x64 CUDA archive installation.
10
+ - Added LFM2.5-8B-A1B Q4_K_M to the curated local-model roster as a native-tool-first sparse-MoE worker candidate, with task-scale `/fitness` gating before role assignment and phone-protocol calibration only after native-call failure.
11
+
12
+ ### Changed
13
+
14
+ - Shape and hard-gate the stable system prompt from the same model-capability profile that filters tools and lanes: lean, minimal, and chat models receive bounded role-specific contracts, oversized project instructions are deferred by path, constrained static memory obeys its prompt budget, and aggregate expansion is rejected after extension overrides without changing native-versus-phone tool protocol selection.
15
+ - Made `write` and `edit` single-call model operations while keeping path/capability preflight, serialized stale-target rechecks, and exclusive create semantics harness-owned.
16
+
17
+ ### Fixed
18
+
19
+ - Hardened phone-protocol tool execution for non-native local models with reasoning-channel envelope recovery, parallel-call identity, single-call write/edit examples, bounded corrective teaching after failures, and protection against repeating an already-successful mutation.
20
+ - Prevented write/edit safety preparation from duplicating model tool calls; edit now also rechecks target identity immediately before writing and discards preview plans when source bytes differ, preserving and reporting external changes.
21
+ - Retained an exact, digest-verified write or edit payload after a target-only collision or path failure so repair needs only a corrected path; references are session-isolated, bounded, expiring, kind-checked, and consumed after a successful mutation, while invalid references and non-target failures are never presented as retargetable.
22
+ - Prevented active goals from becoming permanently blocked by an `Agent is already processing` race: foreground ownership now spans retry/follow-up idle gaps, background handoffs queue into that owner, and goal continuation waits event-first without counting rejected admission as a turn or failure.
23
+ - Kept sanitized tool-failure projections authoritative through session context extensions and invoked each extension context handler once per provider request.
24
+ - Fixed simultaneous background-tool completions racing separate notification prompts: terminal handoffs are now bounded, coalesced, and serialized while every task result remains individually retrievable through `tool_task`.
25
+ - Required Amazon Bedrock to use a durable verified profile-and-region scope, with STS/control-plane discovery, bounded runtime probes, exact model visibility, and request-level region/profile binding; `us-east-2` now resolves only US inference profiles.
26
+ - Kept source and bundled extension loading compatible with the lean Bedrock provider entry so startup and transactional reload resolve the same module boundary.
27
+ - Updated Undici to 8.9.0 to fix cache-directive parsing, retry framing, blob media-type, cache-control whitespace, and cookie-attribute advisories.
28
+ - Serialized asynchronous foreground preparation and terminal background delivery under one identity-bound owner; isolated retained mutation payloads per session and lane without cross-tenant eviction; incrementally indexed worker-input handoffs for long sessions; and transactionally reconciled concurrent `MEMORY.md`/`USER.md` writers with interrupted-write recovery.
29
+
30
+ ## [0.85.3] - 2026-08-03
31
+
32
+ ### Fixed
33
+
34
+ - Fixed the bundled Windows incident collector on PowerShell 5.1 hosts where the module-provided `Get-FileHash` command is unavailable.
35
+ - Fixed native Windows path canonicalization so 8.3 short paths and their long-path spellings resolve to one resource identity.
36
+ - Fixed cooperative worker cleanup under Windows file-lock contention so terminal state is durably persisted, including cleanup requested during the adoption grace window.
37
+ - Fixed system Python detection incorrectly accepting the Windows Store execution alias or another non-Python executable as an installed interpreter.
38
+
39
+ ## [0.85.2] - 2026-08-03
40
+
41
+ ### Fixed
42
+
43
+ - Fixed fresh Windows search-tool provisioning by pinning checksum-verified `fd` release assets instead of relying on GitHub's rate-limited latest-release API, and hardened Windows release-gate portability without enabling unsupported native tmux.
44
+
45
+ ## [0.85.1] - 2026-08-03
46
+
47
+ ### Fixed
48
+
49
+ - Prevented concurrent and ineffective repeated auto-compaction runs in image-heavy sessions and contexts with protected recent messages.
50
+ - Fixed Windows runtime probes and case-insensitive context-file identity, plus Bash-like path parsing, portable `printf`, word-list and arithmetic `for` loops, and nested `break`/`continue` in the managed shell.
51
+ - Preserved managed-tool provisioning causes and bounded execution-failure guidance in recovery logs, failure corpora, and `/toolhealth`.
52
+ - Kept proven native tool-call models off the phone/text protocol and routed phone validation, repair teaching, and failure guidance through the shared tool pipeline.
53
+ - Required the exact release ref to pass Linux and Windows CI before publishing binaries.
54
+
1
55
  ## [0.85.0] - 2026-08-02
2
56
 
3
57
  ### Breaking Changes
@@ -0,0 +1,364 @@
1
+ """Safe integer-expression parser/evaluator for arithmetic ``for ((...))`` loops.
2
+
3
+ The evaluator deliberately does not call Python ``eval``. It accepts the integer,
4
+ variable, update, assignment, comparison, logical, bitwise, and conditional operators
5
+ used by Bash loop headers, then applies signed 64-bit wrapping after arithmetic writes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+
13
+ from state import ShellState
14
+
15
+ _MASK = (1 << 64) - 1
16
+ _SIGN_BIT = 1 << 63
17
+ _ASSIGNMENT_OPERATORS = frozenset(
18
+ {"=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "^=", "|="}
19
+ )
20
+ _TOKEN_RE = re.compile(
21
+ r"\s*(?:"
22
+ r"(?P<number>0[xX][0-9A-Fa-f]+|0[bB][01]+|0[0-7]+|[0-9]+)"
23
+ r"|(?P<variable>\$?[A-Za-z_][A-Za-z0-9_]*)"
24
+ r"|(?P<operator>\*\*|\+\+|--|<<=|>>=|\+=|-=|\*=|/=|%=|&=|\^=|\|="
25
+ r"|\|\||&&|==|!=|<=|>=|<<|>>|[=+\-*/%<>()!~&^|?:,])"
26
+ r")"
27
+ )
28
+
29
+
30
+ class ArithmeticError(ValueError):
31
+ """A bounded syntax or runtime failure in a loop arithmetic expression."""
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class _Token:
36
+ kind: str
37
+ text: str
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class _Number:
42
+ value: int
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class _Variable:
47
+ name: str
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class _Unary:
52
+ operator: str
53
+ operand: "_Expression"
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class _Binary:
58
+ operator: str
59
+ left: "_Expression"
60
+ right: "_Expression"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class _Conditional:
65
+ condition: "_Expression"
66
+ when_true: "_Expression"
67
+ when_false: "_Expression"
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class _Assignment:
72
+ operator: str
73
+ target: _Variable
74
+ value: "_Expression"
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class _Update:
79
+ operator: str
80
+ target: _Variable
81
+ prefix: bool
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class _Sequence:
86
+ expressions: tuple["_Expression", ...]
87
+
88
+
89
+ _Expression = _Number | _Variable | _Unary | _Binary | _Conditional | _Assignment | _Update | _Sequence
90
+
91
+
92
+ def _wrap(value: int) -> int:
93
+ unsigned = value & _MASK
94
+ return unsigned - (1 << 64) if unsigned & _SIGN_BIT else unsigned
95
+
96
+
97
+ def _parse_integer(text: str) -> int:
98
+ if text.lower().startswith(("0x", "0b")):
99
+ return _wrap(int(text, 0))
100
+ if len(text) > 1 and text.startswith("0"):
101
+ return _wrap(int(text, 8))
102
+ return _wrap(int(text, 10))
103
+
104
+
105
+ def _tokenize(source: str) -> list[_Token]:
106
+ tokens: list[_Token] = []
107
+ position = 0
108
+ while position < len(source):
109
+ match = _TOKEN_RE.match(source, position)
110
+ if match is None:
111
+ excerpt = source[position : position + 16]
112
+ raise ArithmeticError(f"unexpected arithmetic token near {excerpt!r}")
113
+ kind = match.lastgroup
114
+ assert kind is not None
115
+ text = match.group(kind)
116
+ if kind == "variable" and text.startswith("$"):
117
+ text = text[1:]
118
+ tokens.append(_Token(kind, text))
119
+ position = match.end()
120
+ tokens.append(_Token("end", ""))
121
+ return tokens
122
+
123
+
124
+ class _Parser:
125
+ def __init__(self, source: str) -> None:
126
+ self.tokens = _tokenize(source)
127
+ self.index = 0
128
+
129
+ def current(self) -> _Token:
130
+ return self.tokens[self.index]
131
+
132
+ def accept(self, operator: str) -> bool:
133
+ if self.current().kind == "operator" and self.current().text == operator:
134
+ self.index += 1
135
+ return True
136
+ return False
137
+
138
+ def require(self, operator: str) -> None:
139
+ if not self.accept(operator):
140
+ raise ArithmeticError(f"expected {operator!r} in arithmetic expression")
141
+
142
+ def parse(self) -> _Expression:
143
+ expression = self.parse_sequence()
144
+ if self.current().kind != "end":
145
+ raise ArithmeticError(f"unexpected arithmetic token {self.current().text!r}")
146
+ return expression
147
+
148
+ def parse_sequence(self) -> _Expression:
149
+ expressions = [self.parse_assignment()]
150
+ while self.accept(","):
151
+ expressions.append(self.parse_assignment())
152
+ return expressions[0] if len(expressions) == 1 else _Sequence(tuple(expressions))
153
+
154
+ def parse_assignment(self) -> _Expression:
155
+ target = self.parse_conditional()
156
+ token = self.current()
157
+ if token.kind != "operator" or token.text not in _ASSIGNMENT_OPERATORS:
158
+ return target
159
+ self.index += 1
160
+ if not isinstance(target, _Variable):
161
+ raise ArithmeticError("the left side of an arithmetic assignment must be a variable")
162
+ return _Assignment(token.text, target, self.parse_assignment())
163
+
164
+ def parse_conditional(self) -> _Expression:
165
+ condition = self.parse_logical_or()
166
+ if not self.accept("?"):
167
+ return condition
168
+ when_true = self.parse_sequence()
169
+ self.require(":")
170
+ return _Conditional(condition, when_true, self.parse_assignment())
171
+
172
+ def parse_logical_or(self) -> _Expression:
173
+ return self._parse_left_associative(self.parse_logical_and, frozenset({"||"}))
174
+
175
+ def parse_logical_and(self) -> _Expression:
176
+ return self._parse_left_associative(self.parse_bitwise_or, frozenset({"&&"}))
177
+
178
+ def parse_bitwise_or(self) -> _Expression:
179
+ return self._parse_left_associative(self.parse_bitwise_xor, frozenset({"|"}))
180
+
181
+ def parse_bitwise_xor(self) -> _Expression:
182
+ return self._parse_left_associative(self.parse_bitwise_and, frozenset({"^"}))
183
+
184
+ def parse_bitwise_and(self) -> _Expression:
185
+ return self._parse_left_associative(self.parse_equality, frozenset({"&"}))
186
+
187
+ def parse_equality(self) -> _Expression:
188
+ return self._parse_left_associative(self.parse_relational, frozenset({"==", "!="}))
189
+
190
+ def parse_relational(self) -> _Expression:
191
+ return self._parse_left_associative(self.parse_shift, frozenset({"<", "<=", ">", ">="}))
192
+
193
+ def parse_shift(self) -> _Expression:
194
+ return self._parse_left_associative(self.parse_additive, frozenset({"<<", ">>"}))
195
+
196
+ def parse_additive(self) -> _Expression:
197
+ return self._parse_left_associative(self.parse_multiplicative, frozenset({"+", "-"}))
198
+
199
+ def parse_multiplicative(self) -> _Expression:
200
+ return self._parse_left_associative(self.parse_power, frozenset({"*", "/", "%"}))
201
+
202
+ def parse_power(self) -> _Expression:
203
+ left = self.parse_unary()
204
+ if self.accept("**"):
205
+ return _Binary("**", left, self.parse_power())
206
+ return left
207
+
208
+ def parse_unary(self) -> _Expression:
209
+ token = self.current()
210
+ if token.kind == "operator" and token.text in ("+", "-", "!", "~"):
211
+ self.index += 1
212
+ return _Unary(token.text, self.parse_unary())
213
+ if token.kind == "operator" and token.text in ("++", "--"):
214
+ self.index += 1
215
+ target = self.parse_unary()
216
+ if not isinstance(target, _Variable):
217
+ raise ArithmeticError(f"{token.text} requires a variable")
218
+ return _Update(token.text, target, True)
219
+ return self.parse_postfix()
220
+
221
+ def parse_postfix(self) -> _Expression:
222
+ expression = self.parse_primary()
223
+ token = self.current()
224
+ if token.kind == "operator" and token.text in ("++", "--"):
225
+ self.index += 1
226
+ if not isinstance(expression, _Variable):
227
+ raise ArithmeticError(f"{token.text} requires a variable")
228
+ return _Update(token.text, expression, False)
229
+ return expression
230
+
231
+ def parse_primary(self) -> _Expression:
232
+ token = self.current()
233
+ if token.kind == "number":
234
+ self.index += 1
235
+ return _Number(_parse_integer(token.text))
236
+ if token.kind == "variable":
237
+ self.index += 1
238
+ return _Variable(token.text)
239
+ if self.accept("("):
240
+ expression = self.parse_sequence()
241
+ self.require(")")
242
+ return expression
243
+ raise ArithmeticError(f"expected an integer, variable, or '('; found {token.text!r}")
244
+
245
+ def _parse_left_associative(self, operand_parser, operators: frozenset[str]) -> _Expression:
246
+ expression = operand_parser()
247
+ while self.current().kind == "operator" and self.current().text in operators:
248
+ operator = self.current().text
249
+ self.index += 1
250
+ expression = _Binary(operator, expression, operand_parser())
251
+ return expression
252
+
253
+
254
+ def compile_arithmetic(source: str) -> _Expression | None:
255
+ stripped = source.strip()
256
+ return None if not stripped else _Parser(stripped).parse()
257
+
258
+
259
+ def _variable_value(variable: _Variable, state: ShellState) -> int:
260
+ text = state.env.get(variable.name, "").strip()
261
+ if not text:
262
+ return 0
263
+ try:
264
+ return _parse_integer(text)
265
+ except ValueError:
266
+ return 0
267
+
268
+
269
+ def _set_variable(variable: _Variable, value: int, state: ShellState) -> int:
270
+ wrapped = _wrap(value)
271
+ state.setenv(variable.name, str(wrapped))
272
+ return wrapped
273
+
274
+
275
+ def _divide(left: int, right: int) -> int:
276
+ if right == 0:
277
+ raise ArithmeticError("division by zero")
278
+ quotient = abs(left) // abs(right)
279
+ return -quotient if (left < 0) != (right < 0) else quotient
280
+
281
+
282
+ def _binary(operator: str, left: int, right: int) -> int:
283
+ if operator == "+":
284
+ return _wrap(left + right)
285
+ if operator == "-":
286
+ return _wrap(left - right)
287
+ if operator == "*":
288
+ return _wrap(left * right)
289
+ if operator == "/":
290
+ return _wrap(_divide(left, right))
291
+ if operator == "%":
292
+ return _wrap(left - _divide(left, right) * right)
293
+ if operator == "**":
294
+ if right < 0:
295
+ raise ArithmeticError("negative exponent")
296
+ return _wrap(pow(left & _MASK, right, 1 << 64))
297
+ if operator in ("<<", ">>"):
298
+ if right < 0 or right >= 64:
299
+ raise ArithmeticError("shift count must be between 0 and 63")
300
+ return _wrap(left << right) if operator == "<<" else _wrap(left >> right)
301
+ if operator == "&":
302
+ return _wrap(left & right)
303
+ if operator == "^":
304
+ return _wrap(left ^ right)
305
+ if operator == "|":
306
+ return _wrap(left | right)
307
+ if operator == "==":
308
+ return int(left == right)
309
+ if operator == "!=":
310
+ return int(left != right)
311
+ if operator == "<":
312
+ return int(left < right)
313
+ if operator == "<=":
314
+ return int(left <= right)
315
+ if operator == ">":
316
+ return int(left > right)
317
+ if operator == ">=":
318
+ return int(left >= right)
319
+ raise ArithmeticError(f"unsupported arithmetic operator {operator!r}")
320
+
321
+
322
+ def evaluate_arithmetic(expression: _Expression | None, state: ShellState) -> int:
323
+ if expression is None:
324
+ return 0
325
+ if isinstance(expression, _Number):
326
+ return expression.value
327
+ if isinstance(expression, _Variable):
328
+ return _variable_value(expression, state)
329
+ if isinstance(expression, _Sequence):
330
+ result = 0
331
+ for item in expression.expressions:
332
+ result = evaluate_arithmetic(item, state)
333
+ return result
334
+ if isinstance(expression, _Conditional):
335
+ branch = expression.when_true if evaluate_arithmetic(expression.condition, state) != 0 else expression.when_false
336
+ return evaluate_arithmetic(branch, state)
337
+ if isinstance(expression, _Unary):
338
+ value = evaluate_arithmetic(expression.operand, state)
339
+ if expression.operator == "+":
340
+ return value
341
+ if expression.operator == "-":
342
+ return _wrap(-value)
343
+ if expression.operator == "!":
344
+ return int(value == 0)
345
+ if expression.operator == "~":
346
+ return _wrap(~value)
347
+ if isinstance(expression, _Binary):
348
+ left = evaluate_arithmetic(expression.left, state)
349
+ if expression.operator == "&&":
350
+ return int(left != 0 and evaluate_arithmetic(expression.right, state) != 0)
351
+ if expression.operator == "||":
352
+ return int(left != 0 or evaluate_arithmetic(expression.right, state) != 0)
353
+ return _binary(expression.operator, left, evaluate_arithmetic(expression.right, state))
354
+ if isinstance(expression, _Assignment):
355
+ right = evaluate_arithmetic(expression.value, state)
356
+ if expression.operator == "=":
357
+ return _set_variable(expression.target, right, state)
358
+ left = _variable_value(expression.target, state)
359
+ return _set_variable(expression.target, _binary(expression.operator[:-1], left, right), state)
360
+ if isinstance(expression, _Update):
361
+ old = _variable_value(expression.target, state)
362
+ new = _set_variable(expression.target, old + (1 if expression.operator == "++" else -1), state)
363
+ return new if expression.prefix else old
364
+ raise ArithmeticError("unrecognized arithmetic expression")
@@ -44,9 +44,10 @@ class ExecContext:
44
44
  builtins: Mapping[str, BuiltinFn]
45
45
  deadline: float | None
46
46
  stderr: BinaryIO | int | None = None
47
+ loop_depth: int = 0
47
48
 
48
49
 
49
50
  # Executor-owned builtins are NOT in commands/REGISTRY: state mutators and `exit`
50
51
  # need ShellState/control-flow access, while the runner needs the executor.
51
- STATE_BUILTINS = {"cd", "export", "unset", "exit"}
52
+ STATE_BUILTINS = {"cd", "export", "unset", "exit", "break", "continue"}
52
53
  RUNNER_BUILTINS = {"xargs"}
@@ -55,3 +55,19 @@ class ShellExit(Exception):
55
55
  def __init__(self, exit_code: int) -> None:
56
56
  super().__init__(exit_code)
57
57
  self.exit_code = exit_code
58
+
59
+
60
+ class LoopControl(Exception):
61
+ """Internal non-local control transfer consumed by enclosing loop nodes."""
62
+
63
+ def __init__(self, levels: int) -> None:
64
+ super().__init__(levels)
65
+ self.levels = levels
66
+
67
+
68
+ class LoopBreak(LoopControl):
69
+ """Leave one or more enclosing loops."""
70
+
71
+
72
+ class LoopContinue(LoopControl):
73
+ """Continue one or more enclosing loops."""