soothe-cli 0.4.9__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/PKG-INFO +2 -2
  2. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/pyproject.toml +1 -1
  3. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +2 -2
  4. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/loop_cmd.py +18 -33
  5. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/run_cmd.py +7 -5
  6. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/daemon.py +21 -9
  7. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/headless.py +2 -2
  8. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/launcher.py +2 -2
  9. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/main.py +1 -5
  10. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/__init__.py +0 -4
  11. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/command_router.py +33 -14
  12. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/slash_commands.py +15 -66
  13. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/event_processor.py +12 -13
  14. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/presentation_engine.py +1 -1
  15. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/processor_state.py +3 -3
  16. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/tui_trace_log.py +1 -0
  17. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_app.py +15 -17
  18. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_commands.py +15 -23
  19. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_execution.py +24 -22
  20. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_history.py +93 -98
  21. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_messages_mixin.py +32 -49
  22. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_model.py +69 -74
  23. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_module_init.py +23 -29
  24. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_startup.py +55 -69
  25. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/command_registry.py +2 -2
  26. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/config.py +4 -4
  27. soothe_cli-0.5.0/src/soothe_cli/tui/daemon_session.py +206 -0
  28. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/model_config.py +17 -85
  29. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/output.py +1 -1
  30. soothe_cli-0.5.0/src/soothe_cli/tui/sessions.py +380 -0
  31. soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/__init__.py +111 -0
  32. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/textual_adapter/_turn.py +1 -1
  33. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/approval.py +2 -2
  34. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/history.py +1 -1
  35. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/loop_selector.py +4 -5
  36. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/welcome.py +43 -7
  37. soothe_cli-0.4.9/src/soothe_cli/cli/commands/thread_cmd.py +0 -412
  38. soothe_cli-0.4.9/src/soothe_cli/tui/daemon_session.py +0 -331
  39. soothe_cli-0.4.9/src/soothe_cli/tui/sessions.py +0 -1359
  40. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/__init__.py +0 -29
  41. soothe_cli-0.4.9/src/soothe_cli/tui/widgets/thread_selector.py +0 -1817
  42. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/.gitignore +0 -0
  43. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/README.md +0 -0
  44. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/__init__.py +0 -0
  45. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/__init__.py +0 -0
  46. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
  47. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
  48. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  49. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/__init__.py +0 -0
  50. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/context.py +0 -0
  51. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/display_line.py +0 -0
  52. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/formatter.py +0 -0
  53. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  54. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  55. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/config/__init__.py +0 -0
  56. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/config/cli_config.py +0 -0
  57. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/plan/__init__.py +0 -0
  58. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/plan/rich_tree.py +0 -0
  59. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/__init__.py +0 -0
  60. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  61. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/config_loader.py +0 -0
  62. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/__init__.py +0 -0
  63. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  64. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/__init__.py +0 -0
  65. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/display_policy.py +0 -0
  66. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/essential_events.py +0 -0
  67. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  68. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  69. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  70. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  71. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  72. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/__init__.py +0 -0
  73. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/_utils.py +0 -0
  74. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  75. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/rendering.py +0 -0
  76. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  77. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  78. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  79. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  80. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  81. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  82. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  83. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  84. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  85. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  86. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  87. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  88. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  89. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  90. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  91. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/__init__.py +0 -0
  92. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  93. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/_cli_context.py +0 -0
  94. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/_env_vars.py +0 -0
  95. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/_session_stats.py +0 -0
  96. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/_version.py +0 -0
  97. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/__init__.py +0 -0
  98. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/_ui.py +0 -0
  99. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/app/app.tcss +0 -0
  100. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/file_ops.py +0 -0
  101. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/formatting.py +0 -0
  102. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/hooks.py +0 -0
  103. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/input.py +0 -0
  104. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/media_utils.py +0 -0
  105. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/message_display_filter.py +0 -0
  106. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/preview_limits.py +0 -0
  107. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/project_utils.py +0 -0
  108. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
  109. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/invocation.py +0 -0
  110. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/load.py +0 -0
  111. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -0
  112. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
  113. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
  114. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +0 -0
  115. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/theme.py +0 -0
  116. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/tool_display.py +0 -0
  117. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/unicode_security.py +0 -0
  118. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/update_check.py +0 -0
  119. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  120. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/_links.py +0 -0
  121. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  122. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  123. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  124. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  125. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  126. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  127. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
  128. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
  129. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
  130. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  131. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  132. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/messages.py +0 -0
  133. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  134. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  135. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/status.py +0 -0
  136. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  137. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  138. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  139. {soothe_cli-0.4.9 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.4.9
3
+ Version: 0.5.0
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/OpenSoothe/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -21,7 +21,7 @@ Requires-Python: <4.0,>=3.11
21
21
  Requires-Dist: python-dotenv<2.0.0,>=1.0.0
22
22
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
23
23
  Requires-Dist: rich>=13.0.0
24
- Requires-Dist: soothe-sdk<1.0.0,>=0.4.0
24
+ Requires-Dist: soothe-sdk<1.0.0,>=0.5.0
25
25
  Requires-Dist: textual>=8.0.0
26
26
  Requires-Dist: typer<1.0.0,>=0.9.0
27
27
  Requires-Dist: websockets>=12.0
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Topic :: Software Development :: Libraries :: Python Modules",
24
24
  ]
