codex-autorunner 0.1.2__py3-none-any.whl → 1.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 +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- 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/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- 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 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -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 +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -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 +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -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 +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -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 +417 -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 +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import dataclasses
|
|
3
|
-
import hashlib
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import re
|
|
7
|
-
from datetime import datetime, timezone
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Dict, Iterable, List, Optional, Tuple
|
|
10
|
-
|
|
11
|
-
from ..integrations.app_server.client import CodexAppServerError
|
|
12
|
-
from ..integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
13
|
-
from .app_server_prompts import build_app_server_snapshot_prompt
|
|
14
|
-
from .app_server_threads import (
|
|
15
|
-
AppServerThreadRegistry,
|
|
16
|
-
default_app_server_threads_path,
|
|
17
|
-
)
|
|
18
|
-
from .config import RepoConfig
|
|
19
|
-
from .engine import Engine
|
|
20
|
-
from .git_utils import (
|
|
21
|
-
git_available,
|
|
22
|
-
git_branch,
|
|
23
|
-
git_diff_name_status,
|
|
24
|
-
git_head_sha,
|
|
25
|
-
git_ls_files,
|
|
26
|
-
git_status_porcelain,
|
|
27
|
-
)
|
|
28
|
-
from .utils import atomic_write, read_json
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class SnapshotError(Exception):
|
|
32
|
-
"""Raised when snapshot generation fails."""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _repo_config(engine: Engine) -> RepoConfig:
|
|
36
|
-
if not isinstance(engine.config, RepoConfig):
|
|
37
|
-
raise SnapshotError("Snapshot generation requires a repo workspace config")
|
|
38
|
-
return engine.config
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _now_iso() -> str:
|
|
42
|
-
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _sha256_text(text: str) -> str:
|
|
46
|
-
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _sha256_bytes(blob: bytes) -> str:
|
|
50
|
-
return hashlib.sha256(blob).hexdigest()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
_REDACTIONS: List[Tuple[re.Pattern[str], str]] = [
|
|
54
|
-
# OpenAI-like keys.
|
|
55
|
-
(re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), "sk-[REDACTED]"),
|
|
56
|
-
# GitHub personal access tokens.
|
|
57
|
-
(re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}\b"), "gh_[REDACTED]"),
|
|
58
|
-
# AWS access key ids (best-effort).
|
|
59
|
-
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "AKIA[REDACTED]"),
|
|
60
|
-
# JWT-ish blobs.
|
|
61
|
-
(
|
|
62
|
-
re.compile(
|
|
63
|
-
r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"
|
|
64
|
-
),
|
|
65
|
-
"[JWT_REDACTED]",
|
|
66
|
-
),
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def redact_text(text: str) -> str:
|
|
71
|
-
redacted = text
|
|
72
|
-
for pattern, replacement in _REDACTIONS:
|
|
73
|
-
redacted = pattern.sub(replacement, redacted)
|
|
74
|
-
return redacted
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
_DEFAULT_IGNORED_DIRS = {
|
|
78
|
-
".git",
|
|
79
|
-
".hg",
|
|
80
|
-
".svn",
|
|
81
|
-
"node_modules",
|
|
82
|
-
".venv",
|
|
83
|
-
"venv",
|
|
84
|
-
"dist",
|
|
85
|
-
"build",
|
|
86
|
-
".mypy_cache",
|
|
87
|
-
".pytest_cache",
|
|
88
|
-
".ruff_cache",
|
|
89
|
-
".cache",
|
|
90
|
-
"__pycache__",
|
|
91
|
-
".tox",
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
_SECRET_BASENAMES = {
|
|
96
|
-
".env",
|
|
97
|
-
".env.local",
|
|
98
|
-
"id_rsa",
|
|
99
|
-
"id_ed25519",
|
|
100
|
-
"known_hosts",
|
|
101
|
-
".npmrc",
|
|
102
|
-
".pypirc",
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
_SECRET_EXTS = {".pem", ".key", ".p12", ".pfx", ".kdbx"}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _looks_like_secret_path(path: Path) -> bool:
|
|
109
|
-
name = path.name
|
|
110
|
-
if name in _SECRET_BASENAMES:
|
|
111
|
-
return True
|
|
112
|
-
if name.startswith(".env."):
|
|
113
|
-
return True
|
|
114
|
-
if path.suffix.lower() in _SECRET_EXTS:
|
|
115
|
-
return True
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _is_probably_binary(blob: bytes) -> bool:
|
|
120
|
-
if b"\x00" in blob:
|
|
121
|
-
return True
|
|
122
|
-
# Heuristic: lots of control chars.
|
|
123
|
-
sample = blob[:2048]
|
|
124
|
-
if not sample:
|
|
125
|
-
return False
|
|
126
|
-
control = sum(1 for b in sample if b < 9 or (13 < b < 32))
|
|
127
|
-
return (control / len(sample)) > 0.3
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _iter_files_fs(repo_root: Path, *, max_files: int = 5000) -> List[str]:
|
|
131
|
-
out: List[str] = []
|
|
132
|
-
for root, dirs, files in os.walk(repo_root):
|
|
133
|
-
rel_root = os.path.relpath(root, repo_root)
|
|
134
|
-
if rel_root == ".":
|
|
135
|
-
rel_root = ""
|
|
136
|
-
dirs[:] = [
|
|
137
|
-
d
|
|
138
|
-
for d in sorted(dirs)
|
|
139
|
-
if d not in _DEFAULT_IGNORED_DIRS
|
|
140
|
-
and not (Path(rel_root) / d).parts[:1] == (".git",)
|
|
141
|
-
]
|
|
142
|
-
for f in sorted(files):
|
|
143
|
-
rel = str(Path(rel_root) / f) if rel_root else f
|
|
144
|
-
if _looks_like_secret_path(Path(rel)):
|
|
145
|
-
continue
|
|
146
|
-
out.append(rel)
|
|
147
|
-
if len(out) >= max_files:
|
|
148
|
-
return out
|
|
149
|
-
return out
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _build_tree_outline(
|
|
153
|
-
rel_paths: Iterable[str], *, max_depth: int, max_entries: int
|
|
154
|
-
) -> str:
|
|
155
|
-
paths = [p for p in rel_paths if p and not _looks_like_secret_path(Path(p))]
|
|
156
|
-
paths = sorted(set(paths))
|
|
157
|
-
|
|
158
|
-
shown = 0
|
|
159
|
-
lines: List[str] = []
|
|
160
|
-
last_parts: List[str] = []
|
|
161
|
-
for rel in paths:
|
|
162
|
-
parts = rel.split("/")
|
|
163
|
-
if len(parts) > max_depth:
|
|
164
|
-
parts = parts[:max_depth]
|
|
165
|
-
parts[-1] = parts[-1] + "/…"
|
|
166
|
-
|
|
167
|
-
# Emit minimal directory structure changes based on common prefix.
|
|
168
|
-
common = 0
|
|
169
|
-
for a, b in zip(last_parts, parts):
|
|
170
|
-
if a != b:
|
|
171
|
-
break
|
|
172
|
-
common += 1
|
|
173
|
-
|
|
174
|
-
# Print remaining parts with indentation.
|
|
175
|
-
for idx in range(common, len(parts)):
|
|
176
|
-
name = parts[idx]
|
|
177
|
-
indent = " " * idx
|
|
178
|
-
prefix = "- " if idx == 0 else "- "
|
|
179
|
-
lines.append(f"{indent}{prefix}{name}")
|
|
180
|
-
last_parts = parts
|
|
181
|
-
|
|
182
|
-
shown += 1
|
|
183
|
-
if shown >= max_entries:
|
|
184
|
-
lines.append(f"- … (truncated after {max_entries} entries)")
|
|
185
|
-
break
|
|
186
|
-
return "\n".join(lines)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def _detect_key_files(repo_root: Path) -> List[Path]:
|
|
190
|
-
candidates = [
|
|
191
|
-
"README.md",
|
|
192
|
-
"README.rst",
|
|
193
|
-
"pyproject.toml",
|
|
194
|
-
"package.json",
|
|
195
|
-
"package-lock.json",
|
|
196
|
-
"pnpm-lock.yaml",
|
|
197
|
-
"yarn.lock",
|
|
198
|
-
"requirements.txt",
|
|
199
|
-
"setup.py",
|
|
200
|
-
"Cargo.toml",
|
|
201
|
-
"go.mod",
|
|
202
|
-
"Makefile",
|
|
203
|
-
"Dockerfile",
|
|
204
|
-
".github/workflows",
|
|
205
|
-
"src/codex_autorunner/cli.py",
|
|
206
|
-
"src/codex_autorunner/server.py",
|
|
207
|
-
"src/codex_autorunner/engine.py",
|
|
208
|
-
]
|
|
209
|
-
found: List[Path] = []
|
|
210
|
-
for rel in candidates:
|
|
211
|
-
p = repo_root / rel
|
|
212
|
-
if p.exists():
|
|
213
|
-
found.append(p)
|
|
214
|
-
return found
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@dataclasses.dataclass(frozen=True)
|
|
218
|
-
class SeedContext:
|
|
219
|
-
text: str
|
|
220
|
-
bytes_read: int
|
|
221
|
-
file_hashes: Dict[str, str]
|
|
222
|
-
head_sha: Optional[str]
|
|
223
|
-
branch: Optional[str]
|
|
224
|
-
seed_hash: str
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _read_text_excerpt(
|
|
228
|
-
path: Path,
|
|
229
|
-
*,
|
|
230
|
-
repo_root: Path,
|
|
231
|
-
max_bytes: int,
|
|
232
|
-
max_chars: int,
|
|
233
|
-
budget_bytes: int,
|
|
234
|
-
bytes_read_so_far: int,
|
|
235
|
-
) -> Tuple[str, int, Optional[str]]:
|
|
236
|
-
rel = str(path.relative_to(repo_root))
|
|
237
|
-
if _looks_like_secret_path(Path(rel)):
|
|
238
|
-
return "", 0, None
|
|
239
|
-
if not path.exists() or not path.is_file():
|
|
240
|
-
return "", 0, None
|
|
241
|
-
|
|
242
|
-
try:
|
|
243
|
-
size = path.stat().st_size
|
|
244
|
-
except OSError:
|
|
245
|
-
return "", 0, None
|
|
246
|
-
|
|
247
|
-
if size > max_bytes:
|
|
248
|
-
return f"_Skipped excerpt (>{max_bytes} bytes): `{rel}`_\n", 0, None
|
|
249
|
-
|
|
250
|
-
remaining = max(0, budget_bytes - bytes_read_so_far)
|
|
251
|
-
if remaining <= 0:
|
|
252
|
-
return "", 0, None
|
|
253
|
-
|
|
254
|
-
to_read = min(max_bytes, remaining)
|
|
255
|
-
try:
|
|
256
|
-
blob = path.read_bytes()[:to_read]
|
|
257
|
-
except OSError:
|
|
258
|
-
return "", 0, None
|
|
259
|
-
|
|
260
|
-
if _is_probably_binary(blob):
|
|
261
|
-
return f"_Skipped binary file: `{rel}`_\n", 0, None
|
|
262
|
-
|
|
263
|
-
decoded = blob.decode("utf-8", errors="replace")
|
|
264
|
-
decoded = decoded.replace("\r\n", "\n")
|
|
265
|
-
decoded = decoded[:max_chars]
|
|
266
|
-
decoded = redact_text(decoded).strip()
|
|
267
|
-
if not decoded:
|
|
268
|
-
return "", 0, _sha256_bytes(blob)
|
|
269
|
-
return decoded + "\n", len(blob), _sha256_bytes(blob)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def collect_seed_context(
|
|
273
|
-
engine: Engine,
|
|
274
|
-
*,
|
|
275
|
-
per_file_read_cap_bytes: int = 200_000,
|
|
276
|
-
total_read_cap_bytes: int = 1_000_000,
|
|
277
|
-
tree_max_depth: int = 4,
|
|
278
|
-
tree_max_entries: int = 500,
|
|
279
|
-
per_doc_max_chars: int = 2000,
|
|
280
|
-
) -> SeedContext:
|
|
281
|
-
repo_root = engine.repo_root
|
|
282
|
-
config = _repo_config(engine)
|
|
283
|
-
git_ok = git_available(repo_root)
|
|
284
|
-
head_sha = git_head_sha(repo_root) if git_ok else None
|
|
285
|
-
branch = git_branch(repo_root) if git_ok else None
|
|
286
|
-
|
|
287
|
-
files = git_ls_files(repo_root) if git_ok else _iter_files_fs(repo_root)
|
|
288
|
-
tree = _build_tree_outline(
|
|
289
|
-
files, max_depth=tree_max_depth, max_entries=tree_max_entries
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
key_files = _detect_key_files(repo_root)
|
|
293
|
-
|
|
294
|
-
bytes_read = 0
|
|
295
|
-
file_hashes: Dict[str, str] = {}
|
|
296
|
-
|
|
297
|
-
def _add_excerpt(title: str, path: Path, max_chars: int) -> str:
|
|
298
|
-
nonlocal bytes_read
|
|
299
|
-
excerpt, inc, digest = _read_text_excerpt(
|
|
300
|
-
path,
|
|
301
|
-
repo_root=repo_root,
|
|
302
|
-
max_bytes=per_file_read_cap_bytes,
|
|
303
|
-
max_chars=max_chars,
|
|
304
|
-
budget_bytes=total_read_cap_bytes,
|
|
305
|
-
bytes_read_so_far=bytes_read,
|
|
306
|
-
)
|
|
307
|
-
bytes_read += inc
|
|
308
|
-
if digest:
|
|
309
|
-
rel = str(path.relative_to(repo_root))
|
|
310
|
-
file_hashes[rel] = digest
|
|
311
|
-
if not excerpt:
|
|
312
|
-
return ""
|
|
313
|
-
if excerpt.startswith("_Skipped "):
|
|
314
|
-
return f"- {excerpt.strip()}\n"
|
|
315
|
-
return f"```text\n{excerpt}```\n"
|
|
316
|
-
|
|
317
|
-
# Work docs are always included, but bounded and redacted.
|
|
318
|
-
docs = {
|
|
319
|
-
"TODO": config.doc_path("todo"),
|
|
320
|
-
"PROGRESS": config.doc_path("progress"),
|
|
321
|
-
"OPINIONS": config.doc_path("opinions"),
|
|
322
|
-
"SPEC": config.doc_path("spec"),
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
parts: List[str] = []
|
|
326
|
-
parts.append("# Seed context (bounded)\n")
|
|
327
|
-
parts.append("## Repo identity\n")
|
|
328
|
-
parts.append(f"- Root: `{repo_root}`\n")
|
|
329
|
-
parts.append(f"- VCS: `git` ({'detected' if git_ok else 'not detected'})\n")
|
|
330
|
-
if branch:
|
|
331
|
-
parts.append(f"- Branch: `{branch}`\n")
|
|
332
|
-
if head_sha:
|
|
333
|
-
parts.append(f"- HEAD: `{head_sha}`\n")
|
|
334
|
-
|
|
335
|
-
parts.append("\n## Tree outline\n")
|
|
336
|
-
parts.append(f"_Max depth={tree_max_depth}, max entries={tree_max_entries}_\n\n")
|
|
337
|
-
parts.append(tree + "\n")
|
|
338
|
-
|
|
339
|
-
parts.append("\n## Key files\n")
|
|
340
|
-
if key_files:
|
|
341
|
-
for p in key_files:
|
|
342
|
-
rel = str(p.relative_to(repo_root))
|
|
343
|
-
parts.append(f"- `{rel}`\n")
|
|
344
|
-
else:
|
|
345
|
-
parts.append("_No key files detected._\n")
|
|
346
|
-
|
|
347
|
-
parts.append("\n## Work docs excerpts\n")
|
|
348
|
-
parts.append(
|
|
349
|
-
"_Excerpts are capped and redacted; edit the real files in `.codex-autorunner/`._\n\n"
|
|
350
|
-
)
|
|
351
|
-
for label, path in docs.items():
|
|
352
|
-
parts.append(f"### {label} (`{path.relative_to(repo_root)}`)\n")
|
|
353
|
-
parts.append(_add_excerpt(label, path, per_doc_max_chars))
|
|
354
|
-
|
|
355
|
-
# Optionally include a tiny README excerpt to ground the model.
|
|
356
|
-
readme = next((p for p in key_files if p.name.lower().startswith("readme")), None)
|
|
357
|
-
if readme:
|
|
358
|
-
parts.append("\n## README excerpt\n")
|
|
359
|
-
parts.append(_add_excerpt("README", readme, 1200))
|
|
360
|
-
|
|
361
|
-
seed_text = "".join(parts).strip() + "\n"
|
|
362
|
-
seed_hash = _sha256_text(seed_text)
|
|
363
|
-
return SeedContext(
|
|
364
|
-
text=seed_text,
|
|
365
|
-
bytes_read=bytes_read,
|
|
366
|
-
file_hashes=file_hashes,
|
|
367
|
-
head_sha=head_sha,
|
|
368
|
-
branch=branch,
|
|
369
|
-
seed_hash=seed_hash,
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def summarize_changes(
|
|
374
|
-
engine: Engine,
|
|
375
|
-
*,
|
|
376
|
-
previous_state: Optional[dict],
|
|
377
|
-
current_seed: SeedContext,
|
|
378
|
-
max_lines: int = 60,
|
|
379
|
-
) -> str:
|
|
380
|
-
repo_root = engine.repo_root
|
|
381
|
-
git_ok = git_available(repo_root)
|
|
382
|
-
prev_sha = None
|
|
383
|
-
if previous_state:
|
|
384
|
-
prev_sha = previous_state.get("head_sha")
|
|
385
|
-
|
|
386
|
-
if git_ok and prev_sha:
|
|
387
|
-
diff_output = git_diff_name_status(repo_root, prev_sha, "HEAD")
|
|
388
|
-
diff_lines = (diff_output or "").strip().splitlines()
|
|
389
|
-
diff_lines = [ln for ln in diff_lines if ln.strip()]
|
|
390
|
-
if diff_lines:
|
|
391
|
-
head = "\n".join(diff_lines[:max_lines])
|
|
392
|
-
tail = "\n… (truncated)\n" if len(diff_lines) > max_lines else "\n"
|
|
393
|
-
return (
|
|
394
|
-
"Changes since last snapshot (git diff --name-status):\n"
|
|
395
|
-
f"```text\n{head}{tail}```\n"
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
status_output = git_status_porcelain(repo_root)
|
|
399
|
-
status_lines = (status_output or "").strip().splitlines()
|
|
400
|
-
status_lines = [ln for ln in status_lines if ln.strip()]
|
|
401
|
-
if status_lines:
|
|
402
|
-
head = "\n".join(status_lines[:max_lines])
|
|
403
|
-
tail = "\n… (truncated)\n" if len(status_lines) > max_lines else "\n"
|
|
404
|
-
return f"Working tree status (git status --porcelain):\n```text\n{head}{tail}```\n"
|
|
405
|
-
|
|
406
|
-
# Fallback: compare seed input file hashes we control.
|
|
407
|
-
prev_hashes: Dict[str, str] = {}
|
|
408
|
-
if previous_state and isinstance(previous_state.get("seed_file_hashes"), dict):
|
|
409
|
-
prev_hashes = dict(previous_state["seed_file_hashes"])
|
|
410
|
-
changed = []
|
|
411
|
-
for rel, digest in sorted(current_seed.file_hashes.items()):
|
|
412
|
-
if prev_hashes.get(rel) != digest:
|
|
413
|
-
changed.append(rel)
|
|
414
|
-
if changed:
|
|
415
|
-
shown = changed[:max_lines]
|
|
416
|
-
suffix = "\n- … (truncated)" if len(changed) > max_lines else ""
|
|
417
|
-
items = "\n".join(f"- `{p}`" for p in shown)
|
|
418
|
-
return f"Changes since last snapshot (seed inputs only):\n{items}{suffix}\n"
|
|
419
|
-
|
|
420
|
-
if prev_sha and not git_ok:
|
|
421
|
-
return "No VCS change summary available (git not detected).\n"
|
|
422
|
-
if not prev_sha:
|
|
423
|
-
return (
|
|
424
|
-
"No previous snapshot SHA recorded; treating as best-effort incremental.\n"
|
|
425
|
-
)
|
|
426
|
-
return "No changes detected (best-effort).\n"
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def load_snapshot(engine: Engine) -> Optional[str]:
|
|
430
|
-
config = _repo_config(engine)
|
|
431
|
-
path = config.doc_path("snapshot")
|
|
432
|
-
if not path.exists():
|
|
433
|
-
return None
|
|
434
|
-
return path.read_text(encoding="utf-8")
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def load_snapshot_state(engine: Engine) -> Optional[dict]:
|
|
438
|
-
config = _repo_config(engine)
|
|
439
|
-
return read_json(config.doc_path("snapshot_state"))
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
@dataclasses.dataclass(frozen=True)
|
|
443
|
-
class SnapshotResult:
|
|
444
|
-
content: str
|
|
445
|
-
truncated: bool
|
|
446
|
-
state: dict
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
class SnapshotService:
|
|
450
|
-
def __init__(
|
|
451
|
-
self,
|
|
452
|
-
engine: Engine,
|
|
453
|
-
*,
|
|
454
|
-
app_server_supervisor: Optional[WorkspaceAppServerSupervisor] = None,
|
|
455
|
-
app_server_threads: Optional[AppServerThreadRegistry] = None,
|
|
456
|
-
) -> None:
|
|
457
|
-
self.engine = engine
|
|
458
|
-
self._app_server_supervisor = app_server_supervisor
|
|
459
|
-
self._app_server_threads = app_server_threads or AppServerThreadRegistry(
|
|
460
|
-
default_app_server_threads_path(self.engine.repo_root)
|
|
461
|
-
)
|
|
462
|
-
self._lock: Optional[asyncio.Lock] = None
|
|
463
|
-
|
|
464
|
-
def _ensure_lock(self) -> asyncio.Lock:
|
|
465
|
-
if self._lock is None:
|
|
466
|
-
try:
|
|
467
|
-
self._lock = asyncio.Lock()
|
|
468
|
-
except RuntimeError:
|
|
469
|
-
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
470
|
-
self._lock = asyncio.Lock()
|
|
471
|
-
return self._lock
|
|
472
|
-
|
|
473
|
-
def _ensure_app_server(self) -> WorkspaceAppServerSupervisor:
|
|
474
|
-
if self._app_server_supervisor is None:
|
|
475
|
-
raise SnapshotError("App-server backend is not configured")
|
|
476
|
-
return self._app_server_supervisor
|
|
477
|
-
|
|
478
|
-
async def generate_snapshot(
|
|
479
|
-
self,
|
|
480
|
-
) -> SnapshotResult:
|
|
481
|
-
lock = self._ensure_lock()
|
|
482
|
-
if lock.locked():
|
|
483
|
-
raise SnapshotError("Snapshot generation already running")
|
|
484
|
-
async with lock:
|
|
485
|
-
config = _repo_config(self.engine)
|
|
486
|
-
previous_snapshot = await asyncio.to_thread(load_snapshot, self.engine)
|
|
487
|
-
previous_state = await asyncio.to_thread(load_snapshot_state, self.engine)
|
|
488
|
-
|
|
489
|
-
seed = await asyncio.to_thread(collect_seed_context, self.engine)
|
|
490
|
-
changes = None
|
|
491
|
-
if previous_snapshot:
|
|
492
|
-
changes = await asyncio.to_thread(
|
|
493
|
-
summarize_changes,
|
|
494
|
-
self.engine,
|
|
495
|
-
previous_state=previous_state,
|
|
496
|
-
current_seed=seed,
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
prompt = build_app_server_snapshot_prompt(
|
|
500
|
-
config,
|
|
501
|
-
seed_context=seed.text,
|
|
502
|
-
previous_snapshot=previous_snapshot,
|
|
503
|
-
changes=changes,
|
|
504
|
-
)
|
|
505
|
-
prompt_hash = _sha256_text(prompt)
|
|
506
|
-
|
|
507
|
-
supervisor = self._ensure_app_server()
|
|
508
|
-
client = await supervisor.get_client(self.engine.repo_root)
|
|
509
|
-
key = "snapshot"
|
|
510
|
-
thread_id = self._app_server_threads.get_thread_id(key)
|
|
511
|
-
if thread_id:
|
|
512
|
-
try:
|
|
513
|
-
result = await client.thread_resume(thread_id)
|
|
514
|
-
resumed = result.get("id")
|
|
515
|
-
if isinstance(resumed, str) and resumed:
|
|
516
|
-
thread_id = resumed
|
|
517
|
-
self._app_server_threads.set_thread_id(key, thread_id)
|
|
518
|
-
except CodexAppServerError:
|
|
519
|
-
self._app_server_threads.reset_thread(key)
|
|
520
|
-
thread_id = None
|
|
521
|
-
if not thread_id:
|
|
522
|
-
thread = await client.thread_start(str(self.engine.repo_root))
|
|
523
|
-
thread_id = thread.get("id")
|
|
524
|
-
if not isinstance(thread_id, str) or not thread_id:
|
|
525
|
-
raise SnapshotError("App-server did not return a thread id")
|
|
526
|
-
self._app_server_threads.set_thread_id(key, thread_id)
|
|
527
|
-
|
|
528
|
-
handle = await client.turn_start(
|
|
529
|
-
thread_id,
|
|
530
|
-
prompt,
|
|
531
|
-
approval_policy="never",
|
|
532
|
-
sandbox_policy="dangerFullAccess",
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
# Wait for completion (no streaming/interrupts for now)
|
|
536
|
-
try:
|
|
537
|
-
await handle.wait(timeout=300) # 5 mins timeout
|
|
538
|
-
except asyncio.TimeoutError as err:
|
|
539
|
-
raise SnapshotError("Snapshot generation timed out") from err
|
|
540
|
-
|
|
541
|
-
# Read the result from disk
|
|
542
|
-
path = config.doc_path("snapshot")
|
|
543
|
-
if not path.exists():
|
|
544
|
-
raise SnapshotError("Agent failed to write snapshot file")
|
|
545
|
-
|
|
546
|
-
final = path.read_text(encoding="utf-8")
|
|
547
|
-
final = redact_text(final).strip() + "\n"
|
|
548
|
-
truncated = False
|
|
549
|
-
|
|
550
|
-
state = {
|
|
551
|
-
"generated_at": _now_iso(),
|
|
552
|
-
"truncated": truncated,
|
|
553
|
-
"head_sha": seed.head_sha,
|
|
554
|
-
"branch": seed.branch,
|
|
555
|
-
"seed_hash": seed.seed_hash,
|
|
556
|
-
"prompt_hash": prompt_hash,
|
|
557
|
-
"seed_bytes_read": seed.bytes_read,
|
|
558
|
-
"seed_file_hashes": seed.file_hashes,
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
atomic_write(
|
|
562
|
-
config.doc_path("snapshot"),
|
|
563
|
-
final if final.endswith("\n") else final + "\n",
|
|
564
|
-
)
|
|
565
|
-
atomic_write(
|
|
566
|
-
config.doc_path("snapshot_state"),
|
|
567
|
-
json.dumps(state, indent=2, sort_keys=True) + "\n",
|
|
568
|
-
)
|
|
569
|
-
return SnapshotResult(content=final, truncated=truncated, state=state)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
# Keep the original function signature for backward compatibility if needed,
|
|
573
|
-
# but it will likely break if used in sync context without loop.
|
|
574
|
-
# We will update callers to use SnapshotService.
|
|
575
|
-
def generate_snapshot(
|
|
576
|
-
engine: Engine,
|
|
577
|
-
*,
|
|
578
|
-
prefer_large_model: bool = True,
|
|
579
|
-
) -> SnapshotResult:
|
|
580
|
-
raise NotImplementedError("Use SnapshotService.generate_snapshot() instead")
|