@caupulican/pi-adaptative 0.84.1 → 0.85.3

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 (214) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -1
  3. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +14 -8
  4. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -1
  5. package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +14 -8
  6. package/dist/bundled-resources/runtimes/pi-shell-engine/arithmetic.py +364 -0
  7. package/dist/bundled-resources/runtimes/pi-shell-engine/context.py +2 -1
  8. package/dist/bundled-resources/runtimes/pi-shell-engine/errors.py +16 -0
  9. package/dist/bundled-resources/runtimes/pi-shell-engine/exec.py +137 -14
  10. package/dist/bundled-resources/runtimes/pi-shell-engine/nodes.py +23 -2
  11. package/dist/bundled-resources/runtimes/pi-shell-engine/parser.py +134 -6
  12. package/dist/bundled-resources/runtimes/pi-shell-engine/tokens.py +25 -2
  13. package/dist/bundled-resources/skills/deduplicate-by-evidence/SKILL.md +7 -5
  14. package/dist/bundled-resources/skills/worker-profile-writer/SKILL.md +63 -0
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +8 -0
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  19. package/dist/core/autonomy/lane-tool-surface.js +4 -2
  20. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  21. package/dist/core/background-lane-controller.d.ts +3 -0
  22. package/dist/core/background-lane-controller.d.ts.map +1 -1
  23. package/dist/core/background-lane-controller.js +6 -0
  24. package/dist/core/background-lane-controller.js.map +1 -1
  25. package/dist/core/compaction-controller.d.ts +2 -0
  26. package/dist/core/compaction-controller.d.ts.map +1 -1
  27. package/dist/core/compaction-controller.js +35 -13
  28. package/dist/core/compaction-controller.js.map +1 -1
  29. package/dist/core/default-tool-surface.d.ts.map +1 -1
  30. package/dist/core/default-tool-surface.js +1 -0
  31. package/dist/core/default-tool-surface.js.map +1 -1
  32. package/dist/core/delegation/worker-delegation-controller.d.ts +7 -0
  33. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
  34. package/dist/core/delegation/worker-delegation-controller.js +28 -0
  35. package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
  36. package/dist/core/delegation/worker-profile-resolver.d.ts +2 -0
  37. package/dist/core/delegation/worker-profile-resolver.d.ts.map +1 -1
  38. package/dist/core/delegation/worker-profile-resolver.js +46 -11
  39. package/dist/core/delegation/worker-profile-resolver.js.map +1 -1
  40. package/dist/core/doctor.d.ts +1 -1
  41. package/dist/core/doctor.d.ts.map +1 -1
  42. package/dist/core/doctor.js +5 -4
  43. package/dist/core/doctor.js.map +1 -1
  44. package/dist/core/failure-corpus.d.ts +26 -1
  45. package/dist/core/failure-corpus.d.ts.map +1 -1
  46. package/dist/core/failure-corpus.js +17 -0
  47. package/dist/core/failure-corpus.js.map +1 -1
  48. package/dist/core/model-capability.d.ts.map +1 -1
  49. package/dist/core/model-capability.js +1 -0
  50. package/dist/core/model-capability.js.map +1 -1
  51. package/dist/core/model-tool-protocol.d.ts +3 -2
  52. package/dist/core/model-tool-protocol.d.ts.map +1 -1
  53. package/dist/core/model-tool-protocol.js +5 -1
  54. package/dist/core/model-tool-protocol.js.map +1 -1
  55. package/dist/core/models/runtime-process.d.ts +7 -1
  56. package/dist/core/models/runtime-process.d.ts.map +1 -1
  57. package/dist/core/models/runtime-process.js +4 -2
  58. package/dist/core/models/runtime-process.js.map +1 -1
  59. package/dist/core/orchestration/lane-tool-manifests.d.ts +1 -1
  60. package/dist/core/orchestration/lane-tool-manifests.d.ts.map +1 -1
  61. package/dist/core/orchestration/lane-tool-manifests.js +6 -1
  62. package/dist/core/orchestration/lane-tool-manifests.js.map +1 -1
  63. package/dist/core/orchestration/session-task-profile-store.d.ts +31 -0
  64. package/dist/core/orchestration/session-task-profile-store.d.ts.map +1 -0
  65. package/dist/core/orchestration/session-task-profile-store.js +138 -0
  66. package/dist/core/orchestration/session-task-profile-store.js.map +1 -0
  67. package/dist/core/orchestration/task-profile-writer.d.ts +58 -0
  68. package/dist/core/orchestration/task-profile-writer.d.ts.map +1 -0
  69. package/dist/core/orchestration/task-profile-writer.js +165 -0
  70. package/dist/core/orchestration/task-profile-writer.js.map +1 -0
  71. package/dist/core/process-matrix/runtime.js +43 -11
  72. package/dist/core/process-matrix/runtime.js.map +1 -1
  73. package/dist/core/resource-loader.d.ts +2 -0
  74. package/dist/core/resource-loader.d.ts.map +1 -1
  75. package/dist/core/resource-loader.js +18 -6
  76. package/dist/core/resource-loader.js.map +1 -1
  77. package/dist/core/runtime-builder.d.ts +3 -1
  78. package/dist/core/runtime-builder.d.ts.map +1 -1
  79. package/dist/core/runtime-builder.js +14 -0
  80. package/dist/core/runtime-builder.js.map +1 -1
  81. package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
  82. package/dist/core/security/untrusted-boundary.js +4 -6
  83. package/dist/core/security/untrusted-boundary.js.map +1 -1
  84. package/dist/core/session-role.d.ts.map +1 -1
  85. package/dist/core/session-role.js +1 -0
  86. package/dist/core/session-role.js.map +1 -1
  87. package/dist/core/system-prompt.d.ts.map +1 -1
  88. package/dist/core/system-prompt.js +21 -23
  89. package/dist/core/system-prompt.js.map +1 -1
  90. package/dist/core/tool-capability-policy.js +1 -1
  91. package/dist/core/tool-capability-policy.js.map +1 -1
  92. package/dist/core/tool-recovery-log-records.d.ts +33 -1
  93. package/dist/core/tool-recovery-log-records.d.ts.map +1 -1
  94. package/dist/core/tool-recovery-log-records.js +59 -12
  95. package/dist/core/tool-recovery-log-records.js.map +1 -1
  96. package/dist/core/tool-recovery-logger.d.ts +12 -1
  97. package/dist/core/tool-recovery-logger.d.ts.map +1 -1
  98. package/dist/core/tool-recovery-logger.js +24 -1
  99. package/dist/core/tool-recovery-logger.js.map +1 -1
  100. package/dist/core/tool-repair-health.d.ts.map +1 -1
  101. package/dist/core/tool-repair-health.js +6 -0
  102. package/dist/core/tool-repair-health.js.map +1 -1
  103. package/dist/core/tools/bash.d.ts +3 -8
  104. package/dist/core/tools/bash.d.ts.map +1 -1
  105. package/dist/core/tools/bash.js +4 -28
  106. package/dist/core/tools/bash.js.map +1 -1
  107. package/dist/core/tools/delegate.js +4 -4
  108. package/dist/core/tools/delegate.js.map +1 -1
  109. package/dist/core/tools/edit-diff.d.ts +32 -5
  110. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  111. package/dist/core/tools/edit-diff.js +137 -69
  112. package/dist/core/tools/edit-diff.js.map +1 -1
  113. package/dist/core/tools/edit.d.ts +22 -7
  114. package/dist/core/tools/edit.d.ts.map +1 -1
  115. package/dist/core/tools/edit.js +128 -55
  116. package/dist/core/tools/edit.js.map +1 -1
  117. package/dist/core/tools/file-mutation-intent.d.ts +70 -0
  118. package/dist/core/tools/file-mutation-intent.d.ts.map +1 -0
  119. package/dist/core/tools/file-mutation-intent.js +325 -0
  120. package/dist/core/tools/file-mutation-intent.js.map +1 -0
  121. package/dist/core/tools/find.d.ts +2 -1
  122. package/dist/core/tools/find.d.ts.map +1 -1
  123. package/dist/core/tools/find.js +2 -6
  124. package/dist/core/tools/find.js.map +1 -1
  125. package/dist/core/tools/goal.d.ts.map +1 -1
  126. package/dist/core/tools/goal.js +41 -18
  127. package/dist/core/tools/goal.js.map +1 -1
  128. package/dist/core/tools/grep.d.ts +2 -1
  129. package/dist/core/tools/grep.d.ts.map +1 -1
  130. package/dist/core/tools/grep.js +2 -6
  131. package/dist/core/tools/grep.js.map +1 -1
  132. package/dist/core/tools/index.d.ts +3 -1
  133. package/dist/core/tools/index.d.ts.map +1 -1
  134. package/dist/core/tools/index.js +31 -8
  135. package/dist/core/tools/index.js.map +1 -1
  136. package/dist/core/tools/managed-search-tool.d.ts +8 -0
  137. package/dist/core/tools/managed-search-tool.d.ts.map +1 -0
  138. package/dist/core/tools/managed-search-tool.js +10 -0
  139. package/dist/core/tools/managed-search-tool.js.map +1 -0
  140. package/dist/core/tools/profile-writer.d.ts +33 -0
  141. package/dist/core/tools/profile-writer.d.ts.map +1 -0
  142. package/dist/core/tools/profile-writer.js +67 -0
  143. package/dist/core/tools/profile-writer.js.map +1 -0
  144. package/dist/core/tools/shell-command-parser.d.ts +4 -3
  145. package/dist/core/tools/shell-command-parser.d.ts.map +1 -1
  146. package/dist/core/tools/shell-command-parser.js +32 -7
  147. package/dist/core/tools/shell-command-parser.js.map +1 -1
  148. package/dist/core/tools/shell-contract-router.d.ts.map +1 -1
  149. package/dist/core/tools/shell-contract-router.js +23 -1
  150. package/dist/core/tools/shell-contract-router.js.map +1 -1
  151. package/dist/core/tools/task-steps.d.ts.map +1 -1
  152. package/dist/core/tools/task-steps.js +6 -11
  153. package/dist/core/tools/task-steps.js.map +1 -1
  154. package/dist/core/tools/tmux-dispatch.d.ts +2 -0
  155. package/dist/core/tools/tmux-dispatch.d.ts.map +1 -1
  156. package/dist/core/tools/tmux-dispatch.js +16 -2
  157. package/dist/core/tools/tmux-dispatch.js.map +1 -1
  158. package/dist/core/tools/windows-shell-engine.js +1 -1
  159. package/dist/core/tools/windows-shell-engine.js.map +1 -1
  160. package/dist/core/tools/write.d.ts +24 -7
  161. package/dist/core/tools/write.d.ts.map +1 -1
  162. package/dist/core/tools/write.js +182 -107
  163. package/dist/core/tools/write.js.map +1 -1
  164. package/dist/index.d.ts +1 -1
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +1 -1
  167. package/dist/index.js.map +1 -1
  168. package/dist/jscpd-cli.d.ts +3 -0
  169. package/dist/jscpd-cli.d.ts.map +1 -0
  170. package/dist/jscpd-cli.js +22 -0
  171. package/dist/jscpd-cli.js.map +1 -0
  172. package/dist/modes/interactive/components/profile-resource-editor.js +2 -2
  173. package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
  174. package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  175. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/tool-execution.js +85 -3
  177. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  178. package/dist/modes/interactive/interactive-mode.js +1 -1
  179. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  180. package/dist/utils/bundled-jscpd.d.ts +20 -0
  181. package/dist/utils/bundled-jscpd.d.ts.map +1 -0
  182. package/dist/utils/bundled-jscpd.js +154 -0
  183. package/dist/utils/bundled-jscpd.js.map +1 -0
  184. package/dist/utils/paths.js +1 -1
  185. package/dist/utils/paths.js.map +1 -1
  186. package/dist/utils/shell.d.ts.map +1 -1
  187. package/dist/utils/shell.js +11 -0
  188. package/dist/utils/shell.js.map +1 -1
  189. package/dist/utils/tools-manager.d.ts +19 -2
  190. package/dist/utils/tools-manager.d.ts.map +1 -1
  191. package/dist/utils/tools-manager.js +92 -40
  192. package/dist/utils/tools-manager.js.map +1 -1
  193. package/docs/index.md +3 -0
  194. package/docs/managed-data-tools.md +15 -0
  195. package/docs/models.md +1 -1
  196. package/docs/quickstart.md +2 -2
  197. package/docs/settings.md +1 -1
  198. package/docs/tool-boundary-performance-roadmap-2026-08-02.md +166 -0
  199. package/docs/tool-repair.md +4 -4
  200. package/docs/windows.md +7 -5
  201. package/docs/worker-profiles.md +16 -0
  202. package/examples/extensions/built-in-tool-renderer.ts +12 -5
  203. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  204. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  205. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  206. package/examples/extensions/minimal-mode.ts +8 -7
  207. package/examples/extensions/prompt-customizer.ts +4 -2
  208. package/examples/extensions/sandbox/package-lock.json +2 -2
  209. package/examples/extensions/sandbox/package.json +1 -1
  210. package/examples/extensions/ssh.ts +113 -24
  211. package/examples/extensions/with-deps/package-lock.json +2 -2
  212. package/examples/extensions/with-deps/package.json +1 -1
  213. package/npm-shrinkwrap.json +112 -12
  214. package/package.json +7 -5
