codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -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 +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -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/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -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 +227 -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 +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- 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 +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- 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 +162 -196
- 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 +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -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 +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.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/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
codex_autorunner/core/config.py
CHANGED
|
@@ -52,13 +52,9 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
52
52
|
"version": CONFIG_VERSION,
|
|
53
53
|
"mode": "repo",
|
|
54
54
|
"docs": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"spec": ".codex-autorunner/SPEC.md",
|
|
59
|
-
"summary": ".codex-autorunner/SUMMARY.md",
|
|
60
|
-
"snapshot": ".codex-autorunner/SNAPSHOT.md",
|
|
61
|
-
"snapshot_state": ".codex-autorunner/snapshot_state.json",
|
|
55
|
+
"active_context": ".codex-autorunner/workspace/active_context.md",
|
|
56
|
+
"decisions": ".codex-autorunner/workspace/decisions.md",
|
|
57
|
+
"spec": ".codex-autorunner/workspace/spec.md",
|
|
62
58
|
},
|
|
63
59
|
"review": {
|
|
64
60
|
"enabled": True,
|
|
@@ -99,6 +95,9 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
99
95
|
"prev_run_max_chars": 6000,
|
|
100
96
|
"template": ".codex-autorunner/prompt.txt",
|
|
101
97
|
},
|
|
98
|
+
"security": {
|
|
99
|
+
"redact_run_logs": True,
|
|
100
|
+
},
|
|
102
101
|
"runner": {
|
|
103
102
|
"sleep_seconds": 5,
|
|
104
103
|
"stop_after_runs": None,
|
|
@@ -118,8 +117,8 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
118
117
|
"reasoning": None,
|
|
119
118
|
"max_wallclock_seconds": None,
|
|
120
119
|
"context": {
|
|
121
|
-
"primary_docs": ["spec", "
|
|
122
|
-
"include_docs": ["
|
|
120
|
+
"primary_docs": ["spec", "decisions"],
|
|
121
|
+
"include_docs": ["active_context"],
|
|
123
122
|
"include_last_run_artifacts": True,
|
|
124
123
|
"max_doc_chars": 20000,
|
|
125
124
|
},
|
|
@@ -129,6 +128,11 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
129
128
|
},
|
|
130
129
|
},
|
|
131
130
|
},
|
|
131
|
+
"ticket_flow": {
|
|
132
|
+
"approval_mode": "yolo",
|
|
133
|
+
# Keep ticket_flow deterministic by default; surfaces can tighten this.
|
|
134
|
+
"default_approval_decision": "accept",
|
|
135
|
+
},
|
|
132
136
|
"git": {
|
|
133
137
|
"auto_commit": False,
|
|
134
138
|
"commit_message_template": "[codex] run #{run_id}",
|
|
@@ -139,34 +143,33 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
139
143
|
"sync_commit_mode": "auto", # none|auto|always
|
|
140
144
|
# Bounds the agentic sync step in GitHubService.sync_pr (seconds).
|
|
141
145
|
"sync_agent_timeout_seconds": 1800,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"stop_condition": "no_issues",
|
|
146
|
-
"max_implementation_runs": None,
|
|
147
|
-
"max_wallclock_seconds": None,
|
|
148
|
-
"review": {
|
|
149
|
-
"include_codex": True,
|
|
150
|
-
"include_github": True,
|
|
151
|
-
"include_checks": True,
|
|
152
|
-
},
|
|
153
|
-
"chatops": {
|
|
154
|
-
"enabled": False,
|
|
155
|
-
"poll_interval_seconds": 60,
|
|
156
|
-
"allow_users": [],
|
|
157
|
-
"allow_associations": [],
|
|
158
|
-
"ignore_bots": True,
|
|
159
|
-
},
|
|
160
|
-
},
|
|
146
|
+
},
|
|
147
|
+
"update": {
|
|
148
|
+
"skip_checks": False,
|
|
161
149
|
},
|
|
162
150
|
"app_server": {
|
|
163
151
|
"command": ["codex", "app-server"],
|
|
164
152
|
"state_root": "~/.codex-autorunner/workspaces",
|
|
153
|
+
"auto_restart": True,
|
|
165
154
|
"max_handles": 20,
|
|
166
155
|
"idle_ttl_seconds": 3600,
|
|
167
156
|
"turn_timeout_seconds": 28800,
|
|
157
|
+
"turn_stall_timeout_seconds": 60,
|
|
158
|
+
"turn_stall_poll_interval_seconds": 2,
|
|
159
|
+
"turn_stall_recovery_min_interval_seconds": 10,
|
|
168
160
|
"request_timeout": None,
|
|
161
|
+
"client": {
|
|
162
|
+
"max_message_bytes": 50 * 1024 * 1024,
|
|
163
|
+
"oversize_preview_bytes": 4096,
|
|
164
|
+
"max_oversize_drain_bytes": 100 * 1024 * 1024,
|
|
165
|
+
"restart_backoff_initial_seconds": 0.5,
|
|
166
|
+
"restart_backoff_max_seconds": 30.0,
|
|
167
|
+
"restart_backoff_jitter_ratio": 0.1,
|
|
168
|
+
},
|
|
169
169
|
"prompts": {
|
|
170
|
+
# NOTE: These keys are legacy names kept for config compatibility.
|
|
171
|
+
# The workspace cutover uses tickets + workspace docs + unified file chat; only
|
|
172
|
+
# the "autorunner" prompt is currently used by the app-server prompt builder.
|
|
170
173
|
"doc_chat": {
|
|
171
174
|
"max_chars": 12000,
|
|
172
175
|
"message_max_chars": 2000,
|
|
@@ -186,6 +189,9 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
186
189
|
},
|
|
187
190
|
},
|
|
188
191
|
},
|
|
192
|
+
"opencode": {
|
|
193
|
+
"session_stall_timeout_seconds": 60,
|
|
194
|
+
},
|
|
189
195
|
"server": {
|
|
190
196
|
"host": "127.0.0.1",
|
|
191
197
|
"port": 4173,
|
|
@@ -249,6 +255,20 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
249
255
|
"timeout_ms": 120000,
|
|
250
256
|
"max_output_chars": 3800,
|
|
251
257
|
},
|
|
258
|
+
"cache": {
|
|
259
|
+
"cleanup_interval_seconds": 300,
|
|
260
|
+
"coalesce_buffer_ttl_seconds": 60,
|
|
261
|
+
"media_batch_buffer_ttl_seconds": 60,
|
|
262
|
+
"model_pending_ttl_seconds": 1800,
|
|
263
|
+
"pending_approval_ttl_seconds": 600,
|
|
264
|
+
"pending_question_ttl_seconds": 600,
|
|
265
|
+
"reasoning_buffer_ttl_seconds": 900,
|
|
266
|
+
"selection_state_ttl_seconds": 1800,
|
|
267
|
+
"turn_preview_ttl_seconds": 900,
|
|
268
|
+
"progress_stream_ttl_seconds": 900,
|
|
269
|
+
"oversize_warning_ttl_seconds": 3600,
|
|
270
|
+
"update_id_persist_interval_seconds": 60,
|
|
271
|
+
},
|
|
252
272
|
"command_registration": {
|
|
253
273
|
"enabled": True,
|
|
254
274
|
"scopes": [
|
|
@@ -256,6 +276,7 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
256
276
|
{"type": "all_group_chats", "language_code": ""},
|
|
257
277
|
],
|
|
258
278
|
},
|
|
279
|
+
"opencode_command": None,
|
|
259
280
|
"state_file": ".codex-autorunner/telegram_state.sqlite3",
|
|
260
281
|
"app_server_command_env": "CAR_TELEGRAM_APP_SERVER_COMMAND",
|
|
261
282
|
"app_server_command": ["codex", "app-server"],
|
|
@@ -264,6 +285,10 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
264
285
|
"idle_ttl_seconds": 3600,
|
|
265
286
|
"turn_timeout_seconds": 28800,
|
|
266
287
|
},
|
|
288
|
+
"agent_timeouts": {
|
|
289
|
+
"codex": 28800,
|
|
290
|
+
"opencode": 28800,
|
|
291
|
+
},
|
|
267
292
|
"polling": {
|
|
268
293
|
"timeout_seconds": 30,
|
|
269
294
|
"allowed_updates": ["message", "edited_message", "callback_query"],
|
|
@@ -395,13 +420,16 @@ REPO_DEFAULT_KEYS = {
|
|
|
395
420
|
"codex",
|
|
396
421
|
"prompt",
|
|
397
422
|
"runner",
|
|
423
|
+
"ticket_flow",
|
|
398
424
|
"git",
|
|
399
425
|
"github",
|
|
426
|
+
"update",
|
|
400
427
|
"notifications",
|
|
401
428
|
"voice",
|
|
402
429
|
"log",
|
|
403
430
|
"server_log",
|
|
404
431
|
"review",
|
|
432
|
+
"opencode",
|
|
405
433
|
}
|
|
406
434
|
DEFAULT_REPO_DEFAULTS = {
|
|
407
435
|
key: json.loads(json.dumps(DEFAULT_REPO_CONFIG[key])) for key in REPO_DEFAULT_KEYS
|
|
@@ -410,10 +438,12 @@ REPO_SHARED_KEYS = {
|
|
|
410
438
|
"agents",
|
|
411
439
|
"server",
|
|
412
440
|
"app_server",
|
|
441
|
+
"opencode",
|
|
413
442
|
"telegram_bot",
|
|
414
443
|
"terminal",
|
|
415
444
|
"static_assets",
|
|
416
445
|
"housekeeping",
|
|
446
|
+
"update",
|
|
417
447
|
}
|
|
418
448
|
|
|
419
449
|
DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
@@ -476,6 +506,20 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
476
506
|
"timeout_ms": 120000,
|
|
477
507
|
"max_output_chars": 3800,
|
|
478
508
|
},
|
|
509
|
+
"cache": {
|
|
510
|
+
"cleanup_interval_seconds": 300,
|
|
511
|
+
"coalesce_buffer_ttl_seconds": 60,
|
|
512
|
+
"media_batch_buffer_ttl_seconds": 60,
|
|
513
|
+
"model_pending_ttl_seconds": 1800,
|
|
514
|
+
"pending_approval_ttl_seconds": 600,
|
|
515
|
+
"pending_question_ttl_seconds": 600,
|
|
516
|
+
"reasoning_buffer_ttl_seconds": 900,
|
|
517
|
+
"selection_state_ttl_seconds": 1800,
|
|
518
|
+
"turn_preview_ttl_seconds": 900,
|
|
519
|
+
"progress_stream_ttl_seconds": 900,
|
|
520
|
+
"oversize_warning_ttl_seconds": 3600,
|
|
521
|
+
"update_id_persist_interval_seconds": 60,
|
|
522
|
+
},
|
|
479
523
|
"command_registration": {
|
|
480
524
|
"enabled": True,
|
|
481
525
|
"scopes": [
|
|
@@ -483,6 +527,7 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
483
527
|
{"type": "all_group_chats", "language_code": ""},
|
|
484
528
|
],
|
|
485
529
|
},
|
|
530
|
+
"opencode_command": None,
|
|
486
531
|
"state_file": ".codex-autorunner/telegram_state.sqlite3",
|
|
487
532
|
"app_server_command_env": "CAR_TELEGRAM_APP_SERVER_COMMAND",
|
|
488
533
|
"app_server_command": ["codex", "app-server"],
|
|
@@ -513,13 +558,28 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
513
558
|
"backup_count": 3,
|
|
514
559
|
},
|
|
515
560
|
},
|
|
561
|
+
"update": {
|
|
562
|
+
"skip_checks": False,
|
|
563
|
+
},
|
|
516
564
|
"app_server": {
|
|
517
565
|
"command": ["codex", "app-server"],
|
|
518
566
|
"state_root": "~/.codex-autorunner/workspaces",
|
|
567
|
+
"auto_restart": True,
|
|
519
568
|
"max_handles": 20,
|
|
520
569
|
"idle_ttl_seconds": 3600,
|
|
521
570
|
"turn_timeout_seconds": 28800,
|
|
571
|
+
"turn_stall_timeout_seconds": 60,
|
|
572
|
+
"turn_stall_poll_interval_seconds": 2,
|
|
573
|
+
"turn_stall_recovery_min_interval_seconds": 10,
|
|
522
574
|
"request_timeout": None,
|
|
575
|
+
"client": {
|
|
576
|
+
"max_message_bytes": 50 * 1024 * 1024,
|
|
577
|
+
"oversize_preview_bytes": 4096,
|
|
578
|
+
"max_oversize_drain_bytes": 100 * 1024 * 1024,
|
|
579
|
+
"restart_backoff_initial_seconds": 0.5,
|
|
580
|
+
"restart_backoff_max_seconds": 30.0,
|
|
581
|
+
"restart_backoff_jitter_ratio": 0.1,
|
|
582
|
+
},
|
|
523
583
|
"prompts": {
|
|
524
584
|
"doc_chat": {
|
|
525
585
|
"max_chars": 12000,
|
|
@@ -540,6 +600,9 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
540
600
|
},
|
|
541
601
|
},
|
|
542
602
|
},
|
|
603
|
+
"opencode": {
|
|
604
|
+
"session_stall_timeout_seconds": 60,
|
|
605
|
+
},
|
|
543
606
|
"server": {
|
|
544
607
|
"host": "127.0.0.1",
|
|
545
608
|
"port": 4173,
|
|
@@ -697,17 +760,37 @@ class AppServerPromptsConfig:
|
|
|
697
760
|
autorunner: AppServerAutorunnerPromptConfig
|
|
698
761
|
|
|
699
762
|
|
|
763
|
+
@dataclasses.dataclass
|
|
764
|
+
class AppServerClientConfig:
|
|
765
|
+
max_message_bytes: int
|
|
766
|
+
oversize_preview_bytes: int
|
|
767
|
+
max_oversize_drain_bytes: int
|
|
768
|
+
restart_backoff_initial_seconds: float
|
|
769
|
+
restart_backoff_max_seconds: float
|
|
770
|
+
restart_backoff_jitter_ratio: float
|
|
771
|
+
|
|
772
|
+
|
|
700
773
|
@dataclasses.dataclass
|
|
701
774
|
class AppServerConfig:
|
|
702
775
|
command: List[str]
|
|
703
776
|
state_root: Path
|
|
777
|
+
auto_restart: Optional[bool]
|
|
704
778
|
max_handles: Optional[int]
|
|
705
779
|
idle_ttl_seconds: Optional[int]
|
|
706
780
|
turn_timeout_seconds: Optional[float]
|
|
781
|
+
turn_stall_timeout_seconds: Optional[float]
|
|
782
|
+
turn_stall_poll_interval_seconds: Optional[float]
|
|
783
|
+
turn_stall_recovery_min_interval_seconds: Optional[float]
|
|
707
784
|
request_timeout: Optional[float]
|
|
785
|
+
client: AppServerClientConfig
|
|
708
786
|
prompts: AppServerPromptsConfig
|
|
709
787
|
|
|
710
788
|
|
|
789
|
+
@dataclasses.dataclass
|
|
790
|
+
class OpenCodeConfig:
|
|
791
|
+
session_stall_timeout_seconds: Optional[float]
|
|
792
|
+
|
|
793
|
+
|
|
711
794
|
@dataclasses.dataclass(frozen=True)
|
|
712
795
|
class AgentConfig:
|
|
713
796
|
binary: str
|
|
@@ -722,6 +805,7 @@ class RepoConfig:
|
|
|
722
805
|
root: Path
|
|
723
806
|
version: int
|
|
724
807
|
mode: str
|
|
808
|
+
security: Dict[str, Any]
|
|
725
809
|
docs: Dict[str, Path]
|
|
726
810
|
codex_binary: str
|
|
727
811
|
codex_args: List[str]
|
|
@@ -735,9 +819,12 @@ class RepoConfig:
|
|
|
735
819
|
runner_stop_after_runs: Optional[int]
|
|
736
820
|
runner_max_wallclock_seconds: Optional[int]
|
|
737
821
|
runner_no_progress_threshold: int
|
|
822
|
+
ticket_flow: Dict[str, Any]
|
|
738
823
|
git_auto_commit: bool
|
|
739
824
|
git_commit_message_template: str
|
|
825
|
+
update_skip_checks: bool
|
|
740
826
|
app_server: AppServerConfig
|
|
827
|
+
opencode: OpenCodeConfig
|
|
741
828
|
server_host: str
|
|
742
829
|
server_port: int
|
|
743
830
|
server_base_path: str
|
|
@@ -785,7 +872,9 @@ class HubConfig:
|
|
|
785
872
|
repo_server_inherit: bool
|
|
786
873
|
update_repo_url: str
|
|
787
874
|
update_repo_ref: str
|
|
875
|
+
update_skip_checks: bool
|
|
788
876
|
app_server: AppServerConfig
|
|
877
|
+
opencode: OpenCodeConfig
|
|
789
878
|
server_host: str
|
|
790
879
|
server_port: int
|
|
791
880
|
server_base_path: str
|
|
@@ -1024,6 +1113,11 @@ def _parse_app_server_config(
|
|
|
1024
1113
|
allow_home=True,
|
|
1025
1114
|
scope="app_server.state_root",
|
|
1026
1115
|
)
|
|
1116
|
+
auto_restart_raw = cfg.get("auto_restart", defaults.get("auto_restart"))
|
|
1117
|
+
if auto_restart_raw is None:
|
|
1118
|
+
auto_restart = None
|
|
1119
|
+
else:
|
|
1120
|
+
auto_restart = bool(auto_restart_raw)
|
|
1027
1121
|
max_handles_raw = cfg.get("max_handles", defaults.get("max_handles"))
|
|
1028
1122
|
max_handles = int(max_handles_raw) if max_handles_raw is not None else None
|
|
1029
1123
|
if max_handles is not None and max_handles <= 0:
|
|
@@ -1040,25 +1134,115 @@ def _parse_app_server_config(
|
|
|
1040
1134
|
)
|
|
1041
1135
|
if turn_timeout_seconds is not None and turn_timeout_seconds <= 0:
|
|
1042
1136
|
turn_timeout_seconds = None
|
|
1137
|
+
stall_timeout_raw = cfg.get(
|
|
1138
|
+
"turn_stall_timeout_seconds", defaults.get("turn_stall_timeout_seconds")
|
|
1139
|
+
)
|
|
1140
|
+
turn_stall_timeout_seconds = (
|
|
1141
|
+
float(stall_timeout_raw) if stall_timeout_raw is not None else None
|
|
1142
|
+
)
|
|
1143
|
+
if turn_stall_timeout_seconds is not None and turn_stall_timeout_seconds <= 0:
|
|
1144
|
+
turn_stall_timeout_seconds = None
|
|
1145
|
+
stall_poll_raw = cfg.get(
|
|
1146
|
+
"turn_stall_poll_interval_seconds",
|
|
1147
|
+
defaults.get("turn_stall_poll_interval_seconds"),
|
|
1148
|
+
)
|
|
1149
|
+
turn_stall_poll_interval_seconds = (
|
|
1150
|
+
float(stall_poll_raw) if stall_poll_raw is not None else None
|
|
1151
|
+
)
|
|
1152
|
+
if (
|
|
1153
|
+
turn_stall_poll_interval_seconds is not None
|
|
1154
|
+
and turn_stall_poll_interval_seconds <= 0
|
|
1155
|
+
):
|
|
1156
|
+
turn_stall_poll_interval_seconds = defaults.get(
|
|
1157
|
+
"turn_stall_poll_interval_seconds"
|
|
1158
|
+
)
|
|
1159
|
+
stall_recovery_raw = cfg.get(
|
|
1160
|
+
"turn_stall_recovery_min_interval_seconds",
|
|
1161
|
+
defaults.get("turn_stall_recovery_min_interval_seconds"),
|
|
1162
|
+
)
|
|
1163
|
+
turn_stall_recovery_min_interval_seconds = (
|
|
1164
|
+
float(stall_recovery_raw) if stall_recovery_raw is not None else None
|
|
1165
|
+
)
|
|
1166
|
+
if (
|
|
1167
|
+
turn_stall_recovery_min_interval_seconds is not None
|
|
1168
|
+
and turn_stall_recovery_min_interval_seconds < 0
|
|
1169
|
+
):
|
|
1170
|
+
turn_stall_recovery_min_interval_seconds = defaults.get(
|
|
1171
|
+
"turn_stall_recovery_min_interval_seconds"
|
|
1172
|
+
)
|
|
1043
1173
|
request_timeout_raw = cfg.get("request_timeout", defaults.get("request_timeout"))
|
|
1044
1174
|
request_timeout = (
|
|
1045
1175
|
float(request_timeout_raw) if request_timeout_raw is not None else None
|
|
1046
1176
|
)
|
|
1047
1177
|
if request_timeout is not None and request_timeout <= 0:
|
|
1048
1178
|
request_timeout = None
|
|
1179
|
+
client_defaults = defaults.get("client")
|
|
1180
|
+
client_defaults = client_defaults if isinstance(client_defaults, dict) else {}
|
|
1181
|
+
client_cfg_raw = cfg.get("client")
|
|
1182
|
+
client_cfg = client_cfg_raw if isinstance(client_cfg_raw, dict) else {}
|
|
1183
|
+
|
|
1184
|
+
def _client_int(key: str) -> int:
|
|
1185
|
+
value = client_cfg.get(key, client_defaults.get(key))
|
|
1186
|
+
value = int(value) if value is not None else 0
|
|
1187
|
+
if value <= 0:
|
|
1188
|
+
value = int(client_defaults.get(key) or 0)
|
|
1189
|
+
return value
|
|
1190
|
+
|
|
1191
|
+
def _client_float(key: str, *, allow_zero: bool = False) -> float:
|
|
1192
|
+
value = client_cfg.get(key, client_defaults.get(key))
|
|
1193
|
+
value = float(value) if value is not None else 0.0
|
|
1194
|
+
if value < 0 or (not allow_zero and value <= 0):
|
|
1195
|
+
value = float(client_defaults.get(key) or 0.0)
|
|
1196
|
+
return value
|
|
1197
|
+
|
|
1049
1198
|
prompt_defaults = defaults.get("prompts")
|
|
1050
1199
|
prompts = _parse_app_server_prompts_config(cfg.get("prompts"), prompt_defaults)
|
|
1051
1200
|
return AppServerConfig(
|
|
1052
1201
|
command=command,
|
|
1053
1202
|
state_root=state_root,
|
|
1203
|
+
auto_restart=auto_restart,
|
|
1054
1204
|
max_handles=max_handles,
|
|
1055
1205
|
idle_ttl_seconds=idle_ttl_seconds,
|
|
1056
1206
|
turn_timeout_seconds=turn_timeout_seconds,
|
|
1207
|
+
turn_stall_timeout_seconds=turn_stall_timeout_seconds,
|
|
1208
|
+
turn_stall_poll_interval_seconds=turn_stall_poll_interval_seconds,
|
|
1209
|
+
turn_stall_recovery_min_interval_seconds=turn_stall_recovery_min_interval_seconds,
|
|
1057
1210
|
request_timeout=request_timeout,
|
|
1211
|
+
client=AppServerClientConfig(
|
|
1212
|
+
max_message_bytes=_client_int("max_message_bytes"),
|
|
1213
|
+
oversize_preview_bytes=_client_int("oversize_preview_bytes"),
|
|
1214
|
+
max_oversize_drain_bytes=_client_int("max_oversize_drain_bytes"),
|
|
1215
|
+
restart_backoff_initial_seconds=_client_float(
|
|
1216
|
+
"restart_backoff_initial_seconds"
|
|
1217
|
+
),
|
|
1218
|
+
restart_backoff_max_seconds=_client_float("restart_backoff_max_seconds"),
|
|
1219
|
+
restart_backoff_jitter_ratio=_client_float(
|
|
1220
|
+
"restart_backoff_jitter_ratio", allow_zero=True
|
|
1221
|
+
),
|
|
1222
|
+
),
|
|
1058
1223
|
prompts=prompts,
|
|
1059
1224
|
)
|
|
1060
1225
|
|
|
1061
1226
|
|
|
1227
|
+
def _parse_opencode_config(
|
|
1228
|
+
cfg: Optional[Dict[str, Any]],
|
|
1229
|
+
_root: Path,
|
|
1230
|
+
defaults: Optional[Dict[str, Any]],
|
|
1231
|
+
) -> OpenCodeConfig:
|
|
1232
|
+
cfg = cfg if isinstance(cfg, dict) else {}
|
|
1233
|
+
defaults = defaults if isinstance(defaults, dict) else {}
|
|
1234
|
+
stall_timeout_raw = cfg.get(
|
|
1235
|
+
"session_stall_timeout_seconds",
|
|
1236
|
+
defaults.get("session_stall_timeout_seconds"),
|
|
1237
|
+
)
|
|
1238
|
+
stall_timeout_seconds = (
|
|
1239
|
+
float(stall_timeout_raw) if stall_timeout_raw is not None else None
|
|
1240
|
+
)
|
|
1241
|
+
if stall_timeout_seconds is not None and stall_timeout_seconds <= 0:
|
|
1242
|
+
stall_timeout_seconds = None
|
|
1243
|
+
return OpenCodeConfig(session_stall_timeout_seconds=stall_timeout_seconds)
|
|
1244
|
+
|
|
1245
|
+
|
|
1062
1246
|
def _parse_agents_config(
|
|
1063
1247
|
cfg: Optional[Dict[str, Any]], defaults: Dict[str, Any]
|
|
1064
1248
|
) -> Dict[str, AgentConfig]:
|
|
@@ -1207,6 +1391,21 @@ def load_hub_config_data(config_path: Path) -> Dict[str, Any]:
|
|
|
1207
1391
|
|
|
1208
1392
|
def _resolve_hub_config_path(start: Path) -> Path:
|
|
1209
1393
|
config_path = find_nearest_hub_config_path(start)
|
|
1394
|
+
if not config_path:
|
|
1395
|
+
# Auto-initialize hub config if missing in the current directory or parents.
|
|
1396
|
+
# If we are in a git repo, we'll initialize a hub there.
|
|
1397
|
+
try:
|
|
1398
|
+
from .utils import find_repo_root
|
|
1399
|
+
|
|
1400
|
+
target_root = find_repo_root(start)
|
|
1401
|
+
except Exception:
|
|
1402
|
+
target_root = start
|
|
1403
|
+
|
|
1404
|
+
from ..bootstrap import seed_hub_files
|
|
1405
|
+
|
|
1406
|
+
seed_hub_files(target_root)
|
|
1407
|
+
config_path = find_nearest_hub_config_path(target_root)
|
|
1408
|
+
|
|
1210
1409
|
if not config_path:
|
|
1211
1410
|
raise ConfigError(
|
|
1212
1411
|
f"Missing hub config file; expected to find {CONFIG_FILENAME} in {start} or parents (use --hub to specify)"
|
|
@@ -1272,15 +1471,9 @@ def load_repo_config(start: Path, hub_path: Optional[Path] = None) -> RepoConfig
|
|
|
1272
1471
|
def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
1273
1472
|
root = config_path.parent.parent.resolve()
|
|
1274
1473
|
docs = {
|
|
1275
|
-
"
|
|
1276
|
-
"
|
|
1277
|
-
"opinions": Path(cfg["docs"]["opinions"]),
|
|
1474
|
+
"active_context": Path(cfg["docs"]["active_context"]),
|
|
1475
|
+
"decisions": Path(cfg["docs"]["decisions"]),
|
|
1278
1476
|
"spec": Path(cfg["docs"]["spec"]),
|
|
1279
|
-
"summary": Path(cfg["docs"]["summary"]),
|
|
1280
|
-
"snapshot": Path(cfg["docs"].get("snapshot", ".codex-autorunner/SNAPSHOT.md")),
|
|
1281
|
-
"snapshot_state": Path(
|
|
1282
|
-
cfg["docs"].get("snapshot_state", ".codex-autorunner/snapshot_state.json")
|
|
1283
|
-
),
|
|
1284
1477
|
}
|
|
1285
1478
|
voice_cfg = cfg.get("voice") if isinstance(cfg.get("voice"), dict) else {}
|
|
1286
1479
|
voice_cfg = cast(Dict[str, Any], voice_cfg)
|
|
@@ -1301,12 +1494,19 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1301
1494
|
cfg.get("notifications") if isinstance(cfg.get("notifications"), dict) else {}
|
|
1302
1495
|
)
|
|
1303
1496
|
notifications_cfg = cast(Dict[str, Any], notifications_cfg)
|
|
1497
|
+
security_cfg = cfg.get("security") if isinstance(cfg.get("security"), dict) else {}
|
|
1498
|
+
security_cfg = cast(Dict[str, Any], security_cfg)
|
|
1304
1499
|
log_cfg = cfg.get("log", {})
|
|
1305
1500
|
log_cfg = cast(Dict[str, Any], log_cfg if isinstance(log_cfg, dict) else {})
|
|
1306
1501
|
server_log_cfg = cfg.get("server_log", {}) or {}
|
|
1307
1502
|
server_log_cfg = cast(
|
|
1308
1503
|
Dict[str, Any], server_log_cfg if isinstance(server_log_cfg, dict) else {}
|
|
1309
1504
|
)
|
|
1505
|
+
update_cfg = cfg.get("update")
|
|
1506
|
+
update_cfg = cast(
|
|
1507
|
+
Dict[str, Any], update_cfg if isinstance(update_cfg, dict) else {}
|
|
1508
|
+
)
|
|
1509
|
+
update_skip_checks = bool(update_cfg.get("skip_checks", False))
|
|
1310
1510
|
return RepoConfig(
|
|
1311
1511
|
raw=cfg,
|
|
1312
1512
|
root=root,
|
|
@@ -1327,11 +1527,17 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1327
1527
|
runner_no_progress_threshold=int(cfg["runner"].get("no_progress_threshold", 3)),
|
|
1328
1528
|
git_auto_commit=bool(cfg["git"].get("auto_commit", False)),
|
|
1329
1529
|
git_commit_message_template=str(cfg["git"].get("commit_message_template")),
|
|
1530
|
+
update_skip_checks=update_skip_checks,
|
|
1531
|
+
ticket_flow=cast(Dict[str, Any], cfg.get("ticket_flow") or {}),
|
|
1330
1532
|
app_server=_parse_app_server_config(
|
|
1331
1533
|
cfg.get("app_server"),
|
|
1332
1534
|
root,
|
|
1333
1535
|
DEFAULT_REPO_CONFIG["app_server"],
|
|
1334
1536
|
),
|
|
1537
|
+
opencode=_parse_opencode_config(
|
|
1538
|
+
cfg.get("opencode"), root, DEFAULT_REPO_CONFIG.get("opencode")
|
|
1539
|
+
),
|
|
1540
|
+
security=security_cfg,
|
|
1335
1541
|
server_host=str(cfg["server"].get("host")),
|
|
1336
1542
|
server_port=int(cfg["server"].get("port")),
|
|
1337
1543
|
server_base_path=_normalize_base_path(cfg["server"].get("base_path", "")),
|
|
@@ -1402,6 +1608,12 @@ def _build_hub_config(config_path: Path, cfg: Dict[str, Any]) -> HubConfig:
|
|
|
1402
1608
|
except ConfigPathError as exc:
|
|
1403
1609
|
raise ConfigError(str(exc)) from exc
|
|
1404
1610
|
|
|
1611
|
+
update_cfg = cfg.get("update")
|
|
1612
|
+
update_cfg = cast(
|
|
1613
|
+
Dict[str, Any], update_cfg if isinstance(update_cfg, dict) else {}
|
|
1614
|
+
)
|
|
1615
|
+
update_skip_checks = bool(update_cfg.get("skip_checks", False))
|
|
1616
|
+
|
|
1405
1617
|
return HubConfig(
|
|
1406
1618
|
raw=cfg,
|
|
1407
1619
|
root=root,
|
|
@@ -1417,11 +1629,15 @@ def _build_hub_config(config_path: Path, cfg: Dict[str, Any]) -> HubConfig:
|
|
|
1417
1629
|
repo_server_inherit=bool(hub_cfg.get("repo_server_inherit", True)),
|
|
1418
1630
|
update_repo_url=str(hub_cfg.get("update_repo_url", "")),
|
|
1419
1631
|
update_repo_ref=str(hub_cfg.get("update_repo_ref", "main")),
|
|
1632
|
+
update_skip_checks=update_skip_checks,
|
|
1420
1633
|
app_server=_parse_app_server_config(
|
|
1421
1634
|
cfg.get("app_server"),
|
|
1422
1635
|
root,
|
|
1423
1636
|
DEFAULT_HUB_CONFIG["app_server"],
|
|
1424
1637
|
),
|
|
1638
|
+
opencode=_parse_opencode_config(
|
|
1639
|
+
cfg.get("opencode"), root, DEFAULT_HUB_CONFIG.get("opencode")
|
|
1640
|
+
),
|
|
1425
1641
|
server_host=str(cfg["server"]["host"]),
|
|
1426
1642
|
server_port=int(cfg["server"]["port"]),
|
|
1427
1643
|
server_base_path=_normalize_base_path(cfg["server"].get("base_path", "")),
|
|
@@ -1501,6 +1717,12 @@ def _validate_app_server_config(cfg: Dict[str, Any]) -> None:
|
|
|
1501
1717
|
app_server_cfg.get("state_root", ""), str
|
|
1502
1718
|
):
|
|
1503
1719
|
raise ConfigError("app_server.state_root must be a string path")
|
|
1720
|
+
if (
|
|
1721
|
+
"auto_restart" in app_server_cfg
|
|
1722
|
+
and app_server_cfg.get("auto_restart") is not None
|
|
1723
|
+
):
|
|
1724
|
+
if not isinstance(app_server_cfg.get("auto_restart"), bool):
|
|
1725
|
+
raise ConfigError("app_server.auto_restart must be boolean or null")
|
|
1504
1726
|
for key in ("max_handles", "idle_ttl_seconds"):
|
|
1505
1727
|
if key in app_server_cfg and app_server_cfg.get(key) is not None:
|
|
1506
1728
|
if not isinstance(app_server_cfg.get(key), int):
|
|
@@ -1519,6 +1741,47 @@ def _validate_app_server_config(cfg: Dict[str, Any]) -> None:
|
|
|
1519
1741
|
):
|
|
1520
1742
|
if not isinstance(app_server_cfg.get("request_timeout"), (int, float)):
|
|
1521
1743
|
raise ConfigError("app_server.request_timeout must be a number or null")
|
|
1744
|
+
for key in (
|
|
1745
|
+
"turn_stall_timeout_seconds",
|
|
1746
|
+
"turn_stall_poll_interval_seconds",
|
|
1747
|
+
"turn_stall_recovery_min_interval_seconds",
|
|
1748
|
+
):
|
|
1749
|
+
if key in app_server_cfg and app_server_cfg.get(key) is not None:
|
|
1750
|
+
if not isinstance(app_server_cfg.get(key), (int, float)):
|
|
1751
|
+
raise ConfigError(f"app_server.{key} must be a number or null")
|
|
1752
|
+
client_cfg = app_server_cfg.get("client")
|
|
1753
|
+
if client_cfg is not None:
|
|
1754
|
+
if not isinstance(client_cfg, dict):
|
|
1755
|
+
raise ConfigError("app_server.client must be a mapping if provided")
|
|
1756
|
+
for key in (
|
|
1757
|
+
"max_message_bytes",
|
|
1758
|
+
"oversize_preview_bytes",
|
|
1759
|
+
"max_oversize_drain_bytes",
|
|
1760
|
+
):
|
|
1761
|
+
if key in client_cfg:
|
|
1762
|
+
value = client_cfg.get(key)
|
|
1763
|
+
if not isinstance(value, int):
|
|
1764
|
+
raise ConfigError(f"app_server.client.{key} must be an integer")
|
|
1765
|
+
if value <= 0:
|
|
1766
|
+
raise ConfigError(f"app_server.client.{key} must be > 0")
|
|
1767
|
+
for key in (
|
|
1768
|
+
"restart_backoff_initial_seconds",
|
|
1769
|
+
"restart_backoff_max_seconds",
|
|
1770
|
+
"restart_backoff_jitter_ratio",
|
|
1771
|
+
):
|
|
1772
|
+
if key in client_cfg:
|
|
1773
|
+
value = client_cfg.get(key)
|
|
1774
|
+
if not isinstance(value, (int, float)):
|
|
1775
|
+
raise ConfigError(
|
|
1776
|
+
f"app_server.client.{key} must be a number if provided"
|
|
1777
|
+
)
|
|
1778
|
+
if key == "restart_backoff_jitter_ratio":
|
|
1779
|
+
if value < 0:
|
|
1780
|
+
raise ConfigError(
|
|
1781
|
+
"app_server.client.restart_backoff_jitter_ratio must be >= 0"
|
|
1782
|
+
)
|
|
1783
|
+
elif value <= 0:
|
|
1784
|
+
raise ConfigError(f"app_server.client.{key} must be > 0")
|
|
1522
1785
|
prompts = app_server_cfg.get("prompts")
|
|
1523
1786
|
if prompts is not None:
|
|
1524
1787
|
if not isinstance(prompts, dict):
|
|
@@ -1562,6 +1825,35 @@ def _validate_app_server_config(cfg: Dict[str, Any]) -> None:
|
|
|
1562
1825
|
)
|
|
1563
1826
|
|
|
1564
1827
|
|
|
1828
|
+
def _validate_opencode_config(cfg: Dict[str, Any]) -> None:
|
|
1829
|
+
opencode_cfg = cfg.get("opencode")
|
|
1830
|
+
if opencode_cfg is None:
|
|
1831
|
+
return
|
|
1832
|
+
if not isinstance(opencode_cfg, dict):
|
|
1833
|
+
raise ConfigError("opencode section must be a mapping if provided")
|
|
1834
|
+
if (
|
|
1835
|
+
"session_stall_timeout_seconds" in opencode_cfg
|
|
1836
|
+
and opencode_cfg.get("session_stall_timeout_seconds") is not None
|
|
1837
|
+
):
|
|
1838
|
+
if not isinstance(
|
|
1839
|
+
opencode_cfg.get("session_stall_timeout_seconds"), (int, float)
|
|
1840
|
+
):
|
|
1841
|
+
raise ConfigError(
|
|
1842
|
+
"opencode.session_stall_timeout_seconds must be a number or null"
|
|
1843
|
+
)
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def _validate_update_config(cfg: Dict[str, Any]) -> None:
|
|
1847
|
+
update_cfg = cfg.get("update")
|
|
1848
|
+
if update_cfg is None:
|
|
1849
|
+
return
|
|
1850
|
+
if not isinstance(update_cfg, dict):
|
|
1851
|
+
raise ConfigError("update section must be a mapping if provided")
|
|
1852
|
+
if "skip_checks" in update_cfg and update_cfg.get("skip_checks") is not None:
|
|
1853
|
+
if not isinstance(update_cfg.get("skip_checks"), bool):
|
|
1854
|
+
raise ConfigError("update.skip_checks must be boolean or null")
|
|
1855
|
+
|
|
1856
|
+
|
|
1565
1857
|
def _validate_agents_config(cfg: Dict[str, Any]) -> None:
|
|
1566
1858
|
agents_cfg = cfg.get("agents")
|
|
1567
1859
|
if agents_cfg is None:
|
|
@@ -1598,7 +1890,7 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1598
1890
|
)
|
|
1599
1891
|
except ConfigPathError as exc:
|
|
1600
1892
|
raise ConfigError(str(exc)) from exc
|
|
1601
|
-
for key in ("
|
|
1893
|
+
for key in ("active_context", "decisions", "spec"):
|
|
1602
1894
|
if not isinstance(docs.get(key), str) or not docs[key]:
|
|
1603
1895
|
raise ConfigError(f"docs.{key} must be a non-empty string path")
|
|
1604
1896
|
_validate_agents_config(cfg)
|
|
@@ -1651,6 +1943,18 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1651
1943
|
val = runner.get(k)
|
|
1652
1944
|
if val is not None and not isinstance(val, int):
|
|
1653
1945
|
raise ConfigError(f"runner.{k} must be an integer or null")
|
|
1946
|
+
ticket_flow_cfg = cfg.get("ticket_flow")
|
|
1947
|
+
if ticket_flow_cfg is not None and not isinstance(ticket_flow_cfg, dict):
|
|
1948
|
+
raise ConfigError("ticket_flow section must be a mapping if provided")
|
|
1949
|
+
if isinstance(ticket_flow_cfg, dict):
|
|
1950
|
+
if "approval_mode" in ticket_flow_cfg and not isinstance(
|
|
1951
|
+
ticket_flow_cfg.get("approval_mode"), str
|
|
1952
|
+
):
|
|
1953
|
+
raise ConfigError("ticket_flow.approval_mode must be a string")
|
|
1954
|
+
if "default_approval_decision" in ticket_flow_cfg and not isinstance(
|
|
1955
|
+
ticket_flow_cfg.get("default_approval_decision"), str
|
|
1956
|
+
):
|
|
1957
|
+
raise ConfigError("ticket_flow.default_approval_decision must be a string")
|
|
1654
1958
|
git = cfg.get("git")
|
|
1655
1959
|
if not isinstance(git, dict):
|
|
1656
1960
|
raise ConfigError("git section must be a mapping")
|
|
@@ -1674,69 +1978,7 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1674
1978
|
github.get("sync_agent_timeout_seconds"), int
|
|
1675
1979
|
):
|
|
1676
1980
|
raise ConfigError("github.sync_agent_timeout_seconds must be an integer")
|
|
1677
|
-
|
|
1678
|
-
if pr_flow is not None:
|
|
1679
|
-
if not isinstance(pr_flow, dict):
|
|
1680
|
-
raise ConfigError("github.pr_flow must be a mapping if provided")
|
|
1681
|
-
if "enabled" in pr_flow and not isinstance(pr_flow.get("enabled"), bool):
|
|
1682
|
-
raise ConfigError("github.pr_flow.enabled must be boolean")
|
|
1683
|
-
if "max_cycles" in pr_flow and not isinstance(
|
|
1684
|
-
pr_flow.get("max_cycles"), int
|
|
1685
|
-
):
|
|
1686
|
-
raise ConfigError("github.pr_flow.max_cycles must be an integer")
|
|
1687
|
-
if "stop_condition" in pr_flow and not isinstance(
|
|
1688
|
-
pr_flow.get("stop_condition"), str
|
|
1689
|
-
):
|
|
1690
|
-
raise ConfigError("github.pr_flow.stop_condition must be a string")
|
|
1691
|
-
for key in ("max_implementation_runs", "max_wallclock_seconds"):
|
|
1692
|
-
val = pr_flow.get(key)
|
|
1693
|
-
if val is not None and not isinstance(val, int):
|
|
1694
|
-
raise ConfigError(
|
|
1695
|
-
f"github.pr_flow.{key} must be an integer or null"
|
|
1696
|
-
)
|
|
1697
|
-
review_cfg = pr_flow.get("review")
|
|
1698
|
-
if review_cfg is not None:
|
|
1699
|
-
if not isinstance(review_cfg, dict):
|
|
1700
|
-
raise ConfigError("github.pr_flow.review must be a mapping")
|
|
1701
|
-
for key in ("include_codex", "include_github", "include_checks"):
|
|
1702
|
-
if key in review_cfg and not isinstance(review_cfg.get(key), bool):
|
|
1703
|
-
raise ConfigError(
|
|
1704
|
-
f"github.pr_flow.review.{key} must be boolean"
|
|
1705
|
-
)
|
|
1706
|
-
chatops_cfg = pr_flow.get("chatops")
|
|
1707
|
-
if chatops_cfg is not None:
|
|
1708
|
-
if not isinstance(chatops_cfg, dict):
|
|
1709
|
-
raise ConfigError("github.pr_flow.chatops must be a mapping")
|
|
1710
|
-
if "enabled" in chatops_cfg and not isinstance(
|
|
1711
|
-
chatops_cfg.get("enabled"), bool
|
|
1712
|
-
):
|
|
1713
|
-
raise ConfigError("github.pr_flow.chatops.enabled must be boolean")
|
|
1714
|
-
if "poll_interval_seconds" in chatops_cfg and not isinstance(
|
|
1715
|
-
chatops_cfg.get("poll_interval_seconds"), int
|
|
1716
|
-
):
|
|
1717
|
-
raise ConfigError(
|
|
1718
|
-
"github.pr_flow.chatops.poll_interval_seconds must be an integer"
|
|
1719
|
-
)
|
|
1720
|
-
for key in ("allow_users", "allow_associations"):
|
|
1721
|
-
if key in chatops_cfg and not isinstance(
|
|
1722
|
-
chatops_cfg.get(key), list
|
|
1723
|
-
):
|
|
1724
|
-
raise ConfigError(
|
|
1725
|
-
f"github.pr_flow.chatops.{key} must be a list"
|
|
1726
|
-
)
|
|
1727
|
-
if "ignore_bots" in chatops_cfg and not isinstance(
|
|
1728
|
-
chatops_cfg.get("ignore_bots"), bool
|
|
1729
|
-
):
|
|
1730
|
-
raise ConfigError(
|
|
1731
|
-
"github.pr_flow.chatops.ignore_bots must be boolean"
|
|
1732
|
-
)
|
|
1733
|
-
if chatops_cfg.get("enabled", False):
|
|
1734
|
-
allow_users = chatops_cfg.get("allow_users") or []
|
|
1735
|
-
allow_assoc = chatops_cfg.get("allow_associations") or []
|
|
1736
|
-
if not allow_users and not allow_assoc:
|
|
1737
|
-
raise ConfigError(
|
|
1738
|
-
"github.pr_flow.chatops.enabled requires at least one of allow_users or allow_associations to be non-empty"
|
|
1739
|
-
)
|
|
1981
|
+
|
|
1740
1982
|
server = cfg.get("server")
|
|
1741
1983
|
if not isinstance(server, dict):
|
|
1742
1984
|
raise ConfigError("server section must be a mapping")
|
|
@@ -1754,6 +1996,8 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1754
1996
|
raise ConfigError("server.auth_token_env must be a string if provided")
|
|
1755
1997
|
_validate_server_security(server)
|
|
1756
1998
|
_validate_app_server_config(cfg)
|
|
1999
|
+
_validate_opencode_config(cfg)
|
|
2000
|
+
_validate_update_config(cfg)
|
|
1757
2001
|
notifications_cfg = cfg.get("notifications")
|
|
1758
2002
|
if notifications_cfg is not None:
|
|
1759
2003
|
if not isinstance(notifications_cfg, dict):
|
|
@@ -1897,6 +2141,8 @@ def _validate_hub_config(cfg: Dict[str, Any]) -> None:
|
|
|
1897
2141
|
if "repo" in cfg:
|
|
1898
2142
|
raise ConfigError("repo section is no longer supported; use repo_defaults")
|
|
1899
2143
|
_validate_agents_config(cfg)
|
|
2144
|
+
_validate_opencode_config(cfg)
|
|
2145
|
+
_validate_update_config(cfg)
|
|
1900
2146
|
repo_defaults = cfg.get("repo_defaults")
|
|
1901
2147
|
if repo_defaults is not None:
|
|
1902
2148
|
if not isinstance(repo_defaults, dict):
|
|
@@ -2195,6 +2441,29 @@ def _validate_telegram_bot_config(cfg: Dict[str, Any]) -> None:
|
|
|
2195
2441
|
raise ConfigError(f"telegram_bot.shell.{key} must be an integer")
|
|
2196
2442
|
if isinstance(value, int) and value <= 0:
|
|
2197
2443
|
raise ConfigError(f"telegram_bot.shell.{key} must be greater than 0")
|
|
2444
|
+
cache_cfg = telegram_cfg.get("cache")
|
|
2445
|
+
if cache_cfg is not None and not isinstance(cache_cfg, dict):
|
|
2446
|
+
raise ConfigError("telegram_bot.cache must be a mapping if provided")
|
|
2447
|
+
if isinstance(cache_cfg, dict):
|
|
2448
|
+
for key in (
|
|
2449
|
+
"cleanup_interval_seconds",
|
|
2450
|
+
"coalesce_buffer_ttl_seconds",
|
|
2451
|
+
"media_batch_buffer_ttl_seconds",
|
|
2452
|
+
"model_pending_ttl_seconds",
|
|
2453
|
+
"pending_approval_ttl_seconds",
|
|
2454
|
+
"pending_question_ttl_seconds",
|
|
2455
|
+
"reasoning_buffer_ttl_seconds",
|
|
2456
|
+
"selection_state_ttl_seconds",
|
|
2457
|
+
"turn_preview_ttl_seconds",
|
|
2458
|
+
"progress_stream_ttl_seconds",
|
|
2459
|
+
"oversize_warning_ttl_seconds",
|
|
2460
|
+
"update_id_persist_interval_seconds",
|
|
2461
|
+
):
|
|
2462
|
+
value = cache_cfg.get(key)
|
|
2463
|
+
if value is not None and not isinstance(value, (int, float)):
|
|
2464
|
+
raise ConfigError(f"telegram_bot.cache.{key} must be a number")
|
|
2465
|
+
if isinstance(value, (int, float)) and value <= 0:
|
|
2466
|
+
raise ConfigError(f"telegram_bot.cache.{key} must be > 0")
|
|
2198
2467
|
command_reg_cfg = telegram_cfg.get("command_registration")
|
|
2199
2468
|
if command_reg_cfg is not None and not isinstance(command_reg_cfg, dict):
|
|
2200
2469
|
raise ConfigError("telegram_bot.command_registration must be a mapping")
|
|
@@ -2232,10 +2501,20 @@ def _validate_telegram_bot_config(cfg: Dict[str, Any]) -> None:
|
|
|
2232
2501
|
raise ConfigError(
|
|
2233
2502
|
"telegram_bot.command_registration.scopes.language_code must be a string or null"
|
|
2234
2503
|
)
|
|
2504
|
+
if "trigger_mode" in telegram_cfg and not isinstance(
|
|
2505
|
+
telegram_cfg.get("trigger_mode"), str
|
|
2506
|
+
):
|
|
2507
|
+
raise ConfigError("telegram_bot.trigger_mode must be a string")
|
|
2235
2508
|
if "state_file" in telegram_cfg and not isinstance(
|
|
2236
2509
|
telegram_cfg.get("state_file"), str
|
|
2237
2510
|
):
|
|
2238
2511
|
raise ConfigError("telegram_bot.state_file must be a string path")
|
|
2512
|
+
if (
|
|
2513
|
+
"opencode_command" in telegram_cfg
|
|
2514
|
+
and not isinstance(telegram_cfg.get("opencode_command"), (list, str))
|
|
2515
|
+
and telegram_cfg.get("opencode_command") is not None
|
|
2516
|
+
):
|
|
2517
|
+
raise ConfigError("telegram_bot.opencode_command must be a list or string")
|
|
2239
2518
|
if "app_server_command" in telegram_cfg and not isinstance(
|
|
2240
2519
|
telegram_cfg.get("app_server_command"), (list, str)
|
|
2241
2520
|
):
|
|
@@ -2252,6 +2531,17 @@ def _validate_telegram_bot_config(cfg: Dict[str, Any]) -> None:
|
|
|
2252
2531
|
raise ConfigError(
|
|
2253
2532
|
"telegram_bot.app_server.turn_timeout_seconds must be a number or null"
|
|
2254
2533
|
)
|
|
2534
|
+
agent_timeouts_cfg = telegram_cfg.get("agent_timeouts")
|
|
2535
|
+
if agent_timeouts_cfg is not None and not isinstance(agent_timeouts_cfg, dict):
|
|
2536
|
+
raise ConfigError("telegram_bot.agent_timeouts must be a mapping if provided")
|
|
2537
|
+
if isinstance(agent_timeouts_cfg, dict):
|
|
2538
|
+
for _key, value in agent_timeouts_cfg.items():
|
|
2539
|
+
if value is None:
|
|
2540
|
+
continue
|
|
2541
|
+
if not isinstance(value, (int, float)):
|
|
2542
|
+
raise ConfigError(
|
|
2543
|
+
"telegram_bot.agent_timeouts values must be numbers or null"
|
|
2544
|
+
)
|
|
2255
2545
|
polling_cfg = telegram_cfg.get("polling")
|
|
2256
2546
|
if polling_cfg is not None and not isinstance(polling_cfg, dict):
|
|
2257
2547
|
raise ConfigError("telegram_bot.polling must be a mapping if provided")
|