codex-autorunner 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +114 -1
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +236 -1
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +5 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9125 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +70 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -9,8 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Callable, Optional
|
|
10
10
|
|
|
11
11
|
from .....agents.registry import validate_agent_id
|
|
12
|
-
from .....core.config import load_repo_config
|
|
13
|
-
from .....core.engine import Engine
|
|
12
|
+
from .....core.config import load_hub_config, load_repo_config
|
|
14
13
|
from .....core.flows import FlowController, FlowStore
|
|
15
14
|
from .....core.flows.models import FlowRunStatus
|
|
16
15
|
from .....core.flows.reconciler import reconcile_flow_run
|
|
@@ -22,19 +21,24 @@ from .....core.flows.ux_helpers import (
|
|
|
22
21
|
issue_md_path,
|
|
23
22
|
seed_issue_from_github,
|
|
24
23
|
seed_issue_from_text,
|
|
24
|
+
ticket_progress,
|
|
25
25
|
)
|
|
26
26
|
from .....core.flows.worker_process import (
|
|
27
27
|
FlowWorkerHealth,
|
|
28
28
|
check_worker_health,
|
|
29
29
|
clear_worker_metadata,
|
|
30
30
|
)
|
|
31
|
+
from .....core.logging_utils import log_event
|
|
32
|
+
from .....core.runtime import RuntimeContext
|
|
31
33
|
from .....core.state import now_iso
|
|
32
34
|
from .....core.utils import atomic_write, canonicalize_path
|
|
33
35
|
from .....flows.ticket_flow import build_ticket_flow_definition
|
|
36
|
+
from .....integrations.agents import build_backend_orchestrator
|
|
34
37
|
from .....integrations.agents.wiring import (
|
|
35
38
|
build_agent_backend_factory,
|
|
36
39
|
build_app_server_supervisor_factory,
|
|
37
40
|
)
|
|
41
|
+
from .....manifest import load_manifest
|
|
38
42
|
from .....tickets import AgentPool
|
|
39
43
|
from .....tickets.files import list_ticket_paths
|
|
40
44
|
from .....tickets.outbox import resolve_outbox_paths
|
|
@@ -67,6 +71,11 @@ def _ticket_dir(repo_root: Path) -> Path:
|
|
|
67
71
|
return repo_root.resolve() / ".codex-autorunner" / "tickets"
|
|
68
72
|
|
|
69
73
|
|
|
74
|
+
def _load_flow_store(repo_root: Path, hub_root: Optional[Path] = None) -> FlowStore:
|
|
75
|
+
config = load_repo_config(repo_root, hub_root)
|
|
76
|
+
return FlowStore(_flow_paths(repo_root)[0], durable=config.durable_writes)
|
|
77
|
+
|
|
78
|
+
|
|
70
79
|
def _normalize_run_id(value: str) -> Optional[str]:
|
|
71
80
|
try:
|
|
72
81
|
return str(uuid.UUID(str(value)))
|
|
@@ -107,16 +116,70 @@ def _flow_help_lines() -> list[str]:
|
|
|
107
116
|
"/flow restart",
|
|
108
117
|
"/flow archive [run_id] [--force]",
|
|
109
118
|
"/flow reply <message>",
|
|
110
|
-
"
|
|
119
|
+
"Alias: /flow start",
|
|
111
120
|
]
|
|
112
121
|
|
|
113
122
|
|
|
123
|
+
def _discover_unregistered_worktrees(
|
|
124
|
+
manifest, hub_root: Optional[Path]
|
|
125
|
+
) -> list[dict[str, object]]:
|
|
126
|
+
if not hub_root:
|
|
127
|
+
return []
|
|
128
|
+
try:
|
|
129
|
+
hub_config = load_hub_config(hub_root)
|
|
130
|
+
except Exception:
|
|
131
|
+
return []
|
|
132
|
+
worktrees_root = hub_config.worktrees_root
|
|
133
|
+
if not worktrees_root.exists() or not worktrees_root.is_dir():
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
known_paths = {(hub_root / repo.path).resolve() for repo in manifest.repos}
|
|
137
|
+
known_ids = {repo.id for repo in manifest.repos}
|
|
138
|
+
extras: list[dict[str, object]] = []
|
|
139
|
+
for child in sorted(worktrees_root.iterdir()):
|
|
140
|
+
if not child.is_dir():
|
|
141
|
+
continue
|
|
142
|
+
if not (child / ".git").exists():
|
|
143
|
+
continue
|
|
144
|
+
resolved = child.resolve()
|
|
145
|
+
if resolved in known_paths:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
flows_root = child / ".codex-autorunner" / "flows"
|
|
149
|
+
flows_db = child / ".codex-autorunner" / "flows.db"
|
|
150
|
+
if not flows_root.exists() and not flows_db.exists():
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
repo_id = child.name
|
|
154
|
+
label = repo_id
|
|
155
|
+
indent = ""
|
|
156
|
+
if "--" in repo_id:
|
|
157
|
+
_, suffix = repo_id.split("--", 1)
|
|
158
|
+
label = suffix or repo_id
|
|
159
|
+
indent = " - "
|
|
160
|
+
label = f"{label} (unregistered)"
|
|
161
|
+
if repo_id in known_ids:
|
|
162
|
+
label = f"{label} (duplicate id)"
|
|
163
|
+
extras.append(
|
|
164
|
+
{
|
|
165
|
+
"repo_id": repo_id,
|
|
166
|
+
"repo_root": resolved,
|
|
167
|
+
"label": label,
|
|
168
|
+
"indent": indent,
|
|
169
|
+
"unregistered": True,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
return extras
|
|
173
|
+
|
|
174
|
+
|
|
114
175
|
def _get_ticket_controller(repo_root: Path) -> FlowController:
|
|
115
176
|
db_path, artifacts_root = _flow_paths(repo_root)
|
|
116
177
|
config = load_repo_config(repo_root)
|
|
117
|
-
|
|
178
|
+
backend_orchestrator = build_backend_orchestrator(repo_root, config)
|
|
179
|
+
engine = RuntimeContext(
|
|
118
180
|
repo_root,
|
|
119
181
|
config=config,
|
|
182
|
+
backend_orchestrator=backend_orchestrator,
|
|
120
183
|
backend_factory=build_agent_backend_factory(repo_root, config),
|
|
121
184
|
app_server_supervisor_factory=build_app_server_supervisor_factory(config),
|
|
122
185
|
agent_id_validator=validate_agent_id,
|
|
@@ -251,61 +314,123 @@ class FlowCommands(SharedHelpers):
|
|
|
251
314
|
|
|
252
315
|
async def _handle_flow(self, message: TelegramMessage, args: str) -> None:
|
|
253
316
|
argv = self._parse_command_args(args)
|
|
317
|
+
|
|
318
|
+
target_repo_root = None
|
|
319
|
+
effective_args = args
|
|
320
|
+
|
|
321
|
+
if argv:
|
|
322
|
+
resolved = self._resolve_workspace(argv[0])
|
|
323
|
+
if resolved:
|
|
324
|
+
target_repo_root = Path(resolved[0])
|
|
325
|
+
argv = argv[1:]
|
|
326
|
+
# Reconstruct args for remainder logic (imperfect but sufficient for text commands)
|
|
327
|
+
effective_args = " ".join(argv)
|
|
328
|
+
|
|
254
329
|
action_raw = argv[0] if argv else ""
|
|
330
|
+
if target_repo_root and not action_raw:
|
|
331
|
+
action_raw = "status"
|
|
332
|
+
argv = ["status"]
|
|
333
|
+
effective_args = "status"
|
|
255
334
|
action = _normalize_flow_action(action_raw)
|
|
256
|
-
_, remainder = _split_flow_action(
|
|
335
|
+
_, remainder = _split_flow_action(effective_args)
|
|
257
336
|
rest_argv = argv[1:]
|
|
258
337
|
|
|
259
338
|
key = await self._resolve_topic_key(message.chat_id, message.thread_id)
|
|
260
339
|
record = await self._store.get_topic(key)
|
|
340
|
+
is_pma = bool(record and getattr(record, "pma_enabled", False))
|
|
341
|
+
is_unbound = bool(not record or not getattr(record, "workspace_path", None))
|
|
342
|
+
|
|
343
|
+
if not target_repo_root and not action_raw:
|
|
344
|
+
# Check if we should show Hub Overview
|
|
345
|
+
if is_pma or is_unbound:
|
|
346
|
+
await self._send_flow_hub_overview(message)
|
|
347
|
+
return
|
|
348
|
+
action = "status"
|
|
349
|
+
rest_argv = []
|
|
261
350
|
|
|
262
351
|
if action == "help":
|
|
263
352
|
await self._send_flow_overview(message, record)
|
|
264
353
|
return
|
|
265
354
|
|
|
266
|
-
if
|
|
355
|
+
if target_repo_root:
|
|
356
|
+
repo_root = canonicalize_path(target_repo_root)
|
|
357
|
+
elif record and record.workspace_path:
|
|
358
|
+
repo_root = canonicalize_path(Path(record.workspace_path))
|
|
359
|
+
else:
|
|
360
|
+
if action == "status" and (is_pma or is_unbound):
|
|
361
|
+
await self._send_flow_hub_overview(message)
|
|
362
|
+
return
|
|
267
363
|
await self._send_message(
|
|
268
364
|
message.chat_id,
|
|
269
|
-
"No workspace bound. Use /
|
|
365
|
+
"No workspace bound. Use /flow <repo-id> status to inspect a repo without binding, or /bind <repo-id> to attach this topic.",
|
|
270
366
|
thread_id=message.thread_id,
|
|
271
367
|
reply_to=message.message_id,
|
|
272
368
|
)
|
|
273
369
|
return
|
|
274
370
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
371
|
+
try:
|
|
372
|
+
if action == "status":
|
|
373
|
+
await self._handle_flow_status_action(message, repo_root, rest_argv)
|
|
374
|
+
return
|
|
375
|
+
if action == "runs":
|
|
376
|
+
await self._handle_flow_runs(message, repo_root, rest_argv)
|
|
377
|
+
return
|
|
378
|
+
if action == "bootstrap":
|
|
379
|
+
await self._handle_flow_bootstrap(message, repo_root, rest_argv)
|
|
380
|
+
return
|
|
381
|
+
if action == "issue":
|
|
382
|
+
await self._handle_flow_issue(message, repo_root, remainder)
|
|
383
|
+
return
|
|
384
|
+
if action == "plan":
|
|
385
|
+
await self._handle_flow_plan(message, repo_root, remainder)
|
|
386
|
+
return
|
|
387
|
+
if action == "resume":
|
|
388
|
+
await self._handle_flow_resume(message, repo_root, rest_argv)
|
|
389
|
+
return
|
|
390
|
+
if action == "stop":
|
|
391
|
+
await self._handle_flow_stop(message, repo_root, rest_argv)
|
|
392
|
+
return
|
|
393
|
+
if action == "recover":
|
|
394
|
+
await self._handle_flow_recover(message, repo_root, rest_argv)
|
|
395
|
+
return
|
|
396
|
+
if action == "restart":
|
|
397
|
+
await self._handle_flow_restart(message, repo_root, rest_argv)
|
|
398
|
+
return
|
|
399
|
+
if action == "archive":
|
|
400
|
+
await self._handle_flow_archive(message, repo_root, rest_argv)
|
|
401
|
+
return
|
|
402
|
+
if action == "reply":
|
|
403
|
+
await self._handle_reply(message, remainder)
|
|
404
|
+
return
|
|
405
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
406
|
+
# Let cancellations propagate so shutdowns/timeouts are not masked.
|
|
407
|
+
raise
|
|
408
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
409
|
+
log_event(
|
|
410
|
+
_logger,
|
|
411
|
+
logging.WARNING,
|
|
412
|
+
"telegram.flow.command_failed",
|
|
413
|
+
chat_id=message.chat_id,
|
|
414
|
+
thread_id=message.thread_id,
|
|
415
|
+
action=action or "unknown",
|
|
416
|
+
exc=exc,
|
|
417
|
+
)
|
|
418
|
+
format_msg = getattr(self, "_with_conversation_id", None)
|
|
419
|
+
error_text = (
|
|
420
|
+
format_msg(
|
|
421
|
+
"Flow command failed; check logs for details.",
|
|
422
|
+
chat_id=message.chat_id,
|
|
423
|
+
thread_id=message.thread_id,
|
|
424
|
+
)
|
|
425
|
+
if callable(format_msg)
|
|
426
|
+
else "Flow command failed; check logs for details."
|
|
427
|
+
)
|
|
428
|
+
await self._send_message(
|
|
429
|
+
message.chat_id,
|
|
430
|
+
error_text,
|
|
431
|
+
thread_id=message.thread_id,
|
|
432
|
+
reply_to=message.message_id,
|
|
433
|
+
)
|
|
309
434
|
return
|
|
310
435
|
|
|
311
436
|
await self._send_message(
|
|
@@ -323,7 +448,7 @@ class FlowCommands(SharedHelpers):
|
|
|
323
448
|
repo_root: Path,
|
|
324
449
|
run_id_raw: Optional[str],
|
|
325
450
|
) -> None:
|
|
326
|
-
store =
|
|
451
|
+
store = _load_flow_store(repo_root)
|
|
327
452
|
try:
|
|
328
453
|
store.initialize()
|
|
329
454
|
record, error = self._resolve_status_record(store, run_id_raw)
|
|
@@ -365,7 +490,7 @@ class FlowCommands(SharedHelpers):
|
|
|
365
490
|
error = None
|
|
366
491
|
notice = None
|
|
367
492
|
if action == "resume":
|
|
368
|
-
store =
|
|
493
|
+
store = _load_flow_store(repo_root)
|
|
369
494
|
try:
|
|
370
495
|
store.initialize()
|
|
371
496
|
run_id, error = self._resolve_run_id_input(store, run_id_raw)
|
|
@@ -388,7 +513,7 @@ class FlowCommands(SharedHelpers):
|
|
|
388
513
|
_spawn_flow_worker(repo_root, updated.id)
|
|
389
514
|
notice = "Resumed."
|
|
390
515
|
elif action == "stop":
|
|
391
|
-
store =
|
|
516
|
+
store = _load_flow_store(repo_root)
|
|
392
517
|
try:
|
|
393
518
|
store.initialize()
|
|
394
519
|
run_id, error = self._resolve_run_id_input(store, run_id_raw)
|
|
@@ -411,7 +536,7 @@ class FlowCommands(SharedHelpers):
|
|
|
411
536
|
await controller.stop_flow(record.id)
|
|
412
537
|
notice = "Stopped."
|
|
413
538
|
elif action == "recover":
|
|
414
|
-
store =
|
|
539
|
+
store = _load_flow_store(repo_root)
|
|
415
540
|
try:
|
|
416
541
|
store.initialize()
|
|
417
542
|
run_id, error = self._resolve_run_id_input(store, run_id_raw)
|
|
@@ -435,7 +560,7 @@ class FlowCommands(SharedHelpers):
|
|
|
435
560
|
finally:
|
|
436
561
|
store.close()
|
|
437
562
|
elif action == "archive":
|
|
438
|
-
store =
|
|
563
|
+
store = _load_flow_store(repo_root)
|
|
439
564
|
record = None
|
|
440
565
|
try:
|
|
441
566
|
store.initialize()
|
|
@@ -479,7 +604,7 @@ class FlowCommands(SharedHelpers):
|
|
|
479
604
|
archived_runs_dir = artifacts_root / record.id / "archived_runs"
|
|
480
605
|
shutil.move(str(run_dir), str(archived_runs_dir))
|
|
481
606
|
|
|
482
|
-
store =
|
|
607
|
+
store = _load_flow_store(repo_root)
|
|
483
608
|
try:
|
|
484
609
|
store.initialize()
|
|
485
610
|
store.delete_flow_run(record.id)
|
|
@@ -569,7 +694,16 @@ class FlowCommands(SharedHelpers):
|
|
|
569
694
|
run = record
|
|
570
695
|
status = getattr(run, "status", None)
|
|
571
696
|
status_value = status.value if status else "unknown"
|
|
697
|
+
progress = snapshot.get("ticket_progress") if snapshot else None
|
|
698
|
+
progress_label = None
|
|
699
|
+
if isinstance(progress, dict):
|
|
700
|
+
done = progress.get("done")
|
|
701
|
+
total = progress.get("total")
|
|
702
|
+
if isinstance(done, int) and isinstance(total, int) and total >= 0:
|
|
703
|
+
progress_label = f"{done}/{total}"
|
|
572
704
|
lines = [f"Run: {run.id}", f"Status: {status_value}"]
|
|
705
|
+
if progress_label:
|
|
706
|
+
lines.append(f"Tickets: {progress_label}")
|
|
573
707
|
flow_type = getattr(run, "flow_type", None)
|
|
574
708
|
if flow_type:
|
|
575
709
|
lines.append(f"Flow: {flow_type}")
|
|
@@ -721,7 +855,7 @@ class FlowCommands(SharedHelpers):
|
|
|
721
855
|
f"Workspace: {repo_root}" if repo_root else "Workspace: unbound",
|
|
722
856
|
]
|
|
723
857
|
if repo_root:
|
|
724
|
-
store =
|
|
858
|
+
store = _load_flow_store(repo_root)
|
|
725
859
|
try:
|
|
726
860
|
store.initialize()
|
|
727
861
|
runs = store.list_flow_runs(flow_type="ticket_flow")
|
|
@@ -741,10 +875,164 @@ class FlowCommands(SharedHelpers):
|
|
|
741
875
|
reply_to=message.message_id,
|
|
742
876
|
)
|
|
743
877
|
|
|
878
|
+
async def _send_flow_hub_overview(self, message: TelegramMessage) -> None:
|
|
879
|
+
if not self._manifest_path or not self._hub_root:
|
|
880
|
+
await self._send_message(
|
|
881
|
+
message.chat_id,
|
|
882
|
+
"Hub manifest not configured.",
|
|
883
|
+
thread_id=message.thread_id,
|
|
884
|
+
reply_to=message.message_id,
|
|
885
|
+
)
|
|
886
|
+
return
|
|
887
|
+
|
|
888
|
+
try:
|
|
889
|
+
manifest = load_manifest(self._manifest_path, self._hub_root)
|
|
890
|
+
except Exception:
|
|
891
|
+
await self._send_message(
|
|
892
|
+
message.chat_id,
|
|
893
|
+
"Failed to load manifest.",
|
|
894
|
+
thread_id=message.thread_id,
|
|
895
|
+
reply_to=message.message_id,
|
|
896
|
+
)
|
|
897
|
+
return
|
|
898
|
+
|
|
899
|
+
def _group_key(repo_id: str) -> tuple[str, Optional[str]]:
|
|
900
|
+
parts = repo_id.split("--", 1)
|
|
901
|
+
if len(parts) == 1:
|
|
902
|
+
return repo_id, None
|
|
903
|
+
return parts[0], parts[1]
|
|
904
|
+
|
|
905
|
+
def _format_status_line(
|
|
906
|
+
label: str,
|
|
907
|
+
*,
|
|
908
|
+
status_icon: str,
|
|
909
|
+
status_value: str,
|
|
910
|
+
progress_label: str,
|
|
911
|
+
run_id: Optional[str],
|
|
912
|
+
indent: str = "",
|
|
913
|
+
) -> str:
|
|
914
|
+
run_suffix = f" run {run_id}" if run_id else ""
|
|
915
|
+
return f"{indent}{status_icon} {label}: {status_value} {progress_label}{run_suffix}"
|
|
916
|
+
|
|
917
|
+
lines = ["Hub Flow Overview:"]
|
|
918
|
+
groups: dict[str, list[tuple[str, str]]] = {}
|
|
919
|
+
group_order: list[str] = []
|
|
920
|
+
|
|
921
|
+
entries: list[dict[str, object]] = []
|
|
922
|
+
for repo in manifest.repos:
|
|
923
|
+
if not repo.enabled:
|
|
924
|
+
continue
|
|
925
|
+
repo_root = (self._hub_root / repo.path).resolve()
|
|
926
|
+
group, suffix = _group_key(repo.id)
|
|
927
|
+
label = suffix or repo.id
|
|
928
|
+
indent = " - " if suffix else ""
|
|
929
|
+
entries.append(
|
|
930
|
+
{
|
|
931
|
+
"repo_id": repo.id,
|
|
932
|
+
"repo_root": repo_root,
|
|
933
|
+
"label": label,
|
|
934
|
+
"indent": indent,
|
|
935
|
+
"group": group,
|
|
936
|
+
"unregistered": False,
|
|
937
|
+
}
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
extras = _discover_unregistered_worktrees(manifest, self._hub_root)
|
|
941
|
+
for extra in extras:
|
|
942
|
+
repo_id = str(extra["repo_id"])
|
|
943
|
+
group, _ = _group_key(repo_id)
|
|
944
|
+
extra["group"] = group
|
|
945
|
+
entries.append(extra)
|
|
946
|
+
|
|
947
|
+
for entry in entries:
|
|
948
|
+
repo_id = str(entry["repo_id"])
|
|
949
|
+
repo_root = Path(entry["repo_root"])
|
|
950
|
+
label = str(entry["label"])
|
|
951
|
+
indent = str(entry.get("indent", ""))
|
|
952
|
+
group = str(entry.get("group", repo_id))
|
|
953
|
+
if group not in groups:
|
|
954
|
+
groups[group] = []
|
|
955
|
+
group_order.append(group)
|
|
956
|
+
|
|
957
|
+
store = _load_flow_store(repo_root)
|
|
958
|
+
try:
|
|
959
|
+
store.initialize()
|
|
960
|
+
progress = ticket_progress(repo_root)
|
|
961
|
+
done = progress.get("done", 0)
|
|
962
|
+
total = progress.get("total", 0)
|
|
963
|
+
progress_label = f"{done}/{total}"
|
|
964
|
+
active = _select_latest_run(store, lambda run: run.status.is_active())
|
|
965
|
+
if active:
|
|
966
|
+
status_icon = (
|
|
967
|
+
"🟢" if active.status == FlowRunStatus.RUNNING else "🟡"
|
|
968
|
+
)
|
|
969
|
+
status_line = _format_status_line(
|
|
970
|
+
label,
|
|
971
|
+
status_icon=status_icon,
|
|
972
|
+
status_value=active.status.value,
|
|
973
|
+
progress_label=progress_label,
|
|
974
|
+
run_id=active.id,
|
|
975
|
+
indent=indent,
|
|
976
|
+
)
|
|
977
|
+
else:
|
|
978
|
+
paused = _select_latest_run(
|
|
979
|
+
store, lambda run: run.status == FlowRunStatus.PAUSED
|
|
980
|
+
)
|
|
981
|
+
if paused:
|
|
982
|
+
status_line = _format_status_line(
|
|
983
|
+
label,
|
|
984
|
+
status_icon="🔴",
|
|
985
|
+
status_value="PAUSED",
|
|
986
|
+
progress_label=progress_label,
|
|
987
|
+
run_id=paused.id,
|
|
988
|
+
indent=indent,
|
|
989
|
+
)
|
|
990
|
+
else:
|
|
991
|
+
status_line = _format_status_line(
|
|
992
|
+
label,
|
|
993
|
+
status_icon="⚪",
|
|
994
|
+
status_value="Idle",
|
|
995
|
+
progress_label=progress_label,
|
|
996
|
+
run_id=None,
|
|
997
|
+
indent=indent,
|
|
998
|
+
)
|
|
999
|
+
except Exception:
|
|
1000
|
+
status_line = f"{indent}❓ {label}: Error reading state"
|
|
1001
|
+
finally:
|
|
1002
|
+
store.close()
|
|
1003
|
+
|
|
1004
|
+
groups[group].append((label, status_line))
|
|
1005
|
+
|
|
1006
|
+
for group in group_order:
|
|
1007
|
+
entries = groups.get(group, [])
|
|
1008
|
+
if not entries:
|
|
1009
|
+
continue
|
|
1010
|
+
entries.sort(key=lambda pair: (0 if pair[0] == group else 1, pair[0]))
|
|
1011
|
+
lines.extend([line for _label, line in entries])
|
|
1012
|
+
lines.append("")
|
|
1013
|
+
|
|
1014
|
+
if lines and lines[-1] == "":
|
|
1015
|
+
lines.pop()
|
|
1016
|
+
if extras:
|
|
1017
|
+
lines.append("")
|
|
1018
|
+
lines.append(
|
|
1019
|
+
"Note: Unregistered worktrees detected. Run 'car hub scan' to register them."
|
|
1020
|
+
)
|
|
1021
|
+
lines.append("")
|
|
1022
|
+
lines.append("Tip: use /flow <repo-id> status for repo details.")
|
|
1023
|
+
|
|
1024
|
+
await self._send_message(
|
|
1025
|
+
message.chat_id,
|
|
1026
|
+
"\n".join(lines),
|
|
1027
|
+
thread_id=message.thread_id,
|
|
1028
|
+
reply_to=message.message_id,
|
|
1029
|
+
parse_mode=None,
|
|
1030
|
+
)
|
|
1031
|
+
|
|
744
1032
|
async def _handle_flow_status_action(
|
|
745
1033
|
self, message: TelegramMessage, repo_root: Path, argv: list[str]
|
|
746
1034
|
) -> None:
|
|
747
|
-
store =
|
|
1035
|
+
store = _load_flow_store(repo_root)
|
|
748
1036
|
try:
|
|
749
1037
|
store.initialize()
|
|
750
1038
|
run_id_raw = self._first_non_flag(argv)
|
|
@@ -785,7 +1073,7 @@ class FlowCommands(SharedHelpers):
|
|
|
785
1073
|
return
|
|
786
1074
|
limit = min(limit_value, 50)
|
|
787
1075
|
|
|
788
|
-
store =
|
|
1076
|
+
store = _load_flow_store(repo_root)
|
|
789
1077
|
try:
|
|
790
1078
|
store.initialize()
|
|
791
1079
|
runs = store.list_flow_runs(flow_type="ticket_flow")
|
|
@@ -838,7 +1126,7 @@ class FlowCommands(SharedHelpers):
|
|
|
838
1126
|
tickets_exist = bool(existing_tickets)
|
|
839
1127
|
issue_exists = issue_md_has_content(repo_root)
|
|
840
1128
|
|
|
841
|
-
store =
|
|
1129
|
+
store = _load_flow_store(repo_root)
|
|
842
1130
|
active_run = None
|
|
843
1131
|
try:
|
|
844
1132
|
store.initialize()
|
|
@@ -865,7 +1153,7 @@ class FlowCommands(SharedHelpers):
|
|
|
865
1153
|
if gh_available:
|
|
866
1154
|
repo_label = f" for {repo_slug}" if repo_slug else ""
|
|
867
1155
|
prompt = (
|
|
868
|
-
"Enter GitHub issue number or URL
|
|
1156
|
+
f"Enter GitHub issue number or URL{repo_label} to seed ISSUE.md:"
|
|
869
1157
|
)
|
|
870
1158
|
issue_ref = await self._prompt_flow_text_input(message, prompt)
|
|
871
1159
|
if not issue_ref:
|
|
@@ -1057,7 +1345,7 @@ You are the first ticket in a new ticket_flow run.
|
|
|
1057
1345
|
async def _handle_flow_resume(
|
|
1058
1346
|
self, message: TelegramMessage, repo_root: Path, argv: list[str]
|
|
1059
1347
|
) -> None:
|
|
1060
|
-
store =
|
|
1348
|
+
store = _load_flow_store(repo_root)
|
|
1061
1349
|
try:
|
|
1062
1350
|
store.initialize()
|
|
1063
1351
|
run_id_raw = self._first_non_flag(argv)
|
|
@@ -1117,7 +1405,7 @@ You are the first ticket in a new ticket_flow run.
|
|
|
1117
1405
|
async def _handle_flow_stop(
|
|
1118
1406
|
self, message: TelegramMessage, repo_root: Path, argv: list[str]
|
|
1119
1407
|
) -> None:
|
|
1120
|
-
store =
|
|
1408
|
+
store = _load_flow_store(repo_root)
|
|
1121
1409
|
try:
|
|
1122
1410
|
store.initialize()
|
|
1123
1411
|
run_id_raw = self._first_non_flag(argv)
|
|
@@ -1165,7 +1453,7 @@ You are the first ticket in a new ticket_flow run.
|
|
|
1165
1453
|
async def _handle_flow_recover(
|
|
1166
1454
|
self, message: TelegramMessage, repo_root: Path, argv: list[str]
|
|
1167
1455
|
) -> None:
|
|
1168
|
-
store =
|
|
1456
|
+
store = _load_flow_store(repo_root)
|
|
1169
1457
|
try:
|
|
1170
1458
|
store.initialize()
|
|
1171
1459
|
run_id_raw = self._first_non_flag(argv)
|
|
@@ -1218,7 +1506,7 @@ You are the first ticket in a new ticket_flow run.
|
|
|
1218
1506
|
argv: Optional[list[str]] = None,
|
|
1219
1507
|
) -> None:
|
|
1220
1508
|
argv = argv or []
|
|
1221
|
-
store =
|
|
1509
|
+
store = _load_flow_store(repo_root)
|
|
1222
1510
|
record = None
|
|
1223
1511
|
try:
|
|
1224
1512
|
store.initialize()
|
|
@@ -1241,7 +1529,7 @@ You are the first ticket in a new ticket_flow run.
|
|
|
1241
1529
|
self, message: TelegramMessage, repo_root: Path, argv: list[str]
|
|
1242
1530
|
) -> None:
|
|
1243
1531
|
force = self._has_flag(argv, "--force")
|
|
1244
|
-
store =
|
|
1532
|
+
store = _load_flow_store(repo_root)
|
|
1245
1533
|
record = None
|
|
1246
1534
|
try:
|
|
1247
1535
|
store.initialize()
|
|
@@ -1304,7 +1592,7 @@ You are the first ticket in a new ticket_flow run.
|
|
|
1304
1592
|
archived_runs_dir = artifacts_root / record.id / "archived_runs"
|
|
1305
1593
|
shutil.move(str(run_dir), str(archived_runs_dir))
|
|
1306
1594
|
|
|
1307
|
-
store =
|
|
1595
|
+
store = _load_flow_store(repo_root)
|
|
1308
1596
|
try:
|
|
1309
1597
|
store.initialize()
|
|
1310
1598
|
store.delete_flow_run(record.id)
|