25
25
  dependencies = [
26
- "soothe-sdk>=0.4.0,<1.0.0", # WebSocket client, protocol, types
26
+ "soothe-sdk>=0.5.0,<1.0.0", # WebSocket client, protocol, types
27
27
  "typer>=0.9.0,<1.0.0", # CLI framework
28
28
  "textual>=8.0.0", # TUI framework
29
29
  "rich>=13.0.0", # Console output
@@ -31,11 +31,11 @@ def run(
31
31
 
32
32
  run_impl(
33
33
  prompt=prompt,
34
- config=config,
35
- thread_id=None,
34
+ resume_loop_id=None,
36
35
  no_tui=True,
37
36
  autonomous=True,
38
37
  max_iterations=max_iterations,
38
+ config_path=config,
39
39
  )
40
40
 
41
41
 
@@ -1,13 +1,9 @@
1
- """Loop management CLI commands for managing AgentLoop instances.
2
-
3
- Replaces thread-based commands with loop-based commands.
4
- Users interact with loops (threads are internal implementation detail).
1
+ """Loop management CLI commands for AgentLoop instances.
5
2
 
6
3
  RFC-503: Loop-First User Experience
7
4
  RFC-504: Loop Management CLI Commands
8
5
 
9
- All loop operations communicate exclusively via daemon WebSocket RPC.
10
- The daemon must be running for loop commands to work.
6
+ All loop operations use daemon WebSocket RPC; the daemon must be running.
11
7
  """
12
8
 
13
9
  from __future__ import annotations
@@ -145,8 +141,6 @@ def list_loops(
145
141
  ) -> None:
146
142
  """List all AgentLoop instances.
147
143
 
148
- Replaces: soothe thread list
149
-
150
144
  Examples:
151
145
  soothe loop list
152
146
  soothe loop list --status running
@@ -178,7 +172,7 @@ def list_loops(
178
172
  table = Table(title="AgentLoops")
179
173
  table.add_column("Loop ID", style="cyan")
180
174
  table.add_column("Status", style="green")
181
- table.add_column("Threads", justify="right")
175
+ table.add_column("Contexts", justify="right")
182
176
  table.add_column("Goals", justify="right")
183
177
  table.add_column("Switches", justify="right")
184
178
  table.add_column("Created", style="dim")
@@ -206,8 +200,6 @@ def describe_loop(
206
200
  ) -> None:
207
201
  """Show detailed loop information.
208
202
 
209
- Replaces: soothe thread describe
210
-
211
203
  Example:
212
204
  soothe loop show loop_abc123
213
205
  soothe loop show loop_abc123 --verbose
@@ -245,13 +237,12 @@ def describe_loop(
245
237
  )
246
238
  )
247
239
 
248
- # Thread context (internal, shown for debugging)
249
- # RFC-503: Hide thread IDs, show only thread count
240
+ # Internal checkpoint context counts from loop metadata
250
241
  console.print(
251
242
  Panel(
252
- f"Internal Threads: {len(loop.get('thread_ids', []))}\n"
253
- f"Thread Switches: {loop.get('total_thread_switches', 0)}",
254
- title="Thread Context (Internal)",
243
+ f"Internal contexts: {len(loop.get('thread_ids', []))}\n"
244
+ f"Context switches: {loop.get('total_thread_switches', 0)}",
245
+ title="Checkpoint contexts (internal)",
255
246
  border_style="dim",
256
247
  )
257
248
  )
@@ -260,7 +251,7 @@ def describe_loop(
260
251
  console.print(
261
252
  Panel(
262
253
  f"Goals Completed: {loop.get('total_goals_completed', 0)}\n"
263
- f"Thread Switches: {loop.get('total_thread_switches', 0)}\n"
254
+ f"Context switches: {loop.get('total_thread_switches', 0)}\n"
264
255
  f"Duration: {format_duration(loop.get('total_duration_ms', 0))}\n"
265
256
  f"Tokens Used: {format_tokens(loop.get('total_tokens_used', 0))}",
266
257
  title="Execution Summary",
@@ -419,9 +410,7 @@ def delete_loop(
419
410
  ) -> None:
420
411
  """Delete loop entirely.
421
412
 
422
- Removes loop directory but preserves thread checkpoints.
423
-
424
- Replaces: soothe thread delete
413
+ Removes this loop's run directory and related artifacts.
425
414
 
426
415
  Example:
427
416
  soothe loop delete loop_abc123
@@ -454,7 +443,7 @@ def delete_loop(
454
443
  console.print(
455
444
  f"[warning]Warning: This will permanently delete {loop_id} and all associated data:[/warning]"
456
445
  )
457
- console.print(f" - {len(loop.get('thread_ids', []))} internal thread contexts")
446
+ console.print(f" - {len(loop.get('thread_ids', []))} internal checkpoint contexts")
458
447
  console.print(f" - {loop.get('total_goals_completed', 0)} goal execution records")
459
448
  console.print(" - Working memory spills")
460
449
 
@@ -481,9 +470,7 @@ def delete_loop(
481
470
  console.print(" Removed checkpoint database")
482
471
  console.print(" Removed metadata")
483
472
  console.print(" Removed working memory spills")
484
- console.print(
485
- "[dim] Preserved thread checkpoints (run `soothe thread delete` to remove)[/dim]"
486
- )
473
+ console.print("[dim] LangGraph checkpoints may remain until pruned separately[/dim]")
487
474
 
488
475
 
489
476
  # Helper functions
@@ -569,7 +556,7 @@ def format_anchor_summary(anchors: list[dict[str, Any]]) -> str:
569
556
  line = f" iteration {anchor['iteration']}: [dim]{anchor['checkpoint_id']}[/dim] "
570
557
  line += f"({anchor['anchor_type']})"
571
558
 
572
- # Check for thread switch (RFC-503: show context refreshed without thread ID)
559
+ # Context refresh when loop scope (LangGraph thread_id) changes between anchors
573
560
  if anchor["iteration"] > 0:
574
561
  prev_anchors = [a for a in anchors if a["iteration"] == anchor["iteration"] - 1]
575
562
  if prev_anchors and prev_anchors[0]["thread_id"] != anchor["thread_id"]:
@@ -587,10 +574,10 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
587
574
  for iteration in main_line:
588
575
  iter_num = iteration["iteration"]
589
576
 
590
- # RFC-503: Hide thread ID, show context refresh notification
577
+ # Iteration marker (IDs omitted in UI)
591
578
  console.print(f" iteration {iter_num}")
592
579
 
593
- # Show context refresh note if thread switch occurred
580
+ # Context refresh when the tree marks a switch
594
581
  if iteration.get("thread_switch"):
595
582
  console.print(" [cyan][context refreshed][/cyan]")
596
583
 
@@ -610,7 +597,7 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
610
597
  console.print("\n[bold red]Failed Branches:[/bold red]")
611
598
 
612
599
  for branch in branches:
613
- # RFC-503: Hide thread ID in failed branches
600
+ # Branch identity only (no per-anchor checkpoint id in UI)
614
601
  console.print(f" [dim]{branch['branch_id']}[/dim] (iteration {branch['iteration']})")
615
602
  console.print(f" ├─ [dim]{branch['root_checkpoint']}[/dim] [root] ← Rewind point")
616
603
 
@@ -689,8 +676,6 @@ def continue_loop(
689
676
  ) -> None:
690
677
  """Continue execution on existing loop.
691
678
 
692
- Replaces: soothe thread continue <thread_id>
693
-
694
679
  Behavior:
695
680
  - Resolve target loop (explicit `LOOP_ID` or most-recent loop)
696
681
  - Launch TUI on that loop
@@ -709,7 +694,7 @@ def continue_loop(
709
694
 
710
695
  run_impl(
711
696
  prompt=prompt,
712
- thread_id=resolved_loop_id,
697
+ resume_loop_id=resolved_loop_id,
713
698
  no_tui=False,
714
699
  autonomous=False,
715
700
  max_iterations=None,
@@ -724,7 +709,7 @@ def detach_loop(
724
709
 
725
710
  Behavior:
726
711
  - Unsubscribe client from loop events
727
- - Loop continues executing (all threads continue)
712
+ - Loop keeps running on the daemon
728
713
  - Loop checkpoint saved at detachment point
729
714
  - Client can reattach later with 'soothe loop attach'
730
715
 
@@ -807,7 +792,7 @@ def attach_loop(
807
792
  Panel(
808
793
  f"Status: {loop.get('status', 'unknown')}\n"
809
794
  f"Goals: {loop.get('total_goals_completed', 0)} completed\n"
810
- f"Internal Threads: {len(loop.get('thread_ids', []))}",
795
+ f"Internal contexts: {len(loop.get('thread_ids', []))}",
811
796
  title=f"Loop: {loop_id} (Reattached)",
812
797
  )
813
798
  )
@@ -17,18 +17,20 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  def run_impl(
19
19
  prompt: str | None,
20
- thread_id: str | None,
20
+ resume_loop_id: str | None,
21
21
  no_tui: bool, # noqa: FBT001
22
22
  autonomous: bool, # noqa: FBT001
23
23
  max_iterations: int | None,
24
24
  streaming_enabled: bool | None = None,
25
25
  streaming_mode: str | None = None,
26
+ *,
27
+ config_path: str | None = None,
26
28
  ) -> None:
27
29
  """Core implementation for running Soothe agent.
28
30
 
29
31
  Args:
30
32
  prompt: Optional prompt for headless mode
31
- thread_id: Thread ID to resume
33
+ resume_loop_id: Existing loop id to attach to (optional)
32
34
  no_tui: Force headless mode
33
35
  autonomous: Enable autonomous iteration mode
34
36
  max_iterations: Max iterations for autonomous mode
@@ -38,7 +40,7 @@ def run_impl(
38
40
  startup_start = time.perf_counter()
39
41
 
40
42
  try:
41
- cfg = load_config()
43
+ cfg = load_config(config_path)
42
44
  log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
43
45
  log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
44
46
  setup_logging(log_level, log_file=log_file)
@@ -65,13 +67,13 @@ def run_impl(
65
67
  run_headless(
66
68
  cfg,
67
69
  prompt or "",
68
- thread_id=thread_id,
70
+ resume_loop_id=resume_loop_id,
69
71
  autonomous=autonomous,
70
72
  max_iterations=max_iterations,
71
73
  )
72
74
  else:
73
75
  # TUI mode (with optional initial prompt)
74
- run_tui(cfg, thread_id=thread_id, initial_prompt=prompt)
76
+ run_tui(cfg, resume_loop_id=resume_loop_id, initial_prompt=prompt)
75
77
 
76
78
  run_elapsed_s = time.perf_counter() - run_start
77
79
  typer.echo(f"Total running time: {run_elapsed_s:.2f}s", err=True)
@@ -13,7 +13,7 @@ from typing import Any
13
13
 
14
14
  import typer
15
15
  from soothe_sdk.client import (
16
- bootstrap_thread_session,
16
+ bootstrap_loop_session,
17
17
  connect_websocket_with_retries,
18
18
  websocket_url_from_config,
19
19
  )
@@ -30,11 +30,19 @@ _SESSION_BOOTSTRAP_TIMEOUT_S = 5.0
30
30
  _QUERY_START_TIMEOUT_S = 20.0
31
31
 
32
32
 
33
+ def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
34
+ """Return whether a daemon frame belongs to the active AgentLoop session."""
35
+ event_type = event.get("type", "")
36
+ if event_type not in {"status", "event"}:
37
+ return True
38
+ return event.get("loop_id") == active_loop_id
39
+
40
+
33
41
  async def run_headless_via_daemon(
34
42
  cfg: Any,
35
43
  prompt: str,
36
44
  *,
37
- thread_id: str | None = None,
45
+ resume_loop_id: str | None = None,
38
46
  autonomous: bool = False,
39
47
  max_iterations: int | None = None,
40
48
  ) -> int:
@@ -51,27 +59,27 @@ async def run_headless_via_daemon(
51
59
  try:
52
60
  await connect_websocket_with_retries(client)
53
61
  cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
54
- status_event = await bootstrap_thread_session(
62
+ status_event = await bootstrap_loop_session(
55
63
  client,
56
- resume_thread_id=thread_id,
64
+ resume_loop_id=resume_loop_id,
57
65
  verbosity="normal",
58
66
  workspace=cli_ws,
59
- thread_status_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
60
- subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
67
+ subscribe_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
61
68
  )
62
69
  if status_event.get("type") == "error":
63
70
  typer.echo(f"Daemon error: {status_event.get('message', 'unknown')}", err=True)
64
71
  return 1
65
72
 
66
- actual_thread_id = status_event.get("thread_id")
67
- if not actual_thread_id:
68
- typer.echo("Error: No thread_id in status message", err=True)
73
+ active_loop_id = status_event.get("loop_id")
74
+ if not active_loop_id:
75
+ typer.echo("Error: No loop_id after session bootstrap", err=True)
69
76
  return 1
70
77
 
71
78
  subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
72
79
 
73
80
  await asyncio.wait_for(
74
81
  client.send_input(
82
+ active_loop_id,
75
83
  cleaned_prompt if subagent_name else prompt,
76
84
  autonomous=autonomous,
77
85
  max_iterations=max_iterations,
@@ -104,6 +112,8 @@ async def run_headless_via_daemon(
104
112
  break
105
113
 
106
114
  event_type = event.get("type", "")
115
+ if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
116
+ continue
107
117
 
108
118
  if event_type == "error":
109
119
  typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
@@ -132,6 +142,8 @@ async def run_headless_via_daemon(
132
142
  break
133
143
  if not nxt:
134
144
  break
145
+ if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
146
+ continue
135
147
  processor.process_event(nxt)
136
148
 
137
149
  processor.process_event(event)
@@ -22,7 +22,7 @@ def run_headless(
22
22
  cfg: CLIConfig,
23
23
  prompt: str,
24
24
  *,
25
- thread_id: str | None = None,
25
+ resume_loop_id: str | None = None,
26
26
  autonomous: bool = False,
27
27
  max_iterations: int | None = None,
28
28
  ) -> None:
@@ -77,7 +77,7 @@ def run_headless(
77
77
  return await run_headless_via_daemon(
78
78
  cfg,
79
79
  prompt,
80
- thread_id=thread_id,
80
+ resume_loop_id=resume_loop_id,
81
81
  autonomous=autonomous,
82
82
  max_iterations=max_iterations,
83
83
  )
@@ -10,7 +10,7 @@ from soothe_cli.config import CLIConfig
10
10
  def run_tui(
11
11
  cfg: CLIConfig,
12
12
  *,
13
- thread_id: str | None = None,
13
+ resume_loop_id: str | None = None,
14
14
  initial_prompt: str | None = None,
15
15
  ) -> None:
16
16
  """Launch the Textual TUI (with daemon auto-start)."""
@@ -19,7 +19,7 @@ def run_tui(
19
19
 
20
20
  run_textual_tui(
21
21
  config=cfg,
22
- thread_id=thread_id,
22
+ resume_loop_id=resume_loop_id,
23
23
  initial_prompt=initial_prompt,
24
24
  )
25
25
  except ImportError:
@@ -106,7 +106,7 @@ def main(
106
106
 
107
107
  run_impl(
108
108
  prompt=prompt,
109
- thread_id=None,
109
+ resume_loop_id=None,
110
110
  no_tui=no_tui,
111
111
  autonomous=False,
112
112
  max_iterations=None,
@@ -118,15 +118,11 @@ def main(
118
118
  # ---------------------------------------------------------------------------
119
119
  # Sub-command groups (nested Typer apps)
120
120
  # ---------------------------------------------------------------------------
121
- # Thread: read-only diagnostics per RFC-503 (Loop-First UX). Lifecycle
122
- # management lives under `soothe loop <subcommand>`.
123
121
 
124
122
  from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
125
123
  from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
126
- from soothe_cli.cli.commands.thread_cmd import thread_app as _thread_app # noqa: E402
127
124
 
128
125
  for _sub_app, _name in (
129
- (_thread_app, "thread"),
130
126
  (_loop_app, "loop"),
131
127
  (_autopilot_app, "autopilot"),
132
128
  ):
@@ -21,8 +21,6 @@ from soothe_sdk.utils import setup_logging
21
21
  # Import from commands subdirectory
22
22
  from soothe_cli.shared.commands.slash_commands import (
23
23
  KEYBOARD_SHORTCUTS,
24
- SLASH_COMMANDS,
25
- parse_autonomous_command,
26
24
  show_commands,
27
25
  show_config,
28
26
  show_history,
@@ -108,8 +106,6 @@ __all__ = [
108
106
  "update_name_map_from_tool_calls",
109
107
  # Slash commands (IG-176)
110
108
  "KEYBOARD_SHORTCUTS",
111
- "SLASH_COMMANDS",
112
- "parse_autonomous_command",
113
109
  "show_commands",
114
110
  "show_config",
115
111
  "show_history",
@@ -42,7 +42,7 @@ def parse_slash_command(input_text: str) -> tuple[str, str | None]:
42
42
 
43
43
 
44
44
  def validate_command(
45
- entry: dict[str, Any], command: str, query: str | None, thread_id: str | None
45
+ entry: dict[str, Any], command: str, query: str | None, loop_id: str | None
46
46
  ) -> tuple[bool, str | None]:
47
47
  """Validate command before routing.
48
48
 
@@ -50,14 +50,13 @@ def validate_command(
50
50
  entry: Command registry entry
51
51
  command: Command name
52
52
  query: Query parameter (if present)
53
- thread_id: Current thread ID
53
+ loop_id: Active AgentLoop id for this session
54
54
 
55
55
  Returns:
56
56
  Tuple of (is_valid, error_message)
57
57
  """
58
- # Check thread requirement
59
- if entry.get("requires_thread") and not thread_id:
60
- return (False, "No active thread")
58
+ if entry.get("requires_loop") and not loop_id:
59
+ return (False, "No active loop")
61
60
 
62
61
  # Check query requirement for routing commands
63
62
  if entry.get("requires_query") and not query:
@@ -110,7 +109,13 @@ def parse_command_params(entry: dict[str, Any], query: str) -> dict[str, Any]:
110
109
  return params
111
110
 
112
111
 
113
- async def route_slash_command(cmd_input: str, console: Console, client: WebSocketClient) -> bool:
112
+ async def route_slash_command(
113
+ cmd_input: str,
114
+ console: Console,
115
+ client: WebSocketClient,
116
+ *,
117
+ loop_id: str | None = None,
118
+ ) -> bool:
114
119
  """Route slash command based on registry metadata (RFC-404).
115
120
 
116
121
  Args:
@@ -137,7 +142,7 @@ async def route_slash_command(cmd_input: str, console: Console, client: WebSocke
137
142
  return True # Handled (as error)
138
143
 
139
144
  # Validate command
140
- is_valid, error = validate_command(entry, command, query, client.thread_id)
145
+ is_valid, error = validate_command(entry, command, query, loop_id)
141
146
  if not is_valid:
142
147
  console.print(f"[red]Error: {error}[/red]")
143
148
  return True # Handled (as error)
@@ -151,13 +156,13 @@ async def route_slash_command(cmd_input: str, console: Console, client: WebSocke
151
156
  return True
152
157
 
153
158
  elif entry["location"] == "daemon" and entry.get("type") == "rpc":
154
- # Daemon RPC: send command_request
155
- await handle_rpc_command(entry, command, query, console, client)
159
+ # Daemon RPC: send command_request (scoped by loop_id)
160
+ await handle_rpc_command(entry, command, query, console, client, loop_id=loop_id)
156
161
  return True
157
162
 
158
163
  elif entry["location"] == "daemon" and entry.get("type") == "routing":
159
164
  # Daemon routing: send as plain text input
160
- await handle_routing_command(cmd_input, console, client)
165
+ await handle_routing_command(cmd_input, console, client, loop_id=loop_id)
161
166
  return True
162
167
 
163
168
  return False
@@ -169,6 +174,8 @@ async def handle_rpc_command(
169
174
  query: str | None,
170
175
  console: Console,
171
176
  client: WebSocketClient,
177
+ *,
178
+ loop_id: str | None = None,
172
179
  ) -> None:
173
180
  """Handle daemon RPC command with structured request/response (RFC-404).
174
181
 
@@ -178,15 +185,17 @@ async def handle_rpc_command(
178
185
  query: Query/params (if present)
179
186
  console: Rich console
180
187
  client: WebSocket client
188
+ loop_id: Active subscribed loop (required for daemon-side binding)
181
189
  """
182
190
  daemon_command = entry["daemon_command"]
183
191
 
184
192
  # Build request
185
- request = {
193
+ request: dict[str, Any] = {
186
194
  "type": "command_request",
187
195
  "command": daemon_command,
188
- "thread_id": client.thread_id,
189
196
  }
197
+ if loop_id:
198
+ request["loop_id"] = loop_id
190
199
 
191
200
  # Parse params if schema exists
192
201
  if entry.get("params_schema") and query:
@@ -225,7 +234,13 @@ async def handle_rpc_command(
225
234
  console.print(f"[red]Error: {exc}[/red]")
226
235
 
227
236
 
228
- async def handle_routing_command(cmd_input: str, console: Console, client: WebSocketClient) -> None:
237
+ async def handle_routing_command(
238
+ cmd_input: str,
239
+ console: Console,
240
+ client: WebSocketClient,
241
+ *,
242
+ loop_id: str | None = None,
243
+ ) -> None:
229
244
  """Handle daemon routing command by sending input with optional subagent (RFC-404).
230
245
 
231
246
  For ``/browser``, ``/claude``, ``/research``, and ``/explore``, sets the WebSocket
@@ -236,9 +251,13 @@ async def handle_routing_command(cmd_input: str, console: Console, client: WebSo
236
251
  cmd_input: Full command input (e.g., "/browser AI trends")
237
252
  console: Rich console
238
253
  client: WebSocket client
254
+ loop_id: Subscribed loop to target (required for ``loop_input``)
239
255
  """
256
+ if not loop_id:
257
+ console.print("[red]Error: No active loop for routing command[/red]")
258
+ return
240
259
  subagent_name, text = parse_subagent_from_input(cmd_input.strip())
241
- await client.send_input(text, preferred_subagent=subagent_name)
260
+ await client.send_input(loop_id, text, preferred_subagent=subagent_name)
242
261
 
243
262
 
244
263
  __all__ = [