codex-autorunner 1.0.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/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- 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/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- 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 +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -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/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- 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 +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- 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 +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
codex_autorunner/codex_cli.py
CHANGED
|
@@ -1,84 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def inject_flag(
|
|
24
|
-
args: Iterable[str],
|
|
25
|
-
flag: str,
|
|
26
|
-
value: Optional[str],
|
|
27
|
-
*,
|
|
28
|
-
subcommands: Iterable[str] = SUBCOMMAND_HINTS,
|
|
29
|
-
) -> list[str]:
|
|
30
|
-
if not value:
|
|
31
|
-
return [str(a) for a in args]
|
|
32
|
-
args_list = [str(a) for a in args]
|
|
33
|
-
if extract_flag_value(args_list, flag):
|
|
34
|
-
return args_list
|
|
35
|
-
insert_at = None
|
|
36
|
-
for cmd in subcommands:
|
|
37
|
-
try:
|
|
38
|
-
insert_at = args_list.index(cmd)
|
|
39
|
-
break
|
|
40
|
-
except ValueError:
|
|
41
|
-
continue
|
|
42
|
-
if insert_at is None:
|
|
43
|
-
# `args` is sometimes a full argv that includes the binary at index 0,
|
|
44
|
-
# e.g. ["codex", "--yolo", ...]. In that case, never prepend flags before
|
|
45
|
-
# the binary or we'll turn argv[0] into e.g. "--model" and crash at spawn.
|
|
46
|
-
if args_list and not args_list[0].startswith("-"):
|
|
47
|
-
return [args_list[0], flag, value] + args_list[1:]
|
|
48
|
-
return [flag, value] + args_list
|
|
49
|
-
return args_list[:insert_at] + [flag, value] + args_list[insert_at:]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def apply_codex_options(
|
|
53
|
-
args: Iterable[str],
|
|
54
|
-
*,
|
|
55
|
-
model: Optional[str] = None,
|
|
56
|
-
reasoning: Optional[str] = None,
|
|
57
|
-
supports_reasoning: Optional[bool] = None,
|
|
58
|
-
) -> list[str]:
|
|
59
|
-
with_model = inject_flag(args, "--model", model)
|
|
60
|
-
if reasoning and supports_reasoning is False:
|
|
61
|
-
return with_model
|
|
62
|
-
return inject_flag(with_model, "--reasoning", reasoning)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _read_help_text(binary: str) -> str:
|
|
66
|
-
try:
|
|
67
|
-
result = subprocess.run(
|
|
68
|
-
[binary, "--help"],
|
|
69
|
-
capture_output=True,
|
|
70
|
-
text=True,
|
|
71
|
-
check=False,
|
|
72
|
-
)
|
|
73
|
-
except FileNotFoundError:
|
|
74
|
-
return ""
|
|
75
|
-
return "\n".join(filter(None, [result.stdout, result.stderr]))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@lru_cache(maxsize=8)
|
|
79
|
-
def supports_flag(binary: str, flag: str) -> bool:
|
|
80
|
-
return flag in _read_help_text(binary)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def supports_reasoning(binary: str) -> bool:
|
|
84
|
-
return supports_flag(binary, "--reasoning")
|
|
1
|
+
"""Backward-compatible Codex CLI helpers.
|
|
2
|
+
|
|
3
|
+
Delegates to core.utils to avoid duplicated logic.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .core.utils import ( # noqa: F401
|
|
7
|
+
apply_codex_options,
|
|
8
|
+
extract_flag_value,
|
|
9
|
+
inject_flag,
|
|
10
|
+
supports_flag,
|
|
11
|
+
supports_reasoning,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"apply_codex_options",
|
|
16
|
+
"extract_flag_value",
|
|
17
|
+
"inject_flag",
|
|
18
|
+
"supports_flag",
|
|
19
|
+
"supports_reasoning",
|
|
20
|
+
]
|
|
@@ -1 +1,21 @@
|
|
|
1
1
|
"""Core runtime primitives."""
|
|
2
|
+
|
|
3
|
+
from .archive import ArchiveResult, archive_worktree_snapshot
|
|
4
|
+
from .context_awareness import CAR_AWARENESS_BLOCK, format_file_role_addendum
|
|
5
|
+
from .lifecycle_events import (
|
|
6
|
+
LifecycleEvent,
|
|
7
|
+
LifecycleEventEmitter,
|
|
8
|
+
LifecycleEventStore,
|
|
9
|
+
LifecycleEventType,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ArchiveResult",
|
|
14
|
+
"archive_worktree_snapshot",
|
|
15
|
+
"CAR_AWARENESS_BLOCK",
|
|
16
|
+
"format_file_role_addendum",
|
|
17
|
+
"LifecycleEvent",
|
|
18
|
+
"LifecycleEventEmitter",
|
|
19
|
+
"LifecycleEventStore",
|
|
20
|
+
"LifecycleEventType",
|
|
21
|
+
]
|
|
@@ -13,9 +13,19 @@ from .config import (
|
|
|
13
13
|
|
|
14
14
|
ABOUT_CAR_BASENAME = "ABOUT_CAR.md"
|
|
15
15
|
ABOUT_CAR_REL_PATH = Path(".codex-autorunner") / ABOUT_CAR_BASENAME
|
|
16
|
+
TICKET_FLOW_QUICKSTART_BASENAME = "TICKET_FLOW_QUICKSTART.md"
|
|
17
|
+
TICKET_FLOW_QUICKSTART_REL_PATH = (
|
|
18
|
+
Path(".codex-autorunner") / TICKET_FLOW_QUICKSTART_BASENAME
|
|
19
|
+
)
|
|
20
|
+
TICKETS_AGENTS_BASENAME = "AGENTS.md"
|
|
21
|
+
TICKETS_AGENTS_REL_PATH = (
|
|
22
|
+
Path(".codex-autorunner") / "tickets" / TICKETS_AGENTS_BASENAME
|
|
23
|
+
)
|
|
16
24
|
|
|
17
25
|
# If this marker is present, codex-autorunner may safely refresh the file content.
|
|
18
26
|
ABOUT_CAR_GENERATED_MARKER = "<!-- CAR:AUTOGENERATED -->"
|
|
27
|
+
TICKET_FLOW_QUICKSTART_GENERATED_MARKER = "<!-- CAR:TICKET_FLOW_QUICKSTART -->"
|
|
28
|
+
TICKETS_AGENTS_GENERATED_MARKER = "<!-- CAR:TICKETS_AGENTS -->"
|
|
19
29
|
|
|
20
30
|
CAR_CONTEXT_KEYWORDS = (
|
|
21
31
|
"car",
|
|
@@ -76,7 +86,11 @@ def build_about_car_markdown(
|
|
|
76
86
|
"You are running inside **Codex Autorunner (CAR)**.\n\n"
|
|
77
87
|
"CAR uses a ticket-first workflow.\n\n"
|
|
78
88
|
"## Required for operation\n"
|
|
79
|
-
"- Tickets live under `.codex-autorunner/tickets
|
|
89
|
+
"- Tickets live under `.codex-autorunner/tickets/` (per-repo/worktree).\n"
|
|
90
|
+
"- If the user provides ticket files, place them in the repo's `.codex-autorunner/tickets/` folder.\n"
|
|
91
|
+
"- Lint ticket frontmatter after edits (runs against all tickets): `python3 .codex-autorunner/bin/lint_tickets.py`.\n\n"
|
|
92
|
+
"## Ticket flow quickstart\n"
|
|
93
|
+
f"- Read `{_display_path(repo_root, TICKET_FLOW_QUICKSTART_REL_PATH)}` for CLI entrypoints + gotchas.\n\n"
|
|
80
94
|
"## Optional workspace docs\n"
|
|
81
95
|
"- **Active context**: "
|
|
82
96
|
f"`{active_context_disp}`\n"
|
|
@@ -91,6 +105,16 @@ def build_about_car_markdown(
|
|
|
91
105
|
"- **Dispatch**: An update or message from the agent.\n"
|
|
92
106
|
"- **Handoff**: Passing control from agent to user (or vice versa).\n"
|
|
93
107
|
"- **Inbox**: Where the agent receives files/messages.\n\n"
|
|
108
|
+
"## Ticket helpers\n"
|
|
109
|
+
"- Use `.codex-autorunner/bin/ticket_tool.py` to list/create/insert/move tickets; it is portable and venv-free.\n"
|
|
110
|
+
'- Common workflows: insert gap before N (`python3 .codex-autorunner/bin/ticket_tool.py insert --before N`); move a block (`... move --start A --end B --to T`); create with auto-quoted frontmatter (`... create --title "Fix #123" --agent codex`).\n'
|
|
111
|
+
"- After any ticket edits, lint all tickets: `python3 .codex-autorunner/bin/lint_tickets.py`.\n\n"
|
|
112
|
+
"## Ticket templates (optional)\n"
|
|
113
|
+
"- CAR can fetch ticket templates from configured git repos (treat templates as code).\n"
|
|
114
|
+
"- Fetch (prints template to stdout): `car templates fetch <repo_id>:<path>[@<ref>]`\n"
|
|
115
|
+
"- Pin to a commit for determinism: `...@<commit_sha>`\n"
|
|
116
|
+
"- Trusted repos skip scanning. Untrusted repos are scanned (cached by blob SHA) before content is returned.\n"
|
|
117
|
+
"- If fetch or scan fails, pause and notify the user rather than guessing.\n\n"
|
|
94
118
|
"## How CAR works (short)\n"
|
|
95
119
|
"- The web UI provides ticket editing + unified file chat.\n"
|
|
96
120
|
"- `car serve` starts the hub web UI. The **Terminal** tab launches the configured `codex` binary in a PTY.\n"
|
|
@@ -139,6 +163,54 @@ def ensure_about_car_file_for_repo(
|
|
|
139
163
|
return path
|
|
140
164
|
|
|
141
165
|
|
|
166
|
+
def build_ticket_flow_quickstart_markdown(*, repo_root: Path) -> str:
|
|
167
|
+
ticket_dir = ".codex-autorunner/tickets/"
|
|
168
|
+
return (
|
|
169
|
+
f"{TICKET_FLOW_QUICKSTART_GENERATED_MARKER}\n"
|
|
170
|
+
"# Ticket Flow Quickstart\n\n"
|
|
171
|
+
"## Start ticket flow via CLI\n"
|
|
172
|
+
"- Bootstrap the first run (creates TICKET-001 if needed):\n"
|
|
173
|
+
" `car flow ticket_flow bootstrap --repo <path>`\n"
|
|
174
|
+
" (alias: `car ticket-flow bootstrap --repo <path>`)\n"
|
|
175
|
+
"- Start/resume without seeding tickets:\n"
|
|
176
|
+
" `car flow ticket_flow start --repo <path>`\n"
|
|
177
|
+
"- Check status:\n"
|
|
178
|
+
" `car flow ticket_flow status --repo <path> [--run-id <uuid>]`\n"
|
|
179
|
+
"- Resume/stop:\n"
|
|
180
|
+
" `car flow ticket_flow resume --repo <path> [--run-id <uuid>]`\n"
|
|
181
|
+
" `car flow ticket_flow stop --repo <path> [--run-id <uuid>]`\n\n"
|
|
182
|
+
"## Where tickets live\n"
|
|
183
|
+
f"- Tickets are per-repo/worktree under `{ticket_dir}`.\n"
|
|
184
|
+
"- If the user provides ticket files, save them directly into that folder.\n\n"
|
|
185
|
+
"## Common gotchas\n"
|
|
186
|
+
"- Hub vs repo: ticket flows run per-repo; CLI commands need a repo path.\n"
|
|
187
|
+
"- `--repo` expects a filesystem path, not a hub repo_id.\n"
|
|
188
|
+
"- Each worktree has its own `.codex-autorunner/` directory.\n"
|
|
189
|
+
"- If this repo/worktree lives under a hub, it must be registered in the hub manifest to show up in the hub UI. Run: `car hub scan` (or create it via `car hub worktree create`).\n"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_tickets_agents_markdown(*, repo_root: Path) -> str:
|
|
194
|
+
quickstart_path = _display_path(repo_root, TICKET_FLOW_QUICKSTART_REL_PATH)
|
|
195
|
+
return (
|
|
196
|
+
f"{TICKETS_AGENTS_GENERATED_MARKER}\n"
|
|
197
|
+
"# Tickets — AGENTS\n\n"
|
|
198
|
+
"This folder is the authoritative ticket queue for this repo/worktree.\n\n"
|
|
199
|
+
"## Ticket files\n"
|
|
200
|
+
"- Store work items as `TICKET-###*.md` (ordered by number).\n"
|
|
201
|
+
"- Keep frontmatter `done: true|false` in sync with completion.\n"
|
|
202
|
+
"- After edits, lint tickets: `python3 .codex-autorunner/bin/lint_tickets.py`.\n\n"
|
|
203
|
+
"## Ticket CLI (portable)\n"
|
|
204
|
+
"- List: `python3 .codex-autorunner/bin/ticket_tool.py list`\n"
|
|
205
|
+
'- Create: `python3 .codex-autorunner/bin/ticket_tool.py create --title "..." --agent codex`\n'
|
|
206
|
+
"- Insert gap: `python3 .codex-autorunner/bin/ticket_tool.py insert --before N`\n"
|
|
207
|
+
"- Move block: `python3 .codex-autorunner/bin/ticket_tool.py move --start A --end B --to T`\n"
|
|
208
|
+
"- Lint: `python3 .codex-autorunner/bin/ticket_tool.py lint`\n\n"
|
|
209
|
+
"## Ticket flow (runner)\n"
|
|
210
|
+
f"- See `{quickstart_path}` for `car flow ticket_flow ...` commands.\n"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
142
214
|
def ensure_about_car_file(config: Config, *, force: bool = False) -> Path:
|
|
143
215
|
"""Config-aware wrapper that uses configured doc paths."""
|
|
144
216
|
repo_root = config.root
|
|
@@ -148,3 +220,49 @@ def ensure_about_car_file(config: Config, *, force: bool = False) -> Path:
|
|
|
148
220
|
"spec": config.doc_path("spec"),
|
|
149
221
|
}
|
|
150
222
|
return ensure_about_car_file_for_repo(repo_root, doc_paths=docs, force=force)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def ensure_ticket_flow_quickstart_file_for_repo(
|
|
226
|
+
repo_root: Path, *, force: bool = False
|
|
227
|
+
) -> Path:
|
|
228
|
+
path = repo_root / TICKET_FLOW_QUICKSTART_REL_PATH
|
|
229
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
content = build_ticket_flow_quickstart_markdown(repo_root=repo_root)
|
|
231
|
+
if content and not content.endswith("\n"):
|
|
232
|
+
content += "\n"
|
|
233
|
+
|
|
234
|
+
if path.exists() and not force:
|
|
235
|
+
try:
|
|
236
|
+
existing = path.read_text(encoding="utf-8")
|
|
237
|
+
except OSError:
|
|
238
|
+
existing = ""
|
|
239
|
+
if TICKET_FLOW_QUICKSTART_GENERATED_MARKER not in existing:
|
|
240
|
+
return path
|
|
241
|
+
if existing == content:
|
|
242
|
+
return path
|
|
243
|
+
|
|
244
|
+
path.write_text(content, encoding="utf-8")
|
|
245
|
+
return path
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def ensure_tickets_agents_file_for_repo(
|
|
249
|
+
repo_root: Path, *, force: bool = False
|
|
250
|
+
) -> Path:
|
|
251
|
+
path = repo_root / TICKETS_AGENTS_REL_PATH
|
|
252
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
content = build_tickets_agents_markdown(repo_root=repo_root)
|
|
254
|
+
if content and not content.endswith("\n"):
|
|
255
|
+
content += "\n"
|
|
256
|
+
|
|
257
|
+
if path.exists() and not force:
|
|
258
|
+
try:
|
|
259
|
+
existing = path.read_text(encoding="utf-8")
|
|
260
|
+
except OSError:
|
|
261
|
+
existing = ""
|
|
262
|
+
if TICKETS_AGENTS_GENERATED_MARKER not in existing:
|
|
263
|
+
return path
|
|
264
|
+
if existing == content:
|
|
265
|
+
return path
|
|
266
|
+
|
|
267
|
+
path.write_text(content, encoding="utf-8")
|
|
268
|
+
return path
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_turn_id(payload: Any) -> Optional[str]:
|
|
5
|
+
if not isinstance(payload, dict):
|
|
6
|
+
return None
|
|
7
|
+
for key in ("turnId", "turn_id", "id"):
|
|
8
|
+
value = payload.get(key)
|
|
9
|
+
if isinstance(value, str):
|
|
10
|
+
return value
|
|
11
|
+
turn = payload.get("turn")
|
|
12
|
+
if isinstance(turn, dict):
|
|
13
|
+
for key in ("id", "turnId", "turn_id"):
|
|
14
|
+
value = turn.get(key)
|
|
15
|
+
if isinstance(value, str):
|
|
16
|
+
return value
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _extract_thread_id_from_container(payload: Any) -> Optional[str]:
|
|
21
|
+
if not isinstance(payload, dict):
|
|
22
|
+
return None
|
|
23
|
+
for key in ("threadId", "thread_id"):
|
|
24
|
+
value = payload.get(key)
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
return value
|
|
27
|
+
thread = payload.get("thread")
|
|
28
|
+
if isinstance(thread, dict):
|
|
29
|
+
for key in ("id", "threadId", "thread_id"):
|
|
30
|
+
value = thread.get(key)
|
|
31
|
+
if isinstance(value, str):
|
|
32
|
+
return value
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def extract_thread_id_for_turn(payload: Any) -> Optional[str]:
|
|
37
|
+
if not isinstance(payload, dict):
|
|
38
|
+
return None
|
|
39
|
+
for candidate in (payload, payload.get("turn"), payload.get("item")):
|
|
40
|
+
thread_id = _extract_thread_id_from_container(candidate)
|
|
41
|
+
if thread_id:
|
|
42
|
+
return thread_id
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_thread_id(payload: Any) -> Optional[str]:
|
|
47
|
+
if not isinstance(payload, dict):
|
|
48
|
+
return None
|
|
49
|
+
for key in ("threadId", "thread_id", "id"):
|
|
50
|
+
value = payload.get(key)
|
|
51
|
+
if isinstance(value, str):
|
|
52
|
+
return value
|
|
53
|
+
thread = payload.get("thread")
|
|
54
|
+
if isinstance(thread, dict):
|
|
55
|
+
for key in ("id", "threadId", "thread_id"):
|
|
56
|
+
value = thread.get(key)
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
return value
|
|
59
|
+
return None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
@@ -16,11 +17,17 @@ FILE_CHAT_KEY = "file_chat"
|
|
|
16
17
|
FILE_CHAT_OPENCODE_KEY = "file_chat.opencode"
|
|
17
18
|
FILE_CHAT_PREFIX = "file_chat."
|
|
18
19
|
FILE_CHAT_OPENCODE_PREFIX = "file_chat.opencode."
|
|
20
|
+
PMA_KEY = "pma"
|
|
21
|
+
PMA_OPENCODE_KEY = "pma.opencode"
|
|
22
|
+
|
|
23
|
+
LOGGER = logging.getLogger("codex_autorunner.app_server")
|
|
19
24
|
|
|
20
25
|
# Static keys that can be reset/managed via the UI.
|
|
21
26
|
FEATURE_KEYS = {
|
|
22
27
|
FILE_CHAT_KEY,
|
|
23
28
|
FILE_CHAT_OPENCODE_KEY,
|
|
29
|
+
PMA_KEY,
|
|
30
|
+
PMA_OPENCODE_KEY,
|
|
24
31
|
"autorunner",
|
|
25
32
|
"autorunner.opencode",
|
|
26
33
|
}
|
|
@@ -89,6 +96,8 @@ class AppServerThreadRegistry:
|
|
|
89
96
|
"file_chat_opencode": threads.get(FILE_CHAT_OPENCODE_KEY),
|
|
90
97
|
"autorunner": threads.get("autorunner"),
|
|
91
98
|
"autorunner_opencode": threads.get("autorunner.opencode"),
|
|
99
|
+
"pma": threads.get(PMA_KEY),
|
|
100
|
+
"pma_opencode": threads.get(PMA_OPENCODE_KEY),
|
|
92
101
|
}
|
|
93
102
|
notice = self.corruption_notice()
|
|
94
103
|
if notice:
|
|
@@ -177,8 +186,14 @@ class AppServerThreadRegistry:
|
|
|
177
186
|
try:
|
|
178
187
|
atomic_write(self._notice_path(), json.dumps(notice, indent=2) + "\n")
|
|
179
188
|
except Exception:
|
|
180
|
-
|
|
189
|
+
LOGGER.warning(
|
|
190
|
+
"Failed to write app server thread corruption notice.",
|
|
191
|
+
exc_info=True,
|
|
192
|
+
)
|
|
181
193
|
try:
|
|
182
194
|
self._save_unlocked({})
|
|
183
195
|
except Exception:
|
|
184
|
-
|
|
196
|
+
LOGGER.warning(
|
|
197
|
+
"Failed to reset app server thread registry after corruption.",
|
|
198
|
+
exc_info=True,
|
|
199
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Optional, Sequence
|
|
4
|
+
|
|
5
|
+
from .logging_utils import log_event
|
|
6
|
+
from .utils import resolve_executable, subprocess_env
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def app_server_env(
|
|
10
|
+
command: Sequence[str],
|
|
11
|
+
cwd: Path,
|
|
12
|
+
*,
|
|
13
|
+
base_env: Optional[dict[str, str]] = None,
|
|
14
|
+
) -> dict[str, str]:
|
|
15
|
+
extra_paths: list[str] = []
|
|
16
|
+
if command:
|
|
17
|
+
binary = command[0]
|
|
18
|
+
resolved = resolve_executable(binary, env=base_env)
|
|
19
|
+
candidate: Optional[Path] = Path(resolved) if resolved else None
|
|
20
|
+
if candidate is None:
|
|
21
|
+
candidate = Path(binary).expanduser()
|
|
22
|
+
if not candidate.is_absolute():
|
|
23
|
+
candidate = (cwd / candidate).resolve()
|
|
24
|
+
if candidate.exists():
|
|
25
|
+
extra_paths.append(str(candidate.parent))
|
|
26
|
+
return subprocess_env(extra_paths=extra_paths, base_env=base_env)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def seed_codex_home(
|
|
30
|
+
codex_home: Path,
|
|
31
|
+
*,
|
|
32
|
+
logger: Any = None,
|
|
33
|
+
event_prefix: str = "app_server",
|
|
34
|
+
) -> None:
|
|
35
|
+
logger = logger or __import__("logging").getLogger(__name__)
|
|
36
|
+
auth_path = codex_home / "auth.json"
|
|
37
|
+
source_root = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
|
38
|
+
if source_root.resolve() == codex_home.resolve():
|
|
39
|
+
return
|
|
40
|
+
source_auth = source_root / "auth.json"
|
|
41
|
+
if auth_path.exists():
|
|
42
|
+
if auth_path.is_symlink() and auth_path.resolve() == source_auth.resolve():
|
|
43
|
+
return
|
|
44
|
+
log_event(
|
|
45
|
+
logger,
|
|
46
|
+
__import__("logging").INFO,
|
|
47
|
+
f"{event_prefix}.codex_home.seed.skipped",
|
|
48
|
+
reason="auth_exists",
|
|
49
|
+
source=str(source_root),
|
|
50
|
+
target=str(codex_home),
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
if not source_root.exists():
|
|
54
|
+
log_event(
|
|
55
|
+
logger,
|
|
56
|
+
__import__("logging").WARNING,
|
|
57
|
+
f"{event_prefix}.codex_home.seed.skipped",
|
|
58
|
+
reason="source_missing",
|
|
59
|
+
source=str(source_root),
|
|
60
|
+
target=str(codex_home),
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
if not source_auth.exists():
|
|
64
|
+
log_event(
|
|
65
|
+
logger,
|
|
66
|
+
__import__("logging").WARNING,
|
|
67
|
+
f"{event_prefix}.codex_home.seed.skipped",
|
|
68
|
+
reason="auth_missing",
|
|
69
|
+
source=str(source_root),
|
|
70
|
+
target=str(codex_home),
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
try:
|
|
74
|
+
auth_path.symlink_to(source_auth)
|
|
75
|
+
log_event(
|
|
76
|
+
logger,
|
|
77
|
+
__import__("logging").INFO,
|
|
78
|
+
f"{event_prefix}.codex_home.seeded",
|
|
79
|
+
source=str(source_root),
|
|
80
|
+
target=str(codex_home),
|
|
81
|
+
)
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
log_event(
|
|
84
|
+
logger,
|
|
85
|
+
__import__("logging").WARNING,
|
|
86
|
+
f"{event_prefix}.codex_home.seed.failed",
|
|
87
|
+
exc=exc,
|
|
88
|
+
source=str(source_root),
|
|
89
|
+
target=str(codex_home),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_app_server_env(
|
|
94
|
+
command: Sequence[str],
|
|
95
|
+
workspace_root: Path,
|
|
96
|
+
state_dir: Path,
|
|
97
|
+
*,
|
|
98
|
+
logger: Any = None,
|
|
99
|
+
event_prefix: str = "app_server",
|
|
100
|
+
base_env: Optional[dict[str, str]] = None,
|
|
101
|
+
) -> dict[str, str]:
|
|
102
|
+
env = app_server_env(command, workspace_root, base_env=base_env)
|
|
103
|
+
codex_home = state_dir / "codex_home"
|
|
104
|
+
codex_home.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
seed_codex_home(codex_home, logger=logger, event_prefix=event_prefix)
|
|
106
|
+
env["CODEX_HOME"] = str(codex_home)
|
|
107
|
+
return env
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_turn_id(payload: Any) -> Optional[str]:
|
|
111
|
+
if not isinstance(payload, dict):
|
|
112
|
+
return None
|
|
113
|
+
for key in ("turnId", "turn_id", "id"):
|
|
114
|
+
value = payload.get(key)
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
return value
|
|
117
|
+
turn = payload.get("turn")
|
|
118
|
+
if isinstance(turn, dict):
|
|
119
|
+
for key in ("id", "turnId", "turn_id"):
|
|
120
|
+
value = turn.get(key)
|
|
121
|
+
if isinstance(value, str):
|
|
122
|
+
return value
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _extract_thread_id_from_container(payload: Any) -> Optional[str]:
|
|
127
|
+
if not isinstance(payload, dict):
|
|
128
|
+
return None
|
|
129
|
+
for key in ("threadId", "thread_id"):
|
|
130
|
+
value = payload.get(key)
|
|
131
|
+
if isinstance(value, str):
|
|
132
|
+
return value
|
|
133
|
+
thread = payload.get("thread")
|
|
134
|
+
if isinstance(thread, dict):
|
|
135
|
+
for key in ("id", "threadId", "thread_id"):
|
|
136
|
+
value = thread.get(key)
|
|
137
|
+
if isinstance(value, str):
|
|
138
|
+
return value
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _extract_thread_id_for_turn(payload: Any) -> Optional[str]:
|
|
143
|
+
if not isinstance(payload, dict):
|
|
144
|
+
return None
|
|
145
|
+
for candidate in (payload, payload.get("turn"), payload.get("item")):
|
|
146
|
+
thread_id = _extract_thread_id_from_container(candidate)
|
|
147
|
+
if thread_id:
|
|
148
|
+
return thread_id
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _extract_thread_id(payload: Any) -> Optional[str]:
|
|
153
|
+
if not isinstance(payload, dict):
|
|
154
|
+
return None
|
|
155
|
+
for key in ("threadId", "thread_id", "id"):
|
|
156
|
+
value = payload.get(key)
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
return value
|
|
159
|
+
thread = payload.get("thread")
|
|
160
|
+
if isinstance(thread, dict):
|
|
161
|
+
for key in ("id", "threadId", "thread_id"):
|
|
162
|
+
value = thread.get(key)
|
|
163
|
+
if isinstance(value, str):
|
|
164
|
+
return value
|
|
165
|
+
return None
|