@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
@@ -10,17 +10,20 @@ import io
10
10
  import os
11
11
  import subprocess
12
12
  import threading
13
+ import time
13
14
  from typing import BinaryIO
14
15
 
16
+ from arithmetic import ArithmeticError, compile_arithmetic, evaluate_arithmetic
15
17
  import nodes
16
18
  import parser as parser_module
17
19
  import proc
18
20
  import tokens as tokens_module
19
21
  from context import RUNNER_BUILTINS, STATE_BUILTINS, BuiltinContext, ExecContext
20
- from errors import ShellExit, UnsupportedConstruct
22
+ from errors import LoopBreak, LoopContinue, LoopControl, ShellExit, UnsupportedConstruct
21
23
  from state import ShellState
22
24
 
23
25
  DEVNULL = os.devnull
26
+ MAX_LOOP_ITERATIONS = 1_000_000
24
27
 
25
28
 
26
29
  class _Redirected:
@@ -344,7 +347,7 @@ def _run_pipeline_elements(elements: list, ctx: ExecContext) -> int:
344
347
  try:
345
348
  # Pipeline stages are subshell-like: state mutations belong to the stage,
346
349
  # never to the parent command context or a sibling stage.
347
- stage_ctx = _sub_ctx(ctx, ctx.state.copy(), sin, sout, stderr_base)
350
+ stage_ctx = _sub_ctx(ctx, ctx.state.copy(), sin, sout, stderr_base, loop_depth=0)
348
351
  results[idx] = _dispatch_element(elements[idx], stage_ctx, sin, sout, stderr_base, is_pipeline=True)
349
352
  except UnsupportedConstruct as exc:
350
353
  # A structured refusal from an inline stage means the WHOLE command
@@ -356,6 +359,10 @@ def _run_pipeline_elements(elements: list, ctx: ExecContext) -> int:
356
359
  results[idx] = 1
357
360
  except ShellExit as exc:
358
361
  results[idx] = exc.exit_code
362
+ except LoopControl:
363
+ # A pipeline stage is a subshell boundary. Loop control in that
364
+ # stage cannot alter an enclosing loop in the parent context.
365
+ results[idx] = 0
359
366
  except BaseException as exc: # noqa: BLE001 - any other stage failure must not crash the engine
360
367
  # Any OTHER exception is a real per-stage failure, not a refusal: it
361
368
  # gets a named, actionable one-line message on the merged sink (never