@@ -592,25 +592,29 @@ function formatDetection(d: TmuxDetection): string {
592
592
  }
593
593
  function setupHelp(d: TmuxDetection): string {
594
594
  if (d.cliAvailable)
595
- return `tmux CLI available: ${d.tmuxBin || "tmux"}\nUse tmux_agent_manager action=workspace_plan or fire_task. Set dryRun=true only when a preview is useful.`;
595
+ return `tmux CLI available: ${d.tmuxBin || "tmux"}\nUse native delegate for ordinary subagents. Use tmux_agent_manager only for an owner-requested persistent interactive pane; set dryRun=true only when a preview is useful.`;
596
596
  return [
597
597
  "tmux CLI not found.",
598
- "Install tmux for your environment, then reload Pi:",
598
+ "Ordinary subagents remain available through native delegate; do not abort delegation.",
599
+ "Install tmux only if persistent interactive provider panes are required, then reload Pi:",
599
600
  "- Debian/Ubuntu/WSL: sudo apt-get install tmux",
600
601
  "- Fedora: sudo dnf install tmux",
601
602
  "- Arch: sudo pacman -S tmux",
602
603
  "- macOS: brew install tmux",
603
- "- Windows: use WSL/MSYS2/Cygwin where a real tmux binary is available on PATH.",
604
+ "- Native Windows: use native delegate. WSL may use tmux when it is installed on PATH.",
604
605
  "cmux is macOS-only and intentionally not used by this tmux manager.",
605
606
  ].join("\n");
606
607
  }
