soothe-cli 0.6.7__tar.gz → 0.6.9__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 (115) hide show
  1. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/PKG-INFO +1 -1
  2. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +74 -13
  3. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/loop_cmd.py +4 -4
  4. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/daemon.py +1 -1
  5. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/headless.py +8 -9
  6. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/main.py +1 -1
  7. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/headless/processor.py +1 -1
  8. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/headless/processor_state.py +1 -1
  9. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/essential_events.py +4 -4
  10. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/tui_trace_log.py +1 -1
  11. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/step_router.py +1 -1
  12. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/transport/session.py +14 -6
  13. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/turn/prepare.py +12 -12
  14. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_app.py +3 -2
  15. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_execution.py +3 -0
  16. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_model.py +57 -1
  17. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_module_init.py +1 -1
  18. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_startup.py +11 -1
  19. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/command_registry.py +1 -1
  20. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/command_router.py +2 -2
  21. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/slash_commands.py +7 -0
  22. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/config.py +1 -1
  23. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/input.py +37 -26
  24. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/preview_limits.py +2 -2
  25. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/sessions.py +2 -2
  26. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/textual_adapter.py +42 -19
  27. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/tips.py +1 -1
  28. soothe_cli-0.6.9/src/soothe_cli/tui/widgets/autopilot_dashboard.py +614 -0
  29. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/autopilot_screen.py +4 -3
  30. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/chat_input.py +37 -5
  31. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/messages.py +75 -7
  32. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/welcome.py +1 -1
  33. soothe_cli-0.6.7/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -318
  34. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/.gitignore +0 -0
  35. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/README.md +0 -0
  36. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/pyproject.toml +0 -0
  37. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/__init__.py +0 -0
  38. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/__init__.py +0 -0
  39. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
  40. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  41. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  42. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
  43. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  44. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  45. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
  46. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/__init__.py +0 -0
  47. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/cli_config.py +0 -0
  48. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/loader.py +0 -0
  49. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/logging_setup.py +0 -0
  50. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/__init__.py +0 -0
  51. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/_utils.py +0 -0
  52. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  53. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  54. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
  55. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  56. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  57. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
  58. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  59. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  60. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  61. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  62. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  63. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  64. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  65. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  66. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/transcript.py +0 -0
  67. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/task_scope.py +0 -0
  68. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  69. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  70. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  71. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  72. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/messages.py +0 -0
  73. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/__init__.py +0 -0
  74. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/_cli_context.py +0 -0
  75. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/_env_vars.py +0 -0
  76. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/_version.py +0 -0
  77. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/__init__.py +0 -0
  78. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_commands.py +0 -0
  79. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_history.py +0 -0
  80. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  81. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_ui.py +0 -0
  82. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/app.tcss +0 -0
  83. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/binding.py +0 -0
  84. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/__init__.py +0 -0
  85. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  86. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/file_change_notify.py +0 -0
  87. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  88. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/hooks.py +0 -0
  89. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/media_utils.py +0 -0
  90. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/model_config.py +0 -0
  91. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/path_utils.py +0 -0
  92. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/project_utils.py +0 -0
  93. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
  94. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
  95. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/load.py +0 -0
  96. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/theme.py +0 -0
  97. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/tool_display.py +0 -0
  98. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/unicode_security.py +0 -0
  99. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/update_check.py +0 -0
  100. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  101. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/_links.py +0 -0
  102. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  103. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  104. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
  105. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
  106. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  107. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/history.py +0 -0
  108. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/loading.py +0 -0
  109. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  110. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  111. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  112. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  113. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  114. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/status.py +0 -0
  115. {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.6.7
3
+ Version: 0.6.9
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -10,6 +10,7 @@ import asyncio
10
10
  import os
11
11
  import sys
12
12
  import time
13
+ from collections import Counter
13
14
  from pathlib import Path
14
15
 
15
16
  import typer
@@ -124,12 +125,40 @@ def submit(
124
125
 
125
126
  @app.command("status")
126
127
  def status() -> None:
127
- """Show overall autopilot state from the daemon."""
128
+ """Show overall autopilot state and goal DAG summary."""
128
129
  client = _require_daemon_http()
129
130
  data = client.status()
130
- typer.echo(f"Autopilot state: {data.get('state', data.get('status', 'unknown'))}")
131
- if "active_goals" in data:
132
- typer.echo(f"Active goals: {len(data['active_goals'])}")
131
+ state = data.get("state", data.get("status", "unknown"))
132
+ running = data.get("running", False)
133
+ dreaming = data.get("dreaming", False)
134
+ typer.echo(f"Autopilot state: {state}")
135
+ typer.echo(f"Scheduling loop: {'running' if running else 'stopped'}")
136
+ if dreaming:
137
+ typer.echo("Dreaming: yes")
138
+ loop_pool = data.get("loop_pool")
139
+ if isinstance(loop_pool, dict) and loop_pool:
140
+ typer.echo(f"Worker pool: {loop_pool}")
141
+
142
+ jobs = client.list_jobs().get("jobs") or []
143
+ goals = client.list_goals().get("goals") or []
144
+ typer.echo(f"\nJobs (root goals): {len(jobs)}")
145
+ if goals:
146
+ counts = Counter(str(g.get("status", "pending")) for g in goals)
147
+ typer.echo(f"Goals in DAG: {len(goals)}")
148
+ for stat, count in sorted(counts.items()):
149
+ typer.echo(f" {stat}: {count}")
150
+ elif not jobs:
151
+ typer.echo("Goals in DAG: 0")
152
+
153
+ if jobs:
154
+ typer.echo("\nJobs:")
155
+ for j in jobs:
156
+ jid = str(j.get("id", "?"))
157
+ sid = jid[:8]
158
+ sstat = j.get("status", "pending")
159
+ sdesc = preview_first(j.get("description", ""), 50)
160
+ typer.echo(f" [{sid}] {sstat:10s} {sdesc}")
161
+ typer.echo("\nFull DAG for a job: soothe autopilot job <job_id>")
133
162
 
134
163
 
135
164
  @app.command("list")
@@ -139,8 +168,20 @@ def list_jobs(
139
168
  """List jobs (root goals) from the daemon autopilot.
140
169
 
141
170
  Jobs are user-submitted tasks. Subgoals created during autonomous
142
- execution are not shown here; use 'goal <id>' to inspect subgoals.
171
+ execution are not shown here; use ``goals`` or ``goal <id>`` for details.
143
172
  """
173
+ _list_jobs_impl(status_filter)
174
+
175
+
176
+ @app.command("jobs")
177
+ def list_jobs_alias(
178
+ status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
179
+ ) -> None:
180
+ """Alias for ``list`` — list root autopilot jobs."""
181
+ _list_jobs_impl(status_filter)
182
+
183
+
184
+ def _list_jobs_impl(status_filter: str) -> None:
144
185
  client = _require_daemon_http()
145
186
  payload = client.list_jobs()
146
187
  jobs = payload.get("jobs") or []
@@ -158,6 +199,29 @@ def list_jobs(
158
199
  typer.echo(f" [{sid}] {sstat:10s} pri={spri:3d} {sdesc}")
159
200
 
160
201
 
202
+ @app.command("goals")
203
+ def list_goals(
204
+ status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
205
+ ) -> None:
206
+ """List all goals in the daemon autopilot DAG (including subgoals)."""
207
+ client = _require_daemon_http()
208
+ payload = client.list_goals()
209
+ goals = payload.get("goals") or []
210
+ if not goals:
211
+ typer.echo("No goals found.")
212
+ return
213
+
214
+ for g in goals:
215
+ if status_filter and g.get("status", "") != status_filter:
216
+ continue
217
+ gid = str(g.get("id", "?"))[:8]
218
+ parent = g.get("parent_id")
219
+ parent_s = f" parent={str(parent)[:8]}" if parent else ""
220
+ desc = preview_first(g.get("description", ""), 50)
221
+ stat = g.get("status", "pending")
222
+ typer.echo(f" [{gid}] {stat:10s}{parent_s} {desc}")
223
+
224
+
161
225
  def _render_dag_tree(dag: dict, root_id: str) -> None:
162
226
  """Render DAG as ASCII tree for job visualization."""
163
227
  nodes = {n["id"]: n for n in dag.get("nodes", [])}
@@ -199,9 +263,9 @@ def _render_dag_tree(dag: dict, root_id: str) -> None:
199
263
 
200
264
  @app.command("job")
201
265
  def show_job(
202
- job_id: str = typer.Argument(..., help="Job ID to show details and DAG."),
266
+ job_id: str = typer.Argument(..., help="Job ID to show details and goal DAG."),
203
267
  ) -> None:
204
- """Show job status and DAG tree visualization.
268
+ """Show job status and goal DAG tree visualization.
205
269
 
206
270
  A job is a root goal submitted by the user. This command shows
207
271
  the job's details and the complete goal DAG under it.
@@ -211,7 +275,7 @@ def show_job(
211
275
  payload = client.get_job(job_id)
212
276
  except RuntimeError as exc:
213
277
  typer.echo(str(exc), err=True)
214
- raise typer.Exit(1)
278
+ raise typer.Exit(1) from exc
215
279
 
216
280
  job = payload.get("job")
217
281
  dag = payload.get("dag")
@@ -228,11 +292,9 @@ def show_job(
228
292
  typer.echo(f"Workspace: {job['workspace']}")
229
293
  created = job.get("created_at", "")
230
294
  if created:
231
- # Truncate timestamp for readability
232
295
  created_short = created[:19] if len(created) > 19 else created
233
296
  typer.echo(f"Created: {created_short}")
234
297
 
235
- # DAG stats from response
236
298
  active = payload.get("active_goals", 0)
237
299
  completed = payload.get("completed_goals", 0)
238
300
  total = payload.get("total_goals", 0)
@@ -240,10 +302,9 @@ def show_job(
240
302
  typer.echo(f"Completed goals: {completed}")
241
303
  typer.echo(f"Total goals: {total}")
242
304
 
243
- # DAG tree
244
- typer.echo("\nDAG:")
305
+ typer.echo("\nGoal DAG:")
245
306
  if dag:
246
- _render_dag_tree(dag, job_id)
307
+ _render_dag_tree(dag, str(job.get("id") or job_id))
247
308
  else:
248
309
  typer.echo(" (no subgoals)")
249
310
 
@@ -1,4 +1,4 @@
1
- """Loop management CLI commands for AgentLoop instances.
1
+ """Loop management CLI commands for StrangeLoop instances.
2
2
 
3
3
  RFC-503: Loop-First User Experience
4
4
  RFC-504: Loop Management CLI Commands
@@ -24,7 +24,7 @@ from soothe_cli.runtime import load_config
24
24
  console = Console()
25
25
 
26
26
  # Create loop command group
27
- loop_app = typer.Typer(help="Manage AgentLoop instances")
27
+ loop_app = typer.Typer(help="Manage StrangeLoop instances")
28
28
 
29
29
 
30
30
  def _require_daemon(ws_url: str) -> None:
@@ -139,7 +139,7 @@ def list_loops(
139
139
  typer.Option("--limit", "-l", help="Limit number of results."),
140
140
  ] = 20,
141
141
  ) -> None:
142
- """List all AgentLoop instances.
142
+ """List all StrangeLoop instances.
143
143
 
144
144
  Examples:
145
145
  soothe loop list
@@ -169,7 +169,7 @@ def list_loops(
169
169
  return
170
170
 
171
171
  # Render table
172
- table = Table(title="AgentLoops")
172
+ table = Table(title="StrangeLoops")
173
173
  table.add_column("Loop ID", style="cyan")
174
174
  table.add_column("Status", style="green")
175
175
  table.add_column("Contexts", justify="right")
@@ -39,7 +39,7 @@ _CANCEL_SEND_TIMEOUT_S = 3.0
39
39
 
40
40
 
41
41
  def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
42
- """Return whether a daemon frame belongs to the active AgentLoop session."""
42
+ """Return whether a daemon frame belongs to the active StrangeLoop session."""
43
43
  event_type = event.get("type", "")
44
44
  if event_type not in {"status", "event"}:
45
45
  return True
@@ -2,7 +2,6 @@
2
2
 
3
3
  import asyncio
4
4
  import sys
5
- import time
6
5
 
7
6
  import typer
8
7
  from soothe_sdk.client import (
@@ -42,7 +41,10 @@ def run_headless(
42
41
  # Auto-start daemon if not running (RFC-0013) - WebSocket RPC checks (IG-174 Phase 1)
43
42
  async def _run_headless_pipeline() -> int:
44
43
  """Ensure daemon is reachable, then run the headless daemon session."""
45
- daemon_live = await is_daemon_live(ws_url, timeout=5.0)
44
+ # Check if daemon is live and ready (IG-489: wait for readiness, not just port-live)
45
+ daemon_live = await is_daemon_live(
46
+ ws_url, timeout=5.0, wait_for_ready=True, ready_timeout=30.0
47
+ )
46
48
 
47
49
  if not daemon_live:
48
50
  # Attempt cleanup if stale daemon (connection exists but daemon not responsive)
@@ -64,13 +66,10 @@ def run_headless(
64
66
  stderr=subprocess.DEVNULL,
65
67
  )
66
68
 
67
- # Wait for daemon to become fully ready with timeout
68
- start_time = time.time()
69
- while time.time() - start_time < _DAEMON_START_WAIT_TIMEOUT:
70
- daemon_live = await is_daemon_live(ws_url, timeout=2.0)
71
- if daemon_live:
72
- break
73
- await asyncio.sleep(0.5)
69
+ # Wait for daemon to become fully ready with timeout (IG-489)
70
+ daemon_live = await is_daemon_live(
71
+ ws_url, timeout=2.0, wait_for_ready=True, ready_timeout=_DAEMON_START_WAIT_TIMEOUT
72
+ )
74
73
  # Note: We don't fail here - let the connection attempt handle errors
75
74
  # This allows tests and edge cases to proceed with mocked daemons
76
75
 
@@ -152,7 +152,7 @@ def main(
152
152
  soothe -p "Research AI advances" # One-shot headless (non-TUI) query
153
153
  soothe -p "Hello" --tui # TUI with an auto-submitted prompt
154
154
  soothe --daemon-port 9000 loop list # Subcommands inherit global flags
155
- soothe loop list # List AgentLoop instances
155
+ soothe loop list # List StrangeLoop instances
156
156
  """
157
157
  # Handle -h/--help flag
158
158
  if show_help:
@@ -110,7 +110,7 @@ class EventProcessor:
110
110
 
111
111
  @property
112
112
  def loop_id(self) -> str:
113
- """Active AgentLoop id from daemon status frames."""
113
+ """Active StrangeLoop id from daemon status frames."""
114
114
  return self._state.loop_id
115
115
 
116
116
  @property
@@ -38,7 +38,7 @@ class ProcessorState:
38
38
  # Current plan state (updated on plan events)
39
39
  current_plan: Plan | None = None
40
40
 
41
- # Active AgentLoop id from daemon status frames (``loop_id``)
41
+ # Active StrangeLoop id from daemon status frames (``loop_id``)
42
42
  loop_id: str = ""
43
43
 
44
44
  # Internal context tracking (suppress internal LLM responses)
@@ -6,24 +6,24 @@ from typing import Final
6
6
 
7
7
  GOAL_START_EVENT_TYPES: Final[frozenset[str]] = frozenset(
8
8
  {
9
- "soothe.cognition.agent_loop.started",
9
+ "soothe.cognition.strange_loop.started",
10
10
  "soothe.cognition.plan.creating",
11
11
  }
12
12
  )
13
13
 
14
14
  STEP_START_EVENT_TYPES: Final[frozenset[str]] = frozenset(
15
15
  {
16
- "soothe.cognition.agent_loop.step.started",
16
+ "soothe.cognition.strange_loop.step.started",
17
17
  }
18
18
  )
19
19
 
20
20
  STEP_COMPLETE_EVENT_TYPES: Final[frozenset[str]] = frozenset(
21
21
  {
22
- "soothe.cognition.agent_loop.step.completed",
22
+ "soothe.cognition.strange_loop.step.completed",
23
23
  }
24
24
  )
25
25
 
26
- LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.agent_loop.reasoned"
26
+ LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.strange_loop.reasoned"
27
27
 
28
28
  ESSENTIAL_PROGRESS_EVENT_TYPES: Final[frozenset[str]] = frozenset(
29
29
  set(GOAL_START_EVENT_TYPES)
@@ -30,7 +30,7 @@ def log_tui_trace(*, tui_debug: bool, event: str, **fields: Any) -> None:
30
30
  tui_debug: Whether tracing is enabled (from config).
31
31
  event: Short event name (e.g. ``renderer.assistant_text``).
32
32
  fields: Key/value pairs appended as ``key='value'`` (strings truncated).
33
- Common fields include ``loop_id`` for the active AgentLoop id.
33
+ Common fields include ``loop_id`` for the active StrangeLoop id.
34
34
  """
35
35
  if not tui_debug:
36
36
  return
@@ -1,4 +1,4 @@
1
- """Per-turn routing for AgentLoop steps, root tools, and subagent task namespaces.
1
+ """Per-turn routing for StrangeLoop steps, root tools, and subagent task namespaces.
2
2
 
3
3
  Owns associations between execute ``step_id``, root ``tool_call_id``, LangGraph
4
4
  subgraph ``namespace``, and ``task`` delegations. Designed for parallel execute waves
@@ -45,6 +45,7 @@ class TuiDaemonSession:
45
45
  self._client = WebSocketClient(url=ws_url)
46
46
  self._rpc_client = WebSocketClient(url=ws_url)
47
47
  self._loop_id: str | None = None
48
+ self._autopilot_mode: str | None = None
48
49
  self._read_lock = asyncio.Lock()
49
50
  self._rpc_lock = asyncio.Lock()
50
51
  self._rpc_connected = False
@@ -54,9 +55,14 @@ class TuiDaemonSession:
54
55
 
55
56
  @property
56
57
  def loop_id(self) -> str | None:
57
- """Active AgentLoop id for this WebSocket session."""
58
+ """Active StrangeLoop id for this WebSocket session."""
58
59
  return self._loop_id
59
60
 
61
+ @property
62
+ def autopilot_mode(self) -> str | None:
63
+ """Active loop Solo/Autopilot mode when known."""
64
+ return self._autopilot_mode
65
+
60
66
  async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
61
67
  """Connect and bootstrap a daemon loop session."""
62
68
  await connect_websocket_with_retries(self._client)
@@ -77,6 +83,8 @@ class TuiDaemonSession:
77
83
  if status_event.get("type") == "error":
78
84
  raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
79
85
  self._loop_id = status_event.get("loop_id")
86
+ mode = status_event.get("autopilot_mode")
87
+ self._autopilot_mode = str(mode) if mode in ("solo", "autopilot") else None
80
88
  return status_event
81
89
 
82
90
  def _resolve_stream_delivery_mode(self) -> str:
@@ -100,7 +108,7 @@ class TuiDaemonSession:
100
108
  return "adaptive"
101
109
 
102
110
  async def new_loop(self) -> dict[str, Any]:
103
- """Start a new AgentLoop conversation."""
111
+ """Start a new StrangeLoop conversation."""
104
112
  return await self._bootstrap_loop(resume_loop_id=None)
105
113
 
106
114
  async def switch_loop(self, loop_id: str) -> dict[str, Any]:
@@ -398,7 +406,7 @@ class TuiDaemonSession:
398
406
  binder that produced the original cards.
399
407
 
400
408
  Args:
401
- loop_id: AgentLoop id.
409
+ loop_id: StrangeLoop id.
402
410
 
403
411
  Returns:
404
412
  ``SimpleNamespace`` with ``cards: list[dict]``, ``seq: int``,
@@ -442,14 +450,14 @@ class TuiDaemonSession:
442
450
  )
443
451
 
444
452
  async def aget_loop_state(self, loop_id: str) -> Any:
445
- """Load agent-loop state channels from the daemon (``loop_state_get`` RPC).
453
+ """Load StrangeLoop state channels from the daemon (``loop_state_get`` RPC).
446
454
 
447
455
  Returns a namespace with a ``values`` mapping so history code can share the
448
456
  same consumption pattern as the in-process agent snapshot, without passing
449
457
  graph config objects over the wire.
450
458
 
451
459
  Args:
452
- loop_id: AgentLoop id.
460
+ loop_id: StrangeLoop id.
453
461
 
454
462
  Returns:
455
463
  ``types.SimpleNamespace`` with ``values: dict[str, Any]``.
@@ -489,7 +497,7 @@ class TuiDaemonSession:
489
497
  """Merge partial state into the loop on the daemon host (``loop_state_update`` RPC).
490
498
 
491
499
  Args:
492
- loop_id: AgentLoop id.
500
+ loop_id: StrangeLoop id.
493
501
  values: Channel updates (e.g. ``messages``) in JSON-serializable form.
494
502
  timeout: RPC wait budget in seconds.
495
503
  as_node: Optional LangGraph node to attribute the write to. When
@@ -11,12 +11,12 @@ from typing import Any
11
11
 
12
12
  from langchain_core.messages import ToolMessage
13
13
  from soothe_sdk.core.events import (
14
- AGENT_LOOP_COMPLETED,
15
- AGENT_LOOP_PLAN_DECISION,
16
- AGENT_LOOP_STARTED,
17
- AGENT_LOOP_STEP_COMPLETED,
18
- AGENT_LOOP_STEP_QUEUED,
19
- AGENT_LOOP_STEP_STARTED,
14
+ STRANGE_LOOP_COMPLETED,
15
+ STRANGE_LOOP_PLAN_DECISION,
16
+ STRANGE_LOOP_STARTED,
17
+ STRANGE_LOOP_STEP_COMPLETED,
18
+ STRANGE_LOOP_STEP_QUEUED,
19
+ STRANGE_LOOP_STEP_STARTED,
20
20
  )
21
21
  from soothe_sdk.ux.classification import classify_event_to_tier
22
22
  from soothe_sdk.ux.loop_stream import assistant_output_phase
@@ -38,12 +38,12 @@ _MAIN_LOOP_CUSTOM_TYPES = frozenset(
38
38
  {
39
39
  STREAM_TOOL_CALL_UPDATE,
40
40
  TOOL_CALL_UPDATES_BATCH,
41
- AGENT_LOOP_STARTED,
42
- AGENT_LOOP_COMPLETED,
43
- AGENT_LOOP_PLAN_DECISION,
44
- AGENT_LOOP_STEP_STARTED,
45
- AGENT_LOOP_STEP_QUEUED,
46
- AGENT_LOOP_STEP_COMPLETED,
41
+ STRANGE_LOOP_STARTED,
42
+ STRANGE_LOOP_COMPLETED,
43
+ STRANGE_LOOP_PLAN_DECISION,
44
+ STRANGE_LOOP_STEP_STARTED,
45
+ STRANGE_LOOP_STEP_QUEUED,
46
+ STRANGE_LOOP_STEP_COMPLETED,
47
47
  }
48
48
  )
49
49
 
@@ -156,7 +156,7 @@ class SootheApp(
156
156
  daemon_config: Loaded Soothe configuration (WebSocket URL, etc.).
157
157
  assistant_id: Agent identifier for memory storage.
158
158
  cwd: Current working directory to display.
159
- resume_loop_id: Initial AgentLoop id when attaching to an existing loop.
159
+ resume_loop_id: Initial StrangeLoop id when attaching to an existing loop.
160
160
  initial_prompt: Optional prompt to auto-submit when session starts.
161
161
  initial_skill: Optional skill name to invoke when session starts.
162
162
  mcp_server_info: MCP server metadata for the `/mcp` viewer.
@@ -174,9 +174,10 @@ class SootheApp(
174
174
 
175
175
  self._cwd = str(cwd) if cwd else str(Path.cwd())
176
176
 
177
- # Active AgentLoop id; LangGraph stores it as configurable.thread_id.
177
+ # Active StrangeLoop id; LangGraph stores it as configurable.thread_id.
178
178
  # Named `_lc_loop_id` to avoid colliding with Textual's App._thread_id.
179
179
  self._lc_loop_id = resume_loop_id
180
+ self._loop_autopilot_mode: str = "solo"
180
181
 
181
182
  self._initial_prompt = initial_prompt
182
183
 
@@ -570,6 +570,7 @@ class _ExecutionMixin:
570
570
  )
571
571
  self._session_state.loop_id = new_loop_id
572
572
  self._lc_loop_id = new_loop_id
573
+ self._apply_loop_autopilot_mode(status_event.get("autopilot_mode"))
573
574
  try:
574
575
  banner = self.query_one("#welcome-banner", WelcomeBanner)
575
576
  banner.update_loop_id(new_loop_id)
@@ -650,6 +651,8 @@ class _ExecutionMixin:
650
651
  await self._submit_autopilot_job(args)
651
652
  elif cmd == "/autopilot-dashboard":
652
653
  await self._show_autopilot_dashboard()
654
+ elif cmd == "/autopilot-toggle":
655
+ await self._toggle_autopilot_mode()
653
656
  elif cmd == "/mcp":
654
657
  await self._show_mcp_viewer()
655
658
  elif cmd == "/theme":
@@ -265,12 +265,26 @@ class _ModelMixin:
265
265
  return None
266
266
  return resp.get("servers")
267
267
 
268
+ def _apply_loop_autopilot_mode(self, mode: str | None) -> None:
269
+ """Sync local Solo/Autopilot mode from daemon bootstrap or toggle events."""
270
+ if mode not in ("solo", "autopilot"):
271
+ return
272
+ self._loop_autopilot_mode = mode
273
+ if self._status_bar is not None:
274
+ label = "Autopilot" if mode == "autopilot" else "Solo"
275
+ self._status_bar.set_session_tip(f"Mode: {label}")
276
+
268
277
  async def _show_autopilot_dashboard(self) -> None:
269
278
  """Show autopilot dashboard as a screen overlay."""
270
279
  from soothe_cli.tui.widgets.autopilot_screen import AutopilotScreen
271
280
 
272
281
  is_narrow = self.size.width < 100
273
- screen = AutopilotScreen(is_narrow=is_narrow)
282
+ mode = (
283
+ self._loop_autopilot_mode
284
+ if self._loop_autopilot_mode in ("solo", "autopilot")
285
+ else "solo"
286
+ )
287
+ screen = AutopilotScreen(is_narrow=is_narrow, mode=mode)
274
288
 
275
289
  def handle_result(result: None) -> None: # noqa: ARG001
276
290
  if self._chat_input:
@@ -278,6 +292,48 @@ class _ModelMixin:
278
292
 
279
293
  self.push_screen(screen, handle_result)
280
294
 
295
+ async def _toggle_autopilot_mode(self) -> None:
296
+ """Toggle autopilot mode (solo ↔ autopilot) via daemon RPC.
297
+
298
+ Sends command_request to daemon and updates local dashboard mode.
299
+ """
300
+ from soothe_cli.tui.widgets.messages import ErrorMessage, UserMessage
301
+
302
+ await self._mount_message(UserMessage("/autopilot-toggle"))
303
+
304
+ lid = self._loop_id
305
+ if not lid:
306
+ await self._mount_message(ErrorMessage("No active loop for autopilot toggle"))
307
+ return
308
+
309
+ if not self._client:
310
+ await self._mount_message(ErrorMessage("Not connected to daemon"))
311
+ return
312
+
313
+ try:
314
+ from soothe_cli.tui.commands.command_router import handle_rpc_command
315
+ from soothe_cli.tui.commands.slash_commands import COMMANDS
316
+
317
+ entry = COMMANDS.get("/autopilot-toggle")
318
+ if not entry:
319
+ await self._mount_message(ErrorMessage("Command /autopilot-toggle not found"))
320
+ return
321
+
322
+ await handle_rpc_command(
323
+ entry,
324
+ "autopilot-toggle",
325
+ None,
326
+ self._console,
327
+ self._client,
328
+ loop_id=lid,
329
+ )
330
+ new_mode = "solo" if self._loop_autopilot_mode == "autopilot" else "autopilot"
331
+ self._apply_loop_autopilot_mode(new_mode)
332
+
333
+ except Exception as exc:
334
+ logger.exception("Autopilot toggle failed")
335
+ await self._mount_message(ErrorMessage(f"Failed to toggle autopilot: {exc}"))
336
+
281
337
  async def _submit_autopilot_job(self, task: str) -> None:
282
338
  """Submit an autopilot job via HTTP REST (like CLI `soothe autopilot run`).
283
339
 
@@ -355,7 +355,7 @@ class AppResult:
355
355
  """Exit code (0 for success, non-zero for error)."""
356
356
 
357
357
  loop_id: str | None
358
- """The final AgentLoop id at shutdown (may change if the user switched loops)."""
358
+ """The final StrangeLoop id at shutdown (may change if the user switched loops)."""
359
359
 
360
360
  session_stats: SessionStats = field(default_factory=SessionStats)
361
361
  """Cumulative usage stats across all turns in the session."""
@@ -442,7 +442,10 @@ class _StartupMixin:
442
442
  ws_url = websocket_url_from_config(self._daemon_config)
443
443
 
444
444
  # Check daemon status via WebSocket RPC (IG-174 Phase 1)
445
- daemon_live = await is_daemon_live(ws_url, timeout=5.0)
445
+ # Wait for daemon to be fully ready, not just port-live (IG-489)
446
+ daemon_live = await is_daemon_live(
447
+ ws_url, timeout=5.0, wait_for_ready=True, ready_timeout=30.0
448
+ )
446
449
 
447
450
  if not daemon_live:
448
451
  # CLI does NOT control daemon start/stop per architectural separation (IG-174/IG-175)
@@ -475,6 +478,13 @@ class _StartupMixin:
475
478
  if self._session_state is not None:
476
479
  self._session_state.loop_id = status_loop_id
477
480
 
481
+ autopilot_mode = event.status_event.get("autopilot_mode")
482
+ if autopilot_mode not in ("solo", "autopilot") and event.session is not None:
483
+ autopilot_mode = getattr(event.session, "autopilot_mode", None)
484
+ self._apply_loop_autopilot_mode(
485
+ str(autopilot_mode) if autopilot_mode in ("solo", "autopilot") else None
486
+ )
487
+
478
488
  try:
479
489
  banner = self.query_one("#welcome-banner", WelcomeBanner)
480
490
  banner.set_connected(self._mcp_tool_count)
@@ -97,7 +97,7 @@ COMMANDS: tuple[SlashCommand, ...] = (
97
97
  ),
98
98
  SlashCommand(
99
99
  name="/resume",
100
- description="Browse and resume AgentLoop instances",
100
+ description="Browse and resume StrangeLoop instances",
101
101
  bypass_tier=BypassTier.IMMEDIATE_UI,
102
102
  hidden_keywords="continue history sessions loops",
103
103
  ),
@@ -50,7 +50,7 @@ def validate_command(
50
50
  entry: Command registry entry
51
51
  command: Command name
52
52
  query: Query parameter (if present)
53
- loop_id: Active AgentLoop id for this session
53
+ loop_id: Active StrangeLoop id for this session
54
54
 
55
55
  Returns:
56
56
  Tuple of (is_valid, error_message)
@@ -245,7 +245,7 @@ async def handle_routing_command(
245
245
 
246
246
  For routing commands that map to a configured subagent id (e.g. ``/research``, ``/explore``),
247
247
  sets the WebSocket ``preferred_subagent`` field so the daemon merges a subagent hint into
248
- AgentLoop (IG-349). Other routing commands (e.g. ``/plan``) are sent as plain text unchanged.
248
+ StrangeLoop (IG-349). Other routing commands (e.g. ``/plan``) are sent as plain text unchanged.
249
249
 
250
250
  Args:
251
251
  cmd_input: Full command input (e.g., "/research topic summary")
@@ -272,6 +272,13 @@ COMMANDS: dict[str, dict[str, Any]] = {
272
272
  "requires_loop": True,
273
273
  "handler": show_autopilot_dashboard,
274
274
  },
275
+ "/autopilot-toggle": {
276
+ "location": "daemon",
277
+ "type": "rpc",
278
+ "daemon_command": "autopilot_toggle",
279
+ "description": "Toggle autopilot mode (solo ↔ autopilot)",
280
+ "requires_loop": True,
281
+ },
275
282
  # Daemon routing commands (3)
276
283
  "/plan": {"location": "daemon", "type": "routing", "description": "Trigger plan mode"},
277
284
  "/tacitus": {
@@ -578,7 +578,7 @@ def build_stream_config(
578
578
  * Including the SDK version here ensures it survives the merge.
579
579
 
580
580
  Args:
581
- loop_id: Active AgentLoop id (stored under LangGraph ``configurable.thread_id``).
581
+ loop_id: Active StrangeLoop id (stored under LangGraph ``configurable.thread_id``).
582
582
  assistant_id: The agent/assistant identifier, if any.
583
583
  sandbox_type: Sandbox provider name for trace metadata, or `None` if no
584
584
  sandbox is active.