@@ -487,6 +494,8 @@ def _dispatch_element(
487
494
  return _execute_subshell(element, ctx, stdin_stream, stdout_stream, stderr_stream)
488
495
  if isinstance(element, nodes.BraceGroup):
489
496
  return _execute_brace_group(element, ctx, stdin_stream, stdout_stream, stderr_stream)
497
+ if isinstance(element, (nodes.ForCommand, nodes.ArithmeticForCommand)):
498
+ return _execute_for_command(element, ctx, stdin_stream, stdout_stream, stderr_stream)
490
499
  if isinstance(element, nodes.SimpleCommand):
491
500
  return _dispatch_simple_command(element, ctx, stdin_stream, stdout_stream, stderr_stream)
492
501
  raise UnsupportedConstruct("malformed-syntax", f"unrecognized pipeline element {type(element)!r}")
@@ -498,6 +507,8 @@ def _sub_ctx(
498
507
  stdin_stream: BinaryIO | int,
499
508
  stdout_stream: BinaryIO | int,
500
509
  stderr_stream: BinaryIO | int,
510
+ *,
511
+ loop_depth: int | None = None,
501
512
  ) -> ExecContext:
502
513
  return ExecContext(
503
514
  state=state,
@@ -508,11 +519,12 @@ def _sub_ctx(
508
519
  builtins=ctx.builtins,
509
520
  deadline=ctx.deadline,
510
521
  stderr=stderr_stream,
522
+ loop_depth=ctx.loop_depth if loop_depth is None else loop_depth,
511
523
  )
512
524
 
513
525
 
514
- def _execute_redirected_group(
515
- node: nodes.Subshell | nodes.BraceGroup,
526
+ def _execute_redirected_compound(
527
+ node: nodes.Subshell | nodes.BraceGroup | nodes.ForCommand | nodes.ArithmeticForCommand,
516
528
  ctx: ExecContext,
517
529
  stdin_stream,
518
530
  stdout_stream,
@@ -526,8 +538,18 @@ def _execute_redirected_group(
526
538
  node.redirects, ctx, stdin_stream, stdout_stream, stderr_stream, tracker
527
539
  )
528
540
  state = ctx.state.copy() if isolated else ctx.state
529
- inner_ctx = _sub_ctx(ctx, state, r_in, r_out, r_err)
541
+ inner_ctx = _sub_ctx(
542
+ ctx,
543
+ state,
544
+ r_in,
545
+ r_out,
546
+ r_err,
547
+ loop_depth=0 if isolated else ctx.loop_depth,
548
+ )
530
549
  try:
550
+ if isinstance(node, (nodes.ForCommand, nodes.ArithmeticForCommand)):
551
+ inner_ctx.loop_depth += 1
552
+ return _execute_loop(node, inner_ctx)
531
553
  return execute(node.body, inner_ctx)
532
554
  except ShellExit as exc:
533
555
  if isolated:
@@ -538,17 +560,89 @@ def _execute_redirected_group(
538
560
 
539
561
 
540
562
  def _execute_subshell(node: nodes.Subshell, ctx: ExecContext, stdin_stream, stdout_stream, stderr_stream) -> int:
541
- return _execute_redirected_group(
563
+ return _execute_redirected_compound(
542
564
  node, ctx, stdin_stream, stdout_stream, stderr_stream, isolated=True
543
565
  )
544
566
 
545
567
 
546
568
  def _execute_brace_group(node: nodes.BraceGroup, ctx: ExecContext, stdin_stream, stdout_stream, stderr_stream) -> int:
547
- return _execute_redirected_group(
569
+ return _execute_redirected_compound(
570
+ node, ctx, stdin_stream, stdout_stream, stderr_stream, isolated=False
571
+ )
572
+
573
+
574
+ def _execute_for_command(
575
+ node: nodes.ForCommand | nodes.ArithmeticForCommand,
576
+ ctx: ExecContext,
577
+ stdin_stream,
578
+ stdout_stream,
579
+ stderr_stream,
580
+ ) -> int:
581
+ return _execute_redirected_compound(
548
582
  node, ctx, stdin_stream, stdout_stream, stderr_stream, isolated=False
549
583
  )
550
584
 
551
585
 
586
+ def _check_loop_budget(ctx: ExecContext, iteration: int) -> None:
587
+ if ctx.deadline is not None and time.monotonic() >= ctx.deadline:
588
+ raise ShellExit(124)
589
+ if iteration >= MAX_LOOP_ITERATIONS:
590
+ _write_merged(
591
+ _merged_sink(ctx),
592
+ f"for: iteration limit ({MAX_LOOP_ITERATIONS}) exceeded\n".encode("utf-8"),
593
+ ctx,
594
+ )
595
+ raise ShellExit(124)
596
+
597
+
598
+ def _execute_loop_body(body: nodes.CommandList, ctx: ExecContext) -> tuple[int, str | None]:
599
+ try:
600
+ return execute(body, ctx), None
601
+ except LoopBreak as exc:
602
+ if exc.levels > 1:
603
+ raise LoopBreak(exc.levels - 1) from None
604
+ return 0, "break"
605
+ except LoopContinue as exc:
606
+ if exc.levels > 1:
607
+ raise LoopContinue(exc.levels - 1) from None
608
+ return 0, "continue"
609
+
610
+
611
+ def _execute_loop(node: nodes.ForCommand | nodes.ArithmeticForCommand, ctx: ExecContext) -> int:
612
+ if isinstance(node, nodes.ForCommand):
613
+ values: list[str] = []
614
+ for item in node.items:
615
+ values.extend(ctx.expand_word(item, ctx))
616
+
617
+ exit_code = 0
618
+ for iteration, value in enumerate(values):
619
+ _check_loop_budget(ctx, iteration)
620
+ ctx.state.setenv(node.name, value)
621
+ exit_code, action = _execute_loop_body(node.body, ctx)
622
+ if action == "break":
623
+ return exit_code
624
+ return exit_code
625
+
626
+ try:
627
+ initializer = compile_arithmetic(node.initializer)
628
+ condition = compile_arithmetic(node.condition)
629
+ update = compile_arithmetic(node.update)
630
+ evaluate_arithmetic(initializer, ctx.state)
631
+ exit_code = 0
632
+ iteration = 0
633
+ while condition is None or evaluate_arithmetic(condition, ctx.state) != 0:
634
+ _check_loop_budget(ctx, iteration)
635
+ iteration += 1
636
+ exit_code, action = _execute_loop_body(node.body, ctx)
637
+ if action == "break":
638
+ return exit_code
639
+ evaluate_arithmetic(update, ctx.state)
640
+ return exit_code
641
+ except ArithmeticError as exc:
642
+ _write_merged(_merged_sink(ctx), f"bash: arithmetic: {exc}\n".encode("utf-8"), ctx)
643
+ return 1
644
+
645
+
552
646
  def _dispatch_simple_command(
553
647
  command: nodes.SimpleCommand,
554
648
  ctx: ExecContext,
@@ -577,7 +671,7 @@ def _dispatch_simple_command(
577
671
  name = argv[0]
578
672
 
579
673
  if name in STATE_BUILTINS:
580
- return _run_state_builtin(name, argv, ctx, r_out)
674
+ return _run_state_builtin(name, argv, ctx, r_out, r_err)
581
675
 
582
676
  if name in RUNNER_BUILTINS:
583
677
  return _run_xargs(argv, ctx, r_in, r_out)
@@ -621,17 +715,46 @@ def _as_stream(fd_or_stream: BinaryIO | int, mode: str) -> BinaryIO:
621
715
  return fd_or_stream
622
716
 
623
717
 
624
- def _run_state_builtin(name: str, argv: list[str], ctx: ExecContext, out_stream: BinaryIO | int) -> int:
718
+ def _run_state_builtin(
719
+ name: str,
720
+ argv: list[str],
721
+ ctx: ExecContext,
722
+ out_stream: BinaryIO | int,
723
+ error_stream: BinaryIO | int | None = None,
724
+ ) -> int:
725
+ diagnostic_stream = error_stream if error_stream is not None else out_stream
726
+ if name in ("break", "continue"):
727
+ if len(argv) > 2:
728
+ _write_merged(diagnostic_stream, f"{name}: too many arguments\n".encode("utf-8"), ctx)
729
+ return 1
730
+ try:
731
+ levels = int(argv[1], 10) if len(argv) == 2 else 1
732
+ except ValueError:
733
+ _write_merged(diagnostic_stream, f"{name}: {argv[1]}: numeric argument required\n".encode("utf-8"), ctx)
734
+ return 1
735
+ if levels <= 0:
736
+ _write_merged(diagnostic_stream, f"{name}: loop count must be positive\n".encode("utf-8"), ctx)
737
+ return 1
738
+ if ctx.loop_depth == 0:
739
+ _write_merged(
740
+ diagnostic_stream,
741
+ f"bash: {name}: only meaningful in a 'for', 'while', or 'until' loop\n".encode("utf-8"),
742
+ ctx,
743
+ )
744
+ return 0
745
+ control = LoopBreak if name == "break" else LoopContinue
746
+ raise control(min(levels, ctx.loop_depth))
747
+
625
748
  if name == "exit":
626
749
  if len(argv) > 2:
627
- _write_merged(out_stream, b"exit: too many arguments\n", ctx)
750
+ _write_merged(diagnostic_stream, b"exit: too many arguments\n", ctx)
628
751
  raise ShellExit(1)
629
752
  if len(argv) == 1:
630
753
  raise ShellExit(ctx.state.last_exit_code)
631
754
  try:
632
755
  exit_code = int(argv[1], 10)
633
756
  except ValueError:
634
- _write_merged(out_stream, f"exit: {argv[1]}: numeric argument required\n".encode("utf-8"), ctx)
757
+ _write_merged(diagnostic_stream, f"exit: {argv[1]}: numeric argument required\n".encode("utf-8"), ctx)
635
758
  raise ShellExit(2)
636
759
  raise ShellExit(exit_code & 0xFF)
637
760
 
@@ -641,16 +764,16 @@ def _run_state_builtin(name: str, argv: list[str], ctx: ExecContext, out_stream:
641
764
  if print_new_cwd:
642
765
  oldpwd = ctx.state.env.get("OLDPWD")
643
766
  if not oldpwd:
644
- _write_merged(out_stream, b"cd: OLDPWD not set\n", ctx)
767
+ _write_merged(diagnostic_stream, b"cd: OLDPWD not set\n", ctx)
645
768
  return 1
646
769
  target = oldpwd
647
770
  if target is None:
648
- _write_merged(out_stream, b"cd: HOME not set\n", ctx)
771
+ _write_merged(diagnostic_stream, b"cd: HOME not set\n", ctx)
649
772
  return 1
650
773
  try:
651
774
  ctx.state.chdir(target)
652
775
  except FileNotFoundError:
653
- _write_merged(out_stream, f"cd: {target}: No such file or directory\n".encode("utf-8"), ctx)
776
+ _write_merged(diagnostic_stream, f"cd: {target}: No such file or directory\n".encode("utf-8"), ctx)
654
777
  return 1
655
778
  if print_new_cwd:
656
779
  _write_merged(out_stream, f"{ctx.state.cwd}\n".encode("utf-8"), ctx)
@@ -1,4 +1,4 @@
1
- """Frozen AST dataclasses for the pi shell engine: word segments + command structure.
1
+ """AST dataclasses for the pi shell engine: word segments + command structure.
2
2
 
3
3
  Transcribed verbatim from windows-shell-workpackages-2026-07-19.md §1.4. Type tags for
4
4
  tests come from `type(node).__name__`; do NOT add a `kind` field to any node.
@@ -95,7 +95,28 @@ class BraceGroup:
95
95
  redirects: list["Redirect"]
96
96
 
97
97
 
98
- PipelineElement = SimpleCommand | Subshell | BraceGroup
98
+ @dataclass
99
+ class ForCommand:
100
+ """POSIX word-list loop, including the omitted-``in`` positional form."""
101
+
102
+ name: str
103
+ items: list["Word"]
104
+ body: "CommandList"
105
+ redirects: list["Redirect"]
106
+
107
+
108
+ @dataclass
109
+ class ArithmeticForCommand:
110
+ """Bash arithmetic loop: ``for ((init; condition; update)); do ...; done``."""
111
+
112
+ initializer: str
113
+ condition: str
114
+ update: str
115
+ body: "CommandList"
116
+ redirects: list["Redirect"]
117
+
118
+
119
+ PipelineElement = SimpleCommand | Subshell | BraceGroup | ForCommand | ArithmeticForCommand
99
120
 
100
121
 
101
122
  @dataclass
@@ -13,9 +13,11 @@ import re
13
13
  from errors import UnsupportedConstruct
14
14
  from nodes import (
15
15
  AndOr,
16
+ ArithmeticForCommand,
16
17
  BraceGroup,
17
18
  CommandList,
18
19
  DQ,
20
+ ForCommand,
19
21
  Lit,
20
22
  Pipeline,
21
23
  PipelineElement,
@@ -28,11 +30,12 @@ from nodes import (
28
30
  from tokens import Token
29
31
 
30
32
  _ASSIGNMENT_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$", re.DOTALL)
33
+ _IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
31
34
  _EXTGLOB_RE = re.compile(r"[@!?*+]\(")
32
35
  _BRACE_EXPANSION_RE = re.compile(r"\{[^{}]*,[^{}]*\}")
33
36
 
34
37
  _JOB_CONTROL_WORDS = {"fg", "bg", "jobs", "wait", "disown"}
35
- _CONTROL_FLOW_WORDS = {"if", "for", "while", "until", "case", "select"}
38
+ _CONTROL_FLOW_WORDS = {"if", "while", "until", "case", "select"}
36
39
  _ARITHMETIC_WORDS = {"let"}
37
40
  _UNSUPPORTED_BUILTIN_WORDS = {"eval", "source", ".", "alias", "trap", "set", "shopt", "read", "declare", "local"}
38
41
  _NESTED_SHELL_WORDS = {"bash", "sh", "zsh", "fish", "cmd", "powershell", "pwsh", "wsl"}
@@ -124,26 +127,47 @@ class _Parser:
124
127
  tok = self.peek()
125
128
  return tok is not None and tok.kind == "OP" and tok.text == text
126
129
 
130
+ def at_unquoted_word(self, text: str) -> bool:
131
+ tok = self.peek()
132
+ return (
133
+ tok is not None
134
+ and tok.kind == "WORD"
135
+ and tok.segments is not None
136
+ and len(tok.segments) == 1
137
+ and isinstance(tok.segments[0], Raw)
138
+ and tok.segments[0].text == text
139
+ )
140
+
141
+ def _at_stop(self, stop_texts: frozenset[str], stop_words: frozenset[str]) -> bool:
142
+ tok = self.peek()
143
+ if tok is None:
144
+ return True
145
+ if tok.kind == "OP":
146
+ return tok.text in stop_texts
147
+ return any(self.at_unquoted_word(word) for word in stop_words)
148
+
127
149
  def _skip_seps(self) -> None:
128
150
  while self.at_op(";") or self.at_op("\n"):
129
151
  self.advance()
130
152
 
131
- def parse_command_list(self, stop_texts: frozenset[str]) -> CommandList:
153
+ def parse_command_list(
154
+ self, stop_texts: frozenset[str], stop_words: frozenset[str] = frozenset()
155
+ ) -> CommandList:
132
156
  entries = []
133
157
  separators: list[str] = []
134
158
  self._skip_seps()
135
159
  while True:
136
- tok = self.peek()
137
- if tok is None or (tok.kind == "OP" and tok.text in stop_texts):
160
+ if self._at_stop(stop_texts, stop_words):
138
161
  break
139
162
  entries.append(self.parse_and_or())
140
163
  if self.at_op("&"):
141
164
  raise UnsupportedConstruct(
142
165
  "job-control", "Background execution ('&') is not supported; run commands synchronously."
143
166
  )
144
- tok = self.peek()
145
- if tok is None or (tok.kind == "OP" and tok.text in stop_texts):
167
+ if self._at_stop(stop_texts, stop_words):
146
168
  break
169
+ tok = self.peek()
170
+ assert tok is not None
147
171
  if tok.kind == "OP" and tok.text in (";", "\n"):
148
172
  separators.append(self.advance().text) # type: ignore[arg-type]
149
173
  self._skip_seps()
@@ -194,6 +218,13 @@ class _Parser:
194
218
  self.advance()
195
219
  redirects = self._parse_redirects()
196
220
  return BraceGroup(body=body, redirects=redirects)
221
+ if self.at_unquoted_word("for"):
222
+ return self.parse_for_command()
223
+ if self.peek() is not None and self.peek().kind == "ARITH":
224
+ raise UnsupportedConstruct(
225
+ "arithmetic-expansion",
226
+ "Arithmetic commands '((...))' are supported only as a for-loop header.",
227
+ )
197
228
  command = self.parse_simple_command()
198
229
  if not command.assignments and not command.words and not command.redirects:
199
230
  # A fully empty SimpleCommand reached as a pipeline element means a missing
@@ -204,6 +235,103 @@ class _Parser:
204
235
  )
205
236
  return command
206
237
 
238
+ def _parse_for_body(self) -> CommandList:
239
+ if not (self.at_op(";") or self.at_op("\n")):
240
+ raise UnsupportedConstruct(
241
+ "malformed-syntax", "A for loop requires ';' or a newline before 'do'."
242
+ )
243
+ self._skip_seps()
244
+ if not self.at_unquoted_word("do"):
245
+ raise UnsupportedConstruct("malformed-syntax", "A for loop is missing its 'do' keyword.")
246
+ self.advance()
247
+
248
+ body = self.parse_command_list(frozenset(), frozenset({"done"}))
249
+ if not self.at_unquoted_word("done"):
250
+ raise UnsupportedConstruct("malformed-syntax", "A for loop is missing its closing 'done' keyword.")
251
+ if not body.entries:
252
+ raise UnsupportedConstruct("malformed-syntax", "A for loop requires at least one command in its body.")
253
+ self.advance()
254
+ return body
255
+
256
+ @staticmethod
257
+ def _split_arithmetic_header(header: str) -> tuple[str, str, str]:
258
+ parts: list[str] = []
259
+ start = 0
260
+ depth = 0
261
+ for index, char in enumerate(header):
262
+ if char == "(":
263
+ depth += 1
264
+ elif char == ")":
265
+ depth -= 1
266
+ if depth < 0:
267
+ raise UnsupportedConstruct(
268
+ "malformed-syntax", "Arithmetic for header contains an unmatched ')'."
269
+ )
270
+ elif char == ";" and depth == 0:
271
+ parts.append(header[start:index].strip())
272
+ start = index + 1
273
+ if depth != 0:
274
+ raise UnsupportedConstruct(
275
+ "malformed-syntax", "Arithmetic for header contains unbalanced parentheses."
276
+ )
277
+ parts.append(header[start:].strip())
278
+ if len(parts) != 3:
279
+ raise UnsupportedConstruct(
280
+ "malformed-syntax",
281
+ "An arithmetic for loop requires exactly 'initializer; condition; update'.",
282
+ )
283
+ return parts[0], parts[1], parts[2]
284
+
285
+ def parse_for_command(self) -> ForCommand | ArithmeticForCommand:
286
+ self.advance() # `for`
287
+ arithmetic = self.peek()
288
+ if arithmetic is not None and arithmetic.kind == "ARITH":
289
+ self.advance()
290
+ initializer, condition, update = self._split_arithmetic_header(arithmetic.text or "")
291
+ body = self._parse_for_body()
292
+ return ArithmeticForCommand(
293
+ initializer=initializer,
294
+ condition=condition,
295
+ update=update,
296
+ body=body,
297
+ redirects=self._parse_redirects(),
298
+ )
299
+
300
+ variable = self.peek()
301
+ variable_name = (
302
+ variable.segments[0].text
303
+ if variable is not None
304
+ and variable.kind == "WORD"
305
+ and variable.segments is not None
306
+ and len(variable.segments) == 1
307
+ and isinstance(variable.segments[0], Raw)
308
+ else ""
309
+ )
310
+ if not _IDENTIFIER_RE.fullmatch(variable_name):
311
+ raise UnsupportedConstruct(
312
+ "malformed-syntax", "A for loop requires an unquoted shell variable name after 'for'."
313
+ )
314
+ self.advance()
315
+ items: list[Word] = []
316
+ if self.at_unquoted_word("in"):
317
+ self.advance()
318
+ while True:
319
+ tok = self.peek()
320
+ if tok is None or tok.kind == "OP":
321
+ break
322
+ if tok.kind != "WORD":
323
+ raise UnsupportedConstruct(
324
+ "malformed-syntax", "Unexpected token in a for loop word list."
325
+ )
326
+ items.append(Word(segments=tok.segments or []))
327
+ self.advance()
328
+ elif not (self.at_op(";") or self.at_op("\n")):
329
+ raise UnsupportedConstruct(
330
+ "malformed-syntax", "A for loop requires 'in values' or ';' after its variable name."
331
+ )
332
+ body = self._parse_for_body()
333
+ return ForCommand(name=variable_name, items=items, body=body, redirects=self._parse_redirects())
334
+
207
335
  def _parse_redirects(self) -> list[Redirect]:
208
336
  redirects: list[Redirect] = []
209
337
  while True:
@@ -20,7 +20,7 @@ _IDENT_START_RE = re.compile(r"[A-Za-z_]")
20
20
  _IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
21
21
  @dataclass
22
22
  class Token:
23
- kind: str # "WORD" | "OP"
23
+ kind: str # "WORD" | "OP" | "ARITH"
24
24
  text: str | None = None # OP: exact operator text (e.g. ">>", "2>&1", "\n")
25
25
  segments: list[Segment] | None = None # WORD: ordered segments
26
26
 
@@ -402,6 +402,26 @@ def _scan_operator(src: str, pos: int) -> tuple[str, int]:
402
402
  raise AssertionError(f"unreachable operator dispatch at {pos!r}")
403
403
 
404
404
 
405
+ def _scan_arithmetic_group(src: str, pos: int) -> tuple[str, int]:
406
+ """Scan ``((...))`` as one token while allowing balanced inner parentheses."""
407
+ depth = 0
408
+ cursor = pos + 2
409
+ while cursor < len(src):
410
+ if src[cursor : cursor + 2] == "))" and depth == 0:
411
+ return src[pos + 2 : cursor], cursor + 2
412
+ char = src[cursor]
413
+ if char == "(":
414
+ depth += 1
415
+ elif char == ")":
416
+ if depth == 0:
417
+ raise UnsupportedConstruct(
418
+ "malformed-syntax", "Arithmetic loop header contains an unmatched ')'."
419
+ )
420
+ depth -= 1
421
+ cursor += 1
422
+ raise _unterminated("arithmetic loop header")
423
+
424
+
405
425
  def tokenize(src: str) -> list[Token]:
406
426
  n = len(src)
407
427
  tokens: list[Token] = []
@@ -425,7 +445,10 @@ def tokenize(src: str) -> list[Token]:
425
445
  pos = n if newline == -1 else newline
426
446
  continue
427
447
  if src[pos : pos + 2] == "((":
428
- raise UnsupportedConstruct("arithmetic-expansion", "Arithmetic command '((...))' is not supported.")
448
+ arithmetic, newpos = _scan_arithmetic_group(src, pos)
449
+ tokens.append(Token(kind="ARITH", text=arithmetic))
450
+ pos = newpos
451
+ continue
429
452
  if c.isdigit():
430
453
  j = pos
431
454
  while j < n and src[j].isdigit():
@@ -49,11 +49,13 @@ Inspect the scanner, wrapper, package scripts, and CI command before trusting a
49
49
  For jscpd:
50
50
 
51
51
  1. Prefer an existing repository wrapper such as `npm run check:clones`.
52
- 2. Require pinned jscpd v5 and verify the major version before relying on its Rust-native CLI. Do not silently fall back to the v4 Node engine.
53
- 3. Explicitly configure `maxLines` and `maxSize`; defaults can omit god units. Keep both far above the largest legitimate owned file.
54
- 4. Pin `minLines`, `minTokens`, `mode`, formats, cross-format policy, exact ignores, JSON reporter, and zero threshold in a regression test.
55
- 5. Reconcile every owned candidate with the analyzed count. Account for below-minimum files by exact path and proof that they cannot contain the configured block size.
56
- 6. Keep console projection bounded and retain the complete JSON report out of model context for explicit inspection.
52
+ 2. Use the Pi-managed jscpd v5 binary; never install jscpd into the project with npm, npx, Bun, or another package manager. If it is unavailable, run `pi doctor` and block or change approach rather than creating project metadata or dependencies.
53
+ 3. Write ad-hoc configs and full reports only under Pi-managed work storage or the OS temporary directory. Do not create repository reports, configs, lockfiles, manifests, or `node_modules` without an explicit request.
54
+ 4. Verify the pinned major version before relying on its Rust-native CLI. Do not silently fall back to the v4 Node engine.
55
+ 5. Explicitly configure `maxLines` and `maxSize`; defaults can omit god units. Keep both far above the largest legitimate owned file.
56
+ 6. Pin `minLines`, `minTokens`, `mode`, formats, cross-format policy, exact ignores, JSON reporter, and zero threshold in a regression test.
57
+ 7. Reconcile every owned candidate with the analyzed count. Account for below-minimum files by exact path and proof that they cannot contain the configured block size.
58
+ 8. Keep console projection bounded and retain the complete JSON report out of model context for explicit inspection.
57
59
 
58
60
  Never make the gate green by raising thresholds, increasing minimum clone size, adding broad ignores, renaming identifiers, or reformatting clone blocks.
59
61
 
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: worker-profile-writer
3
+ description: "Use when an orchestrator needs a task-scoped worker profile with a selected model, thinking level, tools, resources, or tighter budget before delegation."
4
+ ---
5
+
6
+ # Worker Profile Writer
7
+
8
+ ## How to use the skill
9
+
10
+ Use this skill before `delegate` when no owner-authored worker profile is a precise fit.
11
+
12
+ Freedom Dial: high freedom for describing and composing the bounded task; low freedom for authority, persistence, model availability, and budget checks. Pi's core engineering and evidence rules remain mandatory.
13
+
14
+ ## North Star
15
+
16
+ Create the smallest immutable profile that can complete one explicit task, then delegate with the exact harness-issued profile ID. The session log owns task profiles; project and user profile directories remain untouched.
17
+
18
+ ## Core Sections
19
+
20
+ ### 1. Establish the contract
21
+
22
+ 1. State the worker's single outcome, required evidence, intended files, and completion boundary.
23
+ 2. Call `profile_writer` with `action: "inspect"` when authorized bases or configured model/thinking combinations are unknown.
24
+ 3. Choose an owner-authored base whose role and authority already cover the task. Never derive from another generated task profile.
25
+ 4. Prefer the fastest model with demonstrated ability to satisfy the task contract. Configuration and authentication prove availability, not fitness; use existing fitness evidence or `model_fitness` when the risk justifies its token cost.
26
+ 5. Request only the tools and resource profiles the task needs. Omitted fields inherit the base; supplied fields must be exact subsets. Budgets may only tighten.
27
+
28
+ Human edge: if the task requires an unlisted base, unavailable credentials, broader tools/resources, or a looser budget, stop and ask the user to authorize a durable owner-profile change. Do not route around a rejection.
29
+
30
+ ### 2. Execute and verify
31
+
32
+ 1. Call `profile_writer` with `action: "create"`; never supply or invent a profile ID.
33
+ 2. Confirm `created: true`, the expected base, and the reported changed fields.
34
+ 3. Pass the returned `profileId` unchanged to one bounded `delegate` task with explicit acceptance criteria.
35
+ 4. Wait for the event-driven terminal handoff, retrieve once with `delegate_status`, and independently verify worker claims.
36
+ 5. Treat rejection before persistence as a successful safety boundary. Change the approach or request authority; do not retry the same expansion with different wording.
37
+
38
+ ## Anti-Patterns
39
+
40
+ - Writing one-off profile JSON, settings, reports, or dependencies into the repository.
41
+ - Selecting a model or thinking level that `inspect` did not report as available.
42
+ - Giving a worker extra tools "just in case" or loosening the base budget.
43
+ - Inventing, guessing, editing, or reusing a generated profile ID across sessions or branches.
44
+ - Treating a fast model as fit without task-shaped evidence.
45
+
46
+ ## Examples
47
+
48
+ Positive: inspect, derive from a read/write implementer, select an authenticated fast model at `low`, narrow tools to `read`, `grep`, and `edit`, tighten token/tool-call limits, then delegate with the returned `task-...` ID.
49
+
50
+ Negative: a base exposes only `read` and `grep`, but the task requires `edit`. Do not request `edit` from `profile_writer`; select an authorized write-capable base or ask the user for an owner-profile change.
51
+
52
+ ## Self-Check
53
+
54
+ - The task has one bounded outcome and explicit acceptance evidence.
55
+ - The base is owner-authored and authorized for this orchestrator.
56
+ - Model and thinking level were reported available or otherwise resolved by the harness.
57
+ - Tools/resources are subsets and the budget is unchanged or tighter.
58
+ - The returned ID is passed unchanged; no project/user profile file was created.
59
+
60
+ ## Known Gaps
61
+
62
+ - Availability does not prove model quality; use task-shaped fitness evidence where failure risk is material.
63
+ - Generated profiles cannot expand authority or survive outside their owning session branch.