607
608
  async function guardTmux(_ctx: ExtensionContext, detection: TmuxDetection, purpose: string) {
608
609
  if (detection.cliAvailable) return { allowed: true, text: `tmux available for ${purpose}.` };
609
- return { allowed: false, text: `${setupHelp(detection)}\n\nCannot run tmux-managed work until tmux is on PATH.` };
610
+ return {
611
+ allowed: false,
612
+ text: `${setupHelp(detection)}\n\nTmux route unavailable. Continue ordinary subagent work with native delegate.`,
613
+ };
610
614
  }
611
615
  function tmuxManagerInstructions(toolName: string, detection: TmuxDetection): string {
612
616
  return [
613
- `tmux is available (${detection.version || detection.tmuxBin || "tmux"}). For managed ${toolName} work, use tmux_agent_manager instead of direct foreground/background dispatch.`,
617
+ `tmux is available (${detection.version || detection.tmuxBin || "tmux"}). Native delegate remains the standard subagent route. Use tmux_agent_manager for managed ${toolName} panes only when the owner explicitly requested persistent interactive provider processes.`,
614
618
  "Suggested next call:",
615
619
  'tmux_agent_manager({ action: "fire_task", teamTemplate: "builder-validator", task: "<objective>" })',
616
620
  "The launch returns immediately after panes, event watchers, and prompt handoffs are armed.",
@@ -2195,11 +2199,13 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
2195
2199
  name: "tmux_agent_manager",
2196
2200
  label: "tmux Agent Manager",
2197
2201
  description:
2198
- "tmux-exclusive interactive agent/workspace manager for Windows/Linux/macOS environments where tmux is on PATH. Opens real provider CLIs in panes, injects prompts, captures output, and replaces cmux outside iOS/macOS cmux use.",
2202
+ "Specialized tmux-backed interactive provider/workspace manager. Native delegate is the standard cross-platform subagent path. Use this tool only when the owner explicitly requests persistent interactive CLI panes and tmux is available.",
2199
2203
  promptSnippet:
2200
- "Use tmux to launch managed interactive agent/provider panes with prompt injection, captured output, completion markers, shared variables, and final notifications.",
2204
+ "Use native delegate for ordinary subagents. Use tmux only for owner-requested persistent interactive provider panes after action=guard confirms availability.",
2201
2205
  promptGuidelines: [
2202
- "Use tmux_agent_manager for Windows/Linux tmux-managed workers. Do not use cmux on Windows/Linux; cmux is macOS-only and manual-disabled there.",
2206
+ "Use native delegate/delegate_status for ordinary subagent work on every OS. Use tmux_agent_manager only when the owner explicitly requests persistent interactive provider panes and action=guard confirms tmux is available.",
2207
+ "On native Windows, do not attempt tmux dispatch. Native delegate is the supported backend; WSL may use tmux only when installed and explicitly requested.",
2208
+ "Never create or edit orchestration profiles during dispatch; native delegation's owner-configured profile resolver is authoritative.",
2203
2209
  "Prefer action=fire_task for interactive worker batches. It returns after event watchers and prompts are armed; do not wait, poll, or peek for completion.",
2204
2210
  "Use action=list_templates/show_template before assembling repeated teams; pass teamTemplate when a built-in team fits.",
2205
2211
  "Use action=workspace_plan before launch_workspace when designing a pane layout.",
@@ -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."""