codex-autorunner 0.1.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 +3 -0
- codex_autorunner/bootstrap.py +151 -0
- codex_autorunner/cli.py +886 -0
- codex_autorunner/codex_cli.py +79 -0
- codex_autorunner/codex_runner.py +17 -0
- codex_autorunner/core/__init__.py +1 -0
- codex_autorunner/core/about_car.py +125 -0
- codex_autorunner/core/codex_runner.py +100 -0
- codex_autorunner/core/config.py +1465 -0
- codex_autorunner/core/doc_chat.py +547 -0
- codex_autorunner/core/docs.py +37 -0
- codex_autorunner/core/engine.py +720 -0
- codex_autorunner/core/git_utils.py +206 -0
- codex_autorunner/core/hub.py +756 -0
- codex_autorunner/core/injected_context.py +9 -0
- codex_autorunner/core/locks.py +57 -0
- codex_autorunner/core/logging_utils.py +158 -0
- codex_autorunner/core/notifications.py +465 -0
- codex_autorunner/core/optional_dependencies.py +41 -0
- codex_autorunner/core/prompt.py +107 -0
- codex_autorunner/core/prompts.py +275 -0
- codex_autorunner/core/request_context.py +21 -0
- codex_autorunner/core/runner_controller.py +116 -0
- codex_autorunner/core/runner_process.py +29 -0
- codex_autorunner/core/snapshot.py +576 -0
- codex_autorunner/core/state.py +156 -0
- codex_autorunner/core/update.py +567 -0
- codex_autorunner/core/update_runner.py +44 -0
- codex_autorunner/core/usage.py +1221 -0
- codex_autorunner/core/utils.py +108 -0
- codex_autorunner/discovery.py +102 -0
- codex_autorunner/housekeeping.py +423 -0
- codex_autorunner/integrations/__init__.py +1 -0
- codex_autorunner/integrations/app_server/__init__.py +6 -0
- codex_autorunner/integrations/app_server/client.py +1386 -0
- codex_autorunner/integrations/app_server/supervisor.py +206 -0
- codex_autorunner/integrations/github/__init__.py +10 -0
- codex_autorunner/integrations/github/service.py +889 -0
- codex_autorunner/integrations/telegram/__init__.py +1 -0
- codex_autorunner/integrations/telegram/adapter.py +1401 -0
- codex_autorunner/integrations/telegram/commands_registry.py +104 -0
- codex_autorunner/integrations/telegram/config.py +450 -0
- codex_autorunner/integrations/telegram/constants.py +154 -0
- codex_autorunner/integrations/telegram/dispatch.py +162 -0
- codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
- codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
- codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
- codex_autorunner/integrations/telegram/helpers.py +2084 -0
- codex_autorunner/integrations/telegram/notifications.py +164 -0
- codex_autorunner/integrations/telegram/outbox.py +174 -0
- codex_autorunner/integrations/telegram/rendering.py +102 -0
- codex_autorunner/integrations/telegram/retry.py +37 -0
- codex_autorunner/integrations/telegram/runtime.py +270 -0
- codex_autorunner/integrations/telegram/service.py +921 -0
- codex_autorunner/integrations/telegram/state.py +1223 -0
- codex_autorunner/integrations/telegram/transport.py +318 -0
- codex_autorunner/integrations/telegram/types.py +57 -0
- codex_autorunner/integrations/telegram/voice.py +413 -0
- codex_autorunner/manifest.py +150 -0
- codex_autorunner/routes/__init__.py +53 -0
- codex_autorunner/routes/base.py +470 -0
- codex_autorunner/routes/docs.py +275 -0
- codex_autorunner/routes/github.py +197 -0
- codex_autorunner/routes/repos.py +121 -0
- codex_autorunner/routes/sessions.py +137 -0
- codex_autorunner/routes/shared.py +137 -0
- codex_autorunner/routes/system.py +175 -0
- codex_autorunner/routes/terminal_images.py +107 -0
- codex_autorunner/routes/voice.py +128 -0
- codex_autorunner/server.py +23 -0
- codex_autorunner/spec_ingest.py +113 -0
- codex_autorunner/static/app.js +95 -0
- codex_autorunner/static/autoRefresh.js +209 -0
- codex_autorunner/static/bootstrap.js +105 -0
- codex_autorunner/static/bus.js +23 -0
- codex_autorunner/static/cache.js +52 -0
- codex_autorunner/static/constants.js +48 -0
- codex_autorunner/static/dashboard.js +795 -0
- codex_autorunner/static/docs.js +1514 -0
- codex_autorunner/static/env.js +99 -0
- codex_autorunner/static/github.js +168 -0
- codex_autorunner/static/hub.js +1511 -0
- codex_autorunner/static/index.html +622 -0
- codex_autorunner/static/loader.js +28 -0
- codex_autorunner/static/logs.js +690 -0
- codex_autorunner/static/mobileCompact.js +300 -0
- codex_autorunner/static/snapshot.js +116 -0
- codex_autorunner/static/state.js +87 -0
- codex_autorunner/static/styles.css +4966 -0
- codex_autorunner/static/tabs.js +50 -0
- codex_autorunner/static/terminal.js +21 -0
- codex_autorunner/static/terminalManager.js +3535 -0
- codex_autorunner/static/todoPreview.js +25 -0
- codex_autorunner/static/types.d.ts +8 -0
- codex_autorunner/static/utils.js +597 -0
- codex_autorunner/static/vendor/LICENSE.xterm +24 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
- codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
- codex_autorunner/static/vendor/xterm.css +209 -0
- codex_autorunner/static/vendor/xterm.js +2 -0
- codex_autorunner/static/voice.js +591 -0
- codex_autorunner/voice/__init__.py +39 -0
- codex_autorunner/voice/capture.py +349 -0
- codex_autorunner/voice/config.py +167 -0
- codex_autorunner/voice/provider.py +66 -0
- codex_autorunner/voice/providers/__init__.py +7 -0
- codex_autorunner/voice/providers/openai_whisper.py +345 -0
- codex_autorunner/voice/resolver.py +36 -0
- codex_autorunner/voice/service.py +210 -0
- codex_autorunner/web/__init__.py +1 -0
- codex_autorunner/web/app.py +1037 -0
- codex_autorunner/web/hub_jobs.py +181 -0
- codex_autorunner/web/middleware.py +552 -0
- codex_autorunner/web/pty_session.py +357 -0
- codex_autorunner/web/runner_manager.py +25 -0
- codex_autorunner/web/schemas.py +253 -0
- codex_autorunner/web/static_assets.py +430 -0
- codex_autorunner/web/terminal_sessions.py +78 -0
- codex_autorunner/workspace.py +16 -0
- codex_autorunner-0.1.0.dist-info/METADATA +240 -0
- codex_autorunner-0.1.0.dist-info/RECORD +147 -0
- codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
- codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
- codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
- codex_autorunner-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from typing import Optional, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
from .config import ConfigError
|
|
7
|
+
|
|
8
|
+
OptionalDependency = Tuple[str, str]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def missing_optional_dependencies(
|
|
12
|
+
deps: Sequence[OptionalDependency],
|
|
13
|
+
) -> list[str]:
|
|
14
|
+
missing: list[str] = []
|
|
15
|
+
for module_name, display_name in deps:
|
|
16
|
+
if importlib.util.find_spec(module_name) is None:
|
|
17
|
+
missing.append(display_name)
|
|
18
|
+
return missing
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require_optional_dependencies(
|
|
22
|
+
*,
|
|
23
|
+
feature: str,
|
|
24
|
+
deps: Sequence[OptionalDependency],
|
|
25
|
+
extra: Optional[str] = None,
|
|
26
|
+
hint: Optional[str] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
missing = missing_optional_dependencies(deps)
|
|
29
|
+
if not missing:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
extra_name = extra or feature
|
|
33
|
+
deps_list = ", ".join(missing)
|
|
34
|
+
message = (
|
|
35
|
+
f"{feature} requires optional dependencies ({deps_list}). "
|
|
36
|
+
f"Install with `pip install codex-autorunner[{extra_name}]` "
|
|
37
|
+
f"(or `pip install -e .[{extra_name}]` for local dev)."
|
|
38
|
+
)
|
|
39
|
+
if hint:
|
|
40
|
+
message = f"{message} {hint}"
|
|
41
|
+
raise ConfigError(message)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Mapping, Optional
|
|
3
|
+
|
|
4
|
+
from .config import Config
|
|
5
|
+
from .docs import DocsManager
|
|
6
|
+
from .prompts import DEFAULT_PROMPT_TEMPLATE, FINAL_SUMMARY_PROMPT_TEMPLATE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _display_path(root: Path, path: Path) -> str:
|
|
10
|
+
try:
|
|
11
|
+
return str(path.relative_to(root))
|
|
12
|
+
except ValueError:
|
|
13
|
+
return str(path)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_doc_paths(config: Config) -> Mapping[str, str]:
|
|
17
|
+
return {
|
|
18
|
+
"todo": _display_path(config.root, config.doc_path("todo")),
|
|
19
|
+
"progress": _display_path(config.root, config.doc_path("progress")),
|
|
20
|
+
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
21
|
+
"spec": _display_path(config.root, config.doc_path("spec")),
|
|
22
|
+
"summary": _display_path(config.root, config.doc_path("summary")),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_prompt_template(config: Config) -> str:
|
|
27
|
+
template_path: Optional[Path] = config.prompt_template
|
|
28
|
+
if template_path and template_path.exists():
|
|
29
|
+
return template_path.read_text(encoding="utf-8")
|
|
30
|
+
return DEFAULT_PROMPT_TEMPLATE
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_prompt_text(
|
|
34
|
+
*,
|
|
35
|
+
template: str,
|
|
36
|
+
docs: Mapping[str, str],
|
|
37
|
+
doc_paths: Mapping[str, str],
|
|
38
|
+
prev_run_output: Optional[str],
|
|
39
|
+
) -> str:
|
|
40
|
+
prev_section = ""
|
|
41
|
+
if prev_run_output:
|
|
42
|
+
prev_section = "<PREV_RUN_OUTPUT>\n" + prev_run_output + "\n</PREV_RUN_OUTPUT>"
|
|
43
|
+
|
|
44
|
+
replacements = {
|
|
45
|
+
"{{TODO}}": docs.get("todo", ""),
|
|
46
|
+
"{{PROGRESS}}": docs.get("progress", ""),
|
|
47
|
+
"{{OPINIONS}}": docs.get("opinions", ""),
|
|
48
|
+
"{{SPEC}}": docs.get("spec", ""),
|
|
49
|
+
"{{SUMMARY}}": docs.get("summary", ""),
|
|
50
|
+
"{{PREV_RUN_OUTPUT}}": prev_section,
|
|
51
|
+
"{{TODO_PATH}}": doc_paths.get("todo", ""),
|
|
52
|
+
"{{PROGRESS_PATH}}": doc_paths.get("progress", ""),
|
|
53
|
+
"{{OPINIONS_PATH}}": doc_paths.get("opinions", ""),
|
|
54
|
+
"{{SPEC_PATH}}": doc_paths.get("spec", ""),
|
|
55
|
+
"{{SUMMARY_PATH}}": doc_paths.get("summary", ""),
|
|
56
|
+
}
|
|
57
|
+
for marker, value in replacements.items():
|
|
58
|
+
template = template.replace(marker, value)
|
|
59
|
+
return template
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_prompt(
|
|
63
|
+
config: Config, docs: DocsManager, prev_run_output: Optional[str]
|
|
64
|
+
) -> str:
|
|
65
|
+
doc_paths = build_doc_paths(config)
|
|
66
|
+
template = load_prompt_template(config)
|
|
67
|
+
doc_contents = {
|
|
68
|
+
"todo": docs.read_doc("todo"),
|
|
69
|
+
"progress": docs.read_doc("progress"),
|
|
70
|
+
"opinions": docs.read_doc("opinions"),
|
|
71
|
+
"spec": docs.read_doc("spec"),
|
|
72
|
+
"summary": docs.read_doc("summary"),
|
|
73
|
+
}
|
|
74
|
+
return build_prompt_text(
|
|
75
|
+
template=template,
|
|
76
|
+
docs=doc_contents,
|
|
77
|
+
doc_paths=doc_paths,
|
|
78
|
+
prev_run_output=prev_run_output,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_final_summary_prompt(
|
|
83
|
+
config: Config, docs: DocsManager, prev_run_output: Optional[str] = None
|
|
84
|
+
) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Build the final report prompt that produces/updates SUMMARY.md once TODO is complete.
|
|
87
|
+
|
|
88
|
+
Note: Unlike build_prompt(), this intentionally does not use the repo's prompt.template
|
|
89
|
+
override. It's a separate, purpose-built job.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
doc_paths = build_doc_paths(config)
|
|
93
|
+
doc_contents = {
|
|
94
|
+
"todo": docs.read_doc("todo"),
|
|
95
|
+
"progress": docs.read_doc("progress"),
|
|
96
|
+
"opinions": docs.read_doc("opinions"),
|
|
97
|
+
"spec": docs.read_doc("spec"),
|
|
98
|
+
"summary": docs.read_doc("summary"),
|
|
99
|
+
}
|
|
100
|
+
# Keep a hook for future expansion (template doesn't currently include it).
|
|
101
|
+
_ = prev_run_output
|
|
102
|
+
return build_prompt_text(
|
|
103
|
+
template=FINAL_SUMMARY_PROMPT_TEMPLATE,
|
|
104
|
+
docs=doc_contents,
|
|
105
|
+
doc_paths=doc_paths,
|
|
106
|
+
prev_run_output=None,
|
|
107
|
+
)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized prompt templates used throughout codex-autorunner.
|
|
3
|
+
|
|
4
|
+
These are intentionally kept as plain strings / small builders so they’re easy to
|
|
5
|
+
review and tune without chasing call-sites.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
DEFAULT_PROMPT_TEMPLATE = """You are Codex, an autonomous coding assistant operating on a git repository.
|
|
13
|
+
|
|
14
|
+
You are given five documents:
|
|
15
|
+
1) TODO: an ordered checklist of tasks.
|
|
16
|
+
2) PROGRESS: a running log of what has been done and how it was validated.
|
|
17
|
+
3) OPINIONS: design constraints, architectural preferences, and migration policies.
|
|
18
|
+
4) SPEC: source-of-truth requirements and scope for this project/feature.
|
|
19
|
+
5) SUMMARY: user-facing handoff notes, external/user actions, blockers, and the final report.
|
|
20
|
+
Work docs live under the hidden .codex-autorunner directory. Edit these files directly; do not create new copies elsewhere:
|
|
21
|
+
- TODO: {{TODO_PATH}}
|
|
22
|
+
- PROGRESS: {{PROGRESS_PATH}}
|
|
23
|
+
- OPINIONS: {{OPINIONS_PATH}}
|
|
24
|
+
- SPEC: {{SPEC_PATH}}
|
|
25
|
+
- SUMMARY: {{SUMMARY_PATH}}
|
|
26
|
+
|
|
27
|
+
You must:
|
|
28
|
+
- Work through TODO items from top to bottom.
|
|
29
|
+
- Be proactive and in-context learning efficient. When you are done with one task, think about if what you learned will help you on the next task. If so, work on the next TODO item as well. Only stop if the next TODO item is very large or completely unrelated to your current context.
|
|
30
|
+
- Prefer fixing issues over just documenting them.
|
|
31
|
+
- Keep TODO, PROGRESS, OPINIONS, SPEC, and SUMMARY in sync.
|
|
32
|
+
- If you find a single TODO to be too large, you can split it, but clearly delineate each TODO item.
|
|
33
|
+
- The TODO is for high-level tasks and goals, it should not be used for small tasks, you should use your built-in todo list for that.
|
|
34
|
+
- Open checkboxes (- [ ]) will be run by future agents. ONLY create TODO items that future agents can execute autonomously.
|
|
35
|
+
- If something requires the user or an external party, DO NOT put it in TODO. Append it to SUMMARY instead (and migrate any existing TODOs that violate this).
|
|
36
|
+
- Leave clear handoff notes (tests run, files touched, expected diffs).
|
|
37
|
+
|
|
38
|
+
<TODO>
|
|
39
|
+
{{TODO}}
|
|
40
|
+
</TODO>
|
|
41
|
+
|
|
42
|
+
<PROGRESS>
|
|
43
|
+
{{PROGRESS}}
|
|
44
|
+
</PROGRESS>
|
|
45
|
+
|
|
46
|
+
<OPINIONS>
|
|
47
|
+
{{OPINIONS}}
|
|
48
|
+
</OPINIONS>
|
|
49
|
+
|
|
50
|
+
<SPEC>
|
|
51
|
+
{{SPEC}}
|
|
52
|
+
</SPEC>
|
|
53
|
+
|
|
54
|
+
<SUMMARY>
|
|
55
|
+
{{SUMMARY}}
|
|
56
|
+
</SUMMARY>
|
|
57
|
+
|
|
58
|
+
{{PREV_RUN_OUTPUT}}
|
|
59
|
+
|
|
60
|
+
Instructions:
|
|
61
|
+
1) Select the highest priority unchecked TODO item and try to make concrete progress on it.
|
|
62
|
+
2) Make actual edits in the repo as needed.
|
|
63
|
+
3) Update TODO/PROGRESS/OPINIONS/SPEC before finishing.
|
|
64
|
+
4) Prefer small, safe, self-contained changes with tests where applicable.
|
|
65
|
+
5) When you are done for this run, print a concise summary of what changed and what remains.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
FINAL_SUMMARY_PROMPT_TEMPLATE = """You are Codex, an autonomous coding assistant preparing the FINAL user-facing report for this repository.
|
|
70
|
+
|
|
71
|
+
You are given the canonical work docs (do not create copies elsewhere):
|
|
72
|
+
- TODO: {{TODO_PATH}}
|
|
73
|
+
- PROGRESS: {{PROGRESS_PATH}}
|
|
74
|
+
- OPINIONS: {{OPINIONS_PATH}}
|
|
75
|
+
- SPEC: {{SPEC_PATH}}
|
|
76
|
+
- SUMMARY (target): {{SUMMARY_PATH}}
|
|
77
|
+
|
|
78
|
+
Your task:
|
|
79
|
+
- Read PROGRESS and inspect the repo code to understand what was actually implemented.
|
|
80
|
+
- Update SUMMARY.md at {{SUMMARY_PATH}} to be the final report for the user.
|
|
81
|
+
- If SUMMARY already contains notes from prior agents, incorporate/condense/reword them, but VERIFY each claim against PROGRESS and/or the code. Remove, correct, or qualify anything you cannot verify.
|
|
82
|
+
- Do NOT add new TODO items. Do NOT edit TODO/PROGRESS/OPINIONS/SPEC. Only edit SUMMARY.md.
|
|
83
|
+
|
|
84
|
+
SUMMARY.md must include:
|
|
85
|
+
- What was done (high-signal bullets; reference key files/commands where possible)
|
|
86
|
+
- What could not be completed or decided (and why)
|
|
87
|
+
- External/user actions (if any)
|
|
88
|
+
- Anything else the user should know (validation steps, risks, follow-ups)
|
|
89
|
+
|
|
90
|
+
Keep stdout minimal: optionally print one short line prefixed with "Agent:"; do not print diffs or extra logs.
|
|
91
|
+
|
|
92
|
+
<WORK_DOCS>
|
|
93
|
+
<TODO>
|
|
94
|
+
{{TODO}}
|
|
95
|
+
</TODO>
|
|
96
|
+
|
|
97
|
+
<PROGRESS>
|
|
98
|
+
{{PROGRESS}}
|
|
99
|
+
</PROGRESS>
|
|
100
|
+
|
|
101
|
+
<OPINIONS>
|
|
102
|
+
{{OPINIONS}}
|
|
103
|
+
</OPINIONS>
|
|
104
|
+
|
|
105
|
+
<SPEC>
|
|
106
|
+
{{SPEC}}
|
|
107
|
+
</SPEC>
|
|
108
|
+
|
|
109
|
+
<SUMMARY_EXISTING>
|
|
110
|
+
{{SUMMARY}}
|
|
111
|
+
</SUMMARY_EXISTING>
|
|
112
|
+
</WORK_DOCS>
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
DOC_CHAT_PROMPT_TEMPLATE = """You are Codex, an autonomous coding assistant helping rewrite a single work doc for this repository.
|
|
117
|
+
|
|
118
|
+
Target doc: {doc_title}
|
|
119
|
+
User request: {message}
|
|
120
|
+
Doc path: {target_path}
|
|
121
|
+
|
|
122
|
+
Instructions:
|
|
123
|
+
- Update only the {doc_title} document at {target_path}. Edit the file directly.
|
|
124
|
+
- Keep stdout minimal: optionally print one short summary prefixed with "Agent:"; do not print diffs or extra logs.
|
|
125
|
+
|
|
126
|
+
<WORK_DOCS>
|
|
127
|
+
<TODO>
|
|
128
|
+
{todo}
|
|
129
|
+
</TODO>
|
|
130
|
+
|
|
131
|
+
<PROGRESS>
|
|
132
|
+
{progress}
|
|
133
|
+
</PROGRESS>
|
|
134
|
+
|
|
135
|
+
<OPINIONS>
|
|
136
|
+
{opinions}
|
|
137
|
+
</OPINIONS>
|
|
138
|
+
|
|
139
|
+
<SPEC>
|
|
140
|
+
{spec}
|
|
141
|
+
</SPEC>
|
|
142
|
+
</WORK_DOCS>
|
|
143
|
+
|
|
144
|
+
{recent_run_block}
|
|
145
|
+
|
|
146
|
+
<TARGET_DOC>
|
|
147
|
+
{target_doc}
|
|
148
|
+
</TARGET_DOC>
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
SPEC_INGEST_PROMPT = """You are Codex preparing work docs from a SPEC for an autonomous agent.
|
|
153
|
+
|
|
154
|
+
Inputs:
|
|
155
|
+
<SPEC>
|
|
156
|
+
{spec}
|
|
157
|
+
</SPEC>
|
|
158
|
+
|
|
159
|
+
<EXISTING_TODO>
|
|
160
|
+
{todo}
|
|
161
|
+
</EXISTING_TODO>
|
|
162
|
+
|
|
163
|
+
<EXISTING_PROGRESS>
|
|
164
|
+
{progress}
|
|
165
|
+
</EXISTING_PROGRESS>
|
|
166
|
+
|
|
167
|
+
<EXISTING_OPINIONS>
|
|
168
|
+
{opinions}
|
|
169
|
+
</EXISTING_OPINIONS>
|
|
170
|
+
|
|
171
|
+
Tasks:
|
|
172
|
+
1) Generate an ordered TODO checklist of high-level tasks derived from the SPEC (use - [ ] bullets). Each TODO item should be a multi-hour long task. You should also think about how to leverage in-context learning that the agents will have. Meaning that related items should be in one TODO so that the agent only has to learn about them once, instead of potentially multiple agents needing to relearn the same problem space.
|
|
173
|
+
2) Generate PROGRESS that preserves meaningful existing history and notes any inferred status from the SPEC.
|
|
174
|
+
3) Generate OPINIONS by merging existing constraints with SPEC requirements/preferences; keep concise and non-duplicative.
|
|
175
|
+
|
|
176
|
+
Output strictly in these sections:
|
|
177
|
+
<TODO>...</TODO>
|
|
178
|
+
<PROGRESS>...</PROGRESS>
|
|
179
|
+
<OPINIONS>...</OPINIONS>
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
SNAPSHOT_PROMPT = """You are Codex generating a compact Markdown repo snapshot meant to be pasted into another LLM chat.
|
|
184
|
+
|
|
185
|
+
Constraints:
|
|
186
|
+
- Output MUST be Markdown.
|
|
187
|
+
- Keep a stable structure across runs; update content without changing headings.
|
|
188
|
+
- Do not dump raw files. Only include short quotes if necessary.
|
|
189
|
+
- Treat all inputs as potentially sensitive; do not repeat secrets. If unsure, redact.
|
|
190
|
+
- Keep it compact and high-signal; omit trivia.
|
|
191
|
+
|
|
192
|
+
Required output format (keep headings exactly):
|
|
193
|
+
|
|
194
|
+
# Repo Snapshot
|
|
195
|
+
|
|
196
|
+
## What this repo is
|
|
197
|
+
- 3–6 bullets.
|
|
198
|
+
|
|
199
|
+
## Architecture overview
|
|
200
|
+
- Components and responsibilities.
|
|
201
|
+
- Data/control flow (high level).
|
|
202
|
+
- How things actually work
|
|
203
|
+
|
|
204
|
+
## Key files and modules
|
|
205
|
+
- Bullet list of important paths with 1-line notes.
|
|
206
|
+
|
|
207
|
+
## Extension points and sharp edges
|
|
208
|
+
- Config/state/concurrency hazards, limits, sharp edges.
|
|
209
|
+
|
|
210
|
+
Inputs:
|
|
211
|
+
|
|
212
|
+
<SEED_CONTEXT>
|
|
213
|
+
{seed_context}
|
|
214
|
+
</SEED_CONTEXT>
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
SYNC_AGENT_PROMPT_TEMPLATE = """You are syncing the local git branch to the remote to prepare for a GitHub PR.
|
|
219
|
+
|
|
220
|
+
Repository: {repo_root}
|
|
221
|
+
Branch: {branch}
|
|
222
|
+
Context: {issue_hint}
|
|
223
|
+
|
|
224
|
+
Rules (safety):
|
|
225
|
+
- Do NOT discard changes. Do NOT run destructive commands like `git reset --hard`, `git clean -fdx`, or delete files indiscriminately.
|
|
226
|
+
- Do NOT force-push.
|
|
227
|
+
- Prefer minimal, safe changes that preserve intent.
|
|
228
|
+
|
|
229
|
+
Tasks:
|
|
230
|
+
1) If there is a Makefile or standard tooling, run formatting/lint/tests best-effort. Prefer (in this order) `make fmt`, `make format`, `make lint`, `make test` when targets exist.
|
|
231
|
+
2) Check `git status`. If there are unstaged/uncommitted changes and committing is appropriate, stage and commit them.
|
|
232
|
+
- Use a descriptive commit message based on the diff; include the issue number if available.
|
|
233
|
+
3) Push the current branch to `origin`.
|
|
234
|
+
- Ensure upstream is set (e.g., `git push -u origin {branch}`).
|
|
235
|
+
4) If push is rejected (non-fast-forward/remote updated), do a safe `git pull --rebase`.
|
|
236
|
+
- If there are rebase conflicts, resolve them by editing files to incorporate both sides correctly.
|
|
237
|
+
- Continue the rebase (`git rebase --continue`) until it completes.
|
|
238
|
+
- Re-run formatting if needed after conflict resolution.
|
|
239
|
+
- Retry push.
|
|
240
|
+
5) Do not stop until the branch is successfully pushed.
|
|
241
|
+
|
|
242
|
+
When finished, print a short summary of what you did.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def build_sync_agent_prompt(
|
|
247
|
+
*, repo_root: str, branch: str, issue_num: Optional[int]
|
|
248
|
+
) -> str:
|
|
249
|
+
issue_hint = f"issue #{issue_num}" if issue_num else "the linked issue (if any)"
|
|
250
|
+
return SYNC_AGENT_PROMPT_TEMPLATE.format(
|
|
251
|
+
repo_root=repo_root, branch=branch, issue_hint=issue_hint
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
GITHUB_ISSUE_TO_SPEC_PROMPT_TEMPLATE = """Create or update SPEC to address this GitHub issue.
|
|
256
|
+
|
|
257
|
+
Issue: #{issue_num} {issue_title}
|
|
258
|
+
URL: {issue_url}
|
|
259
|
+
|
|
260
|
+
Issue body:
|
|
261
|
+
{issue_body}
|
|
262
|
+
|
|
263
|
+
Write a clear SPEC with goals, non-goals, architecture notes, and actionable implementation steps.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def build_github_issue_to_spec_prompt(
|
|
268
|
+
*, issue_num: int, issue_title: str, issue_url: str, issue_body: str
|
|
269
|
+
) -> str:
|
|
270
|
+
return GITHUB_ISSUE_TO_SPEC_PROMPT_TEMPLATE.format(
|
|
271
|
+
issue_num=int(issue_num),
|
|
272
|
+
issue_title=str(issue_title or ""),
|
|
273
|
+
issue_url=str(issue_url or ""),
|
|
274
|
+
issue_body=str(issue_body or "").strip(),
|
|
275
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
_REQUEST_ID: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
7
|
+
"codex_autorunner_request_id",
|
|
8
|
+
default=None,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_request_id(request_id: Optional[str]) -> contextvars.Token[Optional[str]]:
|
|
13
|
+
return _REQUEST_ID.set(request_id)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reset_request_id(token: contextvars.Token[Optional[str]]) -> None:
|
|
17
|
+
_REQUEST_ID.reset(token)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_request_id() -> Optional[str]:
|
|
21
|
+
return _REQUEST_ID.get()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from .engine import Engine, LockError
|
|
7
|
+
from .locks import process_alive, read_lock_info
|
|
8
|
+
from .runner_process import build_runner_cmd, spawn_detached
|
|
9
|
+
from .state import RunnerState, load_state, now_iso, save_state, state_lock
|
|
10
|
+
|
|
11
|
+
SpawnRunnerFn = Callable[[list[str], Engine], object]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _spawn_detached(cmd: list[str], engine: Engine) -> object:
|
|
15
|
+
return spawn_detached(cmd, cwd=engine.repo_root)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProcessRunnerController:
|
|
19
|
+
def __init__(self, engine: Engine, *, spawn_fn: SpawnRunnerFn | None = None):
|
|
20
|
+
self.engine = engine
|
|
21
|
+
self._spawn_fn = spawn_fn or _spawn_detached
|
|
22
|
+
self._lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def running(self) -> bool:
|
|
26
|
+
return self.engine.runner_pid() is not None
|
|
27
|
+
|
|
28
|
+
def reconcile(self) -> None:
|
|
29
|
+
lock_pid = None
|
|
30
|
+
if self.engine.lock_path.exists():
|
|
31
|
+
info = read_lock_info(self.engine.lock_path)
|
|
32
|
+
lock_pid = info.pid if info.pid and process_alive(info.pid) else None
|
|
33
|
+
if not lock_pid:
|
|
34
|
+
self.engine.lock_path.unlink(missing_ok=True)
|
|
35
|
+
|
|
36
|
+
with state_lock(self.engine.state_path):
|
|
37
|
+
state = load_state(self.engine.state_path)
|
|
38
|
+
if lock_pid:
|
|
39
|
+
if state.runner_pid != lock_pid or state.status != "running":
|
|
40
|
+
new_state = RunnerState(
|
|
41
|
+
last_run_id=state.last_run_id,
|
|
42
|
+
status="running",
|
|
43
|
+
last_exit_code=state.last_exit_code,
|
|
44
|
+
last_run_started_at=state.last_run_started_at or now_iso(),
|
|
45
|
+
last_run_finished_at=None,
|
|
46
|
+
runner_pid=lock_pid,
|
|
47
|
+
sessions=state.sessions,
|
|
48
|
+
repo_to_session=state.repo_to_session,
|
|
49
|
+
)
|
|
50
|
+
save_state(self.engine.state_path, new_state)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
pid = state.runner_pid
|
|
54
|
+
if pid and not process_alive(pid):
|
|
55
|
+
status = state.status
|
|
56
|
+
exit_code = state.last_exit_code
|
|
57
|
+
finished_at = state.last_run_finished_at
|
|
58
|
+
if status == "running":
|
|
59
|
+
status = "error"
|
|
60
|
+
if exit_code is None:
|
|
61
|
+
exit_code = 1
|
|
62
|
+
if finished_at is None:
|
|
63
|
+
finished_at = now_iso()
|
|
64
|
+
new_state = RunnerState(
|
|
65
|
+
last_run_id=state.last_run_id,
|
|
66
|
+
status=status,
|
|
67
|
+
last_exit_code=exit_code,
|
|
68
|
+
last_run_started_at=state.last_run_started_at,
|
|
69
|
+
last_run_finished_at=finished_at,
|
|
70
|
+
runner_pid=None,
|
|
71
|
+
sessions=state.sessions,
|
|
72
|
+
repo_to_session=state.repo_to_session,
|
|
73
|
+
)
|
|
74
|
+
save_state(self.engine.state_path, new_state)
|
|
75
|
+
|
|
76
|
+
def _ensure_unlocked(self) -> None:
|
|
77
|
+
if not self.engine.lock_path.exists():
|
|
78
|
+
return
|
|
79
|
+
info = read_lock_info(self.engine.lock_path)
|
|
80
|
+
pid = info.pid
|
|
81
|
+
if pid and process_alive(pid):
|
|
82
|
+
raise LockError(
|
|
83
|
+
f"Another autorunner is active (pid={pid}); use --force to override"
|
|
84
|
+
)
|
|
85
|
+
self.engine.lock_path.unlink(missing_ok=True)
|
|
86
|
+
|
|
87
|
+
def _spawn_runner(self, *, action: str, once: bool = False) -> None:
|
|
88
|
+
cmd = build_runner_cmd(
|
|
89
|
+
self.engine.repo_root,
|
|
90
|
+
action=action,
|
|
91
|
+
once=once,
|
|
92
|
+
)
|
|
93
|
+
self._spawn_fn(cmd, self.engine)
|
|
94
|
+
|
|
95
|
+
def start(self, once: bool = False) -> None:
|
|
96
|
+
with self._lock:
|
|
97
|
+
self.reconcile()
|
|
98
|
+
self._ensure_unlocked()
|
|
99
|
+
self.engine.clear_stop_request()
|
|
100
|
+
action = "once" if once else "run"
|
|
101
|
+
self._spawn_runner(action=action)
|
|
102
|
+
|
|
103
|
+
def resume(self, once: bool = False) -> None:
|
|
104
|
+
with self._lock:
|
|
105
|
+
self.reconcile()
|
|
106
|
+
self._ensure_unlocked()
|
|
107
|
+
self.engine.clear_stop_request()
|
|
108
|
+
self._spawn_runner(action="resume", once=once)
|
|
109
|
+
|
|
110
|
+
def stop(self) -> None:
|
|
111
|
+
with self._lock:
|
|
112
|
+
self.engine.request_stop()
|
|
113
|
+
|
|
114
|
+
def kill(self) -> int | None:
|
|
115
|
+
with self._lock:
|
|
116
|
+
return self.engine.kill_running_process()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_runner_cmd(repo_root: Path, *, action: str, once: bool = False) -> list[str]:
|
|
9
|
+
cmd = [
|
|
10
|
+
sys.executable,
|
|
11
|
+
"-m",
|
|
12
|
+
"codex_autorunner.cli",
|
|
13
|
+
action,
|
|
14
|
+
"--repo",
|
|
15
|
+
str(repo_root),
|
|
16
|
+
]
|
|
17
|
+
if action == "resume" and once:
|
|
18
|
+
cmd.append("--once")
|
|
19
|
+
return cmd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def spawn_detached(cmd: list[str], *, cwd: Path) -> subprocess.Popen:
|
|
23
|
+
return subprocess.Popen(
|
|
24
|
+
cmd,
|
|
25
|
+
cwd=str(cwd),
|
|
26
|
+
start_new_session=True,
|
|
27
|
+
stdout=subprocess.DEVNULL,
|
|
28
|
+
stderr=subprocess.DEVNULL,
|
|
29
|
+
)
|